From 1833430c5d1f86773c6bdaf725d9440f1c566d82 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Oct 2020 09:22:07 -0500 Subject: [PATCH 01/18] put requirements in my now-usual spot --- requirements-dev.in => requirements/requirements-dev.in | 0 requirements-dev.txt => requirements/requirements-dev.txt | 0 requirements.in => requirements/requirements.in | 0 requirements.txt => requirements/requirements.txt | 0 setup.py | 2 +- 5 files changed, 1 insertion(+), 1 deletion(-) rename requirements-dev.in => requirements/requirements-dev.in (100%) rename requirements-dev.txt => requirements/requirements-dev.txt (100%) rename requirements.in => requirements/requirements.in (100%) rename requirements.txt => requirements/requirements.txt (100%) diff --git a/requirements-dev.in b/requirements/requirements-dev.in similarity index 100% rename from requirements-dev.in rename to requirements/requirements-dev.in diff --git a/requirements-dev.txt b/requirements/requirements-dev.txt similarity index 100% rename from requirements-dev.txt rename to requirements/requirements-dev.txt diff --git a/requirements.in b/requirements/requirements.in similarity index 100% rename from requirements.in rename to requirements/requirements.in diff --git a/requirements.txt b/requirements/requirements.txt similarity index 100% rename from requirements.txt rename to requirements/requirements.txt diff --git a/setup.py b/setup.py index 41d7d2b..a538137 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ HERE = os.path.dirname(os.path.abspath(__file__)) 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] == '-'] -- 2.43.2 From 819bbe74c6dd33b9dc8b081d57773c669d6c76d2 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Oct 2020 09:30:46 -0500 Subject: [PATCH 02/18] recompile requirements basically everything changed here, so... fingers crossed --- requirements/requirements-dev.in | 27 +++++- requirements/requirements-dev.txt | 144 +++++++++++++++++------------- requirements/requirements.txt | 71 ++++++++------- 3 files changed, 144 insertions(+), 98 deletions(-) diff --git a/requirements/requirements-dev.in b/requirements/requirements-dev.in index 8b474bf..1663d96 100644 --- a/requirements/requirements-dev.in +++ b/requirements/requirements-dev.in @@ -1,6 +1,25 @@ -r requirements.in -logilab-common # prospector thing, i guess -pip-tools # pip-compile -prospector # code quality -versioneer # auto-generate version numbers +# 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 diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index 4e73415..69f2d4a 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -2,67 +2,91 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file=requirements-dev.txt requirements-dev.in +# pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in # -astroid==2.2.5 # via pylint, pylint-celery, pylint-flask, requirements-detector -certifi==2019.6.16 # via requests +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.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 +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: -# setuptools==41.4.0 # via logilab-common +# pip +# setuptools diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 7cfadb5..1aac3bc 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,39 +2,42 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file=requirements.txt requirements.in +# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in # -certifi==2019.6.16 # via requests +asgiref==3.2.10 # via django +certifi==2020.6.20 # 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 +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 -- 2.43.2 From 10d73f570a5886e353f58a824ee355126d55435f Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Oct 2020 09:54:12 -0500 Subject: [PATCH 03/18] start using tox, despite 100000 errors --- .gitignore | 2 + .prospector.yaml | 20 ----- tox.ini | 201 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 20 deletions(-) delete mode 100644 .prospector.yaml create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 6e9eb1a..986398e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ build/ dist/ tags/ *.egg-info/ +.tox/ +.coverage dr.botzo.data dr.botzo.cfg localsettings.py diff --git a/.prospector.yaml b/.prospector.yaml deleted file mode 100644 index 838b437..0000000 --- a/.prospector.yaml +++ /dev/null @@ -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 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..9b0ec28 --- /dev/null +++ b/tox.ini @@ -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 -- 2.43.2 From 4d94322c55a1aa684c70c8a49b9a527eb6da29b6 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Oct 2020 11:47:36 -0500 Subject: [PATCH 04/18] refactor PiLog to retain x,y values --- pi/ircplugin.py | 16 ++---- pi/migrations/0003_rename_count_fields.py | 23 +++++++++ pi/migrations/0004_simulation_x_y_logging.py | 25 ++++++++++ pi/models.py | 52 ++++++++++---------- 4 files changed, 78 insertions(+), 38 deletions(-) create mode 100644 pi/migrations/0003_rename_count_fields.py create mode 100644 pi/migrations/0004_simulation_x_y_logging.py diff --git a/pi/ircplugin.py b/pi/ircplugin.py index 375d7bd..87e1e15 100644 --- a/pi/ircplugin.py +++ b/pi/ircplugin.py @@ -1,21 +1,14 @@ # coding: utf-8 - -import logging - +"""Provide pi simulation results to IRC.""" from ircbot.lib import Plugin from pi.models import PiLog -log = logging.getLogger('pi.ircplugin') - - class Pi(Plugin): - """Use the Monte Carlo method to simulate pi.""" def start(self): """Set up the handlers.""" - self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!pi$', self.handle_pi, -20) @@ -23,17 +16,16 @@ class Pi(Plugin): def stop(self): """Tear down handlers.""" - self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_pi) super(Pi, self).stop() def handle_pi(self, connection, event, match): """Handle the pi command by generating another value and presenting it.""" - - newest, x, y, hit = PiLog.objects.simulate() + newest, x, y = PiLog.objects.simulate() 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) diff --git a/pi/migrations/0003_rename_count_fields.py b/pi/migrations/0003_rename_count_fields.py new file mode 100644 index 0000000..e722df5 --- /dev/null +++ b/pi/migrations/0003_rename_count_fields.py @@ -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', + ), + ] diff --git a/pi/migrations/0004_simulation_x_y_logging.py b/pi/migrations/0004_simulation_x_y_logging.py new file mode 100644 index 0000000..02db64a --- /dev/null +++ b/pi/migrations/0004_simulation_x_y_logging.py @@ -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=0.0, max_digits=11), + preserve_default=False, + ), + migrations.AddField( + model_name='pilog', + name='simulation_y', + field=models.DecimalField(decimal_places=10, default=0.0, max_digits=11), + preserve_default=False, + ), + ] diff --git a/pi/models.py b/pi/models.py index 10e9548..0490748 100644 --- a/pi/models.py +++ b/pi/models.py @@ -1,67 +1,67 @@ """Karma logging models.""" - -import logging import math -import pytz import random +import pytz from django.conf import settings from django.db import models -log = logging.getLogger('pi.models') - - class PiLogManager(models.Manager): - """Assemble some queries against PiLog.""" def simulate(self): """Add one more entry to the log, and return it.""" - try: latest = self.latest() 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() - inside = latest.count_inside - total = latest.count_total + inside = latest.total_count_inside + total = latest.total_count x = 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(count_inside=inside+1, count_total=total+1) - else: - newest = PiLog(count_inside=inside, count_total=total+1) - newest.save() + newest = PiLog.objects.create(simulation_x=x, simulation_y=y, + total_count_inside=inside, total_count=total) - return newest, x, y, hit + return newest, x, y class PiLog(models.Model): - """Track pi as it is estimated over time.""" - count_inside = models.PositiveIntegerField() - count_total = models.PositiveIntegerField() + simulation_x = models.DecimalField(max_digits=11, decimal_places=10) + 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) objects = PiLogManager() class Meta: + """Options for the PiLog class.""" + get_latest_by = 'created' def __str__(self): - """String representation.""" - + """Provide string representation.""" 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')) + @property def value(self): - """Return this log entry's value of pi.""" + """Return this log entry's estimated value of pi.""" + 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 -- 2.43.2 From 665f56a430b08b69b9c319aaec099f9222c5583d Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Oct 2020 11:56:48 -0500 Subject: [PATCH 05/18] avoid divide by 0 in getting pi simulation value --- pi/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pi/models.py b/pi/models.py index 0490748..210d8d9 100644 --- a/pi/models.py +++ b/pi/models.py @@ -59,6 +59,8 @@ class PiLog(models.Model): @property def value(self): """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) @property -- 2.43.2 From 691ee7696bb26c536dc3a2d366d0c4be1fb3190e Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Oct 2020 11:58:43 -0500 Subject: [PATCH 06/18] serve basic list/detail pi log simulation views --- dr_botzo/urls.py | 1 + pi/serializers.py | 14 ++++++++++++++ pi/urls.py | 12 ++++++++++++ pi/views.py | 12 ++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 pi/serializers.py create mode 100644 pi/urls.py create mode 100644 pi/views.py diff --git a/dr_botzo/urls.py b/dr_botzo/urls.py index 3abb52b..e9096ce 100644 --- a/dr_botzo/urls.py +++ b/dr_botzo/urls.py @@ -18,6 +18,7 @@ urlpatterns = [ url(r'^itemsets/', include('facts.urls')), url(r'^karma/', include('karma.urls')), url(r'^markov/', include('markov.urls')), + url(r'^pi/', include('pi.urls')), url(r'^races/', include('races.urls')), url(r'^weather/', include('weather.urls')), diff --git a/pi/serializers.py b/pi/serializers.py new file mode 100644 index 0000000..abae8a2 --- /dev/null +++ b/pi/serializers.py @@ -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') diff --git a/pi/urls.py b/pi/urls.py new file mode 100644 index 0000000..3c7596b --- /dev/null +++ b/pi/urls.py @@ -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)), +] diff --git a/pi/views.py b/pi/views.py new file mode 100644 index 0000000..185bfac --- /dev/null +++ b/pi/views.py @@ -0,0 +1,12 @@ +"""Provide pi simulation results.""" +from rest_framework import viewsets + +from pi.models import PiLog +from pi.serializers import PiLogSerializer + + +class PiLogViewSet(viewsets.ReadOnlyModelViewSet): + """Provide list and detail actions for pi simulation log entries.""" + + queryset = PiLog.objects.all() + serializer_class = PiLogSerializer -- 2.43.2 From ef08cec0fb610de75ee8b92e05e435726d2224a8 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Oct 2020 23:58:02 -0500 Subject: [PATCH 07/18] fix field reference in the PiLog.hit property --- pi/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pi/models.py b/pi/models.py index 210d8d9..fbdf6c6 100644 --- a/pi/models.py +++ b/pi/models.py @@ -66,4 +66,4 @@ class PiLog(models.Model): @property def hit(self): """Return if this log entry is inside the unit circle.""" - return math.hypot(self.simulation_x, self.simulation.y) < 1 + return math.hypot(self.simulation_x, self.simulation_y) < 1 -- 2.43.2 From da815a1fc33f5effb172c8ad16aab09e1bbb1a3a Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Oct 2020 23:58:45 -0500 Subject: [PATCH 08/18] provide DRF action to run a pi simulation --- pi/views.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pi/views.py b/pi/views.py index 185bfac..29d932b 100644 --- a/pi/views.py +++ b/pi/views.py @@ -1,12 +1,22 @@ """Provide pi simulation results.""" -from rest_framework import viewsets +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(viewsets.ReadOnlyModelViewSet): +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) -- 2.43.2 From d5e534319359284ac3005a00fdaa238d7765867b Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sun, 25 Oct 2020 11:16:23 -0500 Subject: [PATCH 09/18] have pi simulate via API return a 201 --- pi/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pi/views.py b/pi/views.py index 29d932b..ea63733 100644 --- a/pi/views.py +++ b/pi/views.py @@ -19,4 +19,4 @@ class PiLogViewSet(ReadOnlyModelViewSet): def simulate(self, request): """Run one simulation of the pi estimator.""" simulation, _, _ = PiLog.objects.simulate() - return Response(self.get_serializer(simulation).data) + return Response(self.get_serializer(simulation).data, 201) -- 2.43.2 From a0a1aa10f485b9c375e260cfa9adf1bbceb33b77 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sun, 25 Oct 2020 11:16:48 -0500 Subject: [PATCH 10/18] wow, unit tests! pi is covered (except for the irc plugin) I think I'm going to move the irc stuff into a separate project in the future so I'm not worried about testing that one yet --- tests/test_pi_models.py | 47 +++++++++++++++++++++++++++++++++++++++++ tests/test_pi_views.py | 25 ++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/test_pi_models.py create mode 100644 tests/test_pi_views.py diff --git a/tests/test_pi_models.py b/tests/test_pi_models.py new file mode 100644 index 0000000..ddbf457 --- /dev/null +++ b/tests/test_pi_models.py @@ -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) diff --git a/tests/test_pi_views.py b/tests/test_pi_views.py new file mode 100644 index 0000000..0403f73 --- /dev/null +++ b/tests/test_pi_views.py @@ -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 -- 2.43.2 From bcc5f767ba283ed38565360df5f3ab274aba8c9c Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sun, 25 Oct 2020 11:19:20 -0500 Subject: [PATCH 11/18] little cleanup TODO in PiLog simulate() --- pi/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pi/models.py b/pi/models.py index fbdf6c6..ae622f2 100644 --- a/pi/models.py +++ b/pi/models.py @@ -31,6 +31,7 @@ class PiLogManager(models.Manager): newest = PiLog.objects.create(simulation_x=x, simulation_y=y, total_count_inside=inside, total_count=total) + # TODO: remove the x, y return values, now that we track them in the object return newest, x, y -- 2.43.2 From a6f8fc5dc1626032c4d023e91ffcc23846dea747 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sun, 25 Oct 2020 12:16:33 -0500 Subject: [PATCH 12/18] make more countdown item fields optional in admin --- .../migrations/0006_auto_20201025_1716.py | 23 +++++++++++++++++++ countdown/models.py | 12 +++------- 2 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 countdown/migrations/0006_auto_20201025_1716.py diff --git a/countdown/migrations/0006_auto_20201025_1716.py b/countdown/migrations/0006_auto_20201025_1716.py new file mode 100644 index 0000000..72a2fc3 --- /dev/null +++ b/countdown/migrations/0006_auto_20201025_1716.py @@ -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), + ), + ] diff --git a/countdown/models.py b/countdown/models.py index fc2c89c..6a84fc3 100644 --- a/countdown/models.py +++ b/countdown/models.py @@ -1,14 +1,8 @@ """Countdown item models.""" - -import logging - from django.db import models from django.utils import timezone -log = logging.getLogger('countdown.models') - - class CountdownItem(models.Model): """Track points in time.""" @@ -19,13 +13,13 @@ class CountdownItem(models.Model): sent_reminder = models.BooleanField(default=False) 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) created_time = models.DateTimeField(auto_now_add=True) 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')) -- 2.43.2 From 9d94155f6636dce6d58e4d8433a1cab33215a3da Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sun, 25 Oct 2020 12:20:39 -0500 Subject: [PATCH 13/18] implement basic API GETs for countdown items --- countdown/serializers.py | 14 ++++++++++++++ countdown/urls.py | 12 ++++++++++++ countdown/views.py | 16 ++++++++++++++++ dr_botzo/urls.py | 5 ++--- 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 countdown/serializers.py create mode 100644 countdown/urls.py create mode 100644 countdown/views.py diff --git a/countdown/serializers.py b/countdown/serializers.py new file mode 100644 index 0000000..9c9439b --- /dev/null +++ b/countdown/serializers.py @@ -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__' diff --git a/countdown/urls.py b/countdown/urls.py new file mode 100644 index 0000000..0083170 --- /dev/null +++ b/countdown/urls.py @@ -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)), +] diff --git a/countdown/views.py b/countdown/views.py new file mode 100644 index 0000000..14d2e31 --- /dev/null +++ b/countdown/views.py @@ -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] diff --git a/dr_botzo/urls.py b/dr_botzo/urls.py index e9096ce..c63f3f1 100644 --- a/dr_botzo/urls.py +++ b/dr_botzo/urls.py @@ -1,11 +1,9 @@ """General/baselite/site-wide URLs.""" - +from adminplus.sites import AdminSitePlus from django.conf.urls import include, url from django.contrib import admin from django.views.generic import TemplateView -from adminplus.sites import AdminSitePlus - admin.site = AdminSitePlus() admin.sites.site = admin.site admin.autodiscover() @@ -13,6 +11,7 @@ admin.autodiscover() urlpatterns = [ 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'^dispatch/', include('dispatch.urls')), url(r'^itemsets/', include('facts.urls')), -- 2.43.2 From ab0d73885172418ea3130a93d353182acccc4e04 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sun, 25 Oct 2020 12:28:08 -0500 Subject: [PATCH 14/18] for migrated pi history, set their x,y to -1.0 to be more obvious --- pi/migrations/0004_simulation_x_y_logging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pi/migrations/0004_simulation_x_y_logging.py b/pi/migrations/0004_simulation_x_y_logging.py index 02db64a..0670024 100644 --- a/pi/migrations/0004_simulation_x_y_logging.py +++ b/pi/migrations/0004_simulation_x_y_logging.py @@ -13,13 +13,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name='pilog', name='simulation_x', - field=models.DecimalField(decimal_places=10, default=0.0, max_digits=11), + 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=0.0, max_digits=11), + field=models.DecimalField(decimal_places=10, default=-1.0, max_digits=11), preserve_default=False, ), ] -- 2.43.2 From dc5f243608de3e4fcd7c6acd786310332359e815 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Apr 2021 12:55:51 -0500 Subject: [PATCH 15/18] optionally provide name of new countdown items --- countdown/ircplugin.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/countdown/ircplugin.py b/countdown/ircplugin.py index 8edd977..9780636 100644 --- a/countdown/ircplugin.py +++ b/countdown/ircplugin.py @@ -22,7 +22,8 @@ class Countdown(Plugin): new_reminder_regex = (r'remind\s+(?P[^\s]+)\s+(?Pat|in|on)\s+(?P.*?)\s+' r'(and\s+every\s+(?P.*?)\s+)?' r'(until\s+(?P.*?)\s+)?' - r'(to|that|about)\s+(?P.*)') + r'(to|that|about)\s+(?P.*?)' + r'(?=\s+\("(?P.*)"\)|$)') def __init__(self, bot, connection, event): """Initialize some stuff.""" @@ -42,7 +43,7 @@ class Countdown(Plugin): self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+list$', 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.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], self.new_reminder_regex, self.handle_new_reminder, -50) @@ -99,9 +100,16 @@ class Countdown(Plugin): recurring_period = match.group('recurring_period') recurring_until = match.group('recurring_until') text = match.group('text') + name = match.group('name') 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 if when_type == 'in': -- 2.43.2 From ca8798453c20138e4f7bb33e2d08091e856efb9f Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Apr 2021 12:57:12 -0500 Subject: [PATCH 16/18] fix Dice plugin init --- dice/ircplugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dice/ircplugin.py b/dice/ircplugin.py index 99c7904..55fcb39 100644 --- a/dice/ircplugin.py +++ b/dice/ircplugin.py @@ -14,11 +14,12 @@ logger = logging.getLogger(__name__) class Dice(Plugin): """Roll simple or complex dice strings.""" - def __init__(self): + def __init__(self, bot, connection, event): """Set up the plugin.""" - super(Dice, self).__init__() self.roller = DiceRoller() + super(Dice, self).__init__(bot, connection, event) + def start(self): """Set up the handlers.""" self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!roll\s+(.*)$', -- 2.43.2 From e64af1a0a18c75c5f18b71f2a0eb732972bc8b66 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Apr 2021 12:58:03 -0500 Subject: [PATCH 17/18] report on karma keys --- karma/ircplugin.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/karma/ircplugin.py b/karma/ircplugin.py index 1469c4e..7a97a35 100644 --- a/karma/ircplugin.py +++ b/karma/ircplugin.py @@ -6,6 +6,7 @@ import re import irc.client from django.conf import settings +from django.db.models import Count, Sum from ircbot.lib import Plugin 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('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'], r'^!karma\s+rank\s+(.*)$', 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('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_report) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_stats) @@ -102,6 +107,19 @@ class Karma(Plugin): except KarmaKey.DoesNotExist: 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): """Provide some karma reports.""" -- 2.43.2 From 2f4156ce26cf566ff5c50a90254c5044b5fe0455 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 24 Apr 2021 12:59:12 -0500 Subject: [PATCH 18/18] quote wttr.in requests --- weather/ircplugin.py | 2 +- weather/lib.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/weather/ircplugin.py b/weather/ircplugin.py index 8e5d746..405b4e4 100644 --- a/weather/ircplugin.py +++ b/weather/ircplugin.py @@ -33,7 +33,7 @@ class Weather(Plugin): if len(queryitems) <= 0: return - weather = weather_summary(queryitems[0]) + weather = weather_summary(query) weather_output = (f"Weather in {weather['location']}: {weather['current']['description']}. " f"{weather['current']['temp_F']}/{weather['current']['temp_C']}, " f"feels like {weather['current']['feels_like_temp_F']}/" diff --git a/weather/lib.py b/weather/lib.py index 6b6611f..9b858e6 100644 --- a/weather/lib.py +++ b/weather/lib.py @@ -2,6 +2,7 @@ """Get results of weather queries.""" import logging import requests +from urllib.parse import quote logger = logging.getLogger(__name__) @@ -9,7 +10,7 @@ logger = logging.getLogger(__name__) def query_wttr_in(query): """Hit the wttr.in JSON API with the provided 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() weather_info = response.json() -- 2.43.2