Merge where I left backend-frameworkification #2

Manually merged
bss merged 18 commits from backend-frameworkification into master 2021-04-24 15:36:23 -05:00
31 changed files with 671 additions and 192 deletions

2
.gitignore vendored
View File

@ -3,6 +3,8 @@ build/
dist/ dist/
tags/ tags/
*.egg-info/ *.egg-info/
.tox/
.coverage
dr.botzo.data dr.botzo.data
dr.botzo.cfg dr.botzo.cfg
localsettings.py localsettings.py

View File

@ -1,20 +0,0 @@
doc-warnings: true
strictness: high
ignore-paths:
- migrations
ignore-patterns:
- \.log$
- localsettings.py$
- parsetab.py$
pylint:
enable:
- relative-import
options:
max-line-length: 120
good-names: log
pep8:
options:
max-line-length: 120
pep257:
disable:
- D203

View File

@ -22,7 +22,8 @@ class Countdown(Plugin):
new_reminder_regex = (r'remind\s+(?P<who>[^\s]+)\s+(?P<when_type>at|in|on)\s+(?P<when>.*?)\s+' new_reminder_regex = (r'remind\s+(?P<who>[^\s]+)\s+(?P<when_type>at|in|on)\s+(?P<when>.*?)\s+'
r'(and\s+every\s+(?P<recurring_period>.*?)\s+)?' r'(and\s+every\s+(?P<recurring_period>.*?)\s+)?'
r'(until\s+(?P<recurring_until>.*?)\s+)?' r'(until\s+(?P<recurring_until>.*?)\s+)?'
r'(to|that|about)\s+(?P<text>.*)') r'(to|that|about)\s+(?P<text>.*?)'
r'(?=\s+\("(?P<name>.*)"\)|$)')
def __init__(self, bot, connection, event): def __init__(self, bot, connection, event):
"""Initialize some stuff.""" """Initialize some stuff."""
@ -42,7 +43,7 @@ class Countdown(Plugin):
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+list$', self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+list$',
self.handle_item_list, -20) self.handle_item_list, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+(\S+)$', self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+(.+)$',
self.handle_item_detail, -20) self.handle_item_detail, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], self.new_reminder_regex, self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], self.new_reminder_regex,
self.handle_new_reminder, -50) self.handle_new_reminder, -50)
@ -99,9 +100,16 @@ class Countdown(Plugin):
recurring_period = match.group('recurring_period') recurring_period = match.group('recurring_period')
recurring_until = match.group('recurring_until') recurring_until = match.group('recurring_until')
text = match.group('text') text = match.group('text')
name = match.group('name')
log.debug("%s / %s / %s", who, when, text) log.debug("%s / %s / %s", who, when, text)
item_name = '{0:s}-{1:s}'.format(event.sender_nick, timezone.now().strftime('%s')) if not name:
item_name = '{0:s}-{1:s}'.format(event.sender_nick, timezone.now().strftime('%s'))
else:
if CountdownItem.objects.filter(name=name).count() > 0:
self.bot.reply(event, "item with name '{0:s}' already exists".format(name))
return 'NO MORE'
item_name = name
# parse when to send the notification # parse when to send the notification
if when_type == 'in': if when_type == 'in':

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.2 on 2020-10-25 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('countdown', '0005_countdownitem_recurring_until'),
]
operations = [
migrations.AlterField(
model_name='countdownitem',
name='recurring_period',
field=models.CharField(blank=True, default='', max_length=64),
),
migrations.AlterField(
model_name='countdownitem',
name='reminder_target',
field=models.CharField(blank=True, default='', max_length=64),
),
]

View File

@ -1,14 +1,8 @@
"""Countdown item models.""" """Countdown item models."""
import logging
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
log = logging.getLogger('countdown.models')
class CountdownItem(models.Model): class CountdownItem(models.Model):
"""Track points in time.""" """Track points in time."""
@ -19,13 +13,13 @@ class CountdownItem(models.Model):
sent_reminder = models.BooleanField(default=False) sent_reminder = models.BooleanField(default=False)
reminder_message = models.TextField(default="") reminder_message = models.TextField(default="")
reminder_target = models.CharField(max_length=64, default='') reminder_target = models.CharField(max_length=64, blank=True, default='')
recurring_period = models.CharField(max_length=64, default='') recurring_period = models.CharField(max_length=64, blank=True, default='')
recurring_until = models.DateTimeField(null=True, blank=True, default=None) recurring_until = models.DateTimeField(null=True, blank=True, default=None)
created_time = models.DateTimeField(auto_now_add=True) created_time = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
"""String representation.""" """Summarize object."""
return "{0:s} @ {1:s}".format(self.name, timezone.localtime(self.at_time).strftime('%Y-%m-%d %H:%M:%S %Z')) return "{0:s} @ {1:s}".format(self.name, timezone.localtime(self.at_time).strftime('%Y-%m-%d %H:%M:%S %Z'))

14
countdown/serializers.py Normal file
View File

@ -0,0 +1,14 @@
"""REST serializers for countdown items."""
from rest_framework import serializers
from countdown.models import CountdownItem
class CountdownItemSerializer(serializers.ModelSerializer):
"""Countdown item serializer for the REST API."""
class Meta:
"""Meta options."""
model = CountdownItem
fields = '__all__'

12
countdown/urls.py Normal file
View File

@ -0,0 +1,12 @@
"""URL patterns for the countdown views."""
from django.conf.urls import include, url
from rest_framework.routers import DefaultRouter
from countdown.views import CountdownItemViewSet
router = DefaultRouter()
router.register(r'items', CountdownItemViewSet)
urlpatterns = [
url(r'^api/', include(router.urls)),
]

16
countdown/views.py Normal file
View File

@ -0,0 +1,16 @@
"""Provide an interface to countdown items."""
# from rest_framework.decorators import action
# from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet
from countdown.models import CountdownItem
from countdown.serializers import CountdownItemSerializer
class CountdownItemViewSet(ReadOnlyModelViewSet):
"""Provide list and detail actions for countdown items."""
queryset = CountdownItem.objects.all()
serializer_class = CountdownItemSerializer
permission_classes = [IsAuthenticated]

View File

@ -14,11 +14,12 @@ logger = logging.getLogger(__name__)
class Dice(Plugin): class Dice(Plugin):
"""Roll simple or complex dice strings.""" """Roll simple or complex dice strings."""
def __init__(self): def __init__(self, bot, connection, event):
"""Set up the plugin.""" """Set up the plugin."""
super(Dice, self).__init__()
self.roller = DiceRoller() self.roller = DiceRoller()
super(Dice, self).__init__(bot, connection, event)
def start(self): def start(self):
"""Set up the handlers.""" """Set up the handlers."""
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!roll\s+(.*)$', self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!roll\s+(.*)$',

View File

@ -1,11 +1,9 @@
"""General/baselite/site-wide URLs.""" """General/baselite/site-wide URLs."""
from adminplus.sites import AdminSitePlus
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.views.generic import TemplateView from django.views.generic import TemplateView
from adminplus.sites import AdminSitePlus
admin.site = AdminSitePlus() admin.site = AdminSitePlus()
admin.sites.site = admin.site admin.sites.site = admin.site
admin.autodiscover() admin.autodiscover()
@ -13,11 +11,13 @@ admin.autodiscover()
urlpatterns = [ urlpatterns = [
url(r'^$', TemplateView.as_view(template_name='index.html'), name='index'), url(r'^$', TemplateView.as_view(template_name='index.html'), name='index'),
url(r'^countdown/', include('countdown.urls')),
url(r'^dice/', include('dice.urls')), url(r'^dice/', include('dice.urls')),
url(r'^dispatch/', include('dispatch.urls')), url(r'^dispatch/', include('dispatch.urls')),
url(r'^itemsets/', include('facts.urls')), url(r'^itemsets/', include('facts.urls')),
url(r'^karma/', include('karma.urls')), url(r'^karma/', include('karma.urls')),
url(r'^markov/', include('markov.urls')), url(r'^markov/', include('markov.urls')),
url(r'^pi/', include('pi.urls')),
url(r'^races/', include('races.urls')), url(r'^races/', include('races.urls')),
url(r'^weather/', include('weather.urls')), url(r'^weather/', include('weather.urls')),

View File

@ -6,6 +6,7 @@ import re
import irc.client import irc.client
from django.conf import settings from django.conf import settings
from django.db.models import Count, Sum
from ircbot.lib import Plugin from ircbot.lib import Plugin
from karma.models import KarmaKey, KarmaLogEntry from karma.models import KarmaKey, KarmaLogEntry
@ -23,6 +24,9 @@ class Karma(Plugin):
self.connection.add_global_handler('pubmsg', self.handle_chatter, -20) self.connection.add_global_handler('pubmsg', self.handle_chatter, -20)
self.connection.add_global_handler('privmsg', self.handle_chatter, -20) self.connection.add_global_handler('privmsg', self.handle_chatter, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
(r'^!karma\s+keyreport\s+(.*)'),
self.handle_keyreport, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
r'^!karma\s+rank\s+(.*)$', r'^!karma\s+rank\s+(.*)$',
self.handle_rank, -20) self.handle_rank, -20)
@ -42,6 +46,7 @@ class Karma(Plugin):
self.connection.remove_global_handler('pubmsg', self.handle_chatter) self.connection.remove_global_handler('pubmsg', self.handle_chatter)
self.connection.remove_global_handler('privmsg', self.handle_chatter) self.connection.remove_global_handler('privmsg', self.handle_chatter)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_keyreport)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_rank) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_rank)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_report) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_report)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_stats) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_stats)
@ -102,6 +107,19 @@ class Karma(Plugin):
except KarmaKey.DoesNotExist: except KarmaKey.DoesNotExist:
return self.bot.reply(event, "i have not seen any karma for {0:s}".format(match.group(1))) return self.bot.reply(event, "i have not seen any karma for {0:s}".format(match.group(1)))
def handle_keyreport(self, connection, event, match):
"""Provide report on a karma key."""
key = match.group(1).lower().rstrip()
try:
karma_key = KarmaKey.objects.get(key=key)
karmaers = KarmaLogEntry.objects.filter(key=karma_key)
karmaers = karmaers.values('nickmask').annotate(Sum('delta')).annotate(Count('delta')).order_by('-delta__count')
karmaers_list = [f"{irc.client.NickMask(x['nickmask']).nick} ({x['delta__count']}, {'+' if x['delta__sum'] >= 0 else ''}{x['delta__sum']})" for x in karmaers]
karmaers_list_str = ", ".join(karmaers_list[:10])
return self.bot.reply(event, f"most opinionated on {key}: {karmaers_list_str}")
except KarmaKey.DoesNotExist:
return self.bot.reply(event, "i have not seen any karma for {0:s}".format(match.group(1)))
def handle_report(self, connection, event, match): def handle_report(self, connection, event, match):
"""Provide some karma reports.""" """Provide some karma reports."""

View File

@ -1,21 +1,14 @@
# coding: utf-8 # coding: utf-8
"""Provide pi simulation results to IRC."""
import logging
from ircbot.lib import Plugin from ircbot.lib import Plugin
from pi.models import PiLog from pi.models import PiLog
log = logging.getLogger('pi.ircplugin')
class Pi(Plugin): class Pi(Plugin):
"""Use the Monte Carlo method to simulate pi.""" """Use the Monte Carlo method to simulate pi."""
def start(self): def start(self):
"""Set up the handlers.""" """Set up the handlers."""
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!pi$', self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!pi$',
self.handle_pi, -20) self.handle_pi, -20)
@ -23,17 +16,16 @@ class Pi(Plugin):
def stop(self): def stop(self):
"""Tear down handlers.""" """Tear down handlers."""
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_pi) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_pi)
super(Pi, self).stop() super(Pi, self).stop()
def handle_pi(self, connection, event, match): def handle_pi(self, connection, event, match):
"""Handle the pi command by generating another value and presenting it.""" """Handle the pi command by generating another value and presenting it."""
newest, x, y = PiLog.objects.simulate()
newest, x, y, hit = PiLog.objects.simulate()
msg = ("({0:.10f}, {1:.10f}) is {2}within the unit circle. π is {5:.10f}. (i:{3:d} p:{4:d})" msg = ("({0:.10f}, {1:.10f}) is {2}within the unit circle. π is {5:.10f}. (i:{3:d} p:{4:d})"
"".format(x, y, "" if hit else "not ", newest.count_inside, newest.count_total, newest.value())) "".format(x, y, "" if newest.hit else "not ", newest.total_count_inside,
newest.total_count, newest.value))
return self.bot.reply(event, msg) return self.bot.reply(event, msg)

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.2 on 2020-10-24 16:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pi', '0002_auto_20150521_2204'),
]
operations = [
migrations.RenameField(
model_name='pilog',
old_name='count_total',
new_name='total_count',
),
migrations.RenameField(
model_name='pilog',
old_name='count_inside',
new_name='total_count_inside',
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 3.1.2 on 2020-10-24 16:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pi', '0003_rename_count_fields'),
]
operations = [
migrations.AddField(
model_name='pilog',
name='simulation_x',
field=models.DecimalField(decimal_places=10, default=-1.0, max_digits=11),
preserve_default=False,
),
migrations.AddField(
model_name='pilog',
name='simulation_y',
field=models.DecimalField(decimal_places=10, default=-1.0, max_digits=11),
preserve_default=False,
),
]

View File

@ -1,67 +1,70 @@
"""Karma logging models.""" """Karma logging models."""
import logging
import math import math
import pytz
import random import random
import pytz
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
log = logging.getLogger('pi.models')
class PiLogManager(models.Manager): class PiLogManager(models.Manager):
"""Assemble some queries against PiLog.""" """Assemble some queries against PiLog."""
def simulate(self): def simulate(self):
"""Add one more entry to the log, and return it.""" """Add one more entry to the log, and return it."""
try: try:
latest = self.latest() latest = self.latest()
except PiLog.DoesNotExist: except PiLog.DoesNotExist:
latest = PiLog(count_inside=0, count_total=0) latest = PiLog.objects.create(simulation_x=0.0, simulation_y=0.0,
total_count_inside=0, total_count=0)
latest.save() latest.save()
inside = latest.count_inside inside = latest.total_count_inside
total = latest.count_total total = latest.total_count
x = random.random() x = random.random()
y = random.random() y = random.random()
hit = True if math.hypot(x,y) < 1 else False total += 1
if math.hypot(x, y) < 1:
inside += 1
if hit: newest = PiLog.objects.create(simulation_x=x, simulation_y=y,
newest = PiLog(count_inside=inside+1, count_total=total+1) total_count_inside=inside, total_count=total)
else:
newest = PiLog(count_inside=inside, count_total=total+1)
newest.save()
return newest, x, y, hit # TODO: remove the x, y return values, now that we track them in the object
return newest, x, y
class PiLog(models.Model): class PiLog(models.Model):
"""Track pi as it is estimated over time.""" """Track pi as it is estimated over time."""
count_inside = models.PositiveIntegerField() simulation_x = models.DecimalField(max_digits=11, decimal_places=10)
count_total = models.PositiveIntegerField() simulation_y = models.DecimalField(max_digits=11, decimal_places=10)
total_count_inside = models.PositiveIntegerField()
total_count = models.PositiveIntegerField()
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
objects = PiLogManager() objects = PiLogManager()
class Meta: class Meta:
"""Options for the PiLog class."""
get_latest_by = 'created' get_latest_by = 'created'
def __str__(self): def __str__(self):
"""String representation.""" """Provide string representation."""
tz = pytz.timezone(settings.TIME_ZONE) tz = pytz.timezone(settings.TIME_ZONE)
return "({0:d}/{1:d}) @ {2:s}".format(self.count_inside, self.count_total, return "({0:d}/{1:d}) @ {2:s}".format(self.total_count_inside, self.total_count,
self.created.astimezone(tz).strftime('%Y-%m-%d %H:%M:%S %Z')) self.created.astimezone(tz).strftime('%Y-%m-%d %H:%M:%S %Z'))
@property
def value(self): def value(self):
"""Return this log entry's value of pi.""" """Return this log entry's estimated value of pi."""
if self.total_count == 0:
return 0.0
return 4.0 * int(self.total_count_inside) / int(self.total_count)
return 4.0 * int(self.count_inside) / int(self.count_total) @property
def hit(self):
"""Return if this log entry is inside the unit circle."""
return math.hypot(self.simulation_x, self.simulation_y) < 1

14
pi/serializers.py Normal file
View File

@ -0,0 +1,14 @@
"""REST serializers for pi simulations."""
from rest_framework import serializers
from pi.models import PiLog
class PiLogSerializer(serializers.ModelSerializer):
"""Pi simulation log entry serializer for the REST API."""
class Meta:
"""Meta options."""
model = PiLog
fields = ('id', 'simulation_x', 'simulation_y', 'total_count', 'total_count_inside', 'value', 'hit')

12
pi/urls.py Normal file
View File

@ -0,0 +1,12 @@
"""URL patterns for the pi views."""
from django.conf.urls import include, url
from rest_framework.routers import DefaultRouter
from pi.views import PiLogViewSet
router = DefaultRouter()
router.register(r'simulations', PiLogViewSet)
urlpatterns = [
url(r'^api/', include(router.urls)),
]

22
pi/views.py Normal file
View File

@ -0,0 +1,22 @@
"""Provide pi simulation results."""
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet
from pi.models import PiLog
from pi.serializers import PiLogSerializer
class PiLogViewSet(ReadOnlyModelViewSet):
"""Provide list and detail actions for pi simulation log entries."""
queryset = PiLog.objects.all()
serializer_class = PiLogSerializer
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['post'])
def simulate(self, request):
"""Run one simulation of the pi estimator."""
simulation, _, _ = PiLog.objects.simulate()
return Response(self.get_serializer(simulation).data, 201)

View File

@ -1,6 +0,0 @@
-r requirements.in
logilab-common # prospector thing, i guess
pip-tools # pip-compile
prospector # code quality
versioneer # auto-generate version numbers

View File

@ -1,68 +0,0 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file=requirements-dev.txt requirements-dev.in
#
astroid==2.2.5 # via pylint, pylint-celery, pylint-flask, requirements-detector
certifi==2019.6.16 # via requests
chardet==3.0.4 # via requests
click==7.0 # via pip-tools
django-adminplus==0.5
django-bootstrap3==11.0.0
django-extensions==2.1.9
django-registration-redux==2.6
django==2.2.2
djangorestframework==3.9.4
dodgy==0.1.9 # via prospector
future==0.17.1 # via parsedatetime
idna==2.8 # via requests
inflect==2.1.0 # via jaraco.itertools
irc==15.0.6
isort==4.3.20 # via pylint
jaraco.classes==2.0 # via jaraco.collections
jaraco.collections==2.0 # via irc
jaraco.functools==2.0 # via irc, jaraco.text, tempora
jaraco.itertools==4.4.2 # via irc
jaraco.logging==2.0 # via irc
jaraco.stream==2.0 # via irc
jaraco.text==3.0 # via irc, jaraco.collections
lazy-object-proxy==1.4.1 # via astroid
logilab-common==1.4.2
mccabe==0.6.1 # via prospector, pylint
more-itertools==7.0.0 # via irc, jaraco.functools, jaraco.itertools
oauthlib==3.0.1 # via requests-oauthlib
parsedatetime==2.4
pep8-naming==0.4.1 # via prospector
pip-tools==4.1.0
ply==3.11
prospector==1.1.6.4
pycodestyle==2.4.0 # via prospector
pydocstyle==3.0.0 # via prospector
pyflakes==1.6.0 # via prospector
pylint-celery==0.3 # via prospector
pylint-django==2.0.9 # via prospector
pylint-flask==0.6 # via prospector
pylint-plugin-utils==0.5 # via prospector, pylint-celery, pylint-django, pylint-flask
pylint==2.3.1 # via prospector, pylint-celery, pylint-django, pylint-flask, pylint-plugin-utils
python-dateutil==2.8.0
python-gitlab==1.9.0
python-mpd2==1.0.0
pytz==2019.1
pyyaml==5.1.1 # via prospector
requests-oauthlib==1.2.0 # via twython
requests==2.22.0 # via python-gitlab, requests-oauthlib, twython
requirements-detector==0.6 # via prospector
setoptconf==0.2.0 # via prospector
six==1.12.0 # via astroid, django-extensions, irc, jaraco.classes, jaraco.collections, jaraco.itertools, jaraco.logging, jaraco.stream, logilab-common, pip-tools, pydocstyle, python-dateutil, python-gitlab, tempora
snowballstemmer==1.2.1 # via pydocstyle
sqlparse==0.3.0 # via django
tempora==1.14.1 # via irc, jaraco.logging
twython==3.7.0
typed-ast==1.4.0 # via astroid
urllib3==1.25.3 # via requests
versioneer==0.18
wrapt==1.11.2 # via astroid
# The following packages are considered to be unsafe in a requirements file:
# setuptools==41.4.0 # via logilab-common

View File

@ -1,40 +0,0 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file=requirements.txt requirements.in
#
certifi==2019.6.16 # via requests
chardet==3.0.4 # via requests
django-adminplus==0.5
django-bootstrap3==11.0.0
django-extensions==2.1.9
django-registration-redux==2.6
django==2.2.2
djangorestframework==3.9.4
future==0.17.1 # via parsedatetime
idna==2.8 # via requests
inflect==2.1.0 # via jaraco.itertools
irc==15.0.6
jaraco.classes==2.0 # via jaraco.collections
jaraco.collections==2.0 # via irc
jaraco.functools==2.0 # via irc, jaraco.text, tempora
jaraco.itertools==4.4.2 # via irc
jaraco.logging==2.0 # via irc
jaraco.stream==2.0 # via irc
jaraco.text==3.0 # via irc, jaraco.collections
more-itertools==7.0.0 # via irc, jaraco.functools, jaraco.itertools
oauthlib==3.0.1 # via requests-oauthlib
parsedatetime==2.4
ply==3.11
python-dateutil==2.8.0
python-gitlab==1.9.0
python-mpd2==1.0.0
pytz==2019.1
requests-oauthlib==1.2.0 # via twython
requests==2.22.0 # via python-gitlab, requests-oauthlib, twython
six==1.12.0 # via django-extensions, irc, jaraco.classes, jaraco.collections, jaraco.itertools, jaraco.logging, jaraco.stream, python-dateutil, python-gitlab, tempora
sqlparse==0.3.0 # via django
tempora==1.14.1 # via irc, jaraco.logging
twython==3.7.0
urllib3==1.25.3 # via requests

View File

@ -0,0 +1,25 @@
-r requirements.in
# testing runner, test reporting, packages used during testing (e.g. requests-mock), etc.
pytest
pytest-cov
pytest-django
# linting and other static code analysis
bandit
dlint
flake8 # flake8 and plugins, for local dev linting in vim
flake8-blind-except
flake8-builtins
flake8-docstrings
flake8-executable
flake8-fixme
flake8-isort
flake8-logging-format
flake8-mutable
# maintenance utilities and tox
pip-tools # pip-compile
tox # CI stuff
tox-wheel # build wheels in tox
versioneer # automatic version numbering

View File

@ -0,0 +1,92 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in
#
appdirs==1.4.4 # via virtualenv
asgiref==3.2.10 # via django
attrs==20.2.0 # via pytest
bandit==1.6.2 # via -r requirements/requirements-dev.in
certifi==2020.6.20 # via requests
chardet==3.0.4 # via requests
click==7.1.2 # via pip-tools
coverage==5.3 # via pytest-cov
distlib==0.3.1 # via virtualenv
django-adminplus==0.5 # via -r requirements/requirements.in
django-bootstrap3==14.2.0 # via -r requirements/requirements.in
django-extensions==3.0.9 # via -r requirements/requirements.in
django-registration-redux==2.8 # via -r requirements/requirements.in
django==3.1.2 # via -r requirements/requirements.in, django-bootstrap3, djangorestframework
djangorestframework==3.12.1 # via -r requirements/requirements.in
dlint==0.10.3 # via -r requirements/requirements-dev.in
filelock==3.0.12 # via tox, virtualenv
flake8-blind-except==0.1.1 # via -r requirements/requirements-dev.in
flake8-builtins==1.5.3 # via -r requirements/requirements-dev.in
flake8-docstrings==1.5.0 # via -r requirements/requirements-dev.in
flake8-executable==2.0.4 # via -r requirements/requirements-dev.in
flake8-fixme==1.1.1 # via -r requirements/requirements-dev.in
flake8-isort==4.0.0 # via -r requirements/requirements-dev.in
flake8-logging-format==0.6.0 # via -r requirements/requirements-dev.in
flake8-mutable==1.2.0 # via -r requirements/requirements-dev.in
flake8==3.8.4 # via -r requirements/requirements-dev.in, dlint, flake8-builtins, flake8-docstrings, flake8-executable, flake8-isort, flake8-mutable
gitdb==4.0.5 # via gitpython
gitpython==3.1.11 # via bandit
idna==2.10 # via requests
importlib-metadata==1.7.0 # via django-bootstrap3, flake8, inflect, pluggy, pytest, stevedore, tox, virtualenv
importlib-resources==3.1.1 # via jaraco.text, virtualenv
inflect==4.1.0 # via jaraco.itertools
iniconfig==1.1.1 # via pytest
irc==15.0.6 # via -r requirements/requirements.in
isort==5.6.4 # via flake8-isort
jaraco.classes==3.1.0 # via jaraco.collections
jaraco.collections==3.0.0 # via irc
jaraco.functools==3.0.1 # via irc, jaraco.text, tempora
jaraco.itertools==5.0.0 # via irc
jaraco.logging==3.0.0 # via irc
jaraco.stream==3.0.0 # via irc
jaraco.text==3.2.0 # via irc, jaraco.collections
mccabe==0.6.1 # via flake8
more-itertools==8.5.0 # via irc, jaraco.classes, jaraco.functools, jaraco.itertools
oauthlib==3.1.0 # via requests-oauthlib
packaging==20.4 # via pytest, tox
parsedatetime==2.6 # via -r requirements/requirements.in
pbr==5.5.1 # via stevedore
pip-tools==5.3.1 # via -r requirements/requirements-dev.in
pluggy==0.13.1 # via pytest, tox
ply==3.11 # via -r requirements/requirements.in
py==1.9.0 # via pytest, tox
pycodestyle==2.6.0 # via flake8
pydocstyle==5.1.1 # via flake8-docstrings
pyflakes==2.2.0 # via flake8
pyparsing==2.4.7 # via packaging
pytest-cov==2.10.1 # via -r requirements/requirements-dev.in
pytest-django==4.1.0 # via -r requirements/requirements-dev.in
pytest==6.1.1 # via -r requirements/requirements-dev.in, pytest-cov, pytest-django
python-dateutil==2.8.1 # via -r requirements/requirements.in
python-gitlab==2.5.0 # via -r requirements/requirements.in
python-mpd2==1.1.0 # via -r requirements/requirements.in
pytz==2020.1 # via -r requirements/requirements.in, django, irc, tempora
pyyaml==5.3.1 # via bandit
requests-oauthlib==1.3.0 # via twython
requests==2.24.0 # via python-gitlab, requests-oauthlib, twython
six==1.15.0 # via bandit, irc, jaraco.collections, jaraco.logging, jaraco.text, packaging, pip-tools, python-dateutil, tox, virtualenv
smmap==3.0.4 # via gitdb
snowballstemmer==2.0.0 # via pydocstyle
sqlparse==0.4.1 # via django
stevedore==3.2.2 # via bandit
tempora==4.0.0 # via irc, jaraco.logging
testfixtures==6.15.0 # via flake8-isort
toml==0.10.1 # via pytest, tox
tox-wheel==0.5.0 # via -r requirements/requirements-dev.in
tox==3.20.1 # via -r requirements/requirements-dev.in, tox-wheel
twython==3.8.2 # via -r requirements/requirements.in
urllib3==1.25.11 # via requests
versioneer==0.18 # via -r requirements/requirements-dev.in
virtualenv==20.0.35 # via tox
wheel==0.35.1 # via tox-wheel
zipp==3.3.2 # via importlib-metadata, importlib-resources
# The following packages are considered to be unsafe in a requirements file:
# pip
# setuptools

View File

@ -0,0 +1,43 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in
#
asgiref==3.2.10 # via django
certifi==2020.6.20 # via requests
chardet==3.0.4 # via requests
django-adminplus==0.5 # via -r requirements/requirements.in
django-bootstrap3==14.2.0 # via -r requirements/requirements.in
django-extensions==3.0.9 # via -r requirements/requirements.in
django-registration-redux==2.8 # via -r requirements/requirements.in
django==3.1.2 # via -r requirements/requirements.in, django-bootstrap3, djangorestframework
djangorestframework==3.12.1 # via -r requirements/requirements.in
idna==2.10 # via requests
importlib-metadata==1.7.0 # via django-bootstrap3, inflect
importlib-resources==3.1.1 # via jaraco.text
inflect==4.1.0 # via jaraco.itertools
irc==15.0.6 # via -r requirements/requirements.in
jaraco.classes==3.1.0 # via jaraco.collections
jaraco.collections==3.0.0 # via irc
jaraco.functools==3.0.1 # via irc, jaraco.text, tempora
jaraco.itertools==5.0.0 # via irc
jaraco.logging==3.0.0 # via irc
jaraco.stream==3.0.0 # via irc
jaraco.text==3.2.0 # via irc, jaraco.collections
more-itertools==8.5.0 # via irc, jaraco.classes, jaraco.functools, jaraco.itertools
oauthlib==3.1.0 # via requests-oauthlib
parsedatetime==2.6 # via -r requirements/requirements.in
ply==3.11 # via -r requirements/requirements.in
python-dateutil==2.8.1 # via -r requirements/requirements.in
python-gitlab==2.5.0 # via -r requirements/requirements.in
python-mpd2==1.1.0 # via -r requirements/requirements.in
pytz==2020.1 # via -r requirements/requirements.in, django, irc, tempora
requests-oauthlib==1.3.0 # via twython
requests==2.24.0 # via python-gitlab, requests-oauthlib, twython
six==1.15.0 # via irc, jaraco.collections, jaraco.logging, jaraco.text, python-dateutil
sqlparse==0.4.1 # via django
tempora==4.0.0 # via irc, jaraco.logging
twython==3.8.2 # via -r requirements/requirements.in
urllib3==1.25.11 # via requests
zipp==3.3.2 # via importlib-metadata, importlib-resources

View File

@ -8,7 +8,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
def extract_requires(): def extract_requires():
with open(os.path.join(HERE, 'requirements.in'), 'r') as reqs: with open(os.path.join(HERE, 'requirements/requirements.in'), 'r') as reqs:
return [line.split(' ')[0] for line in reqs if not line[0] == '-'] return [line.split(' ')[0] for line in reqs if not line[0] == '-']

47
tests/test_pi_models.py Normal file
View File

@ -0,0 +1,47 @@
"""Test the pi models."""
from unittest import mock
from django.test import TestCase
from django.utils.timezone import now
from pi.models import PiLog
class PiLogTest(TestCase):
"""Test pi models."""
def test_hit_calculation(self):
"""Test that x,y combinations are properly considered inside or outside the circle."""
hit_item = PiLog(simulation_x=0.0, simulation_y=0.0, total_count=0, total_count_inside=0)
miss_item = PiLog(simulation_x=1.0, simulation_y=1.0, total_count=0, total_count_inside=0)
self.assertTrue(hit_item.hit)
self.assertFalse(miss_item.hit)
def test_value_calculation(self):
"""Test that a simulation's value of pi can be calculated."""
item = PiLog(simulation_x=0.0, simulation_y=0.0, total_count=1000, total_count_inside=788)
zero_item = PiLog(simulation_x=0.0, simulation_y=0.0, total_count=0, total_count_inside=0)
self.assertEqual(item.value, 3.152)
self.assertEqual(zero_item.value, 0.0)
def test_string_repr(self):
"""Test the string repr of a simulation log entry."""
item = PiLog(simulation_x=0.0, simulation_y=0.0, total_count=1000, total_count_inside=788,
created=now())
self.assertIn("(788/1000) @ ", str(item))
def test_simulation_inside_determination(self):
"""Test that running a simulation passes the proper inside value."""
# get at least one simulation in the DB
original_item, _, _ = PiLog.objects.simulate()
with mock.patch('random.random', return_value=1.0):
miss_item, _, _ = PiLog.objects.simulate()
self.assertEqual(miss_item.total_count_inside, original_item.total_count_inside)
with mock.patch('random.random', return_value=0.0):
hit_item, _, _ = PiLog.objects.simulate()
self.assertGreater(hit_item.total_count_inside, original_item.total_count_inside)

25
tests/test_pi_views.py Normal file
View File

@ -0,0 +1,25 @@
"""Test the pi package's views."""
from django.contrib.auth.models import User
from rest_framework.status import HTTP_201_CREATED
from rest_framework.test import APITestCase
from pi.models import PiLog
class PiAPITest(APITestCase):
"""Test pi DRF views."""
def setUp(self):
"""Do pre-test stuff."""
self.client = self.client_class()
self.user = User.objects.create(username='test')
self.client.force_authenticate(user=self.user)
def test_simulate_creates_simulation(self):
"""Test that the simulate action creates a log entry."""
self.assertEqual(PiLog.objects.count(), 0)
resp = self.client.post('/pi/api/simulations/simulate/')
self.assertEqual(resp.status_code, HTTP_201_CREATED)
self.assertEqual(PiLog.objects.count(), 2) # 2 because 0 entry and the real entry

201
tox.ini Normal file
View File

@ -0,0 +1,201 @@
# tox (https://tox.readthedocs.io/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = begin,py36,py37,py38,coverage,security,lint,bundle
[testenv]
# build a wheel and test it
wheel = true
wheel_build_env = build
# whitelist commands we need
whitelist_externals = cp
# install everything via requirements-dev.txt, so that developer environment
# is the same as the tox environment (for ease of use/no weird gotchas in
# local dev results vs. tox results) and also to avoid ticky-tacky maintenance
# of "oh this particular env has weird results unless I install foo" --- just
# shotgun blast install everything everywhere
deps =
-rrequirements/requirements-dev.txt
[testenv:build]
# require setuptools when building
deps = setuptools
[testenv:begin]
# clean up potential previous coverage runs
skip_install = true
commands = coverage erase
[testenv:py36]
# run pytest with coverage
commands =
pytest --cov-append --cov-branch \
--cov={envsitepackagesdir}/acro/ \
--cov={envsitepackagesdir}/countdown/ \
--cov={envsitepackagesdir}/dice/ \
--cov={envsitepackagesdir}/dispatch/ \
--cov={envsitepackagesdir}/dr_botzo/ \
--cov={envsitepackagesdir}/facts/ \
--cov={envsitepackagesdir}/gitlab_bot/ \
--cov={envsitepackagesdir}/ircbot/ \
--cov={envsitepackagesdir}/karma/ \
--cov={envsitepackagesdir}/markov/ \
--cov={envsitepackagesdir}/mpdbot/ \
--cov={envsitepackagesdir}/pi/ \
--cov={envsitepackagesdir}/races/ \
--cov={envsitepackagesdir}/seen/ \
--cov={envsitepackagesdir}/storycraft/ \
--cov={envsitepackagesdir}/transform/ \
--cov={envsitepackagesdir}/twitter/ \
--cov={envsitepackagesdir}/weather/
[testenv:py37]
# run pytest with coverage
commands =
pytest --cov-append --cov-branch \
--cov={envsitepackagesdir}/acro/ \
--cov={envsitepackagesdir}/countdown/ \
--cov={envsitepackagesdir}/dice/ \
--cov={envsitepackagesdir}/dispatch/ \
--cov={envsitepackagesdir}/dr_botzo/ \
--cov={envsitepackagesdir}/facts/ \
--cov={envsitepackagesdir}/gitlab_bot/ \
--cov={envsitepackagesdir}/ircbot/ \
--cov={envsitepackagesdir}/karma/ \
--cov={envsitepackagesdir}/markov/ \
--cov={envsitepackagesdir}/mpdbot/ \
--cov={envsitepackagesdir}/pi/ \
--cov={envsitepackagesdir}/races/ \
--cov={envsitepackagesdir}/seen/ \
--cov={envsitepackagesdir}/storycraft/ \
--cov={envsitepackagesdir}/transform/ \
--cov={envsitepackagesdir}/twitter/ \
--cov={envsitepackagesdir}/weather/
[testenv:py38]
# run pytest with coverage
commands =
pytest --cov-append --cov-branch \
--cov={envsitepackagesdir}/acro/ \
--cov={envsitepackagesdir}/countdown/ \
--cov={envsitepackagesdir}/dice/ \
--cov={envsitepackagesdir}/dispatch/ \
--cov={envsitepackagesdir}/dr_botzo/ \
--cov={envsitepackagesdir}/facts/ \
--cov={envsitepackagesdir}/gitlab_bot/ \
--cov={envsitepackagesdir}/ircbot/ \
--cov={envsitepackagesdir}/karma/ \
--cov={envsitepackagesdir}/markov/ \
--cov={envsitepackagesdir}/mpdbot/ \
--cov={envsitepackagesdir}/pi/ \
--cov={envsitepackagesdir}/races/ \
--cov={envsitepackagesdir}/seen/ \
--cov={envsitepackagesdir}/storycraft/ \
--cov={envsitepackagesdir}/transform/ \
--cov={envsitepackagesdir}/twitter/ \
--cov={envsitepackagesdir}/weather/
[testenv:coverage]
# report on coverage runs from above
skip_install = true
commands =
coverage report --fail-under=95 --show-missing
[testenv:security]
# run security checks
#
# again it seems the most valuable here to run against the packaged code
commands =
bandit \
{envsitepackagesdir}/acro/ \
{envsitepackagesdir}/countdown/ \
{envsitepackagesdir}/dice/ \
{envsitepackagesdir}/dispatch/ \
{envsitepackagesdir}/dr_botzo/ \
{envsitepackagesdir}/facts/ \
{envsitepackagesdir}/gitlab_bot/ \
{envsitepackagesdir}/ircbot/ \
{envsitepackagesdir}/karma/ \
{envsitepackagesdir}/markov/ \
{envsitepackagesdir}/mpdbot/ \
{envsitepackagesdir}/pi/ \
{envsitepackagesdir}/races/ \
{envsitepackagesdir}/seen/ \
{envsitepackagesdir}/storycraft/ \
{envsitepackagesdir}/transform/ \
{envsitepackagesdir}/twitter/ \
{envsitepackagesdir}/weather/ \
-r
[testenv:lint]
# run style checks
commands =
flake8
- flake8 --disable-noqa --ignore= --select=E,W,F,C,D,A,G,B,I,T,M,DUO
[testenv:bundle]
# take extra actions (build sdist, sphinx, whatever) to completely package the app
commands =
cp -r {distdir} .
python setup.py sdist
[coverage:paths]
source =
./
.tox/**/site-packages/
[coverage:run]
branch = True
# redundant with pytest --cov above, but this tricks the coverage.xml report into
# using the full path, otherwise files with the same name in different paths
# get clobbered. maybe appends would fix this, IDK
include =
{envsitepackagesdir}/acro/
{envsitepackagesdir}/countdown/
{envsitepackagesdir}/dice/
{envsitepackagesdir}/dispatch/
{envsitepackagesdir}/dr_botzo/
{envsitepackagesdir}/facts/
{envsitepackagesdir}/gitlab_bot/
{envsitepackagesdir}/ircbot/
{envsitepackagesdir}/karma/
{envsitepackagesdir}/markov/
{envsitepackagesdir}/mpdbot/
{envsitepackagesdir}/pi/
{envsitepackagesdir}/races/
{envsitepackagesdir}/seen/
{envsitepackagesdir}/storycraft/
{envsitepackagesdir}/transform/
{envsitepackagesdir}/twitter/
{envsitepackagesdir}/weather/
omit =
**/_version.py
[flake8]
enable-extensions = G,M
exclude =
.tox/
versioneer.py
_version.py
instance/
extend-ignore = T101
max-complexity = 10
max-line-length = 120
[isort]
line_length = 120
[pytest]
python_files =
*_tests.py
tests.py
test_*.py
DJANGO_SETTINGS_MODULE = dr_botzo.settings
django_find_project = false

View File

@ -33,7 +33,7 @@ class Weather(Plugin):
if len(queryitems) <= 0: if len(queryitems) <= 0:
return return
weather = weather_summary(queryitems[0]) weather = weather_summary(query)
weather_output = (f"Weather in {weather['location']}: {weather['current']['description']}. " weather_output = (f"Weather in {weather['location']}: {weather['current']['description']}. "
f"{weather['current']['temp_F']}/{weather['current']['temp_C']}, " f"{weather['current']['temp_F']}/{weather['current']['temp_C']}, "
f"feels like {weather['current']['feels_like_temp_F']}/" f"feels like {weather['current']['feels_like_temp_F']}/"

View File

@ -2,6 +2,7 @@
"""Get results of weather queries.""" """Get results of weather queries."""
import logging import logging
import requests import requests
from urllib.parse import quote
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -9,7 +10,7 @@ logger = logging.getLogger(__name__)
def query_wttr_in(query): def query_wttr_in(query):
"""Hit the wttr.in JSON API with the provided query.""" """Hit the wttr.in JSON API with the provided query."""
logger.info(f"about to query wttr.in with '{query}'") logger.info(f"about to query wttr.in with '{query}'")
response = requests.get(f'http://wttr.in/{query}?format=j1') response = requests.get(f'http://wttr.in/{quote(query)}?format=j1')
response.raise_for_status() response.raise_for_status()
weather_info = response.json() weather_info = response.json()