diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 33380b8..e8ee826 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -3,7 +3,7 @@ name: Linting on: [push] jobs: - build: + lint: runs-on: ubuntu-latest diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..85b29b4 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,26 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-20.04 + tools: + python: "3.8" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +# formats: +# - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt + diff --git a/README.md b/README.md deleted file mode 100644 index 78b859a..0000000 --- a/README.md +++ /dev/null @@ -1,53 +0,0 @@ -Yarnbot -======= - -Yarnbot is a Slack-bot that knows some yarn-working things, and can search -Ravelry for patterns, yarn, or user favorites. - - -Yarnbot understands: - * <7 character abbreviations - * Yarn weights - * Needle/Hook sizes (say 'US 10', '5mm', 'Crochet L', etc) - * Basic arithmetic expressions - * **weights**: List all yarn weights - * **ravelry favorites** <Ravelry Username> - * **ravelry favorites** <Ravelry Username> **tagged** <tag> - * **ravelry search** <search terms>: Search patterns - * **ravelry yarn** <search terms>: Search yarn - * **ravelry yarn similar to** <search terms>: Find similar yarn - * **info**: Yarnbot info - * **help**: Help text - -## Running - -Before anything else will work, you will need to create a bot in Slack. In your team's app management section, create a custom integration and add a bot configuration. The API Token available there is the SLACK_API_KEY referenced below. - -Use a script such as the provded example to set the various access keys and run the client. It will try to stay connected to slack until you tell it to `go to sleep`, which will cause it to disconnect gracefully. - -Yarnbot will create a log file named `yarnbot.log` in the current directory. By default, it logs at the INFO level, but that can be changed by altering the logging setup at the beginning of `yarnbot.py`. - -As it runs, it keeps track of users it has seen, and saves them in `known_users.pkl`. Please note that is a python pickle file. - -### Access Tokens - -Yarnbot requires a Slack API key, taken from the SLACK_API_KEY environment variable, as well as Ravelry OAuth1 keys, taken from RAV_ACC_KEY and RAV_SEC_KEY. - -## Screenshots - -Some typical yarnbot commands - -![yarnbot commands](https://imgur.com/1cPZXV1.png) - -Ravelry search results show short summaries of each pattern. Clicking the pattern will take you to Ravelry to see details. - -![yarnbot ravelry search](https://imgur.com/hx5Yo7x.png) - -You can also search for yarn... - -![yarnbot ravelry yarn](https://imgur.com/efIld1B.png) - -...and find similar yarn. - -![yarnbot ravelry yarn similar to](https://imgur.com/gfA9aOC.png) - diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..34d71f9 --- /dev/null +++ b/README.rst @@ -0,0 +1,61 @@ +Yarnbot +======= + +Yarnbot is a Slack-bot that knows some yarn-working things, and can search +Ravelry for patterns, yarn, or user favorites. + + +Yarnbot understands: + +* <7 character abbreviations +* Yarn weights +* Needle/Hook sizes (say 'US 10', '5mm', 'Crochet L', etc) +* Basic arithmetic expressions +* **weights**: List all yarn weights +* **ravelry favorites** +* **ravelry favorites** **tagged** +* **ravelry search** : Search patterns +* **ravelry yarn** : Search yarn +* **ravelry yarn similar to** : Find similar yarn +* **info**: Yarnbot info +* **help**: Help text + +Running +------- + +Before anything else will work, you will need to create a bot in Slack. In your team's app management section, create a custom integration and add a bot configuration. The API Token available there is the ``SLACK_API_KEY`` referenced below. + +Use a script such as the provded example to set the various access keys and run the client. It will try to stay connected to slack until you tell it to ``go to sleep``, which will cause it to disconnect gracefully. + +Yarnbot will create a log file named ``yarnbot.log`` in the current directory. By default, it logs at the INFO level, but that can be changed by altering the logging setup at the beginning of ``yarnbot.py``. + +As it runs, it keeps track of users it has seen, and saves them in ``known_users.pkl``. Please note that is a python pickle file. + +Access Tokens +^^^^^^^^^^^^^ + +Yarnbot requires a Slack API key, taken from the ``SLACK_API_KEY`` environment variable, as well as Ravelry OAuth1 keys, taken from ``RAV_ACC_KEY`` and ``RAV_SEC_KEY``. + +Screenshots +----------- + +Some typical yarnbot commands + +.. image:: https://imgur.com/1cPZXV1.png + :alt: yarnbot commands + +Ravelry search results show short summaries of each pattern. Clicking the pattern will take you to Ravelry to see details. + +.. image:: https://imgur.com/hx5Yo7x.png + :alt: yarnbot ravelry search + +You can also search for yarn... + +.. image:: https://imgur.com/efIld1B.png + :alt: yarnbot ravelry yarn + +...and find similar yarn. + +.. image:: https://imgur.com/gfA9aOC.png + :alt: yarnbot ravelry yarn similar to + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/patch.css b/docs/_static/patch.css new file mode 100644 index 0000000..003966e --- /dev/null +++ b/docs/_static/patch.css @@ -0,0 +1,13 @@ +dl:not(.docutils) dt{ + background:rgba(51, 195, 240, 0.1); + color:rgba(0, 174, 228, 0.9); +} + +a { + color: #00aee4; +} + + +.c-primary{ + color:#00aee4; +} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..dd45706 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,96 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, '..') +from yarnbot import __version__ + +# -- Project information ----------------------------------------------------- + +project = 'yarnbot' +copyright = '2022, Nigel Stepp' +author = 'Nigel Stepp' + +# The short X.Y version +version = __version__ + +# The full version, including alpha/beta/rc tags +release = __version__ + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', + 'sphinx.ext.todo', + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.githubpages', + 'sphinx.ext.napoleon' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'karma_sphinx_theme' +#import sphinx_readable_theme +#html_theme = 'readable' +#html_theme_path = [sphinx_readable_theme.get_html_theme_path()] + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), +} + +# -- Options for todo extension ---------------------------------------------- + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + +autodoc_mock_imports = ['slack_bolt'] + +def setup(app): + app.add_css_file('patch.css') + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..ca4387e --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +.. yarnbot documentation master file, created by + sphinx-quickstart on Sun Apr 3 01:16:59 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to yarnbot's documentation! +=================================== + +.. toctree:: + :maxdepth: 4 + :caption: Contents: + + yarnbot + +.. include:: ../README.rst + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..153be5e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..8c69217 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +requests +slack-bolt +karma-sphinx-theme diff --git a/docs/yarnbot.app.rst b/docs/yarnbot.app.rst new file mode 100644 index 0000000..fcfb08d --- /dev/null +++ b/docs/yarnbot.app.rst @@ -0,0 +1,7 @@ +yarnbot.app +=========== + +.. automodule:: yarnbot.app + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/yarnbot.conversations.rst b/docs/yarnbot.conversations.rst new file mode 100644 index 0000000..243cace --- /dev/null +++ b/docs/yarnbot.conversations.rst @@ -0,0 +1,7 @@ +yarnbot.conversations +===================== + +.. automodule:: yarnbot.conversations + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/yarnbot.data.rst b/docs/yarnbot.data.rst new file mode 100644 index 0000000..b1e394e --- /dev/null +++ b/docs/yarnbot.data.rst @@ -0,0 +1,7 @@ +yarnbot.data +============ + +.. automodule:: yarnbot.data + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/yarnbot.ravelry.rst b/docs/yarnbot.ravelry.rst new file mode 100644 index 0000000..71e97c5 --- /dev/null +++ b/docs/yarnbot.ravelry.rst @@ -0,0 +1,7 @@ +yarnbot.ravelry +=============== + +.. automodule:: yarnbot.ravelry + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/yarnbot.rst b/docs/yarnbot.rst new file mode 100644 index 0000000..c5cf0ff --- /dev/null +++ b/docs/yarnbot.rst @@ -0,0 +1,19 @@ +yarnbot +======= + +.. automodule:: yarnbot + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + yarnbot.app + yarnbot.conversations + yarnbot.data + yarnbot.ravelry + yarnbot.state diff --git a/docs/yarnbot.state.rst b/docs/yarnbot.state.rst new file mode 100644 index 0000000..5b6809f --- /dev/null +++ b/docs/yarnbot.state.rst @@ -0,0 +1,7 @@ +yarnbot.state +============= + +.. automodule:: yarnbot.state + :members: + :undoc-members: + :show-inheritance: diff --git a/requirements.txt b/requirements.txt index ef7a2f0..5aaa67d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests -slack_bolt +slack-bolt diff --git a/yarnbot/__init__.py b/yarnbot/__init__.py index 338df0a..798de03 100644 --- a/yarnbot/__init__.py +++ b/yarnbot/__init__.py @@ -1,2 +1,2 @@ -__version__ = '2.0.5' +__version__ = '2.0.6' diff --git a/yarnbot/app.py b/yarnbot/app.py index 3e650f6..d651365 100644 --- a/yarnbot/app.py +++ b/yarnbot/app.py @@ -1,24 +1,22 @@ -''' - Yarnbot - Slack bot for yarn working - - Copyright (C) 2017 Nigel D. Stepp - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - - Nigel Stepp -''' +# Yarnbot - Slack bot for yarn working +# +# Copyright (C) 2017 Nigel D. Stepp +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Nigel Stepp import os import sys @@ -27,13 +25,12 @@ import random import pickle import re -import json -import requests import logging -from collections import UserDict +from typing import Any, Dict, List, Optional -from slack_bolt import App +from slack_bolt import App, Say +from slack_sdk.web.client import WebClient from .conversations import EaseConversation from .state import app_state @@ -42,6 +39,8 @@ from .ravelry import (ravelry_api, ravelry_api_yarn, ravelry_pattern, ravelry_yarn, yarn_distance) +Event = Optional[Dict[str, Any]] + USERDB_FILENAME = 'known_users.pkl' app = App( @@ -49,11 +48,21 @@ signing_secret=os.environ.get("SLACK_SIGNING_SECRET") ) -def strip_punc(s): +def strip_punc(s: str) -> str: return str(s).translate(str.maketrans('','', string.punctuation)).strip() -def start_conversation(conv_name, user_id): +def start_conversation(conv_name: str, user_id: str) -> str: + ''' + Start a conversation for a user. + + Parameters: + conv_name (str): Conversation name. + user_id (str): Start a conversation with this user + + Returns: + str: The initial response + ''' conv = None @@ -65,17 +74,30 @@ def start_conversation(conv_name, user_id): msg = "Starting a conversation with yarnbot, just say 'cancel' to cancel\n" - (reply,terminal) = conv.step('') + + reply = conv.step('') + + if reply is not None: + (reply_msg,terminal) = reply if not terminal: app_state.conversations[user_id] = conv - if reply is not None: - msg += reply + msg += reply_msg return msg -def continue_conversation(user_id, msg): +def continue_conversation(user_id: str, msg: str) -> str: + ''' + Continue already started conversation. + + Parameters: + user_id (str): User we are talking to. + msg (str): The user's last response + + Returns: + str: Our next response + ''' if msg == 'cancel': del app_state.conversations[user_id] @@ -86,18 +108,40 @@ def continue_conversation(user_id, msg): conv = app_state.conversations[user_id] - (reply,terminal) = conv.step(msg) + reply = conv.step(msg) + + if reply is not None: + (reply_msg, terminal) = reply if reply is None or terminal: del app_state.conversations[user_id] - - return reply + + return reply_msg @app.event('message') -def proc_msg(event, say, client): +def proc_msg(event: Event, say: Say, client: WebClient) -> Optional[str]: + ''' + Process message events. + + Parameters: + event (Event): The event + say (Say): say handler + client (WebClient): web api client + + Returns: + Optional[str]: None unless it's time to quit, then 'quit' + + Note: + This still needs to be updated for Bolt; broken out into + individual message responders, and update return value + (at the moment, the 'quit' return does nothing) + ''' reply = None + if event is None: + return None + evt = event logging.debug(str(evt)) @@ -111,7 +155,7 @@ def proc_msg(event, say, client): if user_id == app_state.bot_user_id: return None - + app_state.message_count += 1 direct_msg = channel_id.startswith('D') @@ -154,7 +198,7 @@ def proc_msg(event, say, client): reply = start_conversation('ease',user_id) elif msg_stripped in data.acronyms: reply = "*{0}* is {1}".format(msg_text, data.acronyms[msg_stripped]['desc']) - if data.acronyms[msg_stripped]['url'] != None: + if data.acronyms[msg_stripped]['url'] is not None: reply += "\n<{0}|more info>".format(data.acronyms[msg_stripped]['url']) elif msg_stripped == 'help': reply = "I understand:\n" @@ -170,7 +214,10 @@ def proc_msg(event, say, client): reply += " *ravelry yarn* <search terms>: Search yarn\n" reply += " *ravelry yarn similar to* <search terms>: Find similar yarn\n" reply += " *info*: Yarnbot info\n" - reply += " *help*: This text" + reply += " *help*: This text\n\n" + reply += "I also know how to have convsersations about:" + reply += " *Ease*: Say anything to me with 'help' and 'ease' and I will help\n" + reply += " you calculate ease and gauge in different ways." elif msg_stripped in data.yarn_weights: yarn_info = data.yarn_weights[msg_stripped] reply = "*{0}* weight yarn is number {1}, typically {2} stitches per 4 in., {3}-ply, {4} wraps per inch".format(msg_stripped, @@ -250,7 +297,7 @@ def proc_msg(event, say, client): elif msg_stripped == 'runningconversations': try: - convs = [ u + ': ' + c.label for (u,c) in app_state.conversations.items() ] + convs = [ u + ': ' + c.name for (u,c) in app_state.conversations.items() ] reply = '\n'.join(convs) except: reply = "Couldn't list conversations" @@ -266,7 +313,7 @@ def proc_msg(event, say, client): try: rav_cmd = msg_stripped.split() - + if rav_cmd[1] in ['yarn','yarns'] and rav_cmd[2] in ['similar','comparable'] and rav_cmd[3] == 'to': if 'force' in rav_cmd: @@ -307,29 +354,29 @@ def proc_msg(event, say, client): return None # Sort results by yarn comparison - + similar_sorted = sorted(similar_results['yarns'], key=lambda x: yarn_distance(target_yarn, x)) attachments = [] for info in similar_sorted[0:5]: - + mach_wash = info['machine_washable'] if 'machine_washable' in info else None - if mach_wash == None or not mach_wash: + if mach_wash is None or not mach_wash: mach_wash = 'No' else: mach_wash = 'Yes' organic = info['organic'] if 'organic' in info else None - if organic == None or not organic: + if organic is None or not organic: organic = 'No' else: organic = 'Yes' description = info['yarn_weight']['name'] - if info['gauge_divisor'] != None: + if info['gauge_divisor'] is not None: gauge_range = [] - if info['min_gauge'] != None: + if info['min_gauge'] is not None: gauge_range.append(str(info['min_gauge'])) - if info['max_gauge'] != None: + if info['max_gauge'] is not None: gauge_range.append(str(info['max_gauge'])) description += ', {0} sts = {1} in'.format(' to '.join(gauge_range), info['gauge_divisor']) @@ -353,18 +400,18 @@ def proc_msg(event, say, client): attachments.append( attachment ) msg = u"Yarn most similar to {0} {1} {2}-weight ({3})".format(target_yarn['yarn_company_name'],target_yarn['name'],target_weight,','.join(target_fibers)) - attach_json = json.dumps( attachments ) + #attach_json = json.dumps( attachments ) - send_msg(client, channel_id, msg, attach_json) + send_msg(client, channel_id, msg, attachments) return None elif rav_cmd[1] == 'yarn': - (msg, attach) = ravelry_yarn(rav_cmd[2:]) + (yarn_msg, attach) = ravelry_yarn(rav_cmd[2:]) - if msg != None: - send_msg(client, channel_id, msg, attach) + if yarn_msg is not None: + send_msg(client, channel_id, yarn_msg, attach) else: say(':disappointed:') @@ -372,11 +419,11 @@ def proc_msg(event, say, client): elif rav_cmd[1] == 'search': - - (msg, attach) = ravelry_pattern(rav_cmd[2:]) - if msg != None: - send_msg(client, channel_id, msg, attach) + (rav_search_msg, attach) = ravelry_pattern(rav_cmd[2:]) + + if rav_search_msg is not None: + send_msg(client, channel_id, rav_search_msg, attach) else: say(':disappointed:') @@ -393,7 +440,7 @@ def proc_msg(event, say, client): else: query = " ".join(rav_cmd[3:]) parms.update( {'query': query} ) - msg += u', containing {0}'.format(query) + msg += u', containing {0}'.format(query) rav_result = ravelry_api('/people/{0}/favorites/list.json'.format(fav_user), parms) if rav_result['paginator']['results'] == 0: @@ -402,7 +449,7 @@ def proc_msg(event, say, client): attachments = [] for fav in rav_result['favorites']: - + attachment = dict() attachment['fallback'] = fav['favorited']['name'] attachment['color'] = '#36a64f' @@ -413,25 +460,28 @@ def proc_msg(event, say, client): try: attachment['image_url'] = fav['favorited']['first_photo']['square_url'] except: - logging.warn(u'Ravelry result with missing info: {0}'.format(fav)) + logging.warning(u'Ravelry result with missing info: {0}'.format(fav)) try: attachment['author_name'] = fav['favorited']['designer']['name'] except: - logging.warn(u'Ravelry result with missing info: {0}'.format(fav)) + logging.warning(u'Ravelry result with missing info: {0}'.format(fav)) attachments.append( attachment ) - attach_json = json.dumps( attachments ) - client.chat_postMessage(channel=channel_id, as_user=True, text=msg, - attachments=attach_json) + attachments=attachments) return None except Exception as e: reply = 'Ravelry command error' - logging.warn('Ravelry error line {0}: {1}'.format(sys.exc_info()[2].tb_lineno,e) ) + tb_info = sys.exc_info()[2] + if tb_info is not None: + lineno = str(tb_info.tb_lineno) + else: + lineno = 'unknown' + logging.warning('Ravelry error line {0}: {1}'.format(lineno,e) ) elif msg_stripped == 'hello' or msg_stripped == 'hi' or msg_stripped.startswith('hello ') or msg_stripped.startswith('hi '): reply_ind = random.choice( range(len(data.greetings)) ) @@ -442,7 +492,7 @@ def proc_msg(event, say, client): reply = "I'm yarnbot {0}, started on {1}.\n".format(__version__, time.ctime(app_state.start_time)) reply += "I've processed {0} messages ({1} unknown).".format(app_state.message_count, app_state.unknown_count) elif msg_text == 'go to sleep': - logging.warn('Got kill message') + logging.warning('Got kill message') say("Ok, bye.") return 'quit' elif ('love' in msg_lower) or ('cute' in msg_lower) or ('best' in msg_lower) or ('awesome' in msg_lower) or ('great' in msg_lower): @@ -457,14 +507,18 @@ def proc_msg(event, say, client): reply = data.unknown_replies[reply_ind] app_state.unknown_count += 1 - say(reply) + if reply is None: + say("Somehow I don't know what to say :thinking_face:") + else: + say(reply) return None -def send_msg(client, channel_id, msg, attach=None): +def send_msg(client: WebClient, channel_id: str, msg: str, attach: Optional[List[dict]]=None): + '''Send a message using the chat postMessage API.''' #logging.info('Sending a message to {0}: {1}'.format(channel_id, msg)) - if attach == None: + if attach is None: client.chat_postMessage(channel=channel_id, as_user=True, text=msg) @@ -474,38 +528,40 @@ def send_msg(client, channel_id, msg, attach=None): text=msg, attachments=attach) -def send_direct_msg(client, user_id, msg): +def send_direct_msg(client: WebClient, user_id: str, msg: str): + '''Send a direct message to a user.''' #send_msg(user_id, msg) #return - im = client.im.open(user_id) + im = client.im_open(user=user_id) try: if not 'ok' in im or not im['ok']: - logging.warn('im open failed') + logging.warning('im open failed') return except: - logging.warn('bad im return value: {0}'.format(im)) + logging.warning('bad im return value: {0}'.format(im)) return - + #logging.info('opened IM {0}'.format(im)) - + im_channel = im['channel']['id'] send_msg(client, im_channel, msg) -def welcome_msg(client, user_id, from_user_id=None): +def welcome_msg(client: WebClient, user_id: str, from_user_id: Optional[str]=None): + '''Send a welcome message to a user''' logging.info('Sending welcome message from {0} to {1}'.format(from_user_id,user_id)) - user_info = client.user_info(user_id) + user_info = client.users_info(user=user_id) try: if not 'ok' in user_info or not user_info['ok']: - logging.warn('Error getting user info') + logging.warning('Error getting user info') return except: - logging.warn("user_info wasn't a dict: {0}".format(user_info)) + logging.warning("user_info wasn't a dict: {0}".format(user_info)) return #logging.info('Got user info {0}'.format(user_info)) @@ -526,13 +582,18 @@ def welcome_msg(client, user_id, from_user_id=None): send_direct_msg(client, user_id, welcome) @app.event('team_join') -def welcome_user(event, client): +def welcome_user(event: Event, client: WebClient): + '''Look at team joins and determine if a welcome is in order''' + + if event is None: + logging.warning('Got null team join event') + return user = event['user'] if 'is_bot' in user and user['is_bot']: return - + if user['id'] not in app_state.known_users: app_state.known_users.append(user['id']) save_userdb() @@ -541,6 +602,7 @@ def welcome_user(event, client): def load_userdb(): + '''Load the known users list''' try: userdb_file = open(USERDB_FILENAME, 'rb') @@ -554,10 +616,11 @@ def load_userdb(): app_state.nown_users = pickle.load(userdb_file) except: logging.error('Malformed userdb file') - + userdb_file.close() def save_userdb(): + '''Save the known users list''' try: userdb_file = open(USERDB_FILENAME, 'wb') @@ -566,14 +629,15 @@ def save_userdb(): return pickle.dump(app_state.known_users, userdb_file) - + userdb_file.close() -def main(app): +def main(app: App): + '''Start yarnbot.''' logging.basicConfig(filename='yarnbot.log',level=logging.INFO) - + app_state.start_time = time.time() auth_info = app.client.auth_test() diff --git a/yarnbot/conversations.py b/yarnbot/conversations.py index eebbb52..9c7f8a2 100644 --- a/yarnbot/conversations.py +++ b/yarnbot/conversations.py @@ -1,24 +1,22 @@ -''' - Yarnbot - Slack bot for yarn working - - Copyright (C) 2017 Nigel D. Stepp - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - - Nigel Stepp -''' +# Yarnbot - Slack bot for yarn working +# +# Copyright (C) 2017 Nigel D. Stepp +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Nigel Stepp import re from typing import (Any, Callable, Dict, List, Optional, @@ -33,19 +31,24 @@ # Accept functions def accept_string(s: str) -> AcceptFn: + '''Accept function for a single string''' return lambda m: s.lower() in m.lower() def accept_any_strings(*ss: str) -> AcceptFn: + '''Accept function for some number of strings (logical or)''' return lambda m: any(s.lower() in m.lower() for s in ss) def accept_all_strings(*ss: str) -> AcceptFn: + '''Accept function for some number of strings (logical and)''' return lambda m: all(s.lower() in m.lower() for s in ss) def accept() -> AcceptFn: + '''Always accepts''' return lambda _: True # Extract functions def extract_integer(_data: Dict[str,T], msg: str) -> Optional[int]: + '''Extraction function for integers''' result = re.search('(-?[0-9]+)', msg) if result is not None: @@ -54,6 +57,7 @@ def extract_integer(_data: Dict[str,T], msg: str) -> Optional[int]: return None def extract_numeric(_data: Dict[str,T], msg: str) -> Optional[float]: + '''Extraction function for any numeric''' result = re.search(r'(-?[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)', msg) if result is not None: @@ -189,13 +193,13 @@ class Conversation: To define the state machine for a conversation, provide a dictionary of states in `self.state` containing State objects, with one special state called 'init'. State transitions are provided using the `State.add_trans` method. See `State` - for more details. For instance: + for more details. For instance:: - self.states = {'init':State('init','Welcome to this state machine'}, - 'A',State('A','This is state A, enter a number',{'num':extract_numeric})} - 'end',State('end','You said {num}. Bye.')} - self.states['init'].add_trans(self.states['A'],accept()) - self.states['A'].add_trans(self.states['end'],accept()) + self.states = {'init':State('init','Welcome to this state machine'), + 'A':State('A','This is state A, enter a number',{'num':extract_numeric}), + 'end':State('end','You said {num}. Bye.')} + self.states['init'].add_trans(self.states['A'],accept()) + self.states['A'].add_trans(self.states['end'],accept()) Running this state machine will first print the welcome message, transition to 'A', extract a number from response text, then transition to end. When finished, @@ -204,6 +208,10 @@ class Conversation: ''' def __init__(self, name: str): + ''' + Parameters: + name (str): Conversation name, to be used as a label. + ''' self.name = name self.states = {'init': State('init', 'Subclass to have a real conversation')} self.current_state: Optional[State] = self.states['init'] @@ -279,6 +287,9 @@ def calc_ease(data: Dict[str,float], _msg: str) -> float: return float(data['stitches'])/(data['gauge']/4.) - data['meas'] def __init__(self): + ''' + Create an Ease conversation. + ''' super().__init__('ease') self.states = { diff --git a/yarnbot/data.py b/yarnbot/data.py index b548bf4..29d19df 100644 --- a/yarnbot/data.py +++ b/yarnbot/data.py @@ -1,6 +1,8 @@ -greetings = ["Hi!", "Hello there!", "Hello!", "Greetings, fair yarn worker!"] +from typing import Dict,List,TypedDict,Optional -unknown_replies = ["I'm not sure what you mean.", +greetings: List[str] = ["Hi!", "Hello there!", "Hello!", "Greetings, fair yarn worker!"] + +unknown_replies: List[str] = ["I'm not sure what you mean.", "I'm sorry, I don't know that.", "I'm just a sheep.", "I know a lot about other things that aren't that thing.", @@ -9,20 +11,28 @@ ":confused:", ":persevere:"] -acronyms = { +AcronymData = TypedDict('AcronymData', {'desc':str, 'url':Optional[str]}) + +acronyms: Dict[str,AcronymData] = { 'pat': {'desc': 'pattern', 'url': None}, 'pats': {'desc': 'patterns', 'url': None}, 'patt': {'desc': 'pattern', 'url': None}, 'pm': {'desc': 'place marker', 'url': None}, 'pop': {'desc': 'popcorn', 'url': None}, - 'p2tog': {'desc': 'purl 2 stitches together', 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/decreases/k2tog'}, - 'psso': {'desc': 'pass slipped stitch over', 'url': 'http://newstitchaday.com/pass-slipped-stitch-over-decrease/'}, + 'p2tog': { + 'desc': 'purl 2 stitches together', + 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/decreases/k2tog'}, + 'psso': { + 'desc': 'pass slipped stitch over', + 'url': 'http://newstitchaday.com/pass-slipped-stitch-over-decrease/'}, 'pwise': {'desc': 'purlwise', 'url': None}, 'beg': {'desc': 'begin/beginning', 'url': None}, 'rem': {'desc': 'remain/remaining', 'url': None}, 'bet': {'desc': 'between', 'url': None}, 'rep': {'desc': 'repeat(s)', 'url': None}, - 'bo': {'desc': 'bind off', 'url': 'http://www.vogueknitting.com/pattern_help/how-to/learn_to_knit/binding_off'}, + 'bo': { + 'desc': 'bind off', + 'url': 'http://www.vogueknitting.com/pattern_help/how-to/learn_to_knit/binding_off'}, 'ca': {'desc': 'color A', 'url': None}, 'rh': {'desc': 'right hand', 'url': None}, 'cb': {'desc': 'color B ', 'url': None}, @@ -32,12 +42,19 @@ 'rs': {'desc': 'right side ', 'url': None}, 'sk': {'desc': 'skip', 'url': None}, 'cn': {'desc': 'cable needle', 'url': None}, - 'skp': {'desc': 'slip, knit, pass stitch over; one stitch decreased', 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/decreases/skp'}, - 'co': {'desc': 'cast on', 'url': 'http://www.vogueknitting.com/pattern_help/how-to/learn_to_knit/first_stitches'}, - 'sk2p': {'desc': 'slip 1, knit 2 together, pass slip stitch over the knit 2 together; 2 stitches have been decreased', 'url': 'http://newstitchaday.com/slip-knit-two-pass-double-decrease/'}, + 'skp': { + 'desc': 'slip, knit, pass stitch over; one stitch decreased', + 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/decreases/skp'}, + 'co': { + 'desc': 'cast on', + 'url': 'http://www.vogueknitting.com/pattern_help/how-to/learn_to_knit/first_stitches'}, + 'sk2p': { + 'desc': 'slip 1, knit 2 together, pass slip stitch over the knit 2 together; 2 stitches have been decreased', + 'url': 'http://newstitchaday.com/slip-knit-two-pass-double-decrease/'}, 'cont': {'desc': 'continue ', 'url': None}, - 'sl': {'desc': 'slip ', 'url': None}, - 'dec': {'desc': 'decrease/decreases/decreasing', 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/decreases'}, + 'dec': { + 'desc': 'decrease/decreases/decreasing', + 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/decreases'}, 'sl1k': {'desc': 'slip 1 knitwise', 'url': 'https://www.youtube.com/watch?v=_Wh8kmdqfcw'}, 'dpn': {'desc': 'double pointed needle(s)', 'url': None}, 'sl1p': {'desc': 'slip 1 purlwise', 'url': 'https://www.youtube.com/watch?v=6EX_KbVknP0'}, @@ -45,15 +62,22 @@ 'sl': {'desc': 'st slip stitch(es)', 'url': None}, 'foll': {'desc': 'follow/follows/following', 'url': None}, 'ss': {'desc': 'slip stitch', 'url': None}, - 'ssk': {'desc': 'slip, slip, knit these 2 stiches together; decrease', 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/decreases/ssk'}, - 'inc': {'desc': 'increase/increases/increasing', 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/increases'}, - 'sssk': {'desc': 'slip, slip, slip, knit 3 stitches together', 'url': 'http://newstitchaday.com/slip-slip-slip-knit-double-decrease/'}, + 'ssk': { + 'desc': 'slip, slip, knit these 2 stiches together; decrease', + 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/decreases/ssk'}, + 'inc': { + 'desc': 'increase/increases/increasing', + 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/increases'}, + 'sssk': { + 'desc': 'slip, slip, slip, knit 3 stitches together', + 'url': 'http://newstitchaday.com/slip-slip-slip-knit-double-decrease/'}, 'k': {'desc': 'knit', 'url': 'http://www.vogueknitting.com/pattern_help/how-to/learn_to_knit/the_knit_stitch'}, 'kfb': {'desc': 'knit front and back', 'url': 'http://newstitchaday.com/knit-front-and-back-increase-kfb/'}, 'st': {'desc': 'stitch', 'url': None}, 'sts': {'desc': 'stitches', 'url': None}, - 'k2tog': {'desc': 'knit 2 stitches together', 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/decreases/k2tog'}, - 'st': {'desc': 'st stockinette stitch/stocking stitch', 'url': None}, + 'k2tog': { + 'desc': 'knit 2 stitches together', + 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/decreases/k2tog'}, 'kwise': {'desc': 'knitwise', 'url': None}, 'tbl': {'desc': 'through back loop', 'url': 'http://newstitchaday.com/k-tbl-knit-through-back-loop/'}, 'lh': {'desc': 'left hand', 'url': None}, @@ -62,9 +86,15 @@ 'lps': {'desc': 'loops', 'url': None}, 'ws': {'desc': 'wrong side', 'url': None}, 'wyib': {'desc': 'with yarn in back', 'url': None}, - 'm1': {'desc': 'make one stitch', 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/increases/make_one_increase'}, - 'm1r': {'desc': 'make one stitch right', 'url': 'http://newstitchaday.com/m1r-make-one-right-increase-knitting/'}, - 'm1l': {'desc': 'make one stitch left', 'url': 'http://newstitchaday.com/m1l-make-one-left-increase-knitting/'}, + 'm1': { + 'desc': 'make one stitch', + 'url': 'http://www.vogueknitting.com/pattern_help/how-to/beyond_the_basics/increases/make_one_increase'}, + 'm1r': { + 'desc': 'make one stitch right', + 'url': 'http://newstitchaday.com/m1r-make-one-right-increase-knitting/'}, + 'm1l': { + 'desc': 'make one stitch left', + 'url': 'http://newstitchaday.com/m1l-make-one-left-increase-knitting/'}, 'wyif': {'desc': 'with yarn in front', 'url': None}, 'mc': {'desc': 'main color', 'url': None}, 'yfwd': {'desc': 'yarn forward', 'url': None}, @@ -74,7 +104,7 @@ 'p': {'desc': 'purl', 'url': 'http://www.vogueknitting.com/pattern_help/how-to/learn_to_knit/the_purl_stitch'}, 'yon': {'desc': 'yarn over needle', 'url': None}} -yarn_weights = { +yarn_weights: Dict[str,dict] = { 'cobweb': {'ply': 1, 'wpi': '??', 'gauge': '??', 'number': 0}, 'lace': {'ply': 2, 'wpi': '??', 'gauge': '32-34', 'number': 0}, 'light fingering': {'ply': 3, 'wpi': '??', 'gauge': '32', 'number': 0}, @@ -88,7 +118,7 @@ 'jumbo': {'ply': '??', 'wpi': '0-4', 'gauge': '0-6', 'number': 7} } -yarn_fibers = [ +yarn_fibers: List[str] = [ 'acrylic', 'alpaca', 'angora', @@ -117,12 +147,11 @@ 'yak' ] -needles_by_us = { +needles_by_us: Dict[str,dict] = { '0': {'metric': '2.0', 'uk': '14', 'crochet': '-'}, '1': {'metric': '2.25', 'uk': '13', 'crochet': 'B'}, '2': {'metric': '2.75', 'uk': '12', 'crochet': 'C'}, - '3': {'metric': '3.0', 'uk': '11', 'crochet': '-'}, - '3': {'metric': '3.25', 'uk': '10', 'crochet': 'D'}, + '3': {'metric': '3.0/3.25', 'uk': '10/11', 'crochet': 'D'}, '4': {'metric': '3.5', 'uk': '9', 'crochet': 'E'}, '5': {'metric': '3.75', 'uk': '9', 'crochet': 'F'}, '6': {'metric': '4.0', 'uk': '8', 'crochet': 'G'}, @@ -140,7 +169,7 @@ '35': {'metric': '20.0', 'uk': '-', 'crochet': '-'}, '50': {'metric': '50.0', 'uk': '-', 'crochet': '-'}, } -needles_by_metric = { +needles_by_metric: Dict[str,dict] = { '2.00': {'us': '0', 'uk': '14', 'crochet': '-'}, '2.25': {'us': '1', 'uk': '13', 'crochet': 'B'}, '2.50': {'us': '-', 'uk': '12', 'crochet': '-'}, @@ -165,7 +194,7 @@ '20.00': {'us': '35', 'uk': '-', 'crochet': '-'}, '50.00': {'us': '50', 'uk': '-', 'crochet': '-'}, } -needles_by_crochet = { +needles_by_crochet: Dict[str,dict] = { 'B': {'us': '1', 'uk': '13', 'metric': '2.25'}, 'C': {'us': '2', 'uk': '12', 'metric': '2.75'}, 'D': {'us': '3', 'uk': '10', 'metric': '3.25'}, @@ -178,15 +207,13 @@ 'K': {'us': '10.5', 'uk': '3', 'metric': '6.5'}, 'L': {'us': '11', 'uk': '0', 'metric': '8.0'} } -needles_by_uk = { +needles_by_uk: Dict[str,dict] = { '14': {'us': '0', 'crochet': '-', 'metric': '2.0'}, '13': {'us': '1', 'crochet': 'B', 'metric': '2.25'}, - '12': {'us': '-', 'crochet': '-', 'metric': '2.5'}, - '12': {'us': '2', 'crochet': 'C', 'metric': '2.75'}, + '12': {'us': '2', 'crochet': 'C', 'metric': '2.5/2.75'}, '11': {'us': '3', 'crochet': '-', 'metric': '3.0'}, '10': {'us': '3', 'crochet': 'D', 'metric': '3.25'}, - '9': {'us': '4', 'crochet': 'E', 'metric': '3.5'}, - '9': {'us': '5', 'crochet': 'F', 'metric': '3.75'}, + '9': {'us': '4/5', 'crochet': 'E/F', 'metric': '3.5/3.75'}, '8': {'us': '6', 'crochet': 'G', 'metric': '4.0'}, '7': {'us': '7', 'crochet': '-', 'metric': '4.5'}, '6': {'us': '8', 'crochet': 'H', 'metric': '5.0'}, @@ -200,7 +227,7 @@ '000': {'us': '15', 'crochet': '-', 'metric': '10.0'}, } -jokes = [ +jokes: List[str] = [ "Why did the pig farmer give up knitting?\n\nHe didn't want to cast his purls before swine.", "Did you hear what happened to the cat who ate a ball of yarn?\n\nShe had mittens.", "So this lady was driving down the highway crocheting as she drove. A highway patrolman noticed her and began pursuit. He drove up next to her car, rolled down his window and said, 'Pull over!' She replied, 'No, it's a scarf!'", @@ -208,4 +235,3 @@ "Why are Christmas trees bad at knitting?\n\nBecause they keep dropping their needles", "What did the knitted cap say to the afghan?\n\nYou stay here, I'll go on a head."] - diff --git a/yarnbot/ravelry.py b/yarnbot/ravelry.py index a6f3ca3..04c4b17 100644 --- a/yarnbot/ravelry.py +++ b/yarnbot/ravelry.py @@ -1,13 +1,18 @@ import os -import json import requests +from requests.utils import quote # type: ignore + +from typing import Any, Dict, List, Optional, Tuple from . import data +# TODO replace with ravelry schemas +Json = Dict[str, Any] + RAV_ACC_KEY = os.environ.get('RAV_ACC_KEY') RAV_SEC_KEY = os.environ.get('RAV_SEC_KEY') -def yarn_distance(yarn1, yarn2): +def yarn_distance(yarn1: Json, yarn2: Json) -> float: mass1 = yarn1['grams'] mass2 = yarn2['grams'] @@ -24,34 +29,30 @@ def yarn_distance(yarn1, yarn2): # Density - if mass1 != None and yards1 != None: + density1: Optional[float] = None + if mass1 is not None and yards1 is not None: density1 = float(mass1)/float(yards1) - else: - density1 = None - if mass2 != None and yards2 != None: + density2: Optional[float] = None + if mass2 is not None and yards2 is not None: density2 = float(mass2)/float(yards2) - else: - density2 = None # Gauge - if gauge_div1 != None and min_gauge1 != None: + min_gauge_norm1: Optional[float] = None + if gauge_div1 is not None and min_gauge1 is not None: min_gauge_norm1 = min_gauge1/gauge_div1 - else: - min_gauge_norm1 = None - if gauge_div1 != None and max_gauge1 != None: + + max_gauge_norm1: Optional[float] = None + if gauge_div1 is not None and max_gauge1 is not None: max_gauge_norm1 = max_gauge1/gauge_div1 - else: - max_gauge_norm1 = None - if gauge_div2 != None and min_gauge2 != None: + min_gauge_norm2: Optional[float] = None + if gauge_div2 is not None and min_gauge2 is not None: min_gauge_norm2 = min_gauge2/gauge_div2 - else: - min_gauge_norm2 = None - if gauge_div2 != None and max_gauge2 != None: + + max_gauge_norm2: Optional[float] = None + if gauge_div2 is not None and max_gauge2 is not None: max_gauge_norm2 = max_gauge2/gauge_div2 - else: - max_gauge_norm2 = None # Distance measures @@ -59,34 +60,34 @@ def yarn_distance(yarn1, yarn2): dist = lambda x,y: float(abs(x-y))/(x+y) d = 0. - if density1 != None and density2 != None: + if density1 is not None and density2 is not None: d += dist(density1,density2) else: d += 0.5 - if wpi1 != None and wpi2 != None: + if wpi1 is not None and wpi2 is not None: d += dist(wpi1,wpi2) else: d += 0.5 - if min_gauge_norm1 != None and min_gauge_norm2 != None: + if min_gauge_norm1 is not None and min_gauge_norm2 is not None: d += dist(min_gauge_norm1,min_gauge_norm1) else: d += 0.5 - if max_gauge_norm1 != None and max_gauge_norm2 != None: + if max_gauge_norm1 is not None and max_gauge_norm2 is not None: d += dist(max_gauge_norm1,max_gauge_norm1) else: d += 0.5 return d -def ravelry_api(api_call, parms): +def ravelry_api(api_call: str, parms: Json) -> Json: req = requests.get('https://api.ravelry.com/' + api_call, auth=(RAV_ACC_KEY,RAV_SEC_KEY), params=parms) return req.json() -def ravelry_api_yarn(rav_cmd, page_size=5): +def ravelry_api_yarn(rav_cmd: List[str], page_size: int = 5) -> Tuple[Json,str,Dict[str,str]]: filtered_words = ['or','and','pattern', @@ -98,7 +99,7 @@ def ravelry_api_yarn(rav_cmd, page_size=5): parms = {'photo':'yes', 'page_size':str(page_size), 'sort':'projects'} msg = u'Yarn search results for:' - + filter_weight = [] for w in data.yarn_weights.keys(): s = w.lower().replace(' ','-') @@ -125,20 +126,20 @@ def ravelry_api_yarn(rav_cmd, page_size=5): msg += u' containing "{0}"'.format(' '.join(rav_cmd)) rav_result = ravelry_api('/yarns/search.json', parms) - + return (rav_result, msg, parms) -def ravelry_yarn(rav_cmd): +def ravelry_yarn(rav_cmd: List[str]) -> Tuple[Optional[str],Optional[List[Json]]]: - (rav_result, msg, parms) = ravelry_api_yarn(rav_cmd) + (rav_result, msg, _parms) = ravelry_api_yarn(rav_cmd) if rav_result['paginator']['results'] == 0: return (None,None) attachments = [] for info in rav_result['yarns']: - + mach_wash = info['machine_washable'] if 'machine_washable' in info else None if mach_wash == None or not mach_wash: mach_wash = 'No' @@ -156,11 +157,11 @@ def ravelry_yarn(rav_cmd): else: description = u'roving?' - if info['gauge_divisor'] != None: + if info['gauge_divisor'] is not None: gauge_range = [] - if info['min_gauge'] != None: + if info['min_gauge'] is not None: gauge_range.append(str(info['min_gauge'])) - if info['max_gauge'] != None: + if info['max_gauge'] is not None: gauge_range.append(str(info['max_gauge'])) description += u', {0} sts = {1} in'.format(' to '.join(gauge_range), info['gauge_divisor']) @@ -183,11 +184,9 @@ def ravelry_yarn(rav_cmd): attachments.append( attachment ) - attach_json = json.dumps( attachments ) - - return (msg, attach_json) + return (msg, attachments) -def ravelry_pattern(rav_cmd): +def ravelry_pattern(rav_cmd: List[str]) -> Tuple[Optional[str],Optional[List[Json]]]: filtered_words = ['or','and','pattern', 'patterns','with','using','yarn', @@ -198,7 +197,7 @@ def ravelry_pattern(rav_cmd): parms = {'photo':'yes', 'page_size':'5', 'sort':'best'} msg = u'Pattern search results for:' - + filter_free = 'free' in rav_cmd if filter_free: rav_cmd.remove('free') @@ -215,7 +214,7 @@ def ravelry_pattern(rav_cmd): if 'crochet' in rav_cmd: filter_craft.append('crochet') rav_cmd.remove('crochet') - + filter_weight = [] for w in data.yarn_weights.keys(): s = w.lower().replace(' ','-') @@ -236,7 +235,7 @@ def ravelry_pattern(rav_cmd): parms.update({'query':' '.join(rav_cmd)}) msg += u' containing "{0}"'.format(' '.join(rav_cmd)) - search_query = '&'.join([ k + '=' + requests.utils.quote(v) for (k,v) in parms.items() if k != 'page_size']) + search_query = '&'.join([ k + '=' + quote(v) for (k,v) in parms.items() if k != 'page_size']) search_url = 'http://www.ravelry.com/patterns/search#' + search_query msg += u'\n(<{0}|search on ravelry>)'.format(search_url) @@ -249,7 +248,7 @@ def ravelry_pattern(rav_cmd): attachments = [] for pat in rav_result['patterns']: - + attachment = dict() attachment['fallback'] = pat['name'] attachment['color'] = '#36a64f' @@ -261,8 +260,6 @@ def ravelry_pattern(rav_cmd): attachments.append( attachment ) - attach_json = json.dumps( attachments ) - - return (msg,attach_json) + return (msg,attachments) diff --git a/yarnbot/state.py b/yarnbot/state.py index 701d584..7787fcb 100644 --- a/yarnbot/state.py +++ b/yarnbot/state.py @@ -5,6 +5,11 @@ @dataclass class AppState: + ''' + Keeps application state, to be easily made available + at the global scope. + ''' + known_users: List[str] = field(default_factory=list) conversations: Dict[str,Conversation] = field(default_factory=dict) message_count: int = 0