mirror of
https://github.com/craigerl/aprsd.git
synced 2024-10-31 15:07:13 -04:00
Created plugin.py for Command Plugins
This patch adds the new APRSD Command Plugin architecture. All Comand plugins must implement the same object API, which includes plugin object is subclass of APRSDPluginBase version attribute command_regex attribute command method When an APRS command is detected, then the regex is run against the command. If the command_regex matches, then the plugin's command() method will be called. If the command() method returns a string, then that string is sent as a reply to the APRS caller. A new aprs.yml config section is added to support selecting which plugins to enable. If you want all plugins enabled, then omit "enabled_plugins" entirely from the aprs section of the config. To load custom plugins: 1) create a directory with an __init__.py file 2) Add a plugin.py file that contains your plugin Look at the exmaples directory for an example plugin.
This commit is contained in:
parent
8c9c12b3fc
commit
d09a66006b
163
aprsd/main.py
163
aprsd/main.py
@ -24,7 +24,6 @@
|
||||
import datetime
|
||||
import email
|
||||
import imaplib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pprint
|
||||
@ -33,7 +32,6 @@ import select
|
||||
import signal
|
||||
import smtplib
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
@ -43,14 +41,12 @@ from logging.handlers import RotatingFileHandler
|
||||
import click
|
||||
import click_completion
|
||||
import imapclient
|
||||
import requests
|
||||
import six
|
||||
import yaml
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import utils
|
||||
from aprsd.fuzzyclock import fuzzy
|
||||
from aprsd import plugin, utils
|
||||
|
||||
# setup the global logger
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@ -703,11 +699,6 @@ def sample_config():
|
||||
|
||||
COMMAND_ENVELOPE = {
|
||||
"email": {"command": "^-.*", "function": "command_email"},
|
||||
"fortune": {"command": "^[fF]", "function": "command_fortune"},
|
||||
"location": {"command": "^[lL]", "function": "command_location"},
|
||||
"weather": {"command": "^[wW]", "function": "command_weather"},
|
||||
"ping": {"command": "^[pP]", "function": "command_ping"},
|
||||
"time": {"command": "^[tT]", "function": "command_time"},
|
||||
}
|
||||
|
||||
|
||||
@ -768,149 +759,6 @@ def command_email(fromcall, message, ack):
|
||||
return (fromcall, message, ack)
|
||||
|
||||
|
||||
def command_fortune(fromcall, message, ack):
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
["/usr/games/fortune", "-s", "-n 60"], stdout=subprocess.PIPE
|
||||
)
|
||||
reply = process.communicate()[0]
|
||||
# send_message(fromcall, reply.rstrip())
|
||||
reply = reply.decode(errors="ignore").rstrip()
|
||||
|
||||
except Exception as ex:
|
||||
reply = "Fortune command failed '{}'".format(ex)
|
||||
LOG.error(reply)
|
||||
|
||||
send_message(fromcall, reply)
|
||||
return (fromcall, message, ack)
|
||||
|
||||
|
||||
def command_location(fromcall, message, ack):
|
||||
LOG.info("Location COMMAND")
|
||||
# get last location of a callsign, get descriptive name from weather service
|
||||
try:
|
||||
a = re.search(
|
||||
r"^.*\s+(.*)", message
|
||||
) # optional second argument is a callsign to search
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
searchcall = searchcall.upper()
|
||||
else:
|
||||
searchcall = fromcall # if no second argument, search for calling station
|
||||
url = (
|
||||
"http://api.aprs.fi/api/get?name="
|
||||
+ searchcall
|
||||
+ "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
||||
)
|
||||
response = requests.get(url)
|
||||
# aprs_data = json.loads(response.read())
|
||||
aprs_data = json.loads(response.text)
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
try: # altitude not always provided
|
||||
alt = aprs_data["entries"][0]["altitude"]
|
||||
except Exception:
|
||||
alt = 0
|
||||
altfeet = int(alt * 3.28084)
|
||||
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
|
||||
aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
||||
"ascii", errors="ignore"
|
||||
) # 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)
|
||||
|
||||
reply = "{}: {} {}' {},{} {}h ago".format(
|
||||
searchcall,
|
||||
wx_data["location"]["areaDescription"],
|
||||
str(altfeet),
|
||||
str(alt),
|
||||
str(lon),
|
||||
str("%.1f" % round(delta_hours, 1)),
|
||||
)
|
||||
# reply = reply.encode('ascii', errors='ignore') # unicode to ascii
|
||||
send_message(fromcall, reply.rstrip())
|
||||
except Exception as e:
|
||||
LOG.debug("Locate failed with: " + "%s" % str(e))
|
||||
reply = "Unable to find station " + searchcall + ". Sending beacons?"
|
||||
send_message(fromcall, reply.rstrip())
|
||||
|
||||
return (fromcall, message, ack)
|
||||
|
||||
|
||||
def command_weather(fromcall, message, ack):
|
||||
"""Do weather command and send response."""
|
||||
LOG.info("WEATHER COMMAND")
|
||||
try:
|
||||
url = (
|
||||
"http://api.aprs.fi/api/get?"
|
||||
"&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
||||
"&name=%s" % fromcall
|
||||
)
|
||||
response = requests.get(url)
|
||||
# aprs_data = json.loads(response.read())
|
||||
aprs_data = json.loads(response.text)
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
url2 = (
|
||||
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
||||
"&lon=%s&FcstType=json" % (lat, lon)
|
||||
)
|
||||
response2 = requests.get(url2)
|
||||
# wx_data = json.loads(response2.read())
|
||||
wx_data = json.loads(response2.text)
|
||||
reply = "%sF(%sF/%sF) %s. %s, %s." % (
|
||||
wx_data["currentobservation"]["Temp"],
|
||||
wx_data["data"]["temperature"][0],
|
||||
wx_data["data"]["temperature"][1],
|
||||
wx_data["data"]["weather"][0],
|
||||
wx_data["time"]["startPeriodName"][1],
|
||||
wx_data["data"]["weather"][1],
|
||||
)
|
||||
LOG.debug("reply: " + reply.rstrip())
|
||||
send_message(fromcall, reply.rstrip())
|
||||
except Exception as e:
|
||||
LOG.debug("Weather failed with: " + "%s" % str(e))
|
||||
reply = "Unable to find you (send beacon?)"
|
||||
|
||||
return (fromcall, message, ack)
|
||||
|
||||
|
||||
def command_ping(fromcall, message, ack):
|
||||
LOG.info("PING COMMAND")
|
||||
stm = time.localtime()
|
||||
h = stm.tm_hour
|
||||
m = stm.tm_min
|
||||
s = stm.tm_sec
|
||||
reply = "Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
|
||||
send_message(fromcall, reply.rstrip())
|
||||
return (fromcall, message, ack)
|
||||
|
||||
|
||||
def command_time(fromcall, message, ack):
|
||||
LOG.info("TIME COMMAND")
|
||||
stm = time.localtime()
|
||||
h = stm.tm_hour
|
||||
m = stm.tm_min
|
||||
cur_time = fuzzy(h, m, 1)
|
||||
reply = "{} ({}:{} PDT) ({})".format(
|
||||
cur_time, str(h), str(m).rjust(2, "0"), message.rstrip()
|
||||
)
|
||||
thread = threading.Thread(
|
||||
target=send_message, name="send_message", args=(fromcall, reply)
|
||||
)
|
||||
thread.start()
|
||||
return (fromcall, message, ack)
|
||||
|
||||
|
||||
# main() ###
|
||||
@main.command()
|
||||
@click.option(
|
||||
@ -964,6 +812,9 @@ def server(loglevel, quiet, config_file):
|
||||
|
||||
read_sockets = [client_sock]
|
||||
|
||||
# Register plugins
|
||||
pm = plugin.setup_plugins(CONFIG)
|
||||
|
||||
fromcall = message = ack = None
|
||||
while True:
|
||||
LOG.debug("Main loop start")
|
||||
@ -1038,6 +889,12 @@ def server(loglevel, quiet, config_file):
|
||||
ack_dict.update({int(a.group(1)): 1})
|
||||
continue # break out of this so we don't ack an ack at the end
|
||||
|
||||
# call our `myhook` hook
|
||||
results = pm.hook.run(fromcall=fromcall, message=message, ack=ack)
|
||||
LOG.info("PLUGINS returned {}".format(results))
|
||||
for reply in results:
|
||||
send_message(fromcall, reply)
|
||||
|
||||
# it's not an ack, so try and process user input
|
||||
found_command = False
|
||||
for key in COMMAND_ENVELOPE:
|
||||
|
331
aprsd/plugin.py
Normal file
331
aprsd/plugin.py
Normal file
@ -0,0 +1,331 @@
|
||||
# The base plugin class
|
||||
import abc
|
||||
import fnmatch
|
||||
import imp
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import pluggy
|
||||
import requests
|
||||
import six
|
||||
|
||||
from aprsd.fuzzyclock import fuzzy
|
||||
|
||||
# setup the global logger
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
hookspec = pluggy.HookspecMarker("aprsd")
|
||||
hookimpl = pluggy.HookimplMarker("aprsd")
|
||||
|
||||
CORE_PLUGINS = [
|
||||
"FortunePlugin",
|
||||
"LocationPlugin",
|
||||
"PingPlugin",
|
||||
"TimePlugin",
|
||||
"WeatherPlugin",
|
||||
]
|
||||
|
||||
|
||||
def setup_plugins(config):
|
||||
"""Create the plugin manager and register plugins."""
|
||||
|
||||
LOG.info("Loading Core APRSD Command Plugins")
|
||||
enabled_plugins = config["aprsd"].get("enabled_plugins", None)
|
||||
pm = pluggy.PluginManager("aprsd")
|
||||
pm.add_hookspecs(APRSDCommandSpec)
|
||||
for p_name in CORE_PLUGINS:
|
||||
plugin_obj = None
|
||||
if enabled_plugins:
|
||||
if p_name in enabled_plugins:
|
||||
plugin_obj = globals()[p_name]()
|
||||
else:
|
||||
# Enabled plugins isn't set, so we default to loading all of
|
||||
# the core plugins.
|
||||
plugin_obj = globals()[p_name]()
|
||||
|
||||
if plugin_obj:
|
||||
LOG.info(
|
||||
"Registering Command plugin '{}'({}) '{}'".format(
|
||||
p_name, plugin_obj.version, plugin_obj.command_regex
|
||||
)
|
||||
)
|
||||
pm.register(plugin_obj)
|
||||
|
||||
plugin_dir = config["aprsd"].get("plugin_dir", None)
|
||||
if plugin_dir:
|
||||
LOG.info("Trying to load custom plugins from '{}'".format(plugin_dir))
|
||||
cpm = PluginManager()
|
||||
plugins_list = cpm.load_plugins(plugin_dir)
|
||||
LOG.info("Discovered {} modules to load".format(len(plugins_list)))
|
||||
for o in plugins_list:
|
||||
plugin_obj = None
|
||||
if enabled_plugins:
|
||||
if o["name"] in enabled_plugins:
|
||||
plugin_obj = o["obj"]
|
||||
else:
|
||||
LOG.info(
|
||||
"'{}' plugin not listed in config aprsd:enabled_plugins".format(
|
||||
o["name"]
|
||||
)
|
||||
)
|
||||
else:
|
||||
# not setting enabled plugins means load all?
|
||||
plugin_obj = o["obj"]
|
||||
|
||||
if plugin_obj:
|
||||
LOG.info(
|
||||
"Registering Command plugin '{}'({}) '{}'".format(
|
||||
o["name"], o["obj"].version, o["obj"].command_regex
|
||||
)
|
||||
)
|
||||
pm.register(o["obj"])
|
||||
|
||||
else:
|
||||
LOG.info("Skipping Custom Plugins.")
|
||||
|
||||
LOG.info("Completed Plugin Loading.")
|
||||
return pm
|
||||
|
||||
|
||||
class PluginManager(object):
|
||||
def __init__(self):
|
||||
self.obj_list = []
|
||||
|
||||
def load_plugins(self, module_path):
|
||||
dir_path = os.path.dirname(os.path.realpath(module_path))
|
||||
pattern = "*.py"
|
||||
|
||||
self.obj_list = []
|
||||
|
||||
for path, subdirs, files in os.walk(dir_path):
|
||||
for name in files:
|
||||
if fnmatch.fnmatch(name, pattern):
|
||||
found_module = imp.find_module(name[:-3], [path])
|
||||
module = imp.load_module(
|
||||
name, found_module[0], found_module[1], found_module[2]
|
||||
)
|
||||
for mem_name, obj in inspect.getmembers(module):
|
||||
if (
|
||||
inspect.isclass(obj)
|
||||
and inspect.getmodule(obj) is module
|
||||
and self.is_plugin(obj)
|
||||
):
|
||||
self.obj_list.append({"name": mem_name, "obj": obj()})
|
||||
|
||||
return self.obj_list
|
||||
|
||||
def is_plugin(self, obj):
|
||||
for c in inspect.getmro(obj):
|
||||
if issubclass(c, APRSDPluginBase):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class APRSDCommandSpec:
|
||||
"""A hook specification namespace."""
|
||||
|
||||
@hookspec
|
||||
def run(self, fromcall, message, ack):
|
||||
"""My special little hook that you can customize."""
|
||||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class APRSDPluginBase(object):
|
||||
@property
|
||||
def command_regex(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@hookimpl
|
||||
def run(self, fromcall, message, ack):
|
||||
if re.search(self.command_regex, message):
|
||||
return self.command(fromcall, message, ack)
|
||||
|
||||
@abc.abstractmethod
|
||||
def command(self, fromcall, message, ack):
|
||||
"""This is the command that runs when the regex matches.
|
||||
|
||||
To reply with a message over the air, return a string
|
||||
to send.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FortunePlugin(APRSDPluginBase):
|
||||
"""Fortune."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[fF]"
|
||||
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("FortunePlugin")
|
||||
reply = None
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
["/usr/games/fortune", "-s", "-n 60"], stdout=subprocess.PIPE
|
||||
)
|
||||
reply = process.communicate()[0]
|
||||
# send_message(fromcall, reply.rstrip())
|
||||
reply = reply.decode(errors="ignore").rstrip()
|
||||
except Exception as ex:
|
||||
reply = "Fortune command failed '{}'".format(ex)
|
||||
LOG.error(reply)
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
class LocationPlugin(APRSDPluginBase):
|
||||
"""Location!"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[lL]"
|
||||
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Location Plugin")
|
||||
# get last location of a callsign, get descriptive name from weather service
|
||||
try:
|
||||
a = re.search(
|
||||
r"^.*\s+(.*)", message
|
||||
) # optional second argument is a callsign to search
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
searchcall = searchcall.upper()
|
||||
else:
|
||||
searchcall = (
|
||||
fromcall # if no second argument, search for calling station
|
||||
)
|
||||
url = (
|
||||
"http://api.aprs.fi/api/get?name="
|
||||
+ searchcall
|
||||
+ "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
||||
)
|
||||
response = requests.get(url)
|
||||
# aprs_data = json.loads(response.read())
|
||||
aprs_data = json.loads(response.text)
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
try: # altitude not always provided
|
||||
alt = aprs_data["entries"][0]["altitude"]
|
||||
except Exception:
|
||||
alt = 0
|
||||
altfeet = int(alt * 3.28084)
|
||||
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
|
||||
aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
||||
"ascii", errors="ignore"
|
||||
) # 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)
|
||||
|
||||
reply = "{}: {} {}' {},{} {}h ago".format(
|
||||
searchcall,
|
||||
wx_data["location"]["areaDescription"],
|
||||
str(altfeet),
|
||||
str(alt),
|
||||
str(lon),
|
||||
str("%.1f" % round(delta_hours, 1)),
|
||||
).rstrip()
|
||||
except Exception as e:
|
||||
LOG.debug("Locate failed with: " + "%s" % str(e))
|
||||
reply = "Unable to find station " + searchcall + ". Sending beacons?"
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
class PingPlugin(APRSDPluginBase):
|
||||
"""Ping."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[pP]"
|
||||
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("PINGPlugin")
|
||||
stm = time.localtime()
|
||||
h = stm.tm_hour
|
||||
m = stm.tm_min
|
||||
s = stm.tm_sec
|
||||
reply = (
|
||||
"Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
|
||||
)
|
||||
return reply.rstrip()
|
||||
|
||||
|
||||
class TimePlugin(APRSDPluginBase):
|
||||
"""Time command."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[tT]"
|
||||
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("TIME COMMAND")
|
||||
stm = time.localtime()
|
||||
h = stm.tm_hour
|
||||
m = stm.tm_min
|
||||
cur_time = fuzzy(h, m, 1)
|
||||
reply = "{} ({}:{} PDT) ({})".format(
|
||||
cur_time, str(h), str(m).rjust(2, "0"), message.rstrip()
|
||||
)
|
||||
return reply
|
||||
|
||||
|
||||
class WeatherPlugin(APRSDPluginBase):
|
||||
"""Weather Command"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[wW]"
|
||||
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Weather Plugin")
|
||||
try:
|
||||
url = (
|
||||
"http://api.aprs.fi/api/get?"
|
||||
"&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
||||
"&name=%s" % fromcall
|
||||
)
|
||||
response = requests.get(url)
|
||||
# aprs_data = json.loads(response.read())
|
||||
aprs_data = json.loads(response.text)
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
url2 = (
|
||||
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
||||
"&lon=%s&FcstType=json" % (lat, lon)
|
||||
)
|
||||
response2 = requests.get(url2)
|
||||
# wx_data = json.loads(response2.read())
|
||||
wx_data = json.loads(response2.text)
|
||||
reply = (
|
||||
"%sF(%sF/%sF) %s. %s, %s."
|
||||
% (
|
||||
wx_data["currentobservation"]["Temp"],
|
||||
wx_data["data"]["temperature"][0],
|
||||
wx_data["data"]["temperature"][1],
|
||||
wx_data["data"]["weather"][0],
|
||||
wx_data["time"]["startPeriodName"][1],
|
||||
wx_data["data"]["weather"][1],
|
||||
).rstrip()
|
||||
)
|
||||
LOG.debug("reply: '{}' ".format(reply))
|
||||
except Exception as e:
|
||||
LOG.debug("Weather failed with: " + "%s" % str(e))
|
||||
reply = "Unable to find you (send beacon?)"
|
||||
|
||||
return reply
|
@ -7,6 +7,8 @@ import sys
|
||||
import click
|
||||
import yaml
|
||||
|
||||
from aprsd import plugin
|
||||
|
||||
# an example of what should be in the ~/.aprsd/config.yml
|
||||
DEFAULT_CONFIG_DICT = {
|
||||
"ham": {"callsign": "KFART"},
|
||||
@ -36,6 +38,10 @@ DEFAULT_CONFIG_DICT = {
|
||||
"port": 993,
|
||||
"use_ssl": True,
|
||||
},
|
||||
"aprsd": {
|
||||
"plugin_dir": "~/.config/aprsd/plugins",
|
||||
"enabled_plugins": plugin.CORE_PLUGINS,
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_CONFIG_FILE = "~/.config/aprsd/aprsd.yml"
|
||||
|
@ -1,7 +1,7 @@
|
||||
version: "3"
|
||||
services:
|
||||
aprsd:
|
||||
image: hemna6969/aprsd:latest
|
||||
image: hemna6969/aprsd:latest
|
||||
container_name: aprsd
|
||||
volumes:
|
||||
- /opt/docker/aprsd/config:/config
|
||||
|
0
examples/plugins/__init__.py
Normal file
0
examples/plugins/__init__.py
Normal file
18
examples/plugins/example_plugin.py
Normal file
18
examples/plugins/example_plugin.py
Normal file
@ -0,0 +1,18 @@
|
||||
import logging
|
||||
|
||||
from aprsd import plugin
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class HelloPlugin(plugin.APRSDPluginBase):
|
||||
"""Hello World."""
|
||||
|
||||
version = "1.0"
|
||||
# matches any string starting with h or H
|
||||
command_regex = "^[hH]"
|
||||
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("HelloPlugin")
|
||||
reply = "Hello '{}'".format(fromcall)
|
||||
return reply
|
@ -1,6 +1,7 @@
|
||||
click
|
||||
click-completion
|
||||
imapclient
|
||||
pluggy
|
||||
pbr
|
||||
pyyaml
|
||||
six
|
||||
|
2
tox.ini
2
tox.ini
@ -21,7 +21,7 @@ commands =
|
||||
# Use -Werror to treat warnings as errors.
|
||||
# {envpython} -bb -Werror -m pytest \
|
||||
# --cov="{envsitepackagesdir}/aprsd" --cov-report=html --cov-report=term {posargs}
|
||||
{envpython} -bb -Werror -m pytest {posargs}
|
||||
{envpython} -bb -m pytest {posargs}
|
||||
|
||||
[testenv:py27]
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
|
Loading…
Reference in New Issue
Block a user