Browse Source

More work on wikitools, now with improved API queries and login.

* Site's api_query() is much smarter. It uses a custom urllib2 URL opener with cookie support and catches URLErrors, raising its own brand new exception (SiteAPIError) when something is wrong.
* The opener now uses a custom User-Agent, which is a constant in wiki.tools.constants.
* Site instances automatically login via _login(), which accepts a username and password (provided via config by get_site()) and uses two api_query()s and stores the login data as cookies in self._cookiejar. Login data is not preserved between bot restarts yet. Login errors, e.g. a bad password or username, raise the new LoginError.
* Site's get_user()'s username argument is now optional. If left blank, will return the current logged-in user, provided by an API query.
* Misc cleanup throughout.
tags/v0.1^2
Ben Kurtovic 13 years ago
parent
commit
74ddc5b702
4 changed files with 97 additions and 13 deletions
  1. +9
    -1
      wiki/tools/constants.py
  2. +8
    -0
      wiki/tools/exceptions.py
  3. +14
    -4
      wiki/tools/functions.py
  4. +66
    -8
      wiki/tools/site.py

+ 9
- 1
wiki/tools/constants.py View File

@@ -3,9 +3,17 @@
""" """
EarwigBot's Wiki Toolset: Constants EarwigBot's Wiki Toolset: Constants


This module defines some useful constants.
This module defines some useful constants, such as default namespace IDs for
easy lookup and our user agent.

Import with `from wiki.tools.constants import *`.
""" """


import platform

# User agent when making API queries
USER_AGENT = "EarwigBot/0.1-dev (Python/{0}; https://github.com/earwig/earwigbot)".format(platform.python_version())

# Default namespace IDs # Default namespace IDs
NS_MAIN = 0 NS_MAIN = 0
NS_TALK = 1 NS_TALK = 1


+ 8
- 0
wiki/tools/exceptions.py View File

@@ -13,6 +13,14 @@ class SiteNotFoundError(WikiToolsetError):
"""A site matching the args given to get_site() could not be found in the """A site matching the args given to get_site() could not be found in the
config file.""" config file."""


class SiteAPIError(WikiToolsetError):
"""We couldn't connect to a site's API, perhaps because the server doesn't
exist, our URL is wrong, or they're having temporary problems."""

class LoginError(WikiToolsetError):
"""An error occured while trying to login. Perhaps the username/password is
incorrect."""

class NamespaceNotFoundError(WikiToolsetError): class NamespaceNotFoundError(WikiToolsetError):
"""A requested namespace name or namespace ID does not exist.""" """A requested namespace name or namespace ID does not exist."""




+ 14
- 4
wiki/tools/functions.py View File

@@ -67,10 +67,14 @@ def _get_site_object_from_dict(name, d):
namespaces = d["namespaces"] namespaces = d["namespaces"]
except KeyError: except KeyError:
namespaces = None namespaces = None
try:
login = (config.wiki["username"], config.wiki["password"])
except KeyError:
login = (None, None)


return Site(name=name, project=project, lang=lang, base_url=base_url, return Site(name=name, project=project, lang=lang, base_url=base_url,
article_path=article_path, script_path=script_path, article_path=article_path, script_path=script_path,
sql=(sql_server, sql_db), namespaces=namespaces)
sql=(sql_server, sql_db), namespaces=namespaces, login=login)


def get_site(name=None, project=None, lang=None): def get_site(name=None, project=None, lang=None):
"""Returns a Site instance based on information from our config file. """Returns a Site instance based on information from our config file.
@@ -86,6 +90,10 @@ def get_site(name=None, project=None, lang=None):
member of config.wiki["sites"], `s`, for which s["project"] == project and member of config.wiki["sites"], `s`, for which s["project"] == project and
s["lang"] == lang. s["lang"] == lang.


We will attempt to login to the site automatically
using config.wiki["username"] and config.wiki["password"] if both are
defined.

Specifying a project without a lang or a lang without a project will raise Specifying a project without a lang or a lang without a project will raise
TypeError. If all three args are specified, `name` will be first tried, TypeError. If all three args are specified, `name` will be first tried,
then `project` and `lang`. If, with any number of args, a site cannot be then `project` and `lang`. If, with any number of args, a site cannot be
@@ -96,7 +104,8 @@ def get_site(name=None, project=None, lang=None):
_load_config() _load_config()


# someone specified a project without a lang (or a lang without a project)! # someone specified a project without a lang (or a lang without a project)!
if (project is None and lang is not None) or (project is not None and lang is None):
if (project is None and lang is not None) or (project is not None and
lang is None):
e = "Keyword arguments 'lang' and 'project' must be specified together." e = "Keyword arguments 'lang' and 'project' must be specified together."
raise TypeError(e) raise TypeError(e)


@@ -120,13 +129,14 @@ def get_site(name=None, project=None, lang=None):
try: try:
site = config.wiki["sites"][name] site = config.wiki["sites"][name]
except KeyError: except KeyError:
if project is None: # implies lang is None, i.e., only name was given
if project is None: # implies lang is None, so only name was given
e = "Site '{0}' not found in config.".format(name) e = "Site '{0}' not found in config.".format(name)
raise SiteNotFoundError(e) raise SiteNotFoundError(e)
for sitename, site in config.wiki["sites"].items(): for sitename, site in config.wiki["sites"].items():
if site["project"] == project and site["lang"] == lang: if site["project"] == project and site["lang"] == lang:
return _get_site_object_from_dict(sitename, site) return _get_site_object_from_dict(sitename, site)
e = "Neither site '{0}' nor site '{1}:{2}' found in config.".format(name, project, lang)
e = "Neither site '{0}' nor site '{1}:{2}' found in config."
e.format(name, project, lang)
raise SiteNotFoundError(e) raise SiteNotFoundError(e)
else: else:
return _get_site_object_from_dict(name, site) return _get_site_object_from_dict(name, site)


+ 66
- 8
wiki/tools/site.py View File

@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-


from cookielib import CookieJar
from json import loads from json import loads
from urllib import urlencode from urllib import urlencode
from urllib2 import urlopen
from urllib2 import build_opener, HTTPCookieProcessor, URLError


from wiki.tools.category import Category from wiki.tools.category import Category
from wiki.tools.constants import * from wiki.tools.constants import *
from wiki.tools.exceptions import NamespaceNotFoundError
from wiki.tools.exceptions import *
from wiki.tools.page import Page from wiki.tools.page import Page
from wiki.tools.user import User from wiki.tools.user import User


@@ -17,10 +18,12 @@ class Site(object):


def __init__(self, name=None, project=None, lang=None, base_url=None, def __init__(self, name=None, project=None, lang=None, base_url=None,
article_path=None, script_path=None, sql=(None, None), article_path=None, script_path=None, sql=(None, None),
namespaces=None):
namespaces=None, login=(None, None)):
""" """
Docstring needed Docstring needed
""" """
# attributes referring to site information, filled in by an API query
# if they are missing (and an API url is available)
self._name = name self._name = name
self._project = project self._project = project
self._lang = lang self._lang = lang
@@ -30,9 +33,45 @@ class Site(object):
self._sql = sql self._sql = sql
self._namespaces = namespaces self._namespaces = namespaces


# get all of the above attributes that were not specified by the user
# set up cookiejar and URL opener for making API queries
self._cookiejar = CookieJar(cookie_file)
self._opener = build_opener(HTTPCookieProcessor(self._cookiejar))
self._opener.addheaders = [('User-agent', USER_AGENT)]

# use a username and password to login if they were provided
if login[0] is not None and login[1] is not None:
self._login(login[0], login[1])

# get all of the above attributes that were not specified as arguments
self._load_attributes() self._load_attributes()


def _login(self, name, password, token="", attempt=0):
"""
Docstring needed
"""
params = {"action": "login", "lgname": name, "lgpassword": password,
"lgtoken": token}
result = self.api_query(params)
res = result["login"]["result"]

if res == "Success":
return
elif res == "NeedToken" and attempt == 0:
token = result["login"]["token"]
return self._login(name, password, token, attempt=1)
else:
if res == "Illegal":
e = "The provided username is illegal."
elif res == "NotExists":
e = "The provided username does not exist."
elif res == "EmptyPass":
e = "No password was given."
elif res == "WrongPass" or res == "WrongPluginPass":
e = "The given password is incorrect."
else:
e = "Couldn't login; server says '{0}'.".format(res)
raise LoginError(e)

def _load_attributes(self, force=False): def _load_attributes(self, force=False):
""" """
Docstring needed Docstring needed
@@ -103,10 +142,24 @@ class Site(object):
Docstring needed Docstring needed
""" """
url = ''.join((self._base_url, self._script_path, "/api.php")) url = ''.join((self._base_url, self._script_path, "/api.php"))
params["format"] = "json"
params["format"] = "json" # this is the only format we understand
data = urlencode(params) data = urlencode(params)
result = urlopen(url, data).read()
return loads(result)

try:
response = self._opener.open(url, data)
except URLError as error:
if hasattr(error, "reason"):
e = "API query at {0} failed because {1}.".format(error.geturl,
error.reason)
elif hasattr(error, "code"):
e = "API query at {0} failed; got an error code of {1}."
e = e.format(error.geturl, error.code)
else:
e = "API query failed."
raise SiteAPIError(e)
else:
result = response.read()
return loads(result) # parse as a JSON object


def name(self): def name(self):
""" """
@@ -195,8 +248,13 @@ class Site(object):
pagename = "{0}:{1}".format(prefix, catname) pagename = "{0}:{1}".format(prefix, catname)
return Category(self, pagename) return Category(self, pagename)


def get_user(self, username):
def get_user(self, username=None):
""" """
Docstring needed Docstring needed
""" """
if username is None:
params = {"action": "query", "meta": "userinfo"}
result = self.api_query(params)
username = result["query"]["userinfo"]["name"]

return User(self, username) return User(self, username)

Loading…
Cancel
Save