mirror of
https://github.com/craigerl/aprsd.git
synced 2025-07-04 01:45:17 -04: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
|
# Setup Timezone
|
||||||
ENV TZ=US/Eastern
|
ENV TZ=US/Eastern
|
||||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
#RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
RUN apt-get install -y tzdata
|
#RUN apt-get install -y tzdata
|
||||||
RUN dpkg-reconfigure --frontend noninteractive tzdata
|
#RUN dpkg-reconfigure --frontend noninteractive tzdata
|
||||||
|
|
||||||
|
|
||||||
RUN addgroup --gid 1000 $APRS_USER
|
RUN addgroup --gid 1000 $APRS_USER
|
||||||
|
@ -2,11 +2,11 @@ FROM alpine:latest as aprsd
|
|||||||
|
|
||||||
# Dockerfile for building a container during aprsd development.
|
# Dockerfile for building a container during aprsd development.
|
||||||
|
|
||||||
ENV VERSION=1.0.0
|
ENV VERSION=1.5.1
|
||||||
ENV APRS_USER=aprs
|
ENV APRS_USER=aprs
|
||||||
ENV HOME=/home/aprs
|
ENV HOME=/home/aprs
|
||||||
ENV APRSD=http://github.com/craigerl/aprsd.git
|
ENV APRSD=http://github.com/craigerl/aprsd.git
|
||||||
ENV APRSD_BRANCH="v1.1.0"
|
ENV APRSD_BRANCH="master"
|
||||||
ENV VIRTUAL_ENV=$HOME/.venv3
|
ENV VIRTUAL_ENV=$HOME/.venv3
|
||||||
|
|
||||||
ENV INSTALL=$HOME/install
|
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 logging
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprsd import plugin
|
from aprsd import plugin, plugin_utils, utils
|
||||||
import requests
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -16,11 +14,15 @@ class LocationPlugin(plugin.APRSDPluginBase):
|
|||||||
command_regex = "^[lL]"
|
command_regex = "^[lL]"
|
||||||
command_name = "location"
|
command_name = "location"
|
||||||
|
|
||||||
config_items = {"apikey": "aprs.fi api key here"}
|
|
||||||
|
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("Location Plugin")
|
LOG.info("Location Plugin")
|
||||||
# get last location of a callsign, get descriptive name from weather service
|
# 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"]
|
api_key = self.config["aprs.fi"]["apiKey"]
|
||||||
try:
|
try:
|
||||||
# optional second argument is a callsign to search
|
# optional second argument is a callsign to search
|
||||||
@ -31,14 +33,13 @@ class LocationPlugin(plugin.APRSDPluginBase):
|
|||||||
else:
|
else:
|
||||||
# if no second argument, search for calling station
|
# if no second argument, search for calling station
|
||||||
searchcall = fromcall
|
searchcall = fromcall
|
||||||
url = (
|
|
||||||
"http://api.aprs.fi/api/get?name="
|
try:
|
||||||
+ searchcall
|
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
||||||
+ "&what=loc&apikey={}&format=json".format(api_key)
|
except Exception as ex:
|
||||||
)
|
LOG.error("Failed to fetch aprs.fi '{}'".format(ex))
|
||||||
response = requests.get(url)
|
return "Failed to fetch aprs.fi location"
|
||||||
# aprs_data = json.loads(response.read())
|
|
||||||
aprs_data = json.loads(response.text)
|
|
||||||
LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||||
lat = aprs_data["entries"][0]["lat"]
|
lat = aprs_data["entries"][0]["lat"]
|
||||||
lon = aprs_data["entries"][0]["lng"]
|
lon = aprs_data["entries"][0]["lng"]
|
||||||
@ -53,15 +54,12 @@ class LocationPlugin(plugin.APRSDPluginBase):
|
|||||||
# ) # unicode to ascii
|
# ) # unicode to ascii
|
||||||
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
||||||
delta_hours = delta_seconds / 60 / 60
|
delta_hours = delta_seconds / 60 / 60
|
||||||
url2 = (
|
|
||||||
"https://forecast.weather.gov/MapClick.php?lat="
|
try:
|
||||||
+ str(lat)
|
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
||||||
+ "&lon="
|
except Exception as ex:
|
||||||
+ str(lon)
|
LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(ex))
|
||||||
+ "&FcstType=json"
|
wx_data["location"]["areaDescription"] = "Unkown Location"
|
||||||
)
|
|
||||||
response2 = requests.get(url2)
|
|
||||||
wx_data = json.loads(response2.text)
|
|
||||||
|
|
||||||
reply = "{}: {} {}' {},{} {}h ago".format(
|
reply = "{}: {} {}' {},{} {}h ago".format(
|
||||||
searchcall,
|
searchcall,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprsd import fuzzyclock, plugin
|
from aprsd import fuzzyclock, plugin, plugin_utils, utils
|
||||||
|
from opencage.geocoder import OpenCageGeocode
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -20,15 +21,7 @@ class TimePlugin(plugin.APRSDPluginBase):
|
|||||||
def _get_utcnow(self):
|
def _get_utcnow(self):
|
||||||
return pytz.datetime.datetime.utcnow()
|
return pytz.datetime.datetime.utcnow()
|
||||||
|
|
||||||
def command(self, fromcall, message, ack):
|
def build_date_str(self, localzone):
|
||||||
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
|
|
||||||
utcnow = self._get_utcnow()
|
utcnow = self._get_utcnow()
|
||||||
gmt_t = pytz.utc.localize(utcnow)
|
gmt_t = pytz.utc.localize(utcnow)
|
||||||
local_t = gmt_t.astimezone(localzone)
|
local_t = gmt_t.astimezone(localzone)
|
||||||
@ -38,10 +31,94 @@ class TimePlugin(plugin.APRSDPluginBase):
|
|||||||
local_min = local_t.strftime("%M")
|
local_min = local_t.strftime("%M")
|
||||||
cur_time = fuzzyclock.fuzzy(int(local_hour), int(local_min), 1)
|
cur_time = fuzzyclock.fuzzy(int(local_hour), int(local_min), 1)
|
||||||
|
|
||||||
reply = "{} ({}) ({})".format(
|
reply = "{} ({})".format(
|
||||||
cur_time,
|
cur_time,
|
||||||
local_short_str,
|
local_short_str,
|
||||||
message.rstrip(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return reply
|
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",
|
"logfile": "/tmp/aprsd.log",
|
||||||
},
|
},
|
||||||
"aprs.fi": {"apiKey": "set me"},
|
"aprs.fi": {"apiKey": "set me"},
|
||||||
|
"openweathermap": {"apiKey": "set me"},
|
||||||
|
"opencagedata": {"apiKey": "set me"},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"aa": "5551239999@vtext.com",
|
"aa": "5551239999@vtext.com",
|
||||||
"cl": "craiglamparter@somedomain.org",
|
"cl": "craiglamparter@somedomain.org",
|
||||||
@ -111,6 +113,24 @@ def add_config_comments(raw_yaml):
|
|||||||
end_idx,
|
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
|
return raw_yaml
|
||||||
|
|
||||||
|
|
||||||
@ -154,6 +174,35 @@ def get_config(config_file):
|
|||||||
sys.exit(-1)
|
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
|
# This method tries to parse the config yaml file
|
||||||
# and consume the settings.
|
# and consume the settings.
|
||||||
# If the required params don't exist,
|
# If the required params don't exist,
|
||||||
@ -167,30 +216,12 @@ def parse_config(config_file):
|
|||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
def check_option(config, section, name=None, default=None, default_fail=None):
|
def check_option(config, section, name=None, default=None, default_fail=None):
|
||||||
if section in config:
|
try:
|
||||||
|
config = check_config_option(config, section, name, default, default_fail)
|
||||||
if name and name not in config[section]:
|
except Exception as ex:
|
||||||
if not default:
|
fail(repr(ex))
|
||||||
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.")
|
|
||||||
else:
|
else:
|
||||||
fail("'%s' section wasn't in config file" % section)
|
return config
|
||||||
return config
|
|
||||||
|
|
||||||
config = get_config(config_file)
|
config = get_config(config_file)
|
||||||
check_option(config, "shortcuts")
|
check_option(config, "shortcuts")
|
||||||
|
@ -23,4 +23,4 @@ if [ ! -e "$APRSD_CONFIG" ]; then
|
|||||||
echo "'$APRSD_CONFIG' File does not exist. Creating."
|
echo "'$APRSD_CONFIG' File does not exist. Creating."
|
||||||
aprsd sample-config > $APRSD_CONFIG
|
aprsd sample-config > $APRSD_CONFIG
|
||||||
fi
|
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
|
py3-validate-email
|
||||||
pre-commit
|
pre-commit
|
||||||
pytz
|
pytz
|
||||||
|
opencage
|
||||||
|
@ -2,14 +2,18 @@
|
|||||||
# This file is autogenerated by pip-compile
|
# This file is autogenerated by pip-compile
|
||||||
# To update, run:
|
# To update, run:
|
||||||
#
|
#
|
||||||
# pip-compile requirements.in
|
# pip-compile
|
||||||
#
|
#
|
||||||
appdirs==1.4.4
|
appdirs==1.4.4
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
aprslib==0.6.47
|
aprslib==0.6.47
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
backoff==1.10.0
|
||||||
|
# via opencage
|
||||||
certifi==2020.12.5
|
certifi==2020.12.5
|
||||||
# via requests
|
# via requests
|
||||||
|
cffi==1.14.4
|
||||||
|
# via cryptography
|
||||||
cfgv==3.2.0
|
cfgv==3.2.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
chardet==4.0.0
|
chardet==4.0.0
|
||||||
@ -20,6 +24,8 @@ click==7.1.2
|
|||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# click-completion
|
# click-completion
|
||||||
|
cryptography==3.3.1
|
||||||
|
# via pyopenssl
|
||||||
distlib==0.3.1
|
distlib==0.3.1
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
dnspython==2.1.0
|
dnspython==2.1.0
|
||||||
@ -42,6 +48,8 @@ markupsafe==1.1.1
|
|||||||
# via jinja2
|
# via jinja2
|
||||||
nodeenv==1.5.0
|
nodeenv==1.5.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
|
opencage==1.2.2
|
||||||
|
# via -r requirements.in
|
||||||
pbr==5.5.1
|
pbr==5.5.1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
pluggy==0.13.1
|
pluggy==0.13.1
|
||||||
@ -50,6 +58,10 @@ pre-commit==2.9.3
|
|||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
py3-validate-email==0.2.12
|
py3-validate-email==0.2.12
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
pycparser==2.20
|
||||||
|
# via cffi
|
||||||
|
pyopenssl==20.0.1
|
||||||
|
# via opencage
|
||||||
pytz==2020.5
|
pytz==2020.5
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
pyyaml==5.3.1
|
pyyaml==5.3.1
|
||||||
@ -57,14 +69,19 @@ pyyaml==5.3.1
|
|||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# pre-commit
|
# pre-commit
|
||||||
requests==2.25.1
|
requests==2.25.1
|
||||||
# via -r requirements.in
|
# via
|
||||||
|
# -r requirements.in
|
||||||
|
# opencage
|
||||||
shellingham==1.3.2
|
shellingham==1.3.2
|
||||||
# via click-completion
|
# via click-completion
|
||||||
six==1.15.0
|
six==1.15.0
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# click-completion
|
# click-completion
|
||||||
|
# cryptography
|
||||||
# imapclient
|
# imapclient
|
||||||
|
# opencage
|
||||||
|
# pyopenssl
|
||||||
# virtualenv
|
# virtualenv
|
||||||
thesmuggler==1.0.1
|
thesmuggler==1.0.1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
@ -97,10 +97,9 @@ class TestPlugin(unittest.TestCase):
|
|||||||
|
|
||||||
message = "time"
|
message = "time"
|
||||||
local_short_str = local_t.strftime("%H:%M %Z")
|
local_short_str = local_t.strftime("%H:%M %Z")
|
||||||
expected = "{} ({}) ({})".format(
|
expected = "{} ({})".format(
|
||||||
cur_time,
|
cur_time,
|
||||||
local_short_str,
|
local_short_str,
|
||||||
message.rstrip(),
|
|
||||||
)
|
)
|
||||||
actual = time.run(fromcall, message, ack)
|
actual = time.run(fromcall, message, ack)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user