Browse Source

Merge branch 'develop' into feature/tests-framework

tags/v0.1^2
Ben Kurtovic 13 years ago
parent
commit
719f6f02a4
13 changed files with 506 additions and 47 deletions
  1. +1
    -0
      bot/commands/afc_report.py
  2. +1
    -0
      bot/commands/afc_status.py
  3. +1
    -1
      bot/commands/ctcp.py
  4. +38
    -0
      bot/commands/editcount.py
  5. +31
    -0
      bot/commands/praise.py
  6. +63
    -0
      bot/commands/registration.py
  7. +9
    -8
      bot/commands/rights.py
  8. +10
    -0
      bot/wiki/category.py
  9. +25
    -6
      bot/wiki/exceptions.py
  10. +4
    -2
      bot/wiki/functions.py
  11. +227
    -12
      bot/wiki/page.py
  12. +81
    -16
      bot/wiki/site.py
  13. +15
    -2
      bot/wiki/user.py

+ 1
- 0
bot/commands/afc_report.py View File

@@ -11,6 +11,7 @@ class Command(BaseCommand):

def process(self, data):
self.site = wiki.get_site()
self.site._maxlag = None
self.data = data

if not data.args:


+ 1
- 0
bot/commands/afc_status.py View File

@@ -27,6 +27,7 @@ class Command(BaseCommand):

def process(self, data):
self.site = wiki.get_site()
self.site._maxlag = None

if data.line[1] == "JOIN":
notice = self.get_join_notice()


+ 1
- 1
bot/commands/ctcp.py View File

@@ -42,6 +42,6 @@ class Command(BaseCommand):

elif command == "VERSION":
default = "EarwigBot - 0.1-dev - Python/$1 https://github.com/earwig/earwigbot"
vers = config.metadata.get("ircVersion", default)
vers = config.irc.get("version", default)
vers = vers.replace("$1", platform.python_version())
self.connection.notice(target, "\x01VERSION {0}\x01".format(vers))

+ 38
- 0
bot/commands/editcount.py View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-

from urllib import quote_plus

from classes import BaseCommand
import wiki

class Command(BaseCommand):
"""Return a user's edit count."""
name = "editcount"

def check(self, data):
commands = ["ec", "editcount"]
if data.is_command and data.command in commands:
return True
return False

def process(self, data):
if not data.args:
name = data.nick
else:
name = ' '.join(data.args)

site = wiki.get_site()
site._maxlag = None
user = site.get_user(name)

try:
count = user.editcount()
except wiki.UserNotFoundError:
msg = "the user \x0302{0}\x0301 does not exist."
self.connection.reply(data, msg.format(name))
return

safe = quote_plus(user.name())
url = "http://toolserver.org/~soxred93/pcount/index.php?name={0}&lang=en&wiki=wikipedia"
msg = "\x0302{0}\x0301 has {1} edits ({2})."
self.connection.reply(data, msg.format(name, count, url.format(safe)))

+ 31
- 0
bot/commands/praise.py View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-

import random

from classes import BaseCommand

class Command(BaseCommand):
"""Praise people!"""
name = "praise"

def check(self, data):
commands = ["praise", "earwig", "leonard", "leonard^bloom", "groove",
"groovedog"]
return data.is_command and data.command in commands

def process(self, data):
if data.command == "earwig":
msg = "\x02Earwig\x0F is the bestest Python programmer ever!"
elif data.command in ["leonard", "leonard^bloom"]:
msg = "\x02Leonard^Bloom\x0F is the biggest slacker ever!"
elif data.command in ["groove", "groovedog"]:
msg = "\x02GrooveDog\x0F is the bestest heh evar!"
else:
if not data.args:
msg = "You use this command to praise certain people. Who they are is a secret."
else:
msg = "You're doing it wrong."
self.connection.reply(data, msg)
return

self.connection.say(data.chan, msg)

+ 63
- 0
bot/commands/registration.py View File

@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-

import time

from classes import BaseCommand
import wiki

class Command(BaseCommand):
"""Return when a user registered."""
name = "registration"

def check(self, data):
commands = ["registration", "age"]
if data.is_command and data.command in commands:
return True
return False

def process(self, data):
if not data.args:
name = data.nick
else:
name = ' '.join(data.args)

site = wiki.get_site()
site._maxlag = None
user = site.get_user(name)

try:
reg = user.registration()
except wiki.UserNotFoundError:
msg = "the user \x0302{0}\x0301 does not exist."
self.connection.reply(data, msg.format(name))
return

date = time.strftime("%b %d, %Y at %H:%M:%S UTC", reg)
age = self.get_diff(time.mktime(reg), time.mktime(time.gmtime()))

g = user.gender()
if g == "male":
gender = "He's"
elif g == "female":
gender = "She's"
else:
gender = "They're"
msg = "\x0302{0}\x0301 registered on {1}. {2} {3} old."
self.connection.reply(data, msg.format(name, date, gender, age))

def get_diff(self, t1, t2):
parts = {"years": 31536000, "days": 86400, "hours": 3600,
"minutes": 60, "seconds": 1}
msg = []

order = sorted(parts.items(), key=lambda x: x[1], reverse=True)
for key, value in order:
num = 0
while t2 - t1 > value:
t1 += value
num += 1
if num or (not num and msg):
msg.append(" ".join((str(num), key)))

return ", ".join(msg)

+ 9
- 8
bot/commands/rights.py View File

@@ -4,7 +4,7 @@ from classes import BaseCommand
import wiki

class Command(BaseCommand):
"""Retrieve a list of rights for a given username."""
"""Retrieve a list of rights for a given name."""
name = "rights"

def check(self, data):
@@ -15,18 +15,19 @@ class Command(BaseCommand):

def process(self, data):
if not data.args:
self.connection.reply(data, "who do you want me to look up?")
return
name = data.nick
else:
name = ' '.join(data.args)

username = ' '.join(data.args)
site = wiki.get_site()
user = site.get_user(username)
site._maxlag = None
user = site.get_user(name)

try:
rights = user.groups()
except wiki.UserNotFoundError:
msg = "the user \x0302{0}\x0301 does not exist."
self.connection.reply(data, msg.format(username))
self.connection.reply(data, msg.format(name))
return

try:
@@ -34,4 +35,4 @@ class Command(BaseCommand):
except ValueError:
pass
msg = "the rights for \x0302{0}\x0301 are {1}."
self.connection.reply(data, msg.format(username, ', '.join(rights)))
self.connection.reply(data, msg.format(name, ', '.join(rights)))

+ 10
- 0
bot/wiki/category.py View File

@@ -16,6 +16,16 @@ class Category(Page):
members -- returns a list of titles in the category
"""

def __repr__(self):
"""Returns the canonical string representation of the Category."""
res = ", ".join(("Category(title={0!r}", "follow_redirects={1!r}",
"site={2!r})"))
return res.format(self._title, self._follow_redirects, self._site)

def __str__(self):
"""Returns a nice string representation of the Category."""
return '<Category "{0}" of {1}>'.format(self.title(), str(self._site))

def members(self, limit=50):
"""Returns a list of titles in the category.



+ 25
- 6
bot/wiki/exceptions.py View File

@@ -3,7 +3,7 @@
"""
EarwigBot's Wiki Toolset: Exceptions

This module contains all exceptions used by the wiki package.
This module contains all exceptions used by the wiki package. There are a lot.
"""

class WikiToolsetError(Exception):
@@ -22,11 +22,6 @@ class LoginError(WikiToolsetError):
"""An error occured while trying to login. Perhaps the username/password is
incorrect."""

class PermissionsError(WikiToolsetError):
"""We tried to do something we don't have permission to, like a non-admin
trying to delete a page, or trying to edit a page when no login information
was provided."""

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

@@ -45,3 +40,27 @@ class RedirectError(WikiToolsetError):
class UserNotFoundError(WikiToolsetError):
"""Attempting to get certain information about a user that does not
exist."""

class EditError(WikiToolsetError):
"""We got some error while editing. Sometimes, a subclass of this exception
will be used, like PermissionsError or EditConflictError."""

class PermissionsError(EditError):
"""We tried to do something we don't have permission to, like a non-admin
trying to delete a page, or trying to edit a page when no login information
was provided."""

class EditConflictError(EditError):
"""We've gotten an edit conflict or a (rarer) delete/recreate conflict."""

class NoContentError(EditError):
"""We tried to create a page or new section with no content."""

class ContentTooBigError(EditError):
"""The edit we tried to push exceeded the article size limit."""

class SpamDetectedError(EditError):
"""The spam filter refused our edit."""

class FilteredError(EditError):
"""The edit filter refused our edit."""

+ 4
- 2
bot/wiki/functions.py View File

@@ -86,7 +86,9 @@ def _get_site_object_from_dict(name, d):
namespaces = d.get("namespaces", {})
login = (config.wiki.get("username"), config.wiki.get("password"))
cookiejar = _get_cookiejar()
user_agent = config.metadata.get("userAgent")
user_agent = config.wiki.get("userAgent")
assert_edit = config.wiki.get("assert")
maxlag = config.wiki.get("maxlag")

if user_agent:
user_agent = user_agent.replace("$1", platform.python_version())
@@ -102,7 +104,7 @@ def _get_site_object_from_dict(name, d):
return Site(name=name, project=project, lang=lang, base_url=base_url,
article_path=article_path, script_path=script_path, sql=sql,
namespaces=namespaces, login=login, cookiejar=cookiejar,
user_agent=user_agent)
user_agent=user_agent, assert_edit=assert_edit, maxlag=maxlag)

def get_site(name=None, project=None, lang=None):
"""Returns a Site instance based on information from our config file.


+ 227
- 12
bot/wiki/page.py View File

@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-

from hashlib import md5
import re
from time import gmtime, strftime
from urllib import quote

from wiki.exceptions import *
@@ -25,7 +27,9 @@ class Page(object):
is_redirect -- returns True if the page is a redirect, else False
toggle_talk -- returns a content page's talk page, or vice versa
get -- returns page content
get_redirect_target -- if the page is a redirect, returns its destination
get_redirect_target -- if the page is a redirect, returns its destination
edit -- replaces the page's content or creates a new page
add_section -- add a new section at the bottom of the page
"""

def __init__(self, site, title, follow_redirects=False):
@@ -54,6 +58,11 @@ class Page(object):
self._content = None
self._creator = None

# Attributes used for editing/deleting/protecting/etc:
self._token = None
self._basetimestamp = None
self._starttimestamp = None

# Try to determine the page's namespace using our site's namespace
# converter:
prefix = self._title.split(":", 1)[0]
@@ -72,6 +81,16 @@ class Page(object):
else:
self._is_talkpage = self._namespace % 2 == 1

def __repr__(self):
"""Returns the canonical string representation of the Page."""
res = ", ".join(("Page(title={0!r}", "follow_redirects={1!r}",
"site={2!r})"))
return res.format(self._title, self._follow_redirects, self._site)

def __str__(self):
"""Returns a nice string representation of the Page."""
return '<Page "{0}" of {1}>'.format(self.title(), str(self._site))

def _force_validity(self):
"""Used to ensure that our page's title is valid.

@@ -124,16 +143,16 @@ class Page(object):
"""Loads various data from the API in a single query.

Loads self._title, ._exists, ._is_redirect, ._pageid, ._fullurl,
._protection, ._namespace, ._is_talkpage, ._creator, and ._lastrevid
using the API. It will do a query of its own unless `result` is
provided, in which case we'll pretend `result` is what the query
returned.
._protection, ._namespace, ._is_talkpage, ._creator, ._lastrevid,
._token, and ._starttimestamp using the API. It will do a query of
its own unless `result` is provided, in which case we'll pretend
`result` is what the query returned.

Assuming the API is sound, this should not raise any exceptions.
"""
if result is None:
params = {"action": "query", "rvprop": "user", "rvdir": "newer",
"prop": "info|revisions", "rvlimit": 1,
params = {"action": "query", "rvprop": "user", "intoken": "edit",
"prop": "info|revisions", "rvlimit": 1, "rvdir": "newer",
"titles": self._title, "inprop": "protection|url"}
result = self._site._api_query(params)

@@ -168,6 +187,13 @@ class Page(object):
self._fullurl = res["fullurl"]
self._protection = res["protection"]

try:
self._token = res["edittoken"]
except KeyError:
pass
else:
self._starttimestamp = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime())

# We've determined the namespace and talkpage status in __init__()
# based on the title, but now we can be sure:
self._namespace = res["ns"]
@@ -192,13 +218,13 @@ class Page(object):
"""
if result is None:
params = {"action": "query", "prop": "revisions", "rvlimit": 1,
"rvprop": "content", "titles": self._title}
"rvprop": "content|timestamp", "titles": self._title}
result = self._site._api_query(params)

res = result["query"]["pages"].values()[0]
try:
content = res["revisions"][0]["*"]
self._content = content
self._content = res["revisions"][0]["*"]
self._basetimestamp = res["revisions"][0]["timestamp"]
except KeyError:
# This can only happen if the page was deleted since we last called
# self._load_attributes(). In that case, some of our attributes are
@@ -206,6 +232,166 @@ class Page(object):
self._load_attributes()
self._force_existence()

def _edit(self, params=None, text=None, summary=None, minor=None, bot=None,
force=None, section=None, captcha_id=None, captcha_word=None,
tries=0):
"""Edit the page!

If `params` is given, we'll use it as our API query parameters.
Otherwise, we'll build params using the given kwargs via
_build_edit_params().
We'll then try to do the API query, and catch any errors the API raises
in _handle_edit_errors(). We'll then throw these back as subclasses of
EditError.
"""
# Try to get our edit token, and die if we can't:
if not self._token:
self._load_attributes()
if not self._token:
e = "You don't have permission to edit this page."
raise PermissionsError(e)
# Weed out invalid pages before we get too far:
self._force_validity()

# Build our API query string:
if not params:
params = self._build_edit_params(text, summary, minor, bot, force,
section, captcha_id, captcha_word)
else: # Make sure we have the right token:
params["token"] = self._token

# Try the API query, catching most errors with our handler:
try:
result = self._site._api_query(params)
except SiteAPIError as error:
if not hasattr(error, "code"):
raise # We can only handle errors with a code attribute
result = self._handle_edit_errors(error, params, tries)

# If everything was successful, reset invalidated attributes:
if result["edit"]["result"] == "Success":
self._content = None
self._basetimestamp = None
self._exists = 0
return

# If we're here, then the edit failed. If it's because of AssertEdit,
# handle that. Otherwise, die - something odd is going on:
try:
assertion = result["edit"]["assert"]
except KeyError:
raise EditError(result["edit"])
self._handle_assert_edit(assertion, params, tries)

def _build_edit_params(self, text, summary, minor, bot, force, section,
captcha_id, captcha_word):
"""Given some keyword arguments, build an API edit query string."""
hashed = md5(text).hexdigest() # Checksum to ensure text is correct
params = {"action": "edit", "title": self._title, "text": text,
"token": self._token, "summary": summary, "md5": hashed}

if section:
params["section"] = section
if captcha_id and captcha_word:
params["captchaid"] = captcha_id
params["captchaword"] = captcha_word
if minor:
params["minor"] = "true"
else:
params["notminor"] = "true"
if bot:
params["bot"] = "true"

if not force:
params["starttimestamp"] = self._starttimestamp
if self._basetimestamp:
params["basetimestamp"] = self._basetimestamp
if self._exists == 2:
# Page does not exist; don't edit if it already exists:
params["createonly"] = "true"
else:
params["recreate"] = "true"

return params

def _handle_edit_errors(self, error, params, tries):
"""If our edit fails due to some error, try to handle it.

We'll either raise an appropriate exception (for example, if the page
is protected), or we'll try to fix it (for example, if we can't edit
due to being logged out, we'll try to log in).
"""
if error.code in ["noedit", "cantcreate", "protectedtitle",
"noimageredirect"]:
raise PermissionsError(error.info)

elif error.code in ["noedit-anon", "cantcreate-anon",
"noimageredirect-anon"]:
if not all(self._site._login_info):
# Insufficient login info:
raise PermissionsError(error.info)
if tries == 0:
# We have login info; try to login:
self._site._login(self._site._login_info)
self._token = None # Need a new token; old one is invalid now
return self._edit(params=params, tries=1)
else:
# We already tried to log in and failed!
e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug."
raise LoginError(e)

elif error.code in ["editconflict", "pagedeleted", "articleexists"]:
# These attributes are now invalidated:
self._content = None
self._basetimestamp = None
self._exists = 0
raise EditConflictError(error.info)

elif error.code in ["emptypage", "emptynewsection"]:
raise NoContentError(error.info)

elif error.code == "contenttoobig":
raise ContentTooBigError(error.info)

elif error.code == "spamdetected":
raise SpamDetectedError(error.info)

elif error.code == "filtered":
raise FilteredError(error.info)

raise EditError(": ".join((error.code, error.info)))

def _handle_assert_edit(self, assertion, params, tries):
"""If we can't edit due to a failed AssertEdit assertion, handle that.

If the assertion was 'user' and we have valid login information, try to
log in. Otherwise, raise PermissionsError with details.
"""
if assertion == "user":
if not all(self._site._login_info):
# Insufficient login info:
e = "AssertEdit: user assertion failed, and no login info was provided."
raise PermissionsError(e)
if tries == 0:
# We have login info; try to login:
self._site._login(self._site._login_info)
self._token = None # Need a new token; old one is invalid now
return self._edit(params=params, tries=1)
else:
# We already tried to log in and failed!
e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug."
raise LoginError(e)

elif assertion == "bot":
e = "AssertEdit: bot assertion failed; we don't have a bot flag!"
raise PermissionsError(e)

# Unknown assertion, maybe "true", "false", or "exists":
e = "AssertEdit: assertion '{0}' failed.".format(assertion)
raise PermissionsError(e)

def title(self, force=False):
"""Returns the Page's title, or pagename.

@@ -394,9 +580,9 @@ class Page(object):
if force or self._exists == 0:
# Kill two birds with one stone by doing an API query for both our
# attributes and our page content:
params = {"action": "query", "rvprop": "content", "rvlimit": 1,
params = {"action": "query", "rvlimit": 1, "titles": self._title,
"prop": "info|revisions", "inprop": "protection|url",
"titles": self._title}
"intoken": "edit", "rvprop": "content|timestamp"}
result = self._site._api_query(params)
self._load_attributes(result=result)
self._force_existence()
@@ -438,3 +624,32 @@ class Page(object):
except IndexError:
e = "The page does not appear to have a redirect target."
raise RedirectError(e)

def edit(self, text, summary, minor=False, bot=True, force=False):
"""Replaces the page's content or creates a new page.

`text` is the new page content, with `summary` as the edit summary.
If `minor` is True, the edit will be marked as minor. If `bot` is true,
the edit will be marked as a bot edit, but only if we actually have a
bot flag.

Use `force` to push the new content even if there's an edit conflict or
the page was deleted/recreated between getting our edit token and
editing our page. Be careful with this!
"""
self._edit(text=text, summary=summary, minor=minor, bot=bot,
force=force)

def add_section(self, text, title, minor=False, bot=True, force=False):
"""Adds a new section to the bottom of the page.

The arguments for this are the same as those for edit(), but instead of
providing a summary, you provide a section title.

Likewise, raised exceptions are the same as edit()'s.

This should create the page if it does not already exist, with just the
new section as content.
"""
self._edit(text=text, summary=title, minor=minor, bot=bot, force=force,
section="new")

+ 81
- 16
bot/wiki/site.py View File

@@ -5,6 +5,7 @@ from gzip import GzipFile
from json import loads
from re import escape as re_escape, match as re_match
from StringIO import StringIO
from time import sleep
from urllib import unquote_plus, urlencode
from urllib2 import build_opener, HTTPCookieProcessor, URLError
from urlparse import urlparse
@@ -41,7 +42,7 @@ class Site(object):
def __init__(self, name=None, project=None, lang=None, base_url=None,
article_path=None, script_path=None, sql=(None, None),
namespaces=None, login=(None, None), cookiejar=None,
user_agent=None):
user_agent=None, assert_edit=None, maxlag=None):
"""Constructor for new Site instances.

This probably isn't necessary to call yourself unless you're building a
@@ -69,6 +70,11 @@ class Site(object):
self._sql = sql
self._namespaces = namespaces

# Attributes used when querying the API:
self._assert_edit = assert_edit
self._maxlag = maxlag
self._max_retries = 5

# Set up cookiejar and URL opener for making API queries:
if cookiejar is not None:
self._cookiejar = cookiejar
@@ -90,22 +96,50 @@ class Site(object):
if logged_in_as is None or name != logged_in_as:
self._login(login)

def _api_query(self, params):
def __repr__(self):
"""Returns the canonical string representation of the Site."""
res = ", ".join((
"Site(name={_name!r}", "project={_project!r}", "lang={_lang!r}",
"base_url={_base_url!r}", "article_path={_article_path!r}",
"script_path={_script_path!r}", "assert_edit={_assert_edit!r}",
"maxlag={_maxlag!r}", "sql={_sql!r}", "login={0}",
"user_agent={2!r}", "cookiejar={1})"
))
name, password = self._login_info
login = "({0}, {1})".format(repr(name), "hidden" if password else None)
cookies = self._cookiejar.__class__.__name__
try:
cookies += "({0!r})".format(self._cookiejar.filename)
except AttributeError:
cookies += "()"
agent = self._opener.addheaders[0][1]
return res.format(login, cookies, agent, **self.__dict__)

def __str__(self):
"""Returns a nice string representation of the Site."""
res = "<Site {0} ({1}:{2}) at {3}>"
return res.format(self.name(), self.project(), self.lang(),
self.domain())

def _api_query(self, params, tries=0, wait=5):
"""Do an API query with `params` as a dict of parameters.

This will first attempt to construct an API url from self._base_url and
self._script_path. We need both of these, or else we'll raise
SiteAPIError.

We'll encode the given params, adding format=json along the way, and
make the request through self._opener, which has built-in cookie
We'll encode the given params, adding format=json along the way, as
well as &assert= and &maxlag= based on self._assert_edit and _maxlag.
We make the request through self._opener, which has built-in cookie
support via self._cookiejar, a User-Agent (wiki.constants.USER_AGENT),
and Accept-Encoding set to "gzip".
Assuming everything went well, we'll gunzip the data (if compressed),
load it as a JSON object, and return it.

If our request failed, we'll raise SiteAPIError with details.
If our request failed for some reason, we'll raise SiteAPIError with
details. If that reason was due to maxlag, we'll sleep for a bit and
then repeat the query until we exceed self._max_retries.

There's helpful MediaWiki API documentation at
<http://www.mediawiki.org/wiki/API>.
@@ -115,7 +149,13 @@ class Site(object):
raise SiteAPIError(e)

url = ''.join((self._base_url, self._script_path, "/api.php"))

params["format"] = "json" # This is the only format we understand
if self._assert_edit: # If requested, ensure that we're logged in
params["assert"] = self._assert_edit
if self._maxlag: # If requested, don't overload the servers
params["maxlag"] = self._maxlag

data = urlencode(params)

print url, data # debug code
@@ -124,21 +164,46 @@ class Site(object):
response = self._opener.open(url, data)
except URLError as error:
if hasattr(error, "reason"):
e = "API query at {0} failed because {1}."
e = e.format(error.geturl, error.reason)
e = "API query failed: {0}.".format(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)
e = "API query failed: got an error code of {0}."
e = e.format(error.code)
else:
e = "API query failed."
raise SiteAPIError(e)

result = response.read()
if response.headers.get("Content-Encoding") == "gzip":
stream = StringIO(result)
gzipper = GzipFile(fileobj=stream)
result = gzipper.read()

try:
res = loads(result) # Parse as a JSON object
except ValueError:
e = "API query failed: JSON could not be decoded."
raise SiteAPIError(e)

try:
code = res["error"]["code"]
info = res["error"]["info"]
except (TypeError, KeyError):
return res

if code == "maxlag":
if tries >= self._max_retries:
e = "Maximum number of retries reached ({0})."
raise SiteAPIError(e.format(self._max_retries))
tries += 1
msg = 'Server says: "{0}". Retrying in {1} seconds ({2}/{3}).'
print msg.format(info, wait, tries, self._max_retries)
sleep(wait)
return self._api_query(params, tries=tries, wait=wait*3)
else:
result = response.read()
if response.headers.get("Content-Encoding") == "gzip":
stream = StringIO(result)
gzipper = GzipFile(fileobj=stream)
result = gzipper.read()
return loads(result) # Parse as a JSON object
e = 'API query failed: got error "{0}"; server says: "{1}".'
error = SiteAPIError(e.format(code, info))
error.code, error.info = code, info
raise error

def _load_attributes(self, force=False):
"""Load data about our Site from the API.


+ 15
- 2
bot/wiki/user.py View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-

from time import strptime
from time import gmtime, strptime

from wiki.constants import *
from wiki.exceptions import UserNotFoundError
@@ -45,6 +45,14 @@ class User(object):
self._site = site
self._name = name

def __repr__(self):
"""Returns the canonical string representation of the User."""
return "User(name={0!r}, site={1!r})".format(self._name, self._site)

def __str__(self):
"""Returns a nice string representation of the User."""
return '<User "{0}" of {1}>'.format(self.name(), str(self._site))

def _get_attribute(self, attr, force):
"""Internally used to get an attribute by name.

@@ -101,7 +109,12 @@ class User(object):
self._editcount = res["editcount"]

reg = res["registration"]
self._registration = strptime(reg, "%Y-%m-%dT%H:%M:%SZ")
try:
self._registration = strptime(reg, "%Y-%m-%dT%H:%M:%SZ")
except TypeError:
# Sometimes the API doesn't give a date; the user's probably really
# old. There's nothing else we can do!
self._registration = gmtime(0)

try:
res["emailable"]


Loading…
Cancel
Save