Add supybot module

This commit is contained in:
James Vega 2007-04-22 20:10:26 +00:00
parent da614f5561
commit dbfec8afb9
334 changed files with 0 additions and 87429 deletions

View File

@ -1,3 +0,0 @@
*.pyc
*.pyo
*~

7
ACKS
View File

@ -1,7 +0,0 @@
johhnyace, who gave me the modem that helped me tremendously in development.
bwp, who rewrote the Http.weather command, and also hosted the example
"supybot" in #supybot on OFTC and Freenode for quite some time.
sweede, for hosting the "main" supybot for awhile.
HostPC.com, for hosting the current example "supybot" and for graciously
providing DNS services and email.

10
BUGS
View File

@ -1,10 +0,0 @@
We're sure there are tons of them. When you find them, send them to us and
we'll fix them ASAP. We'd love to have a bugless bot someday...
Incidentally, the way to "send the bugs to us" is via SourceForge:
<http://sourceforge.net/tracker/?atid=489447&group_id=58965&func=browse>
Known bugs that probably won't get fixed:
BadWords' outFilter filters colors. It's not a high priority to get
that fixed.

1495
ChangeLog

File diff suppressed because it is too large Load Diff

67
DEVS
View File

@ -1,67 +0,0 @@
These are the developers of Supybot, in approximate order of ____.
Jeremy Fincher (jemfinch) is a Computer Science (and possibly
philosophy) student at The Ohio State University. He spends most of
his free time with his girlfriend Meg, but also...well, there's not
much also :) He hopes to graduate with good enough grades to go to
law school or seminary at some point in the future. He initially
wrote the majority of the Supybot framework and standard plugins,
though he's been trying to slowly phase himself out of plugin-writing
and more into framework-enhancement. Rather than list the specific
things he's done, you can just assume that if someone else isn't
claiming it, it was probably done by him.
Daniel DiPaolo (Strike/ddipaolo) is a lazy Texan punk with a job as an IT
monkey who spends his free time coding, playing ultimate frisbee, and arguing
pointless things on the internet. As far as the bot goes, he's mainly a
plugin developer but he has helped here and there with various under-the-hood
things and is one of the few people (other than jemfinch) who understands the
inner workings of Supybot. His biggest plugin contribution (in terms of sheer
lines of code) has been the MoobotFactoids plugin and all the workd involved
in getting that plugin to work, but he has also helped with a lot of testing,
debugging, and brainstorming. He also wrote the Dunno, News, and Todo plugins
and is responsible for a significant amount of code in the Poll, Debian,
QuoteGrabs, Karma, and ChannelDB plugins.
James Vega (jamessan) is an Electrical Engineering/Computer Science student at
Northeastern University. He wrote the Sourceforge and Ebay plugins as well as
the first incarnation of the Babelfish commands and most of Amazon. He has
also performed a significant amount of maintenance and refactoring of plugins
in general. Some of the plugins that were affected the most are Debian,
FunDB, Gameknot, Http, Note, and Quote. All of the link snarfers, save
Bugzilla's, were also written by jamessan. His meddlings have prompted the
implementation of Toggleables, which eventually evolved to Configurables and
then to the current registry system. As well as being the current webmaster,
he also overhauled the tool which is used to generate the site's HTML
documentation for Supybot and setup the weekly creation of CVS snapshots.
Brett Kelly (inkedmn) is a hobbyist (soon to be professional :)) coder
from southern California who enjoys collecting tattoos (on his body) and
drinking coffee with his wife. He initially wrote the Note plugin as well
as several commands in the Http plugin.
Vincent Foley-Bourgon is a recently-graduated student from Quebec who
enjoys anything pointless, unprofitable, and generally useless. Recently
returning to Supybot development (after writing the original freshmeat
command for the Http plugin) he wrote the entire Hangman infrastructure
for the Words plugin.
Daniel Berlin is a soon to be lawyer with a background in computer science
and compilers. He enjoys selling crack to young homeless orphans, and works
on Supybot when he's not lawyering or hacking on gcc.
Keith Jones (kmj) dislikes talking about himself in the third person. He
has an MS in Computer Science, and has decided to see how long he can go
without using that in any kind of professional capacity. To that end he
is currently taking some math classes and applying to math Ph.D programs
so some day he can be a professor at a college near a snowy mountain where
he will ski every morning. So far, he hasn't done much for the project
except squeeze Doug Bell's GPL'd unit conversion code into a supybot plugin.
Stéphan Kochen (G-LiTe) is a lazy (soon to be) computer science student.
He's usually just freelancing and submitting patches here and there when he
bumps into a bug that bothers him, but Supybot is one of the first projects
he semi-actively tries to work on. ;) His biggest contribution has been the
refactoring of the supybot-wizard script to use the registry, though he also
likes to track down those nasty obscure bugs which haunt many of our fine
applications these days.

View File

@ -1,8 +0,0 @@
No, really. Darcs is way cooler and the Darcs repository has all the new
code (like our new supybot-newplugin and supybot-test).
"darcs get http://source.supybot.com/supybot/" for the new Supybot code.
"darcs get http://source.supybot.com/supybot-plugins/" for plugins that aren't
included in the official Supybot.
You can get Darcs at http://darcs.net/

110
INSTALL
View File

@ -1,110 +0,0 @@
So what do you do? That depends on which operating system you're
running. We've split this document up to address the different
methods, so find the section for your operating system and continue
from there. First let's start with the parts that are common to all
OSes.
###
# COMMON:
###
First things first: Supybot *requires* at least Python 2.3. There ain't
no getting around it. We do not require any version greater than 2.3,
but we will be compatible with any version of Python >= 2.3. If you're
a Python developer, you probably know how superior 2.3 is to previous
incarnations. If you're not, just think about the difference between a
bowl of plain vanilla ice cream and a banana split. Or something like
that. Either way, *We're* Python developers and we like banana splits.
So, be sure to install python2.3 or greater before continuing. You can
get it from http://www.python.org/
For more information and help on how to use Supybot, checkout
the documents under docs/ (especially GETTING_STARTED and CONFIGURATION).
Our forums (http://forums.supybot.org/) may also be of use, especially
the "Tips and Tricks" topic under "Supybot User Discussion".
###
# UNIX/Linux/*BSD:
###
If you're installing Python using your distributor's packages, you may
need a python-dev package installed, too. If you don't have a
/usr/lib/python2.3/distutils directory (assuming /usr/lib/python2.3 is
where your Python libs are installed), then you will need a python-dev
package.
After you extract Supybot and cd into the supybot directory just
created, you'll want to run (as root) "python setup.py install". This
will install Supybot globally. If you need to install locally for
whatever reason, see the addendum near the end of this document.
You'll then have several new programs installed where Python scripts
are normally installed on your system (/usr/bin or /usr/local/bin are
common on UNIX systems). The two that might be of particular interest
to you, the new user, are "supybot" and "supybot-wizard". The former
("supybot") is the script to run an actual bot; the latter
("supybot-wizard") is an in-depth wizard that provides a nice user
interface for creating a registry file for your bot.
So after running supybot-wizard, you've got a nice registry file
handy. If you're not satisfied with your answers to any of the
questions you were asked, feel free to run the program again until
you're satisfied with all your answers. Once you're satisfied,
though, run the "supybot" program with the registry file you created
as an argument. This will start the bot; unless you turned off
logging to stdout, you'll see some nice log messages describing what
the bot is doing at any particular moment; it may pause for a
significant amount of time after saying "Connecting to ..." while the
server tries to check its ident.
###
# Windows:
###
*** If you are using an IPV6 connection, you will not be able to run
Supybot under Windows (unless Python has fixed things). Current
versions of Python for Windows are *not* built with IPV6 support. This
isn't expected to be fixed until Python 2.4, at the earliest.
Now that you have Python installed, open up a command prompt. The
easiest way to do this is to open the run dialog (Programs -> run) and
type "cmd" (for Windows 2000/XP/2003) or "command" (for Windows 9x).
In order to reduce the amount of typing you need to do, I suggest
adding Python's directory to your path. If you installed Python using
the default settings, you would then do the following in the command
prompt (otherwise change the path to match your settings):
set PATH=%PATH%;C:\Python23\
You should now be able to type "python" to start the Python
interpreter (CTRL-Z and Return to exit). Now that that's setup,
you'll want to cd into the directory that was created when you
unzipped Supybot; I'll assume you unzipped it to C:\Supybot for these
instructions. From C:\Supybot, run "python setup.py install". This
will install Supybot under C:\Python23\. If you want to install
Supybot to a non-default location, see the addendum near the end of
this document. You will now have several new programs installed in
C:\Python23\Scripts\. The two that might be of particular interest to
you, the new user, are "supybot" and "supybot-wizard". The former
("supybot") is the script to run an actual bot; the latter
("supybot-wizard") is an in-depth wizard that provides a nice user
interface for creating a registry file for your bot.
Now you will want to run "python C:\Python23\Scripts\supybot-wizard"
to generate a registry file for your bot. So after running
supybot-wizard, you've got a nice registry file handy. If you're not
satisfied with your answers to any of the questions you were asked,
feel free to run the program again until you're satisfied with all
your answers. Once you're satisfied, though, run "python
C:\Python23\Scripts\supybot botname.conf". This will start the bot;
unless you turned off logging to stdout, you'll see some nice log
messages describing what the bot is doing at any particular moment; it
may pause for a significant amount of time after saying "Connecting
to ..." while the server tries to check its ident.
###
# Addenda
###
Local installs: See this forum post: http://tinyurl.com/2tb37

28
LICENSE
View File

@ -1,28 +0,0 @@
Copyright (c) 2002-2004 Jeremiah Fincher and others
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions, and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions, and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the author of this software nor the name of
contributors to this software may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Portions of the included source code are copyright by its original author(s)
and remain subject to its associated license.

38
README
View File

@ -1,38 +0,0 @@
EVERYONE:
---------
Read LICENSE. It's a 2-clause BSD license, but you should read it anyway.
USERS:
------
If you're upgrading, read RELNOTES. If you're new to Supybot,
read docs/GETTING_STARTED for an introduction to the bot, and read
docs/CAPABILITIES to see how to use capabilities to your greater
benefit.
If you have any trouble, feel free to swing by #supybot on
irc.freenode.net or irc.oftc.net (we have a Supybot there relaying,
so either network works) and ask questions. We'll be happy to help
wherever we can. And by all means, if you find anything hard to
understand or think you know of a better way to do something,
*please* post it on Sourceforge.net so we can improve the bot!
WINDOWS USERS:
--------------
The wizards (supybot-wizard, supybot-newplugin, and
supybot-adduser) are all installed to your Python directory's
\Scripts. What that *probably* means is that you'll run them like
this: C:\Python23\python C:\Python23\Scripts\supybot-wizard
DEVELOPERS:
-----------
Read OVERVIEW to see what the modules are used for. Read PLUGIN-EXAMPLE
to see some examples of callbacks and commands written for the bot.
Read INTERFACES to see what kinds of objects you'll be dealing with.
Read STYLE if you wish to contribute; all contributed code must meet
the guidelines set forth there.
Be sure to run "test/test.py --help" to see what options are available
to you when testing. Windows users in particular should be sure to
exclude test_Debian.py and test_Unix.py.

262
RELNOTES
View File

@ -1,262 +0,0 @@
Version 0.80.1
Bugfix release. No incompatibilities.
Version 0.80.0
We *finally* hit 0.80.0! This release is completely compatible with
the last release candidate.
An update to Babelfish may cause an error message to be displayed in
the console when the bot is first run. The error message should be
gone when the bot is restarted.
We also have a new community website at http://www.supybot.com/ where
our users can submit their own plugins, view/download other people's
plugins and discuss all things Supybot-related.
Version 0.80.0rc3
Another bugfix release. This one was pretty important as it actually
makes supybot.database.plugins.channelSpecific work properly.
Version 0.80.0rc2
supybot.databases.plugins.channelSpecific.channel was renamed to
supybot.databases.plugins.channelSpecific.link.
supybot.databases.plugins.channelSpecific.link.allow was added, which
determines whether a channel will allow other channels to link to its
database.
Infobot is no longer deprecated and the following changes were made to
its config variables:
supybot.plugins.Infobot.answerUnaddressedQuestions was renamed to
supybot.plugins.Infobot.unaddressed.answerQuestions.
supybot.plugins.Infobot.snarfUnaddressedDefinitions was renamed to
supybot.plugins.Infobot.unaddressed.snarfDefinitions.
supybot.plugins.Infobot.unaddressed.replyExistingFactoid was added to
determine whether the bot will reply when someone attempts to create a
duplicate factoid.
Version 0.80.0pre6
Another bugfix release. No incompatibilities known. The only
registry change is that supybot.databases.users.hash has been
removed.
Version 0.80.0pre5
Completely bugfix release. No incompatibilies known.
Version 0.80.0pre4
Mainly a bug fix release. This will likely be the last release before
0.80.0final, but we're gonna let it stew for a couple weeks to attempt
to catch any lingering bugs.
ansycoreDrivers is now deprecated in favor of socketDrivers or
twistedDrivers.
supybot.databases.plugins.channelSpecific.channel is now a channelValue
so that you can link specific channels together (instead of all channels
being linked together).
For those of you that use eval and/or exec, they have been removed from
the Owner plugin and are now in sandbox/Debug.py (which you'll have to
grab from CVS).
Version 0.80.0pre3
The database format for the Note plugin has changed to a flatfile
format; use tools/noteConvert.py to convert it to the new format.
Ditto that for the URL database.
FunDB is deprecated and will be removed at the next major release;
use tools/fundbConvert.py to convert your old FunDB databases to Lart
and Praise databases.
If you had turned off supybot.databases.plugins.channelSpecific, your
non-channel-specific database files had gone directly into your data/
directory. We had some problems with poor interactions between that
configuration variable and channel capabilities, though, so we
changed the implementation so that non-channel-specific databases are
considered databases of a single (configurable) channel (defaulting
to "#"). This will also help others who are converting from
channel-specific to non-channel-specific databases, but for you
who've already made the switch, you'll need to move your database
files again, from data/ to data/# (or whatever channel you might
change that variable to).
supybot.channels doesn't exist anymore; now the only list of channels
to join is per-network, in supybot.networks.<network>.channels.
We weren't serializing supybot.replies.* properly in older versions.
Now we are, but the old, improperly serialized versions won't work
properly. Remove from your configuration file all variables
beginning with "supybot.replies" before you start the bot.
The URL database has been changed again, but it will use a different
filename so you shouldn't run into conflicts, just a newly-empty
database.
We upgraded the SOAP stuff in others; you may do well to do a
setup.py install --clean this time around.
Version 0.80.0pre2
Many more bugs have been fixed. A few more plugins have been updated
to use our new-style database abstraction. If it seems like your
databases are suddenly empty, look for a new database file named
Plugin.dbtype.db. We've also added a few more configuration variables.
Version 0.80.0pre1
Tons of bugs fixed, many features and plugins added. Everything
should be entirely compatible; many more configuration variables have
been added.
Version 0.79.9999
Some more bugs fixed, added a few features and a couple configuration
variabless. This should hopefully be the last release before 0.80.0,
which will finally bring us to pure Beta status.
Version 0.79.999
Some bugs fixed, but the ones that were fixed were pretty big. This
is, of course, completely compatible with the last release.
Version 0.79.99
Many bugs fixed, thanks to the users who reported them. We're
getting asymptotically closer to 0.80.0 -- maybe this'll be the last
one, maybe we'll have to release an 0.79.999 -- either way, we're
getting close :) Check out the ChangeLog for the fixes and a few new
features.
Version 0.79.9
We've changed so much stuff in this release that we've given up on
users upgrading their configuration files for the new release. So
do a clean install (python2.3 setup.py install --clean), run the
wizard again, and kick some butt.
(It's rumored that you can save most of your old configuration by
appending your new configuration at the end of your old configuration
and running supybot with that new configuration file. This, of
course, comes with no warranty or guarantee of utility -- try it if
you want, but backup your original configuration file!)
Version 0.77.2
This is a drop-in replacement for 0.77.1, with two exceptions. The
configuration variable formerly known as
"supybot.plugins.Services.password" is now known as
"supybot.plugins.Services.NickServ.password", due to the fact that
there might be different passwords for NickServ and ChanServ (and
ChanServ passwords are per-channel, whereas NickServ passwords are
global). If you're using the Services plugin, you'll need to make
this change in order to continue identifying with services. The
configuration variable formerly known as
"supybot.plugins.Babelfish.disabledLanguages" is now known as
"supybot.plugins.Babelfish.languages". The configuration variable now
accepts the languages that *will* be translated as opposed to ones
that are *not* translated.
Tests and the developer sandbox are not longer delivered with our
release tarballs. If you're a developer and you want these, you
should either check out CVS or download one of our weekly CVS
snapshots, available at http://supybot.sourceforge.net/snapshots/ .
Version 0.77.1
This is a drop-in replacement for 0.77.0 -- no incompatibilities, to
out knowledge. Simply install over your old installation and restart
your bot :)
Version 0.77.0
Setup.py will automatically remove your old installations for you, no
need to worry about that yourself.
Configuration has been *entirely* redone. Read the new
GETTING_STARTED document to see how to work with configuration
variables now. Your old botscripts from earlier versions *will not*
work with the new configuration method. We'd appreciate it if you'd
rerun the wizard in order for us to find any bugs that remain in it
before we officially declare ourselves Beta. Note also that because
of the new configuration method, the interface for plugins' configure
function has changed: there are no longer any onStart or afterConnect
arguments, so all configuration should be performed via the registry.
Channel capabilities have been changed; rather than being
#channel.capability, they're now #channel,capability. It's a bit
uglier, we know, but dots can be valid in channel names, and we
needed the dot for handling plugin.command capabilities.
tools/ircdbConvert.py should update this for you.
The on-disk format of the user/channel databases has changed to be far
more readable. A conversion utility is included, as mentioned before:
tools/ircdbConvert.py. Run this with no arguments to see the
directions for using it.
Uh, we were just kidding about the upgrade script in 0.76.0 :) It'll
be a little while longer. We do have several little upgrade scripts,
though.
Version 0.76.1
Almost entirely bugfixes, just some minor (and some less minor) bugs
that need to get in before we really start hacking on the next
version. Should be *entirely* compatible with 0.76.0.
Version 0.76.0
Major bugfix release. A great number of bugs fixed. This is the last
release without an upgrade script.
The only hiccup in the upgrade from 0.75.0 should be that you'll need
to update your botscript to reflect the removal of the debug module.
We'd rather you use supybot-wizard to generate a new botscript, of
course, but if you insist on modifying your existing botscript, take a
look at
<http://cvs.sourceforge.net/viewcvs.py/supybot/supybot/src/template.py?r1=1.20&r2=1.21>
to see what you need to do.
Version 0.75.0
Don't forget to reinstall (i.e., run "python setup.py install" as
root). Sometimes it even does good to remove the old installation;
$PYTHON/site-packages/supybot can be removed with no problems
whatsoever.
You will need to re-run supybot-wizard and generate a new botscript.
The Infobot plugin has been removed from this release; it's not ready
for prime time. If you're interested in getting it running (i.e., you
want full Infobot compatibility and aren't satisfied with either
MoobotFactoids or Factoids) then swing over to #supybot and we can
discuss the tests. We simply don't know enough about Infobot to make
sure our Infobot plugin is an exact replica, and need someone's help
with making the changes necessary for that.

40
TODO
View File

@ -1,40 +0,0 @@
Roughly in order of precedence (the closer to the front of the file,
the more likely it'll be done before next release):
* We should have a channel-global value in ircdb.IrcChannel for
ignoring unregistered users.
* We need to note when bans expire on a channel and send the unban,
that way plugins can use the ircdb ban stuff and not worry about
sending or scheduling unbans.
* We should probably add a "hello" command to make things more
compatible with Eggdrop, since we've been replacing many eggdrop
bots lately.
* We should be able to set the log level for plugins individually.
* Rbot has a "remind" plugin that seems pretty cool, we should get
one as well. It might do well as a command in the Later plugin.
* MozBot has a "list" plugin that seems pretty cool, we should get
one as well.
Problems that involve a lot of tedious minutiae, but really need to
be done at some point:
Hard problems that won't get done until someone really wants to have
some fun:
* Redundant relaying -- having more than one bot handle relaying,
where a secondary one will automatically take over if the first
becomes incapacitated.
* Have the bot detect when other bots are in the channel responding
to the same prefix character, and do something based on that.
* Unicode support. Basically, we'd have to have a way to set the
encoding for the server, and use unicode throughout internally.
The real issue is how much it would affect the current code to
switch it over to unicode, and what kind of burden it would put on
plugin authors to deal with that issue.

63
debian/changelog vendored
View File

@ -1,63 +0,0 @@
supybot (0.79.9999+0.80.0pre2-1) unstable; urgency=high
* New upstream
* Urgency high because it still fixes previous bugs.
-- James Vega <vega.james@gmail.com> Fri, 17 Sep 2004 11:18:43 -0400
supybot (0.79.9999-1) unstable; urgency=high
* New upstream
* Urgency high because it still fixes previous bugs.
-- James Vega <vega.james@gmail.com> Mon, 6 Sep 2004 15:40:13 -0400
supybot (0.79.999-1) unstable; urgency=high
* New upstream
* Urgency high because of a bug in the Ebay plugin that would hang
the bot.
-- James Vega <vega.james@gmail.com> Tue, 31 Aug 2004 21:58:59 -0400
supybot (0.79.99-1) unstable; urgency=high
* New upstream
* Urgency high as 0.79.9 had a serious bug in src/callbacks.py
* Added a watch file
-- James Vega <vega.james@gmail.com> Mon, 30 Aug 2004 09:20:48 -0400
supybot (0.79.9-1) unstable; urgency=high
* New upstream (Closes: #268311)
* New maintainer: James Vega <vega.james@gmail.com>
* Urgency high to get the RC fix into Sarge (hopefully)
* debian/control:
+ Downgrade Recommends: python-sqlite to Suggests: python-sqlite
+ Update Build-Depends: cdbs, debhelper (>= 4.1.67)
+ Bump Standard-Version to 3.6.1.0, no changes necessary
* Convert to cdbs
-- James Vega <vega.james@gmail.com> Thu, 26 Aug 2004 17:28:03 -0400
supybot (0.77.2-1) unstable; urgency=low
* New upstream
-- Jonathan Hseu <vomjom@debian.org> Sun, 18 Apr 2004 02:58:25 -0500
supybot (0.77.1-1) unstable; urgency=low
* New upstream
* Add python-dev as a build-dep (closes: Bug#242953)
-- Jonathan Hseu <vomjom@debian.org> Sat, 10 Apr 2004 13:06:38 -0500
supybot (0.77.0-1) unstable; urgency=low
* Initial Release.
* Paste LICENSE file into copyright
-- Jonathan Hseu <vomjom@debian.org> Tue, 10 Mar 2004 00:59:46 -0600

1
debian/compat vendored
View File

@ -1 +0,0 @@
4

21
debian/control vendored
View File

@ -1,21 +0,0 @@
Source: supybot
Section: net
Priority: optional
Maintainer: James Vega <vega.james@gmail.com>
Build-Depends: debhelper (>= 4.1.67), cdbs, python (>= 2.3), python-dev (>= 2.3)
Standards-Version: 3.6.1.0
Package: supybot
Architecture: all
Depends: ${python:Depends}
Suggests: python-twisted, python-sqlite
Description: robust and user friendly Python IRC bot
Supybot is a robust (it doesn't crash), user friendly (it's easy
to configure) and programmer friendly (plugins are *extremely*
easy to write) Python IRC bot. It aims to be an adequate
replacement for most existing IRC bots. It includes a very
flexible and powerful ACL system for controlling access to
commands, as well as more than 50 builtin plugins providing
around 400 actual commands.
.
Homepage: http://supybot.sourceforge.net/

53
debian/copyright vendored
View File

@ -1,53 +0,0 @@
This package was debianized by Jonathan Hseu <vomjom@debian.org> on
Tue, 3 Feb 2004 21:45:46 -0600.
It was downloaded from http://supybot.sourceforge.net/
Upstream Authors:
/usr/share/doc/supybot/DEVS includes the names and descriptions of the
developers.
Copyright:
Files located in /usr/lib/python2.3/site-packages/supybot/others/ have their
own respective licenses, which are either commented at the top of the file, or
are held in the __license__ variable.
The licenses in that directory are either:
BSD-style, GPL, or the Python License
The first two can be found in /usr/share/common-licenses/
The Python License can be found in:
/usr/share/doc/python/copyright
The copyright for all other files are as follows (BSD-style):
Copyright (c) 2002, 2003, 2004 Jeremiah Fincher
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions, and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions, and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the author of this software nor the name of
contributors to this software may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Portions of the included source code are copyright by its original author(s)
and remains subject to its associated license.

15
debian/docs vendored
View File

@ -1,15 +0,0 @@
ACKS
BUGS
DEVS
README
RELNOTES
TODO
docs/CAPABILITIES
docs/CONFIGURATION
docs/FAQ
docs/GETTING_STARTED
docs/HACKING
docs/INTERFACES
docs/OVERVIEW
docs/PLUGIN-EXAMPLE
docs/STYLE

6
debian/rules vendored
View File

@ -1,6 +0,0 @@
#!/usr/bin/make -f
include /usr/share/cdbs/1/rules/debhelper.mk
include /usr/share/cdbs/1/class/python-distutils.mk
DEB_INSTALL_MANPAGES_supybot := docs/man/supybot-adduser.1 docs/man/supybot-newplugin.1\
docs/man/supybot.1 docs/man/supybot-wizard.1

6
debian/watch vendored
View File

@ -1,6 +0,0 @@
# Example watch control file for uscan
# Rename this file to "watch" and then you can run the "uscan" command
# to check for upstream updates and more.
# Site Directory Pattern Version Script
version=2
http://osdn.dl.sourceforge.net/supybot/Supybot-(\d.\d\d.\d+(.\d+)?)\.tar\.gz

View File

@ -1,118 +0,0 @@
Ok, some explanation of the capabilities system is probably in
order. With most IRC bots (including the ones I've written myself
prior to this one) "what a user can do" is set in one of two ways. On
the *really* simple bots, each user has a numeric "level" and commands
check to see if a user has a "high enough level" to perform some
operation. On bots that are slightly more complicated, users have a
list of "flags" whose meanings are hardcoded, and the bot checks to
see if a user possesses the necessary flag before performing some
operation. Both methods, IMO, are rather arbitrary, and force the
user and the programmer to be unduly confined to less expressive
constructs.
This bot is different. Every user has a set of "capabilities" that is
consulted every time they give the bot a command. Commands, rather
than checking for a user level of 100, or checking if the user has an
"o" flag, are instead able to check if a user has the "owner"
capability. At this point such a difference might not seem
revolutionary, but at least we can already tell that this method is
self-documenting, and easier for users and developers to understand
what's truly going on.
If that was all, well, the capability system would be "cool", but not
many people would say it was "awesome". But it *is* awesome! Several
things are happening behind the scene that make it awesome, and these
are things that couldn't happen if the bot was using numeric
userlevels or single-character flags. First, whenever a user issues
the bot a command, the command dispatcher checks to make sure the user
doesn't have the "anticapability" for that command. An anticapability
is a capability that, instead of saying "what a user can do", says
what a user *cannot* do. It's formed rather simply by adding a dash
("-") to the beginning of a capability; "rot13" is a capability, and
"-rot13" is an anticapability. Anyway, when a user issues the bot a
command, perhaps "calc" or "help", the bot first checks to make sure
the user doesn't have the "-calc" or the "-help" capabilities before
even considering responding to the user. So commands can be turned on
or off on a *per user* basis, offering fine-grained control not often
(if at all!) seen in other bots.
But that's not all! The capabilities system also supports *Channel*
capabilities, which are capabilities that only apply to a specific
channel; they're of the form "#channel,capability". Whenever a user
issues a command to the bot in a channel, the command dispatcher also
checks to make sure the user doesn't have the anticapability for that
command *in that channel*, and if the user does, the bot won't respond
to the user in the channel. Thus now, in addition to having the
ability to turn individual commands on or off for an individual user,
we can now turn commands on or off for an individual user on an
individual channel!
So when a user "foo" sends a command "bar" to the bot on channel
"#baz", first the bot checks to see if the user has the anticapability
for the command by itself, "-bar". If so, it returns right then and
there, completely ignoring the fact that the user issued that command
to it. If the user doesn't have that anticapability, then the bot
checks to see if the user issued the command over a channel, and if
so, checks to see if the user has the antichannelcapability for that
command, "#baz,-bar". If so, again, he returns right then and there
and doesn't even think about responding to the bot. If neither of
these anticapabilities are present, then the bot just responds to the
user like normal.
From a programming perspective, capabilties are easy to use and
flexible. Any command can check if a user has any capability, even
ones not thought of when the bot was originally written.
Commands/Callbacks can add their own capabilities -- it's as easy as
just checking for a capability and documenting somewhere that a user
needs that capability to do something.
From an end-user perspective, capabilities remove a lot of the mystery
and esotery of bot control, in addition to giving the user absolutely
finegrained control over what users are allowed to do with the bot.
Additionally, defaults can be set by the end-user for both individual
channels and for the bot as a whole, letting an end-user set the
policy he wants the bot to follow for users that haven't yet
registered in his user database. It's really a revolution!
There are several default capabilities the bot uses. The most
important of these is the "owner" capability. This capability allows
the person having it to use *any* command. It's best to keep this
capability reserved to people who actually have access to the shell
the bot is running on.
There is also the "admin" capability for non-owners that are highly
trusted to administer the bot appropriately. They can do things such
as change the bot's nick, globally enable/disable commands, cause the
bot to ignore a given user, set the prefixchar, report bugs, etc.
They generally cannot do administration related to channels, which is
reserved for people with the next capability.
People who are to administer channels with the bot should have the
#channel,op capability -- whatever channel they are to administrate,
they should have that channel capability for "op". For example, since
I want inkedmn to be an administrator in #supybot, I'll give him the
#supybot,op capability. This is in addition to his admin capability,
since the admin capability doesn't give the person having it control
over channels. #channel,op is used for such things as
giving/receiving ops, kickbanning people, lobotomizing the bot,
ignoring users in the channel, and managing the channel capabilities.
The #channel,op capability is also basically the equivalent of the
owner capability for capabilities involving #channel -- basically
anyone with the #channel,op capability is considered to have all
positive capabilities and no negative capabilities for #channel.
One other globally important capability exists: "trusted". This is a
command that basically says "This user can be trusted not to try and
crash the bot." It allows users to call commands like Math.icalc,
which potentially could cause the bot to begin a calculation that
could potentially never return (a calculation like 10**10**10**10).
Another command that requires the trusted capability is Utilties.re,
which (due to the regular expression implementation in Python (and any
other language that uses NFA regular expressions, like Perl or Ruby or
Lua or ...) which can allow a regular expression to take exponential
time to process). Consider what would happen if someone gave the
bot the command 're [format join "" s/./ [dict go] /] [dict go]'
Other plugins may require different capabilities; the Factoids plugin
requires #channel,factoids, the Topic plugin requires #channel,topic,
etc.

View File

@ -1,174 +0,0 @@
So you've got your Supybot up and running and there are some things
you don't like about it. Fortunately for you, chances are that these
things are configurable, and this document is here to tell you how to
configure them.
Configuration of Supybot is handled via the Config plugin, which
controls runtime access to Supybot's registry (the configuration file
generated by the supybot-wizard program you ran). The Config plugin
provides a way to get or set variables, to list the available
variables, and even to get help for certain variables. Take a moment
now to read the help for each of those commands: config, list, and
help. If you don't know how to get help on those commands, go ahead
and read our GETTING_STARTED document before this one.
Now, if you're used to the Windows registry, don't worry, Supybot's
registry is completely different. For one, it's completely plain
text. There's no binary database sensitive to corruption, it's not
necessary to use another program to edit it -- all you need is a
simple text editor. But there is at least one good idea in Windows'
registry: hierarchical configuration. Supybot's configuration
variables are organized in a hierarchy: variables having to do with
the way Supybot makes replies all start with supybot.reply; variables
having to do with the way a plugin works all start with
supybot.plugins.Plugin (where Plugin is the name of the plugin in
question). This hierarchy is nice because it means the user isn't
inundated with hundreds of unrelated and unsorted configuration
variables.
Some of the more important configuration values are located directly
under the base group, supybot. Things like the bot's nick, its ident,
etc. Along with these config values are a few subgroups that contain
other values. Some of the more prominent subgroups are: plugins
(where all the plugin-specific configuration is held), reply (where
variables affecting the way a Supybot makes its replies resides),
replies (where all the specific standard replies are kept), and
directories (where all the directories a Supybot uses are defined).
There are other subgroups as well, but these are the ones we'll use in
our example.
Using the Config plugin, you can list the values in a subgroup and get
or set any of the values anywhere in the configuration hierarchy. For
example, let's say you wanted to see what configuration values were
under the "supybot" (the base group) hierarchy. You would simply
issue this command:
<jemfinch|lambda> @config list supybot
<supybot> @capabilities, @commands, @databases, @debug, @directories, @drivers,
@log, @networks, @nick, @plugins, @protocols, @replies, @reply,
alwaysJoinOnInvite, channels, defaultIgnore, defaultSocketTimeout,
externalIP, flush, followIdentificationThroughNickChanges,
humanTimestampFormat, ident, pidFile, snarfThrottle, upkeepInterval,
and user
These are all the configuration groups and values which are under the
base "supybot" group. Actually, their full names would each have a
"supybot." appended on to the front of them, but it is omitted in the
listing in order to shorten the output. The first entries in the output are
the groups (distinguished by the @ symbol in front of them), and the rest are
the configuration values.
Okay, now that you've used the Config plugin to list configuration
variables, it's time that we start looking at individual variables and
their values.
The first (and perhaps most important) thing you should know about
each configuration variable is that they all have an associated help
string to tell you what they represent. So the first command we'll
cover is "config help". To see the help string for any value or
group, simply use the "config help" command. For example, to see what
this "supybot.snarfThrottle" configuration variable is all about, we'd
do this:
<jemfinch|lambda> @config help supybot.snarfThrottle
<supybot> jemfinch|lambda: A floating point number of seconds to throttle snarfed
URLs, in order to prevent loops between two bots snarfing the same URLs and
having the snarfed URL in the output of the snarf message. (Current value:
10.0)
Pretty simple, eh?
Now, if you're curious what the current value of a configuration
variable is, you'll use the "config" command with one argument, the
name of the variable you want to see the value of:
<jemfinch|lambda> @config supybot.reply.whenAddressedBy.chars
<supybot> jemfinch|lambda: '@'
To set this value, just stick an extra argument after the name:
<jemfinch|lambda> @config supybot.reply.whenAddressedBy.chars @$
<supybot> jemfinch|lambda: The operation succeeded.
Now, check this out:
<jemfinch|lambda> $config supybot.reply.whenAddressedBy.chars
<supybot> jemfinch|lambda: '@$'
Note that we used $ as our prefix character, and that the value of the
configuration variable changed. If I were to use the "flush" command
now, this change would be flushed to the registry file on disk (this
would also happen if I made the bot quit, or pressed Ctrl-C in the
terminal the bot was running in). Instead, I'll revert the change:
<jemfinch|lambda> $config supybot.reply.whenAddressedBy.chars @
<supybot> jemfinch|lambda: The operation succeeded.
<jemfinch|lambda> $note that this makes no response.
If you're ever curious what the default for a given configuration
variable is, use the "config default" command:
<jemfinch|lambda> @config default supybot.reply.whenAddressedBy.chars
<supybot> jemfinch|lambda: ''
Thus, to reset a configuration variable to its default value, you can
simply say:
<jemfinch|lambda> @config supybot.reply.whenAddressedBy.chars [config
default supybot.reply.whenAddressedBy.chars]
<supybot> jemfinch|lambda: The operation succeeded.
<jemfinch|lambda> @note that this does nothing
Simple, eh?
Now, let's say you want to find all configuration variables that might
be even remotely related to opping. For that, you'll want the "config
search" command. Check this out:
<jemfinch|lambda> @config search op
<supybot> jemfinch|lambda:
supybot.plugins.Enforcer.autoOp,
supybot.plugins.Enforcer.autoHalfop,
supybot.plugins.Enforcer.takeRevenge.onOps,
supybot.plugins.Enforcer.cycleToGetOps,
supybot.plugins.Topic, supybot.plugins.Topic.public,
supybot.plugins.Topic.separator,
supybot.plugins.Topic.format,
supybot.plugins.Topic.recognizeTopiclen,
supybot.plugins.Topic.default,
supybot.plugins.Topic.undo.maz, and
supybot.plugins.Relay.topicSync
Sure, it showed up all the topic-related stuff in there, but it also
showed you all the op-related stuff, too. Do note, however, that you
can only see configuration variables for plugins that you have loaded
or that you loaded in the past; if you've never loaded a plugin,
there's no way for the bot to know what configuration variables it
registers.
Some people might like editing their registry file directly rather
than manipulating all these things through the bot. For those people,
we offer the "config reload" command, which reloads both registry
configuration and user/channel/ignore database configuration. Just
edit the interesting files and then give the bot the "config reload"
command and it'll work as expected. Do note, however, that Supybot
flushes his configuration files and databases to disk every hour or
so, and if this happens after you've edited your configuration files
but before you reload your changes, you could lose the changes you
made. To prevent this, set the supybot.flush value to Off, and no
automatic flushing will occur.
Many configuration variables can be specific to individual channels.
The Config plugin provides an easy way to configure something for a
specific channel; for instance, in order to set the prefix chars for a
specific channel, do this in that channel:
config channel supybot.reply.whenAddressedBy.chars !
That'll set the prefix chars in the channel that message is sent in to
!. Voila, channel-specific values! Also, note that when using the
Config plugin's list command, channel-specific values are preceded by
a '#' character to indicate such.
Anyway, that's about it for configuration. Have fun, and enjoy your
configurable bot!

View File

@ -1,25 +0,0 @@
JADE=/usr/bin/jade
JADETEX=/usr/bin/jadetex
DVIPDF=/usr/bin/dvipdfm
HTMLSTYLESHEET=/usr/share/sgml/docbook/stylesheet/dsssl/modular/html/docbook.dsl
PRINTSTYLESHEET=/usr/share/sgml/docbook/stylesheet/dsssl/modular/print/docbook.dsl
html: example.sgml capabilities.sgml
$(JADE) -t xml -d $(HTMLSTYLESHEET) $<
example.dvi: example.sgml
$(JADE) -t tex -d $(PRINTSTYLESHEET) $<
$(JADETEX) $(addsuffix .tex, $(basename $<))
example.pdf: example.dvi
$(DVIPDF) -o $(addsuffix .pdf, $(basename $<)) $<
capabilities.dvi: capabilities.sgml
$(JADE) -t tex -d $(PRINTSTYLESHEET) $<
$(JADETEX) $(addsuffix .tex, $(basename $<))
capabilities.pdf: capabilities.dvi
$(DVIPDF) -o $(addsuffix .pdf, $(basename $<)) $<
clean:
rm -f *.html

View File

@ -1,177 +0,0 @@
The Official Supybot DocBook Metadocumentation
(or, How Does One SGML File Turn Into All Those Document Formats?)
Okay, though this isn't the case yet, ideally all of Supybot's documentation
can and will be done using DocBook and DocBook-related tools as well as a few
custom extensions that I've written.
- How does DocBook work?
First things first, you have to understand sort of how DocBook works in order
to figure out how our documentation gets generated from just one file. What
DocBook is, basically, is just a DTD (Document Type Definition). What that
means is that it simply specifies how a document can be structured and still be
considered a valid document by placing restrictions on what elements go where.
It's a popular DTD because it is structured very well and it's not only fairly
generic, but it also has nice elements that make documenting things (such as
supybot) rather easy. It focuses on structure and content instead of
presentation which is what makes it nice for writing things which are output
format agnostic.
So, let's say we've written a proper DocBook document, now what? Well, using
an output formatting tool and a stylesheet, you create whatever form of output
you want. What we use for producing the outptut is jade, and DocBook comes
with a few stylesheets that work with that tool to create output formats like
HTML and TeX. From the TeX file we produce a DVI (device independent) file
with latex, and from that we produce the print formats of our documents, like
PDF and Postscript using tools like dvips and dvipdfm.
- What extra stuff do we do?
Well, since our documents all have to do with an IRC bot, there are some very
common things that we talk about a lot that we might like to format specially.
For example, when we discuss a particular command for the bot we might like to
have that text appear slightly different to emphasize the fact that it is
special. So, for the commonly used items that weren't already covered by
DocBook's DTD, I added elements into a new DTD which just extends DocBook's
DTD. So now we have elements like <nick> and <channel> and <botcommand> that
we can use for our documentation.
Of course, now that we have used a DTD with more stuff in it (than DocBook),
the stylesheets that DocBook provides won't do any special formatting for those
new elements so we have to write new stylesheets as well. Once again I just
extended the existing ones with formatting instructions on how to treat the new
elements. So with this done, now our HTML and TeX (and whatever else) output
will be properly formatted.
- How do I make my own changes to the DTD and stylesheets?
Primarily, you don't :) Ask me (Strike) first about it, and I will generally
write them for you or explain a better way of doing things. This is especially
true for the DTD, because that must remain consistent everywhere we write/read
supybot docs based on it. The stylesheets are more lax and can be modified to
produce whatever kind of output you wish.
So, with that warning/reminder out of the way, here's how to modify each
anyway. This doesn't really assume any knowledge of how to write a DTD, nor is
it an exhaustive reference on writing one, so don't treat it as such. I'm
basically just going to explain how to add extra elements that will play well
with the DocBook DTD.
-- Adding an element to the DTD
If you've decided that there's a certain "thing" that's mentioned a lot in the
documentation and deserves classification (for potential special formatting),
you'll probably want to create a new element (or set of elements) for it. I'll
walk you through how I added the "nick" element to our DTD (though many/most of
the elements I added follow an identical process).
The very first thing you need to figure out is: where in my document does this
element "fit". That is to say, what elements should/can rightly contain this
particular one? In the case of the "nick" element, it's basically always an
inline-formatted deal that belongs in paragraphs for the most part. For those
of you scratching your heads at that last sentence, perhaps thinking "okay, so
how are we supposed to know what is relevant?" I say, "don't worry, I learned
by example as well." Basically, I just looked through the DocBook DTD and
figured out where things belong. Now, even if you don't know the DocBook DTD
front-to-back, you can still peruse it to figure out where your new element
belongs. Obviously, you should probably know *some* DocBook to figure out what
each element means, but luckily all of our docs have been converted to DocBook
and serve as nice examples of the usage of many elements :)
Now, to figure out where something like "nick" belongs. In many ways, a nick
is sort of like a variable name (at least in documentation usage). So, the
element I chose to base it off of was "varname". If you have the DocBook DTD
installed (as you should if you intend on making extensions to it), the varname
element definition is contained in the dbpoolx.mod filename (in Debian, it's
under /usr/share/sgml/docbook/dtd/4.2). How did I know this? Well, grep is
your friend and mine too, and dbpoolx is the only filename that shows up when
grepping for "varname" in the DocBook DTD directory. So, we open up dbpoolx.mod and search for varname. The first thing we find it in looks like this:
<!ENTITY % tech.char.class
"action|application
|classname|methodname|interfacename|exceptionname
|ooclass|oointerface|ooexception
|command|computeroutput
|database|email|envar|errorcode|errorname|errortype|errortext|filename
|function|guibutton|guiicon|guilabel|guimenu|guimenuitem
|guisubmenu|hardware|interface|keycap
|keycode|keycombo|keysym|literal|constant|markup|medialabel
|menuchoice|mousebutton|option|optional|parameter
|prompt|property|replaceable|returnvalue|sgmltag|structfield
|structname|symbol|systemitem|token|type|userinput|varname
%ebnf.inline.hook;
%local.tech.char.class;">
Hmm, this doesn't look like a definition of varname (to me, but I sort of
cheated by having read about DocBook before-hand ;)), but it will be important
to remember for later. Let's try and find the element definition for varname
(so, basically, let's look for the first line that starts with "<!ELEMENT ").
The first line I come up with when I search is:
<!ELEMENT varname %ho; (%smallcptr.char.mix;)*>
Rather than write a separate tutorial for interpreting DTDs, I found a good
SGML tutorial online that explains everything necessary to help you parse the
DocBook DTD to figure out what the varname element really is, as well as to
help you learn all the stuff necessary for what we will cover in creating our
new nick element. That tutorial is at
http://www.w3.org/TR/WD-html40-970708/intro/sgmltut.html#howtodtd (it's for
reading the HTML DTD, but it applies to any DTD).
So, now that we understand how to write/read things for a DTD, we arrive at the
time where we can write the actual definition of our "nick" element:
<!ELEMENT Nick - - ((%smallcptr.char.mix;)+)>
As we learned in the above tutorial, this means that we are creating an element
named "nick", which must have start and end tags, and is defined to contain one
or more of whatever is in "smallcptr.char.mix". And rather than hunt through
the DocBook DTD to figure out what that is, for now we'll just live with the
fact that whatever can go into a DocBook varname can go into our new nick
element. If you feel so inclined, feel free to try and define the content
model for nick to only include valid nick characters. It's perfectly doable,
and I'll probably do it at some point but I haven't yet.
Since we're extending the DocBook DTD, I also decided that it'd be nice to
follow the element creation conventions observed in their DTD, so there are a
few more lines associated with our new nick element. All of them are related
to the attributes of the element, and allowing for them to be extended by
external DTDs (much like we are doing, only we aren't changing attributes of
existing elements, just adding our own). The first one is:
<!ENTITY % local.nick.attrib "">
This basically defines an empty entity named local.nick.attrib which we will
include so that if anyone chooses to extend the nick attributes, all they have
to do is redefine local.nick.attrib.
<!ENTITY % nick.role.attrib "%role.attrib;">
To tell you the truth, I'm not entirely sure what this is for, but it follows the DocBook convention :)
<!ATTLIST Nick
%common.attrib;
%local.nick.attrib;
%nick.role.attrib;
>
This is, of course, our attribute list for our nick element. It consists of
the two things we just defined as well as common.attrib which contains things
like "id" and whatnot which all DocBook elements are expected to have.
-- Extending the DocBook DTD to recognize new elements
So, that's all you need to define your new element. But, we're not done just
yet! We're almost there, we just need to make it so that it works with the
existing DocBook elements, otherwise it's no good to us. Since we defined our
element to esentially be the same as varname, it probably belongs at the same
place within the DocBook schema as varname. Do you remember when we had that
large entity definition that wasn't what we were looking for at the time though
I said it'd be important later? Well, later is now. So, what that line tells
us is what class of elements DocBook has varname in, which is
"tech.char.class". And thanks to the DocBook convention of defining a
local.<classname> entity that we can extend, all we have to do is redefine
local.tech.char.class to contain "nick", and we are done.
You may notice, however, that we don't actually put varname right into the
local.tech.char.class entity, but instead we create our own
supybot.tech.char.class class of elements that are supybot-specific (and are
the equivalent of DocBook's tech.char.class elements) and instead, put all of
those into the local.tech.char.class entity. Basically, we just go through one
more level of indirection.

View File

@ -1,221 +0,0 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article>
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Supybot capabilities system explanation</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>18 Feb 2004</date>
<revremark>Initial Docbook translation</revremark>
</revision>
<revision>
<revnumber>0.2</revnumber>
<date>04 Sep 2004</date>
<revremark>Update Docbook translation</revremark>
</revision>
</revhistory>
</articleinfo>
<sect1>
<title>Introduction</title>
<subtitle>
Supybot's capabilities overview and comparisons to other bots
</subtitle>
<para>
Ok, some some explanation of the capabilities system is probably
in order. With most IRC bots (including the ones I've written
myself prior to this one) &ldquo;what a user can do&rdquo; is set
in one of two ways. On the <emphasis>really</emphasis> simple
bots, each user has a numeric &ldquo;level&rdquo; and commands
check to see if a user has a &ldquo;high enough level&rdquo; to
perform some operation. On bots that are slightly more
complicated, users have a list of &ldquo;flags&rdquo; whose
meanings are hardcoded, and the bot checks to see if a user
possesses the necessary flag before performing some operation.
Both methods, IMO, are rather arbitrary, and force the user and
the programmer to be unduly confined to less expressive
constructs.
</para>
<para>
This bot is different. Every user has a set of
&ldquo;capabilities&rdquo; that is consulted every time they give
the bot a command. Commands, rather than checking for a user
level of 100, or checking if the user has an <varname>o</varname>
flag, are instead able to check if a user has the
<capability>owner</capability> capability. At this point such a
difference might not seem revolutionary, but at least we can
already tell that this method is self-documenting, and easier for
users and developers to understand what's truly going on.
</para>
</sect1>
<sect1>
<title>What sets supybot's capabilities apart</title>
<para>
If that was all, well, the capability system would be
&ldquo;cool&rdquo;, but not many people would say it was
&ldquo;awesome&rdquo;. But it <emphasis>is</emphasis> awesome!
Several things are happening behind the scene that make it
awesome, and these are things that couldn't happen if the bot was
using numeric userlevels or single-character flags. First,
whenever a user issues the bot a command, the command dispatcher
checks to make sure the user doesn't have the
&ldquo;anticapability&rdquo; for that command. An anticapability is
a capability that, instead of saying &ldquo;what a user can
do&rdquo;, says what a user <emphasis>cannot</emphasis> do. It's
formed rather simply by adding a dash (&ldquo;-&rdquo;) to the
beginning of a capability; <botcommand>rot13</botcommand> is a
capability, and <botcommand>-rot13</botcommand> is an
anticapability. Anyway, when a user issues the bot a command,
perhaps <botcommand>calc</botcommand> or
<botcommand>help</botcommand>, the bot first checks to make sure
the user doesn't have the <capability>-calc</capability> or the
<capability>-help</capability> capabilities before even
considering responding to the user. So commands can be turned on
or off on a <emphasis>per user</emphasis> basis, offering
finegrained control not often (if at all!) seen in other bots.
</para>
<sect2>
<title>Channel capabilities</title>
<para>
But that's not all! The capabilities system also supports
<emphasis>Channel</emphasis> capabilities, which are
capabilities that only apply to a specific channel; they're of
the form <capability>#channel,capability</capability>.
Whenever a user issues a command to the bot in a channel, the
command dispatcher also checks to make sure the user doesn't
have the anticapability for that command <emphasis>in that
channel</emphasis> and if the user does, the bot won't respond
to the user in the channel. Thus now, in addition to having
the ability to turn individual commands on or off for an
individual user, we can now turn commands on or off for an
individual user on an individual channel!
</para>
<para>
So when a user <nick>foo</nick> sends a command
<botcommand>bar</botcommand> to the bot on channel
<channel>#baz</channel>, first the bot checks to see if the
user has the anticapability for the command by itself,
<capability>-bar</capability>. If so, it returns right then
and there, compltely ignoring the fact that the user issued
that command to it. If the user doesn't have that
anticapability, then the bot checks to see if the user issued
the command over a channel, and if so, checks to see if the
user has the antichannelcapability for that command,
<capability>#baz,-bar</capability>. If so, again, he returns
right then and there and doesn't even think about responding
to the bot. If neither of these anticapabilities are present,
then the bot just responds to the user like normal.
</para>
</sect2>
</sect1>
<sect1>
<title>Motivations behind the capabilities system</title>
<sect2>
<title>A programmer's perspective</title>
<para>
From a programming perspective, capabilties are easy to use
and flexible. Any command can check if a user has any
capability, even ones not thought of when the bot was
originally written. Commands/Callbacks can add their own
capabilities &ndash; it's as easy as just checking for a
capability and documenting somewhere that a user needs that
capability to do something.
</para>
</sect2>
<sect2>
<title>An end-user's perspective</title>
<para>
From an end-user perspective, capabilities remove a lot of the
mystery and esotery of bot control, in addition to giving the
user absolutely finegrained control over what users are
allowed to do with the bot. Additionally, defaults can be set
by the end-user for both individual channels and for the bot
as a whole, letting an end-user set the policy he wants the
bot to follow for users that haven't yet registered in his
user database.
</para>
</sect2>
<para>
It's really a revolution!
</para>
</sect1>
<sect1>
<title>Hard-coded supybot capabilities</title>
<para>
There are several default capabilities the bot uses. The most
important of these is the <capability>owner</capability>
capability. This capability allows the person having it to use
<emphasis>any</emphasis> command. It's best to keep this
capability reserved to people who actually have access to the
shell the bot is running on.
</para>
<para>
There is also the <capability>admin</capability> capability for
non-owners that are highly trusted to administer the bot
appropriately. They can do things such as change the bot's nick,
globally enable/disable commands, cause the bot to ignore a given
user, set the prefixchar, report bugs, etc. They generally cannot
do administration related to channels, which is reserved for
people with the next capability.
</para>
<para>
People who are to administer channels with the bot should have the
<capability>#channel,op</capability> capability &ndash; whatever
channel they are to administrate, they should have that channel
capability for <capability>op</capability>. For example, since I
want <nick>inkedmn</nick> to be an administrator in
<channel>#supybot</channel>, I'll give him the
<capability>#supybot,op</capability> capability. This is in
addition to his <capability>admin</capability> capability, since
the <capability>admin</capability> capability doesn't give the
person having it control over channels.
<capability>#channel.op</capability> is used for such things as
giving/receiving ops, kickbanning people, lobotomizing the bot,
ignoring users in the channel, and managing the channel
capabilities. The <capability>#channel,op</capability> capability
is also basically the equivalent of the owner capability for
capabilities involving <channel>#channel</channel> &ndash;
basically anyone with the <capability>#channel,op</capability>
capability is considered to have all positive capabilities and no
negative capabilities for <channel>#channel</channel>.
</para>
<para>
One other globally important capability exists:
<capability>trusted</capability>. This is a command that
basically says &ldquo;This user can be trusted not to try and
crash the bot.&rdquo; It allows users to call commands like
<botcommand>Math.icalc</botcommand>, which potentially could cause the
bot to begin a calculation that could potentially never return (a
calculation like 10**10**10**10). Another command that requires
the trusted capability is <botcommand>Utilties.re</botcommand>, which
(due to the regular expression implementation in Python (and any
other language that uses NFA regular expressions, like Perl or
Ruby or Lua or &hellip;) which can allow a regular expression to
take exponential time to process). Consider what would happen if
the someone gave the bot the command <literal>re [strjoin "" s/./
[dict go] /] [dict go]</literal>.
</para>
</sect1>
<sect1>
<title>Other capabilities</title>
<para>
Other plugins may require different capabilities; the
<plugin>Factoids</plugin> plugin requires
<capability>#channel,factoids</capability>, the <plugin>Topic</plugin>
plugin requires <capability>#channel,topic</capability>, etc.
</para>
</sect1>
</article>

View File

@ -1,316 +0,0 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article>
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<author>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Supybot configuration system explanation</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>18 Feb 2004</date>
<revremark>Initial Docbook translation</revremark>
</revision>
<revision>
<revnumber>0.2</revnumber>
<date>26 Feb 2004</date>
<revremark>Conversion to Supybot DTD</revremark>
</revision>
<revision>
<revnumber>0.3</revnumber>
<date>4 Sep 2004</date>
<revremark>Update Docbook translation</revremark>
</revision>
</revhistory>
</articleinfo>
<sect1>
<title>Introduction</title>
<para>
So you've got your Supybot up and running and there are some
things you don't like about it. Fortunately for you, chances are
that these things are configurable, and this document is here to
tell you how to configure them.
</para>
<para>
Configuration of Supybot is handled via the
<plugin>Config</plugin> plugin, which controls runtime access to
Supybot's registry (the configuration file generated by the
<script>supybot-wizard</script> program you ran). The
<plugin>Config</plugin> plugin provides a way to get or set
variables, to list the available variables, and even to get help
for certain variables. Take a moment now to read the help for
each of those commands: <botcommand>config</botcommand>,
<botcommand>list</botcommand>, and
<botcommand>help</botcommand>. If you don't know how to get help on
those commands, go ahead and read our
<filename>GETTING_STARTED</filename> document before this one.
</para>
</sect1>
<sect1>
<title>Supybot's registry</title>
<para>
Now, if you're used to the Windows registry, don't worry,
Supybot's registry is completely different. For one, it's
completely plain text. There's no binary database sensitive to
corruption, it's not necessary to use another program to edit it
&ndash; all you need is a simple text editor. But there is at
least one good idea in Windows' registry: hierarchical
configuration. Supybot's configuration variables are organized in
a hierarchy: variables having to do with the way Supybot makes
replies all start with
<registrygroup>supybot.reply</registrygroup>; variables having to
do with the way a plugin works all start with
<registrygroup>supybot.plugins.Plugin</registrygroup> (where
<plugin>Plugin</plugin> is the name of the plugin in question).
This hierarchy is nice because it means the user isn't inundated
with hundreds of unrelated and unsorted configuration variables.
</para>
<para>
Some of the more important configuration values are located
directly under the base group,
<registrygroup>supybot</registrygroup>. Things like the bot's
nick, its ident, etc. Along with these config values are a few
subgroups that contain other values. Some of the more prominent
subgroups are: <registrygroup>plugins</registrygroup> (where all
the plugin-specific configuration is held),
<registrygroup>reply</registrygroup> (where variables affecting
the way a Supybot makes its replies resides),
<registrygroup>replies</registrygroup> (where all the specific
standard replies are kept), and
<registrygroup>directories</registrygroup> (where all the
directories a Supybot uses are defined). There are other
subgroups as well, but these are the ones we'll use in our
example.
</para>
<sect2>
<title>Config plugin commands</title>
<sect3>
<title>Listing registry contents</title>
<para>
Using the <plugin>Config</plugin> plugin, you can list
the values in a subgroup and get or set any of the values
anywhere in the configuration hierarchy. For example,
let's say you wanted to see what configuration values were
under the <registrygroup>supybot</registrygroup> (the base
group) hierarchy. You would simply issue this command:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config list supybot
&lt;supybot&gt; @capabilities, @commands, @databases, @debug, @directories, @drivers,
@log, @networks, @nick, @plugins, @protocols, @replies, @reply,
alwaysJoinOnInvite, channels, defaultIgnore, defaultSocketTimeout,
externalIP, flush, followIdentificationThroughNickChanges,
humanTimestampFormat, ident, pidFile, snarfThrottle, upkeepInterval,
and user
</ircsession>
<para>
These are all the configuration groups and values which
are under the base <registrygroup>supybot</registrygroup>
group. Actually, their full names would each have a
&ldquo;supybot.&rdquo; appended on to the front of them,
but it is omitted in the listing in order to shorten the
output. The first entries in the output are the groups
(distinguished by the @ symbol in front of them), and the
rest are the configuration values.
</para>
</sect3>
<sect2>
<title>Supybot's registry</title>
<sect3>
<title>Dealing with registry values</title>
<para>
Okay, now that you've used the <plugin>Config</plugin>
plugin to list configuration variables, it's time that we
start looking at individual variables and their values.
</para>
<sect4>
<title>Built-in help for registry values</title>
<para>
The first (and perhaps most important) thing you
should know about each configuration variable is that
they all have an associated help string to tell you
what they represent. So the first command we'll cover
is <botcommand>config help</botcommand>. To see the
help string for any value or group, simply use the
<botcommand>config help</botcommand> command. For
example, to see what this
<registrygroup>supybot.snarfThrottle</registrygroup>
configuration variable is all about, we'd do this:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config help supybot.snarfThrottle
&lt;supybot&gt; jemfinch|lambda: A floating point number of seconds to throttle snarfed
URLs, in order to prevent loops between two bots snarfing the same URLs and
having the snarfed URL in the output of the snarf message. (Current value:
10.0)
</ircsession>
<para>
Pretty simple, eh?
</para>
</sect4>
<sect4>
<title>Getting/setting registry values</title>
<para>
Now, if you're curious what the current value of a
configuration variable is, you'll use the
<botcommand>config</botcommand> command with one
argument, the name of the variable you want to see the
value of:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config supybot.reply.whenAddressedBy.chars
&lt;supybot&gt; jemfinch|lambda: '@'
</ircsession>
<para>
To set this value, just stick an extra argument after
the name:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config supybot.reply.whenAddressedBy.chars @$
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
</ircsession>
<para>
Now, check this out:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; $config supybot.reply.whenAddressedBy.chars
&lt;supybot&gt; jemfinch|lambda: '@$'
</ircsession>
<para>
Note that we used <literal>$</literal> as our prefix
character, and that the value of the configuration
variable changed. If I were to use the
<botcommand>flush</botcommand> command now, this
change would be flushed to the registry file on disk
(this would also happen if I made the bot quit, or
pressed
<keycombo>
<keycap>Ctrl</keycap>
<keycap>C</keycap>
</keycombo>
in the terminal the bot was running in). Instead,
I'll revert the change:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; $config supybot.reply.whenAddressedBy.chars @
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; $note that this makes no response.
</ircsession>
<para>
If you're ever curious what the default for a given
configuration variable is, use the <botcommand>config
default</botcommand> command:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config default supybot.reply.whenAddressedBy.chars
&lt;supybot&gt; jemfinch|lambda: ''
</ircsession>
<para>
Thus, to reset a configuration variable to its default
value, you can simply say:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config supybot.reply.whenAddressedBy.chars [config default
supybot.reply.whenAddressedBy.chars]
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; @note that this does nothing
</ircsession>
<para>
Simple, eh?
</para>
</sect4>
</sect3>
<sect3>
<title>Searching the registry</title>
<para>
Now, let's say you want to find all configuration
variables that might be even remotely related to opping.
For that, you'll want the <botcommand>config
search</botcommand> command. Check this out:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config search op
&lt;supybot&gt; jemfinch|lambda:
supybot.plugins.Enforcer.autoOp,
supybot.plugins.Enforcer.autoHalfop,
supybot.plugins.Enforcer.takeRevenge.onOps,
supybot.plugins.Enforcer.cycleToGetOps,
supybot.plugins.Topic, supybot.plugins.Topic.public,
supybot.plugins.Topic.separator,
supybot.plugins.Topic.format,
supybot.plugins.Topic.recognizeTopiclen,
supybot.plugins.Topic.default,
supybot.plugins.Topic.undo.maz, and
supybot.plugins.Relay.topicSync
</ircsession>
<para>
Sure, it showed up all the topic-related stuff in there,
but it also showed you all the op-related stuff, too. Do
note, however, that you can only see configuration
variables for plugins that you have loaded or that you
loaded in the past; if you've never loaded a plugin,
there's no way for the bot to know what configuration
variables it registers.
</para>
<para>
Some people might like editing their registry file
directly rather than manipulating all these things through
the bot. For those people, we offer the
<botcommand>config reload</botcommand> command, which
reloads both registry configuration and
user/channel/ignore database configuration. Just edit the
interesting files and then give the bot the
<botcommand>config reload</botcommand> command and it'll
work as expected. Do note, however, that Supybot flushes
his configuration files and databases to disk every hour
or so, and if this happens after you've edited your
configuration files but before you reload your changes,
you could lose the changes you made. To prevent this, set
the <registrygroup>supybot.flush</registrygroup> value to
<literal>Off</literal>, and no automatic flushing will
occur.
</para>
</sect3>
<sect3>
<title>Channel-specific configuration</title>
<para>
Many configuration variables can be specific to individual
channels. The <plugin>Config</plugin> plugin provides an
easy way to configure something for a specific channel;
for instance, in order to set the prefix chars for a
specific channel, do this in that channel:
</para>
<ircsession>
config channel supybot.reply.whenAddressedBy.chars !
</ircsession>
<para>
That'll set the prefix chars in the channel that message
is sent in to <literal>!</literal>. Voila,
channel-specific values!
</para>
</sect3>
</sect2>
</sect1>
<sect1>
<title>All done!</title>
<para>
Anyway, that's about it for configuration. Have fun, and enjoy
your configurable bot!
</para>
</sect1>
</article>

View File

@ -1,359 +0,0 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article class="faq">
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Supybot Frequently Asked Questions</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>18 Feb 2004</date>
<revremark>Initial Docbook translation</revremark>
</revision>
<revision>
<revnumber>0.2</revnumber>
<date>26 Feb 2004</date>
<revremark>Changed to Supybot DTD</revremark>
</revision>
</revhistory>
</articleinfo>
<qandaset defaultlabel="qanda">
<qandaentry>
<question>
<para>
Why does my bot not recognize me or tell me that I don't
have the <capability>owner</capability> capability?
</para>
</question>
<answer>
<para>
Because you've not given it anything to recognize you
from! You'll need to identify with the bot
(<botcommand>help identify</botcommand> to see how that
works) or add your hostmask to your user record
(<botcommand>help addhostmask</botcommand> to see how that
works) for it to know that you're you. You may wish to
note that <botcommand>addhostmask</botcommand> can accept
a password; rather than identify, you can send the command
<botcommand>addhostmask myOwnerUser [hostmask]
myOwnerUserPassword</botcommand> and the bot will add your
current hostmask to your owner user (of course, you should
change <literal>myOwnerUser</literal> and
<literal>myOwnerUserPassword</literal> appropriately for
your bot).
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
How do I make Supybot op my users?
</para>
</question>
<answer>
<para>
First, you'll have to make sure that your users register
with the bot. They can do this with the
<botcommand>register</botcommand> command. After they do
so, you'll want to add the
<capability>#channel,op</capability> capability to their
user. Use the <botcommand>channel
addcapability</botcommand> command to do this. After
that, your users should be able to use the
<botcommand>op</botcommand> command to get ops.
</para>
<para>
If you want your users to be auto-opped when they join the
channel, you'll need to load the <plugin>Enforcer</plugin>
plugin and turn its <registrygroup>autoOp</registrygroup>
configuration variable on. Use the
<botcommand>config</botcommand> command to do so. Here's
an example of how to do these steps:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; I'm going to make an example session for giving
you auto-ops, for our FAQ.
&lt;dunk1&gt; ah ok ;]
&lt;jemfinch|lambda&gt; First, I need you to register with supybot, using
the "register" command (remember to send it in private).
&lt;dunk1&gt; done
&lt;jemfinch|lambda&gt; what name are you registered under?
&lt;dunk1&gt; dunk1
&lt;jemfinch|lambda&gt; ok, cool.
&lt;jemfinch|lambda&gt; @channel addcapability dunk1 op
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; now use the "op" command to get ops.
&lt;dunk1&gt; @op
&mdash; supybot gives channel operator status to dunk1
&lt;dunk1&gt; works!
&lt;dunk1&gt; ;]
&lt;jemfinch|lambda&gt; @load Enforcer
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; @config channel supybot.plugins.Enforcer.autoOp On
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; ok, now cycle the channel (part and then rejoin)
&lt;&ndash; dunk1 (dunker@freebsd.nl) has left #supybot
&ndash;&gt; dunk1 (dunker@freebsd.nl) has joined #supybot
&mdash; supybot gives channel operator status to dunk1
&lt;jemfinch|lambda&gt; cool, thanks :)
</ircsession>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Can users with the <capability>admin</capability>
capability change configuration variables?
</para>
</question>
<answer>
<para>
Currently, no. Since this is the first release of Supybot
that uses the registry, we wanted to stay on the
conservative side and require the
<capability>owner</capability> capability for changing all
non-channel-related configuration variables. Feel free to
make your case to us as to why a certain configuration
variable should only require the
<capability>admin</capability> capability instead of the
<capability>owner</capability> capability, and if we agree
with you, we'll change it for the next release.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Can Supybot do factoids?
</para>
</question>
<answer>
<para>
Supybot most certainly can! In fact, we offer three
full-fledged factoids-related plugins!
</para>
<para>
<plugin>Factoids</plugin> (written by
<nick>jemfinch</nick>) is Supybot's original
factoids-related plugin. It offers full integration with
Supybot's nested commands as well as a complete 1:n key to
factoid ratio, with lookup by individual number. Factoids
also uses a channel-specific database instead of a global
database though that's configurable with the
<registrygroup>supybot.databases.plugins.channelSpecific</registrygroup>
configuration variable.
</para>
<para>
<plugin>MoobotFactoids</plugin> (written by
<nick>Strike</nick>) is much more full-featured, offering
users the ability to define factoids in a slightly more
user-friendly way, as well as parsing factoids to handle
&lt;reply&gt;, &lt;action&gt;, and alternations (defining
a factoid &ldquo;test&rdquo; as
&ldquo;&lt;reply&gt;(foo|bar|baz)&rdquo; will make the bot
send &ldquo;foo&rdquo; or &ldquo;bar&rdquo; or
&ldquo;baz&rdquo; to the channel (without the normal
&ldquo;test is &rdquo; at the beginning)). If you're
accustomed to Moobot's factoids or Blootbot's factoids,
then this is the Factoids plugin for you. Unfortunately,
due to the more natural definition syntax (required to be
comaptible with Moobot) you can't define Factoids with
nested commands; you'll have to evaluate the command first
and then copy the result into your factoid definition.
</para>
<para>
<plugin>Infobot</plugin> (written by
<nick>jamessan</nick>) is used for Infobot compatibility;
if you still want the basic functionality of Infobot, this
is the plugin to use.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Can I import my Infobot/Blootbot/Moobot factoids into
Supybot?
</para>
</question>
<answer>
<para>
As of present, we have no automated way to do so.
<nick>Strike</nick> has written a few scripts for
importing a Moobot database into MoobotFactoids, however,
so you'll want to talk to him about helping you with that.
We're certainly happy to help you convert such databases;
if you can provide us with such a database exported to a
flat file, we can probably do the rest of the work to
write a script that imports it into a database for one of
our factoids-related plugins.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Do I really have to use separate databases for each
channel?
</para>
</question>
<answer>
<para>
Of course not! We default to separate databases for each
channel because, well, that's what <nick>jemfinch</nick>
always thought was reasonable. Anyway, if you change the
configuration variable
<registrygroup>supybot.databases.plugins.channelSpecific</registrygroup>
to <literal>False</literal> instead of
<literal>True</literal>, for <emphasis>most</emphasis>
databases, each channel will share the same database (the
exceptions are <plugin>ChannelStats</plugin>,
<plugin>Herald</plugin>, <plugin>Seen</plugin>, and
<plugin>WordStats</plugin>, which are inherently rather
channel-based).
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Karma doesn't seem to work for me.
</para>
</question>
<answer>
<para>
<plugin>Karma</plugin> by default doesn't acknowledge
karma updates. If you check the karma of whatever you
increased/decreased, you'll note that your increment or
decrement still took place. If you'd rather
<plugin>Karma</plugin> acknowledge karma updates, change
the
<registrygroup>supybot.plugins.Karma.response</registrygroup>
configuration variable to <literal>On</literal>.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
I added an alias, but it doesn't work!
</para>
</question>
<answer>
<para>
Take a look at <botcommand>help &lt;alias you
added&gt;</botcommand>. If the alias the bot has listed
doesn't match what you're giving it, chances re you need
to quote your alias in order for the brackets not to be
evaluated. For instance, if you're adding an alias to
give you a link to your homepage, you need to say:
<para>
<ircsession>
alias add mylink "format concat http://myhost.com/
[urlquote $1]"
</ircsession>
<para>
and not:
</para>
<ircsession>
alias add mylink format concat http://myhost.com/
[urlquote $1]
</ircsession>
<para>
The first version works; the second version will always
return the same url.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Is there a command that can tell me what capability
another command requires?
</para>
</question>
<answer>
<para>
No, there isn't, and there probably never will be.
Commands have the flexibility to check any capabilities
they wish to check; while this flexibility is useful, it
also makes it hard to guess what capability a certain
command requires. We could make a solution that would
work in a large majority of cases, but it wouldn't (and
couldn't!) be absolutely correct in all circumstances, and
since we're anal and we hate doing things halfway, we
probably won't ever add this partial solution.
</para>
<para>
Besides, is the error message so bad? If we did have such
a command, many users would call the command, see that
they could perform it, and then run the command, thus
doubling the activity in the channel. Is that something
you want?
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
How do I make my Supybot connect to multiple servers?
</para>
</question>
<answer>
<para>
Just use the <botcommand>connect</botcommand> command in
the <plugin>Owner</plugin> plugin. Easy as pie!
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
I found a bug, what do I do?
</para>
</question>
<answer>
<para>
Submit it on Sourceforge through our Sourceforge project
page:
<ulink
url="http://sourceforge.net/tracker/?group_id=58965&amp;atid=489447">
http://sourceforge.net/tracker/?group_id=58965&amp;atid=489447
</ulink>. If Sourceforge happens to be down when you try
to submit your bug, then post it in the "Supybot Developer
Discussion" forum at our forums at
<ulink url="http://forums.supybot.org">
http://forums.supybot.org/
</ulink>. If that doesn't work, email
<email>supybot-bugs@lists.sourceforge.net</email>. If
that doesn't work, email
<email>jemfinch@supybot.org</email>. If that doesn't
work, find yourself some carrier pigeons and &hellip; hah!
You thought I was serious!
</para>
<para>
Anyway, when you submit your bug, we'll need several
things. If the bug involved an uncaught exception, we
need the traceback (basically the stuff from
&ldquo;Uncaught exception in &hellip;&rdquo; to the next
log entry). We'd also like to see the commands that
caused the bug, or happened around the time you saw the
bug. If the bug involved a database, we'd love to see the
database. Remember, it's always worse to send us too
little information in a bug report than too much.
</para>
</answer>
</qandaentry>
</qandaset>
</article>

View File

@ -1,297 +0,0 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article>
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Getting started with Supybot</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>18 Feb 2004</date>
<revremark>Initial Docbook translation</revremark>
</revision>
</revhistory>
</articleinfo>
<sect1>
<title>Introduction</title>
<para>
Ok, so you've decided to try out Supybot. That's great! The more
people who use Supybot, the more people can submit bugs and help
us to make it the best IRC bot in the world :)
</para>
<para>
First things first: Supybot <emphasis>requires</emphasis> Python
2.3. There ain't no getting around it. If you're a Python
developer, you probably know how superior 2.3 is to previous
incarnations. If you're not, just think about the difference
between a bowl of plain vanilla ice cream and a banana split. Or
something like that. Either way, <emphasis>we're</emphasis>
Python developers and we like banana splits.
</para>
</sect1>
<sect1>
<title>Installing the bot and its utilities</title>
<para>
So what do you do? First thing you'll want to do is run (with
root/admin privileges) <application>python setup.py
install</application>. This will install Supybot globally. If
you need to install locally for whatever reason, see this <ulink
url="http://tinyurl.com/2tb37">forum post</ulink> on how to do so.
You'll then have several new programs installed where Python
scripts are normally installed on your system
(<filename>/usr/bin</filename> or
<filename>/usr/local/bin</filename> are common on UNIX systems;
<filename>C:\Python23\Scripts</filename> is a common place on
Windows; and (watch out, this is a long one :))
<filename>/System/Library/Frameworks/Python.framework/Versions/2.3/bin</filename>
is a common place on MacOS X.). The two that might be of
particular interest to you, the new user, are
<script>supybot</script> and
<script>supybot-wizard</script> The former
(<script>supybot</script> is the script to run an actual
bot; the latter (<script>supybot-wizard</script> is an
in-depth wizard that provides a nice user interface for creating
configuration files for your bot. We'd prefer you to the use
<script>supybot-wizard</script>, but if you're in a
hurry or don't feel like being asked many questions, just run
supybot with no arguments and it'll ask you only the questions
necessary ")to run a bot.
</para>
</sect1>
<sect1>
<title>Firing up the bot for the first time</title>
<para>
So after running either of those two programs, you've got a nice
registry file handy. If you're not satisfied with your answers
to any of the questions you were asked, feel free to run the
program again until you're satisfied with all your answers. Once
you're satisfied, though, run the
<script>supybot</script> program with the
registry file you created as an argument. This will start the
bot; unless you turned off logging to stdout, you'll see some nice
log messages describing what the bot is doing at any particular
moment; it may pause for a significant amount of time after saying
"Connecting to ..." while the server tries to check its ident.
</para>
</sect1>
<sect1>
<title>Your first interactions with the bot</title>
<para>
Ok, so let's assume your bot connected to the server fine and
joined the channels you told it to join. For now we'll assume you
named your bot <nick>supybot</nick> (you probably didn't,
but it'll make it much clearer in the examples that follow to
assume that you did). We'll also assume that you told it to join
<channel>#channel</channel> (a nice generic name for a channel,
isn't it? :)) So what do you do with this bot that you just made
to join your channel? Try this in the channel:
</para>
<ircsession>
supybot: list
</ircsession>
<para>
Replacing <nick>supybot</nick> with the actual name you
picked for your bot, of course. Your bot should reply with a list
of the plugins he currently has loaded. At least
<plugin>Admin</plugin>, <plugin>Channel</plugin>,
<plugin>Config</plugin>, <plugin>Misc</plugin>,
<plugin>Owner</plugin>, and <plugin>User</plugin> should be
there; if you used <script>supybot-wizard</script> to
create your configuration file you may have many more plugins
loaded. The <botcommand>list</botcommand> command can also be used to
list the commands in a given plugin:
</para>
<ircsession>
supybot: list Misc
</ircsession>
<para>
Will list all the commands in the <plugin>Misc</plugin> plugin.
</para>
<sect2>
<title>Accessing the bot's online help</title>
<para>
If you want to see the help for any command, just use
the <botcommand>help</botcommand> command:
</para>
<ircsession>
supybot: help help
supybot: help list
supybot: help load
</ircsession>
</sect2>
<sect2>
<title>Dealing with ambiguous commands</title>
<para>
Sometimes more than one plugin will have a given command; for
instance, the <botcommand>list</botcommand> command exists in both
the <plugin>Misc</plugin> and <plugin>Config</plugin>
plugins (both loaded by default). <plugin>List</plugin>, in
this case, defaults to the <plugin>Misc</plugin> plugin, but
you may want to get the help for the
<botcommand>list</botcommand>
command in the <plugin>Config</plugin> plugin. In that
case, you'll want to give your command like this:
</para>
<ircsession>
supybot: help config list
</ircsession>
<para>
Anytime your bot tells you that a given command is defined in
several plugins, you'll want to use this syntax
(<botcommand>plugin command</botcommand>) to disambiguate which
plugin's command you wish to call. For instance, if you
wanted to call the <plugin>Config</plugin> plugin's
<botcommand>list</botcommand> command, then you'd need to say:
</para>
<ircsession>
supybot: config list
</ircsession>
<para>
Rather than just <botcommand>list</botcommand>.
</para>
</sect2>
<sect2>
<title>Loading plugins</title>
<para>
Now that you know how to deal with plugins having commands
with the same name, let's take a look at loading other
plugins. If you didn't use
<script>supybot-wizard</script>, though, you might
do well to try it before playing around with loading plugins
yourself: each plugin has its own
<function>configure</function> function that the wizard uses
to setup the appropriate registry entries if the plugin
requires any.
</para>
<sect3>
<title>Identifying yourself as the bot owner</title>
<para>
Now, if you do want to play around with loading plugins,
you're going to need to have the
<capability>owner</capability>
capability. If you ran the wizard, then chances are you
already added an owner user for yourself. If not,
however, you can add one via the handy-dandy
<script>supybot-adduser</script> script. You'll
want to run it while the bot is not running (otherwise it
could overwrite
<script>supybot-adduser</script>'s changes to
your user database before you get a chance to reload
them). Just follow the prompts, and when it asks if you
want to give the user any capabilities, say yes and then
give yourself the <capability>owner</capability> capability
(without the quotes), restart the bot and you'll be ready
to load some plugins!
</para>
<para>
Now, in order for the bot to recognize you as your owner
user, you'll have to identify with the bot. Open up a
query window in your irc client (/query should do it; if
not, just know that you can't identify in a channel
because it requires sending your password to the bot).
Then type this:
</para>
<ircsession>
help identify
</ircsession>
<para>
And follow the instructions; the command you send will
probably look like this, with your owner user and password
replaced:
</para>
<ircsession>
identify myowneruser myuserpassword
</ircsession>
<para>
The bot will tell you that &ldquo;The operation
succeeded&rdquo; if you got the right name and password.
Now that you're identified, you can do anything that
requires any privilege: that includes all the commands in
the <plugin>Owner</plugin> and <plugin>Admin</plugin>
plugins, which you may want to take a look at (using the
<botcommand>list</botcommand> and
<botcommand>help</botcommand>
commands, of course). One command in particular that you
might want to use (it's from the <plugin>User</plugin>
plugin) is the <botcommand>addhostmask</botcommand> command: it
lets you add a hostmask to your user record so the bot
recognizes you by your hostmask instead of requiring you
to always identify with it before it recognizes you. Use
the <botcommand>help</botcommand> command to see how this
command works. Here's how I often use it:
</para>
<ircsession>
addhostmask myuser [hostmask] mypassword
</ircsession>
<para>
You may not have seen that "[hostmask]" syntax before.
Supybot allows nested commands, which means that any
command's output can be nested as an argument to another
command. The hostmask command from the
<plugin>Misc</plugin> plugin returns the hostmask of a
given nick, but if given no arguments, it returns the
hostmask of the person giving the command. So the command
above adds the hostmask I'm currently using to my user's
list of recognized hostmasks. I'm only required to give
<literal>mypassword</literal> if I'm not already
identified with the bot.
</para>
</sect3>
</sect2>
<sect2>
<title>The <botcommand>more</botcommand> command</title>
<para>
Another command you might find yourself needing somewhat often
is the <botcommand>more</botcommand> command. The IRC protocol
limits messages to 512 bytes, 60 or so of which must be
devoted to some bookkeeping. Sometimes, however, Supybot
wants to send a message that's longer than that. What it
does, then, is break it into "chunks" and send the first one,
following it with "(X more messages)" where X is how many more
chunks there are. To get to these chunks, use the more
command. One way to try is to look at the listing of
configuration groups for the bot (more on this in the
CONFIGURATION document) by giving the command "config list
supybot". Last I checked, it'll overflow into a second chunk.
When you invoke this command, you should see output like:
</para>
<ircsession>
&lt;supybot&gt; nick, ident, user, server, password, channels, prefixChars,
defaultCapabilities, defaultAllow, defaultIgnore,
humanTimestampFormat, externalIP, bracketSyntax, pipeSyntax,
followIdentificationThroughNickChanges, alwaysJoinOnInvite,
showSimpleSyntax, maxHistoryLength, nickmods, throttleTime,
snarfThrottle, threadAllCommands, pingServer, pingInterval,
upkeepInterval, flush, (1 more message)
</ircsession>
<para>
Now, to see the rest of the output, simply give the command
<botcommand>more</botcommand>, and it will show you the rest:
</para>
<ircsession>
&lt;jemfinch&gt; more
&lt;supybot&gt; httpPeekSize, and defaultSocketTimeout
</ircsession>
</sect2>
</sect1>
<sect1>
<title>You're ready!</title>
<para>
You should now have a solid foundation for using Supybot. Be sure
to check the help that is built-in to the bot itself if you have
any questions, and enjoy using Supybot!
</para>
</sect1>
</article>

View File

@ -1,585 +0,0 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article>
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Supybot developer interfaces</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>19 Feb 2004</date>
<revremark>Initial Docbook translation</revremark>
</revision>
<revision>
<revnumber>0.2</revnumber>
<date>26 Feb 2004</date>
<revremark>Converted to Supybot DTD</revremark>
</revision>
</revhistory>
</articleinfo>
<sect1>
<title>Available interfaces</title>
<para>
These are the interfaces for some of the objects you'll deal with
if you code for Supybot.
</para>
<sect2>
<title><classname>ircmsgs.IrcMsg</classname>
<para>
This is the object that represents an IRC message. It has
several methods and attributes. The most important thing
about this class, however, is that it <emphasis>is</emphasis>
hashable, and thus <emphasis>cannot</emphasis> be modified.
Do not change any attributes; any code that modifies an IRC
message is <emphasis>broken</emphasis> and should not exist.
</para>
<variablelist>
<title>Interesting methods</title>
<varlistentry>
<term>__init__</term>
<listitem>
<para>
One of the more complex initializers in a class.
It can be used in three different ways:
</para>
<orderedlist numeration="arabic" spacing="normal">
<listitem>
<para>
It can be given a string, as one received
from the server, which it will then parse
into its separate components and
instantiate the class with those
components as attributes.
</para>
</listitem>
<listitem>
<para>
It can be given a command, some (optional)
arguments, and a (optional) prefix, and
will instantiate the class with those
components as attributes.
</para>
</listitem>
<listitem>
<para>
It can be given, in addition to any of the
above arguments, a <varname>msg</varname>
keyword argument that will use the
attributes of msg as defaults. This
exists to make it easier to copy messages,
since the class is immutable.
</para>
</listitem>
</orderedlist>
</listitem>
</varlistentry>
<varlistentry>
<term>__str__</term>
<listitem>
<para>
This returns the message in a string form suitable
for sending to a server.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>__repr__</term>
<listitem>
<para>
This returns the message in a form suitable for
<function>eval()</function>, assuming the name
<varname>IrcMsg</varname> is in your namespace and
is bound to this class.
</para>
</listitem>
</varlistentry>
</variablelist>
<para>
The following attributes are the meat of this class. These
are generally what you'll be looking at with
<varname>IrcMsg</varname>s.
</para>
<variablelist>
<title>Interesting attributes</title>
<varlistentry>
<term>command</term>
<listitem>
<para>
This is the command of the
<varname>IrcMsg</varname> &ndash;
<literal>PRIVMSG</literal>,
<literal>NOTICE</literal>,
<literal>WHOIS</literal>,
etc.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>args</term>
<listitem>
<para>
This is a tuple of the arguments to the
<varname>IrcMsg</varname>. Some messages have
arguments, some don't, depending on what command
they are. You are, of course, always assured that
<varname>args</varname> exists and is a tuple,
though it might be empty.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>prefix</term>
<listitem>
<para>
This is the hostmask of the person/server the
message is from. In general, you won't be setting
this on your outgoing messages, but incoming
messages will always have one. This is the whole
hostmask; if the message was received from a
server, it'll be the server's hostmask; if the
message was received from a user, it'll be the
whole user hostmask. In that case, however, it's
also parsed out into the
<varname>nick</varname>/<varname>user</varname>/<varname>host</varname>
attributes, which are probably more useful to
check for many purposes.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>nick</term>
<listitem>
<para>
If the message was sent by a user, this will be
the nick of the user. If it was sent by a server,
this will be the server's name (something like
<literal>calvino.freenode.net</literal> or
similar).
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>user</term>
<listitem>
<para>
If the message was sent by a user, this will be
the user string of the user &ndash; what they put
into their IRC client for their "full name." If
it was sent by a server, it'll be the server's
name, again.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>host</term>
<listitem>
<para>
If the message was sent by a user, this will be
the host portion of their hostmask. If it was
sent by a server, it'll be the server's name (yet
again :))
</para>
</listitem>
</varlistentry>
</variablelist>
</sect2>
<sect2>
<title><classname>irclib.Irc</classname>
<para>
This is the object to handle everything about IRC except the
actual connection to the server itself.
(<emphasis>NOTE</emphasis> that the object actually received
by commands in subclasses of
<classname>callbacks.Privmsg</classname> is an
<classname>IrcObjectProxy</classname>, which is described
later. It augments the following interface with several
methods of its own to help plugin authors.)
</para>
<variablelist>
<title>Interesting methods</title>
<varlistentry>
<term>queueMsg</term>
<listitem>
<para>
Queues a message for sending to the server. The
queue is generally FIFO, but it does prioritize
messages based on their command.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>sendMsg</term>
<listitem>
<para>
Queues a message for sending to the server prior
to any messages in the normal queue. This is
exactly a FIFO queue, no reordering is done at
all.
</para>
</listitem>
</varlistentry>
<!--<note>
<para>
The following two methods are the most important for
people writing new <varname>IrcDriver</varname>s.
Otherwise, you really don't need to pay attention to
them.
</para>
</note>-->
<varlistentry>
<term>feedMsg</term>
<listitem>
<para>
Feeds the <varname>Irc</varname> object a message
for it handle appropriately, as well as passing it
on to callbacks.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>takeMsg</term>
<listitem>
<para>
If the <varname>Irc</varname> object has a message
it's ready to send to the server, this will return
it. Otherwise, it will return
<literal>None</literal>.
</para>
</listitem>
</varlistentry>
<!--<note>
<para>
The next several methods are of far more marginal
utility. But someone may need them, so they're
documented here.
</para>
</note>-->
<varlistentry>
<term>addCallback</term>
<listitem>
<para>
Takes a callback to add to the list of callbacks
in the <varname>Irc</varname> object. See the
interface for <varname>IrcCallback</varname> for
more information.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>getCallback</term>
<listitem>
<para>
Gets a callback by name, if it is in the
<varname>Irc</varname> object's list of callbacks.
If it it isn't, returns <literal>None</literal>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>removeCallback</term>
<listitem>
<para>
Removes a callback by name. Returns a list of the
callbacks removed (since it is technically
possible to have multiple callbacks with the same
name. This list may be empty.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>__init__</term>
<listitem>
<para>
Requires a <varname>nick</varname>. Optional
arguments include <varname>user</varname> and
<varname>ident</varname>, which default to the
nick given, <varname>password</varname>, which
defaults to the empty password, and
<varname>callbacks</varname>, a list of callbacks
(which defaults to nothing, an empty list).
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>reset</term>
<listitem>
<para>
Resets the <varname>Irc</varname> object to its
original state, as well as sends a
<function>reset()</function> to every callbacks.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>die</term>
<listitem>
<para>
Kills the IRC object and all its callbacks.
</para>
</listitem>
</varlistentry>
</variablelist>
<variablelist>
<title>Interesting attributes</title>
<varlistentry>
<term>nick</term>
<listitem>
<para>
The current nick of the bot.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>prefix</term>
<listitem>
<para>
The current prefix of the bot.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>server</term>
<listitem>
<para>
The current server the bot is connected to.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>network</term>
<listitem>
<para>
The current network name the bot is connected to.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>afterConnect</term>
<listitem>
<para>
<literal>False</literal> until the bot has
received a command sent after the connection is
finished &ndash; 376, 377, or 422.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>state</term>
<listitem>
<para>
An <varname>IrcState</varname> object for this
particular connection. See the interface for the
<varname>IrcState</varname> object for more
information.
</para>
</listitem>
</varlistentry>
</variablelist>
</sect2>
<sect2>
<title><classname>irclib.IrcCallback</classname></title>
<variablelist>
<title>Interesting Methods</title>
<varlistentry>
<term>name</term>
<listitem>
<para>
Returns the name of the callback. The default
implementation simply returns the name of the
class.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>__call__</term>
<listitem>
<para>
Called by the <varname>Irc</varname> object with
itself and the message whenever a message is fed
to the <varname>Irc</varname> object. Nothing is
done with the return value.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>inFilter</term>
<listitem>
<para>
Called by the <varname>Irc</varname> object with
itself and the message whenever a message is fed
to the <varname>Irc</varname> object. The return
value should be an <varname>IrcMsg</varname>
object to be passed to the next callback in the
<varname>Irc</varname>'s list of callbacks. If
<literal>None</literal> is returned, all
processing stops. This gives callbacks an
oppurtunity to "filter" incoming messages before
general callbacks are given them.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>outFilter</term>
<listitem>
<para>
Basically equivalent to
<varname>inFilter</varname>, except instead of
being called on messages as they enter the
<varname>Irc</varname> object, it's called on
messages as they leave the <varname>Irc</varname>
object.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>die</term>
<listitem>
<para>
Called when the parent <varname>Irc</varname> is
told to die. This gives callbacks an oppurtunity
to close open files, network connections, or
databases before they're deleted.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>reset</term>
<listitem>
<para>
Called when the parent <varname>Irc</varname> is
told to reset (which is generally when
reconnecting to the server). Most callbacks don't
need to define this.
</para>
</listitem>
</varlistentry>
</variablelist>
<variablelist>
<title>Interesting attributes</title>
<varlistentry>
<term>priority</term>
<listitem>
<para>
Determines the priority of the callback in the
<varname>Irc</varname> object's list of callbacks.
Defaults to <literal>99</literal>; the valid range
includes <literal>0</literal> through
<literal>sys.maxint-1</literal> (don't use
<literal>sys.maxint</literal> itself, that's
reserved for the <varname>Misc</varname> plugin).
The lower the number, the higher the priority.
High priority callbacks are called earlier in the
<varname>inFilter</varname> cycle, earlier in the
<varname>__call__</varname> cycle, and later in
the <varname>outFilter</varname> cycle &ndash;
basically, they're given the first chances on the
way in and the last chances on the way out.
</para>
</listitem>
</varlistentry>
</variablelist>
</sect2>
<sect2>
<title><classname>callbacks.IrcObjectProxy</classname></title>
<para>
<classname>IrcObjectProxy</classname> is a proxy for an
<classname>irclib.Irc</classname> instance that serves to
provide a much fuller interface for handling replies and
errors as well as to handle the nesting of commands. This is
what you'll be dealing with almost all the time when writing
commands; when writing <function>doCommand</function> methods
(the kind you read about in the interface description of
<classname>irclib.IrcCallback</classname>) you'll be dealing
with plain old <classname>irclib.Irc</classname> objects.
</para>
<variablelist>
<title>Interesting methods</title>
<varlistentry>
<term>reply</term>
<listitem>
<para>
Called to reply to the current message with a
string that is to be the reply. Uses the
<function>queueMsg</function> command discussed in
the <classname>irclib.Irc</classname> section.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>replySuccess</term>
<term>replyError</term>
<listitem>
<para>
These reply with the configured responses for
success and generic error, respectively. If an
additional argument is given, it's (intelligently)
appended to the generic message to be more
specific.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>error</term>
<listitem>
<para>
Called to send an error reply to the current
message; not only does the response indicate an
error, but commands that error out break the
nested-command chain, which is generally useful
for not confusing the user :)
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>errorNoCapability</term>
<listitem>
<para>
Like <function>error</function>, except it accepts
the capability that's missing and integrates it
into the configured error message for such things.
Also accepts an additional string for a more
descriptive message, if that's what you want.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>errorPossibleBug</term>
<term>errorNotRegistered</term>
<term>errorNoUser</term>
<term>errorRequiresPrivacy</term>
<listitem>
<para>
These methods reply with the appropriate
configured error message for the conditions in
their names; they all take an additional arguments
to be more specific about the conditions they
indicate, but this argument is very rarely
necessary.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>getRealIrc</term>
<listitem>
<para>
Returns the actual <classname>Irc</classname>
object being proxied for.
</para>
</listitem>
</varlistentry>
</variablelist>
</sect2>
</sect1>
</article>

View File

@ -1,633 +0,0 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article>
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Supybot plugin author example</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>13 Sep 2003</date>
<revremark>Initial revision</revremark>
</revision>
<revision>
<revnumber>0.2</revnumber>
<date>14 Sep 2003</date>
<revremark>Converted to DocBook</revremark>
</revision>
<revision>
<revnumber>0.3</revnumber>
<date>24 Nov 2003</date>
<revremark>
Updated to match EXAMPLE included with 0.75.0
</revremark>
</revision>
<revision>
<revnumber>0.4</revnumber>
<date>26 Feb 2004</date>
<revremark>Converted to use Supybot DTD</revremark>
</revision>
<revision>
<revnumber>0.5</revnumber>
<date>4 Sep 2004</date>
<revremark>Updated Docbook translation</revremark>
</revision>
</revhistory>
</articleinfo>
<sect1>
<title>Introduction</title>
<para>
Ok, so you want to write a callback for supybot. Good, then this
is the place to be. We're going to start from the top (the
highest level, where supybot code does the most work for you) and
move lower after that.
</para>
<para>
So have you used supybot? If not, you need to go use it, get a
feel for it, see how the various commands work and such.
</para>
<para>
So now that we know you've used supybot, we'll start getting into
details.
</para>
</sect1>
<sect1>
<title>Creating your own plugin</title>
<sect2>
<title>
Using <script>scripts/newplugin.py</script>
</title>
<para>
First, the easiest way to start writing a module is to use the
wizard provided, <script>scripts/newplugin.py</script>.
Here's an example session:
</para>
<screen>
functor% scripts/newplugin.py
What should the name of the plugin be? Random
Supybot offers two major types of plugins: command-based and regexp-
based. Command-based plugins are the kind of plugins you've seen most
when you've used supybot. They're also the most featureful and
easiest to write. Commands can be nested, for instance, whereas
regexp-based callbacks can't do nesting. That doesn't mean that
you'll never want regexp-based callbacks. They offer a flexibility
that command-based callbacks don't offer; however, they don't tie into
the whole system as well. If you need to combine a command-based
callback with some regexp-based methods, you can do so by subclassing
callbacks.PrivmsgCommandAndRegexp and then adding a class-level
attribute "regexps" that is a sets.Set of methods that are regexp-
based. But you'll have to do that yourself after this wizard is
finished :)
Do you want a command-based plugin or a regexp-based plugin? [command/
regexp] command
Sometimes you'll want a callback to be threaded. If its methods
(command or regexp-based, either one) will take a signficant amount
of time to run, you'll want to thread them so they don't block
the entire bot.
Does your plugin need to be threaded? [y/n] n
Your new plugin template is in plugins/Random.py
functor%
</screen>
<para>
So that's what it looks like. Now let's look at the source
code (if you'd like to look at it in your programming editor,
the whole plugin is available as
<filename>examples/Random.py</filename>):
</para>
<programlisting>
#!/usr/bin/env python
###
# Copyright (c) 2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Add the module docstring here. This will be used by the setup.py script.
"""
__revision__ = "$Id$"
__author__ = ''
import supybot.plugins as plugins
import supybot.conf as conf
import supybot.utils as utils
import supybot.privmsgs as privmsgs
import supybot.callbacks as callbacks
def configure(onStart, afterConnect, advanced):
# This will be called by setup.py to configure this module. Advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from questions import expect, anything, something, yn
conf.registerPlugin('Random', True)
class Random(callbacks.Privmsg):
pass
Class = Random
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
</programlisting>
</sect2>
<sect2>
<title>Customizing the boilerplate code</title>
<para>
So a few notes, before we customize it.
</para>
<para>
You'll probably want to change the copyright notice to be your
name. It wouldn't stick even if you kept my name, so you
might as well :)
</para>
<para>
Describe what you want the plugin to do in the docstring.
This is used in <script>scripts/setup.py</script> in
order to explain to the user the purpose of the module. It's
also returned when someone asks the bot for help for a given
module (instead of help for a certain command). We'll change
this one to <literal>"Lots of stuff relating to random
numbers."</literal>
</para>
<para>
Then there are the imports. The
<module>callbacks</module>
module is used (the class you're given subclasses
<classname>callbacks.Privmsg</classname>) but the
<module>privmsgs</module> module isn't used. That's
alright; we can almost guarantee you'll use it, so we go ahead
and add the import to the template.
</para>
<para>
Then you see a <function>configure</function> function. This
the function that's called when users decide to add your
module in <script>scripts/setup.py</script>. You'll
note that by default it simply registers the plugin to be
automatically loaded on startup. For many
plugins this is all you need; for more complex plugins, you
might need to ask questions and add commands based on the
answers.
</para>
</sect2>
<sect2>
<title>Digging in: customizing the plugin class</title>
<para>
Now comes the meat of the plugin: the plugin class.
</para>
<para>
What you're given is a skeleton: a simple subclass of
<classname>callbacks.Privmsg</classname> for you to start
with. Now let's add a command.
</para>
<para>
I don't know what you know about random number generators, but
the short of it is that they start at a certain number (a
seed) and they continue (via some somewhat
complicated/unpredictable algorithm) from there. This seed
(and the rest of the sequence, really) is all nice and
packaged up in Python's <module>random</module> module, the
<varname>Random</varname> object. So the first thing we're
going to have to do is give our plugin a
<varname>Random</varname> object.
</para>
<para>
Normally, when we want to give instances of a class an object,
we'll do so in the <function>__init__</function> method. And
that works great for plugins, too. The one thing you have to
be careful of is that you call the superclass
<function>__init__</function> method at the end of your own
<function>__init__</function>. So to add this
<classname>random.Random</classname> object to our plugin, we
can replace the <keyword>pass</keyword> statement with
this:
</para>
<programlisting>
def __init__(self):
self.rng = random.Random()
callbacks.Privmsg.__init__(self)
</programlisting>
<para>
(<varname>rng</varname>is an abbreviation for "random number
generator," in case you were curious)
</para>
<para>
Do be careful not to give your <function>__init__</function>
any arguments (other than <varname>self</varname>, of course).
There's no way anything will ever get to them! If you have
some sort of initial values you need to get to your plugin
before it can do anything interesting, you should get those
values from the registry.
</para>
<para>
There's an easier way to get our plugin to have its own rng
than to define an <function>__init__</function>. Plugins are
unique among classes because we're always certain that there
will only be one instance -- supybot doesn't allow us to load
multiple instances of a single plugin. So instead of adding
the rng in <function>__init__</function>, we can just add it
as a attribute to the class itself. Like so (replacing the
<function>pass</function> statement again):
</para>
<programlisting>
rng = random.Random()
</programlisting>
<para>
And we save two lines of code and make our code a little more
clear :)
</para>
<para>
Now that we have an RNG, we need some way to get random
numbers. So first, we'll add a command that simply gets the
next random number and gives it back to the user. It takes no
arguments, of course (what would you give it?). Here's the
command, and I'll follow that with the explanation of what
each part means.
</para>
<programlisting>
def random(self, irc, msg, args):
"""takes no arguments
Returns the next random number generated by the random number
generator.
"""
irc.reply(str(self.rng.random()))
</programlisting>
<para>
And that's it! Pretty simple, huh? Anyway, you're probably
wondering what all that <emphasis>means</emphasis>. We'll
start with the <keyword>def</keyword> statement:
</para>
<programlisting>
def random(self, irc, msg, args):
</programlisting>
<para>
What that does is define a command
<botcommand>random</botcommand>. You can call it by saying
"@random" (or whatever prefix character your specific bot
uses). The arguments are a bit less obvious.
<varname>self</varname> is self-evident (hah!).
<varname>irc</varname> is the <classname>Irc</classname>
object passed to the command; <varname>msg</varname> is the
original <classname>IrcMsg</classname> object. But you're
really not going to have to deal with either of these too much
(with the exception of calling <function>irc.reply</function>
or <function>irc.error</function>). What you're
<emphasis>really</emphasis> interested in is the
<varname>args</varname> arg. That is a list of all the
arguments passed to your command, pre-parsed and already
evaluated (i.e., you never have to worry about nested
commands, or handling double quoted strings, or splitting on
whitespace &ndash; the work has already been done for you).
You can read about the <classname>Irc</classname> object in
<filename>irclib.py</filename> (you won't find
<function>.reply</function> or <function>.error</function>
there, though, because you're actually getting an
<classname>IrcObjectProxy</classname>, but that's beyond the
level we want to describe here :)). You can read about the
<varname>msg</varname> object in
<filename>ircmsgs.py</filename>. But again, you'll very
rarely be using these objects.
</para>
<para>
(In case you're curious, the answer is yes, you
<emphasis>must</emphasis> name your arguments <varname>(self,
irc, msg, args)</varname>. The names of those arguments is
one of the ways that supybot uses to determine which methods
in a plugin class are commands and which aren't. And while
we're talking about naming restrictions, all your commands
should be named in all-lowercase with no underscores. Before
calling a command, supybot always converts the command name to
lowercase and removes all dashes and underscores. On the
other hand, you now know an easy way to make sure a method is
never called (even if its arguments are <varname>(self, irc,
msg, args)</varname>, however unlikely that may be). Just
name it with an underscore or an uppercase letter in it :))
</para>
<para>
You'll also note that the docstring is odd. The wonderful
thing about the supybot framework is that it's easy to write
complete commands with help and everything: the docstring
<emphasis>is</emphasis> the help! Given the above docstring,
this is what a supybot does:
</para>
<ircsession>
&lt;jemfinch&gt; @help random
&lt;angryman&gt; jemfinch: (random takes no arguments) -- Returns the
next random number from the random number generator.
</ircsession>
<para>
Now on to the actual body of the function:
</para>
<programlisting>
irc.reply(msg, str(self.rng.random()))
</programlisting>
<para>
<function>irc.reply</function> simply takes one simple
argument: a string The string is the reply to be sent. Don't
worry about length restrictions or anything
&ndash; if the string you want to send is too big for an IRC
message (and oftentimes that turns out to be the case :)) the
Supybot framework handles that entirely transparently to you.
Do make sure, however, that you give
<function>irc.reply</function> a string. It doesn't take
anything else (sometimes even unicode fails!). That's why we
have "str(self.rng.random())" instead of simply
"self.rng.random()" &ndash; we had to give
<function>irc.reply</function> a string.
</para>
<para>
Anyway, now that we have an RNG, we have a need for seed! Of
course, Python gives us a good seed already (it uses the
current time as a seed if we don't give it one) but users
might want to be able to repeat "random" sequences, so letting
them set the seed is a good thing. So we'll add a seed
command to give the RNG a specific seed:
</para>
<programlisting>
def seed(self, irc, msg, args):
"""&lt;seed&gt;
Sets the seed of the random number generator. &lt;seed&gt; must be
an int or a long.
"""
seed = privmsgs.getArgs(args)
try:
seed = long(seed)
except ValueError:
# It wasn't a valid long!
irc.error(msg, '&lt;seed&gt; must be a valid int or long.')
return
self.rng.seed(seed)
irc.replySuccess()
</programlisting>
<para>
So this one's a bit more complicated. But it's still pretty
simple. The method name is <botcommand>seed</botcommand> so
that'll be the command name. The arguments are the same, the
docstring is of the same form, so we don't need to go over
that again. The body of the function, however, is
significantly different.
</para>
<para>
<function>privmsgs.getArgs</function> is a function you're
going to be seeing a lot of when you write plugins for
supybot. What it does is basically give you the right number
of arguments for your comamnd. In this case, we want one
argument. But we might have been given any number of
arguments by the user. So
<function>privmsgs.getArgs</function> joins them
appropriately, leaving us with one single "seed" argument (by
default, it returns one argument as a single value; more
arguments are returned in a tuple/list). Yes, we could've
just said "seed = args[0]" and gotten the first argument, but
what if the user didn't pass us an argument at all? Then
we've got to catch the <classname>IndexError</classname> from
<varname>args[0]</varname> and complain to the user about it.
<function>privmsgs.getArgs</function>, on the other hand,
handles all that for us. If the user didn't give us enough
arguments, it'll reply with the help string for the command,
thus saving us the effort.
</para>
<para>
So we have the seed from
<function>privmsgs.getArgs</function>. But it's a string.
The next three lines is pretty darn obvious: we're just
converting the string to a int of some sort. But if it's not,
that's when we're going to call
<function>irc.error</function>. It has the same interface as
we saw before in <function>irc.reply</function>, but it makes
sure to remind the user that an error has been encountered
(currently, that means it puts <literal>"Error: "</literal> at
the beginning of the message). After erroring, we return.
It's important to remember this <keyword>return</keyword>
here; otherwise, we'll just keep going down through the
function and try to use this <varname>seed</varname> variable
that never got assigned. A good general rule of thumb is that
any time you use <function>irc.error</function>, you'll want
to return immediately afterwards.
</para>
<para>
Then we set the seed &ndash; that's a simple function on our
rng object. Assuming that succeeds (and doesn't raise an
exception, which it shouldn't, because we already read the
documentation and know that it should work) we reply to say
that everything worked fine. That's what
<function>irc.replySuccess</function> says. By default, it
has the very dry (and appropriately robot-like) "The operation
succeeded." but you're perfectly welcome to customize it
yourself &ndash; the registry was written to be modified!
</para>
<para>
So that's a bit more complicated command. But we still
haven't dealt with multiple arguments. Let's do that
next.
</para>
<para>
So these random numbers are useful, but they're not the kind
of random numbers we usually want in Real Life. In Real Life,
we like to tell someone to "pick a number between 1 and 10."
So let's write a function that does that. Of course, we won't
hardcode the 1 or the 10 into the function, but we'll take
them as arguments. First the function:
</para>
<programlisting>
def range(self, irc, msg, args):
"""&lt;start&gt; &lt;end&gt;
Returns a number between &lt;start&gt; and &lt;end&gt;, inclusive (i.e., the number
can be either of the endpoints.
"""
(start, end) = privmsgs.getArgs(args, required=2)
try:
end = int(end)
start = int(start)
except ValueError:
irc.error(msg, '&lt;start&gt; and &lt;end&gt; must both be integers.')
return
# .randrange() doesn't include the endpoint, so we use end+1.
irc.reply(msg, str(self.rng.randrange(start, end+1)))
</programlisting>
<para>
Pretty simple. This is becoming old hat by now. The only new
thing here is the call to
<function>privmsgs.getArgs</function>. We have to make sure,
since we want two values, to pass a keyword parameter
"required" into <function>privmsgs.getArgs</function>. Of
course, <function>privmsgs.getArgs</function> handles all the
checking for missing arguments and whatnot so we don't have
to.
</para>
<para>
The <classname>Random</classname> object we're using offers us
a "sample" method that takes a sequence and a number (we'll
call it <varname>N</varname>) and returns a list of
<varname>N</varname> items taken randomly from the sequence.
So I'll show you an example that takes advantage of multiple
arguments but doesn't use
<function>privmsgs.getArgs</function> (and thus has to handle
its own errors if the number of arguments isn't right).
Here's the code:
</para>
<programlisting>
def sample(self, irc, msg, args):
"""&lt;number of items&gt; [&lt;text&gt; ...]
Returns a sample of the &lt;number of items&gt; taken from the remaining
arguments. Obviously &lt;number of items&gt; must be less than the number
of arguments given.
"""
try:
n = int(args.pop(0))
except IndexError: # raised by .pop(0)
raise callbacks.ArgumentError
except ValueError:
irc.error('&lt;number of items&gt; must be an integer.')
return
if n &gt; len(args):
irc.error('&lt;number of items&gt; must be less than the number '
'of arguments.')
return
sample = self.rng.sample(args, n)
irc.reply(utils.commaAndify(map(repr, sample)))
</programlisting>
<para>
Most everything here is familiar. The difference between this
and the previous examples is that we're dealing with
<varname>args</varname> directly, rather than through
<function>getArgs</function>. Since we already have the
arguments in a list, it doesn't make any sense to have
<function>privmsgs.getArgs</function> smush them all together
into a big long string that we'll just have to re-split. But
we still want the nice error handling of
<function>privmsgs.getArgs</function>. So what do we do? We
raise <classname>callbacks.ArgumentError</classname>! That's
the secret juju that <function>privmsgs.getArgs</function> is
doing; now we're just doing it ourself. Someone up our
callchain knows how to handle it so a neat error message is
returned. So in this function, if
<function>.pop(0)</function> fails, we weren't given enough
arguments and thus need to tell the user how to call us.
</para>
<para>
So we have the args, we have the number, we do a simple call
to <function>random.sample</function> and then we do this
funky <function>utils.commaAndify</function> to it. Yeah, so
I was running low on useful names :) Anyway, what it does is
take a list of strings and return a string with them joined by
a comma, the last one being joined with a comma and "and". So
the list ['foo', 'bar', 'baz'] becomes "foo, bar, and baz".
It's pretty useful for showing the user lists in a useful
form. We map the strings with <function>repr()</function>
first just to surround them with quotes.
</para>
<para>
So we have one more example. Yes, I hear your groans, but
it's pedagogically useful :) This time we're going to write a
command that makes the bot roll a die. It'll take one
argument (the number of sides on the die) and will respond
with the equivalent of "/me rolls a __" where __ is the number
the bot rolled. So here's the code:
</para>
<programlisting>
def diceroll(self, irc, msg, args):
"""[&lt;number of sides&gt;]
Rolls a die with &lt;number of sides&gt; sides. The default number
of sides is 6.
"""
try:
n = privmsgs.getArgs(args, required=0, optional=1)
if not n:
n = 6
n = int(n)
except ValueError:
irc.error(msg, 'Dice have integer numbers of sides. Use one.')
return
s = 'rolls a %s' % self.rng.randrange(1, n+1)
irc.reply(s, action=True)
</programlisting>
<para>
There's a lot of stuff you haven't seen before in there. The
most important, though, is the first thing you'll notice
that's different: the <function>privmsg.getArgs</function>
call. Here we're offering a default argument in case the user
is too lazy to supply one (or just wants a nice, standard
six-sided die :)) <function>privmsgs.getArgs</function>
supports that; we'll just tell it that we don't
<emphasis>need</emphasis> any arguments (via
<varname>required=0</varname>) and that we <emphasis>might
like</emphasis> one argument (<varname>optional=1</varname>).
If the user provides an argument, we'll get it -- if they
don't, we'll just get an empty string. Hence the "if not n: n
= 6", where we provide the default.
</para>
<para>
You'll also note that <function>irc.reply</function> was given
a keyword argument here, <varname>action</varname>. This
means that the reply is to be made as an action rather than a
normal reply.
</para>
<para>
So that's our plugin. 5 commands, each building in
complexity. You should now be able to write most anything you
want to do in Supybot. Except regexp-based plugins, but
that's a story for another day (and those aren't nearly as
cool as these command-based callbacks anyway :)). Now we need
to flesh it out to make it a full-fledged plugin.
</para>
</sect2>
<sect2>
<title>Using the registry in your plugin</title>
<para>
TODO: Describe the registry and how to write a proper plugin
configure function.
</para>
</sect2>
<para>
We've written our own plugin from scratch (well, from the
boilerplate that we got from
<script>scripts/newplugin.py</script> :)) and
survived! Now go write more plugins for supybot, and send
them to me so I can use them too :)
</para>
</sect1>
</article>

View File

@ -1,46 +0,0 @@
(define %stylesheet% "../stylesheets/supybot.css")
(element botcommand
(make element gi: "span"
attributes: '(("class" "botcommand"))
(process-children)))
(element plugin
(make element gi: "span"
attributes: '(("class" "plugin"))
(process-children)))
(element flag
(make element gi: "span"
attributes: '(("class" "flag"))
(process-children)))
(element nick
(make element gi: "span"
attributes: '(("class" "nick"))
(process-children)))
(element capability
(make element gi: "span"
attributes: '(("class" "capability"))
(process-children)))
(element registrygroup
(make element gi: "span"
attributes: '(("class" "registrygroup"))
(process-children)))
(element ircsession
(make element gi: "pre"
attributes: '(("class" "ircsession"))
(process-children)))
(element script
(make element gi: "span"
attributes: '(("class" "script"))
(process-children)))
(element channel
(make element gi: "span"
attributes: '(("class" "channel"))
(process-children)))

View File

@ -1,43 +0,0 @@
(define %mono-font-family% "Courier New")
(element botcommand
(make sequence
font-family-name: %mono-font-family%))
(element plugin
(make sequence
font-weight: 'bold))
(element flag
(make sequence
font-posture: 'italic))
(element nick
(make sequence
font-family-name: %mono-font-family%))
(element capability
(make sequence
font-weight: 'bold))
(element registrygroup
(make sequence
font-weight: 'bold))
(element ircsession
(make paragraph
font-family-name: %mono-font-family%
space-before: 12pt
space-after: 12pt
start-indent: 6pt
lines: 'asis
input-whitespace-treatment: 'preserve))
(element script
(make sequence
font-family-name: %mono-font-family%))
(element channel
(make sequence
font-weight: 'bold))

View File

@ -1,41 +0,0 @@
.channel {
font-weight: bold;
}
.botcommand {
font-family: monospace;
}
.flag {
font-family: monospace;
}
.nick {
font-style: italic;
}
.plugin {
font-family: monospace;
}
.capability {
font-family: monospace;
}
.registrygroup {
font-family: monospace;
}
.ircsession {
font-family: monospace;
display: block;
background-color: #666666;
}
.script {
font-family: monospace;
}
.channel {
font-weight: bold;
}

View File

@ -1,23 +0,0 @@
<!DOCTYPE style-sheet PUBLIC "-//James Clark//DTD DSSSL Style Sheet//EN" [
<!ENTITY print-ss PUBLIC
"-//Norman Walsh//DOCUMENT DocBook Print Stylesheet//EN" CDATA DSSSL>
<!ENTITY html-ss PUBLIC
"-//Norman Walsh//DOCUMENT DocBook HTML Stylesheet//EN" CDATA DSSSL>
<!ENTITY supybot-print SYSTEM "supybot-print.dsl">
<!ENTITY supybot-html SYSTEM "supybot-html.dsl">
]>
<style-sheet>
<style-specification id="print" use="print-stylesheet">
<style-specification-body>
&supybot-print;
</style-specification-body>
</style-specification>
<style-specification id="html" use="html-stylesheet">
<style-specification-body>
&supybot-html;
</style-specification-body>
</style-specification>
<external-specification id="print-stylesheet" document="print-ss">
<external-specification id="html-stylesheet" document="html-ss">
</style-sheet>

View File

@ -1,139 +0,0 @@
<!-- Segregate all of our stuff into its own class for possible extension
later and just because I wanted to write my own class :) -->
<!ENTITY % local.supybot.tech.char.class "">
<!ENTITY % supybot.tech.char.class "BotCommand|Plugin|Flag|Nick|Capability
|RegistryGroup|Registry|Script
|Channel %local.supybot.tech.char.class;">
<!-- Stuff that isn't supybot-specific, but it's python-related and no
suitable element exists in the DocBook DTD -->
<!ENTITY % local.python.tech.char.class "">
<!ENTITY % python.tech.char.class "Module|Keyword
%local.python.tech.char.class;">
<!-- Pretty much all of our stuff fits where stuff in the tech.char class
goes, so we simply add our stuff using the local extension -->
<!ENTITY % local.tech.char.class "|%supybot.tech.char.class;
|%python.tech.char.class;">
<!-- linespecific is the same class as things like screen and programlisting,
so it's added here to fit with the DocBook stuff (i.e., so putting an
ircsession in where one of those previous two elements would be is a
valid operation -->
<!ENTITY % local.linespecific.class "|IrcSession">
<!-- Source the original DocBook DTD -->
<!ENTITY % DocBookDTD PUBLIC "-//OASIS//DTD DocBook V4.1//EN">
%DocBookDTD;
<!ELEMENT BotCommand - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.botcommand.attrib "">
<!ENTITY % botcommand.role.attrib "%role.attrib;">
<!ATTLIST BotCommand
%common.attrib;
%local.botcommand.attrib;
%botcommand.role.attrib;
>
<!ELEMENT Plugin - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.plugin.attrib "">
<!ENTITY % plugin.role.attrib "%role.attrib;">
<!ATTLIST Plugin
%common.attrib;
%local.plugin.attrib;
%plugin.role.attrib;
>
<!ELEMENT Flag - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.flag.attrib "">
<!ENTITY % flag.role.attrib
"
flagtype (arg|noarg) #IMPLIED
%role.attrib;"
>
<!ATTLIST Flag
%common.attrib;
%local.flag.attrib;
%flag.role.attrib;
>
<!ELEMENT Nick - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.nick.attrib "">
<!ENTITY % nick.role.attrib "%role.attrib;">
<!ATTLIST Nick
%common.attrib;
%local.nick.attrib;
%nick.role.attrib;
>
<!ELEMENT Capability - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.capability.attrib "">
<!ENTITY % capability.role.attrib "%role.attrib;">
<!ATTLIST Capability
%common.attrib;
%local.capability.attrib;
%capability.role.attrib;
>
<!ELEMENT Comment - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.comment.attrib "">
<!ENTITY % comment.role.attrib "%role.attrib;">
<!ATTLIST Comment
%common.attrib;
%local.comment.attrib;
%comment.role.attrib;
>
<!ELEMENT RegistryGroup - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.registrygroup.attrib "">
<!ENTITY % registrygroup.role.attrib "%role.attrib;">
<!ATTLIST RegistryGroup
%common.attrib;
%local.registrygroup.attrib;
%registrygroup.role.attrib;
>
<!ELEMENT Registry - - ((RegistryGroup|Comment)+)>
<!ENTITY % local.registry.attrib "">
<!ENTITY % registry.role.attrib "%role.attrib;">
<!ATTLIST Registry
%common.attrib;
%local.registry.attrib;
%registry.role.attrib;
>
<!ELEMENT IrcSession - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.ircsession.attrib "">
<!ENTITY % ircsession.role.attrib "%role.attrib;">
<!ATTLIST IrcSession
%common.attrib;
%local.ircsession.attrib;
%ircsession.role.attrib;
>
<!ELEMENT Script - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.script.attrib "">
<!ENTITY % script.role.attrib "%role.attrib;">
<!ATTLIST Script
%common.attrib;
%local.script.attrib;
%script.role.attrib;
>
<!ELEMENT Channel - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.channel.attrib "">
<!ENTITY % channel.role.attrib "%role.attrib;">
<!ATTLIST Channel
%common.attrib;
%local.channel.attrib;
%channel.role.attrib;
>
<!ELEMENT Module - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.module.attrib "">
<!ENTITY % module.role.attrib "%role.attrib;">
<!ATTLIST Module
%common.attrib;
%local.module.attrib;
%module.role.attrib;
>

240
docs/FAQ
View File

@ -1,240 +0,0 @@
Q: Why does my bot not recognize me or tell me that I don't have the
"owner" capability?
A: Because you've not given it anything to recognize you from!
You'll need to identify with the bot ("help identify" to see how
that works) or add your hostmask to your user record ("help
addhostmask" to see how that works) for it to know that you're you.
You may wish to note that addhostmask can accept a password; rather
than identify, you can send the command "addhostmask myOwnerUser
[hostmask] myOwnerUserPassword" and the bot will add your current
hostmask to your owner user (of course, you should change
myOwnerUser and myOwnerUserPassword appropriately for your bot).
Q: What's a hostmask?
A: Each user on IRC is uniquely identified by a string which we call
a "hostmask." The IRC RFC refers to it as a prefix. Either way,
it consists of a nick, a user, and a host, in the form
nick!user@host. If your Supybot complains that something you've
given to it isn't a hostmask, make sure that you have those three
components and that they're joined in the appropriate manner.
Q: How do I make my Supybot op my users?
A: First, you'll have to make sure that your users register with the
bot. They can do this with the "register" command. After they do
so, you'll want to add the #channel,op capability to their user.
Use the "channel addcapability" command to do this. After that,
your users should be able to use the "op" command to get ops.
If you want your users to be auto-opped when they join the channel,
you'll need to load the Enforcer plugin and turn its autoOp
configuration variable on. Use the "config" command to do so.
Here's an example of how to do these steps:
<jemfinch> I'm going to make an example session for giving
you auto-ops, for our FAQ.
<dunk1> ah ok ;]
<jemfinch> First, I need you to register with supybot, using
the "register" command (remember to send it in
private).
<dunk1> done
<jemfinch> what name are you registered under?
<dunk1> dunk1
<jemfinch> ok, cool.
<jemfinch> @channel addcapability dunk1 op
<supybot> jemfinch: The operation succeeded.
<jemfinch> now use the "op" command to get ops.
<dunk1> @op
--- supybot gives channel operator status to dunk1
<dunk1> works!
<dunk1> ;]
<jemfinch> @load Enforcer
<supybot> jemfinch: The operation succeeded.
<jemfinch> @config channel supybot.plugins.Enforcer.autoOp On
<supybot> jemfinch: The operation succeeded.
<jemfinch> ok, now cycle the channel (part and then rejoin)
<-- dunk1 (dunker@freebsd.nl) has left #supybot
--> dunk1 (dunker@freebsd.nl) has joined #supybot
--- supybot gives channel operator status to dunk1
<jemfinch> cool, thanks :)
Q: Can users with the "admin" capability change configuration
variables?
A: Currently, no. Feel free to make your case to us as to why a
certain configuration variable should only require the "admin"
capability instead of the "owner" capability, and if we agree
with you, we'll change it for the next release.
Q: Can Supybot do factoids?
A: Supybot most certainly can! In fact, we offer three full-fledged
factoids-related plugins!
Factoids (written by jemfinch) is Supybot's original
factoids-related plugin. It offers full integration with Supybot's
nested commands as well as a complete 1:n key to factoid ratio,
with lookup by individual number. Factoids also uses a
channel-specific database instead of a global database, though
that's configurable with the
supybot.databases.plugins.channelSpecific configuration variable.
MoobotFactoids (written by Strike) is much more full-featured,
offering users the ability to define factoids in a slightly more
user-friendly way, as well as parsing factoids to handle <reply>,
<action>, and alternations (defining a factoid "test" as
"<reply>(foo|bar|baz)" will make the bot send "foo" or "bar" or
"baz" to the channel (without the normal "test is " at the
beginning)). If you're accustomed to Moobot's factoids or
Blootbot's factoids, then this is the Factoids plugin for you.
Unfortunately, due to the more natural definition syntax (required
to be compatible with Moobot) you can't define Factoids with nested
commands; you'll have to evaluate the command first and then copy
the result into your factoid definition.
Infobot (written by jamessan) is used for Infobot compatibility;
if you still want the basic functionality of Infobot, this is the
plugin to use.
Q: Can I import my Infobot/Blootbot/Moobot factoids into Supybot?
A: As of present, we have no automated way to do so. Strike has
written a few scripts for importing a Moobot database into
MoobotFactoids, however, so you'll want to talk to him about
helping you with that. We're certainly happy to help you convert
such databases; if you can provide us with such a database exported
to a flat file, we can probably do the rest of the work to write a
script that imports it into a database for one of our
factoids-related plugins.
Q: Do I really have to use separate databases for each channel?
A: Of course not! We default to separate databases for each channel
because, well, that's what jemfinch always thought was
reasonable. Anyway, if you change the configuration variable
supybot.databases.plugins.channelSpecific to False instead of
True, for *most* databases, each channel will share the same
database (the exceptions are ChannelStats, Herald, Seen, and
WordStats, which are inherently rather channel-based).
Q: Karma doesn't seem to work for me.
A: Karma by default doesn't acknowledge karma updates. If you check
the karma of whatever you increased/decreased, you'll note that
your increment or decrement still took place. If you'd rather
Karma acknowledge karma updates, change the
supybot.plugins.Karma.response configuration variable to On.
Q: I added an alias, but it doesn't work!
A: Take a look at "help <alias you added>". If the alias the bot has
listed doesn't match what you're giving it, chances are you need
to quote your alias in order for the brackets not to be
evaluated. For instance, if you're adding an alias to give you a
link to your homepage, you need to say:
alias add mylink "format concat http://my.host.com/ [urlquote $1]"
and not:
alias add mylink format concat http://my.host.com/ [urlquote $1]
The first version works; the second version will always return the
same url.
Q: Is there a command that can tell me what capability another
command requires?
A: No, there isn't, and there probably never will be. Commands have
the flexibility to check any capabilities they wish to check;
while this flexibility is useful, it also makes it hard to guess
what capability a certain command requires. We could make a
solution that would work in a large majority of cases, but it
wouldn't (and couldn't!) be absolutely correct in all
circumstances, and since we're anal and we hate doing things
halfway, we probably won't ever add this partial solution.
Besides, is the error message so bad? If we did have such a
command, many users would call the command, see that they could
perform it, and then run the command, thus doubling the activity
in the channel. Is that something you want?
Q: How do I make my Supybot connect to multiple servers?
A: Just use the "connect" command in the Network plugin. Easy as pie!
Q: My bot can't handle nicks with brackets in them! It always
complains about something not being a valid command, or about
spurious or missing right brackets, etc.
A: You should quote arguments (using double quotes, like this:
"foo[bar]") that have brackets in them that you don't wish to be
evaluated as nested commands. Otherwise, you can turn off nested
commands by setting supybot.commands.nested to False, or change
the brackets that nest commands, by setting
supybot.commands.nested.brackets to some other value (like <>,
which can't occur in IRC nicks).
Q: I've edited my configuration file, but my Supybot doesn't notice
the changes! Even if I restart it, it doesn't see them. What's
the deal?
A: Supybot won't reload its configuration files unless you tell it
to. In addition, when Supybot exits (and periodically while it
runs) it flushes its configuration file to disk. The safest way
to avoid problems with configuration file edits is simply to exit
the bot before editing the configuration file(s). If you don't
wish to do that, however, you can edit the file, save the changes,
and tell the bot to reload its configuration, either via the
reload command in the Config plugin, or by sending the bot a
SIGHUP. There is a brief period in this whole sequence where the
bot can flush its configuration to disk after you write your
changes, but we even have something to fix that: set the
configuration variable supybot.flush to False, and then reload the
configuration.
Q: I found a bug, what do I do?
A: Submit it on Sourceforge through our Sourceforge project page:
<http://sourceforge.net/tracker/?group_id=58965&atid=489447>. If
Sourceforge happens to be down when you try to submit your bug,
then post it in the "Supybot Developer Discussion" forum at our
forums at <http://forums.supybot.org/>. If that doesn't work,
email supybot-bugs@lists.sourceforge.net. If that doesn't work,
email jemfinch@supybot.org. If that doesn't work, find yourself
some carrier pigeons and ... hah! You thought I was serious!
Anyway, when you submit your bug, we'll need several things. If
the bug involved an uncaught exception, we need the traceback
(basically the stuff from "Uncaught exception in ..." to the next
log entry). We'd also like to see the commands that caused the
bug, or happened around the time you saw the bug. If the bug
involved a database, we'd love to see the database. Remember, it's
always worse to send us too little information in a bug report than
too much.
Q: Is there a way just to load *all* the plugins Supybot has?
A: No, there isn't. Even if there were, some plugins conflict with
other plugins, so it wouldn't make much sense to load them. For
instance, what would a bot do with Factoids, MoobotFactoids, and
Infobot all loaded? Probably just annoy people :)
If you want to know more about the plugins that are available,
check out our "Plugin index" at our website.

View File

@ -1,71 +0,0 @@
These are a list of recurrent bugs in Supybot, and ways to notice
them. It just might come in useful for people maintaining code.
1. Using == or != when you mean ircutils.strEqual. Nicks, prefixes,
and channels should be compared for equality not by == or !=, but
by ircutils.strEqual. This does a case-normalized check. Don't
just use lower() because that doesn't use the rfc1459 casemapping.
To find:
grep == plugins/*.py | egrep "nick|channel"
grep "!=" plugins/*.py | egrep "nick|channel"
2. Using a warning log when it really should be an info log. Users
don't like to see warnings. If we have warning logs, they'll
complain. So let's try to make them complain as little as
possible, and only use warnings when you've encountered a *very*
odd situation, or when you need more information before
determining if something is correct.
An example: The Services plugin has two methods, doNickservNotice
and doChanservNotice, which doNotice dispatches to appropriately.
Now, we can't possibly predict all the possible messages that
ChanServ or NickServ might send to us. So I put a default clause
in there that just says, "Hey, this is an unexpected message from
ChanServ/NickServ: ..." I log this at warning level because I
want to know when there's a NickServ/ChanServ message I haven't
seen before. This works: the warning makes users report it.
Another example: We used to log failures in snarfers at the
warning level. But do the users really want to be warned when a
given "url" isn't valid? No! So now we just make it an info log,
so that users who care can still see why a snarfer didn't snarf,
but no one complains to us about it.
To find:
grep log.warning plugins/*.py
3. Spelling errors. They plague almost every large piece of
software. Supybot is no different, but we have a weapon against
them: find-strings.py. Give find-strings.py a set of files, and
it'll extract all the non-raw literal strings from the source code
and output them to a file, with the originating file, line number,
and string. Spell-checking Supybot is just a matter of taking
this 10,000 line file and an hour and running aspell over it,
correcting the errors in the original file as you find them.
To find:
find-strings.py src/*.py plugins/*.py scripts/supybot*
4. Pegging the CPU. It has happened in the past that bugs in Supybot
have caused 100% CPU usage. These are insidious, and generally
hard to track down. Here are our tools against them; we assuming
that the bug is reproducible.
First, I load the Debug plugin and settrace to a file, then I
quickly reproduce the bug, and let the CPU spin for awhile. Then
I kill the bot and check the trace file (which should be large).
I look for patterns that would indicate an infinite loop of some
sort.
Second, I strace the bot when it's looping. If I see output, it's
making syscalls; if I don't see any output, it's not. If it's
making syscalls, that might mean it's looping in the network
drivers somehow; if not, it's somewhere else.
Third, I check that no regexps could be causing it. They're
notorious for appearing safe, but actually hiding exponential
complexity code (the kind of code that strong cryptography is
based on).
After that, I pray harder :)

View File

@ -1,143 +0,0 @@
Ok, so you've decided to try out Supybot. That's great! The more
people who use Supybot, the more people can submit bugs and help us to
make it the best IRC bot in the world :)
First things first. You should have already read through INSTALL
before reading any further.
Ok, so let's assume your bot connected to the server fine and joined
the channels you told it to join. For now we'll assume you named your
bot "supybot" (you probably didn't, but it'll make it much clearer in
the examples that follow to assume that you did). We'll also assume
that you told it to join #channel (a nice generic name for a channel,
isn't it? :)) So what do you do with this bot that you just made to
join your channel? Try this in the channel:
supybot: list
Replacing "supybot" with the actual name you picked for your bot, of
course. Your bot should reply with a list of the plugins he currently
has loaded. At least Admin, Channel, Config, Misc, Owner, and User
should be there; if you used supybot-wizard to create your
configuration file you may have many more plugins loaded. The list
command can also be used to list the commands in a given plugin:
supybot: list Misc
Will list all the commands in the Misc plugin. If you want to see the
help for any command, just use the help command:
supybot: help help
supybot: help list
supybot: help load
Sometimes more than one plugin will have a given command; for
instance, the "list" command exists in both the Misc and Config
plugins (both loaded by default). List, in this case, defaults to the
Misc plugin, but you may want to get the help for the list command in
the Config plugin. In that case, you'll want to give your command
like this:
supybot: help config list
Anytime your bot tells you that a given command is defined in several
plugins, you'll want to use this syntax ("plugin command") to
disambiguate which plugin's command you wish to call. For instance,
if you wanted to call the Config plugin's list command, then you'd
need to say:
supybot: config list
Rather than just "list".
Now that you know how to deal with plugins having commands with the
same name, let's take a look at loading other plugins. If you didn't
use supybot-wizard, though, you might do well to try it before playing
around with loading plugins yourself: each plugin has its own
configure function that the wizard uses to setup the appropriate
registry entries if the plugin requires any.
Now, if you do want to play around with loading plugins, you're going
to need to have the owner capability. If you ran the wizard, then
chances are you already added an owner user for yourself. If not,
however, you can add one via the handy-dandy supybot-adduser script.
You'll want to run it while the bot is not running (otherwise it could
overwrite supybot-adduser's changes to your user database before you
get a chance to reload them). Just follow the prompts, and when it
asks if you want to give the user any capabilities, say yes and then
give yourself the "owner" capability (without the quotes), restart the
bot and you'll be ready to load some plugins!
Now, in order for the bot to recognize you as your owner user, you'll
have to identify with the bot. Open up a query window in your irc
client (/query should do it; if not, just know that you can't identify
in a channel because it requires sending your password to the bot).
Then type this:
help identify
And follow the instructions; the command you send will probably look
like this, with your owner user and password replaced:
identify myowneruser myuserpassword
The bot will tell you that "The operation succeeded" if you got the
right name and password. Now that you're identified, you can do
anything that requires any privilege: that includes all the commands
in the Owner and Admin plugins, which you may want to take a look at
(using the list and help commands, of course). One command in
particular that you might want to use (it's from the User plugin) is
the addhostmask command: it lets you add a hostmask to your user
record so the bot recognizes you by your hostmask instead of requiring
you to always identify with it before it recognizes you. Use the help
command to see how this command works. Here's how I often use it:
addhostmask myuser [hostmask] mypassword
You may not have seen that "[hostmask]" syntax before. Supybot allows
nested commands, which means that any command's output can be nested
as an argument to another command. The hostmask command from the Misc
plugin returns the hostmask of a given nick, but if given no
arguments, it returns the hostmask of the person giving the command.
So the command above adds the hostmask I'm currently using to my
user's list of recognized hostmasks. I'm only required to give
mypassword if I'm not already identified with the bot.
Now, with all that out of the way, we can continue telling you how to
load plugins :) Remember earlier when I told you to try "help
load"? That's the very command you'll be using. Basically, if you
want to load, say, the Fun plugin, then "load Fun". Simple, right?
If you need a list of the plugins you can load, you'll have to either
list the directory the plugins are in, or you'll have to check our
website -- look for the "Plugin index."
Another command you might find yourself needing somewhat often is the
"more" command. The IRC protocol limits messages to 512 bytes, 60 or
so of which must be devoted to some bookkeeping. Sometimes, however,
Supybot wants to send a message that's longer than that. What it
does, then, is break it into "chunks" and send the first one,
following it with "(X more messages)" where X is how many more chunks
there are. To get to these chunks, use the more command. One way to
try is to look at the default value of
supybot.replies.genericNoCapability -- it's so long that it'll
stretch across two messages.
<jemfinch|lambda> $config default
supybot.replies.genericNoCapability
<lambdaman> jemfinch|lambda: You're missing some capability
you need. This could be because you actually
possess the anti-capability for the capability
that's required of you, or because the channel
provides that anti-capability by default, or
because the global capabilities include that
anti-capability. Or, it could be because the
channel or the global defaultAllow is set to
False, meaning (1 more message)
<jemfinch|lambda> $more
<lambdaman> jemfinch|lambda: that no commands are allowed
unless explicitly in your capabilities. Either
way, you can't do what you want to do.
You should now have a solid foundation for using Supybot. Be sure to
check the help that is built-in to the bot itself if you have any
questions, and enjoy using Supybot!

View File

@ -1,48 +0,0 @@
So, you want to hack on Supybot? Cool! I'm glad -- more developers
means more users, and more users means better software (although I
suppose more developers means better software even without the
addition of more users :))
Anyway, there are a few things you should know before you submit your
code to be accepted into Supybot. The first, and most important
thing is that we really do value your contribution. We may say that
it's not appropriate for the core distribution and any number of
varying reasons, but regardless, we're happy that you're hacking on
Supybot and bending it to your will, and we'll be happy to post your
patch as long as it applies cleanly.
The second thing you should know is that, despite the fact that we're
happy you want to contribute to Supybot, we're not afraid to piss you
off by turning down your code. We won't hesitate to reject code
because it's "bad" or because it doesn't fit our style guidelines
(read docs/STYLE). We don't really care if it makes you angry or
makes you use another IRC bot; we're in the practice of writing good
software, not placating whiners. Despite this, we're not entirely
heartless, and if you've done something we're interested in, we're
willing to work with you and your code until such a time as it's
ready to be accepted into the core. But if, at some point, we say,
"This needs fixed" and you say, "I refuse to fix it," you can go put
your code on the patch tracker; our time together is done. Supybot
is #1 here -- we don't care about your feelings, we don't care about
jamessan's feelings, we don't care about jemfinch's feelings if it
means that the code quality and user experience of Supybot is to
suffer.
Anyway, the normal process is that you'll submit a few patches,
jemfinch will review them and tell you what needs to happen for them
to be accepted into the core, you'll fix those problems, jemfinch
will review them again, that cycle will repeat a few times. When
your code is to jemfinch's satisfaction, it'll be integrated into the
core. For many people, this is the end of the line. For some
others (perhaps you!), you'll continue to write patches for Supybot,
and your coding ability and commitment will be obvious through
those. If your code quality is consistently high enough that
jemfinch (or other Supybot developers) don't have to spend a
significant amount of time reviewing your code, you'll be added as a
developer on the SF.net project and given commit access to our CVS
repository. From then on, you can do what you want, but be aware
that the other developers are watching what you do -- if you have a
big architecture change, you should probably talk to them before you
commit.
So welcome aboard, and have fun hacking on Supybot!

View File

@ -1,271 +0,0 @@
These are the interfaces for some of the objects you'll deal with if
you code for Supybot.
ircmsgs.IrcMsg:
This is the object that represents an IRC message. It has
several methods and attributes. The most important thing
about this class, however, is that it *is* hashable, and thus
*cannot* be modified. Do not change any attributes; any code
that modifies an IRC message is *broken* and should not
exist.
Interesting Methods:
__init__: One of the more complex initializers in
a class. It can be used in three different ways:
1) It can be given a string, as one received from
the server, which it will then parse into its
separate components and instantiate the class
with those components as attributes.
2) It can be given a command, some (optional)
arguments, and a (optional) prefix, and will
instantiate the class with those components as
attributes.
3) It can be given, in addition to any of the
above arguments, a 'msg' keyword argument that
will use the attributes of msg as defaults.
This exists to make it easier to copy
messages, since the class is immutable.
__str__: This returns the message in a string form
suitable for sending to a server.
__repr__: This returns the message in a form
suitable for eval(), assuming the name "IrcMsg" is
in your namespace and is bound to this class.
Interesting Attributes:
This is the meat of this class. These are
generally what you'll be looking at with IrcMsgs.
command: This is the command of the IrcMsg --
PRIVMSG, NOTICE, WHOIS, etc.
args: This is a tuple of the arguments to the
IrcMsg. Some messages have arguments, some don't,
depending on what command they are. You are, of
course, always assured that args exists and is a
tuple, though it might be empty.
prefix: This is the hostmask of the person/server
the message is from. In general, you won't be
setting this on your outgoing messages, but
incoming messages will always have one. This is
the whole hostmask; if the message was received
from a server, it'll be the server's hostmask; if
the message was received from a user, it'll be the
whole user hostmask. In that case, however, it's
also parsed out into the nick/user/host
attributes, which are probably more useful to
check for many purposes.
nick: If the message was sent by a user, this will
be the nick of the user. If it was sent by a
server, this will be the server's name (something
like calvino.freenode.net or similar).
user: If the message was sent by a user, this will
be the user string of the user -- what they put
into their IRC client for their "full name." If
it was sent by a server, it'll be the server's
name, again.
host: If the message was sent by a user, this will
be the host portion of their hostmask. If it was
sent by a server, it'll be the server's name (yet
again :))
irclib.Irc:
This is the object to handle everything about IRC except the
actual connection to the server itself. (*NOTE* that the
object actually received by commands in subclasses of
callbacks.Privmsg is an IrcObjectProxy, which is described
later. It augments the following interface with several
methods of its own to help plugin authors.)
Interesting Methods:
The two following messages (queueMsg and
sendMsg) are the methods by far most commonly
called by plugin authors. They're generally
the only methods you need to pay attention to
if you're writing plugins.
queueMsg: Queues a message for sending to the
server. The queue is generally FIFO, but it
does prioritize messages based on their command.
sendMsg: Queues a message for sending to the
server prior to any messages in the normal
queue. This is exactly a FIFO queue, no
reordering is done at all.
The following two methods are the most important
for people writing new IrcDrivers. Otherwise,
you really don't need to pay attention to them.
feedMsg: Feeds the Irc object a message for it
handle appropriately, as well as passing it on
to callbacks.
takeMsg: If the Irc object has a message it's
ready to send to the server, this will return
it. Otherwise, it will return None.
The next several methods are of far more marginal
utility. But someone may need them, so they're
documented here.
addCallback: Takes a callback to add to the list
of callbacks in the Irc object. See the
interface for IrcCallback for more information.
getCallback: Gets a callback by name, if it is
in the Irc object's list of callbacks. If it
it isn't, returns None.
removeCallback: Removes a callback by name.
Returns a list of the callbacks removed (since
it is technically possible to have multiple
callbacks with the same name. This list may
be empty.
__init__: Requires a nick. Optional arguments
include user and ident, which default to the
nick given, password, which defaults to the empty
password, and callbacks, a list of callbacks
(which defaults to nothing, an empty list).
reset: Resets the Irc object to its original
state, as well as sends a reset() to every
callbacks.
die: Kills the IRC object and all its callbacks.
Interesting attributes:
nick: The current nick of the bot.
prefix: The current prefix of the bot.
server: The current server the bot is connected to.
network: The current network name the bot is connected to.
afterConnect: False until the bot has received a
command sent after the connection is finished --
376, 377, or 422.
state: An IrcState object for this particular
connection. See the interface for the IrcState
object for more information.
irclib.IrcCallback:
Interesting Methods:
name: Returns the name of the callback. The
default implementation simply returns the name
of the class.
__call__: Called by the Irc object with itself
and the message whenever a message is fed to
the Irc object. Nothing is done with the return
value.
inFilter: Called by the Irc object with itself
and the message whenever a message is fed to
the Irc object. The return value should be an
IrcMsg object to be passed to the next callback
in the Irc's list of callbacks. If None is
returned, all processing stops. This gives
callbacks an oppurtunity to "filter" incoming
messages before general callbacks are given
them.
outFilter: Basically equivalent to inFilter,
except instead of being called on messages
as they enter the Irc object, it's called on
messages as they leave the Irc object.
die: Called when the parent Irc is told to
die. This gives callbacks an oppurtunity to
close open files, network connections, or
databases before they're deleted.
reset: Called when the parent Irc is told to
reset (which is generally when reconnecting
to the server). Most callbacks don't need
to define this.
Interesting attributes:
priority: Determines the priority of the
callback in the Irc object's list of
callbacks. Defaults to 99; the valid range
includes 0 through sys.maxint-1 (don't use
sys.maxint itself, that's reserved for the
Misc plugin). The lower the number, the
higher the priority. High priority
callbacks are called earlier in the
inFilter cycle, earlier in the __call__
cycle, and later in the outFilter cycle --
basically, they're given the first chances
on the way in and the last chances on the
way out.
callbacks.IrcObjectProxy:
IrcObjectProxy is a proxy for an irclib.Irc instance that
serves to provide a much fuller interface for handling
replies and errors as well as to handle the nesting of
commands. This is what you'll be dealing with almost all the
time when writing commands; when writing doCommand methods
(the kind you read about in the interface description of
irclib.IrcCallback) you'll be dealing with plain old
irclib.Irc objects.
Interesting methods:
reply: Called to reply to the current message
with a string that is to be the reply.
replySuccess, replyError: These reply with the
configured responses for success and generic
error, respectively. If an additional argument
is given, it's (intelligently) appended to the
generic message to be more specific.
error: Called to send an error reply to the
current message; not only does the response
indicate an error, but commands that error out
break the nested-command chain, which is
generally useful for not confusing the user :)
errorNoCapability: Like error, except it accepts
the capability that's missing and integrates it
into the configured error message for such
things. Also accepts an additional string for a
more descriptive message, if that's what you
want.
errorPossibleBug, errorNotRegistered,
errorNoUser, errorRequiresPrivacy: These methods
reply with the appropriate configured error
message for the conditions in their names; they
all take an additional arguments to be more
specific about the conditions they indicate, but
this argument is very rarely necessary.
getRealIrc: Returns the actual Irc object being
proxied for.
replies: Sends a collection of messages to a given
target, much like reply; except in this case, the user
can configure whether the messages will be sent
one-by-one or combined into a single message. Thus, the
method accepts a "prefixer" argument, which prefixes the
messages with a given string (or according to a given
function), a "joiner" string (or function) used to join
the messages into a single message if necessary, and an
onlyPrefixFirst argument which determines whether only
the first message will be prefixed when the messages are
sent separately (it defaults to False).

View File

@ -1,61 +0,0 @@
So here's a general *programming* introduction to what the different
modules do and what services they provide. It is, however, only an
introduction. Read the modules themselves for a much more detailed
explanation :)
fix.py: Stuff that Python should (but doesn't) include by default.
cdb.py: A constant database library, translated from C (and my O'Caml
version) More information available at
http://cr.yp.to/cdb.html. Not currently used since we
switched to PySQLite.
ansi.py: Contains different ANSI color sequences.
Mostly used by the debug module.
conf.py: The configuration file for the bot -- it sets a lot of
variables that other modules check for when they have
questions about what they need to do.
world.py: Just a dropping off place for some globals that need to be
shared among all the modules. It's obviously not used *a
lot*, but some things seem to fit better here than anywhere
else.
privmsgs.py: Basic stuff relating to callbacks.Privmsg, the base class
for most plugins.
callbacks.py: A few basic callbacks providing significant
functionality. You'll likely be inheriting from Privmsg
quite a bit.
ircdb.py: The users and channels databases are here, as well as the
IrcUser and IrcChannel classes. Look here when you want to
restrict a command to only certain users.
irclib.py: Provides the most important class in the irclib, Irc. It
represents a connection to an IRC server, but it's entirely
separate from the network implementation. It's connected
to the network (or whatever else drives it) by a "driver"
module which uses its feedMsg/takeMsg functions.
drivers.py: The baseclass (IrcDriver) for various drivers to drive
irclib.Irc classes. Also, the driving mechanism.
asyncoreDrivers.py: The asyncore-based drivers for use with the bot.
socketDrivers.py: The plain old socket-based drivers for use with the
bot.
twistedDrivers.py: The Twisted <http://www.twistedmatrix.com/> drivers
for use with the bot.
ircmsgs.py: The IrcMsg class (get to know it :)) and various functions
for making the creation of IrcMsgs easier.
ircutils.py: Various utility functions for Irc -- read the module to
see what goodies are there :)
schedule.py: A schedule driver (which is automatically registered with
the drivers module) to run things at a particular time,
or at specified periods of time.

View File

@ -1,449 +0,0 @@
*******************************************************************************
The plugins/Random.py included with the distribution has been
updated to use an argument-parsing module recently added to Supybot
but not yet documented. Still, all of the functions used in this
tutorial are available, so the code (as shown in this tutorial*
should still work.
*******************************************************************************
Ok, so you want to write a callback for Supybot. Good, then this is
the place to be. We're going to start from the top (the highest
level, where Supybot code does the most work for you) and move lower
after that.
So have you used Supybot? If not, you need to go use it, get a feel
for it, see how the various commands work and such.
So now that we know you've used Supybot, we'll start getting into
details.
First, the easiest way to start writing a module is to use the wizard
provided, scripts/newplugin.py. Here's an example session:
-----
functor% supybot-newplugin
What should the name of the plugin be? Random
Supybot offers two major types of plugins: command-based and regexp-
based. Command-based plugins are the kind of plugins you've seen most
when you've used supybot. They're also the most featureful and
easiest to write. Commands can be nested, for instance, whereas
regexp-based callbacks can't do nesting. That doesn't mean that
you'll never want regexp-based callbacks. They offer a flexibility
that command-based callbacks don't offer; however, they don't tie into
the whole system as well. If you need to combine a command-based
callback with some regexp-based methods, you can do so by subclassing
callbacks.PrivmsgCommandAndRegexp and then adding a class-level
attribute "regexps" that is a sets.Set of methods that are regexp-
based. But you'll have to do that yourself after this wizard is
finished :)
Do you want a command-based plugin or a regexp-based plugin? [command/
regexp] command
Sometimes you'll want a callback to be threaded. If its methods
(command or regexp-based, either one) will take a signficant amount
of time to run, you'll want to thread them so they don't block
the entire bot.
Does your plugin need to be threaded? [y/n] n
Your new plugin template is Random.py
functor%
-----
So that's what it looks like. Now let's look at the source code (if
you'd like to look at it in your programming editor, the whole plugin
is available as examples/Random.py):
-----
#!/usr/bin/env python
###
# Copyright (c) 2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Add the module docstring here. This will be used by the setup.py script.
"""
__revision__ = "$Id$"
__author__ = ''
import supybot.plugins as plugins
import supybot.conf as conf
import supybot.utils as utils
import supybot.privmsgs as privmsgs
import supybot.callbacks as callbacks
def configure(advanced):
# This will be called by setup.py to configure this module. Advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from questions import expect, anything, something, yn
conf.registerPlugin('Random', True)
class Random(callbacks.Privmsg):
pass
Class = Random
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
-----
So a few notes, before we customize it.
You'll probably want to change the copyright notice to be your name.
It wouldn't stick even if you kept my name, so you might as well :)
Describe what you want the plugin to do in the docstring. This is
used in supybot-wizard in order to explain to the user the purpose of
the module. It's also returned when someone asks the bot for help for
a given module (instead of help for a certain command). We'll change
this one to "Lots of stuff relating to random numbers."
Then there are the imports. The callbacks module is used (the class
you're given subclasses callbacks.Privmsg) but the privmsgs module
isn't used. That's alright; we can almost guarantee you'll use it, so
we go ahead and add the import to the template.
Then you see a "configure" function. This is the function that's
called when users decide to add your module in supybot-wizard. You'll
note that by default it simply registers the plugin to be
automatically loaded on startup. For many plugins this is all you
need; for more complex plugins, you might need to ask questions and
add commands based on the answers.
Now comes the meat of the plugin: the plugin class.
What you're given is a skeleton: a simple subclass of
callbacks.Privmsg for you to start with. Now let's add a command.
I don't know what you know about random number generators, but the
short of it is that they start at a certain number (a seed) and they
continue (via some somewhat complicated/unpredictable algorithm) from
there. This seed (and the rest of the sequence, really) is all nice
and packaged up in Python's random module, the Random object. So the
first thing we're going to have to do is give our plugin a Random
object.
Normally, when we want to give instances of a class an object, we'll
do so in the __init__ method. And that works great for plugins, too.
The one thing you have to be careful of is that you call the
superclass __init__ method at the end of your own __init__. So to add
this random.Random object to our plugin, we can replace the "pass"
statement with this:
def __init__(self):
self.rng = random.Random()
callbacks.Privmsg.__init__(self)
(rng is an abbreviation for "random number generator," in case you
were curious)
Do be careful not to give your __init__ any arguments (other than
self, of course). There's no way anything will ever get to them! If
you have some sort of initial values you need to get to your plugin
before it can do anything interesting, you should get those values
from the registry.
There's an easier way to get our plugin to have its own rng than to
define an __init__. Plugins are unique among classes because we're
always certain that there will only be one instance -- supybot doesn't
allow us to load multiple instances of a single plugin. So instead of
adding the rng in __init__, we can just add it as a attribute to the
class itself. Like so (replacing the "pass" statement again):
rng = random.Random()
And we save two lines of code and make our code a little more clear :)
Now that we have an RNG, we need some way to get random numbers. So
first, we'll add a command that simply gets the next random number and
gives it back to the user. It takes no arguments, of course (what
would you give it?). Here's the command, and I'll follow that with
the explanation of what each part means.
def random(self, irc, msg, args):
"""takes no arguments
Returns the next random number generated by the random number
generator.
"""
irc.reply(str(self.rng.random()))
And that's it! Pretty simple, huh? Anyway, you're probably wondering
what all that *means*. We'll start with the def statement:
def random(self, irc, msg, args):
What that does is define a command "random". You can call it by
saying "@random" (or whatever prefix character your specific bot
uses). The arguments are a bit less obvious. Self is self-evident
(hah!). irc is the Irc object passed to the command; msg is the
original IrcMsg object. But you're really not going to have to deal
with either of these too much (with the exception of calling irc.reply
or irc.error). What you're *really* interested in is the args arg.
That is a list of all the arguments passed to your command, pre-parsed
and already evaluated (i.e., you never have to worry about nested
commands, or handling double quoted strings, or splitting on
whitespace -- the work has already been done for you). You can read
about the Irc object in irclib.py (you won't find .reply or .error
there, though, because you're actually getting an IrcObjectProxy, but
that's beyond the level we want to describe here :)). You can read
about the msg object in ircmsgs.py. But again, you'll very rarely be
using these objects.
(In case you're curious, the answer is yes, you *must* name your
arguments (self, irc, msg, args). The names of those arguments is one
of the ways that supybot uses to determine which methods in a plugin
class are commands and which aren't. And while we're talking about
naming restrictions, all your commands should be named in
all-lowercase with no underscores. Before calling a command, supybot
always converts the command name to lowercase and removes all dashes
and underscores. On the other hand, you now know an easy way to make
sure a method is never called (even if its arguments are (self, irc,
msg, args), however unlikely that may be). Just name it with an
underscore or an uppercase letter in it :))
You'll also note that the docstring is odd. The wonderful thing about
the supybot framework is that it's easy to write complete commands
with help and everything: the docstring *IS* the help! Given the
above docstring, this is what a supybot does:
<jemfinch> @help random
<angryman> jemfinch: (random takes no arguments) -- Returns the
next random number from the random number generator.
Now on to the actual body of the function:
irc.reply(str(self.rng.random()))
irc.reply takes one simple argument: a string. The string is the
reply to be sent. Don't worry about length restrictions or anything
-- if the string you want to send is too big for an IRC message (and
oftentimes that turns out to be the case :)) the Supybot framework
handles that entirely transparently to you. Do make sure, however,
that you give irc.reply a string. It doesn't take anything else
(sometimes even unicode fails!). That's why we have
"str(self.rng.random())" instead of simply "self.rng.random()" -- we
had to give irc.reply a string.
Anyway, now that we have an RNG, we have a need for seed! Of course,
Python gives us a good seed already (it uses the current time as a
seed if we don't give it one) but users might want to be able to
repeat "random" sequences, so letting them set the seed is a good
thing. So we'll add a seed command to give the RNG a specific seed:
def seed(self, irc, msg, args):
"""<seed>
Sets the seed of the random number generator. <seed> must be
an int or a long.
"""
seed = privmsgs.getArgs(args)
try:
seed = long(seed)
except ValueError:
# It wasn't a valid long!
irc.error('<seed> must be a valid int or long.')
return
self.rng.seed(seed)
irc.replySuccess()
So this one's a bit more complicated. But it's still pretty simple.
The method name is "seed" so that'll be the command name. The
arguments are the same, the docstring is of the same form, so we don't
need to go over that again. The body of the function, however, is
significantly different.
privmsgs.getArgs is a function you're going to be seeing a lot of when
you write plugins for Supybot. What it does is basically give you the
right number of arguments for your comamnd. In this case, we want one
argument. But we might have been given any number of arguments by the
user. So privmsgs.getArgs joins them appropriately, leaving us with
one single "seed" argument (by default, it returns one argument as a
single value; more arguments are returned in a tuple/list). Yes, we
could've just said "seed = args[0]" and gotten the first argument, but
what if the user didn't pass us an argument at all? Then we've got to
catch the IndexError from args[0] and complain to the user about it.
privmsgs.getArgs, on the other hand, handles all that for us. If the
user didn't give us enough arguments, it'll reply with the help string
for the command, thus saving us the effort.
So we have the seed from privmsgs.getArgs. But it's a string. The
next three lines are pretty darn obvious: we're just converting the
string to a int of some sort. But if it's not, that's when we're
going to call irc.error. It has the same interface as we saw before
in irc.reply, but it makes sure to remind the user that an error has
been encountered (currently, that means it puts "Error: " at the
beginning of the message). After erroring, we return. It's important
to remember this return here; otherwise, we'll just keep going down
through the function and try to use this "seed" variable that never
got assigned. A good general rule of thumb is that any time you use
irc.error, you'll want to return immediately afterwards.
Then we set the seed -- that's a simple function on our rng object.
Assuming that succeeds (and doesn't raise an exception, which it
shouldn't, because we already read the documentation and know that it
should work) we reply to say that everything worked fine. That's what
irc.replySuccess says. By default, it has the very dry (and
appropriately robot-like) "The operation succeeded." but you're
perfectly welcome to customize it yourself -- the registry was written to
be modified!
So that's a bit more complicated command. But we still haven't dealt
with multiple arguments. Let's do that next.
So these random numbers are useful, but they're not the kind of random
numbers we usually want in Real Life. In Real Life, we like to tell
someone to "pick a number between 1 and 10." So let's write a
function that does that. Of course, we won't hardcode the 1 or the 10
into the function, but we'll take them as arguments. First the
function:
def range(self, irc, msg, args):
"""<start> <end>
Returns a number between <start> and <end>, inclusive (i.e., the number
can be either of the endpoints.
"""
(start, end) = privmsgs.getArgs(args, required=2)
try:
end = int(end)
start = int(start)
except ValueError:
irc.error('<start> and <end> must both be integers.')
return
# .randrange() doesn't include the endpoint, so we use end+1.
irc.reply(str(self.rng.randrange(start, end+1)))
Pretty simple. This is becoming old hat by now. The only new thing
here is the call to privmsgs.getArgs. We have to make sure, since we
want two values, to pass a keyword parameter "required" into
privmsgs.getArgs. Of course, privmsgs.getArgs handles all the
checking for missing arguments and whatnot so we don't have to.
The Random object we're using offers us a "sample" method that takes a
sequence and a number (we'll call it N) and returns a list of N items
taken randomly from the sequence. So I'll show you an example that
takes advantage of multiple arguments but doesn't use privmsgs.getArgs
(and thus has to handle its own errors if the number of arguments
isn't right). Here's the code:
def sample(self, irc, msg, args):
"""<number of items> [<text> ...]
Returns a sample of the <number of items> taken from the remaining
arguments. Obviously <number of items> must be less than the number
of arguments given.
"""
try:
n = int(args.pop(0))
except IndexError: # raised by .pop(0)
raise callbacks.ArgumentError
except ValueError:
irc.error('<number of items> must be an integer.')
return
if n > len(args):
irc.error('<number of items> must be less than the number '
'of arguments.')
return
sample = self.rng.sample(args, n)
irc.reply(utils.commaAndify(map(repr, sample)))
Most everything here is familiar. The difference between this and the
previous examples is that we're dealing with args directly, rather
than through getArgs. Since we already have the arguments in a list,
it doesn't make any sense to have privmsgs.getArgs smush them all
together into a big long string that we'll just have to re-split. But
we still want the nice error handling of privmsgs.getArgs. So what do
we do? We raise callbacks.ArgumentError! That's the secret juju that
privmsgs.getArgs is doing; now we're just doing it ourself. Someone
up our callchain knows how to handle it so a neat error message is
returned. So in this function, if .pop(0) fails, we weren't given
enough arguments and thus need to tell the user how to call us.
So we have the args, we have the number, we do a simple call to
random.sample and then we do this funky utils.commaAndify to it.
Yeah, so I was running low on useful names :) Anyway, what it does is
take a list of strings and return a string with them joined by a
comma, the last one being joined with a comma and "and". So the list
['foo', 'bar', 'baz'] becomes "foo, bar, and baz". It's pretty useful
for showing the user lists in a useful form. We map the strings with
repr() first just to surround them with quotes.
So we have one more example. Yes, I hear your groans, but it's
pedagogically useful :) This time we're going to write a command that
makes the bot roll a die. It'll take one argument (the number of
sides on the die) and will respond with the equivalent of "/me rolls a
__" where __ is the number the bot rolled. So here's the code:
def diceroll(self, irc, msg, args):
"""[<number of sides>]
Rolls a die with <number of sides> sides. The default number
of sides is 6.
"""
try:
n = privmsgs.getArgs(args, required=0, optional=1)
if not n:
n = 6
n = int(n)
except ValueError:
irc.error('Dice have integer numbers of sides. Use one.')
return
s = 'rolls a %s' % self.rng.randrange(1, n+1)
irc.reply(s, action=True)
There's a lot of stuff you haven't seen before in there. The most
important, though, is the first thing you'll notice that's different:
the privmsg.getArgs call. Here we're offering a default argument in
case the user is too lazy to supply one (or just wants a nice,
standard six-sided die :)) privmsgs.getArgs supports that; we'll just
tell it that we don't *need* any arguments (via required=0) and that
we *might like* one argument (optional=1). If the user provides an
argument, we'll get it -- if they don't, we'll just get an empty
string. Hence the "if not n: n = 6", where we provide the default.
You'll also note that irc.reply was given a keyword argument here,
"action". This means that the reply is to be made as an action rather
than a normal reply.
So that's our plugin. 5 commands, each building in complexity. You
should now be able to write most anything you want to do in Supybot.
Except regexp-based plugins, but that's a story for another day (and
those aren't nearly as cool as these command-based callbacks anyway
:)). Now we need to flesh it out to make it a full-fledged plugin.
TODO: Describe the registry and how to write a proper plugin configure
function.
We've written our own plugin from scratch (well, from the boilerplate
that we got from scripts/newplugin.py :)) and survived! Now go write
more plugins for supybot, and send them to me so I can use them too :)

View File

@ -1,188 +0,0 @@
====================================================================
Code not following these style guidelines fastidiously is likely
(*very* likely) not to be accepted into the Supybot core.
====================================================================
Read PEP 8 (Guido's Style Guide) and know that we use almost all the
same style guidelines.
Maximum line length is 79 characters. 78 is a safer bet, though.
This is **NON-NEGOTIABLE**. Your code will not be accepted while you
are violating this guidline.
Identation is 4 spaces per level. No tabs. This also is
**NON-NEGOTIABLE**. Your code, again, will *never* be accepted while
you have literal tabs in it.
Single quotes are used for all string literals that aren't docstrings.
They're just easier to type.
Triple double quotes (""") are always used for docstrings.
Raw strings (r'' or r"") should be used for regular expressions.
Spaces go around all operators (except around '=' in default
arguments to functions) and after all commas (unless doing so keeps a
line within the 79 character limit).
Functions calls should look like this: "foo(bar(baz(x), y))". They
should not look like "foo (bar (baz (x), y))", or like
"foo(bar(baz(x), y) )" or like anything else. I hate extraneous
spaces.
Class names are StudlyCaps. Method and function names are camelCaps
(StudlyCaps with an initial lowercase letter). If variable and
attribute names can maintain readability without being camelCaps,
then they should be entirely in lowercase, otherwise they should also
use camelCaps. Plugin names are StudlyCaps.
Imports should always happen at the top of the module, one import per
line (so if imports need to be added or removed later, it can be done
easily).
Unless absolutely required by some external force, imports should be
ordered by the string length of the module imported. I just think it
looks prettier.
A blank line should be between all consecutive method declarations in
a class definition. Two blank lines should be between all
consecutive class definitions in a file. Comments are even better
than blank lines for separating classes.
Database filenames should generally begin with the name of the plugin
and the extension should be 'db'. plugins.DBHandler does this
already.
Whenever creating a file descriptor or socket, keep a reference
around and be sure to close it. There should be no code like this:
s = urllib2.urlopen('url').read()
Instead, do this:
fd = urllib2.urlopen('url')
try:
s = fd.read()
finally:
fd.close()
This is to be sure the bot doesn't leak file descriptors.
All plugin files should include a docstring decsribing what the
plugin does. This docstring will be returned when the user is
configuring the plugin. All plugin classes should also include a
docstring describing how to do things with the plugin; this docstring
will be returned when the user requests help on a plugin name.
Method docstrings in classes deriving from callbacks.Privmsg should
include an argument list as their first line, and after that a blank
line followed by a longer description of what the command does. The
argument list is used by the 'syntax' command, and the longer
description is used by the 'help' command.
Whenever joining more than two strings, use string interpolation, not
addition:
s = x + y + z # Bad.
s = '%s%s%s' % (x, y, z) # Good.
s = ''.join([x, y, z]) # Best, but not as general.
This has to do with efficiency; the intermediate string x+y is made
(and thus copied) before x+y+z is made, so it's less efficient.
People who use string concatenation in a for loop will be swiftly
kicked in the head.
When writing strings that have formatting characters in them, don't
use anything but %s unless you absolutely must. In particular, %d
should never be used, it's less general than %s and serves no useful
purpose. If you got the %d wrong, you'll get an exception that says,
"foo instance can't be converted to an integer." But if you use %s,
you'll get to see your nice little foo instance, if it doesn't
convert to a string cleanly, and if it does convert cleanly, you'll
get to see what you expect to see. Basically, %d just sucks.
As a corrolary to the above, note that sometimes %f is used, but on
when floats need to be formatted, e.g., %.2f.
Use the log module to its fullest; when you need to print some values
to debug, use self.log.debug to do so, and leave those statements in
the code (commented out) so they can later be re-enabled. Remember
that once code is buggy, it tends to have more bugs, and you'll
probably need those print statements again.
While on the topic of logs, note that we do not use % (i.e.,
str.__mod__) with logged strings; we simple pass the format
parameters as additional arguments. The reason is simple: the
logging module supports it, and it's cleaner (fewer tokens/glyphs) to
read.
While still on the topic of logs, it's also important to pick the
appropriate log level for given information.
DEBUG: Appropriate to tell a programmer *how* we're doing
something (i.e., debugging printfs, basically). If
you're trying to figure out why your code doesn't work,
DEBUG is the new printf -- use that, and leave the
statements in your code.
INFO: Appropriate to tell a user *what* we're doing, when what
we're doing isn't important for the user to pay attention
to. A user who likes to keep up with things should enjoy
watching our logging at the INFO level; it shouldn't be
too low-level, but it should give enough information that
it keeps him relatively interested at peak times.
WARNING: Appropriate to tell a user when we're doing something
that he really ought to pay attention to. Users should
see WARNING and think, "Hmm, should I tell the Supybot
developers about this?" Later, he should decide not to,
but it should give the user a moment to pause and think
about what's actually happening with his bot.
ERROR: Appropriate to tell a user when something has gone wrong.
Uncaught exceptions are ERRORs. Conditions that we
absolutely want to hear about should be errors. Things
that should *scare* the user should be errors.
CRITICAL: Not really appropriate. I can think of no absolutely
critical issue yet encountered in Supybot; the only
possible thing I can imagine is to notify the user that
the partition on which Supybot is running has filled up.
That would be a CRITICAL condition, but it would also be
hard to log :)
All plugins should have test cases written for them. Even if it
doesn't actually test anything but just exists, it's good to have the
test there so there's a place to add more tests later (and so we can
be sure that all plugins are adequately documented; PluginTestCase
checks that every command has documentation)
All uses of eval() that expect to get integrated in Supybot must be
approved by jemfinch, no exceptions. Chances are, it won't be
accepted. Have you looked at utils.safeEval?
SQL table names should be all-lowercase and include underscores to
separate words. This is because SQL itself is case-insensitive.
This doesn't change, however the fact that variable/member names
should be camel case.
SQL statements in code should put SQL words in ALL CAPS:
"""SELECT quote FROM quotes ORDER BY random() LIMIT 1""". This makes
SQL significantly easier to read.
Common variable names:
L => an arbitrary list.
t => an arbitrary tuple.
x => an arbitrary float.
s => an arbitrary string.
f => an arbitrary function.
p => an arbitrary predicate.
i,n => an arbitrary integer.
cb => an arbitrary callback.
db => a database handle.
fd => a file-like object.
msg => an ircmsgs.IrcMsg object.
irc => an irclib.Irc object (or proxy)
nick => a string that is an IRC nick.
channel => a string that is an IRC channel.
hostmask => a string that is a user's IRC prefix.
When the semantic functionality (that is, the "meaning" of a variable
is obvious from context, one of these names should be used. This
just makes it easier for people reading our code to know what a
variable represents without scouring the surrounding code.
Multiple variable assignments should always be surrounded with
parentheses -- i.e., if you're using the partition function, then
your assignment statement should look like
(good, bad) = partition(p, L). The parentheses make it obvious that
you're doing a multiple assignment, and that's important because I
hate reading code and wondering where a variable came from.

View File

@ -1,43 +0,0 @@
.\" Process this file with
.\" groff -man -Tascii supybot-adduser.1
.\"
.TH SUPYBOT\-ADDUSER 1 "SEPTEMBER 2004"
.SH NAME
supybot\-adduser \- Adds a user to a Supybot users.conf file
.SH SYNOPSIS
.B supybot\-adduser
.RI [ options ] " users.conf
.SH DESCRIPTION
.B
supybot\-adduser
adds a user to the specified users.conf file.
.SH OPTIONS
.TP
.B \-\^\-version
Show version of program.
.TP
.BR \-h ", " \-\^\-help
Show summary of options.
.TP
.BR \-u " NAME" "\fR,\fP \-\^\-username=" NAME
Specifies the username to use for the new user.
.TP
.BR \-x ", " \-\^\-hashed
Hash encrypt the password.
.TP
.BR \-n ", " \-\^\-plain
Store the password in plain text.
.TP
.BR \-c " CAPABILITY" "\fR,\fP \-\^\-capability=" CAPABILITY
Capability the user should have; this option may be given
multiple times.
.SH "SEE ALSO"
.IR python (1),
.IR supybot (1),
.IR supybot\-wizard (1),
.IR supybot\-newplugin (1)
.SH AUTHOR
This manual page was originally written by James Vega
<vega dot james at gmail dot com>. Permission is granted to copy,
distribute and/or modify this document under the terms of the Supybot
license, a BSD\-style license.

View File

@ -1,41 +0,0 @@
.\" Process this file with
.\" groff -man -Tascii supybot-newplugin.1
.\"
.TH SUPYBOT\-NEWPLUGIN 1 "SEPTEMBER 2004"
.SH NAME
supybot\-newplugin \- A wizard for creating Supybot plugins
.SH SYNOPSIS
.B supybot\-newplugin
.RI [ options ]
.SH DESCRIPTION
.B
supybot\-newplugin
is a wizard that creates a template python source file for a new
.IR supybot (1)
plugin.
.SH OPTIONS
.TP
.B \-\^\-version
Show version of program.
.TP
.BR \-h ", " \-\^\-help
Show summary of options.
.TP
.BR \-r ", " \-\^\-regexp
Uses a regexp\-based callback.
.TP
.BI \-n " NAME" "\fR,\fP \-\^\-name=" NAME
Sets the name for the plugin.
.TP
.BR \-t ", " \-\^\-thread
Makes the plugin threaded.
.SH "SEE ALSO"
.IR python (1),
.IR supybot (1),
.IR supybot\-wizard (1),
.IR supybot\-adduser (1),
.SH AUTHOR
This manual page was originally written by James Vega
<vega dot james at gmail dot com>. Permission is granted to copy,
distribute and/or modify this document under the terms of the Supybot
license, a BSD\-style license.

View File

@ -1,40 +0,0 @@
.\" Process this file with
.\" groff -man -Tascii supybot-wizard.1
.\"
.TH SUPYBOT\-WIZARD 1 "SEPTEMBER 2004"
.SH NAME
supybot\-wizard \- A wizard for creating Supybot configuration files
.SH SYNOPSIS
.B supybot\-wizard
.RI [ options ]
.SH DESCRIPTION
.B
supybot\-wizard
is an in\-depth wizard that provides a nice user interface for creating
configuration files for
.IR supybot (1).
.SH OPTIONS
.TP
.B \-\^\-version
Show version of program.
.TP
.BR \-h ", " \-\^\-help
Show summary of options.
.TP
.B \-\^\-allow\-root
Determines whether the wizard will be allowed to run as root. You do not
want this. Do not do it. Even if you think you want it, you do not.
.TP
.B \-\^\-no\-network
Determines whether the wizard will be allowed to run without a network
connection.
.SH "SEE ALSO"
.IR python (1),
.IR supybot (1),
.IR supybot\-adduser (1),
.IR supybot\-newplugin (1)
.SH AUTHOR
This manual page was originally written by James Vega
<vega dot james at gmail dot com>. Permission is granted to copy,
distribute and/or modify this document under the terms of the Supybot
license, a BSD\-style license.

View File

@ -1,68 +0,0 @@
.\" Process this file with
.\" groff -man -Tascii supybot.1
.\"
.TH SUPYBOT 1 "SEPTEMBER 2004"
.SH NAME
supybot \- A robust and user friendly Python IRC bot
.SH SYNOPSIS
.B supybot
.RI [ options ] " configFile
.SH DESCRIPTION
.B
Supybot
is a robust, user\-friendly, and programmer\-friendly Python IRC bot.
It aims to be an adequate replacement for most existing IRC bots. It
includes a very flexible and powerful ACL system for controlling access
to commands, as well as more than 50 builtin plugins providing around
400 actual commands.
.SH OPTIONS
.TP
.B \-\^\-version
Show version of program.
.TP
.BR \-h ", " \-\^\-help
Show summary of options.
.TP
.BR \-P ", " \-\^\-profile
Enable profiling.
.TP
.B \-O
Optimizes asserts out of the code; \-O0 optimizes asserts and uses
.IR psyco .
.TP
.BI \-n " NICK" "\fR,\fP \-\^\-nick=" NICK
Nick the bot should use.
.TP
.BI \-u " USER" "\fR,\fP \-\^\-user=" USER
Full username the bot should use.
.TP
.BI \-i " IDENT" "\fR,\fP \-\^\-ident=" IDENT
Ident the bot should use.
.TP
.BR \-d ", " \-\^\-daemon
Determines whether the bot will daemonize. This is a no\-op on
non\-POSIX systems.
.TP
.B \-\^\-allow\-default\-owner
Determines whether the bot will allow its defaultCapabilities not to
include "\-owner", thus giving all users the owner capability by
default. This is dumb, hence we require a command\-line option to
enable it.
.TP
.B \-\^\-allow\-root
Determines whether the bot will be allowed to run as root. You do not
want this. Do not do it. Even if you think you want it, you do not.
.TP
.B \-\^\-debug
Determines whether some extra debugging stuff will be logged by this
script.
.SH "SEE ALSO"
.IR python (1),
.IR supybot\-wizard (1),
.IR supybot\-adduser (1),
.IR supybot\-newplugin (1)
.SH AUTHOR
This manual page was originally written by James Vega
<vega dot james at gmail dot com>. Permission is granted to copy,
distribute and/or modify this document under the terms of the Supybot
license, a BSD\-style license.

View File

@ -1,450 +0,0 @@
"""Beautiful Soup
Elixir and Tonic
"The Screen-Scraper's Friend"
The BeautifulSoup class turns arbitrarily bad HTML into a tree-like
nested tag-soup list of Tag objects and text snippets. A Tag object
corresponds to an HTML tag. It knows about the HTML tag's attributes,
and contains a representation of everything contained between the
original tag and its closing tag (if any). It's easy to extract Tags
that meet certain criteria.
A well-formed HTML document will yield a well-formed data
structure. An ill-formed HTML document will yield a correspondingly
ill-formed data structure. If your document is only locally
well-formed, you can use this to process the well-formed part of it.
#Example:
#--------
from BeautifulSoup import BeautifulSoup
text = '''<html>
<head><title>The Title</title></head>
<body>
<a class="foo" href="http://www.crummy.com/">Link <i>text (italicized)</i></a>
<a href="http://www.foo.com/">Link text 2</a>
</body>
</html>'''
soup = BeautifulSoup()
soup.feed(text)
print soup("a") #Returns a list of 2 Tag objects, one for each link in
#the source
print soup.first("a", {'class':'foo'})['href'] #Returns http://www.crummy.com/
print soup.first("title").contents[0] #Returns "The title"
print soup.first("a", {'href':'http://www.crummy.com/'}).first("i").contents[0]
#Returns "text (italicized)"
#Example of SQL-style attribute wildcards -- all four 'find' calls will
#find the link.
#----------------------------------------------------------------------
soup = BeautifulSoup()
soup.feed('''<a href="http://foo.com/">bla</a>''')
print soup.fetch('a', {'href': 'http://foo.com/'})
print soup.fetch('a', {'href': 'http://%'})
print soup.fetch('a', {'href': '%.com/'})
print soup.fetch('a', {'href': '%o.c%'})
#Example with horrible HTML:
#---------------------------
soup = BeautifulSoup()
soup.feed('''<body>
Go <a class="that" href="here.html"><i>here</i></a>
or <i>go <b><a href="index.html">Home</a>
</html>''')
print soup.fetch('a') #Returns a list of 2 Tag objects.
print soup.first(attrs={'href': 'here.html'})['class'] #Returns "that"
print soup.first(attrs={'class': 'that'}).first('i').contents[0] #returns "here"
This library has no external dependencies. It works with Python 1.5.2
and up. If you can install a Python extension, you might want to use
the ElementTree Tidy HTML Tree Builder instead:
http://www.effbot.org/zone/element-tidylib.htm
You can use BeautifulSoup on any SGML-like substance, such as XML or a
domain-specific language that looks like HTML but has different tag
names. For such purposes you may want to use the BeautifulStoneSoup
class, which knows nothing at all about HTML per se. I also reserve
the right to make the BeautifulSoup parser smarter between releases,
so if you want forwards-compatibility without having to think about
it, you might want to go with BeautifulStoneSoup.
Release status:
(I do a new release whenever I make a change that breaks backwards
compatibility.)
Current release:
Applied patch from Richie Hindle (richie at entrian dot com) that
makes tag.string a shorthand for tag.contents[0].string when the tag
has only one string-owning child.
1.2 "Who for such dainties would not stoop?" (2004/07/08): Applied
patch from Ben Last (ben at benlast dot com) that made
Tag.renderContents() correctly handle Unicode.
Made BeautifulStoneSoup even dumber by making it not implicitly
close a tag when another tag of the same type is encountered; only
when an actual closing tag is encountered. This change courtesy of
Fuzzy (mike at pcblokes dot com). BeautifulSoup still works as
before.
1.1 "Swimming in a hot tureen": Added more 'nestable' tags. Changed
popping semantics so that when a nestable tag is encountered, tags are
popped up to the previously encountered nestable tag (of whatever kind).
I will revert this if enough people complain, but it should make
more people's lives easier than harder.
This enhancement was suggested by Anthony Baxter (anthony at
interlink dot com dot au).
1.0 "So rich and green": Initial release.
Retreived from: http://www.crummy.com/software/BeautifulSoup/
"""
__author__ = "Leonard Richardson (leonardr@segfault.org)"
__version__ = "1.1 $Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2004 Leonard Richardson"
__license__ = "Python"
from sgmllib import SGMLParser
import string
import types
class PageElement:
"""Contains the navigational information for some part of the page
(either a tag or a piece of text)"""
def __init__(self, parent=None, previous=None):
self.parent = parent
self.previous = previous
self.next = None
class NavigableText(PageElement):
"""A simple wrapper around a string that keeps track of where in
the document the string was found. Doesn't implement all the
string methods because I'm lazy. You could have this extend
UserString if you were using 2.2."""
def __init__(self, string, parent=None, previous=None):
PageElement.__init__(self, parent, previous)
self.string = string
def __eq__(self, other):
return self.string == str(other)
def __str__(self):
return self.string
def strip(self):
return self.string.strip()
class Tag(PageElement):
"""Represents a found HTML tag with its attributes and contents."""
def __init__(self, name, attrs={}, parent=None, previous=None):
PageElement.__init__(self, parent, previous)
self.name = name
self.attrs = attrs
self.contents = []
self.foundClose = 0
def get(self, key, default=None):
return self._getAttrMap().get(key, default)
def __call__(self, *args):
return apply(self.fetch, args)
def __getitem__(self, key):
return self._getAttrMap()[key]
def __setitem__(self, key, value):
self._getAttrMap()
self.attrMap[key] = value
for i in range(0, len(self.attrs)):
if self.attrs[i][0] == key:
self.attrs[i] = (key, value)
def _getAttrMap(self):
if not hasattr(self, 'attrMap'):
self.attrMap = {}
for (key, value) in self.attrs:
self.attrMap[key] = value
return self.attrMap
def __repr__(self):
return str(self)
def __ne__(self, other):
return not self == other
def __eq__(self, other):
if not isinstance(other, Tag) or self.name != other.name or self.attrs != other.attrs or len(self.contents) != len(other.contents):
return 0
for i in range(0, len(self.contents)):
if self.contents[i] != other.contents[i]:
return 0
return 1
def __str__(self):
attrs = ''
if self.attrs:
for key, val in self.attrs:
attrs = attrs + ' %s="%s"' % (key, val)
close = ''
closeTag = ''
if self.isSelfClosing():
close = ' /'
elif self.foundClose:
closeTag = '</%s>' % self.name
s = self.renderContents()
if not hasattr(self, 'hideTag'):
s = '<%s%s%s>' % (self.name, attrs, close) + s + closeTag
return s
def renderContents(self):
s='' #non-Unicode
for c in self.contents:
try:
s = s + str(c)
except UnicodeEncodeError:
if type(s) <> types.UnicodeType:
s = s.decode('utf8') #convert ascii to Unicode
#str() should, strictly speaking, not return a Unicode
#string, but NavigableText never checks and will return
#Unicode data if it was initialised with it.
s = s + str(c)
return s
def isSelfClosing(self):
return self.name in BeautifulSoup.SELF_CLOSING_TAGS
def append(self, tag):
self.contents.append(tag)
def first(self, name=None, attrs={}, contents=None, recursive=1):
r = None
l = self.fetch(name, attrs, contents, recursive)
if l:
r = l[0]
return r
def fetch(self, name=None, attrs={}, contents=None, recursive=1):
"""Extracts Tag objects that match the given criteria. You
can specify the name of the Tag, any attributes you want the
Tag to have, and what text and Tags you want to see inside the
Tag."""
if contents and type(contents) != type([]):
contents = [contents]
results = []
for i in self.contents:
if isinstance(i, Tag):
if not name or i.name == name:
match = 1
for attr, value in attrs.items():
check = i.get(attr)
#By default, find the specific value called for.
#Use SQL-style wildcards to find substrings, prefix,
#suffix, etc.
result = (check == value)
if check and value:
if len(value) > 1 and value[0] == '%' and value[-1] == '%' and value[-2] != '\\':
result = (check.find(value[1:-1]) != -1)
elif value[0] == '%':
print "blah"
result = check.rfind(value[1:]) == len(check)-len(value)+1
elif value[-1] == '%':
result = check.find(value[:-1]) == 0
if not result:
match = 0
break
match = match and (not contents or i.contents == contents)
if match:
results.append(i)
if recursive:
results.extend(i.fetch(name, attrs, contents, recursive))
return results
class BeautifulSoup(SGMLParser, Tag):
"""The actual parser. It knows the following facts about HTML, and
not much else:
* Some tags have no closing tag and should be interpreted as being
closed as soon as they are encountered.
* Most tags can't be nested; encountering an open tag when there's
already an open tag of that type in the stack means that the
previous tag of that type should be implicitly closed. However,
some tags can be nested. When a nestable tag is encountered,
it's okay to close all unclosed tags up to the last nestable
tag. It might not be safe to close any more, so that's all it
closes.
* The text inside some tags (ie. 'script') may contain tags which
are not really part of the document and which should be parsed
as text, not tags. If you want to parse the text as tags, you can
always get it and parse it explicitly."""
SELF_CLOSING_TAGS = ['br', 'hr', 'input', 'img', 'meta', 'spacer',
'link', 'frame']
NESTABLE_TAGS = ['font', 'table', 'tr', 'td', 'th', 'tbody', 'p',
'div']
QUOTE_TAGS = ['script']
IMPLICITLY_CLOSE_TAGS = 1
def __init__(self, text=None):
Tag.__init__(self, '[document]')
SGMLParser.__init__(self)
self.quoteStack = []
self.hideTag = 1
self.reset()
if text:
self.feed(text)
def feed(self, text):
SGMLParser.feed(self, text)
self.endData()
def reset(self):
SGMLParser.reset(self)
self.currentData = ''
self.currentTag = None
self.tagStack = []
self.pushTag(self)
def popTag(self, closedTagName=None):
tag = self.tagStack.pop()
if closedTagName == tag.name:
tag.foundClose = 1
# Tags with just one string-owning child get the same string
# property as the child, so that soup.tag.string is shorthand
# for soup.tag.contents[0].string
if len(self.currentTag.contents) == 1 and \
hasattr(self.currentTag.contents[0], 'string'):
self.currentTag.string = self.currentTag.contents[0].string
#print "Pop", tag.name
self.currentTag = self.tagStack[-1]
return self.currentTag
def pushTag(self, tag):
#print "Push", tag.name
if self.currentTag:
self.currentTag.append(tag)
self.tagStack.append(tag)
self.currentTag = self.tagStack[-1]
def endData(self):
if self.currentData:
if not string.strip(self.currentData):
if '\n' in self.currentData:
self.currentData = '\n'
else:
self.currentData = ' '
o = NavigableText(self.currentData, self.currentTag, self.previous)
if self.previous:
self.previous.next = o
self.previous = o
self.currentTag.contents.append(o)
self.currentData = ''
def _popToTag(self, name, closedTag=0):
"""Pops the tag stack up to and including the most recent
instance of the given tag. If a list of tags is given, will
accept any of those tags as an excuse to stop popping, and will
*not* pop the tag that caused it to stop popping."""
if self.IMPLICITLY_CLOSE_TAGS:
closedTag = 1
numPops = 0
mostRecentTag = None
oneTag = (type(name) == types.StringType)
for i in range(len(self.tagStack)-1, 0, -1):
thisTag = self.tagStack[i].name
if (oneTag and thisTag == name) \
or (not oneTag and thisTag in name):
numPops = len(self.tagStack)-i
break
if not oneTag:
numPops = numPops - 1
closedTagName = None
if closedTag:
closedTagName = name
for i in range(0, numPops):
mostRecentTag = self.popTag(closedTagName)
return mostRecentTag
def unknown_starttag(self, name, attrs):
if self.quoteStack:
#This is not a real tag.
#print "<%s> is not real!" % name
attrs = map(lambda(x, y): '%s="%s"' % (x, y), attrs)
self.handle_data('<%s %s>' % (name, attrs))
return
self.endData()
tag = Tag(name, attrs, self.currentTag, self.previous)
if self.previous:
self.previous.next = tag
self.previous = tag
if not name in self.SELF_CLOSING_TAGS:
if name in self.NESTABLE_TAGS:
self._popToTag(self.NESTABLE_TAGS)
else:
self._popToTag(name)
self.pushTag(tag)
if name in self.SELF_CLOSING_TAGS:
self.popTag()
if name in self.QUOTE_TAGS:
#print "Beginning quote (%s)" % name
self.quoteStack.append(name)
def unknown_endtag(self, name):
if self.quoteStack and self.quoteStack[-1] != name:
#This is not a real end tag.
#print "</%s> is not real!" % name
self.handle_data('</%s>' % name)
return
self.endData()
self._popToTag(name, 1)
if self.quoteStack and self.quoteStack[-1] == name:
#print "That's the end of %s!" % self.quoteStack[-1]
self.quoteStack.pop()
def handle_data(self, data):
self.currentData = self.currentData + data
def handle_comment(self, text):
"Propagate comments right through."
self.handle_data("<!--%s-->" % text)
def handle_charref(self, ref):
"Propagate char refs right through."
self.handle_data('&#%s;' % ref)
def handle_entityref(self, ref):
"Propagate entity refs right through."
self.handle_data('&%s;' % ref)
def handle_decl(self, data):
"Propagate DOCTYPEs right through."
self.handle_data('<!%s>' % data)
class BeautifulStoneSoup(BeautifulSoup):
"""A version of BeautifulSoup that doesn't know anything at all
about what HTML tags have special behavior. Useful for parsing
things that aren't HTML, or when BeautifulSoup makes an assumption
counter to what you were expecting."""
IMPLICITLY_CLOSE_TAGS = 0
SELF_CLOSING_TAGS = []
NESTABLE_TAGS = []
QUOTE_TAGS = []

View File

@ -1,85 +0,0 @@
"""
Facade that hides the differences between the SOAPpy and SOAP.py
libraries, so that google.py doesn't have to deal with them.
@author: Brian Landers <brian@bluecoat93.org>
@license: Python
@version: 0.5.4
"""
import warnings
from distutils.version import LooseVersion
__author__ = "Brian Landers <brian@bluecoat93.org>"
__version__ = "0.6"
__license__ = "Python"
#
# Wrapper around the python 'warnings' facility
#
def warn( message, level=RuntimeWarning ):
warnings.warn( message, level, stacklevel=3 )
# We can't use older version of SOAPpy, due to bugs that break the Google API
minSOAPpyVersion = "0.11.3"
#
# Try loading SOAPpy first. If that fails, fall back to the old SOAP.py
#
SOAPpy = None
try:
import SOAPpy
from SOAPpy import SOAPProxy, Types
if LooseVersion( minSOAPpyVersion ) > \
LooseVersion( SOAPpy.version.__version__ ):
warn( "Versions of SOAPpy before %s have known bugs that prevent " +
"PyGoogle from functioning." % minSOAPpyVersion )
raise ImportError
except ImportError:
warn( "SOAPpy not imported. Trying legacy SOAP.py.",
DeprecationWarning )
try:
import SOAP
except ImportError:
raise RuntimeError( "Unable to find SOAPpy or SOAP. Can't continue.\n" )
#
# Constants that differ between the modules
#
if SOAPpy:
false = Types.booleanType(0)
true = Types.booleanType(1)
structType = Types.structType
faultType = Types.faultType
else:
false = SOAP.booleanType(0)
true = SOAP.booleanType(1)
structType = SOAP.structType
faultType = SOAP.faultType
#
# Get a SOAP Proxy object in the correct way for the module we're using
#
def getProxy( url, namespace, http_proxy ):
if SOAPpy:
return SOAPProxy( url,
namespace = namespace,
http_proxy = http_proxy )
else:
return SOAP.SOAPProxy( url,
namespace = namespace,
http_proxy = http_proxy )
#
# Convert an object to a dictionary in the proper way for the module
# we're using for SOAP
#
def toDict( obj ):
if SOAPpy:
return obj._asdict()
else:
return obj._asdict

View File

@ -1,478 +0,0 @@
"""
################################################################################
#
# SOAPpy - Cayce Ullman (cayce@actzero.com)
# Brian Matthews (blm@actzero.com)
# Gregory Warnes (gregory_r_warnes@groton.pfizer.com)
# Christopher Blunck (blunck@gst.com)
#
################################################################################
# Copyright (c) 2003, Pfizer
# Copyright (c) 2001, Cayce Ullman.
# Copyright (c) 2001, Brian Matthews.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# Neither the name of actzero, inc. nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
################################################################################
"""
ident = '$Id$'
from version import __version__
from __future__ import nested_scopes
#import xml.sax
import urllib
from types import *
import re
import base64
# SOAPpy modules
from Errors import *
from Config import Config
from Parser import parseSOAPRPC
from SOAPBuilder import buildSOAP
from Utilities import *
from Types import faultType, simplify
################################################################################
# Client
################################################################################
def SOAPUserAgent():
return "SOAPpy " + __version__ + " (pywebsvcs.sf.net)"
class SOAPAddress:
def __init__(self, url, config = Config):
proto, uri = urllib.splittype(url)
# apply some defaults
if uri[0:2] != '//':
if proto != None:
uri = proto + ':' + uri
uri = '//' + uri
proto = 'http'
host, path = urllib.splithost(uri)
try:
int(host)
host = 'localhost:' + host
except:
pass
if not path:
path = '/'
if proto not in ('http', 'https', 'httpg'):
raise IOError, "unsupported SOAP protocol"
if proto == 'httpg' and not config.GSIclient:
raise AttributeError, \
"GSI client not supported by this Python installation"
if proto == 'https' and not config.SSLclient:
raise AttributeError, \
"SSL client not supported by this Python installation"
self.user,host = urllib.splituser(host)
self.proto = proto
self.host = host
self.path = path
def __str__(self):
return "%(proto)s://%(host)s%(path)s" % self.__dict__
__repr__ = __str__
class HTTPTransport:
def getNS(self, original_namespace, data):
"""Extract the (possibly extended) namespace from the returned
SOAP message."""
if type(original_namespace) == StringType:
pattern="xmlns:\w+=['\"](" + original_namespace + "[^'\"]*)['\"]"
match = re.search(pattern, data)
if match:
return match.group(1)
else:
return original_namespace
else:
return original_namespace
# Need a Timeout someday?
def call(self, addr, data, namespace, soapaction = None, encoding = None,
http_proxy = None, config = Config):
import httplib
if not isinstance(addr, SOAPAddress):
addr = SOAPAddress(addr, config)
# Build a request
if http_proxy:
real_addr = http_proxy
real_path = addr.proto + "://" + addr.host + addr.path
else:
real_addr = addr.host
real_path = addr.path
if addr.proto == 'httpg':
from pyGlobus.io import GSIHTTP
r = GSIHTTP(real_addr, tcpAttr = config.tcpAttr)
elif addr.proto == 'https':
r = httplib.HTTPS(real_addr)
else:
r = httplib.HTTP(real_addr)
r.putrequest("POST", real_path)
r.putheader("Host", addr.host)
r.putheader("User-agent", SOAPUserAgent())
t = 'text/xml';
if encoding != None:
t += '; charset="%s"' % encoding
r.putheader("Content-type", t)
r.putheader("Content-length", str(len(data)))
# if user is not a user:passwd format
# we'll receive a failure from the server. . .I guess (??)
if addr.user != None:
val = base64.encodestring(addr.user)
r.putheader('Authorization','Basic ' + val.replace('\012',''))
# This fixes sending either "" or "None"
if soapaction == None or len(soapaction) == 0:
r.putheader("SOAPAction", "")
else:
r.putheader("SOAPAction", '"%s"' % soapaction)
if config.dumpHeadersOut:
s = 'Outgoing HTTP headers'
debugHeader(s)
print "POST %s %s" % (real_path, r._http_vsn_str)
print "Host:", addr.host
print "User-agent: SOAPpy " + __version__ + " (http://pywebsvcs.sf.net)"
print "Content-type:", t
print "Content-length:", len(data)
print 'SOAPAction: "%s"' % soapaction
debugFooter(s)
r.endheaders()
if config.dumpSOAPOut:
s = 'Outgoing SOAP'
debugHeader(s)
print data,
if data[-1] != '\n':
print
debugFooter(s)
# send the payload
r.send(data)
# read response line
code, msg, headers = r.getreply()
content_type = headers.get("content-type","text/xml")
content_length = headers.get("Content-length")
if content_length == None:
# No Content-Length provided; just read the whole socket
# This won't work with HTTP/1.1 chunked encoding
data = r.getfile().read()
message_len = len(data)
else:
message_len = int(content_length)
data = r.getfile().read(message_len)
if(config.debug):
print "code=",code
print "msg=", msg
print "headers=", headers
print "content-type=", content_type
print "data=", data
if config.dumpHeadersIn:
s = 'Incoming HTTP headers'
debugHeader(s)
if headers.headers:
print "HTTP/1.? %d %s" % (code, msg)
print "\n".join(map (lambda x: x.strip(), headers.headers))
else:
print "HTTP/0.9 %d %s" % (code, msg)
debugFooter(s)
def startswith(string, val):
return string[0:len(val)] == val
if code == 500 and not \
( startswith(content_type, "text/xml") and message_len > 0 ):
raise HTTPError(code, msg)
if config.dumpSOAPIn:
s = 'Incoming SOAP'
debugHeader(s)
print data,
if (len(data)>0) and (data[-1] != '\n'):
print
debugFooter(s)
if code not in (200, 500):
raise HTTPError(code, msg)
# get the new namespace
if namespace is None:
new_ns = None
else:
new_ns = self.getNS(namespace, data)
# return response payload
return data, new_ns
################################################################################
# SOAP Proxy
################################################################################
class SOAPProxy:
def __init__(self, proxy, namespace = None, soapaction = None,
header = None, methodattrs = None, transport = HTTPTransport,
encoding = 'UTF-8', throw_faults = 1, unwrap_results = None,
http_proxy=None, config = Config, noroot = 0,
simplify_objects=None):
# Test the encoding, raising an exception if it's not known
if encoding != None:
''.encode(encoding)
# get default values for unwrap_results and simplify_objects
# from config
if unwrap_results is None:
self.unwrap_results=config.unwrap_results
else:
self.unwrap_results=unwrap_results
if simplify_objects is None:
self.simplify_objects=config.simplify_objects
else:
self.simplify_objects=simplify_objects
self.proxy = SOAPAddress(proxy, config)
self.namespace = namespace
self.soapaction = soapaction
self.header = header
self.methodattrs = methodattrs
self.transport = transport()
self.encoding = encoding
self.throw_faults = throw_faults
self.http_proxy = http_proxy
self.config = config
self.noroot = noroot
# GSI Additions
if hasattr(config, "channel_mode") and \
hasattr(config, "delegation_mode"):
self.channel_mode = config.channel_mode
self.delegation_mode = config.delegation_mode
#end GSI Additions
def invoke(self, method, args):
return self.__call(method, args, {})
def __call(self, name, args, kw, ns = None, sa = None, hd = None,
ma = None):
ns = ns or self.namespace
ma = ma or self.methodattrs
if sa: # Get soapaction
if type(sa) == TupleType:
sa = sa[0]
else:
if self.soapaction:
sa = self.soapaction
else:
sa = name
if hd: # Get header
if type(hd) == TupleType:
hd = hd[0]
else:
hd = self.header
hd = hd or self.header
if ma: # Get methodattrs
if type(ma) == TupleType: ma = ma[0]
else:
ma = self.methodattrs
ma = ma or self.methodattrs
m = buildSOAP(args = args, kw = kw, method = name, namespace = ns,
header = hd, methodattrs = ma, encoding = self.encoding,
config = self.config, noroot = self.noroot)
call_retry = 0
try:
r, self.namespace = self.transport.call(self.proxy, m, ns, sa,
encoding = self.encoding,
http_proxy = self.http_proxy,
config = self.config)
except Exception, ex:
#
# Call failed.
#
# See if we have a fault handling vector installed in our
# config. If we do, invoke it. If it returns a true value,
# retry the call.
#
# In any circumstance other than the fault handler returning
# true, reraise the exception. This keeps the semantics of this
# code the same as without the faultHandler code.
#
if hasattr(self.config, "faultHandler"):
if callable(self.config.faultHandler):
call_retry = self.config.faultHandler(self.proxy, ex)
if not call_retry:
raise
else:
raise
else:
raise
if call_retry:
r, self.namespace = self.transport.call(self.proxy, m, ns, sa,
encoding = self.encoding,
http_proxy = self.http_proxy,
config = self.config)
p, attrs = parseSOAPRPC(r, attrs = 1)
try:
throw_struct = self.throw_faults and \
isinstance (p, faultType)
except:
throw_struct = 0
if throw_struct:
#print p
raise p
# If unwrap_results=1 and there is only element in the struct,
# SOAPProxy will assume that this element is the result
# and return it rather than the struct containing it.
# Otherwise SOAPproxy will return the struct with all the
# elements as attributes.
if self.unwrap_results:
try:
count = 0
for i in p.__dict__.keys():
if i[0] != "_": # don't count the private stuff
count += 1
t = getattr(p, i)
if count == 1: # Only one piece of data, bubble it up
p = t
except:
pass
# Automatically simplfy SOAP complex types into the
# corresponding python types. (structType --> dict,
# arrayType --> array, etc.)
if self.simplify_objects:
p = simplify(p)
if self.config.returnAllAttrs:
return p, attrs
return p
def _callWithBody(self, body):
return self.__call(None, body, {})
def __getattr__(self, name): # hook to catch method calls
if name == '__del__':
raise AttributeError, name
return self.__Method(self.__call, name, config = self.config)
# To handle attribute wierdness
class __Method:
# Some magic to bind a SOAP method to an RPC server.
# Supports "nested" methods (e.g. examples.getStateName) -- concept
# borrowed from xmlrpc/soaplib -- www.pythonware.com
# Altered (improved?) to let you inline namespaces on a per call
# basis ala SOAP::LITE -- www.soaplite.com
def __init__(self, call, name, ns = None, sa = None, hd = None,
ma = None, config = Config):
self.__call = call
self.__name = name
self.__ns = ns
self.__sa = sa
self.__hd = hd
self.__ma = ma
self.__config = config
return
def __call__(self, *args, **kw):
if self.__name[0] == "_":
if self.__name in ["__repr__","__str__"]:
return self.__repr__()
else:
return self.__f_call(*args, **kw)
else:
return self.__r_call(*args, **kw)
def __getattr__(self, name):
if name == '__del__':
raise AttributeError, name
if self.__name[0] == "_":
# Don't nest method if it is a directive
return self.__class__(self.__call, name, self.__ns,
self.__sa, self.__hd, self.__ma)
return self.__class__(self.__call, "%s.%s" % (self.__name, name),
self.__ns, self.__sa, self.__hd, self.__ma)
def __f_call(self, *args, **kw):
if self.__name == "_ns": self.__ns = args
elif self.__name == "_sa": self.__sa = args
elif self.__name == "_hd": self.__hd = args
elif self.__name == "_ma": self.__ma = args
return self
def __r_call(self, *args, **kw):
return self.__call(self.__name, args, kw, self.__ns, self.__sa,
self.__hd, self.__ma)
def __repr__(self):
return "<%s at %d>" % (self.__class__, id(self))

View File

@ -1,202 +0,0 @@
"""
################################################################################
# Copyright (c) 2003, Pfizer
# Copyright (c) 2001, Cayce Ullman.
# Copyright (c) 2001, Brian Matthews.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# Neither the name of actzero, inc. nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
################################################################################
"""
ident = '$Id$'
from version import __version__
import copy, socket
from types import *
from NS import NS
################################################################################
# Configuration class
################################################################################
class SOAPConfig:
__readonly = ('SSLserver', 'SSLclient', 'GSIserver', 'GSIclient')
def __init__(self, config = None, **kw):
d = self.__dict__
if config:
if not isinstance(config, SOAPConfig):
raise AttributeError, \
"initializer must be SOAPConfig instance"
s = config.__dict__
for k, v in s.items():
if k[0] != '_':
d[k] = v
else:
# Setting debug also sets returnFaultInfo,
# dumpHeadersIn, dumpHeadersOut, dumpSOAPIn, and dumpSOAPOut
self.debug = 0
self.dumpFaultInfo = 0
# Setting namespaceStyle sets typesNamespace, typesNamespaceURI,
# schemaNamespace, and schemaNamespaceURI
self.namespaceStyle = '1999'
self.strictNamespaces = 0
self.typed = 1
self.buildWithNamespacePrefix = 1
self.returnAllAttrs = 0
# Strict checking of range for floats and doubles
self.strict_range = 0
# Default encoding for dictionary keys
self.dict_encoding = 'ascii'
# New argument name handling mechanism. See
# README.MethodParameterNaming for details
self.specialArgs = 1
# If unwrap_results=1 and there is only element in the struct,
# SOAPProxy will assume that this element is the result
# and return it rather than the struct containing it.
# Otherwise SOAPproxy will return the struct with all the
# elements as attributes.
self.unwrap_results = 1
# Automatically convert SOAP complex types, and
# (recursively) public contents into the corresponding
# python types. (Private subobjects have names that start
# with '_'.)
#
# Conversions:
# - faultType --> raise python exception
# - arrayType --> array
# - compoundType --> dictionary
#
self.simplify_objects = 0
# Per-class authorization method. If this is set, before
# calling a any class method, the specified authorization
# method will be called. If it returns 1, the method call
# will proceed, otherwise the call will throw with an
# authorization error.
self.authMethod = None
# Globus Support if pyGlobus.io available
try:
from pyGlobus import io;
d['GSIserver'] = 1
d['GSIclient'] = 1
except:
d['GSIserver'] = 0
d['GSIclient'] = 0
# Server SSL support if M2Crypto.SSL available
try:
from M2Crypto import SSL
d['SSLserver'] = 1
except:
d['SSLserver'] = 0
# Client SSL support if socket.ssl available
try:
from socket import ssl
d['SSLclient'] = 1
except:
d['SSLclient'] = 0
for k, v in kw.items():
if k[0] != '_':
setattr(self, k, v)
def __setattr__(self, name, value):
if name in self.__readonly:
raise AttributeError, "readonly configuration setting"
d = self.__dict__
if name in ('typesNamespace', 'typesNamespaceURI',
'schemaNamespace', 'schemaNamespaceURI'):
if name[-3:] == 'URI':
base, uri = name[:-3], 1
else:
base, uri = name, 0
if type(value) == StringType:
if NS.NSMAP.has_key(value):
n = (value, NS.NSMAP[value])
elif NS.NSMAP_R.has_key(value):
n = (NS.NSMAP_R[value], value)
else:
raise AttributeError, "unknown namespace"
elif type(value) in (ListType, TupleType):
if uri:
n = (value[1], value[0])
else:
n = (value[0], value[1])
else:
raise AttributeError, "unknown namespace type"
d[base], d[base + 'URI'] = n
try:
d['namespaceStyle'] = \
NS.STMAP_R[(d['typesNamespace'], d['schemaNamespace'])]
except:
d['namespaceStyle'] = ''
elif name == 'namespaceStyle':
value = str(value)
if not NS.STMAP.has_key(value):
raise AttributeError, "unknown namespace style"
d[name] = value
n = d['typesNamespace'] = NS.STMAP[value][0]
d['typesNamespaceURI'] = NS.NSMAP[n]
n = d['schemaNamespace'] = NS.STMAP[value][1]
d['schemaNamespaceURI'] = NS.NSMAP[n]
elif name == 'debug':
d[name] = \
d['returnFaultInfo'] = \
d['dumpHeadersIn'] = \
d['dumpHeadersOut'] = \
d['dumpSOAPIn'] = \
d['dumpSOAPOut'] = value
else:
d[name] = value
Config = SOAPConfig()

View File

@ -1,79 +0,0 @@
"""
################################################################################
#
# SOAPpy - Cayce Ullman (cayce@actzero.com)
# Brian Matthews (blm@actzero.com)
# Gregory Warnes (gregory_r_warnes@groton.pfizer.com)
# Christopher Blunck (blunck@gst.com)
#
################################################################################
# Copyright (c) 2003, Pfizer
# Copyright (c) 2001, Cayce Ullman.
# Copyright (c) 2001, Brian Matthews.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# Neither the name of actzero, inc. nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
################################################################################
"""
ident = '$Id$'
from version import __version__
import exceptions
################################################################################
# Exceptions
################################################################################
class Error(exceptions.Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return "<Error : %s>" % self.msg
__repr__ = __str__
def __call__(self):
return (msg,)
class RecursionError(Error):
pass
class UnknownTypeError(Error):
pass
class HTTPError(Error):
# indicates an HTTP protocol error
def __init__(self, code, msg):
self.code = code
self.msg = msg
def __str__(self):
return "<HTTPError %s %s>" % (self.code, self.msg)
__repr__ = __str__
def __call___(self):
return (self.code, self.msg, )
class UnderflowError(exceptions.ArithmeticError):
pass

View File

@ -1,143 +0,0 @@
"""
GSIServer - Contributed by Ivan R. Judson <judson@mcs.anl.gov>
################################################################################
#
# SOAPpy - Cayce Ullman (cayce@actzero.com)
# Brian Matthews (blm@actzero.com)
# Gregory Warnes (gregory_r_warnes@groton.pfizer.com)
# Christopher Blunck (blunck@gst.com)
#
################################################################################
# Copyright (c) 2003, Pfizer
# Copyright (c) 2001, Cayce Ullman.
# Copyright (c) 2001, Brian Matthews.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# Neither the name of actzero, inc. nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
################################################################################
"""
ident = '$Id$'
from version import __version__
from __future__ import nested_scopes
#import xml.sax
import re
import socket
import sys
import SocketServer
from types import *
import BaseHTTPServer
# SOAPpy modules
from Parser import parseSOAPRPC
from Config import SOAPConfig
from Types import faultType, voidType, simplify
from NS import NS
from SOAPBuilder import buildSOAP
from Utilities import debugHeader, debugFooter
try: from M2Crypto import SSL
except: pass
#####
from Server import *
from pyGlobus.io import GSITCPSocketServer, ThreadingGSITCPSocketServer
from pyGlobus import ioc
def GSIConfig():
config = SOAPConfig()
config.channel_mode = ioc.GLOBUS_IO_SECURE_CHANNEL_MODE_GSI_WRAP
config.delegation_mode = ioc.GLOBUS_IO_SECURE_DELEGATION_MODE_FULL_PROXY
config.tcpAttr = None
config.authMethod = "_authorize"
return config
Config = GSIConfig()
class GSISOAPServer(GSITCPSocketServer, SOAPServerBase):
def __init__(self, addr = ('localhost', 8000),
RequestHandler = SOAPRequestHandler, log = 0,
encoding = 'UTF-8', config = Config, namespace = None):
# Test the encoding, raising an exception if it's not known
if encoding != None:
''.encode(encoding)
self.namespace = namespace
self.objmap = {}
self.funcmap = {}
self.encoding = encoding
self.config = config
self.log = log
self.allow_reuse_address= 1
GSITCPSocketServer.__init__(self, addr, RequestHandler,
self.config.channel_mode,
self.config.delegation_mode,
tcpAttr = self.config.tcpAttr)
def get_request(self):
sock, addr = GSITCPSocketServer.get_request(self)
return sock, addr
class ThreadingGSISOAPServer(ThreadingGSITCPSocketServer, SOAPServerBase):
def __init__(self, addr = ('localhost', 8000),
RequestHandler = SOAPRequestHandler, log = 0,
encoding = 'UTF-8', config = Config, namespace = None):
# Test the encoding, raising an exception if it's not known
if encoding != None:
''.encode(encoding)
self.namespace = namespace
self.objmap = {}
self.funcmap = {}
self.encoding = encoding
self.config = config
self.log = log
self.allow_reuse_address= 1
ThreadingGSITCPSocketServer.__init__(self, addr, RequestHandler,
self.config.channel_mode,
self.config.delegation_mode,
tcpAttr = self.config.tcpAttr)
def get_request(self):
sock, addr = ThreadingGSITCPSocketServer.get_request(self)
return sock, addr

View File

@ -1,104 +0,0 @@
"""
################################################################################
#
# SOAPpy - Cayce Ullman (cayce@actzero.com)
# Brian Matthews (blm@actzero.com)
# Gregory Warnes (gregory_r_warnes@groton.pfizer.com)
# Christopher Blunck (blunck@gst.com)
#
################################################################################
# Copyright (c) 2003, Pfizer
# Copyright (c) 2001, Cayce Ullman.
# Copyright (c) 2001, Brian Matthews.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# Neither the name of actzero, inc. nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
################################################################################
"""
from __future__ import nested_scopes
ident = '$Id$'
from version import __version__
##############################################################################
# Namespace Class
################################################################################
def invertDict(dict):
d = {}
for k, v in dict.items():
d[v] = k
return d
class NS:
XML = "http://www.w3.org/XML/1998/namespace"
ENV = "http://schemas.xmlsoap.org/soap/envelope/"
ENC = "http://schemas.xmlsoap.org/soap/encoding/"
XSD = "http://www.w3.org/1999/XMLSchema"
XSD2 = "http://www.w3.org/2000/10/XMLSchema"
XSD3 = "http://www.w3.org/2001/XMLSchema"
XSD_L = [XSD, XSD2, XSD3]
EXSD_L= [ENC, XSD, XSD2, XSD3]
XSI = "http://www.w3.org/1999/XMLSchema-instance"
XSI2 = "http://www.w3.org/2000/10/XMLSchema-instance"
XSI3 = "http://www.w3.org/2001/XMLSchema-instance"
XSI_L = [XSI, XSI2, XSI3]
URN = "http://soapinterop.org/xsd"
# For generated messages
XML_T = "xml"
ENV_T = "SOAP-ENV"
ENC_T = "SOAP-ENC"
XSD_T = "xsd"
XSD2_T= "xsd2"
XSD3_T= "xsd3"
XSI_T = "xsi"
XSI2_T= "xsi2"
XSI3_T= "xsi3"
URN_T = "urn"
NSMAP = {ENV_T: ENV, ENC_T: ENC, XSD_T: XSD, XSD2_T: XSD2,
XSD3_T: XSD3, XSI_T: XSI, XSI2_T: XSI2, XSI3_T: XSI3,
URN_T: URN}
NSMAP_R = invertDict(NSMAP)
STMAP = {'1999': (XSD_T, XSI_T), '2000': (XSD2_T, XSI2_T),
'2001': (XSD3_T, XSI3_T)}
STMAP_R = invertDict(STMAP)
def __init__(self):
raise Error, "Don't instantiate this"

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +0,0 @@
"""This file is here for backward compatibility with versions <= 0.9.9
Delete when 1.0.0 is released!
"""
ident = '$Id$'
from version import __version__
from Client import *
from Config import *
from Errors import *
from NS import *
from Parser import *
from SOAPBuilder import *
from Server import *
from Types import *
from Utilities import *
import wstools
import WSDL
from warnings import warn
warn("""
The sub-module SOAPpy.SOAP is deprecated and is only
provided for short-term backward compatibility. Objects are now
available directly within the SOAPpy module. Thus, instead of
from SOAPpy import SOAP
...
SOAP.SOAPProxy(...)
use
from SOAPpy import SOAPProxy
...
SOAPProxy(...)
instead.
""", DeprecationWarning)

View File

@ -1,620 +0,0 @@
"""
################################################################################
# Copyright (c) 2003, Pfizer
# Copyright (c) 2001, Cayce Ullman.
# Copyright (c) 2001, Brian Matthews.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# Neither the name of actzero, inc. nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
################################################################################
"""
ident = '$Id$'
from version import __version__
import cgi
import copy
from wstools.XMLname import toXMLname, fromXMLname
import fpconst
# SOAPpy modules
from Config import Config
from NS import NS
from Types import *
# Test whether this Python version has Types.BooleanType
# If it doesn't have it, then False and True are serialized as integers
try:
BooleanType
pythonHasBooleanType = 1
except NameError:
pythonHasBooleanType = 0
################################################################################
# SOAP Builder
################################################################################
class SOAPBuilder:
_xml_top = '<?xml version="1.0"?>\n'
_xml_enc_top = '<?xml version="1.0" encoding="%s"?>\n'
_env_top = '%(ENV_T)s:Envelope %(ENV_T)s:encodingStyle="%(ENC)s"' % \
NS.__dict__
_env_bot = '</%(ENV_T)s:Envelope>\n' % NS.__dict__
# Namespaces potentially defined in the Envelope tag.
_env_ns = {NS.ENC: NS.ENC_T, NS.ENV: NS.ENV_T,
NS.XSD: NS.XSD_T, NS.XSD2: NS.XSD2_T, NS.XSD3: NS.XSD3_T,
NS.XSI: NS.XSI_T, NS.XSI2: NS.XSI2_T, NS.XSI3: NS.XSI3_T}
def __init__(self, args = (), kw = {}, method = None, namespace = None,
header = None, methodattrs = None, envelope = 1, encoding = 'UTF-8',
use_refs = 0, config = Config, noroot = 0):
# Test the encoding, raising an exception if it's not known
if encoding != None:
''.encode(encoding)
self.args = args
self.kw = kw
self.envelope = envelope
self.encoding = encoding
self.method = method
self.namespace = namespace
self.header = header
self.methodattrs= methodattrs
self.use_refs = use_refs
self.config = config
self.out = []
self.tcounter = 0
self.ncounter = 1
self.icounter = 1
self.envns = {}
self.ids = {}
self.depth = 0
self.multirefs = []
self.multis = 0
self.body = not isinstance(args, bodyType)
self.noroot = noroot
def build(self):
if Config.debug: print "In build."
ns_map = {}
# Cache whether typing is on or not
typed = self.config.typed
if self.header:
# Create a header.
self.dump(self.header, "Header", typed = typed)
self.header = None # Wipe it out so no one is using it.
if self.body:
# Call genns to record that we've used SOAP-ENV.
self.depth += 1
body_ns = self.genns(ns_map, NS.ENV)[0]
self.out.append("<%sBody>\n" % body_ns)
if self.method:
self.depth += 1
a = ''
if self.methodattrs:
for (k, v) in self.methodattrs.items():
a += ' %s="%s"' % (k, v)
if self.namespace: # Use the namespace info handed to us
methodns, n = self.genns(ns_map, self.namespace)
else:
methodns, n = '', ''
self.out.append('<%s%s%s%s%s>\n' % (
methodns, self.method, n, a, self.genroot(ns_map)))
try:
if type(self.args) != TupleType:
args = (self.args,)
else:
args = self.args
for i in args:
self.dump(i, typed = typed, ns_map = ns_map)
if hasattr(self.config, "argsOrdering") and self.config.argsOrdering.has_key(self.method):
for k in self.config.argsOrdering.get(self.method):
self.dump(self.kw.get(k), k, typed = typed, ns_map = ns_map)
else:
for (k, v) in self.kw.items():
self.dump(v, k, typed = typed, ns_map = ns_map)
except RecursionError:
if self.use_refs == 0:
# restart
b = SOAPBuilder(args = self.args, kw = self.kw,
method = self.method, namespace = self.namespace,
header = self.header, methodattrs = self.methodattrs,
envelope = self.envelope, encoding = self.encoding,
use_refs = 1, config = self.config)
return b.build()
raise
if self.method:
self.out.append("</%s%s>\n" % (methodns, self.method))
self.depth -= 1
if self.body:
# dump may add to self.multirefs, but the for loop will keep
# going until it has used all of self.multirefs, even those
# entries added while in the loop.
self.multis = 1
for obj, tag in self.multirefs:
self.dump(obj, tag, typed = typed, ns_map = ns_map)
self.out.append("</%sBody>\n" % body_ns)
self.depth -= 1
if self.envelope:
e = map (lambda ns: ' xmlns:%s="%s"' % (ns[1], ns[0]),
self.envns.items())
self.out = ['<', self._env_top] + e + ['>\n'] + \
self.out + \
[self._env_bot]
if self.encoding != None:
self.out.insert(0, self._xml_enc_top % self.encoding)
return ''.join(self.out).encode(self.encoding)
self.out.insert(0, self._xml_top)
return ''.join(self.out)
def gentag(self):
if Config.debug: print "In gentag."
self.tcounter += 1
return "v%d" % self.tcounter
def genns(self, ns_map, nsURI):
if nsURI == None:
return ('', '')
if type(nsURI) == TupleType: # already a tuple
if len(nsURI) == 2:
ns, nsURI = nsURI
else:
ns, nsURI = None, nsURI[0]
else:
ns = None
if ns_map.has_key(nsURI):
return (ns_map[nsURI] + ':', '')
if self._env_ns.has_key(nsURI):
ns = self.envns[nsURI] = ns_map[nsURI] = self._env_ns[nsURI]
return (ns + ':', '')
if not ns:
ns = "ns%d" % self.ncounter
self.ncounter += 1
ns_map[nsURI] = ns
if self.config.buildWithNamespacePrefix:
return (ns + ':', ' xmlns:%s="%s"' % (ns, nsURI))
else:
return ('', ' xmlns="%s"' % (nsURI))
def genroot(self, ns_map):
if self.noroot:
return ''
if self.depth != 2:
return ''
ns, n = self.genns(ns_map, NS.ENC)
return ' %sroot="%d"%s' % (ns, not self.multis, n)
# checkref checks an element to see if it needs to be encoded as a
# multi-reference element or not. If it returns None, the element has
# been handled and the caller can continue with subsequent elements.
# If it returns a string, the string should be included in the opening
# tag of the marshaled element.
def checkref(self, obj, tag, ns_map):
if self.depth < 2:
return ''
if not self.ids.has_key(id(obj)):
n = self.ids[id(obj)] = self.icounter
self.icounter = n + 1
if self.use_refs == 0:
return ''
if self.depth == 2:
return ' id="i%d"' % n
self.multirefs.append((obj, tag))
else:
if self.use_refs == 0:
raise RecursionError, "Cannot serialize recursive object"
n = self.ids[id(obj)]
if self.multis and self.depth == 2:
return ' id="i%d"' % n
self.out.append('<%s href="#i%d"%s/>\n' %
(tag, n, self.genroot(ns_map)))
return None
# dumpers
def dump(self, obj, tag = None, typed = 1, ns_map = {}):
if Config.debug: print "In dump.", "obj=", obj
ns_map = ns_map.copy()
self.depth += 1
if type(tag) not in (NoneType, StringType, UnicodeType):
raise KeyError, "tag must be a string or None"
try:
meth = getattr(self, "dump_" + type(obj).__name__)
except AttributeError:
if type(obj) == LongType:
obj_type = "integer"
elif pythonHasBooleanType and type(obj) == BooleanType:
obj_type = "boolean"
else:
obj_type = type(obj).__name__
self.out.append(self.dumper(None, obj_type, obj, tag, typed,
ns_map, self.genroot(ns_map)))
else:
meth(obj, tag, typed, ns_map)
self.depth -= 1
# generic dumper
def dumper(self, nsURI, obj_type, obj, tag, typed = 1, ns_map = {},
rootattr = '', id = '',
xml = '<%(tag)s%(type)s%(id)s%(attrs)s%(root)s>%(data)s</%(tag)s>\n'):
if Config.debug: print "In dumper."
if nsURI == None:
nsURI = self.config.typesNamespaceURI
tag = tag or self.gentag()
tag = toXMLname(tag) # convert from SOAP 1.2 XML name encoding
a = n = t = ''
if typed and obj_type:
ns, n = self.genns(ns_map, nsURI)
ins = self.genns(ns_map, self.config.schemaNamespaceURI)[0]
t = ' %stype="%s%s"%s' % (ins, ns, obj_type, n)
try: a = obj._marshalAttrs(ns_map, self)
except: pass
try: data = obj._marshalData()
except:
if (obj_type != "string"): # strings are already encoded
data = cgi.escape(str(obj))
else:
data = obj
return xml % {"tag": tag, "type": t, "data": data, "root": rootattr,
"id": id, "attrs": a}
def dump_float(self, obj, tag, typed = 1, ns_map = {}):
if Config.debug: print "In dump_float."
tag = tag or self.gentag()
tag = toXMLname(tag) # convert from SOAP 1.2 XML name encoding
if Config.strict_range:
doubleType(obj)
if fpconst.isPosInf(obj):
obj = "INF"
elif fpconst.isNegInf(obj):
obj = "-INF"
elif fpconst.isNaN(obj):
obj = "NaN"
else:
obj = str(obj)
# Note: python 'float' is actually a SOAP 'double'.
self.out.append(self.dumper(None, "double", obj, tag, typed, ns_map,
self.genroot(ns_map)))
def dump_string(self, obj, tag, typed = 0, ns_map = {}):
if Config.debug: print "In dump_string."
tag = tag or self.gentag()
tag = toXMLname(tag) # convert from SOAP 1.2 XML name encoding
id = self.checkref(obj, tag, ns_map)
if id == None:
return
try: data = obj._marshalData()
except: data = obj
self.out.append(self.dumper(None, "string", cgi.escape(data), tag,
typed, ns_map, self.genroot(ns_map), id))
dump_str = dump_string # For Python 2.2+
dump_unicode = dump_string
def dump_None(self, obj, tag, typed = 0, ns_map = {}):
if Config.debug: print "In dump_None."
tag = tag or self.gentag()
tag = toXMLname(tag) # convert from SOAP 1.2 XML name encoding
ns = self.genns(ns_map, self.config.schemaNamespaceURI)[0]
self.out.append('<%s %snull="1"%s/>\n' %
(tag, ns, self.genroot(ns_map)))
dump_NoneType = dump_None # For Python 2.2+
def dump_list(self, obj, tag, typed = 1, ns_map = {}):
if Config.debug: print "In dump_list.", "obj=", obj
tag = tag or self.gentag()
tag = toXMLname(tag) # convert from SOAP 1.2 XML name encoding
if type(obj) == InstanceType:
data = obj.data
else:
data = obj
id = self.checkref(obj, tag, ns_map)
if id == None:
return
try:
sample = data[0]
empty = 0
except:
# preserve type if present
if getattr(obj,"_typed",None) and getattr(obj,"_type",None):
if getattr(obj, "_complexType", None):
sample = typedArrayType(typed=obj._type,
complexType = obj._complexType)
sample._typename = obj._type
obj._ns = NS.URN
else:
sample = typedArrayType(typed=obj._type)
else:
sample = structType()
empty = 1
# First scan list to see if all are the same type
same_type = 1
if not empty:
for i in data[1:]:
if type(sample) != type(i) or \
(type(sample) == InstanceType and \
sample.__class__ != i.__class__):
same_type = 0
break
ndecl = ''
if same_type:
if (isinstance(sample, structType)) or \
type(sample) == DictType or \
(isinstance(sample, anyType) and \
(getattr(sample, "_complexType", None) and \
sample._complexType)): # force to urn struct
try:
tns = obj._ns or NS.URN
except:
tns = NS.URN
ns, ndecl = self.genns(ns_map, tns)
try:
typename = sample._typename
except:
typename = "SOAPStruct"
t = ns + typename
elif isinstance(sample, anyType):
ns = sample._validNamespaceURI(self.config.typesNamespaceURI,
self.config.strictNamespaces)
if ns:
ns, ndecl = self.genns(ns_map, ns)
t = ns + sample._type
else:
t = 'ur-type'
else:
typename = type(sample).__name__
# For Python 2.2+
if type(sample) == StringType: typename = 'string'
# HACK: python 'float' is actually a SOAP 'double'.
if typename=="float": typename="double"
t = self.genns(ns_map, self.config.typesNamespaceURI)[0] + \
typename
else:
t = self.genns(ns_map, self.config.typesNamespaceURI)[0] + \
"ur-type"
try: a = obj._marshalAttrs(ns_map, self)
except: a = ''
ens, edecl = self.genns(ns_map, NS.ENC)
ins, idecl = self.genns(ns_map, self.config.schemaNamespaceURI)
self.out.append(
'<%s %sarrayType="%s[%d]" %stype="%sArray"%s%s%s%s%s%s>\n' %
(tag, ens, t, len(data), ins, ens, ndecl, edecl, idecl,
self.genroot(ns_map), id, a))
typed = not same_type
try: elemsname = obj._elemsname
except: elemsname = "item"
for i in data:
self.dump(i, elemsname, typed, ns_map)
self.out.append('</%s>\n' % tag)
dump_tuple = dump_list
def dump_dictionary(self, obj, tag, typed = 1, ns_map = {}):
if Config.debug: print "In dump_dictionary."
tag = tag or self.gentag()
tag = toXMLname(tag) # convert from SOAP 1.2 XML name encoding
id = self.checkref(obj, tag, ns_map)
if id == None:
return
try: a = obj._marshalAttrs(ns_map, self)
except: a = ''
self.out.append('<%s%s%s%s>\n' %
(tag, id, a, self.genroot(ns_map)))
for (k, v) in obj.items():
if k[0] != "_":
self.dump(v, k, 1, ns_map)
self.out.append('</%s>\n' % tag)
dump_dict = dump_dictionary # For Python 2.2+
def dump_instance(self, obj, tag, typed = 1, ns_map = {}):
if Config.debug: print "In dump_instance.", "obj=", obj, "tag=", tag
if not tag:
# If it has a name use it.
if isinstance(obj, anyType) and obj._name:
tag = obj._name
else:
tag = self.gentag()
tag = toXMLname(tag) # convert from SOAP 1.2 XML name encoding
if isinstance(obj, arrayType): # Array
self.dump_list(obj, tag, typed, ns_map)
return
if isinstance(obj, faultType): # Fault
cns, cdecl = self.genns(ns_map, NS.ENC)
vns, vdecl = self.genns(ns_map, NS.ENV)
self.out.append('''<%sFault %sroot="1"%s%s>
<faultcode>%s</faultcode>
<faultstring>%s</faultstring>
''' % (vns, cns, vdecl, cdecl, obj.faultcode, obj.faultstring))
if hasattr(obj, "detail"):
self.dump(obj.detail, "detail", typed, ns_map)
self.out.append("</%sFault>\n" % vns)
return
r = self.genroot(ns_map)
try: a = obj._marshalAttrs(ns_map, self)
except: a = ''
if isinstance(obj, voidType): # void
self.out.append("<%s%s%s></%s>\n" % (tag, a, r, tag))
return
id = self.checkref(obj, tag, ns_map)
if id == None:
return
if isinstance(obj, structType):
# Check for namespace
ndecl = ''
ns = obj._validNamespaceURI(self.config.typesNamespaceURI,
self.config.strictNamespaces)
if ns:
ns, ndecl = self.genns(ns_map, ns)
tag = ns + tag
self.out.append("<%s%s%s%s%s>\n" % (tag, ndecl, id, a, r))
keylist = obj.__dict__.keys()
# first write out items with order information
if hasattr(obj, '_keyord'):
for i in range(len(obj._keyord)):
self.dump(obj._aslist(i), obj._keyord[i], 1, ns_map)
keylist.remove(obj._keyord[i])
# now write out the rest
for k in keylist:
if (k[0] != "_"):
self.dump(getattr(obj,k), k, 1, ns_map)
if isinstance(obj, bodyType):
self.multis = 1
for v, k in self.multirefs:
self.dump(v, k, typed = typed, ns_map = ns_map)
self.out.append('</%s>\n' % tag)
elif isinstance(obj, anyType):
t = ''
if typed:
ns = obj._validNamespaceURI(self.config.typesNamespaceURI,
self.config.strictNamespaces)
if ns:
ons, ondecl = self.genns(ns_map, ns)
ins, indecl = self.genns(ns_map,
self.config.schemaNamespaceURI)
t = ' %stype="%s%s"%s%s' % \
(ins, ons, obj._type, ondecl, indecl)
self.out.append('<%s%s%s%s%s>%s</%s>\n' %
(tag, t, id, a, r, obj._marshalData(), tag))
else: # Some Class
self.out.append('<%s%s%s>\n' % (tag, id, r))
for (k, v) in obj.__dict__.items():
if k[0] != "_":
self.dump(v, k, 1, ns_map)
self.out.append('</%s>\n' % tag)
################################################################################
# SOAPBuilder's more public interface
################################################################################
def buildSOAP(args=(), kw={}, method=None, namespace=None, header=None,
methodattrs=None,envelope=1,encoding='UTF-8',config=Config,noroot = 0):
t = SOAPBuilder(args=args,kw=kw, method=method, namespace=namespace,
header=header, methodattrs=methodattrs,envelope=envelope,
encoding=encoding, config=config,noroot=noroot)
return t.build()

View File

@ -1,706 +0,0 @@
"""
################################################################################
#
# SOAPpy - Cayce Ullman (cayce@actzero.com)
# Brian Matthews (blm@actzero.com)
# Gregory Warnes (gregory_r_warnes@groton.pfizer.com)
# Christopher Blunck (blunck@gst.com)
#
################################################################################
# Copyright (c) 2003, Pfizer
# Copyright (c) 2001, Cayce Ullman.
# Copyright (c) 2001, Brian Matthews.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# Neither the name of actzero, inc. nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
################################################################################
"""
ident = '$Id$'
from version import __version__
from __future__ import nested_scopes
#import xml.sax
import re
import socket
import sys
import SocketServer
from types import *
import BaseHTTPServer
import thread
# SOAPpy modules
from Parser import parseSOAPRPC
from Config import Config
from Types import faultType, voidType, simplify
from NS import NS
from SOAPBuilder import buildSOAP
from Utilities import debugHeader, debugFooter
try: from M2Crypto import SSL
except: pass
ident = '$Id$'
from version import __version__
################################################################################
# Call context dictionary
################################################################################
_contexts = dict()
def GetSOAPContext():
global _contexts
return _contexts[thread.get_ident()]
################################################################################
# Server
################################################################################
# Method Signature class for adding extra info to registered funcs, right now
# used just to indicate it should be called with keywords, instead of ordered
# params.
class MethodSig:
def __init__(self, func, keywords=0, context=0):
self.func = func
self.keywords = keywords
self.context = context
self.__name__ = func.__name__
def __call__(self, *args, **kw):
return apply(self.func,args,kw)
class SOAPContext:
def __init__(self, header, body, attrs, xmldata, connection, httpheaders,
soapaction):
self.header = header
self.body = body
self.attrs = attrs
self.xmldata = xmldata
self.connection = connection
self.httpheaders= httpheaders
self.soapaction = soapaction
# A class to describe how header messages are handled
class HeaderHandler:
# Initially fail out if there are any problems.
def __init__(self, header, attrs):
for i in header.__dict__.keys():
if i[0] == "_":
continue
d = getattr(header, i)
try:
fault = int(attrs[id(d)][(NS.ENV, 'mustUnderstand')])
except:
fault = 0
if fault:
raise faultType, ("%s:MustUnderstand" % NS.ENV_T,
"Required Header Misunderstood",
"%s" % i)
################################################################################
# SOAP Server
################################################################################
class SOAPServerBase:
def get_request(self):
sock, addr = SocketServer.TCPServer.get_request(self)
if self.ssl_context:
sock = SSL.Connection(self.ssl_context, sock)
sock._setup_ssl(addr)
if sock.accept_ssl() != 1:
raise socket.error, "Couldn't accept SSL connection"
return sock, addr
def registerObject(self, object, namespace = '', path = ''):
if namespace == '' and path == '': namespace = self.namespace
if namespace == '' and path != '':
namespace = path.replace("/", ":")
if namespace[0] == ":": namespace = namespace[1:]
self.objmap[namespace] = object
def registerFunction(self, function, namespace = '', funcName = None,
path = ''):
if not funcName : funcName = function.__name__
if namespace == '' and path == '': namespace = self.namespace
if namespace == '' and path != '':
namespace = path.replace("/", ":")
if namespace[0] == ":": namespace = namespace[1:]
if self.funcmap.has_key(namespace):
self.funcmap[namespace][funcName] = function
else:
self.funcmap[namespace] = {funcName : function}
def registerKWObject(self, object, namespace = '', path = ''):
if namespace == '' and path == '': namespace = self.namespace
if namespace == '' and path != '':
namespace = path.replace("/", ":")
if namespace[0] == ":": namespace = namespace[1:]
for i in dir(object.__class__):
if i[0] != "_" and callable(getattr(object, i)):
self.registerKWFunction(getattr(object,i), namespace)
# convenience - wraps your func for you.
def registerKWFunction(self, function, namespace = '', funcName = None,
path = ''):
if namespace == '' and path == '': namespace = self.namespace
if namespace == '' and path != '':
namespace = path.replace("/", ":")
if namespace[0] == ":": namespace = namespace[1:]
self.registerFunction(MethodSig(function,keywords=1), namespace,
funcName)
def unregisterObject(self, object, namespace = '', path = ''):
if namespace == '' and path == '': namespace = self.namespace
if namespace == '' and path != '':
namespace = path.replace("/", ":")
if namespace[0] == ":": namespace = namespace[1:]
del self.objmap[namespace]
class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def version_string(self):
return '<a href="http://pywebsvcs.sf.net">' + \
'SOAPpy ' + __version__ + '</a> (Python ' + \
sys.version.split()[0] + ')'
def date_time_string(self):
self.__last_date_time_string = \
BaseHTTPServer.BaseHTTPRequestHandler.\
date_time_string(self)
return self.__last_date_time_string
def do_POST(self):
global _contexts
status = 500
try:
if self.server.config.dumpHeadersIn:
s = 'Incoming HTTP headers'
debugHeader(s)
print self.raw_requestline.strip()
print "\n".join(map (lambda x: x.strip(),
self.headers.headers))
debugFooter(s)
data = self.rfile.read(int(self.headers["Content-length"]))
if self.server.config.dumpSOAPIn:
s = 'Incoming SOAP'
debugHeader(s)
print data,
if data[-1] != '\n':
print
debugFooter(s)
(r, header, body, attrs) = \
parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
method = r._name
args = r._aslist()
kw = r._asdict()
if Config.simplify_objects:
args = simplify(args)
kw = simplify(kw)
# Handle mixed named and unnamed arguments by assuming
# that all arguments with names of the form "v[0-9]+"
# are unnamed and should be passed in numeric order,
# other arguments are named and should be passed using
# this name.
# This is a non-standard exension to the SOAP protocol,
# but is supported by Apache AXIS.
# It is enabled by default. To disable, set
# Config.specialArgs to False.
if Config.specialArgs:
ordered_args = {}
named_args = {}
for (k,v) in kw.items():
if k[0]=="v":
try:
i = int(k[1:])
ordered_args[i] = v
except ValueError:
named_args[str(k)] = v
else:
named_args[str(k)] = v
# We have to decide namespace precedence
# I'm happy with the following scenario
# if r._ns is specified use it, if not check for
# a path, if it's specified convert it and use it as the
# namespace. If both are specified, use r._ns.
ns = r._ns
if len(self.path) > 1 and not ns:
ns = self.path.replace("/", ":")
if ns[0] == ":": ns = ns[1:]
# authorization method
a = None
keylist = ordered_args.keys()
keylist.sort()
# create list in proper order w/o names
tmp = map( lambda x: ordered_args[x], keylist)
ordered_args = tmp
#print '<-> Argument Matching Yielded:'
#print '<-> Ordered Arguments:' + str(ordered_args)
#print '<-> Named Arguments :' + str(named_args)
resp = ""
# For fault messages
if ns:
nsmethod = "%s:%s" % (ns, method)
else:
nsmethod = method
try:
# First look for registered functions
if self.server.funcmap.has_key(ns) and \
self.server.funcmap[ns].has_key(method):
f = self.server.funcmap[ns][method]
# look for the authorization method
if self.server.config.authMethod != None:
authmethod = self.server.config.authMethod
if self.server.funcmap.has_key(ns) and \
self.server.funcmap[ns].has_key(authmethod):
a = self.server.funcmap[ns][authmethod]
else:
# Now look at registered objects
# Check for nested attributes. This works even if
# there are none, because the split will return
# [method]
f = self.server.objmap[ns]
# Look for the authorization method
if self.server.config.authMethod != None:
authmethod = self.server.config.authMethod
if hasattr(f, authmethod):
a = getattr(f, authmethod)
# then continue looking for the method
l = method.split(".")
for i in l:
f = getattr(f, i)
except:
info = sys.exc_info()
try:
resp = buildSOAP(faultType("%s:Client" % NS.ENV_T,
"Method Not Found",
"%s : %s %s %s" % (nsmethod,
info[0],
info[1],
info[2])),
encoding = self.server.encoding,
config = self.server.config)
finally:
del info
status = 500
else:
try:
if header:
x = HeaderHandler(header, attrs)
fr = 1
# call context book keeping
# We're stuffing the method into the soapaction if there
# isn't one, someday, we'll set that on the client
# and it won't be necessary here
# for now we're doing both
if "SOAPAction".lower() not in self.headers.keys() or \
self.headers["SOAPAction"] == "\"\"":
self.headers["SOAPAction"] = method
thread_id = thread.get_ident()
_contexts[thread_id] = SOAPContext(header, body,
attrs, data,
self.connection,
self.headers,
self.headers["SOAPAction"])
# Do an authorization check
if a != None:
if not apply(a, (), {"_SOAPContext" :
_contexts[thread_id] }):
raise faultType("%s:Server" % NS.ENV_T,
"Authorization failed.",
"%s" % nsmethod)
# If it's wrapped, some special action may be needed
if isinstance(f, MethodSig):
c = None
if f.context: # retrieve context object
c = _contexts[thread_id]
if Config.specialArgs:
if c:
named_args["_SOAPContext"] = c
fr = apply(f, ordered_args, named_args)
elif f.keywords:
# This is lame, but have to de-unicode
# keywords
strkw = {}
for (k, v) in kw.items():
strkw[str(k)] = v
if c:
strkw["_SOAPContext"] = c
fr = apply(f, (), strkw)
elif c:
fr = apply(f, args, {'_SOAPContext':c})
else:
fr = apply(f, args, {})
else:
if Config.specialArgs:
fr = apply(f, ordered_args, named_args)
else:
fr = apply(f, args, {})
if type(fr) == type(self) and \
isinstance(fr, voidType):
resp = buildSOAP(kw = {'%sResponse' % method: fr},
encoding = self.server.encoding,
config = self.server.config)
else:
resp = buildSOAP(kw =
{'%sResponse' % method: {'Result': fr}},
encoding = self.server.encoding,
config = self.server.config)
# Clean up _contexts
if _contexts.has_key(thread_id):
del _contexts[thread_id]
except Exception, e:
import traceback
info = sys.exc_info()
try:
if self.server.config.dumpFaultInfo:
s = 'Method %s exception' % nsmethod
debugHeader(s)
traceback.print_exception(info[0], info[1],
info[2])
debugFooter(s)
if isinstance(e, faultType):
f = e
else:
f = faultType("%s:Server" % NS.ENV_T,
"Method Failed",
"%s" % nsmethod)
if self.server.config.returnFaultInfo:
f._setDetail("".join(traceback.format_exception(
info[0], info[1], info[2])))
elif not hasattr(f, 'detail'):
f._setDetail("%s %s" % (info[0], info[1]))
finally:
del info
resp = buildSOAP(f, encoding = self.server.encoding,
config = self.server.config)
status = 500
else:
status = 200
except faultType, e:
import traceback
info = sys.exc_info()
try:
if self.server.config.dumpFaultInfo:
s = 'Received fault exception'
debugHeader(s)
traceback.print_exception(info[0], info[1],
info[2])
debugFooter(s)
if self.server.config.returnFaultInfo:
e._setDetail("".join(traceback.format_exception(
info[0], info[1], info[2])))
elif not hasattr(e, 'detail'):
e._setDetail("%s %s" % (info[0], info[1]))
finally:
del info
resp = buildSOAP(e, encoding = self.server.encoding,
config = self.server.config)
status = 500
except Exception, e:
# internal error, report as HTTP server error
if self.server.config.dumpFaultInfo:
s = 'Internal exception %s' % e
import traceback
debugHeader(s)
info = sys.exc_info()
try:
traceback.print_exception(info[0], info[1], info[2])
finally:
del info
debugFooter(s)
self.send_response(500)
self.end_headers()
if self.server.config.dumpHeadersOut and \
self.request_version != 'HTTP/0.9':
s = 'Outgoing HTTP headers'
debugHeader(s)
if self.responses.has_key(status):
s = ' ' + self.responses[status][0]
else:
s = ''
print "%s %d%s" % (self.protocol_version, 500, s)
print "Server:", self.version_string()
print "Date:", self.__last_date_time_string
debugFooter(s)
else:
# got a valid SOAP response
self.send_response(status)
t = 'text/xml';
if self.server.encoding != None:
t += '; charset="%s"' % self.server.encoding
self.send_header("Content-type", t)
self.send_header("Content-length", str(len(resp)))
self.end_headers()
if self.server.config.dumpHeadersOut and \
self.request_version != 'HTTP/0.9':
s = 'Outgoing HTTP headers'
debugHeader(s)
if self.responses.has_key(status):
s = ' ' + self.responses[status][0]
else:
s = ''
print "%s %d%s" % (self.protocol_version, status, s)
print "Server:", self.version_string()
print "Date:", self.__last_date_time_string
print "Content-type:", t
print "Content-length:", len(resp)
debugFooter(s)
if self.server.config.dumpSOAPOut:
s = 'Outgoing SOAP'
debugHeader(s)
print resp,
if resp[-1] != '\n':
print
debugFooter(s)
self.wfile.write(resp)
self.wfile.flush()
# We should be able to shut down both a regular and an SSL
# connection, but under Python 2.1, calling shutdown on an
# SSL connections drops the output, so this work-around.
# This should be investigated more someday.
if self.server.config.SSLserver and \
isinstance(self.connection, SSL.Connection):
self.connection.set_shutdown(SSL.SSL_SENT_SHUTDOWN |
SSL.SSL_RECEIVED_SHUTDOWN)
else:
self.connection.shutdown(1)
def do_GET(self):
#print 'command ', self.command
#print 'path ', self.path
#print 'request_version', self.request_version
#print 'headers'
#print ' type ', self.headers.type
#print ' maintype', self.headers.maintype
#print ' subtype ', self.headers.subtype
#print ' params ', self.headers.plist
path = self.path.lower()
if path.endswith('wsdl'):
method = 'wsdl'
function = namespace = None
if self.server.funcmap.has_key(namespace) \
and self.server.funcmap[namespace].has_key(method):
function = self.server.funcmap[namespace][method]
else:
if namespace in self.server.objmap.keys():
function = self.server.objmap[namespace]
l = method.split(".")
for i in l:
function = getattr(function, i)
if function:
self.send_response(200)
self.send_header("Content-type", 'text/plain')
self.end_headers()
response = apply(function, ())
self.wfile.write(str(response))
return
# return error
self.send_response(200)
self.send_header("Content-type", 'text/html')
self.end_headers()
self.wfile.write('''\
<title>
<head>Error!</head>
</title>
<body>
<h1>Oops!</h1>
<p>
This server supports HTTP GET requests only for the the purpose of
obtaining Web Services Description Language (WSDL) for a specific
service.
Either you requested an URL that does not end in "wsdl" or this
server does not implement a wsdl method.
</p>
</body>''')
def log_message(self, format, *args):
if self.server.log:
BaseHTTPServer.BaseHTTPRequestHandler.\
log_message (self, format, *args)
class SOAPServer(SOAPServerBase, SocketServer.TCPServer):
def __init__(self, addr = ('localhost', 8000),
RequestHandler = SOAPRequestHandler, log = 0, encoding = 'UTF-8',
config = Config, namespace = None, ssl_context = None):
# Test the encoding, raising an exception if it's not known
if encoding != None:
''.encode(encoding)
if ssl_context != None and not config.SSLserver:
raise AttributeError, \
"SSL server not supported by this Python installation"
self.namespace = namespace
self.objmap = {}
self.funcmap = {}
self.ssl_context = ssl_context
self.encoding = encoding
self.config = config
self.log = log
self.allow_reuse_address= 1
SocketServer.TCPServer.__init__(self, addr, RequestHandler)
class ThreadingSOAPServer(SOAPServerBase, SocketServer.ThreadingTCPServer):
def __init__(self, addr = ('localhost', 8000),
RequestHandler = SOAPRequestHandler, log = 0, encoding = 'UTF-8',
config = Config, namespace = None, ssl_context = None):
# Test the encoding, raising an exception if it's not known
if encoding != None:
''.encode(encoding)
if ssl_context != None and not config.SSLserver:
raise AttributeError, \
"SSL server not supported by this Python installation"
self.namespace = namespace
self.objmap = {}
self.funcmap = {}
self.ssl_context = ssl_context
self.encoding = encoding
self.config = config
self.log = log
self.allow_reuse_address= 1
SocketServer.ThreadingTCPServer.__init__(self, addr, RequestHandler)
# only define class if Unix domain sockets are available
if hasattr(socket, "AF_UNIX"):
class SOAPUnixSocketServer(SOAPServerBase, SocketServer.UnixStreamServer):
def __init__(self, addr = 8000,
RequestHandler = SOAPRequestHandler, log = 0, encoding = 'UTF-8',
config = Config, namespace = None, ssl_context = None):
# Test the encoding, raising an exception if it's not known
if encoding != None:
''.encode(encoding)
if ssl_context != None and not config.SSLserver:
raise AttributeError, \
"SSL server not supported by this Python installation"
self.namespace = namespace
self.objmap = {}
self.funcmap = {}
self.ssl_context = ssl_context
self.encoding = encoding
self.config = config
self.log = log
self.allow_reuse_address= 1
SocketServer.UnixStreamServer.__init__(self, str(addr), RequestHandler)

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
"""Provide a class for loading data from URL's that handles basic
authentication"""
ident = '$Id$'
from version import __version__
from Config import Config
from urllib import FancyURLopener
class URLopener(FancyURLopener):
username = None
passwd = None
def __init__(self, username=None, passwd=None, *args, **kw):
FancyURLopener.__init__( self, *args, **kw)
self.username = username
self.passwd = passwd
def prompt_user_passwd(self, host, realm):
return self.username, self.passwd

View File

@ -1,178 +0,0 @@
"""
################################################################################
# Copyright (c) 2003, Pfizer
# Copyright (c) 2001, Cayce Ullman.
# Copyright (c) 2001, Brian Matthews.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# Neither the name of actzero, inc. nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
################################################################################
"""
ident = '$Id$'
from version import __version__
import exceptions
import copy
import re
import string
import sys
from types import *
# SOAPpy modules
from Errors import *
################################################################################
# Utility infielders
################################################################################
def collapseWhiteSpace(s):
return re.sub('\s+', ' ', s).strip()
def decodeHexString(data):
conv = {
'0': 0x0, '1': 0x1, '2': 0x2, '3': 0x3, '4': 0x4,
'5': 0x5, '6': 0x6, '7': 0x7, '8': 0x8, '9': 0x9,
'a': 0xa, 'b': 0xb, 'c': 0xc, 'd': 0xd, 'e': 0xe,
'f': 0xf,
'A': 0xa, 'B': 0xb, 'C': 0xc, 'D': 0xd, 'E': 0xe,
'F': 0xf,
}
ws = string.whitespace
bin = ''
i = 0
while i < len(data):
if data[i] not in ws:
break
i += 1
low = 0
while i < len(data):
c = data[i]
if c in string.whitespace:
break
try:
c = conv[c]
except KeyError:
raise ValueError, \
"invalid hex string character `%s'" % c
if low:
bin += chr(high * 16 + c)
low = 0
else:
high = c
low = 1
i += 1
if low:
raise ValueError, "invalid hex string length"
while i < len(data):
if data[i] not in string.whitespace:
raise ValueError, \
"invalid hex string character `%s'" % c
i += 1
return bin
def encodeHexString(data):
h = ''
for i in data:
h += "%02X" % ord(i)
return h
def leapMonth(year, month):
return month == 2 and \
year % 4 == 0 and \
(year % 100 != 0 or year % 400 == 0)
def cleanDate(d, first = 0):
ranges = (None, (1, 12), (1, 31), (0, 23), (0, 59), (0, 61))
months = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
names = ('year', 'month', 'day', 'hours', 'minutes', 'seconds')
if len(d) != 6:
raise ValueError, "date must have 6 elements"
for i in range(first, 6):
s = d[i]
if type(s) == FloatType:
if i < 5:
try:
s = int(s)
except OverflowError:
if i > 0:
raise
s = long(s)
if s != d[i]:
raise ValueError, "%s must be integral" % names[i]
d[i] = s
elif type(s) == LongType:
try: s = int(s)
except: pass
elif type(s) != IntType:
raise TypeError, "%s isn't a valid type" % names[i]
if i == first and s < 0:
continue
if ranges[i] != None and \
(s < ranges[i][0] or ranges[i][1] < s):
raise ValueError, "%s out of range" % names[i]
if first < 6 and d[5] >= 61:
raise ValueError, "seconds out of range"
if first < 2:
leap = first < 1 and leapMonth(d[0], d[1])
if d[2] > months[d[1]] + leap:
raise ValueError, "day out of range"
def debugHeader(title):
s = '*** ' + title + ' '
print s + ('*' * (72 - len(s)))
def debugFooter(title):
print '*' * 72
sys.stdout.flush()

View File

@ -1,102 +0,0 @@
"""Parse web services description language to get SOAP methods.
Rudimentary support."""
ident = '$Id$'
from version import __version__
import wstools
from Client import SOAPProxy, SOAPAddress
from Config import Config
import urllib
class Proxy:
"""WSDL Proxy.
SOAPProxy wrapper that parses method names, namespaces, soap actions from
the web service description language (WSDL) file passed into the
constructor. The WSDL reference can be passed in as a stream, an url, a
file name, or a string.
Loads info into self.methods, a dictionary with methodname keys and values
of WSDLTools.SOAPCallinfo.
For example,
url = 'http://www.xmethods.org/sd/2001/TemperatureService.wsdl'
wsdl = WSDL.Proxy(url)
print len(wsdl.methods) # 1
print wsdl.methods.keys() # getTemp
See WSDLTools.SOAPCallinfo for more info on each method's attributes.
"""
def __init__(self, wsdlsource, config=Config, **kw ):
reader = wstools.WSDLTools.WSDLReader()
self.wsdl = None
# From Mark Pilgrim's "Dive Into Python" toolkit.py--open anything.
if self.wsdl is None and hasattr(wsdlsource, "read"):
#print 'stream'
self.wsdl = reader.loadFromStream(wsdlsource)
# NOT TESTED (as of April 17, 2003)
#if self.wsdl is None and wsdlsource == '-':
# import sys
# self.wsdl = reader.loadFromStream(sys.stdin)
# print 'stdin'
if self.wsdl is None:
try:
file(wsdlsource)
self.wsdl = reader.loadFromFile(wsdlsource)
#print 'file'
except (IOError, OSError):
pass
if self.wsdl is None:
try:
stream = urllib.urlopen(wsdlsource)
self.wsdl = reader.loadFromStream(stream, wsdlsource)
except (IOError, OSError): pass
if self.wsdl is None:
import StringIO
self.wsdl = reader.loadFromString(str(wsdlsource))
#print 'string'
# Package wsdl info as a dictionary of remote methods, with method name
# as key (based on ServiceProxy.__init__ in ZSI library).
self.methods = {}
service = self.wsdl.services[0]
port = service.ports[0]
name = service.name
binding = port.getBinding()
portType = binding.getPortType()
for operation in portType.operations:
callinfo = wstools.WSDLTools.callInfoFromWSDL(port, operation.name)
self.methods[callinfo.methodName] = callinfo
self.soapproxy = SOAPProxy('http://localhost/dummy.webservice',
config=config, **kw)
def __str__(self):
s = ''
for method in self.methods.values():
s += str(method)
return s
def __getattr__(self, name):
"""Set up environment then let parent class handle call.
Raises AttributeError is method name is not found."""
if not self.methods.has_key(name): raise AttributeError, name
callinfo = self.methods[name]
self.soapproxy.proxy = SOAPAddress(callinfo.location)
self.soapproxy.namespace = callinfo.namespace
self.soapproxy.soapaction = callinfo.soapAction
return self.soapproxy.__getattr__(name)

View File

@ -1,15 +0,0 @@
ident = '$Id$'
from version import __version__
from Client import *
from Config import *
from Errors import *
from NS import *
from Parser import *
from SOAPBuilder import *
from Server import *
from Types import *
from Utilities import *
import wstools
import WSDL

View File

@ -1,2 +0,0 @@
__version__="0.11.6"

View File

@ -1,91 +0,0 @@
"""Namespace module, so you don't need PyXML
"""
try:
from xml.ns import SOAP, SCHEMA, WSDL, XMLNS, DSIG, ENCRYPTION
except:
class SOAP:
ENV = "http://schemas.xmlsoap.org/soap/envelope/"
ENC = "http://schemas.xmlsoap.org/soap/encoding/"
ACTOR_NEXT = "http://schemas.xmlsoap.org/soap/actor/next"
class SCHEMA:
XSD1 = "http://www.w3.org/1999/XMLSchema"
XSD2 = "http://www.w3.org/2000/10/XMLSchema"
XSD3 = "http://www.w3.org/2001/XMLSchema"
XSD_LIST = [ XSD1, XSD2, XSD3 ]
XSI1 = "http://www.w3.org/1999/XMLSchema-instance"
XSI2 = "http://www.w3.org/2000/10/XMLSchema-instance"
XSI3 = "http://www.w3.org/2001/XMLSchema-instance"
XSI_LIST = [ XSI1, XSI2, XSI3 ]
BASE = XSD3
class WSDL:
BASE = "http://schemas.xmlsoap.org/wsdl/"
BIND_HTTP = "http://schemas.xmlsoap.org/wsdl/http/"
BIND_MIME = "http://schemas.xmlsoap.org/wsdl/mime/"
BIND_SOAP = "http://schemas.xmlsoap.org/wsdl/soap/"
class XMLNS:
BASE = "http://www.w3.org/2000/xmlns/"
XML = "http://www.w3.org/XML/1998/namespace"
HTML = "http://www.w3.org/TR/REC-html40"
class DSIG:
BASE = "http://www.w3.org/2000/09/xmldsig#"
C14N = "http://www.w3.org/TR/2000/CR-xml-c14n-20010315"
C14N_COMM = "http://www.w3.org/TR/2000/CR-xml-c14n-20010315#WithComments"
C14N_EXCL = "http://www.w3.org/2001/10/xml-exc-c14n#"
DIGEST_MD2 = "http://www.w3.org/2000/09/xmldsig#md2"
DIGEST_MD5 = "http://www.w3.org/2000/09/xmldsig#md5"
DIGEST_SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
ENC_BASE64 = "http://www.w3.org/2000/09/xmldsig#base64"
ENVELOPED = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
HMAC_SHA1 = "http://www.w3.org/2000/09/xmldsig#hmac-sha1"
SIG_DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
SIG_RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
XPATH = "http://www.w3.org/TR/1999/REC-xpath-19991116"
XSLT = "http://www.w3.org/TR/1999/REC-xslt-19991116"
class ENCRYPTION:
BASE = "http://www.w3.org/2001/04/xmlenc#"
BLOCK_3DES = "http://www.w3.org/2001/04/xmlenc#des-cbc"
BLOCK_AES128 = "http://www.w3.org/2001/04/xmlenc#aes128-cbc"
BLOCK_AES192 = "http://www.w3.org/2001/04/xmlenc#aes192-cbc"
BLOCK_AES256 = "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
DIGEST_RIPEMD160 = "http://www.w3.org/2001/04/xmlenc#ripemd160"
DIGEST_SHA256 = "http://www.w3.org/2001/04/xmlenc#sha256"
DIGEST_SHA512 = "http://www.w3.org/2001/04/xmlenc#sha512"
KA_DH = "http://www.w3.org/2001/04/xmlenc#dh"
KT_RSA_1_5 = "http://www.w3.org/2001/04/xmlenc#rsa-1_5"
KT_RSA_OAEP = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"
STREAM_ARCFOUR = "http://www.w3.org/2001/04/xmlenc#arcfour"
WRAP_3DES = "http://www.w3.org/2001/04/xmlenc#kw-3des"
WRAP_AES128 = "http://www.w3.org/2001/04/xmlenc#kw-aes128"
WRAP_AES192 = "http://www.w3.org/2001/04/xmlenc#kw-aes192"
WRAP_AES256 = "http://www.w3.org/2001/04/xmlenc#kw-aes256"
class WSSE:
BASE = "http://schemas.xmlsoap.org/ws/2002/04/secext"
class WSU:
BASE = "http://schemas.xmlsoap.org/ws/2002/04/utility"
UTILITY = "http://schemas.xmlsoap.org/ws/2002/07/utility"
class WSR:
PROPERTIES = "http://www.ibm.com/xmlns/stdwip/web-services/WS-ResourceProperties"
LIFETIME = "http://www.ibm.com/xmlns/stdwip/web-services/WS-ResourceLifetime"
class WSA:
ADDRESS = "http://schemas.xmlsoap.org/ws/2003/03/addressing"
ADDRESS2004 = "http://schemas.xmlsoap.org/ws/2004/03/addressing"
ANONYMOUS = "%s/role/anonymous" %ADDRESS
ANONYMOUS2004 = "%s/role/anonymous" %ADDRESS2004
FAULT = "http://schemas.xmlsoap.org/ws/2004/03/addressing/fault"
class WSP:
POLICY = "http://schemas.xmlsoap.org/ws/2002/12/policy"

View File

@ -1,179 +0,0 @@
"""Based on code from timeout_socket.py, with some tweaks for compatibility.
These tweaks should really be rolled back into timeout_socket, but it's
not totally clear who is maintaining it at this point. In the meantime,
we'll use a different module name for our tweaked version to avoid any
confusion.
The original timeout_socket is by:
Scott Cotton <scott@chronis.pobox.com>
Lloyd Zusman <ljz@asfast.com>
Phil Mayes <pmayes@olivebr.com>
Piers Lauder <piers@cs.su.oz.au>
Radovan Garabik <garabik@melkor.dnp.fmph.uniba.sk>
"""
ident = "$Id$"
import string, socket, select, errno
WSAEINVAL = getattr(errno, 'WSAEINVAL', 10022)
class TimeoutSocket:
"""A socket imposter that supports timeout limits."""
def __init__(self, timeout=20, sock=None):
self.timeout = float(timeout)
self.inbuf = ''
if sock is None:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock = sock
self.sock.setblocking(0)
self._rbuf = ''
self._wbuf = ''
def __getattr__(self, name):
# Delegate to real socket attributes.
return getattr(self.sock, name)
def connect(self, *addr):
timeout = self.timeout
sock = self.sock
try:
# Non-blocking mode
sock.setblocking(0)
apply(sock.connect, addr)
sock.setblocking(timeout != 0)
return 1
except socket.error,why:
if not timeout:
raise
sock.setblocking(1)
if len(why.args) == 1:
code = 0
else:
code, why = why
if code not in (
errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK
):
raise
r,w,e = select.select([],[sock],[],timeout)
if w:
try:
apply(sock.connect, addr)
return 1
except socket.error,why:
if len(why.args) == 1:
code = 0
else:
code, why = why
if code in (errno.EISCONN, WSAEINVAL):
return 1
raise
raise TimeoutError('socket connect() timeout.')
def send(self, data, flags=0):
total = len(data)
next = 0
while 1:
r, w, e = select.select([],[self.sock], [], self.timeout)
if w:
buff = data[next:next + 8192]
sent = self.sock.send(buff, flags)
next = next + sent
if next == total:
return total
continue
raise TimeoutError('socket send() timeout.')
def recv(self, amt, flags=0):
if select.select([self.sock], [], [], self.timeout)[0]:
return self.sock.recv(amt, flags)
raise TimeoutError('socket recv() timeout.')
buffsize = 4096
handles = 1
def makefile(self, mode="r", buffsize=-1):
self.handles = self.handles + 1
self.mode = mode
return self
def close(self):
self.handles = self.handles - 1
if self.handles == 0 and self.sock.fileno() >= 0:
self.sock.close()
def read(self, n=-1):
if not isinstance(n, type(1)):
n = -1
if n >= 0:
k = len(self._rbuf)
if n <= k:
data = self._rbuf[:n]
self._rbuf = self._rbuf[n:]
return data
n = n - k
L = [self._rbuf]
self._rbuf = ""
while n > 0:
new = self.recv(max(n, self.buffsize))
if not new: break
k = len(new)
if k > n:
L.append(new[:n])
self._rbuf = new[n:]
break
L.append(new)
n = n - k
return "".join(L)
k = max(4096, self.buffsize)
L = [self._rbuf]
self._rbuf = ""
while 1:
new = self.recv(k)
if not new: break
L.append(new)
k = min(k*2, 1024**2)
return "".join(L)
def readline(self, limit=-1):
data = ""
i = self._rbuf.find('\n')
while i < 0 and not (0 < limit <= len(self._rbuf)):
new = self.recv(self.buffsize)
if not new: break
i = new.find('\n')
if i >= 0: i = i + len(self._rbuf)
self._rbuf = self._rbuf + new
if i < 0: i = len(self._rbuf)
else: i = i+1
if 0 <= limit < len(self._rbuf): i = limit
data, self._rbuf = self._rbuf[:i], self._rbuf[i:]
return data
def readlines(self, sizehint = 0):
total = 0
list = []
while 1:
line = self.readline()
if not line: break
list.append(line)
total += len(line)
if sizehint and total >= sizehint:
break
return list
def writelines(self, list):
self.send(''.join(list))
def write(self, data):
self.send(data)
def flush(self):
pass
class TimeoutError(Exception):
pass

View File

@ -1,99 +0,0 @@
"""
A more or less complete user-defined wrapper around tuple objects.
Adapted version of the standard library's UserList.
Taken from Stefan Schwarzer's ftputil library, available at
<http://www.ndh.net/home/sschwarzer/python/python_software.html>, and used under this license:
Copyright (C) 1999, Stefan Schwarzer
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of the above author nor the names of the
contributors to the software may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
# $Id$
#XXX tuple instances (in Python 2.2) contain also:
# __class__, __delattr__, __getattribute__, __hash__, __new__,
# __reduce__, __setattr__, __str__
# What about these?
class UserTuple:
def __init__(self, inittuple=None):
self.data = ()
if inittuple is not None:
# XXX should this accept an arbitrary sequence?
if type(inittuple) == type(self.data):
self.data = inittuple
elif isinstance(inittuple, UserTuple):
# this results in
# self.data is inittuple.data
# but that's ok for tuples because they are
# immutable. (Builtin tuples behave the same.)
self.data = inittuple.data[:]
else:
# the same applies here; (t is tuple(t)) == 1
self.data = tuple(inittuple)
def __repr__(self): return repr(self.data)
def __lt__(self, other): return self.data < self.__cast(other)
def __le__(self, other): return self.data <= self.__cast(other)
def __eq__(self, other): return self.data == self.__cast(other)
def __ne__(self, other): return self.data != self.__cast(other)
def __gt__(self, other): return self.data > self.__cast(other)
def __ge__(self, other): return self.data >= self.__cast(other)
def __cast(self, other):
if isinstance(other, UserTuple): return other.data
else: return other
def __cmp__(self, other):
return cmp(self.data, self.__cast(other))
def __contains__(self, item): return item in self.data
def __len__(self): return len(self.data)
def __getitem__(self, i): return self.data[i]
def __getslice__(self, i, j):
i = max(i, 0); j = max(j, 0)
return self.__class__(self.data[i:j])
def __add__(self, other):
if isinstance(other, UserTuple):
return self.__class__(self.data + other.data)
elif isinstance(other, type(self.data)):
return self.__class__(self.data + other)
else:
return self.__class__(self.data + tuple(other))
# dir( () ) contains no __radd__ (at least in Python 2.2)
def __mul__(self, n):
return self.__class__(self.data*n)
__rmul__ = __mul__

View File

@ -1,839 +0,0 @@
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
ident = "$Id$"
import types
import string, httplib, smtplib, urllib, socket, weakref
import xml.dom.minidom
from string import join, strip, split
from UserDict import UserDict
from StringIO import StringIO
from TimeoutSocket import TimeoutSocket, TimeoutError
from urlparse import urlparse
from httplib import HTTPConnection, HTTPSConnection
from exceptions import Exception
try:
from xml.dom.ext import SplitQName
except:
def SplitQName(qname):
'''SplitQName(qname) -> (string, string)
Split Qualified Name into a tuple of len 2, consisting
of the prefix and the local name.
(prefix, localName)
Special Cases:
xmlns -- (localName, 'xmlns')
None -- (None, localName)
'''
l = qname.split(':')
if len(l) == 1:
l.insert(0, None)
elif len(l) == 2:
if l[0] == 'xmlns':
l.reverse()
else:
return
return tuple(l)
class RecursionError(Exception):
"""Used to indicate a HTTP redirect recursion."""
pass
class HTTPResponse:
"""Captures the information in an HTTP response message."""
def __init__(self, response):
self.status = response.status
self.reason = response.reason
self.headers = response.msg
self.body = response.read() or None
response.close()
class TimeoutHTTP(HTTPConnection):
"""A custom http connection object that supports socket timeout."""
def __init__(self, host, port=None, timeout=20):
HTTPConnection.__init__(self, host, port)
self.timeout = timeout
def connect(self):
self.sock = TimeoutSocket(self.timeout)
self.sock.connect((self.host, self.port))
class TimeoutHTTPS(HTTPSConnection):
"""A custom https object that supports socket timeout. Note that this
is not really complete. The builtin SSL support in the Python socket
module requires a real socket (type) to be passed in to be hooked to
SSL. That means our fake socket won't work and our timeout hacks are
bypassed for send and recv calls. Since our hack _is_ in place at
connect() time, it should at least provide some timeout protection."""
def __init__(self, host, port=None, timeout=20, **kwargs):
HTTPSConnection.__init__(self, str(host), port, **kwargs)
self.timeout = timeout
def connect(self):
sock = TimeoutSocket(self.timeout)
sock.connect((self.host, self.port))
realsock = getattr(sock.sock, '_sock', sock.sock)
ssl = socket.ssl(realsock, self.key_file, self.cert_file)
self.sock = httplib.FakeSocket(sock, ssl)
def urlopen(url, timeout=20, redirects=None):
"""A minimal urlopen replacement hack that supports timeouts for http.
Note that this supports GET only."""
scheme, host, path, params, query, frag = urlparse(url)
if not scheme in ('http', 'https'):
return urllib.urlopen(url)
if params: path = '%s;%s' % (path, params)
if query: path = '%s?%s' % (path, query)
if frag: path = '%s#%s' % (path, frag)
if scheme == 'https':
# If ssl is not compiled into Python, you will not get an exception
# until a conn.endheaders() call. We need to know sooner, so use
# getattr.
if hasattr(socket, 'ssl'):
conn = TimeoutHTTPS(host, None, timeout)
else:
import M2Crypto
ctx = M2Crypto.SSL.Context()
ctx.set_session_timeout(timeout)
conn = M2Crypto.httpslib.HTTPSConnection(host, ssl_context=ctx)
#conn.set_debuglevel(1)
else:
conn = TimeoutHTTP(host, None, timeout)
conn.putrequest('GET', path)
conn.putheader('Connection', 'close')
conn.endheaders()
response = None
while 1:
response = conn.getresponse()
if response.status != 100:
break
conn._HTTPConnection__state = httplib._CS_REQ_SENT
conn._HTTPConnection__response = None
status = response.status
# If we get an HTTP redirect, we will follow it automatically.
if status >= 300 and status < 400:
location = response.msg.getheader('location')
if location is not None:
response.close()
if redirects is not None and redirects.has_key(location):
raise RecursionError(
'Circular HTTP redirection detected.'
)
if redirects is None:
redirects = {}
redirects[location] = 1
return urlopen(location, timeout, redirects)
raise HTTPResponse(response)
if not (status >= 200 and status < 300):
raise HTTPResponse(response)
body = StringIO(response.read())
response.close()
return body
class DOM:
"""The DOM singleton defines a number of XML related constants and
provides a number of utility methods for DOM related tasks. It
also provides some basic abstractions so that the rest of the
package need not care about actual DOM implementation in use."""
# Namespace stuff related to the SOAP specification.
NS_SOAP_ENV_1_1 = 'http://schemas.xmlsoap.org/soap/envelope/'
NS_SOAP_ENC_1_1 = 'http://schemas.xmlsoap.org/soap/encoding/'
NS_SOAP_ENV_1_2 = 'http://www.w3.org/2001/06/soap-envelope'
NS_SOAP_ENC_1_2 = 'http://www.w3.org/2001/06/soap-encoding'
NS_SOAP_ENV_ALL = (NS_SOAP_ENV_1_1, NS_SOAP_ENV_1_2)
NS_SOAP_ENC_ALL = (NS_SOAP_ENC_1_1, NS_SOAP_ENC_1_2)
NS_SOAP_ENV = NS_SOAP_ENV_1_1
NS_SOAP_ENC = NS_SOAP_ENC_1_1
_soap_uri_mapping = {
NS_SOAP_ENV_1_1 : '1.1',
NS_SOAP_ENV_1_2 : '1.2',
}
SOAP_ACTOR_NEXT_1_1 = 'http://schemas.xmlsoap.org/soap/actor/next'
SOAP_ACTOR_NEXT_1_2 = 'http://www.w3.org/2001/06/soap-envelope/actor/next'
SOAP_ACTOR_NEXT_ALL = (SOAP_ACTOR_NEXT_1_1, SOAP_ACTOR_NEXT_1_2)
def SOAPUriToVersion(self, uri):
"""Return the SOAP version related to an envelope uri."""
value = self._soap_uri_mapping.get(uri)
if value is not None:
return value
raise ValueError(
'Unsupported SOAP envelope uri: %s' % uri
)
def GetSOAPEnvUri(self, version):
"""Return the appropriate SOAP envelope uri for a given
human-friendly SOAP version string (e.g. '1.1')."""
attrname = 'NS_SOAP_ENV_%s' % join(split(version, '.'), '_')
value = getattr(self, attrname, None)
if value is not None:
return value
raise ValueError(
'Unsupported SOAP version: %s' % version
)
def GetSOAPEncUri(self, version):
"""Return the appropriate SOAP encoding uri for a given
human-friendly SOAP version string (e.g. '1.1')."""
attrname = 'NS_SOAP_ENC_%s' % join(split(version, '.'), '_')
value = getattr(self, attrname, None)
if value is not None:
return value
raise ValueError(
'Unsupported SOAP version: %s' % version
)
def GetSOAPActorNextUri(self, version):
"""Return the right special next-actor uri for a given
human-friendly SOAP version string (e.g. '1.1')."""
attrname = 'SOAP_ACTOR_NEXT_%s' % join(split(version, '.'), '_')
value = getattr(self, attrname, None)
if value is not None:
return value
raise ValueError(
'Unsupported SOAP version: %s' % version
)
# Namespace stuff related to XML Schema.
NS_XSD_99 = 'http://www.w3.org/1999/XMLSchema'
NS_XSI_99 = 'http://www.w3.org/1999/XMLSchema-instance'
NS_XSD_00 = 'http://www.w3.org/2000/10/XMLSchema'
NS_XSI_00 = 'http://www.w3.org/2000/10/XMLSchema-instance'
NS_XSD_01 = 'http://www.w3.org/2001/XMLSchema'
NS_XSI_01 = 'http://www.w3.org/2001/XMLSchema-instance'
NS_XSD_ALL = (NS_XSD_99, NS_XSD_00, NS_XSD_01)
NS_XSI_ALL = (NS_XSI_99, NS_XSI_00, NS_XSI_01)
NS_XSD = NS_XSD_01
NS_XSI = NS_XSI_01
_xsd_uri_mapping = {
NS_XSD_99 : NS_XSI_99,
NS_XSD_00 : NS_XSI_00,
NS_XSD_01 : NS_XSI_01,
}
for key, value in _xsd_uri_mapping.items():
_xsd_uri_mapping[value] = key
def InstanceUriForSchemaUri(self, uri):
"""Return the appropriate matching XML Schema instance uri for
the given XML Schema namespace uri."""
return self._xsd_uri_mapping.get(uri)
def SchemaUriForInstanceUri(self, uri):
"""Return the appropriate matching XML Schema namespace uri for
the given XML Schema instance namespace uri."""
return self._xsd_uri_mapping.get(uri)
# Namespace stuff related to WSDL.
NS_WSDL_1_1 = 'http://schemas.xmlsoap.org/wsdl/'
NS_WSDL_ALL = (NS_WSDL_1_1,)
NS_WSDL = NS_WSDL_1_1
NS_SOAP_BINDING_1_1 = 'http://schemas.xmlsoap.org/wsdl/soap/'
NS_HTTP_BINDING_1_1 = 'http://schemas.xmlsoap.org/wsdl/http/'
NS_MIME_BINDING_1_1 = 'http://schemas.xmlsoap.org/wsdl/mime/'
NS_SOAP_BINDING_ALL = (NS_SOAP_BINDING_1_1,)
NS_HTTP_BINDING_ALL = (NS_HTTP_BINDING_1_1,)
NS_MIME_BINDING_ALL = (NS_MIME_BINDING_1_1,)
NS_SOAP_BINDING = NS_SOAP_BINDING_1_1
NS_HTTP_BINDING = NS_HTTP_BINDING_1_1
NS_MIME_BINDING = NS_MIME_BINDING_1_1
NS_SOAP_HTTP_1_1 = 'http://schemas.xmlsoap.org/soap/http'
NS_SOAP_HTTP_ALL = (NS_SOAP_HTTP_1_1,)
NS_SOAP_HTTP = NS_SOAP_HTTP_1_1
_wsdl_uri_mapping = {
NS_WSDL_1_1 : '1.1',
}
def WSDLUriToVersion(self, uri):
"""Return the WSDL version related to a WSDL namespace uri."""
value = self._wsdl_uri_mapping.get(uri)
if value is not None:
return value
raise ValueError(
'Unsupported SOAP envelope uri: %s' % uri
)
def GetWSDLUri(self, version):
attr = 'NS_WSDL_%s' % join(split(version, '.'), '_')
value = getattr(self, attr, None)
if value is not None:
return value
raise ValueError(
'Unsupported WSDL version: %s' % version
)
def GetWSDLSoapBindingUri(self, version):
attr = 'NS_SOAP_BINDING_%s' % join(split(version, '.'), '_')
value = getattr(self, attr, None)
if value is not None:
return value
raise ValueError(
'Unsupported WSDL version: %s' % version
)
def GetWSDLHttpBindingUri(self, version):
attr = 'NS_HTTP_BINDING_%s' % join(split(version, '.'), '_')
value = getattr(self, attr, None)
if value is not None:
return value
raise ValueError(
'Unsupported WSDL version: %s' % version
)
def GetWSDLMimeBindingUri(self, version):
attr = 'NS_MIME_BINDING_%s' % join(split(version, '.'), '_')
value = getattr(self, attr, None)
if value is not None:
return value
raise ValueError(
'Unsupported WSDL version: %s' % version
)
def GetWSDLHttpTransportUri(self, version):
attr = 'NS_SOAP_HTTP_%s' % join(split(version, '.'), '_')
value = getattr(self, attr, None)
if value is not None:
return value
raise ValueError(
'Unsupported WSDL version: %s' % version
)
# Other xml namespace constants.
NS_XMLNS = 'http://www.w3.org/2000/xmlns/'
def isElement(self, node, name, nsuri=None):
"""Return true if the given node is an element with the given
name and optional namespace uri."""
if node.nodeType != node.ELEMENT_NODE:
return 0
return node.localName == name and \
(nsuri is None or self.nsUriMatch(node.namespaceURI, nsuri))
def getElement(self, node, name, nsuri=None, default=join):
"""Return the first child of node with a matching name and
namespace uri, or the default if one is provided."""
nsmatch = self.nsUriMatch
ELEMENT_NODE = node.ELEMENT_NODE
for child in node.childNodes:
if child.nodeType == ELEMENT_NODE:
if ((child.localName == name or name is None) and
(nsuri is None or nsmatch(child.namespaceURI, nsuri))
):
return child
if default is not join:
return default
raise KeyError, name
def getElementById(self, node, id, default=join):
"""Return the first child of node matching an id reference."""
attrget = self.getAttr
ELEMENT_NODE = node.ELEMENT_NODE
for child in node.childNodes:
if child.nodeType == ELEMENT_NODE:
if attrget(child, 'id') == id:
return child
if default is not join:
return default
raise KeyError, name
def getMappingById(self, document, depth=None, element=None,
mapping=None, level=1):
"""Create an id -> element mapping of those elements within a
document that define an id attribute. The depth of the search
may be controlled by using the (1-based) depth argument."""
if document is not None:
element = document.documentElement
mapping = {}
attr = element._attrs.get('id', None)
if attr is not None:
mapping[attr.value] = element
if depth is None or depth > level:
level = level + 1
ELEMENT_NODE = element.ELEMENT_NODE
for child in element.childNodes:
if child.nodeType == ELEMENT_NODE:
self.getMappingById(None, depth, child, mapping, level)
return mapping
def getElements(self, node, name, nsuri=None):
"""Return a sequence of the child elements of the given node that
match the given name and optional namespace uri."""
nsmatch = self.nsUriMatch
result = []
ELEMENT_NODE = node.ELEMENT_NODE
for child in node.childNodes:
if child.nodeType == ELEMENT_NODE:
if ((child.localName == name or name is None) and (
(nsuri is None) or nsmatch(child.namespaceURI, nsuri))):
result.append(child)
return result
def hasAttr(self, node, name, nsuri=None):
"""Return true if element has attribute with the given name and
optional nsuri. If nsuri is not specified, returns true if an
attribute exists with the given name with any namespace."""
if nsuri is None:
if node.hasAttribute(name):
return True
return False
return node.hasAttributeNS(nsuri, name)
def getAttr(self, node, name, nsuri=None, default=join):
"""Return the value of the attribute named 'name' with the
optional nsuri, or the default if one is specified. If
nsuri is not specified, an attribute that matches the
given name will be returned regardless of namespace."""
if nsuri is None:
result = node._attrs.get(name, None)
if result is None:
for item in node._attrsNS.keys():
if item[1] == name:
result = node._attrsNS[item]
break
else:
result = node._attrsNS.get((nsuri, name), None)
if result is not None:
return result.value
if default is not join:
return default
return ''
def getAttrs(self, node):
"""Return a Collection of all attributes
"""
attrs = {}
for k,v in node._attrs.items():
attrs[k] = v.value
return attrs
def getElementText(self, node, preserve_ws=None):
"""Return the text value of an xml element node. Leading and trailing
whitespace is stripped from the value unless the preserve_ws flag
is passed with a true value."""
result = []
for child in node.childNodes:
nodetype = child.nodeType
if nodetype == child.TEXT_NODE or \
nodetype == child.CDATA_SECTION_NODE:
result.append(child.nodeValue)
value = join(result, '')
if preserve_ws is None:
value = strip(value)
return value
def findNamespaceURI(self, prefix, node):
"""Find a namespace uri given a prefix and a context node."""
attrkey = (self.NS_XMLNS, prefix)
DOCUMENT_NODE = node.DOCUMENT_NODE
ELEMENT_NODE = node.ELEMENT_NODE
while 1:
if node.nodeType != ELEMENT_NODE:
node = node.parentNode
continue
result = node._attrsNS.get(attrkey, None)
if result is not None:
return result.value
if hasattr(node, '__imported__'):
raise DOMException('Value for prefix %s not found.' % prefix)
node = node.parentNode
if node.nodeType == DOCUMENT_NODE:
raise DOMException('Value for prefix %s not found.' % prefix)
def findDefaultNS(self, node):
"""Return the current default namespace uri for the given node."""
attrkey = (self.NS_XMLNS, 'xmlns')
DOCUMENT_NODE = node.DOCUMENT_NODE
ELEMENT_NODE = node.ELEMENT_NODE
while 1:
if node.nodeType != ELEMENT_NODE:
node = node.parentNode
continue
result = node._attrsNS.get(attrkey, None)
if result is not None:
return result.value
if hasattr(node, '__imported__'):
raise DOMException('Cannot determine default namespace.')
node = node.parentNode
if node.nodeType == DOCUMENT_NODE:
raise DOMException('Cannot determine default namespace.')
def findTargetNS(self, node):
"""Return the defined target namespace uri for the given node."""
attrget = self.getAttr
attrkey = (self.NS_XMLNS, 'xmlns')
DOCUMENT_NODE = node.DOCUMENT_NODE
ELEMENT_NODE = node.ELEMENT_NODE
while 1:
if node.nodeType != ELEMENT_NODE:
node = node.parentNode
continue
result = attrget(node, 'targetNamespace', default=None)
if result is not None:
return result
node = node.parentNode
if node.nodeType == DOCUMENT_NODE:
raise DOMException('Cannot determine target namespace.')
def getTypeRef(self, element):
"""Return (namespaceURI, name) for a type attribue of the given
element, or None if the element does not have a type attribute."""
typeattr = self.getAttr(element, 'type', default=None)
if typeattr is None:
return None
parts = typeattr.split(':', 1)
if len(parts) == 2:
nsuri = self.findNamespaceURI(parts[0], element)
else:
nsuri = self.findDefaultNS(element)
return (nsuri, parts[1])
def importNode(self, document, node, deep=0):
"""Implements (well enough for our purposes) DOM node import."""
nodetype = node.nodeType
if nodetype in (node.DOCUMENT_NODE, node.DOCUMENT_TYPE_NODE):
raise DOMException('Illegal node type for importNode')
if nodetype == node.ENTITY_REFERENCE_NODE:
deep = 0
clone = node.cloneNode(deep)
self._setOwnerDoc(document, clone)
clone.__imported__ = 1
return clone
def _setOwnerDoc(self, document, node):
node.ownerDocument = document
for child in node.childNodes:
self._setOwnerDoc(document, child)
def nsUriMatch(self, value, wanted, strict=0, tt=type(())):
"""Return a true value if two namespace uri values match."""
if value == wanted or (type(wanted) is tt) and value in wanted:
return 1
if not strict:
wanted = type(wanted) is tt and wanted or (wanted,)
value = value[-1:] != '/' and value or value[:-1]
for item in wanted:
if item == value or item[:-1] == value:
return 1
return 0
def createDocument(self, nsuri, qname, doctype=None):
"""Create a new writable DOM document object."""
impl = xml.dom.minidom.getDOMImplementation()
return impl.createDocument(nsuri, qname, doctype)
def loadDocument(self, data):
"""Load an xml file from a file-like object and return a DOM
document instance."""
return xml.dom.minidom.parse(data)
def loadFromURL(self, url):
"""Load an xml file from a URL and return a DOM document."""
file = urlopen(url)
try: result = self.loadDocument(file)
finally: file.close()
return result
class DOMException(Exception):
pass
DOM = DOM()
class Collection(UserDict):
"""Helper class for maintaining ordered named collections."""
default = lambda self,k: k.name
def __init__(self, parent, key=None):
UserDict.__init__(self)
self.parent = weakref.ref(parent)
self.list = []
self._func = key or self.default
def __getitem__(self, key):
if type(key) is type(1):
return self.list[key]
return self.data[key]
def __setitem__(self, key, item):
item.parent = weakref.ref(self)
self.list.append(item)
self.data[key] = item
def keys(self):
return map(lambda i: self._func(i), self.list)
def items(self):
return map(lambda i: (self._func(i), i), self.list)
def values(self):
return self.list
class CollectionNS(UserDict):
"""Helper class for maintaining ordered named collections."""
default = lambda self,k: k.name
def __init__(self, parent, key=None):
UserDict.__init__(self)
self.parent = weakref.ref(parent)
self.targetNamespace = None
self.list = []
self._func = key or self.default
def __getitem__(self, key):
self.targetNamespace = self.parent().targetNamespace
if type(key) is types.IntType:
return self.list[key]
elif self.__isSequence(key):
nsuri,name = key
return self.data[nsuri][name]
return self.data[self.parent().targetNamespace][key]
def __setitem__(self, key, item):
item.parent = weakref.ref(self)
self.list.append(item)
targetNamespace = getattr(item, 'targetNamespace', self.parent().targetNamespace)
if not self.data.has_key(targetNamespace):
self.data[targetNamespace] = {}
self.data[targetNamespace][key] = item
def __isSequence(self, key):
return (type(key) in (types.TupleType,types.ListType) and len(key) == 2)
def keys(self):
keys = []
for tns in self.data.keys():
keys.append(map(lambda i: (tns,self._func(i)), self.data[tns].values()))
return keys
def items(self):
return map(lambda i: (self._func(i), i), self.list)
def values(self):
return self.list
# This is a runtime guerilla patch for pulldom (used by minidom) so
# that xml namespace declaration attributes are not lost in parsing.
# We need them to do correct QName linking for XML Schema and WSDL.
# The patch has been submitted to SF for the next Python version.
from xml.dom.pulldom import PullDOM, START_ELEMENT
if 1:
def startPrefixMapping(self, prefix, uri):
if not hasattr(self, '_xmlns_attrs'):
self._xmlns_attrs = []
self._xmlns_attrs.append((prefix or 'xmlns', uri))
self._ns_contexts.append(self._current_context.copy())
self._current_context[uri] = prefix or ''
PullDOM.startPrefixMapping = startPrefixMapping
def startElementNS(self, name, tagName , attrs):
# Retrieve xml namespace declaration attributes.
xmlns_uri = 'http://www.w3.org/2000/xmlns/'
xmlns_attrs = getattr(self, '_xmlns_attrs', None)
if xmlns_attrs is not None:
for aname, value in xmlns_attrs:
attrs._attrs[(xmlns_uri, aname)] = value
self._xmlns_attrs = []
uri, localname = name
if uri:
# When using namespaces, the reader may or may not
# provide us with the original name. If not, create
# *a* valid tagName from the current context.
if tagName is None:
prefix = self._current_context[uri]
if prefix:
tagName = prefix + ":" + localname
else:
tagName = localname
if self.document:
node = self.document.createElementNS(uri, tagName)
else:
node = self.buildDocument(uri, tagName)
else:
# When the tagname is not prefixed, it just appears as
# localname
if self.document:
node = self.document.createElement(localname)
else:
node = self.buildDocument(None, localname)
for aname,value in attrs.items():
a_uri, a_localname = aname
if a_uri == xmlns_uri:
if a_localname == 'xmlns':
qname = a_localname
else:
qname = 'xmlns:' + a_localname
attr = self.document.createAttributeNS(a_uri, qname)
node.setAttributeNodeNS(attr)
elif a_uri:
prefix = self._current_context[a_uri]
if prefix:
qname = prefix + ":" + a_localname
else:
qname = a_localname
attr = self.document.createAttributeNS(a_uri, qname)
node.setAttributeNodeNS(attr)
else:
attr = self.document.createAttribute(a_localname)
node.setAttributeNode(attr)
attr.value = value
self.lastEvent[1] = [(START_ELEMENT, node), None]
self.lastEvent = self.lastEvent[1]
self.push(node)
PullDOM.startElementNS = startElementNS
#
# This is a runtime guerilla patch for minidom so
# that xmlns prefixed attributes dont raise AttributeErrors
# during cloning.
#
# Namespace declarations can appear in any start-tag, must look for xmlns
# prefixed attribute names during cloning.
#
# key (attr.namespaceURI, tag)
# ('http://www.w3.org/2000/xmlns/', u'xsd') <xml.dom.minidom.Attr instance at 0x82227c4>
# ('http://www.w3.org/2000/xmlns/', 'xmlns') <xml.dom.minidom.Attr instance at 0x8414b3c>
#
# xml.dom.minidom.Attr.nodeName = xmlns:xsd
# xml.dom.minidom.Attr.value = = http://www.w3.org/2001/XMLSchema
if 1:
def _clone_node(node, deep, newOwnerDocument):
"""
Clone a node and give it the new owner document.
Called by Node.cloneNode and Document.importNode
"""
if node.ownerDocument.isSameNode(newOwnerDocument):
operation = xml.dom.UserDataHandler.NODE_CLONED
else:
operation = xml.dom.UserDataHandler.NODE_IMPORTED
if node.nodeType == xml.dom.minidom.Node.ELEMENT_NODE:
clone = newOwnerDocument.createElementNS(node.namespaceURI,
node.nodeName)
for attr in node.attributes.values():
clone.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.value)
prefix, tag = xml.dom.minidom._nssplit(attr.nodeName)
if prefix == 'xmlns':
a = clone.getAttributeNodeNS(attr.namespaceURI, tag)
elif prefix:
a = clone.getAttributeNodeNS(attr.namespaceURI, tag)
else:
a = clone.getAttributeNodeNS(attr.namespaceURI, attr.nodeName)
a.specified = attr.specified
if deep:
for child in node.childNodes:
c = xml.dom.minidom._clone_node(child, deep, newOwnerDocument)
clone.appendChild(c)
elif node.nodeType == xml.dom.minidom.Node.DOCUMENT_FRAGMENT_NODE:
clone = newOwnerDocument.createDocumentFragment()
if deep:
for child in node.childNodes:
c = xml.dom.minidom._clone_node(child, deep, newOwnerDocument)
clone.appendChild(c)
elif node.nodeType == xml.dom.minidom.Node.TEXT_NODE:
clone = newOwnerDocument.createTextNode(node.data)
elif node.nodeType == xml.dom.minidom.Node.CDATA_SECTION_NODE:
clone = newOwnerDocument.createCDATASection(node.data)
elif node.nodeType == xml.dom.minidom.Node.PROCESSING_INSTRUCTION_NODE:
clone = newOwnerDocument.createProcessingInstruction(node.target,
node.data)
elif node.nodeType == xml.dom.minidom.Node.COMMENT_NODE:
clone = newOwnerDocument.createComment(node.data)
elif node.nodeType == xml.dom.minidom.Node.ATTRIBUTE_NODE:
clone = newOwnerDocument.createAttributeNS(node.namespaceURI,
node.nodeName)
clone.specified = True
clone.value = node.value
elif node.nodeType == xml.dom.minidom.Node.DOCUMENT_TYPE_NODE:
assert node.ownerDocument is not newOwnerDocument
operation = xml.dom.UserDataHandler.NODE_IMPORTED
clone = newOwnerDocument.implementation.createDocumentType(
node.name, node.publicId, node.systemId)
clone.ownerDocument = newOwnerDocument
if deep:
clone.entities._seq = []
clone.notations._seq = []
for n in node.notations._seq:
notation = xml.dom.minidom.Notation(n.nodeName, n.publicId, n.systemId)
notation.ownerDocument = newOwnerDocument
clone.notations._seq.append(notation)
if hasattr(n, '_call_user_data_handler'):
n._call_user_data_handler(operation, n, notation)
for e in node.entities._seq:
entity = xml.dom.minidom.Entity(e.nodeName, e.publicId, e.systemId,
e.notationName)
entity.actualEncoding = e.actualEncoding
entity.encoding = e.encoding
entity.version = e.version
entity.ownerDocument = newOwnerDocument
clone.entities._seq.append(entity)
if hasattr(e, '_call_user_data_handler'):
e._call_user_data_handler(operation, n, entity)
else:
# Note the cloning of Document and DocumentType nodes is
# implemenetation specific. minidom handles those cases
# directly in the cloneNode() methods.
raise xml.dom.NotSupportedErr("Cannot clone node %s" % repr(node))
# Check for _call_user_data_handler() since this could conceivably
# used with other DOM implementations (one of the FourThought
# DOMs, perhaps?).
if hasattr(node, '_call_user_data_handler'):
node._call_user_data_handler(operation, node, clone)
return clone
xml.dom.minidom._clone_node = _clone_node

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,88 +0,0 @@
"""Translate strings to and from SOAP 1.2 XML name encoding
Implements rules for mapping application defined name to XML names
specified by the w3 SOAP working group for SOAP version 1.2 in
Appendix A of "SOAP Version 1.2 Part 2: Adjuncts", W3C Working Draft
17, December 2001, <http://www.w3.org/TR/soap12-part2/#namemap>
Also see <http://www.w3.org/2000/xp/Group/xmlp-issues>.
Author: Gregory R. Warnes <gregory_r_warnes@groton.pfizer.com>
Date:: 2002-04-25
Version 0.9.0
"""
ident = "$Id$"
from re import *
def _NCNameChar(x):
return x.isalpha() or x.isdigit() or x=="." or x=='-' or x=="_"
def _NCNameStartChar(x):
return x.isalpha() or x=="_"
def _toUnicodeHex(x):
hexval = hex(ord(x[0]))[2:]
hexlen = len(hexval)
# Make hexval have either 4 or 8 digits by prepending 0's
if (hexlen==1): hexval = "000" + hexval
elif (hexlen==2): hexval = "00" + hexval
elif (hexlen==3): hexval = "0" + hexval
elif (hexlen==4): hexval = "" + hexval
elif (hexlen==5): hexval = "000" + hexval
elif (hexlen==6): hexval = "00" + hexval
elif (hexlen==7): hexval = "0" + hexval
elif (hexlen==8): hexval = "" + hexval
else: raise Exception, "Illegal Value returned from hex(ord(x))"
return "_x"+ hexval + "_"
def _fromUnicodeHex(x):
return eval( r'u"\u'+x[2:-1]+'"' )
def toXMLname(string):
"""Convert string to a XML name."""
if string.find(':') != -1 :
(prefix, localname) = string.split(':',1)
else:
prefix = None
localname = string
T = unicode(localname)
N = len(localname)
X = [];
for i in range(N) :
if i< N-1 and T[i]==u'_' and T[i+1]==u'x':
X.append(u'_x005F_')
elif i==0 and N >= 3 and \
( T[0]==u'x' or T[0]==u'X' ) and \
( T[1]==u'm' or T[1]==u'M' ) and \
( T[2]==u'l' or T[2]==u'L' ):
X.append(u'_xFFFF_' + T[0])
elif (not _NCNameChar(T[i])) or (i==0 and not _NCNameStartChar(T[i])):
X.append(_toUnicodeHex(T[i]))
else:
X.append(T[i])
return u''.join(X)
def fromXMLname(string):
"""Convert XML name to unicode string."""
retval = sub(r'_xFFFF_','', string )
def fun( matchobj ):
return _fromUnicodeHex( matchobj.group(0) )
retval = sub(r'_x[0-9A-Za-z]+_', fun, retval )
return retval

View File

@ -1,35 +0,0 @@
"""WSDL parsing services package for Web Services for Python."""
ident = "$Id$"
import WSDLTools
import XMLname
from logging import getLogger as _getLogger
import logging.config as _config
LOGGING = 'logging.txt'
DEBUG = True
#
# If LOGGING configuration file is not found, turn off logging
# and use _noLogger class because logging module's performance
# is terrible.
#
try:
_config.fileConfig(LOGGING)
except:
DEBUG = False
class Base:
def __init__(self, module=__name__):
self.logger = _noLogger()
if DEBUG is True:
self.logger = _getLogger('%s-%s(%x)' %(module, self.__class__, id(self)))
class _noLogger:
def __init__(self, *args): pass
def warning(self, *args): pass
def debug(self, *args): pass
def error(self, *args): pass

View File

View File

@ -1,364 +0,0 @@
"""Python wrapper
for Amazon web APIs
This module allows you to access Amazon's web APIs,
to do things like search Amazon and get the results programmatically.
Described here:
http://www.amazon.com/webservices
You need a Amazon-provided license key to use these services.
Follow the link above to get one. These functions will look in
several places (in this order) for the license key:
- the "license_key" argument of each function
- the module-level LICENSE_KEY variable (call setLicense once to set it)
- an environment variable called AMAZON_LICENSE_KEY
- a file called ".amazonkey" in the current directory
- a file called "amazonkey.txt" in the current directory
- a file called ".amazonkey" in your home directory
- a file called "amazonkey.txt" in your home directory
- a file called ".amazonkey" in the same directory as amazon.py
- a file called "amazonkey.txt" in the same directory as amazon.py
Sample usage:
>>> import amazon
>>> amazon.setLicense('...') # must get your own key!
>>> pythonBooks = amazon.searchByKeyword('Python')
>>> pythonBooks[0].ProductName
u'Learning Python (Help for Programmers)'
>>> pythonBooks[0].URL
...
>>> pythonBooks[0].OurPrice
...
Other available functions:
- browseBestSellers
- searchByASIN
- searchByUPC
- searchByAuthor
- searchByArtist
- searchByActor
- searchByDirector
- searchByManufacturer
- searchByListMania
- searchSimilar
- searchByWishlist
Other usage notes:
- Most functions can take product_line as well, see source for possible values
- All functions can take type="lite" to get less detail in results
- All functions can take page=N to get second, third, fourth page of results
- All functions can take license_key="XYZ", instead of setting it globally
- All functions can take http_proxy="http://x/y/z" which overrides your system setting
"""
__author__ = "Mark Pilgrim (f8dy@diveintomark.org)"
__version__ = "0.64.1"
__cvsversion__ = "$Revision$"[11:-2]
__date__ = "$Date$"[7:-2]
__copyright__ = "Copyright (c) 2002 Mark Pilgrim"
__license__ = "Python"
# Powersearch and return object type fix by Joseph Reagle <geek@goatee.net>
# Locale support by Michael Josephson <mike@josephson.org>
# Modification to _contentsOf to strip trailing whitespace when loading Amazon key
# from a file submitted by Patrick Phalen.
# Support for specifying locale and associates ID as search parameters and
# internationalisation fix for the SalesRank integer conversion by
# Christian Theune <ct@gocept.com>, gocept gmbh & co. kg
# Support for BlendedSearch contributed by Alex Choo
from xml.dom import minidom
import os, sys, getopt, cgi, urllib, string
try:
import timeoutsocket # http://www.timo-tasi.org/python/timeoutsocket.py
timeoutsocket.setDefaultSocketTimeout(10)
except ImportError:
pass
LICENSE_KEY = None
ASSOCIATE = "webservices-20"
HTTP_PROXY = None
LOCALE = "us"
# don't touch the rest of these constants
class AmazonError(Exception): pass
class NoLicenseKey(Exception): pass
_amazonfile1 = ".amazonkey"
_amazonfile2 = "amazonkey.txt"
_licenseLocations = (
(lambda key: key, 'passed to the function in license_key variable'),
(lambda key: LICENSE_KEY, 'module-level LICENSE_KEY variable (call setLicense to set it)'),
(lambda key: os.environ.get('AMAZON_LICENSE_KEY', None), 'an environment variable called AMAZON_LICENSE_KEY'),
(lambda key: _contentsOf(os.getcwd(), _amazonfile1), '%s in the current directory' % _amazonfile1),
(lambda key: _contentsOf(os.getcwd(), _amazonfile2), '%s in the current directory' % _amazonfile2),
(lambda key: _contentsOf(os.environ.get('HOME', ''), _amazonfile1), '%s in your home directory' % _amazonfile1),
(lambda key: _contentsOf(os.environ.get('HOME', ''), _amazonfile2), '%s in your home directory' % _amazonfile2),
(lambda key: _contentsOf(_getScriptDir(), _amazonfile1), '%s in the amazon.py directory' % _amazonfile1),
(lambda key: _contentsOf(_getScriptDir(), _amazonfile2), '%s in the amazon.py directory' % _amazonfile2)
)
_supportedLocales = {
"us" : (None, "xml.amazon.com"),
"uk" : ("uk", "xml-eu.amazon.com"),
"de" : ("de", "xml-eu.amazon.com"),
"jp" : ("jp", "xml.amazon.co.jp")
}
## administrative functions
def version():
print """PyAmazon %(__version__)s
%(__copyright__)s
released %(__date__)s
""" % globals()
def setAssociate(associate):
global ASSOCIATE
ASSOCIATE=associate
def getAssociate(override=None):
return override or ASSOCIATE
## utility functions
def _checkLocaleSupported(locale):
if not _supportedLocales.has_key(locale):
raise AmazonError, ("Unsupported locale. Locale must be one of: %s" %
string.join(_supportedLocales, ", "))
def setLocale(locale):
"""set locale"""
global LOCALE
_checkLocaleSupported(locale)
LOCALE = locale
def getLocale(locale=None):
"""get locale"""
return locale or LOCALE
def setLicense(license_key):
"""set license key"""
global LICENSE_KEY
LICENSE_KEY = license_key
def getLicense(license_key = None):
"""get license key
license key can come from any number of locations;
see module docs for search order"""
for get, location in _licenseLocations:
rc = get(license_key)
if rc: return rc
raise NoLicenseKey, 'get a license key at http://www.amazon.com/webservices'
def setProxy(http_proxy):
"""set HTTP proxy"""
global HTTP_PROXY
HTTP_PROXY = http_proxy
def getProxy(http_proxy = None):
"""get HTTP proxy"""
return http_proxy or HTTP_PROXY
def getProxies(http_proxy = None):
http_proxy = getProxy(http_proxy)
if http_proxy:
proxies = {"http": http_proxy}
else:
proxies = None
return proxies
def _contentsOf(dirname, filename):
filename = os.path.join(dirname, filename)
if not os.path.exists(filename): return None
fsock = open(filename)
contents = fsock.read().strip()
fsock.close()
return contents
def _getScriptDir():
if __name__ == '__main__':
return os.path.abspath(os.path.dirname(sys.argv[0]))
else:
return os.path.abspath(os.path.dirname(sys.modules[__name__].__file__))
class Bag: pass
def unmarshal(element):
rc = Bag()
if isinstance(element, minidom.Element) and (element.tagName == 'Details'):
rc.URL = element.attributes["url"].value
childElements = [e for e in element.childNodes if isinstance(e, minidom.Element)]
if childElements:
for child in childElements:
key = child.tagName
if hasattr(rc, key):
if type(getattr(rc, key)) <> type([]):
setattr(rc, key, [getattr(rc, key)])
setattr(rc, key, getattr(rc, key) + [unmarshal(child)])
elif isinstance(child, minidom.Element) and (child.tagName == 'Details'):
# make the first Details element a key
setattr(rc,key,[unmarshal(child)])
#dbg: because otherwise 'hasattr' only tests
#dbg: on the second occurence: if there's a
#dbg: single return to a query, it's not a
#dbg: list. This module should always
#dbg: return a list of Details objects.
else:
setattr(rc, key, unmarshal(child))
else:
rc = "".join([e.data for e in element.childNodes if isinstance(e, minidom.Text)])
if element.tagName == 'SalesRank':
rc = rc.replace('.', '')
rc = rc.replace(',', '')
rc = int(rc)
return rc
def buildURL(search_type, keyword, product_line, type, page, license_key, locale, associate):
_checkLocaleSupported(locale)
url = "http://" + _supportedLocales[locale][1] + "/onca/xml3?f=xml"
url += "&t=%s" % associate
url += "&dev-t=%s" % license_key.strip()
url += "&type=%s" % type
if _supportedLocales[locale][0]:
url += "&locale=%s" % _supportedLocales[locale][0]
if page:
url += "&page=%s" % page
if product_line:
url += "&mode=%s" % product_line
url += "&%s=%s" % (search_type, urllib.quote(keyword))
return url
## main functions
def search(search_type, keyword, product_line, type = "heavy", page = None,
license_key=None, http_proxy = None, locale = None, associate = None):
"""search Amazon
You need a license key to call this function; see
http://www.amazon.com/webservices
to get one. Then you can either pass it to
this function every time, or set it globally; see the module docs for details.
Parameters:
keyword - keyword to search
search_type - in (KeywordSearch, BrowseNodeSearch, AsinSearch, UpcSearch, AuthorSearch, ArtistSearch, ActorSearch, DirectorSearch, ManufacturerSearch, ListManiaSearch, SimilaritySearch)
product_line - type of product to search for. restrictions based on search_type
UpcSearch - in (music, classical)
AuthorSearch - must be "books"
ArtistSearch - in (music, classical)
ActorSearch - in (dvd, vhs, video)
DirectorSearch - in (dvd, vhs, video)
ManufacturerSearch - in (electronics, kitchen, videogames, software, photo, pc-hardware)
http_proxy (optional) - address of HTTP proxy to use for sending and receiving SOAP messages
Returns: list of Bags, each Bag may contain the following attributes:
Asin - Amazon ID ("ASIN" number) of this item
Authors - list of authors
Availability - "available", etc.
BrowseList - list of related categories
Catalog - catalog type ("Book", etc)
CollectiblePrice - ?, format "$34.95"
ImageUrlLarge - URL of large image of this item
ImageUrlMedium - URL of medium image of this item
ImageUrlSmall - URL of small image of this item
Isbn - ISBN number
ListPrice - list price, format "$34.95"
Lists - list of ListMania lists that include this item
Manufacturer - manufacturer
Media - media ("Paperback", "Audio CD", etc)
NumMedia - number of different media types in which this item is available
OurPrice - Amazon price, format "$24.47"
ProductName - name of this item
ReleaseDate - release date, format "09 April, 1999"
Reviews - reviews (AvgCustomerRating, plus list of CustomerReview with Rating, Summary, Content)
SalesRank - sales rank (integer)
SimilarProducts - list of Product, which is ASIN number
ThirdPartyNewPrice - ?, format "$34.95"
URL - URL of this item
"""
license_key = getLicense(license_key)
locale = getLocale(locale)
associate = getAssociate(associate)
url = buildURL(search_type, keyword, product_line, type, page,
license_key, locale, associate)
proxies = getProxies(http_proxy)
u = urllib.FancyURLopener(proxies)
usock = u.open(url)
xmldoc = minidom.parse(usock)
# from xml.dom.ext import PrettyPrint
# PrettyPrint(xmldoc)
usock.close()
if search_type == "BlendedSearch":
data = unmarshal(xmldoc).BlendedSearch
else:
data = unmarshal(xmldoc).ProductInfo
if hasattr(data, 'ErrorMsg'):
raise AmazonError, data.ErrorMsg
else:
if search_type == "BlendedSearch":
# a list of ProductLine containing a list of ProductInfo
# containing a list of Details.
return data
else:
return data.Details
def searchByKeyword(keyword, product_line="books", type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
return search("KeywordSearch", keyword, product_line, type, page, license_key, http_proxy, locale, associate)
def browseBestSellers(browse_node, product_line="books", type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
return search("BrowseNodeSearch", browse_node, product_line, type, page, license_key, http_proxy, locale, associate)
def searchByASIN(ASIN, type="heavy", license_key=None, http_proxy=None, locale=None, associate=None):
return search("AsinSearch", ASIN, None, type, None, license_key, http_proxy, locale, associate)
def searchByUPC(UPC, type="heavy", license_key=None, http_proxy=None, locale=None, associate=None):
return search("UpcSearch", UPC, None, type, None, license_key, http_proxy, locale, associate)
def searchByAuthor(author, type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
return search("AuthorSearch", author, "books", type, page, license_key, http_proxy, locale, associate)
def searchByArtist(artist, product_line="music", type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
if product_line not in ("music", "classical"):
raise AmazonError, "product_line must be in ('music', 'classical')"
return search("ArtistSearch", artist, product_line, type, page, license_key, http_proxy, locale, associate)
def searchByActor(actor, product_line="dvd", type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
if product_line not in ("dvd", "vhs", "video"):
raise AmazonError, "product_line must be in ('dvd', 'vhs', 'video')"
return search("ActorSearch", actor, product_line, type, page, license_key, http_proxy, locale, associate)
def searchByDirector(director, product_line="dvd", type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
if product_line not in ("dvd", "vhs", "video"):
raise AmazonError, "product_line must be in ('dvd', 'vhs', 'video')"
return search("DirectorSearch", director, product_line, type, page, license_key, http_proxy, locale, associate)
def searchByManufacturer(manufacturer, product_line="pc-hardware", type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
if product_line not in ("electronics", "kitchen", "videogames", "software", "photo", "pc-hardware"):
raise AmazonError, "product_line must be in ('electronics', 'kitchen', 'videogames', 'software', 'photo', 'pc-hardware')"
return search("ManufacturerSearch", manufacturer, product_line, type, page, license_key, http_proxy, locale, associate)
def searchByListMania(listManiaID, type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
return search("ListManiaSearch", listManiaID, None, type, page, license_key, http_proxy, locale, associate)
def searchSimilar(ASIN, type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
return search("SimilaritySearch", ASIN, None, type, page, license_key, http_proxy, locale, associate)
def searchByWishlist(wishlistID, type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
return search("WishlistSearch", wishlistID, None, type, page, license_key, http_proxy, locale, associate)
def searchByPower(keyword, product_line="books", type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
return search("PowerSearch", keyword, product_line, type, page, license_key, http_proxy, locale, associate)
# >>> RecentKing = amazon.searchByPower('author:Stephen King and pubdate:2003')
# >>> SnowCrash = amazon.searchByPower('title:Snow Crash')
def searchByBlended(keyword, type="heavy", page=1, license_key=None, http_proxy=None, locale=None, associate=None):
return search("BlendedSearch", keyword, None, type, page, license_key, http_proxy, locale, associate)

View File

@ -1,294 +0,0 @@
# -*- Mode: Python; tab-width: 4 -*-
# Id: asynchat.py,v 2.26 2000/09/07 22:29:26 rushing Exp
# Author: Sam Rushing <rushing@nightmare.com>
# ======================================================================
# Copyright 1996 by Sam Rushing
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Sam
# Rushing not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# ======================================================================
r"""A class supporting chat-style (command/response) protocols.
This class adds support for 'chat' style protocols - where one side
sends a 'command', and the other sends a response (examples would be
the common internet protocols - smtp, nntp, ftp, etc..).
The handle_read() method looks at the input stream for the current
'terminator' (usually '\r\n' for single-line responses, '\r\n.\r\n'
for multi-line output), calling self.found_terminator() on its
receipt.
for example:
Say you build an async nntp client using this class. At the start
of the connection, you'll have self.terminator set to '\r\n', in
order to process the single-line greeting. Just before issuing a
'LIST' command you'll set it to '\r\n.\r\n'. The output of the LIST
command will be accumulated (using your own 'collect_incoming_data'
method) up to the terminator, and then control will be returned to
you - by calling your self.found_terminator() method.
"""
import socket
import asyncore
class async_chat (asyncore.dispatcher):
"""This is an abstract class. You must derive from this class, and add
the two methods collect_incoming_data() and found_terminator()"""
# these are overridable defaults
ac_in_buffer_size = 4096
ac_out_buffer_size = 4096
def __init__ (self, conn=None):
self.ac_in_buffer = ''
self.ac_out_buffer = ''
self.producer_fifo = fifo()
asyncore.dispatcher.__init__ (self, conn)
def set_terminator (self, term):
"Set the input delimiter. Can be a fixed string of any length, an integer, or None"
self.terminator = term
def get_terminator (self):
return self.terminator
# grab some more data from the socket,
# throw it to the collector method,
# check for the terminator,
# if found, transition to the next state.
def handle_read (self):
try:
data = self.recv (self.ac_in_buffer_size)
except socket.error, _:
self.handle_error()
return
self.ac_in_buffer = self.ac_in_buffer + data
# Continue to search for self.terminator in self.ac_in_buffer,
# while calling self.collect_incoming_data. The while loop
# is necessary because we might read several data+terminator
# combos with a single recv(1024).
while self.ac_in_buffer:
lb = len(self.ac_in_buffer)
terminator = self.get_terminator()
if terminator is None:
# no terminator, collect it all
self.collect_incoming_data (self.ac_in_buffer)
self.ac_in_buffer = ''
elif type(terminator) == type(0):
# numeric terminator
n = terminator
if lb < n:
self.collect_incoming_data (self.ac_in_buffer)
self.ac_in_buffer = ''
self.terminator = self.terminator - lb
else:
self.collect_incoming_data (self.ac_in_buffer[:n])
self.ac_in_buffer = self.ac_in_buffer[n:]
self.terminator = 0
self.found_terminator()
else:
# 3 cases:
# 1) end of buffer matches terminator exactly:
# collect data, transition
# 2) end of buffer matches some prefix:
# collect data to the prefix
# 3) end of buffer does not match any prefix:
# collect data
terminator_len = len(terminator)
index = self.ac_in_buffer.find(terminator)
if index != -1:
# we found the terminator
if index > 0:
# don't bother reporting the empty string (source of subtle bugs)
self.collect_incoming_data (self.ac_in_buffer[:index])
self.ac_in_buffer = self.ac_in_buffer[index+terminator_len:]
# This does the Right Thing if the terminator is changed here.
self.found_terminator()
else:
# check for a prefix of the terminator
index = find_prefix_at_end (self.ac_in_buffer, terminator)
if index:
if index != lb:
# we found a prefix, collect up to the prefix
self.collect_incoming_data (self.ac_in_buffer[:-index])
self.ac_in_buffer = self.ac_in_buffer[-index:]
break
else:
# no prefix, collect it all
self.collect_incoming_data (self.ac_in_buffer)
self.ac_in_buffer = ''
def handle_write (self):
self.initiate_send ()
def handle_close (self):
self.close()
def push (self, data):
self.producer_fifo.push (simple_producer (data))
self.initiate_send()
def push_with_producer (self, producer):
self.producer_fifo.push (producer)
self.initiate_send()
def readable (self):
"predicate for inclusion in the readable for select()"
return (len(self.ac_in_buffer) <= self.ac_in_buffer_size)
def writable (self):
"predicate for inclusion in the writable for select()"
# return len(self.ac_out_buffer) or len(self.producer_fifo) or (not self.connected)
# this is about twice as fast, though not as clear.
return not (
(self.ac_out_buffer == '') and
self.producer_fifo.is_empty() and
self.connected
)
def close_when_done (self):
"automatically close this channel once the outgoing queue is empty"
self.producer_fifo.push (None)
# refill the outgoing buffer by calling the more() method
# of the first producer in the queue
def refill_buffer (self):
_string_type = type('')
while 1:
if len(self.producer_fifo):
p = self.producer_fifo.first()
# a 'None' in the producer fifo is a sentinel,
# telling us to close the channel.
if p is None:
if not self.ac_out_buffer:
self.producer_fifo.pop()
self.close()
return
elif type(p) is _string_type:
self.producer_fifo.pop()
self.ac_out_buffer = self.ac_out_buffer + p
return
data = p.more()
if data:
self.ac_out_buffer = self.ac_out_buffer + data
return
else:
self.producer_fifo.pop()
else:
return
def initiate_send (self):
obs = self.ac_out_buffer_size
# try to refill the buffer
if (len (self.ac_out_buffer) < obs):
self.refill_buffer()
if self.ac_out_buffer and self.connected:
# try to send the buffer
try:
num_sent = self.send (self.ac_out_buffer[:obs])
if num_sent:
self.ac_out_buffer = self.ac_out_buffer[num_sent:]
except socket.error, _:
self.handle_error()
return
def discard_buffers (self):
# Emergencies only!
self.ac_in_buffer = ''
self.ac_out_buffer = ''
while self.producer_fifo:
self.producer_fifo.pop()
class simple_producer:
def __init__ (self, data, buffer_size=512):
self.data = data
self.buffer_size = buffer_size
def more (self):
if len (self.data) > self.buffer_size:
result = self.data[:self.buffer_size]
self.data = self.data[self.buffer_size:]
return result
else:
result = self.data
self.data = ''
return result
class fifo:
def __init__ (self, list=None):
if not list:
self.list = []
else:
self.list = list
def __len__ (self):
return len(self.list)
def is_empty (self):
return self.list == []
def first (self):
return self.list[0]
def push (self, data):
self.list.append (data)
def pop (self):
if self.list:
result = self.list[0]
del self.list[0]
return (1, result)
else:
return (0, None)
# Given 'haystack', see if any prefix of 'needle' is at its end. This
# assumes an exact match has already been checked. Return the number of
# characters matched.
# for example:
# f_p_a_e ("qwerty\r", "\r\n") => 1
# f_p_a_e ("qwerty\r\n", "\r\n") => 2
# f_p_a_e ("qwertydkjf", "\r\n") => 0
# this could maybe be made faster with a computed regex?
# [answer: no; circa Python-2.0, Jan 2001]
# python: 18307/s
# re: 12820/s
# regex: 14035/s
def find_prefix_at_end (haystack, needle):
nl = len(needle)
result = 0
for i in range (1,nl):
if haystack[-(nl-i):] == needle[:(nl-i)]:
result = nl-i
break
return result
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,494 +0,0 @@
# -*- Mode: Python -*-
# Id: asyncore.py,v 2.51 2000/09/07 22:29:26 rushing Exp
# Author: Sam Rushing <rushing@nightmare.com>
# ======================================================================
# Copyright 1996 by Sam Rushing
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Sam
# Rushing not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# ======================================================================
"""Basic infrastructure for asynchronous socket service clients and servers.
There are only two ways to have a program on a single processor do "more
than one thing at a time". Multi-threaded programming is the simplest and
most popular way to do it, but there is another very different technique,
that lets you have nearly all the advantages of multi-threading, without
actually using multiple threads. it's really only practical if your program
is largely I/O bound. If your program is CPU bound, then pre-emptive
scheduled threads are probably what you really need. Network servers are
rarely CPU-bound, however.
If your operating system supports the select() system call in its I/O
library (and nearly all do), then you can use it to juggle multiple
communication channels at once; doing other work while your I/O is taking
place in the "background." Although this strategy can seem strange and
complex, especially at first, it is in many ways easier to understand and
control than multi-threaded programming. The module documented here solves
many of the difficult problems for you, making the task of building
sophisticated high-performance network servers and clients a snap.
"""
import exceptions
import select
import socket
import sys
import time
import os
from errno import EALREADY, EINPROGRESS, EWOULDBLOCK, ECONNRESET, \
ENOTCONN, ESHUTDOWN, EINTR, EISCONN
try:
socket_map
except NameError:
socket_map = {}
class ExitNow(exceptions.Exception):
pass
def read(obj):
try:
obj.handle_read_event()
except ExitNow:
raise
except:
obj.handle_error()
def write(obj):
try:
obj.handle_write_event()
except ExitNow:
raise
except:
obj.handle_error()
def readwrite(obj, flags):
try:
if flags & select.POLLIN:
obj.handle_read_event()
if flags & select.POLLOUT:
obj.handle_write_event()
except ExitNow:
raise
except:
obj.handle_error()
def poll(timeout=0.0, map=None):
if map is None:
map = socket_map
if map:
r = []; w = []; e = []
for fd, obj in map.items():
if obj.readable():
r.append(fd)
if obj.writable():
w.append(fd)
if [] == r == w == e:
time.sleep(timeout)
else:
try:
r, w, e = select.select(r, w, e, timeout)
except select.error, err:
if err[0] != EINTR:
raise
else:
return
for fd in r:
obj = map.get(fd)
if obj is None:
continue
read(obj)
for fd in w:
obj = map.get(fd)
if obj is None:
continue
write(obj)
def poll2(timeout=0.0, map=None):
import poll
if map is None:
map = socket_map
if timeout is not None:
# timeout is in milliseconds
timeout = int(timeout*1000)
if map:
l = []
for fd, obj in map.items():
flags = 0
if obj.readable():
flags = poll.POLLIN
if obj.writable():
flags = flags | poll.POLLOUT
if flags:
l.append((fd, flags))
r = poll.poll(l, timeout)
for fd, flags in r:
obj = map.get(fd)
if obj is None:
continue
readwrite(obj, flags)
def poll3(timeout=0.0, map=None):
# Use the poll() support added to the select module in Python 2.0
if map is None:
map = socket_map
if timeout is not None:
# timeout is in milliseconds
timeout = int(timeout*1000)
pollster = select.poll()
if map:
for fd, obj in map.items():
flags = 0
if obj.readable():
flags = select.POLLIN
if obj.writable():
flags = flags | select.POLLOUT
if flags:
pollster.register(fd, flags)
try:
r = pollster.poll(timeout)
except select.error, err:
if err[0] != EINTR:
raise
r = []
for fd, flags in r:
obj = map.get(fd)
if obj is None:
continue
readwrite(obj, flags)
def loop(timeout=30.0, use_poll=0, map=None):
if map is None:
map = socket_map
if use_poll:
if hasattr(select, 'poll'):
poll_fun = poll3
else:
poll_fun = poll2
else:
poll_fun = poll
while map:
poll_fun(timeout, map)
class dispatcher:
debug = 0
connected = 0
accepting = 0
closing = 0
addr = None
def __init__(self, sock=None, map=None):
if sock:
self.set_socket(sock, map)
# I think it should inherit this anyway
self.socket.setblocking(0)
self.connected = 1
# XXX Does the constructor require that the socket passed
# be connected?
try:
self.addr = sock.getpeername()
except socket.error:
# The addr isn't crucial
pass
else:
self.socket = None
def __repr__(self):
status = [self.__class__.__module__+"."+self.__class__.__name__]
if self.accepting and self.addr:
status.append('listening')
elif self.connected:
status.append('connected')
if self.addr is not None:
try:
status.append('%s:%d' % self.addr)
except TypeError:
status.append(repr(self.addr))
return '<%s at %#x>' % (' '.join(status), id(self))
def add_channel(self, map=None):
#self.log_info('adding channel %s' % self)
if map is None:
map = socket_map
map[self._fileno] = self
def del_channel(self, map=None):
fd = self._fileno
if map is None:
map = socket_map
if map.has_key(fd):
#self.log_info('closing channel %d:%s' % (fd, self))
del map[fd]
def create_socket(self, family, type):
self.family_and_type = family, type
self.socket = socket.socket(family, type)
self.socket.setblocking(0)
self._fileno = self.socket.fileno()
self.add_channel()
def set_socket(self, sock, map=None):
self.socket = sock
## self.__dict__['socket'] = sock
self._fileno = sock.fileno()
self.add_channel(map)
def set_reuse_addr(self):
# try to re-use a server port if possible
try:
self.socket.setsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR,
self.socket.getsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR) | 1
)
except socket.error:
pass
# ==================================================
# predicates for select()
# these are used as filters for the lists of sockets
# to pass to select().
# ==================================================
def readable(self):
return True
if os.name == 'mac':
# The macintosh will select a listening socket for
# write if you let it. What might this mean?
def writable(self):
return not self.accepting
else:
def writable(self):
return True
# ==================================================
# socket object methods.
# ==================================================
def listen(self, num):
self.accepting = 1
if os.name == 'nt' and num > 5:
num = 1
return self.socket.listen(num)
def bind(self, addr):
self.addr = addr
return self.socket.bind(addr)
def connect(self, address):
self.connected = 0
err = self.socket.connect_ex(address)
# XXX Should interpret Winsock return values
if err in (EINPROGRESS, EALREADY, EWOULDBLOCK):
return
if err in (0, EISCONN):
self.addr = address
self.connected = 1
self.handle_connect()
else:
raise socket.error, err
def accept(self):
# XXX can return either an address pair or None
try:
conn, addr = self.socket.accept()
return conn, addr
except socket.error, why:
if why[0] == EWOULDBLOCK:
pass
else:
raise socket.error, why
def send(self, data):
try:
result = self.socket.send(data)
return result
except socket.error, why:
if why[0] == EWOULDBLOCK:
return 0
else:
raise socket.error, why
return 0
def recv(self, buffer_size):
try:
data = self.socket.recv(buffer_size)
if not data:
# a closed connection is indicated by signaling
# a read condition, and having recv() return 0.
self.handle_close()
return ''
else:
return data
except socket.error, why:
# winsock sometimes throws ENOTCONN
if why[0] in [ECONNRESET, ENOTCONN, ESHUTDOWN]:
self.handle_close()
return ''
else:
raise socket.error, why
def close(self):
self.del_channel()
self.socket.close()
# cheap inheritance, used to pass all other attribute
# references to the underlying socket object.
def __getattr__(self, attr):
return getattr(self.socket, attr)
# log and log_info may be overridden to provide more sophisticated
# logging and warning methods. In general, log is for 'hit' logging
# and 'log_info' is for informational, warning and error logging.
def log(self, message):
sys.stderr.write('log: %s\n' % str(message))
def log_info(self, message, type='info'):
if __debug__ or type != 'info':
print '%s: %s' % (type, message)
def handle_read_event(self):
if self.accepting:
# for an accepting socket, getting a read implies
# that we are connected
if not self.connected:
self.connected = 1
self.handle_accept()
elif not self.connected:
self.handle_connect()
self.connected = 1
self.handle_read()
else:
self.handle_read()
def handle_write_event(self):
# getting a write implies that we are connected
if not self.connected:
self.handle_connect()
self.connected = 1
self.handle_write()
def handle_expt_event(self):
self.handle_expt()
def handle_error(self):
nil, t, v, tbinfo = compact_traceback()
# sometimes a user repr method will crash.
try:
self_repr = repr(self)
except:
self_repr = '<__repr__(self) failed for object at %0x>' % id(self)
self.log_info(
'uncaptured python exception, closing channel %s (%s:%s %s)' % (
self_repr,
t,
v,
tbinfo
),
'error'
)
self.close()
def handle_expt(self):
self.log_info('unhandled exception', 'warning')
def handle_read(self):
self.log_info('unhandled read event', 'warning')
def handle_write(self):
self.log_info('unhandled write event', 'warning')
def handle_connect(self):
self.log_info('unhandled connect event', 'warning')
def handle_accept(self):
self.log_info('unhandled accept event', 'warning')
def handle_close(self):
self.log_info('unhandled close event', 'warning')
self.close()
# ---------------------------------------------------------------------------
# adds simple buffered output capability, useful for simple clients.
# [for more sophisticated usage use asynchat.async_chat]
# ---------------------------------------------------------------------------
class dispatcher_with_send(dispatcher):
def __init__(self, sock=None):
dispatcher.__init__(self, sock)
self.out_buffer = ''
def initiate_send(self):
num_sent = 0
num_sent = dispatcher.send(self, self.out_buffer[:512])
self.out_buffer = self.out_buffer[num_sent:]
def handle_write(self):
self.initiate_send()
def writable(self):
return (not self.connected) or len(self.out_buffer)
def send(self, data):
if self.debug:
self.log_info('sending %s' % repr(data))
self.out_buffer = self.out_buffer + data
self.initiate_send()
# ---------------------------------------------------------------------------
# used for debugging.
# ---------------------------------------------------------------------------
def compact_traceback():
t, v, tb = sys.exc_info()
tbinfo = []
assert tb # Must have a traceback
while tb:
tbinfo.append((
tb.tb_frame.f_code.co_filename,
tb.tb_frame.f_code.co_name,
str(tb.tb_lineno)
))
tb = tb.tb_next
# just to be safe
del tb
file, function, line = tbinfo[-1]
info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo])
return (file, function, line), t, v, info
def close_all(map=None):
if map is None:
map = socket_map
for x in map.values():
x.socket.close()
map.clear()

View File

@ -1,179 +0,0 @@
# babelizer.py - API for simple access to babelfish.altavista.com.
# Requires python 2.0 or better.
#
# See it in use at http://babel.MrFeinberg.com/
"""API for simple access to babelfish.altavista.com.
Summary:
import babelizer
print ' '.join(babelizer.available_languages)
print babelizer.translate( 'How much is that doggie in the window?',
'English', 'French' )
def babel_callback(phrase):
print phrase
sys.stdout.flush()
babelizer.babelize( 'I love a reigning knight.',
'English', 'German',
callback = babel_callback )
available_languages
A list of languages available for use with babelfish.
translate( phrase, from_lang, to_lang )
Uses babelfish to translate phrase from from_lang to to_lang.
babelize(phrase, from_lang, through_lang, limit = 12, callback = None)
Uses babelfish to translate back and forth between from_lang and
through_lang until either no more changes occur in translation or
limit iterations have been reached, whichever comes first. Takes
an optional callback function which should receive a single
parameter, being the next translation. Without the callback
returns a list of successive translations.
It's only guaranteed to work if 'english' is one of the two languages
given to either of the translation methods.
Both translation methods throw exceptions which are all subclasses of
BabelizerError. They include
LanguageNotAvailableError
Thrown on an attempt to use an unknown language.
BabelfishChangedError
Thrown when babelfish.altavista.com changes some detail of their
layout, and babelizer can no longer parse the results or submit
the correct form (a not infrequent occurance).
BabelizerIOError
Thrown for various networking and IO errors.
Version: $Id$
Author: Jonathan Feinberg <jdf@pobox.com>
"""
import re, string, urllib
"""
Various patterns I have encountered in looking for the babelfish result.
We try each of them in turn, based on the relative number of times I've
seen each of these patterns. $1.00 to anyone who can provide a heuristic
for knowing which one to use. This includes AltaVista employees.
"""
__where = [ re.compile(r'lang=..>([^<]*)</div'),
re.compile(r'name=\"q\" value=\"([^\"]*)\">'),
re.compile(r'div style=padding:10px;>([^<]+)</div'),
]
__languages = { 'english' : 'en',
'chinese_simple' : 'zh',
'chinese_traditional' : 'zt',
'french' : 'fr',
'german' : 'de',
'italian' : 'it',
'japanese' : 'ja',
'korean' : 'ko',
'spanish' : 'es',
'portuguese' : 'pt',
'russian' : 'ru',
'greek' : 'el',
'dutch' : 'nl',
}
"""
All of the available language names.
"""
available_languages = [ x.title() for x in __languages.keys() ]
"""
Calling translate() or babelize() can raise a BabelizerError
"""
class BabelizerError(Exception):
pass
class LanguageNotAvailableError(BabelizerError):
pass
class BabelfishChangedError(BabelizerError):
pass
class BabelizerIOError(BabelizerError):
pass
def clean(text):
return ' '.join(string.replace(text.strip(), "\n", ' ').split())
def translate(phrase, from_lang, to_lang):
phrase = clean(phrase)
try:
from_code = __languages[from_lang.lower()]
to_code = __languages[to_lang.lower()]
except KeyError, lang:
raise LanguageNotAvailableError(lang)
params = urllib.urlencode( { 'BabelFishFrontPage' : 'yes',
'doit' : 'done',
'tt' : 'urltext',
'intl' : '1',
'urltext' : phrase,
'lp' : from_code + '_' + to_code } )
try:
response = urllib.urlopen('http://babelfish.altavista.com/babelfish/tr', params)
except IOError, what:
raise BabelizerIOError("Couldn't talk to server: %s" % what)
except:
print "Unexpected error:", sys.exc_info()[0]
html = response.read()
try:
begin = html.index('<!-- Target text (content) -->')
end = html.index('<!-- end: Target text (content) -->')
html = html[begin:end]
except ValueError:
pass
for regex in __where:
match = regex.search(html)
if match:
break
if not match:
raise BabelfishChangedError("Can't recognize translated string.")
return clean(match.group(1))
def babelize(phrase, from_language, through_language, limit = 12, callback = None):
phrase = clean(phrase)
seen = { phrase: 1 }
results = []
if callback:
def_callback = callback
else:
def_callback = results.append
def_callback(phrase)
flip = { from_language: through_language, through_language: from_language }
next = from_language
for i in range(limit):
phrase = translate(phrase, next, flip[next])
if seen.has_key(phrase):
break
seen[phrase] = 1
def_callback(phrase)
next = flip[next]
# next is set to the language of the last entry. this should be the same
# as the language we are translating to
if next != through_language:
phrase = translate(phrase, next, flip[next])
def_callback(phrase)
if not callback:
return results
if __name__ == '__main__':
import sys
def printer(x):
print x
sys.stdout.flush();
babelize("I won't take that sort of treatment from you, or from your doggie!",
'english', 'french', callback = printer)

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
"""
Copyright (c) 2003 Gustavo Niemeyer <niemeyer@conectiva.com>
This module offers extensions to the standard python 2.3+
datetime module.
"""
__author__ = "Gustavo Niemeyer <niemeyer@conectiva.com>"
__license__ = "PSF License"

View File

@ -1,869 +0,0 @@
# -*- coding:iso-8859-1 -*-
# Gotten from: http://moin.conectiva.com.br/DateUtil
"""
Copyright (c) 2003 Gustavo Niemeyer <niemeyer@conectiva.com>
This module offers extensions to the standard python 2.3+
datetime module.
"""
__author__ = "Gustavo Niemeyer <niemeyer@conectiva.com>"
__license__ = "PSF License"
import os.path
import string
import sys
import time
import datetime
import relativedelta
import tz
__all__ = ["parse", "parserinfo"]
# Some pointers:
#
# http://www.cl.cam.ac.uk/~mgk25/iso-time.html
# http://www.iso.ch/iso/en/prods-services/popstds/datesandtime.html
# http://www.w3.org/TR/NOTE-datetime
# http://ringmaster.arc.nasa.gov/tools/time_formats.html
# http://search.cpan.org/author/MUIR/Time-modules-2003.0211/lib/Time/ParseDate.pm
# http://stein.cshl.org/jade/distrib/docs/java.text.SimpleDateFormat.html
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
class _timelex:
def __init__(self, instream):
if isinstance(instream, basestring):
instream = StringIO(instream)
self.instream = instream
self.wordchars = ('abcdfeghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ_'
'ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ'
'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ')
self.numchars = '0123456789'
self.whitespace = ' \t\r\n'
self.charstack = []
self.tokenstack = []
self.eof = False
def get_token(self):
if self.tokenstack:
return self.tokenstack.pop(0)
seenletters = False
token = None
state = None
wordchars = self.wordchars
numchars = self.numchars
whitespace = self.whitespace
while not self.eof:
if self.charstack:
nextchar = self.charstack.pop(0)
else:
nextchar = self.instream.read(1)
if not nextchar:
self.eof = True
break
elif not state:
token = nextchar
if nextchar in wordchars:
state = 'a'
elif nextchar in numchars:
state = '0'
elif nextchar in whitespace:
token = ' '
break # emit token
else:
break # emit token
elif state == 'a':
seenletters = True
if nextchar in wordchars:
token += nextchar
elif nextchar == '.':
token += nextchar
state = 'a.'
else:
self.charstack.append(nextchar)
break # emit token
elif state == '0':
if nextchar in numchars:
token += nextchar
elif nextchar == '.':
token += nextchar
state = '0.'
else:
self.charstack.append(nextchar)
break # emit token
elif state == 'a.':
seenletters = True
if nextchar == '.' or nextchar in wordchars:
token += nextchar
elif nextchar in numchars and token[-1] == '.':
token += nextchar
state = '0.'
else:
self.charstack.append(nextchar)
break # emit token
elif state == '0.':
if nextchar == '.' or nextchar in numchars:
token += nextchar
elif nextchar in wordchars and token[-1] == '.':
token += nextchar
state = 'a.'
else:
self.charstack.append(nextchar)
break # emit token
if (state in ('a.', '0.') and
(seenletters or token.count('.') > 1 or token[-1] == '.')):
l = token.split('.')
token = l[0]
for tok in l[1:]:
self.tokenstack.append('.')
if tok:
self.tokenstack.append(tok)
return token
def __iter__(self):
return self
def next(self):
token = self.get_token()
if token is None:
raise StopIteration
return token
def split(cls, s):
return list(cls(s))
split = classmethod(split)
class _resultbase(object):
def __init__(self):
for attr in self.__slots__:
setattr(self, attr, None)
def _repr(self, classname):
l = []
for attr in self.__slots__:
value = getattr(self, attr)
if value is not None:
l.append("%s=%s" % (attr, `value`))
return "%s(%s)" % (classname, ", ".join(l))
def __repr__(self):
return self._repr(self.__class__.__name__)
class parserinfo:
# m from a.m/p.m, t from ISO T separator
JUMP = [" ", ".", ",", ";", "-", "/", "'",
"at", "on", "and", "ad", "m", "t", "of",
"st", "nd", "rd", "th"]
WEEKDAYS = [("Mon", "Monday"),
("Tue", "Tuesday"),
("Wed", "Wednesday"),
("Thu", "Thursday"),
("Fri", "Friday"),
("Sat", "Saturday"),
("Sun", "Sunday")]
MONTHS = [("Jan", "January"),
("Feb", "February"),
("Mar", "March"),
("Apr", "April"),
("May", "May"),
("Jun", "June"),
("Jul", "July"),
("Aug", "August"),
("Sep", "September"),
("Oct", "October"),
("Nov", "November"),
("Dec", "December")]
HMS = [("h", "hour", "hours"),
("m", "minute", "minutes"),
("s", "second", "seconds")]
AMPM = [("am", "a"),
("pm", "p")]
UTCZONE = ["UTC", "GMT", "Z"]
PERTAIN = ["of"]
TZOFFSET = {}
def __init__(self, dayfirst=False, yearfirst=False):
self._jump = self._convert(self.JUMP)
self._weekdays = self._convert(self.WEEKDAYS)
self._months = self._convert(self.MONTHS)
self._hms = self._convert(self.HMS)
self._ampm = self._convert(self.AMPM)
self._utczone = self._convert(self.UTCZONE)
self._pertain = self._convert(self.PERTAIN)
self.dayfirst = dayfirst
self.yearfirst = yearfirst
self._year = time.localtime().tm_year
self._century = self._year/100*100
def _convert(self, lst):
dct = {}
for i in range(len(lst)):
v = lst[i]
if isinstance(v, tuple):
for v in v:
dct[v.lower()] = i
else:
dct[v.lower()] = i
return dct
def jump(self, name):
return name.lower() in self._jump
def weekday(self, name):
if len(name) >= 3:
try:
return self._weekdays[name.lower()]
except KeyError:
pass
return None
def month(self, name):
if len(name) >= 3:
try:
return self._months[name.lower()]+1
except KeyError:
pass
return None
def hms(self, name):
try:
return self._hms[name.lower()]
except KeyError:
return None
def ampm(self, name):
try:
return self._ampm[name.lower()]
except KeyError:
return None
def pertain(self, name):
return name.lower() in self._pertain
def utczone(self, name):
return name.lower() in self._utczone
def tzoffset(self, name):
if name in self._utczone:
return 0
return self.TZOFFSET.get(name)
def convertyear(self, year):
if year < 100:
year += self._century
if abs(year-self._year) >= 50:
if year < self._year:
year += 100
else:
year -= 100
return year
def validate(self, res):
# move to info
if res.year is not None:
res.year = self.convertyear(res.year)
if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z':
res.tzname = "UTC"
res.tzoffset = 0
elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
res.tzoffset = 0
return True
class parser:
def __init__(self, parserinfo=parserinfo):
self.info = parserinfo()
def parse(self, timestr, default=None,
ignoretz=False, tzinfos=None,
**kwargs):
if not default:
default = datetime.datetime.now().replace(hour=0, minute=0,
second=0, microsecond=0)
res = self._parse(timestr, **kwargs)
if res is None:
raise ValueError, "unknown string format"
repl = {}
for attr in ["year", "month", "day", "hour",
"minute", "second", "microsecond"]:
value = getattr(res, attr)
if value is not None:
repl[attr] = value
ret = default.replace(**repl)
if res.weekday is not None and not res.day:
ret = ret+relativedelta.relativedelta(weekday=res.weekday)
if not ignoretz:
if callable(tzinfos) or tzinfos and res.tzname in tzinfos:
if callable(tzinfos):
tzdata = tzinfos(res.tzname, res.tzoffset)
else:
tzdata = tzinfos.get(res.tzname)
if isinstance(tzdata, datetime.tzinfo):
tzinfo = tzdata
elif isinstance(tzdata, basestring):
tzinfo = tz.tzstr(tzdata)
elif isinstance(tzdata, int):
tzinfo = tz.tzoffset(res.tzname, tzdata)
else:
raise ValueError, "offset must be tzinfo subclass, " \
"tz string, or int offset"
ret = ret.replace(tzinfo=tzinfo)
elif res.tzname and res.tzname in time.tzname:
ret = ret.replace(tzinfo=tz.tzlocal())
elif res.tzoffset == 0:
ret = ret.replace(tzinfo=tz.tzutc())
elif res.tzoffset:
ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
return ret
class _result(_resultbase):
__slots__ = ["year", "month", "day", "weekday",
"hour", "minute", "second", "microsecond",
"tzname", "tzoffset"]
def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False):
info = self.info
if dayfirst is None:
dayfirst = info.dayfirst
if yearfirst is None:
yearfirst = info.yearfirst
res = self._result()
l = _timelex.split(timestr)
try:
# year/month/day list
ymd = []
# Index of the month string in ymd
mstridx = -1
len_l = len(l)
i = 0
while i < len_l:
# Check if it's a number
try:
value = float(l[i])
except ValueError:
value = None
if value is not None:
# Token is a number
len_li = len(l[i])
i += 1
if (len(ymd) == 3 and len_li in (2, 4)
and (i >= len_l or l[i] != ':')):
# 19990101T23[59]
s = l[i-1]
res.hour = int(s[:2])
if len_li == 4:
res.minute = int(s[2:])
elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6):
# YYMMDD or HHMMSS[.ss]
s = l[i-1]
if not ymd and l[i-1].find('.') == -1:
ymd.append(info.convertyear(int(s[:2])))
ymd.append(int(s[2:4]))
ymd.append(int(s[4:]))
else:
# 19990101T235959[.59]
res.hour = int(s[:2])
res.minute = int(s[2:4])
value = float(s[4:])
res.second = int(value)
if value%1:
res.microsecond = int(1000000*(value%1))
elif len_li == 8:
# YYYYMMDD
s = l[i-1]
ymd.append(int(s[:4]))
ymd.append(int(s[4:6]))
ymd.append(int(s[6:]))
elif len_li in (12, 14):
# YYYYMMDDhhmm[ss]
s = l[i-1]
ymd.append(int(s[:4]))
ymd.append(int(s[4:6]))
ymd.append(int(s[6:8]))
res.hour = int(s[8:10])
res.minute = int(s[10:12])
if len_li == 14:
res.second = int(s[12:])
elif ((i < len_l and info.hms(l[i]) is not None) or
(i+1 < len_l and l[i] == ' ' and
info.hms(l[i+1]) is not None)):
# HH[ ]h or MM[ ]m or SS[.ss][ ]s
if l[i] == ' ':
i += 1
idx = info.hms(l[i])
while True:
if idx == 0:
res.hour = int(value)
if value%1:
res.minute = int(60*(value%1))
elif idx == 1:
res.minute = int(value)
if value%1:
res.second = int(60*(value%1))
elif idx == 2:
res.second = int(value)
if value%1:
res.microsecond = int(1000000*(value%1))
i += 1
if i >= len_l or idx == 2:
break
# 12h00
try:
value = float(l[i])
except ValueError:
break
else:
i += 1
idx += 1
if i < len_l:
newidx = info.hms(l[i])
if newidx is not None:
idx = newidx
elif i+1 < len_l and l[i] == ':':
# HH:MM[:SS[.ss]]
res.hour = int(value)
i += 1
value = float(l[i])
res.minute = int(value)
if value%1:
res.second = int(60*(value%1))
i += 1
if i < len_l and l[i] == ':':
value = float(l[i+1])
res.second = int(value)
if value%1:
res.microsecond = int(1000000*(value%1))
i += 2
elif i < len_l and l[i] in ('-', '/', '.'):
sep = l[i]
ymd.append(int(value))
i += 1
if i < len_l and not info.jump(l[i]):
try:
# 01-01[-01]
ymd.append(int(l[i]))
except ValueError:
# 01-Jan[-01]
value = info.month(l[i])
if value is not None:
ymd.append(value)
assert mstridx == -1
mstridx = len(ymd)-1
else:
return None
i += 1
if i < len_l and l[i] == sep:
# We have three members
i += 1
value = info.month(l[i])
if value is not None:
ymd.append(value)
mstridx = len(ymd)-1
assert mstridx == -1
else:
ymd.append(int(l[i]))
i += 1
elif i >= len_l or info.jump(l[i]):
if i+1 < len_l and info.ampm(l[i+1]) is not None:
# 12 am
res.hour = int(value)
if res.hour < 12 and info.ampm(l[i+1]) == 1:
res.hour += 12
elif res.hour == 12 and info.ampm(l[i+1]) == 0:
res.hour = 0
i += 1
else:
# Year, month or day
ymd.append(int(value))
i += 1
elif info.ampm(l[i]) is not None:
# 12am
res.hour = int(value)
if res.hour < 12 and info.ampm(l[i]) == 1:
res.hour += 12
elif res.hour == 12 and info.ampm(l[i]) == 0:
res.hour = 0
i += 1
elif not fuzzy:
return None
else:
i += 1
continue
# Check weekday
value = info.weekday(l[i])
if value is not None:
res.weekday = value
i += 1
continue
# Check month name
value = info.month(l[i])
if value is not None:
ymd.append(value)
assert mstridx == -1
mstridx = len(ymd)-1
i += 1
if i < len_l:
if l[i] in ('-', '/'):
# Jan-01[-99]
sep = l[i]
i += 1
ymd.append(int(l[i]))
i += 1
if i < len_l and l[i] == sep:
# Jan-01-99
i += 1
ymd.append(int(l[i]))
i += 1
elif (i+3 < len_l and l[i] == l[i+2] == ' '
and info.pertain(l[i+1])):
# Jan of 01
# In this case, 01 is clearly year
try:
value = int(l[i+3])
except ValueError:
# Wrong guess
pass
else:
# Convert it here to become unambiguous
ymd.append(info.convertyear(value))
i += 4
continue
# Check am/pm
value = info.ampm(l[i])
if value is not None:
if value == 1 and res.hour < 12:
res.hour += 12
elif value == 0 and res.hour == 12:
res.hour = 0
i += 1
continue
# Check for a timezone name
if (res.hour is not None and len(l[i]) <= 5 and
res.tzname is None and res.tzoffset is None and
not [x for x in l[i] if x not in string.ascii_uppercase]):
res.tzname = l[i]
res.tzoffset = info.tzoffset(res.tzname)
i += 1
# Check for something like GMT+3, or BRST+3. Notice
# that it doesn't mean "I am 3 hours after GMT", but
# "my time +3 is GMT". If found, we reverse the
# logic so that timezone parsing code will get it
# right.
if i < len_l and l[i] in ('+', '-'):
l[i] = ('+', '-')[l[i] == '+']
res.tzoffset = None
if info.utczone(res.tzname):
# With something like GMT+3, the timezone
# is *not* GMT.
res.tzname = None
continue
# Check for a numbered timezone
if res.hour is not None and l[i] in ('+', '-'):
signal = (-1,1)[l[i] == '+']
i += 1
len_li = len(l[i])
if len_li == 4:
# -0300
res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60
elif i+1 < len_l and l[i+1] == ':':
# -03:00
res.tzoffset = int(l[i])*3600+int(l[i+2])*60
i += 2
elif len_li <= 2:
# -[0]3
res.tzoffset = int(l[i][:2])*3600
else:
return None
i += 1
res.tzoffset *= signal
# Look for a timezone name between parenthesis
if (i+3 < len_l and
info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and
3 <= len(l[i+2]) <= 5 and
not [x for x in l[i+2]
if x not in string.ascii_uppercase]):
# -0300 (BRST)
res.tzname = l[i+2]
i += 4
continue
# Check jumps
if not (info.jump(l[i]) or fuzzy):
return None
i += 1
# Process year/month/day
len_ymd = len(ymd)
if len_ymd > 3:
# More than three members!?
return None
elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2):
# One member, or two members with a month string
if mstridx != -1:
res.month = ymd[mstridx]
del ymd[mstridx]
if len_ymd > 1 or mstridx == -1:
if ymd[0] > 31:
res.year = ymd[0]
else:
res.day = ymd[0]
elif len_ymd == 2:
# Two members with numbers
if ymd[0] > 31:
# 99-01
res.year, res.month = ymd
elif ymd[1] > 31:
# 01-99
res.month, res.year = ymd
elif dayfirst and ymd[1] <= 12:
# 13-01
res.day, res.month = ymd
else:
# 01-13
res.month, res.day = ymd
if len_ymd == 3:
# Three members
if mstridx == 0:
res.month, res.day, res.year = ymd
elif mstridx == 1:
if ymd[0] > 31 or (yearfirst and ymd[2] <= 31):
# 99-Jan-01
res.year, res.month, res.day = ymd
else:
# 01-Jan-01
# Give precendence to day-first, since
# two-digit years is usually hand-written.
res.day, res.month, res.year = ymd
elif mstridx == 2:
# WTF!?
if ymd[1] > 31:
# 01-99-Jan
res.day, res.year, res.month = ymd
else:
# 99-01-Jan
res.year, res.day, res.month = ymd
else:
if ymd[0] > 31 or \
(yearfirst and ymd[1] <= 12 and ymd[2] <= 31):
# 99-01-01
res.year, res.month, res.day = ymd
elif ymd[0] > 12 or (dayfirst and ymd[1] <= 12):
# 13-01-01
res.day, res.month, res.year = ymd
else:
# 01-13-01
res.month, res.day, res.year = ymd
except (IndexError, ValueError, AssertionError):
return None
if not info.validate(res):
return None
return res
DEFAULTPARSER = parser()
def parse(timestr, parserinfo=None, **kwargs):
if parserinfo:
return parser(parserinfo).parse(timestr, **kwargs)
else:
return DEFAULTPARSER.parse(timestr, **kwargs)
class _tzparser:
class _result(_resultbase):
__slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset",
"start", "end"]
class _attr(_resultbase):
__slots__ = ["month", "week", "weekday",
"yday", "jyday", "day", "time"]
def __repr__(self):
return self._repr("")
def __init__(self):
_resultbase.__init__(self)
self.start = self._attr()
self.end = self._attr()
def parse(self, tzstr):
res = self._result()
l = _timelex.split(tzstr)
try:
len_l = len(l)
i = 0
while i < len_l:
# BRST+3[BRDT[+2]]
j = i
while j < len_l and not [x for x in l[j]
if x in "0123456789:,-+"]:
j += 1
if j != i:
if not res.stdabbr:
offattr = "stdoffset"
res.stdabbr = "".join(l[i:j])
else:
offattr = "dstoffset"
res.dstabbr = "".join(l[i:j])
i = j
if (i < len_l and
(l[i] in ('+', '-') or l[i][0] in "0123456789")):
if l[i] in ('+', '-'):
signal = (1,-1)[l[i] == '+']
i += 1
else:
signal = -1
len_li = len(l[i])
if len_li == 4:
# -0300
setattr(res, offattr,
(int(l[i][:2])*3600+int(l[i][2:])*60)*signal)
elif i+1 < len_l and l[i+1] == ':':
# -03:00
setattr(res, offattr,
(int(l[i])*3600+int(l[i+2])*60)*signal)
i += 2
elif len_li <= 2:
# -[0]3
setattr(res, offattr,
int(l[i][:2])*3600*signal)
else:
return None
i += 1
if res.dstabbr:
break
else:
break
if i < len_l:
for j in range(i, len_l):
if l[j] == ';': l[j] = ','
assert l[i] == ','
i += 1
if i >= len_l:
pass
elif (8 <= l.count(',') <= 9 and
not [y for x in l[i:] if x != ','
for y in x if y not in "0123456789"]):
# GMT0BST,3,0,30,3600,10,0,26,7200[,3600]
for x in (res.start, res.end):
x.month = int(l[i])
i += 2
if l[i] == '-':
value = int(l[i+1])*-1
i += 1
else:
value = int(l[i])
i += 2
if value:
x.week = value
x.weekday = (int(l[i])-1)%7
else:
x.day = int(l[i])
i += 2
x.time = int(l[i])
i += 2
if i < len_l:
if l[i] in ('-','+'):
signal = (-1,1)[l[i] == "+"]
i += 1
else:
signal = 1
res.dstoffset = (res.stdoffset+int(l[i]))*signal
elif (l.count(',') == 2 and l[i:].count('/') <= 2 and
not [y for x in l[i:] if x not in (',','/','J','M',
'.','-',':')
for y in x if y not in "0123456789"]):
for x in (res.start, res.end):
if l[i] == 'J':
# non-leap year day (1 based)
i += 1
x.jyday = int(l[i])
elif l[i] == 'M':
# month[-.]week[-.]weekday
i += 1
x.month = int(l[i])
i += 1
assert l[i] in ('-', '.')
i += 1
x.week = int(l[i])
if x.week == 5:
x.week = -1
i += 1
assert l[i] in ('-', '.')
i += 1
x.weekday = (int(l[i])-1)%7
else:
# year day (zero based)
x.yday = int(l[i])+1
i += 1
if i < len_l and l[i] == '/':
i += 1
# start time
len_li = len(l[i])
if len_li == 4:
# -0300
x.time = (int(l[i][:2])*3600+int(l[i][2:])*60)
elif i+1 < len_l and l[i+1] == ':':
# -03:00
x.time = int(l[i])*3600+int(l[i+2])*60
i += 2
if i+1 < len_l and l[i+1] == ':':
i += 2
x.time += int(l[i])
elif len_li <= 2:
# -[0]3
x.time = (int(l[i][:2])*3600)
else:
return None
i += 1
assert i == len_l or l[i] == ','
i += 1
assert i >= len_l
except (IndexError, ValueError, AssertionError):
return None
return res
DEFAULTTZPARSER = _tzparser()
def _parsetz(tzstr):
return DEFAULTTZPARSER.parse(tzstr)
# vim:ts=4:sw=4:et

View File

@ -1,432 +0,0 @@
"""
Copyright (c) 2003 Gustavo Niemeyer <niemeyer@conectiva.com>
This module offers extensions to the standard python 2.3+
datetime module.
"""
__author__ = "Gustavo Niemeyer <niemeyer@conectiva.com>"
__license__ = "PSF License"
import datetime
import calendar
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
class weekday(object):
__slots__ = ["weekday", "n"]
def __init__(self, weekday, n=0):
self.weekday = weekday
self.n = n
def __call__(self, n):
if n == self.n:
return self
else:
return self.__class__(self.weekday, n)
def __eq__(self, other):
try:
if self.weekday != other.weekday or self.n != other.n:
return False
except AttributeError:
return False
return True
def __repr__(self):
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
if not self.n:
return s
else:
return "%s(%+d)" % (s, self.n)
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
class relativedelta:
"""
The relativedelta type is based on the specification of the excelent
work done by M.-A. Lemburg in his mx.DateTime extension. However,
notice that this type does *NOT* implement the same algorithm as
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
There's two different ways to build a relativedelta instance. The
first one is passing it two date/datetime classes:
relativedelta(datetime1, datetime2)
And the other way is to use the following keyword arguments:
year, month, day, hour, minute, second, microsecond:
Absolute information.
years, months, weeks, days, hours, minutes, seconds, microseconds:
Relative information, may be negative.
weekday:
One of the weekday instances (MO, TU, etc). These instances may
receive a parameter N, specifying the Nth weekday, which could
be positive or negative (like MO(+1) or MO(-2). Not specifying
it is the same as specifying +1. You can also use an integer,
where 0=MO.
leapdays:
Will add given days to the date found, if year is a leap
year, and the date found is post 28 of february.
yearday, nlyearday:
Set the yearday or the non-leap year day (jump leap days).
These are converted to day/month/leapdays information.
Here is the behavior of operations with relativedelta:
1) Calculate the absolute year, using the 'year' argument, or the
original datetime year, if the argument is not present.
2) Add the relative 'years' argument to the absolute year.
3) Do steps 1 and 2 for month/months.
4) Calculate the absolute day, using the 'day' argument, or the
original datetime day, if the argument is not present. Then,
subtract from the day until it fits in the year and month
found after their operations.
5) Add the relative 'days' argument to the absolute day. Notice
that the 'weeks' argument is multiplied by 7 and added to
'days'.
6) Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
microsecond/microseconds.
7) If the 'weekday' argument is present, calculate the weekday,
with the given (wday, nth) tuple. wday is the index of the
weekday (0-6, 0=Mon), and nth is the number of weeks to add
forward or backward, depending on its signal. Notice that if
the calculated date is already Monday, for example, using
(0, 1) or (0, -1) won't change the day.
"""
def __init__(self, dt1=None, dt2=None,
years=0, months=0, days=0, leapdays=0, weeks=0,
hours=0, minutes=0, seconds=0, microseconds=0,
year=None, month=None, day=None, weekday=None,
yearday=None, nlyearday=None,
hour=None, minute=None, second=None, microsecond=None):
if dt1 and dt2:
if not isinstance(dt1, datetime.date) or \
not isinstance(dt2, datetime.date):
raise TypeError, "relativedelta only diffs datetime/date"
if type(dt1) is not type(dt2):
if not isinstance(dt1, datetime.datetime):
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
elif not isinstance(dt2, datetime.datetime):
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
self.years = 0
self.months = 0
self.days = 0
self.leapdays = 0
self.hours = 0
self.minutes = 0
self.seconds = 0
self.microseconds = 0
self.year = None
self.month = None
self.day = None
self.weekday = None
self.hour = None
self.minute = None
self.second = None
self.microsecond = None
self._has_time = 0
months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month)
self._set_months(months)
dtm = self.__radd__(dt2)
if dt1 < dt2:
while dt1 > dtm:
months += 1
self._set_months(months)
dtm = self.__radd__(dt2)
else:
while dt1 < dtm:
months -= 1
self._set_months(months)
dtm = self.__radd__(dt2)
delta = dt1 - dtm
self.seconds = delta.seconds+delta.days*86400
self.microseconds = delta.microseconds
else:
self.years = years
self.months = months
self.days = days+weeks*7
self.leapdays = leapdays
self.hours = hours
self.minutes = minutes
self.seconds = seconds
self.microseconds = microseconds
self.year = year
self.month = month
self.day = day
self.hour = hour
self.minute = minute
self.second = second
self.microsecond = microsecond
if type(weekday) is int:
self.weekday = weekdays[weekday]
else:
self.weekday = weekday
yday = 0
if nlyearday:
yday = nlyearday
elif yearday:
yday = yearday
if yearday > 59:
self.leapdays = -1
if yday:
ydayidx = [31,59,90,120,151,181,212,243,273,304,334,366]
for idx, ydays in enumerate(ydayidx):
if yday <= ydays:
self.month = idx+1
if idx == 0:
self.day = ydays
else:
self.day = yday-ydayidx[idx-1]
break
else:
raise ValueError, "invalid year day (%d)" % yday
self._fix()
def _fix(self):
if abs(self.microseconds) > 999999:
s = self.microseconds/abs(self.microseconds)
div, mod = divmod(self.microseconds*s, 1000000)
self.microseconds = mod*s
self.seconds += div*s
if abs(self.seconds) > 59:
s = self.seconds/abs(self.seconds)
div, mod = divmod(self.seconds*s, 60)
self.seconds = mod*s
self.minutes += div*s
if abs(self.minutes) > 59:
s = self.minutes/abs(self.minutes)
div, mod = divmod(self.minutes*s, 60)
self.minutes = mod*s
self.hours += div*s
if abs(self.hours) > 23:
s = self.hours/abs(self.hours)
div, mod = divmod(self.hours*s, 24)
self.hours = mod*s
self.days += div*s
if abs(self.months) > 11:
s = self.months/abs(self.months)
div, mod = divmod(self.months*s, 12)
self.months = mod*s
self.years += div*s
if (self.hours or self.minutes or self.seconds or self.microseconds or
self.hour is not None or self.minute is not None or
self.second is not None or self.microsecond is not None):
self._has_time = 1
else:
self._has_time = 0
def _set_months(self, months):
self.months = months
if abs(self.months) > 11:
s = self.months/abs(self.months)
div, mod = divmod(self.months*s, 12)
self.months = mod*s
self.years = div*s
else:
self.years = 0
def __radd__(self, other):
if not isinstance(other, datetime.date):
raise TypeError, "unsupported type for add operation"
elif self._has_time and not isinstance(other, datetime.datetime):
other = datetime.datetime.fromordinal(other.toordinal())
year = (self.year or other.year)+self.years
month = self.month or other.month
if self.months:
assert 1 <= abs(self.months) <= 12
month += self.months
if month > 12:
year += 1
month -= 12
elif month < 1:
year -= 1
month += 12
day = min(calendar.monthrange(year, month)[1],
self.day or other.day)
repl = {"year": year, "month": month, "day": day}
for attr in ["hour", "minute", "second", "microsecond"]:
value = getattr(self, attr)
if value is not None:
repl[attr] = value
days = self.days
if self.leapdays and month > 2 and calendar.isleap(year):
days += self.leapdays
ret = (other.replace(**repl)
+ datetime.timedelta(days=days,
hours=self.hours,
minutes=self.minutes,
seconds=self.seconds,
microseconds=self.microseconds))
if self.weekday:
weekday, nth = self.weekday.weekday, self.weekday.n or 1
jumpdays = (abs(nth)-1)*7
if nth > 0:
jumpdays += (7-ret.weekday()+weekday)%7
else:
jumpdays += (ret.weekday()-weekday)%7
jumpdays *= -1
ret += datetime.timedelta(days=jumpdays)
return ret
def __rsub__(self, other):
return self.__neg__().__radd__(other)
def __add__(self, other):
if not isinstance(other, relativedelta):
raise TypeError, "unsupported type for add operation"
return relativedelta(years=other.years+self.years,
months=other.months+self.months,
days=other.days+self.days,
hours=other.hours+self.hours,
minutes=other.minutes+self.minutes,
seconds=other.seconds+self.seconds,
microseconds=other.microseconds+self.microseconds,
leapdays=other.leapdays or self.leapdays,
year=other.year or self.year,
month=other.month or self.month,
day=other.day or self.day,
weekday=other.weekday or self.weekday,
hour=other.hour or self.hour,
minute=other.minute or self.minute,
second=other.second or self.second,
microsecond=other.second or self.microsecond)
def __sub__(self, other):
if not isinstance(other, relativedelta):
raise TypeError, "unsupported type for sub operation"
return relativedelta(years=other.years-self.years,
months=other.months-self.months,
days=other.days-self.days,
hours=other.hours-self.hours,
minutes=other.minutes-self.minutes,
seconds=other.seconds-self.seconds,
microseconds=other.microseconds-self.microseconds,
leapdays=other.leapdays or self.leapdays,
year=other.year or self.year,
month=other.month or self.month,
day=other.day or self.day,
weekday=other.weekday or self.weekday,
hour=other.hour or self.hour,
minute=other.minute or self.minute,
second=other.second or self.second,
microsecond=other.second or self.microsecond)
def __neg__(self):
return relativedelta(years=-self.years,
months=-self.months,
days=-self.days,
hours=-self.hours,
minutes=-self.minutes,
seconds=-self.seconds,
microseconds=-self.microseconds,
leapdays=self.leapdays,
year=self.year,
month=self.month,
day=self.day,
weekday=self.weekday,
hour=self.hour,
minute=self.minute,
second=self.second,
microsecond=self.microsecond)
def __nonzero__(self):
return not (not self.years and
not self.months and
not self.days and
not self.hours and
not self.minutes and
not self.seconds and
not self.microseconds and
not self.leapdays and
self.year is None and
self.month is None and
self.day is None and
self.weekday is None and
self.hour is None and
self.minute is None and
self.second is None and
self.microsecond is None)
def __mul__(self, other):
f = float(other)
return relativedelta(years=self.years*f,
months=self.months*f,
days=self.days*f,
hours=self.hours*f,
minutes=self.minutes*f,
seconds=self.seconds*f,
microseconds=self.microseconds*f,
leapdays=self.leapdays,
year=self.year,
month=self.month,
day=self.day,
weekday=self.weekday,
hour=self.hour,
minute=self.minute,
second=self.second,
microsecond=self.microsecond)
def __eq__(self, other):
if not isinstance(other, relativedelta):
return False
if self.weekday or other.weekday:
if not self.weekday or not other.weekday:
return False
if self.weekday.weekday != other.weekday.weekday:
return False
n1, n2 = self.weekday.n, other.weekday.n
if n1 != n2 and not (n1 in (0, 1) and n2 in (0, 1)):
return False
return (self.years == other.years and
self.months == other.months and
self.days == other.days and
self.hours == other.hours and
self.minutes == other.minutes and
self.seconds == other.seconds and
self.leapdays == other.leapdays and
self.year == other.year and
self.month == other.month and
self.day == other.day and
self.hour == other.hour and
self.minute == other.minute and
self.second == other.second and
self.microsecond == other.microsecond)
def __ne__(self, other):
return not self.__eq__(other)
def __div__(self, other):
return self.__mul__(1/float(other))
def __repr__(self):
l = []
for attr in ["years", "months", "days", "leapdays",
"hours", "minutes", "seconds", "microseconds"]:
value = getattr(self, attr)
if value:
l.append("%s=%+d" % (attr, value))
for attr in ["year", "month", "day", "weekday",
"hour", "minute", "second", "microsecond"]:
value = getattr(self, attr)
if value is not None:
l.append("%s=%s" % (attr, `value`))
return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
# vim:ts=4:sw=4:et

View File

@ -1,883 +0,0 @@
"""
Copyright (c) 2003 Gustavo Niemeyer <niemeyer@conectiva.com>
This module offers extensions to the standard python 2.3+
datetime module.
"""
__author__ = "Gustavo Niemeyer <niemeyer@conectiva.com>"
__license__ = "PSF License"
import datetime
import struct
import time
relativedelta = None
parser = None
rrule = None
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile",
"tzrange", "tzstr", "tzical", "gettz"]
ZERO = datetime.timedelta(0)
EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal()
class tzutc(datetime.tzinfo):
def utcoffset(self, dt):
return ZERO
def dst(self, dt):
return ZERO
def tzname(self, dt):
return "UTC"
def __eq__(self, other):
return (isinstance(other, tzutc) or
(isinstance(other, tzoffset) and other._offset == ZERO))
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "%s()" % self.__class__.__name__
class tzoffset(datetime.tzinfo):
def __init__(self, name, offset):
self._name = name
self._offset = datetime.timedelta(seconds=offset)
def utcoffset(self, dt):
return self._offset
def dst(self, dt):
return ZERO
def tzname(self, dt):
return self._name
def __eq__(self, other):
return (isinstance(other, tzoffset) and
self._offset == other._offset)
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "%s(%s, %s)" % (self.__class__.__name__,
`self._name`,
self._offset.days*86400+self._offset.seconds)
class tzlocal(datetime.tzinfo):
_std_offset = datetime.timedelta(seconds=-time.timezone)
if time.daylight:
_dst_offset = datetime.timedelta(seconds=-time.altzone)
else:
_dst_offset = _std_offset
def utcoffset(self, dt):
if self._isdst(dt):
return self._dst_offset
else:
return self._std_offset
def dst(self, dt):
if self._isdst(dt):
return self._dst_offset-self._std_offset
else:
return ZERO
def tzname(self, dt):
return time.tzname[self._isdst(dt)]
def _isdst(self, dt):
# We can't use mktime here. It is unstable when deciding if
# the hour near to a change is DST or not.
#
# timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
# dt.minute, dt.second, dt.weekday(), 0, -1))
# return time.localtime(timestamp).tm_isdst
#
# The code above yields the following result:
#
#>>> import tz, datetime
#>>> t = tz.tzlocal()
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
#'BRDT'
#>>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
#'BRST'
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
#'BRST'
#>>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
#'BRDT'
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
#'BRDT'
#
# Here is a more stable implementation:
#
timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
+ dt.hour * 3600
+ dt.minute * 60
+ dt.second)
return time.localtime(timestamp+time.timezone).tm_isdst
def __eq__(self, other):
if not isinstance(other, tzlocal):
return False
return (self._std_offset == other._std_offset and
self._dst_offset == other._dst_offset)
return True
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "%s()" % self.__class__.__name__
class _ttinfo(object):
__slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"]
def __init__(self):
for attr in self.__slots__:
setattr(self, attr, None)
def __repr__(self):
l = []
for attr in self.__slots__:
value = getattr(self, attr)
if value is not None:
l.append("%s=%s" % (attr, `value`))
return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
def __eq__(self, other):
if not isinstance(other, _ttinfo):
return False
return (self.offset == other.offset and
self.delta == other.delta and
self.isdst == other.isdst and
self.abbr == other.abbr and
self.isstd == other.isstd and
self.isgmt == other.isgmt)
def __ne__(self, other):
return not self.__eq__(other)
class tzfile(datetime.tzinfo):
# http://www.twinsun.com/tz/tz-link.htm
# ftp://elsie.nci.nih.gov/pub/tz*.tar.gz
def __init__(self, fileobj):
if isinstance(fileobj, basestring):
self._s = fileobj
fileobj = open(fileobj)
elif hasattr(fileobj, "name"):
self._s = fileobj.name
else:
self._s = `fileobj`
# From tzfile(5):
#
# The time zone information files used by tzset(3)
# begin with the magic characters "TZif" to identify
# them as time zone information files, followed by
# sixteen bytes reserved for future use, followed by
# six four-byte values of type long, written in a
# ``standard'' byte order (the high-order byte
# of the value is written first).
if fileobj.read(4) != "TZif":
raise ValueError, "magic not found"
fileobj.read(16)
(
# The number of UTC/local indicators stored in the file.
ttisgmtcnt,
# The number of standard/wall indicators stored in the file.
ttisstdcnt,
# The number of leap seconds for which data is
# stored in the file.
leapcnt,
# The number of "transition times" for which data
# is stored in the file.
timecnt,
# The number of "local time types" for which data
# is stored in the file (must not be zero).
typecnt,
# The number of characters of "time zone
# abbreviation strings" stored in the file.
charcnt,
) = struct.unpack(">6l", fileobj.read(24))
# The above header is followed by tzh_timecnt four-byte
# values of type long, sorted in ascending order.
# These values are written in ``standard'' byte order.
# Each is used as a transition time (as returned by
# time(2)) at which the rules for computing local time
# change.
if timecnt:
self._trans_list = struct.unpack(">%dl" % timecnt,
fileobj.read(timecnt*4))
else:
self._trans_list = []
# Next come tzh_timecnt one-byte values of type unsigned
# char; each one tells which of the different types of
# ``local time'' types described in the file is associated
# with the same-indexed transition time. These values
# serve as indices into an array of ttinfo structures that
# appears next in the file.
if timecnt:
self._trans_idx = struct.unpack(">%dB" % timecnt,
fileobj.read(timecnt))
else:
self._trans_idx = []
# Each ttinfo structure is written as a four-byte value
# for tt_gmtoff of type long, in a standard byte
# order, followed by a one-byte value for tt_isdst
# and a one-byte value for tt_abbrind. In each
# structure, tt_gmtoff gives the number of
# seconds to be added to UTC, tt_isdst tells whether
# tm_isdst should be set by localtime(3), and
# tt_abbrind serves as an index into the array of
# time zone abbreviation characters that follow the
# ttinfo structure(s) in the file.
ttinfo = []
for i in range(typecnt):
ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
abbr = fileobj.read(charcnt)
# Then there are tzh_leapcnt pairs of four-byte
# values, written in standard byte order; the
# first value of each pair gives the time (as
# returned by time(2)) at which a leap second
# occurs; the second gives the total number of
# leap seconds to be applied after the given time.
# The pairs of values are sorted in ascending order
# by time.
# Not used, for now
if leapcnt:
leap = struct.unpack(">%dl" % leapcnt*2,
fileobj.read(leapcnt*8))
# Then there are tzh_ttisstdcnt standard/wall
# indicators, each stored as a one-byte value;
# they tell whether the transition times associated
# with local time types were specified as standard
# time or wall clock time, and are used when
# a time zone file is used in handling POSIX-style
# time zone environment variables.
if ttisstdcnt:
isstd = struct.unpack(">%db" % ttisstdcnt,
fileobj.read(ttisstdcnt))
# Finally, there are tzh_ttisgmtcnt UTC/local
# indicators, each stored as a one-byte value;
# they tell whether the transition times associated
# with local time types were specified as UTC or
# local time, and are used when a time zone file
# is used in handling POSIX-style time zone envi-
# ronment variables.
if ttisgmtcnt:
isgmt = struct.unpack(">%db" % ttisgmtcnt,
fileobj.read(ttisgmtcnt))
# ** Everything has been read **
# Build ttinfo list
self._ttinfo_list = []
for i in range(typecnt):
tti = _ttinfo()
tti.offset = ttinfo[i][0]
tti.delta = datetime.timedelta(seconds=ttinfo[i][0])
tti.isdst = ttinfo[i][1]
tti.abbr = abbr[ttinfo[i][2]:abbr.find('\x00', ttinfo[i][2])]
tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
self._ttinfo_list.append(tti)
# Replace ttinfo indexes for ttinfo objects.
trans_idx = []
for idx in self._trans_idx:
trans_idx.append(self._ttinfo_list[idx])
self._trans_idx = tuple(trans_idx)
# Set standard, dst, and before ttinfos. before will be
# used when a given time is before any transitions,
# and will be set to the first non-dst ttinfo, or to
# the first dst, if all of them are dst.
self._ttinfo_std = None
self._ttinfo_dst = None
self._ttinfo_before = None
if self._ttinfo_list:
if not self._trans_list:
self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0]
else:
for i in range(timecnt-1,-1,-1):
tti = self._trans_idx[i]
if not self._ttinfo_std and not tti.isdst:
self._ttinfo_std = tti
elif not self._ttinfo_dst and tti.isdst:
self._ttinfo_dst = tti
if self._ttinfo_std and self._ttinfo_dst:
break
else:
if self._ttinfo_dst and not self._ttinfo_std:
self._ttinfo_std = self._ttinfo_dst
for tti in self._ttinfo_list:
if not tti.isdst:
self._ttinfo_before = tti
break
else:
self._ttinfo_before = self._ttinfo_list[0]
# Now fix transition times to become relative to wall time.
#
# I'm not sure about this. In my tests, the tz source file
# is setup to wall time, and in the binary file isstd and
# isgmt are off, so it should be in wall time. OTOH, it's
# always in gmt time. Let me know if you have comments
# about this.
laststdoffset = 0
self._trans_list = list(self._trans_list)
for i in range(len(self._trans_list)):
tti = self._trans_idx[i]
if not tti.isdst:
# This is std time.
self._trans_list[i] += tti.offset
laststdoffset = tti.offset
else:
# This is dst time. Convert to std.
self._trans_list[i] += laststdoffset
self._trans_list = tuple(self._trans_list)
def _find_ttinfo(self, dt, laststd=0):
timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
+ dt.hour * 3600
+ dt.minute * 60
+ dt.second)
idx = 0
for trans in self._trans_list:
if timestamp < trans:
break
idx += 1
else:
return self._ttinfo_std
if idx == 0:
return self._ttinfo_before
if laststd:
while idx > 0:
tti = self._trans_idx[idx-1]
if not tti.isdst:
return tti
idx -= 1
else:
return self._ttinfo_std
else:
return self._trans_idx[idx-1]
def utcoffset(self, dt):
if not self._ttinfo_std:
return ZERO
return self._find_ttinfo(dt).delta
def dst(self, dt):
if not self._ttinfo_dst:
return ZERO
tti = self._find_ttinfo(dt)
if not tti.isdst:
return ZERO
# The documentation says that utcoffset()-dst() must
# be constant for every dt.
return self._find_ttinfo(dt, laststd=1).delta-tti.delta
# An alternative for that would be:
#
# return self._ttinfo_dst.offset-self._ttinfo_std.offset
#
# However, this class stores historical changes in the
# dst offset, so I belive that this wouldn't be the right
# way to implement this.
def tzname(self, dt):
if not self._ttinfo_std:
return None
return self._find_ttinfo(dt).abbr
def __eq__(self, other):
if not isinstance(other, tzfile):
return False
return (self._trans_list == other._trans_list and
self._trans_idx == other._trans_idx and
self._ttinfo_list == other._ttinfo_list)
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, `self._s`)
class tzrange(datetime.tzinfo):
def __init__(self, stdabbr, stdoffset=None,
dstabbr=None, dstoffset=None,
start=None, end=None):
global relativedelta
if not relativedelta:
from dateutil import relativedelta
self._std_abbr = stdabbr
self._dst_abbr = dstabbr
if stdoffset is not None:
self._std_offset = datetime.timedelta(seconds=stdoffset)
else:
self._std_offset = ZERO
if dstoffset is not None:
self._dst_offset = datetime.timedelta(seconds=dstoffset)
elif dstabbr and stdoffset is not None:
self._dst_offset = self._std_offset+datetime.timedelta(hours=+1)
else:
self._dst_offset = ZERO
if start is None:
self._start_delta = relativedelta.relativedelta(
hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
else:
self._start_delta = start
if end is None:
self._end_delta = relativedelta.relativedelta(
hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
else:
self._end_delta = end
def utcoffset(self, dt):
if self._isdst(dt):
return self._dst_offset
else:
return self._std_offset
def dst(self, dt):
if self._isdst(dt):
return self._dst_offset-self._std_offset
else:
return ZERO
def tzname(self, dt):
if self._isdst(dt):
return self._dst_abbr
else:
return self._std_abbr
def _isdst(self, dt):
if not self._start_delta:
return False
year = datetime.date(dt.year,1,1)
start = year+self._start_delta
end = year+self._end_delta
dt = dt.replace(tzinfo=None)
if start < end:
return dt >= start and dt < end
else:
return dt >= start or dt < end
def __eq__(self, other):
if not isinstance(other, tzrange):
return False
return (self._std_abbr == other._std_abbr and
self._dst_abbr == other._dst_abbr and
self._std_offset == other._std_offset and
self._dst_offset == other._dst_offset and
self._start_delta == other._start_delta and
self._end_delta == other._end_delta)
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "%s(...)" % self.__class__.__name__
class tzstr(tzrange):
def __init__(self, s):
global parser
if not parser:
from dateutil import parser
self._s = s
res = parser._parsetz(s)
if res is None:
raise ValueError, "unknown string format"
# We must initialize it first, since _delta() needs
# _std_offset and _dst_offset set. Use False in start/end
# to avoid building it two times.
tzrange.__init__(self, res.stdabbr, res.stdoffset,
res.dstabbr, res.dstoffset,
start=False, end=False)
self._start_delta = self._delta(res.start)
if self._start_delta:
self._end_delta = self._delta(res.end, isend=1)
def _delta(self, x, isend=0):
kwargs = {}
if x.month is not None:
kwargs["month"] = x.month
if x.weekday is not None:
kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
if x.week > 0:
kwargs["day"] = 1
else:
kwargs["day"] = 31
elif x.day:
kwargs["day"] = x.day
elif x.yday is not None:
kwargs["yearday"] = x.yday
elif x.jyday is not None:
kwargs["nlyearday"] = x.jyday
if not kwargs:
# Default is to start on first sunday of april, and end
# on last sunday of october.
if not isend:
kwargs["month"] = 4
kwargs["day"] = 1
kwargs["weekday"] = relativedelta.SU(+1)
else:
kwargs["month"] = 10
kwargs["day"] = 31
kwargs["weekday"] = relativedelta.SU(-1)
if x.time is not None:
kwargs["seconds"] = x.time
else:
# Default is 2AM.
kwargs["seconds"] = 7200
if isend:
# Convert to standard time, to follow the documented way
# of working with the extra hour. See the documentation
# of the tzinfo class.
delta = self._dst_offset-self._std_offset
kwargs["seconds"] -= delta.seconds+delta.days*86400
return relativedelta.relativedelta(**kwargs)
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, `self._s`)
class _tzicalvtzcomp:
def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
tzname=None, rrule=None):
self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom
self.isdst = isdst
self.tzname = tzname
self.rrule = rrule
class _tzicalvtz(datetime.tzinfo):
def __init__(self, tzid, comps=[]):
self._tzid = tzid
self._comps = comps
self._cachedate = []
self._cachecomp = []
def _find_comp(self, dt):
if len(self._comps) == 1:
return self._comps[0]
dt = dt.replace(tzinfo=None)
try:
return self._cachecomp[self._cachedate.index(dt)]
except ValueError:
pass
lastcomp = None
lastcompdt = None
for comp in self._comps:
if not comp.isdst:
# Handle the extra hour in DST -> STD
compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True)
else:
compdt = comp.rrule.before(dt, inc=True)
if compdt and (not lastcompdt or lastcompdt < compdt):
lastcompdt = compdt
lastcomp = comp
if not lastcomp:
# RFC says nothing about what to do when a given
# time is before the first onset date. We'll look for the
# first standard component, or the first component, if
# none is found.
for comp in self._comps:
if not comp.isdst:
lastcomp = comp
break
else:
lastcomp = comp[0]
self._cachedate.insert(0, dt)
self._cachecomp.insert(0, lastcomp)
if len(self._cachedate) > 10:
self._cachedate.pop()
self._cachecomp.pop()
return lastcomp
def utcoffset(self, dt):
return self._find_comp(dt).tzoffsetto
def dst(self, dt):
comp = self._find_comp(dt)
if comp.isdst:
return comp.tzoffsetdiff
else:
return ZERO
def tzname(self, dt):
return self._find_comp(dt).tzname
def __repr__(self):
return "<tzicalvtz %s>" % `self._tzid`
class tzical:
def __init__(self, fileobj):
global rrule
if not rrule:
from dateutil import rrule
if isinstance(fileobj, basestring):
self._s = fileobj
fileobj = open(fileobj)
elif hasattr(fileobj, "name"):
self._s = fileobj.name
else:
self._s = `fileobj`
self._vtz = {}
self._parse_rfc(fileobj.read())
def keys(self):
return self._vtz.keys()
def get(self, tzid=None):
if tzid is None:
keys = self._vtz.keys()
if len(keys) == 0:
raise "no timezones defined"
elif len(keys) > 1:
raise "more than one timezone available"
tzid = keys[0]
return self._vtz.get(tzid)
def _parse_offset(self, s):
s = s.strip()
if not s:
raise ValueError, "empty offset"
if s[0] in ('+', '-'):
signal = (-1,+1)[s[0]=='+']
s = s[1:]
else:
signal = +1
if len(s) == 4:
return (int(s[:2])*3600+int(s[2:])*60)*signal
elif len(s) == 6:
return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal
else:
raise ValueError, "invalid offset: "+s
def _parse_rfc(self, s):
lines = s.splitlines()
if not lines:
raise ValueError, "empty string"
# Unfold
i = 0
while i < len(lines):
line = lines[i].rstrip()
if not line:
del lines[i]
elif i > 0 and line[0] == " ":
lines[i-1] += line[1:]
del lines[i]
else:
i += 1
invtz = False
comptype = None
for line in lines:
if not line:
continue
name, value = line.split(':', 1)
parms = name.split(';')
if not parms:
raise ValueError, "empty property name"
name = parms[0].upper()
parms = parms[1:]
if invtz:
if name == "BEGIN":
if value in ("STANDARD", "DAYLIGHT"):
# Process component
pass
else:
raise ValueError, "unknown component: "+value
comptype = value
founddtstart = False
tzoffsetfrom = None
tzoffsetto = None
rrulelines = []
tzname = None
elif name == "END":
if value == "VTIMEZONE":
if comptype:
raise ValueError, \
"component not closed: "+comptype
if not tzid:
raise ValueError, \
"mandatory TZID not found"
if not comps:
raise ValueError, \
"at least one component is needed"
# Process vtimezone
self._vtz[tzid] = _tzicalvtz(tzid, comps)
invtz = False
elif value == comptype:
if not founddtstart:
raise ValueError, \
"mandatory DTSTART not found"
if not tzoffsetfrom:
raise ValueError, \
"mandatory TZOFFSETFROM not found"
if not tzoffsetto:
raise ValueError, \
"mandatory TZOFFSETFROM not found"
# Process component
rr = None
if rrulelines:
rr = rrule.rrulestr("\n".join(rrulelines),
compatible=True,
ignoretz=True,
cache=True)
comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
(comptype == "DAYLIGHT"),
tzname, rr)
comps.append(comp)
comptype = None
else:
raise ValueError, \
"invalid component end: "+value
elif comptype:
if name == "DTSTART":
rrulelines.append(line)
founddtstart = True
elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
rrulelines.append(line)
elif name == "TZOFFSETFROM":
if parms:
raise ValueError, \
"unsupported %s parm: %s "%(name, parms[0])
tzoffsetfrom = self._parse_offset(value)
elif name == "TZOFFSETTO":
if parms:
raise ValueError, \
"unsupported TZOFFSETTO parm: "+parms[0]
tzoffsetto = self._parse_offset(value)
elif name == "TZNAME":
if parms:
raise ValueError, \
"unsupported TZNAME parm: "+parms[0]
tzname = value
elif name == "COMMENT":
pass
else:
raise ValueError, "unsupported property: "+name
else:
if name == "TZID":
if parms:
raise ValueError, \
"unsupported TZID parm: "+parms[0]
tzid = value
elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
pass
else:
raise ValueError, "unsupported property: "+name
elif name == "BEGIN" and value == "VTIMEZONE":
tzid = None
comps = []
invtz = True
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, `self._s`)
TZFILES = ["/etc/localtime", "localtime"]
TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"]
import os
def gettz(name=None):
tz = None
if not name:
try:
name = os.environ["TZ"]
except KeyError:
pass
if name is None:
for filepath in TZFILES:
if not os.path.isabs(filepath):
filename = filepath
for path in TZPATHS:
filepath = os.path.join(path, filename)
if os.path.isfile(filepath):
break
else:
continue
if os.path.isfile(filepath):
try:
tz = tzfile(filepath)
break
except (IOError, OSError, ValueError):
pass
else:
if name and name[0] == ":":
name = name[:-1]
for path in TZPATHS:
filepath = os.path.join(path, name)
if not os.path.isfile(filepath):
filepath = filepath.replace(' ','_')
if not os.path.isfile(filepath):
continue
try:
tz = tzfile(filepath)
break
except (IOError, OSError, ValueError):
pass
else:
for c in name:
# name must have at least one offset to be a tzstr
if c in "0123456789":
try:
tz = tzstr(name)
except ValueError:
pass
break
else:
if name in ("GMT", "UTC"):
tz = tzutc()
elif name in time.tzname:
tz = tzlocal()
return tz
# vim:ts=4:sw=4:et

View File

@ -1,323 +0,0 @@
# Client for the DICT protocol (RFC2229)
#
# Copyright (C) 2002 John Goerzen
#
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# Retrieved from: http://gopher.quux.org:80/devel
import socket, re
version = '1.0'
def dequote(s):
"""Will remove single or double quotes from the start and end of a string
and return the result."""
quotechars = "'\""
while len(s) and s[0] in quotechars:
s = s[1:]
while len(s) and s[-1] in quotechars:
s = s[0:-1]
return s
def enquote(s):
"""This function will put a string in double quotes, properly
escaping any existing double quotes with a backslash. It will
return the result."""
return '"%s"' % s.replace('"', "\\\"")
class Connection:
"""This class is used to establish a connection to a database server.
You will usually use this as the first call into the dictclient library.
Instantiating it takes two optional arguments: a hostname (a string)
and a port (an int). The hostname defaults to localhost
and the port to 2628, the port specified in RFC."""
def __init__(self, hostname='localhost', port=2628):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((hostname, port))
self.rfile = self.sock.makefile("rt")
self.wfile = self.sock.makefile("wt", 0)
self.saveconnectioninfo()
def getresultcode(self):
"""Generic function to get a result code. It will return a list
consisting of two items: the integer result code and the text
following. You will not usually use this function directly."""
line = self.rfile.readline().strip()
code, text = line.split(' ', 1)
return [int(code), text]
def get200result(self):
"""Used when expecting a single line of text -- a 200-class
result. Returns [intcode, remaindertext]"""
code, text = self.getresultcode()
if code < 200 or code >= 300:
raise Exception, "Got '%s' when 200-class response expected" % \
line
return [code, text]
def get100block(self):
"""Used when expecting multiple lines of text -- gets the block
part only. Does not get any codes or anything! Returns a string."""
data = []
while 1:
line = self.rfile.readline().strip()
if line == '.':
break
data.append(line)
return "\n".join(data)
def get100result(self):
"""Used when expecting multiple lines of text, terminated by a period
and a 200 code. Returns: [initialcode, [bodytext_1lineperentry],
finalcode]"""
code, text = self.getresultcode()
if code < 100 or code >= 200:
raise Exception, "Got '%s' when 100-class response expected" % \
code
bodylines = self.get100block().split("\n")
code2 = self.get200result()[0]
return [code, bodylines, code2]
def get100dict(self):
"""Used when expecting a dictionary of results. Will read from
the initial 100 code, to a period and the 200 code."""
dict = {}
for line in self.get100result()[1]:
key, val = line.split(' ', 1)
dict[key] = dequote(val)
return dict
def saveconnectioninfo(self):
"""Called by __init__ to handle the initial connection. Will
save off the capabilities and messageid."""
code, string = self.get200result()
assert code == 220
m = re.search('<(.*)> (<.*>)$', string)
assert m is not None
capstr, msgid = m.groups()
self.capabilities = capstr.split('.')
self.messageid = msgid
def getcapabilities(self):
"""Returns a list of the capabilities advertised by the server."""
return self.capabilities
def getmessageid(self):
"""Returns the message id, including angle brackets."""
return self.messageid
def getdbdescs(self):
"""Gets a dict of available databases. The key is the db name
and the value is the db description. This command may generate
network traffic!"""
if hasattr(self, 'dbdescs'):
return self.dbdescs
self.sendcommand("SHOW DB")
self.dbdescs = self.get100dict()
return self.dbdescs
def getstratdescs(self):
"""Gets a dict of available strategies. The key is the strat
name and the value is the strat description. This call may
generate network traffic!"""
if hasattr(self, 'stratdescs'):
return self.stratdescs
self.sendcommand("SHOW STRAT")
self.stratdescs = self.get100dict()
return self.stratdescs
def getdbobj(self, dbname):
"""Gets a Database object corresponding to the database name passed
in. This function explicitly will *not* generate network traffic.
If you have not yet run getdbdescs(), it will fail."""
if not hasattr(self, 'dbobjs'):
self.dbobjs = {}
if self.dbobjs.has_key(dbname):
return self.dbobjs[dbname]
# We use self.dbdescs explicitly since we don't want to
# generate net traffic with this request!
if dbname != '*' and dbname != '!' and \
not dbname in self.dbdescs.keys():
raise Exception, "Invalid database name '%s'" % dbname
self.dbobjs[dbname] = Database(self, dbname)
return self.dbobjs[dbname]
def sendcommand(self, command):
"""Takes a command, without a newline character, and sends it to
the server."""
self.wfile.write(command + "\n")
def define(self, database, word):
"""Returns a list of Definition objects for each matching
definition. Parameters are the database name and the word
to look up. This is one of the main functions you will use
to interact with the server. Returns a list of Definition
objects. If there are no matches, an empty list is returned.
Note: database may be '*' which means to search all databases,
or '!' which means to return matches from the first database that
has a match."""
self.getdbdescs() # Prime the cache
if database != '*' and database != '!' and \
not database in self.getdbdescs():
raise Exception, "Invalid database '%s' specified" % database
self.sendcommand("DEFINE " + enquote(database) + " " + enquote(word))
code = self.getresultcode()[0]
retval = []
if code == 552:
# No definitions.
return []
if code != 150:
raise Exception, "Unknown code %d" % code
while 1:
code, text = self.getresultcode()
if code != 151:
break
m = re.search('^"(.+)" (\S+)', text)
assert m is not None
resultword, resultdb = m.groups()
defstr = self.get100block()
retval.append(Definition(self, self.getdbobj(resultdb),
resultword, defstr))
return retval
def match(self, database, strategy, word):
"""Gets matches for a query. Arguments are database name,
the strategy (see available ones in getstratdescs()), and the
pattern/word to look for. Returns a list of Definition objects.
If there is no match, an empty list is returned.
Note: database may be '*' which means to search all databases,
or '!' which means to return matches from the first database that
has a match."""
self.getstratdescs() # Prime the cache
self.getdbdescs() # Prime the cache
if not strategy in self.getstratdescs().keys():
raise Exception, "Invalid strategy '%s'" % strategy
if database != '*' and database != '!' and \
not database in self.getdbdescs().keys():
raise Exception, "Invalid database name '%s'" % database
self.sendcommand("MATCH %s %s %s" % (enquote(database),
enquote(strategy),
enquote(word)))
code = self.getresultcode()[0]
if code == 552:
# No Matches
return []
if code != 152:
raise Exception, "Unexpected code %d" % code
retval = []
for matchline in self.get100block().split("\n"):
matchdict, matchword = matchline.split(" ", 1)
retval.append(Definition(self, self.getdbobj(matchdict),
dequote(matchword)))
if self.getresultcode()[0] != 250:
raise Exception, "Unexpected end-of-list code %d" % code
return retval
class Database:
"""An object corresponding to a particular database in a server."""
def __init__(self, dictconn, dbname):
"""Initialize the object -- requires a Connection object and
a database name."""
self.conn = dictconn
self.name = dbname
def getname(self):
"""Returns the short name for this database."""
return self.name
def getdescription(self):
if hasattr(self, 'description'):
return self.description
if self.getname() == '*':
self.description = 'All Databases'
elif self.getname() == '!':
self.description = 'First matching database'
else:
self.description = self.conn.getdbdescs()[self.getname()]
return self.description
def getinfo(self):
"""Returns a string of info describing this database."""
if hasattr(self, 'info'):
return self.info
if self.getname() == '*':
self.info = "This special database will search all databases on the system."
elif self.getname() == '!':
self.info = "This special database will return matches from the first matching database."
else:
self.conn.sendcommand("SHOW INFO " + self.name)
self.info = "\n".join(self.conn.get100result()[1])
return self.info
def define(self, word):
"""Get a definition from within this database.
The argument, word, is the word to look up. The return value is the
same as from Connection.define()."""
return self.conn.define(self.getname(), word)
def match(self, strategy, word):
"""Get a match from within this database.
The argument, word, is the word to look up. The return value is
the same as from Connection.define()."""
return self.conn.match(self.getname(), strategy, word)
class Definition:
"""An object corresponding to a single definition."""
def __init__(self, dictconn, db, word, defstr = None):
"""Instantiate the object. Requires: a Connection object,
a Database object (NOT corresponding to '*' or '!' databases),
a word. Optional: a definition string. If not supplied,
it will be fetched if/when it is requested."""
self.conn = dictconn
self.db = db
self.word = word
self.defstr = defstr
def getdb(self):
"""Get the Database object corresponding to this definition."""
return self.db
def getdefstr(self):
"""Get the definition string (the actual content) of this
definition."""
if not self.defstr:
self.defstr = self.conn.define(self.getdb().getname(), self.word)[0].getdefstr()
return self.defstr
def getword(self):
"""Get the word this object describes."""
return self.word

View File

@ -1,163 +0,0 @@
"""Utilities for handling IEEE 754 floating point special values
This python module implements constants and functions for working with
IEEE754 double-precision special values. It provides constants for
Not-a-Number (NaN), Positive Infinity (PosInf), and Negative Infinity
(NegInf), as well as functions to test for these values.
The code is implemented in pure python by taking advantage of the
'struct' standard module. Care has been taken to generate proper
results on both big-endian and little-endian machines. Some efficiency
could be gained by translating the core routines into C.
See <http://babbage.cs.qc.edu/courses/cs341/IEEE-754references.html>
for reference material on the IEEE 754 floating point standard.
Further information on this package is available at
<http://www.analytics.washington.edu/statcomp/projects/rzope/fpconst/>.
Author: Gregory R. Warnes <gregory_r_warnes@groton.pfizer.com>
Date:: 2003-04-08
Copyright: (c) 2003, Pfizer, Inc.
"""
__version__ = "0.7.0"
ident = "$Id$"
import struct, operator
# check endianess
_big_endian = struct.pack('i',1)[0] != '\x01'
# and define appropriate constants
if(_big_endian):
NaN = struct.unpack('d', '\x7F\xF8\x00\x00\x00\x00\x00\x00')[0]
PosInf = struct.unpack('d', '\x7F\xF0\x00\x00\x00\x00\x00\x00')[0]
NegInf = -PosInf
else:
NaN = struct.unpack('d', '\x00\x00\x00\x00\x00\x00\xf8\xff')[0]
PosInf = struct.unpack('d', '\x00\x00\x00\x00\x00\x00\xf0\x7f')[0]
NegInf = -PosInf
def _double_as_bytes(dval):
"Use struct.unpack to decode a double precision float into eight bytes"
tmp = list(struct.unpack('8B',struct.pack('d', dval)))
if not _big_endian:
tmp.reverse()
return tmp
##
## Functions to extract components of the IEEE 754 floating point format
##
def _sign(dval):
"Extract the sign bit from a double-precision floating point value"
bb = _double_as_bytes(dval)
return bb[0] >> 7 & 0x01
def _exponent(dval):
"""Extract the exponentent bits from a double-precision floating
point value.
Note that for normalized values, the exponent bits have an offset
of 1023. As a consequence, the actual exponentent is obtained
by subtracting 1023 from the value returned by this function
"""
bb = _double_as_bytes(dval)
return (bb[0] << 4 | bb[1] >> 4) & 0x7ff
def _mantissa(dval):
"""Extract the _mantissa bits from a double-precision floating
point value."""
bb = _double_as_bytes(dval)
mantissa = bb[1] & 0x0f << 48
mantissa += bb[2] << 40
mantissa += bb[3] << 32
mantissa += bb[4]
return mantissa
def _zero_mantissa(dval):
"""Determine whether the mantissa bits of the given double are all
zero."""
bb = _double_as_bytes(dval)
return ((bb[1] & 0x0f) | reduce(operator.or_, bb[2:])) == 0
##
## Functions to test for IEEE 754 special values
##
def isNaN(value):
"Determine if the argument is a IEEE 754 NaN (Not a Number) value."
return (_exponent(value)==0x7ff and not _zero_mantissa(value))
def isInf(value):
"""Determine if the argument is an infinite IEEE 754 value (positive
or negative inifinity)"""
return (_exponent(value)==0x7ff and _zero_mantissa(value))
def isFinite(value):
"""Determine if the argument is an finite IEEE 754 value (i.e., is
not NaN, positive or negative inifinity)"""
return (_exponent(value)!=0x7ff)
def isPosInf(value):
"Determine if the argument is a IEEE 754 positive infinity value"
return (_sign(value)==0 and _exponent(value)==0x7ff and \
_zero_mantissa(value))
def isNegInf(value):
"Determine if the argument is a IEEE 754 negative infinity value"
return (_sign(value)==1 and _exponent(value)==0x7ff and \
_zero_mantissa(value))
##
## Functions to test public functions.
##
def test_isNaN():
assert( not isNaN(PosInf) )
assert( not isNaN(NegInf) )
assert( isNaN(NaN ) )
assert( not isNaN( 1.0) )
assert( not isNaN( -1.0) )
def test_isInf():
assert( isInf(PosInf) )
assert( isInf(NegInf) )
assert( not isInf(NaN ) )
assert( not isInf( 1.0) )
assert( not isInf( -1.0) )
def test_isFinite():
assert( not isFinite(PosInf) )
assert( not isFinite(NegInf) )
assert( not isFinite(NaN ) )
assert( isFinite( 1.0) )
assert( isFinite( -1.0) )
def test_isPosInf():
assert( isPosInf(PosInf) )
assert( not isPosInf(NegInf) )
assert( not isPosInf(NaN ) )
assert( not isPosInf( 1.0) )
assert( not isPosInf( -1.0) )
def test_isNegInf():
assert( not isNegInf(PosInf) )
assert( isNegInf(NegInf) )
assert( not isNegInf(NaN ) )
assert( not isNegInf( 1.0) )
assert( not isNegInf( -1.0) )
# overall test
def test():
test_isNaN()
test_isInf()
test_isFinite()
test_isPosInf()
test_isNegInf()
if __name__ == "__main__":
test()

View File

@ -1,638 +0,0 @@
"""
Python wrapper for Google web APIs
This module allows you to access Google's web APIs through SOAP,
to do things like search Google and get the results programmatically.
Described U{here <http://www.google.com/apis/>}
You need a Google-provided license key to use these services.
Follow the link above to get one. These functions will look in
several places (in this order) for the license key:
- the "license_key" argument of each function
- the module-level LICENSE_KEY variable (call setLicense once to set it)
- an environment variable called GOOGLE_LICENSE_KEY
- a file called ".googlekey" in the current directory
- a file called "googlekey.txt" in the current directory
- a file called ".googlekey" in your home directory
- a file called "googlekey.txt" in your home directory
- a file called ".googlekey" in the same directory as google.py
- a file called "googlekey.txt" in the same directory as google.py
Sample usage::
>>> import google
>>> google.setLicense('...') # must get your own key!
>>> data = google.doGoogleSearch('python')
>>> data.meta.searchTime
0.043221000000000002
>>> data.results[0].URL
'http://www.python.org/'
>>> data.results[0].title
'<b>Python</b> Language Website'
@newfield contrib: Contributors
@author: Mark Pilgrim <f8dy@diveintomark.org>
@author: Brian Landers <brian@bluecoat93.org>
@license: Python
@version: 0.6
@contrib: David Ascher, for the install script
@contrib: Erik Max Francis, for the command line interface
@contrib: Michael Twomey, for HTTP proxy support
@contrib: Mark Recht, for patches to support SOAPpy
"""
__author__ = "Mark Pilgrim (f8dy@diveintomark.org)"
__version__ = "0.6"
__cvsversion__ = "$Revision$"[11:-2]
__date__ = "$Date$"[7:-2]
__copyright__ = "Copyright (c) 2002 Mark Pilgrim"
__license__ = "Python"
__credits__ = """David Ascher, for the install script
Erik Max Francis, for the command line interface
Michael Twomey, for HTTP proxy support"""
import os, sys, getopt
import GoogleSOAPFacade
LICENSE_KEY = None
HTTP_PROXY = None
#
# Constants
#
_url = 'http://api.google.com/search/beta2'
_namespace = 'urn:GoogleSearch'
_googlefile1 = ".googlekey"
_googlefile2 = "googlekey.txt"
_false = GoogleSOAPFacade.false
_true = GoogleSOAPFacade.true
_licenseLocations = (
( lambda key: key,
'passed to the function in license_key variable' ),
( lambda key: LICENSE_KEY,
'module-level LICENSE_KEY variable (call setLicense to set it)' ),
( lambda key: os.environ.get( 'GOOGLE_LICENSE_KEY', None ),
'an environment variable called GOOGLE_LICENSE_KEY' ),
( lambda key: _contentsOf( os.getcwd(), _googlefile1 ),
'%s in the current directory' % _googlefile1),
( lambda key: _contentsOf( os.getcwd(), _googlefile2 ),
'%s in the current directory' % _googlefile2),
( lambda key: _contentsOf( os.environ.get( 'HOME', '' ), _googlefile1 ),
'%s in your home directory' % _googlefile1),
( lambda key: _contentsOf( os.environ.get( 'HOME', '' ), _googlefile2 ),
'%s in your home directory' % _googlefile2 ),
( lambda key: _contentsOf( _getScriptDir(), _googlefile1 ),
'%s in the google.py directory' % _googlefile1 ),
( lambda key: _contentsOf( _getScriptDir(), _googlefile2 ),
'%s in the google.py directory' % _googlefile2 )
)
## ----------------------------------------------------------------------
## Exceptions
## ----------------------------------------------------------------------
class NoLicenseKey(Exception):
"""
Thrown when the API is unable to find a valid license key.
"""
pass
## ----------------------------------------------------------------------
## administrative functions (non-API)
## ----------------------------------------------------------------------
def _version():
"""
Display a formatted version string for the module
"""
print """PyGoogle %(__version__)s
%(__copyright__)s
released %(__date__)s
Thanks to:
%(__credits__)s""" % globals()
def _usage():
"""
Display usage information for the command-line interface
"""
program = os.path.basename(sys.argv[0])
print """Usage: %(program)s [options] [querytype] query
options:
-k, --key= <license key> Google license key (see important note below)
-1, -l, --lucky show only first hit
-m, --meta show meta information
-r, --reverse show results in reverse order
-x, --proxy= <url> use HTTP proxy
-h, --help print this help
-v, --version print version and copyright information
-t, --test run test queries
querytype:
-s, --search= <query> search (default)
-c, --cache= <url> retrieve cached page
-p, --spelling= <word> check spelling
IMPORTANT NOTE: all Google functions require a valid license key;
visit http://www.google.com/apis/ to get one. %(program)s will look in
these places (in order) and use the first license key it finds:
* the key specified on the command line""" % vars()
for get, location in _licenseLocations[2:]:
print " *", location
## ----------------------------------------------------------------------
## utility functions (API)
## ----------------------------------------------------------------------
def setLicense(license_key):
"""
Set the U{Google APIs <http://www.google.com/api>} license key
@param license_key: The new key to use
@type license_key: String
@todo: validate the key?
"""
global LICENSE_KEY
LICENSE_KEY = license_key
def getLicense(license_key = None):
"""
Get the U{Google APIs <http://www.google.com/api>} license key
The key can be read from any number of locations. See the module-leve
documentation for the search order.
@return: the license key
@rtype: String
@raise NoLicenseKey: if no valid key could be found
"""
for get, location in _licenseLocations:
rc = get(license_key)
if rc: return rc
_usage()
raise NoLicenseKey, 'get a license key at http://www.google.com/apis/'
def setProxy(http_proxy):
"""
Set the HTTP proxy to be used when accessing Google
@param http_proxy: the proxy to use
@type http_proxy: String
@todo: validiate the input?
"""
global HTTP_PROXY
HTTP_PROXY = http_proxy
def getProxy(http_proxy = None):
"""
Get the HTTP proxy we use for accessing Google
@return: the proxy
@rtype: String
"""
return http_proxy or HTTP_PROXY
def _contentsOf(dirname, filename):
filename = os.path.join(dirname, filename)
if not os.path.exists(filename): return None
fsock = open(filename)
contents = fsock.read()
fsock.close()
return contents
def _getScriptDir():
if __name__ == '__main__':
return os.path.abspath(os.path.dirname(sys.argv[0]))
else:
return os.path.abspath(os.path.dirname(sys.modules[__name__].__file__))
def _marshalBoolean(value):
if value:
return _true
else:
return _false
def _getRemoteServer( http_proxy ):
return GoogleSOAPFacade.getProxy( _url, _namespace, http_proxy )
## ----------------------------------------------------------------------
## search results classes
## ----------------------------------------------------------------------
class _SearchBase:
def __init__(self, params):
for k, v in params.items():
if isinstance(v, GoogleSOAPFacade.structType):
v = GoogleSOAPFacade.toDict( v )
try:
if isinstance(v[0], GoogleSOAPFacade.structType):
v = [ SOAPProxy.toDict( node ) for node in v ]
except:
pass
self.__dict__[str(k)] = v
## ----------------------------------------------------------------------
class SearchResultsMetaData(_SearchBase):
"""
Container class for metadata about a given search query's results.
@ivar documentFiltering: is duplicate page filtering active?
@ivar searchComments: human-readable informational message
example::
"'the' is a very common word and was not included in your search"
@ivar estimatedTotalResultsCount: estimated total number of results
for this query.
@ivar estimateIsExact: is estimatedTotalResultsCount an exact value?
@ivar searchQuery: search string that initiated this search
@ivar startIndex: index of the first result returned (zero-based)
@ivar endIndex: index of the last result returned (zero-based)
@ivar searchTips: human-readable informational message on how to better
use Google.
@ivar directoryCategories: list of categories for the search results
This field is a list of dictionaries, like so::
{ 'fullViewableName': 'the Open Directory category',
'specialEncoding': 'encoding scheme of this directory category'
}
@ivar searchTime: total search time, in seconds
"""
pass
## ----------------------------------------------------------------------
class SearchResult(_SearchBase):
"""
Encapsulates the results from a search.
@ivar URL: URL
@ivar title: title (HTML)
@ivar snippet: snippet showing query context (HTML
@ivar cachedSize: size of cached version of this result, (KB)
@ivar relatedInformationPresent: is the "related:" keyword supported?
Flag indicates that the "related:" keyword is supported for this URL
@ivar hostName: used when filtering occurs
When filtering occurs, a maximum of two results from any given
host is returned. When this occurs, the second resultElement
that comes from that host contains the host name in this parameter.
@ivar directoryCategory: Open Directory category information
This field is a dictionary with the following values::
{ 'fullViewableName': 'the Open Directory category',
'specialEncoding' : 'encoding scheme of this directory category'
}
@ivar directoryTitle: Open Directory title of this result (or blank)
@ivar summary: Open Directory summary for this result (or blank)
"""
pass
## ----------------------------------------------------------------------
class SearchReturnValue:
"""
complete search results for a single query
@ivar meta: L{SearchResultsMetaData} instance for this query
@ivar results: list of L{SearchResult} objects for this query
"""
def __init__( self, metadata, results ):
self.meta = metadata
self.results = results
## ----------------------------------------------------------------------
## main functions
## ----------------------------------------------------------------------
def doGoogleSearch( q, start = 0, maxResults = 10, filter = 1,
restrict='', safeSearch = 0, language = '',
inputencoding = '', outputencoding = '',\
license_key = None, http_proxy = None ):
"""
Search Google using the SOAP API and return the results.
You need a license key to call this function; see the
U{Google APIs <http://www.google.com/apis/>} site to get one.
Then you can either pass it to this function every time, or
set it globally; see the L{google} module-level docs for details.
See U{http://www.google.com/help/features.html}
for examples of advanced features. Anything that works at the
Google web site will work as a query string in this method.
You can use the C{start} and C{maxResults} parameters to page
through multiple pages of results. Note that 'maxResults' is
currently limited by Google to 10.
See the API reference for more advanced examples and a full list of
country codes and topics for use in the C{restrict} parameter, along
with legal values for the C{language}, C{inputencoding}, and
C{outputencoding} parameters.
You can download the API documentation
U{http://www.google.com/apis/download.html <here>}.
@param q: search string.
@type q: String
@param start: (optional) zero-based index of first desired result.
@type start: int
@param maxResults: (optional) maximum number of results to return.
@type maxResults: int
@param filter: (optional) flag to request filtering of similar results
@type filter: int
@param restrict: (optional) restrict results by country or topic.
@type restrict: String
@param safeSearch: (optional)
@type safeSearch: int
@param language: (optional)
@type language: String
@param inputencoding: (optional)
@type inputencoding: String
@param outputencoding: (optional)
@type outputencoding: String
@param license_key: (optional) the Google API license key to use
@type license_key: String
@param http_proxy: (optional) the HTTP proxy to use for talking to Google
@type http_proxy: String
@return: the search results encapsulated in an object
@rtype: L{SearchReturnValue}
"""
license_key = getLicense( license_key )
http_proxy = getProxy( http_proxy )
remoteserver = _getRemoteServer( http_proxy )
filter = _marshalBoolean( filter )
safeSearch = _marshalBoolean( safeSearch )
data = remoteserver.doGoogleSearch( license_key, q, start, maxResults,
filter, restrict, safeSearch,
language, inputencoding,
outputencoding )
metadata = GoogleSOAPFacade.toDict( data )
del metadata["resultElements"]
metadata = SearchResultsMetaData( metadata )
results = [ SearchResult( GoogleSOAPFacade.toDict( node ) ) \
for node in data.resultElements ]
return SearchReturnValue( metadata, results )
## ----------------------------------------------------------------------
def doGetCachedPage( url, license_key = None, http_proxy = None ):
"""
Retrieve a page from the Google cache.
You need a license key to call this function; see the
U{Google APIs <http://www.google.com/apis/>} site to get one.
Then you can either pass it to this function every time, or
set it globally; see the L{google} module-level docs for details.
@param url: full URL to the page to retrieve
@type url: String
@param license_key: (optional) the Google API key to use
@type license_key: String
@param http_proxy: (optional) the HTTP proxy server to use
@type http_proxy: String
@return: full text of the cached page
@rtype: String
"""
license_key = getLicense( license_key )
http_proxy = getProxy( http_proxy )
remoteserver = _getRemoteServer( http_proxy )
return remoteserver.doGetCachedPage( license_key, url )
## ----------------------------------------------------------------------
def doSpellingSuggestion( phrase, license_key = None, http_proxy = None ):
"""
Get spelling suggestions from Google
You need a license key to call this function; see the
U{Google APIs <http://www.google.com/apis/>} site to get one.
Then you can either pass it to this function every time, or
set it globally; see the L{google} module-level docs for details.
@param phrase: word or phrase to spell-check
@type phrase: String
@param license_key: (optional) the Google API key to use
@type license_key: String
@param http_proxy: (optional) the HTTP proxy to use
@type http_proxy: String
@return: text of any suggested replacement, or None
"""
license_key = getLicense( license_key )
http_proxy = getProxy( http_proxy)
remoteserver = _getRemoteServer( http_proxy )
return remoteserver.doSpellingSuggestion( license_key, phrase )
## ----------------------------------------------------------------------
## functional test suite (see googletest.py for unit test suite)
## ----------------------------------------------------------------------
def _test():
"""
Run functional test suite.
"""
try:
getLicense(None)
except NoLicenseKey:
return
print "Searching for Python at google.com..."
data = doGoogleSearch( "Python" )
_output( data, { "func": "doGoogleSearch"} )
print "\nSearching for 5 _French_ pages about Python, "
print "encoded in ISO-8859-1..."
data = doGoogleSearch( "Python", language = 'lang_fr',
outputencoding = 'ISO-8859-1',
maxResults = 5 )
_output( data, { "func": "doGoogleSearch" } )
phrase = "Pyhton programming languager"
print "\nTesting spelling suggestions for '%s'..." % phrase
data = doSpellingSuggestion( phrase )
_output( data, { "func": "doSpellingSuggestion" } )
## ----------------------------------------------------------------------
## Command-line interface
## ----------------------------------------------------------------------
class _OutputFormatter:
def boil(self, data):
if type(data) == type(u""):
return data.encode("ISO-8859-1", "replace")
else:
return data
class _TextOutputFormatter(_OutputFormatter):
def common(self, data, params):
if params.get("showMeta", 0):
meta = data.meta
for category in meta.directoryCategories:
print "directoryCategory: %s" % \
self.boil(category["fullViewableName"])
for attr in [node for node in dir(meta) if \
node <> "directoryCategories" and node[:2] <> '__']:
print "%s:" % attr, self.boil(getattr(meta, attr))
def doGoogleSearch(self, data, params):
results = data.results
if params.get("feelingLucky", 0):
results = results[:1]
if params.get("reverseOrder", 0):
results.reverse()
for result in results:
for attr in dir(result):
if attr == "directoryCategory":
print "directoryCategory:", \
self.boil(result.directoryCategory["fullViewableName"])
elif attr[:2] <> '__':
print "%s:" % attr, self.boil(getattr(result, attr))
print
self.common(data, params)
def doGetCachedPage(self, data, params):
print data
self.common(data, params)
doSpellingSuggestion = doGetCachedPage
def _makeFormatter(outputFormat):
classname = "_%sOutputFormatter" % outputFormat.capitalize()
return globals()[classname]()
def _output(results, params):
formatter = _makeFormatter(params.get("outputFormat", "text"))
outputmethod = getattr(formatter, params["func"])
outputmethod(results, params)
def main(argv):
"""
Command-line interface.
"""
if not argv:
_usage()
return
q = None
func = None
http_proxy = None
license_key = None
feelingLucky = 0
showMeta = 0
reverseOrder = 0
runTest = 0
outputFormat = "text"
try:
opts, args = getopt.getopt(argv, "s:c:p:k:lmrx:hvt1",
["search=", "cache=", "spelling=", "key=", "lucky", "meta",
"reverse", "proxy=", "help", "version", "test"])
except getopt.GetoptError:
_usage()
sys.exit(2)
for opt, arg in opts:
if opt in ("-s", "--search"):
q = arg
func = "doGoogleSearch"
elif opt in ("-c", "--cache"):
q = arg
func = "doGetCachedPage"
elif opt in ("-p", "--spelling"):
q = arg
func = "doSpellingSuggestion"
elif opt in ("-k", "--key"):
license_key = arg
elif opt in ("-l", "-1", "--lucky"):
feelingLucky = 1
elif opt in ("-m", "--meta"):
showMeta = 1
elif opt in ("-r", "--reverse"):
reverseOrder = 1
elif opt in ("-x", "--proxy"):
http_proxy = arg
elif opt in ("-h", "--help"):
_usage()
elif opt in ("-v", "--version"):
_version()
elif opt in ("-t", "--test"):
runTest = 1
if runTest:
setLicense(license_key)
setProxy(http_proxy)
_test()
if args and not q:
q = args[0]
func = "doGoogleSearch"
if func:
results = globals()[func]( q, http_proxy=http_proxy,
license_key=license_key )
_output(results, locals())
if __name__ == '__main__':
main(sys.argv[1:])

View File

@ -1,193 +0,0 @@
###
# Copyright (c) 2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import supybot.fix
# Constants.
S = 's'
C = 'c'
H = 'h'
D = 'd'
class Rank(int):
def __str__(self):
if 2 <= self <= 10:
return str(self)
elif self == 11:
return 'J'
elif self == 12:
return 'Q'
elif self == 13:
return 'K'
elif self == 14:
return 'A'
elif self == 1:
return 'A'
A = Rank(14)
K = Rank(13)
Q = Rank(12)
J = Rank(11)
class Card(object):
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __str__(self):
return '%s%s' % (self.rank, self.suit)
__repr__ = __str__
def __cmp__(self, other):
return cmp(self.rank, other.rank)
def combinations(L, n):
if len(L) >= n:
if n == 0:
yield []
else:
(first, rest) = (L[:1], L[1:])
for miniL in combinations(rest, n-1):
yield first + miniL
for miniL in combinations(rest, n):
yield miniL
def sort(hand):
hand.sort()
hand.reverse()
return hand
def getFlush(hand):
if all(lambda c: c.suit == hand[0].suit, hand):
return sort(hand)
return []
def getStraight(hand):
sort(hand)
diffs = [x.rank-y.rank for (x,y) in window(hand,2)]
if diffs == [1,1,1,1]:
return hand
elif hand[0].rank == A and diffs == [9,1,1,1]:
ace = hand.pop(0)
hand.append(ace)
return hand
return []
def getPair(hand):
sort(hand)
for (x, y) in window(hand, 2):
if x.rank == y.rank:
return ([x,y], [c for c in hand if c.rank != x.rank])
return ([], hand)
def getTrips(hand):
sort(hand)
for (x, y, z) in window(hand, 3):
if x.rank == y.rank == z.rank:
return ([x,y,z], [c for c in hand if c.rank != x.rank])
return ([], hand)
def getFours(hand):
sort(hand)
for (x, y, z, w) in window(hand, 4):
if x.rank == y.rank == z.rank == w.rank:
return ([x,y,z,w], [c for c in hand if c.rank != x.rank])
return ([], hand)
STRAIGHT_FLUSH = '8:straight flush'
FOUR_OF_A_KIND = '7:four of a kind'
FULL_HOUSE = '6:full house'
FLUSH = '5:flush'
STRAIGHT = '4:straight'
THREE_OF_A_KIND = '3:three of a kind'
TWO_PAIR = '2:two pair'
PAIR = '1:pair'
RUNT = '0:runt'
def score(cards):
"""Returns a comparable value for a list of cards."""
def getRank(hand):
assert len(hand) == 5
(pair, pairRest) = getPair(hand)
if pair:
# Can't be flushes or straights.
(trips, tripRest) = getTrips(hand)
if trips:
(fours, fourRest) = getFours(hand)
if fours:
return (FOUR_OF_A_KIND, fours + fourRest)
(pair, _) = getPair(tripRest)
if pair:
# Full house.
return (FULL_HOUSE, trips + pair)
sort(tripRest)
return (THREE_OF_A_KIND, trips + tripRest)
(otherPair, twoPairRest) = getPair(pairRest)
if otherPair:
if otherPair[0] > pair[0]:
return (TWO_PAIR, otherPair + pair + twoPairRest)
else:
return (TWO_PAIR, pair + otherPair + twoPairRest)
sort(pairRest)
return (PAIR, pair + pairRest)
else:
flush = getFlush(hand)
if flush:
straight = getStraight(hand)
if straight:
return (STRAIGHT_FLUSH, straight)
return (FLUSH, flush)
straight = getStraight(hand)
if straight:
return (STRAIGHT, straight)
hand.sort()
return (RUNT, hand)
first = 0
second = None
for hand in combinations(cards, 5):
(maybeFirst, maybeSecond) = getRank(hand)
if maybeFirst > first:
first = maybeFirst
second = maybeSecond
elif maybeFirst == first:
second = max(second, maybeSecond)
assert len(second) == 5, 'invalid second len'
return (first, second)
deck = []
for suit in [S, H, C, D]:
for rank in [A, K, Q, J] + range(10, 1, -1):
deck.append(Card(rank, suit))
if __name__ == '__main__':
import random
random.shuffle(deck)
for hand in window(deck, 7):
print '%s: %s' % (hand, score(hand))

View File

@ -1,96 +0,0 @@
# This module is part of the Pyndex project and is Copyright 2003 Amir
# Bakhtiar (amir@divmod.org). This is free software; you can redistribute
# it and/or modify it under the terms of version 2.1 of the GNU Lesser
# General Public License as published by the Free Software Foundation.
import string
class Splitter(object):
"""Split plain text into words" utility class
Adapted from David Mertz's article in IBM developerWorks
Needs work to handle international characters, etc"""
## __slots__ = ['stemmer', 'porter', 'stopwording', 'word_only', 'nonword',
## 'nondigits', 'alpha', 'ident', 'tokens', 'position']
stopWords = {'and': 1, 'be': 1, 'to': 1, 'that': 1, 'into': 1,
'it': 1, 'but': 1, 'as': 1, 'are': 1, 'they': 1,
'in': 1, 'not': 1, 'such': 1, 'with': 1, 'by': 1,
'is': 1, 'if': 1, 'a': 1, 'on': 1, 'for': 1,
'no': 1, 'these': 1, 'of': 1, 'there': 1,
'this': 1, 'will': 1, 'their': 1, 's': 1, 't': 1,
'then': 1, 'the': 1, 'was': 1, 'or': 1, 'at': 1}
yes = string.lowercase + string.digits + '' # throw in any extras
nonword = ''
for i in range(0,255):
if chr(i) not in yes:
nonword += chr(i)
word_only = string.maketrans(nonword, " " * len(nonword))
nondigits = string.join(map(chr, range(0,48)) + map(chr, range(58,255)), '')
alpha = string.join(map(chr, range(65,91)) + map(chr, range(97,123)), '')
ident = string.join(map(chr, range(256)), '')
def close(self):
# Lupy support
pass
def tokenStream(self, fieldName, file, casesensitive=False):
"""Split text/plain string into a list of words
"""
self.tokens = self.split(file.read())
self.position = 0
return self
def next(self):
if self.position >= len(self.tokens):
return None
res = Token(self.tokens[self.position])
self.position += 1
return res
def split(self, text, casesensitive=0):
# Speedup trick: attributes into local scope
word_only = self.word_only
ident = self.ident
alpha = self.alpha
nondigits = self.nondigits
# Let's adjust case if not case-sensitive
if not casesensitive: text = string.lower(text)
# Split the raw text
allwords = text.translate(word_only).split() # Let's strip funny byte values
# Finally, let's skip some words not worth indexing
words = []
for word in allwords:
if len(word) > 32: continue # too long (probably gibberish)
# Identify common patterns in non-word data (binary, UU/MIME, etc)
num_nonalpha = len(word.translate(ident, alpha))
numdigits = len(word.translate(ident, nondigits))
if numdigits > len(word)-2: # almost all digits
if numdigits > 5: # too many digits is gibberish
continue # a moderate number is year/zipcode/etc
elif num_nonalpha*2 > len(word): # too much scattered nonalpha = gibberish
continue
word = word.translate(word_only) # Let's strip funny byte values
subwords = word.split() # maybe embedded non-alphanumeric
for subword in subwords: # ...so we might have subwords
if len(subword) <= 1: continue # too short a subword
words.append(subword)
return words
class Token:
def __init__(self, trmText):
self.trmText = trmText
def termText(self):
return self.trmText

View File

@ -1,309 +0,0 @@
# This module is part of the Divmod project and is Copyright 2003 Amir Bakhtiar:
# amir@divmod.org. This is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
import operator
import string
import math
from sets import Set
from splitter import Splitter
class BayesData(dict):
def __init__(self, name='', pool=None):
self.name = name
self.training = []
self.pool = pool
self.tokenCount = 0
self.trainCount = 0
def trainedOn(self, item):
return item in self.training
def __repr__(self):
return '<BayesDict: %s, %s tokens>' % (self.name, self.tokenCount)
class Bayes(object):
def __init__(self, tokenizer=None, combiner=None, dataClass=None):
if dataClass is None:
self.dataClass = BayesData
else:
self.dataClass = dataClass
self.corpus = self.dataClass('__Corpus__')
self.pools = {}
self.pools['__Corpus__'] = self.corpus
self.trainCount = 0
self.splitter = Splitter()
self.dirty = True
# The tokenizer takes an object and returns
# a list of strings
if tokenizer is None:
self.tokenizer = self.getTokens
else:
self.tokenizer = tokenizer
# The combiner combines probabilities
if combiner is None:
self.combiner = self.robinson
else:
self.combiner = combiner
def split(self, text):
return self.splitter.split(text)
def commit(self):
self.save()
def newPool(self, poolName):
"""Create a new pool, without actually doing any
training.
"""
self.dirty = True # not always true, but it's simple
return self.pools.setdefault(poolName, self.dataClass(poolName))
def removePool(self, poolName):
del(self.pools[poolName])
self.dirty = True
def renamePool(self, poolName, newName):
self.pools[newName] = self.pools[poolName]
self.pools[newName].name = newName
self.removePool(poolName)
self.dirty = True
def mergePools(self, destPool, sourcePool):
"""Merge an existing pool into another.
The data from sourcePool is merged into destPool.
The arguments are the names of the pools to be merged.
The pool named sourcePool is left in tact and you may
want to call removePool() to get rid of it.
"""
sp = self.pools[sourcePool]
dp = self.pools[destPool]
for tok, count in sp.items():
if dp.get(tok):
dp[tok] += count
else:
dp[tok] = count
dp.tokenCount += 1
self.dirty = True
def poolData(self, poolName):
"""Return a list of the (token, count) tuples.
"""
return self.pools[poolName].items()
def poolTokens(self, poolName):
"""Return a list of the tokens in this pool.
"""
return [tok for tok, count in self.poolData(poolName)]
def save(self, fname='bayesdata.dat'):
from cPickle import dump
fp = open(fname, 'wb')
dump(self.pools, fp)
fp.close()
def load(self, fname='bayesdata.dat'):
from cPickle import load
fp = open(fname, 'rb')
self.pools = load(fp)
fp.close()
self.corpus = self.pools['__Corpus__']
self.dirty = True
def poolNames(self):
"""Return a sorted list of Pool names.
Does not include the system pool '__Corpus__'.
"""
pools = self.pools.keys()
pools.remove('__Corpus__')
pools = [pool for pool in pools]
pools.sort()
return pools
def buildCache(self):
""" merges corpora and computes probabilities
"""
self.cache = {}
for pname, pool in self.pools.items():
# skip our special pool
if pname == '__Corpus__':
continue
poolCount = len(pool)
themCount = max(len(self.corpus) - poolCount, 1)
cacheDict = self.cache.setdefault(pname, self.dataClass(pname))
for word, totCount in self.corpus.items():
# for every word in the copus
# check to see if this pool contains this word
thisCount = float(pool.get(word, 0.0))
otherCount = float(totCount) - thisCount
if not poolCount:
goodMetric = 1.0
else:
goodMetric = min(1.0, otherCount/poolCount)
badMetric = min(1.0, thisCount/themCount)
f = badMetric / (goodMetric + badMetric)
# PROBABILITY_THRESHOLD
if abs(f-0.5) >= 0.1 :
# GOOD_PROB, BAD_PROB
cacheDict[word] = max(0.0001, min(0.9999, f))
def poolProbs(self):
if self.dirty:
self.buildCache()
self.dirty = False
return self.cache
def getTokens(self, obj):
"""Hopefully it's a string and we'll just split it
on non-alphanumeric stuff.
Override this in your subclass for objects other
than text.
Alternatively, you can pass in a tokenizer as part of
instance creation.
"""
return self.split(obj)
def getProbs(self, pool, words):
""" extracts the probabilities of tokens in a message
"""
probs = [(word, pool[word]) for word in words if word in pool]
probs.sort(lambda x,y: cmp(y[1],x[1]))
return probs[:2048]
def train(self, pool, item, uid=None):
"""Train Bayes by telling him that item belongs
in pool. uid is optional and may be used to uniquely
identify the item that is being trained on.
"""
tokens = self.tokenizer(item)
pool = self.pools.setdefault(pool, self.dataClass(pool))
self._train(pool, tokens)
self.corpus.trainCount += 1
pool.trainCount += 1
if uid:
pool.training.append(uid)
self.dirty = True
def untrain(self, pool, item, uid=None):
tokens = self.tokenizer(item)
pool = self.pools.get(pool, None)
if not pool:
return
self._untrain(pool, tokens)
# I guess we want to count this as additional training?
self.corpus.trainCount += 1
pool.trainCount += 1
if uid:
pool.training.remove(uid)
self.dirty = True
def _train(self, pool, tokens):
wc = 0
for token in tokens:
count = pool.get(token, 0)
pool[token] = count + 1
count = self.corpus.get(token, 0)
self.corpus[token] = count + 1
wc += 1
pool.tokenCount += wc
self.corpus.tokenCount += wc
def _untrain(self, pool, tokens):
for token in tokens:
count = pool.get(token, 0)
if count:
if count == 1:
del(pool[token])
else:
pool[token] = count - 1
pool.tokenCount -= 1
count = self.corpus.get(token, 0)
if count:
if count == 1:
del(self.corpus[token])
else:
self.corpus[token] = count - 1
self.corpus.tokenCount -= 1
def trainedOn(self, msg):
for p in self.cache.values():
if msg in p.training:
return True
return False
def guess(self, msg):
tokens = Set(self.tokenizer(msg))
pools = self.poolProbs()
res = {}
for pname, pprobs in pools.items():
p = self.getProbs(pprobs, tokens)
if len(p) != 0:
res[pname]=self.combiner(p, pname)
res = res.items()
res.sort(lambda x,y: cmp(y[1], x[1]))
return res
def robinson(self, probs, ignore):
""" computes the probability of a message being spam (Robinson's method)
P = 1 - prod(1-p)^(1/n)
Q = 1 - prod(p)^(1/n)
S = (1 + (P-Q)/(P+Q)) / 2
Courtesy of http://christophe.delord.free.fr/en/index.html
"""
nth = 1./len(probs)
P = 1.0 - reduce(operator.mul, map(lambda p: 1.0-p[1], probs), 1.0) ** nth
Q = 1.0 - reduce(operator.mul, map(lambda p: p[1], probs)) ** nth
S = (P - Q) / (P + Q)
return (1 + S) / 2
def robinsonFisher(self, probs, ignore):
""" computes the probability of a message being spam (Robinson-Fisher method)
H = C-1( -2.ln(prod(p)), 2*n )
S = C-1( -2.ln(prod(1-p)), 2*n )
I = (1 + H - S) / 2
Courtesy of http://christophe.delord.free.fr/en/index.html
"""
n = len(probs)
try: H = chi2P(-2.0 * math.log(reduce(operator.mul, map(lambda p: p[1], probs), 1.0)), 2*n)
except OverflowError: H = 0.0
try: S = chi2P(-2.0 * math.log(reduce(operator.mul, map(lambda p: 1.0-p[1], probs), 1.0)), 2*n)
except OverflowError: S = 0.0
return (1 + H - S) / 2
def __repr__(self):
return '<Bayes: %s>' % [self.pools[p] for p in self.poolNames()]
def __len__(self):
return len(self.corpus)
def chi2P(chi, df):
""" return P(chisq >= chi, with df degree of freedom)
df must be even
"""
assert df & 1 == 0
m = chi / 2.0
sum = term = math.exp(-m)
for i in range(1, df/2):
term *= m/i
sum += term
return min(sum, 1.0)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,218 +0,0 @@
"""A lexical analyzer class for simple shell-like syntaxes."""
# Module and documentation by Eric S. Raymond, 21 Dec 1998
# Input stacking and error message cleanup added by ESR, March 2000
# push_source() and pop_source() made explicit by ESR, January 2001.
import os.path
import sys
__all__ = ["shlex"]
class shlex:
"A lexical analyzer class for simple shell-like syntaxes."
def __init__(self, instream=None, infile=None):
if instream is not None:
self.instream = instream
self.infile = infile
else:
self.instream = sys.stdin
self.infile = None
self.commenters = '#'
self.wordchars = ('abcdfeghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_')
self.whitespace = ' \t\r\n'
self.quotes = '\'"'
self.state = ' '
self.pushback = []
self.lineno = 1
self.debug = 0
self.token = ''
self.backslash = False
self.filestack = []
self.source = None
if self.debug:
print 'shlex: reading from %s, line %d' \
% (self.instream, self.lineno)
def push_token(self, tok):
"Push a token onto the stack popped by the get_token method"
if self.debug >= 1:
print "shlex: pushing token " + `tok`
self.pushback = [tok] + self.pushback
def push_source(self, newstream, newfile=None):
"Push an input source onto the lexer's input source stack."
self.filestack.insert(0, (self.infile, self.instream, self.lineno))
self.infile = newfile
self.instream = newstream
self.lineno = 1
if self.debug:
if newfile is not None:
print 'shlex: pushing to file %s' % (self.infile,)
else:
print 'shlex: pushing to stream %s' % (self.instream,)
def pop_source(self):
"Pop the input source stack."
self.instream.close()
(self.infile, self.instream, self.lineno) = self.filestack[0]
self.filestack = self.filestack[1:]
if self.debug:
print 'shlex: popping to %s, line %d' \
% (self.instream, self.lineno)
self.state = ' '
def get_token(self):
"Get a token from the input stream (or from stack if it's nonempty)"
if self.pushback:
tok = self.pushback[0]
self.pushback = self.pushback[1:]
if self.debug >= 1:
print "shlex: popping token " + `tok`
return tok
# No pushback. Get a token.
raw = self.read_token()
# Handle inclusions
while raw == self.source:
spec = self.sourcehook(self.read_token())
if spec:
(newfile, newstream) = spec
self.push_source(newstream, newfile)
raw = self.get_token()
# Maybe we got EOF instead?
while raw == "":
if len(self.filestack) == 0:
return ""
else:
self.pop_source()
raw = self.get_token()
# Neither inclusion nor EOF
if self.debug >= 1:
if raw:
print "shlex: token=" + `raw`
else:
print "shlex: token=EOF"
return raw
def read_token(self):
"Read a token from the input stream (no pushback or inclusions)"
while 1:
nextchar = self.instream.read(1)
if nextchar == '\n':
self.lineno = self.lineno + 1
if self.debug >= 3:
print "shlex: in state", repr(self.state), \
"I see character:", repr(nextchar)
if self.state is None:
self.token = '' # past end of file
break
elif self.state == ' ':
if not nextchar:
self.state = None # end of file
break
elif nextchar in self.whitespace:
if self.debug >= 2:
print "shlex: I see whitespace in whitespace state"
if self.token:
break # emit current token
else:
continue
elif nextchar in self.commenters:
self.instream.readline()
self.lineno = self.lineno + 1
elif nextchar in self.wordchars:
self.token = nextchar
self.state = 'a'
elif nextchar in self.quotes:
self.token = nextchar
self.state = nextchar
else:
self.token = nextchar
if self.token:
break # emit current token
else:
continue
elif self.state in self.quotes:
self.token = self.token + nextchar
if nextchar == '\\':
if self.backslash:
self.backslash = False
else:
self.backslash = True
else:
if not self.backslash and nextchar == self.state:
self.state = ' '
break
elif self.backslash:
self.backslash = False
elif not nextchar: # end of file
if self.debug >= 2:
print "shlex: I see EOF in quotes state"
# XXX what error should be raised here?
raise ValueError, "No closing quotation"
elif self.state == 'a':
if not nextchar:
self.state = None # end of file
break
elif nextchar in self.whitespace:
if self.debug >= 2:
print "shlex: I see whitespace in word state"
self.state = ' '
if self.token:
break # emit current token
else:
continue
elif nextchar in self.commenters:
self.instream.readline()
self.lineno = self.lineno + 1
elif nextchar in self.wordchars or nextchar in self.quotes:
self.token = self.token + nextchar
else:
self.pushback = [nextchar] + self.pushback
if self.debug >= 2:
print "shlex: I see punctuation in word state"
self.state = ' '
if self.token:
break # emit current token
else:
continue
result = self.token
self.token = ''
if self.debug > 1:
if result:
print "shlex: raw token=" + `result`
else:
print "shlex: raw token=EOF"
return result
def sourcehook(self, newfile):
"Hook called on a filename to be sourced."
if newfile[0] == '"':
newfile = newfile[1:-1]
# This implements cpp-like semantics for relative-path inclusion.
if type(self.infile) == type("") and not os.path.isabs(newfile):
newfile = os.path.join(os.path.dirname(self.infile), newfile)
return (newfile, open(newfile, "r"))
def error_leader(self, infile=None, lineno=None):
"Emit a C-compiler-like, Emacs-friendly error-message leader."
if infile is None:
infile = self.infile
if lineno is None:
lineno = self.lineno
return "\"%s\", line %d: " % (infile, lineno)
if __name__ == '__main__':
if len(sys.argv) == 1:
lexer = shlex()
else:
file = sys.argv[1]
lexer = shlex(open(file), file)
while 1:
tt = lexer.get_token()
if tt:
print "Token: " + repr(tt)
else:
break

View File

@ -1,784 +0,0 @@
'''
Python unit testing framework, based on Erich Gamma's JUnit and Kent Beck's
Smalltalk testing framework.
This module contains the core framework classes that form the basis of
specific test cases and suites (TestCase, TestSuite etc.), and also a
text-based utility class for running the tests and reporting the results
(TextTestRunner).
Simple usage:
import unittest
class IntegerArithmenticTestCase(unittest.TestCase):
def testAdd(self): ## test method names begin 'test*'
self.assertEquals((1 + 2), 3)
self.assertEquals(0 + 1, 1)
def testMultiply(self):
self.assertEquals((0 * 10), 0)
self.assertEquals((5 * 8), 40)
if __name__ == '__main__':
unittest.main()
Further information is available in the bundled documentation, and from
http://pyunit.sourceforge.net/
Copyright (c) 1999, 2000, 2001 Steve Purcell
This module is free software, and you may redistribute it and/or modify
it under the same terms as Python itself, so long as this copyright message
and disclaimer are retained in their original form.
IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
'''
__author__ = "Steve Purcell"
__email__ = "stephen_purcell at yahoo dot com"
__version__ = "#Revision: 1.46 $"[11:-2]
import time
import sys
import traceback
import string
import os
import types
###
# Globals
###
asserts = 0
##############################################################################
# Test framework core
##############################################################################
# All classes defined herein are 'new-style' classes, allowing use of 'super()'
__metaclass__ = type
def _strclass(cls):
return "%s.%s" % (cls.__module__, cls.__name__)
class TestResult:
"""Holder for test result information.
Test results are automatically managed by the TestCase and TestSuite
classes, and do not need to be explicitly manipulated by writers of tests.
Each instance holds the total number of tests run, and collections of
failures and errors that occurred among those test runs. The collections
contain tuples of (testcase, exceptioninfo), where exceptioninfo is the
formatted traceback of the error that occurred.
"""
def __init__(self):
self.failures = []
self.errors = []
self.testsRun = 0
self.shouldStop = 0
def startTest(self, test):
"Called when the given test is about to be run"
self.testsRun = self.testsRun + 1
def stopTest(self, test):
"Called when the given test has been run"
pass
def addError(self, test, err):
"""Called when an error has occurred. 'err' is a tuple of values as
returned by sys.exc_info().
"""
self.errors.append((test, self._exc_info_to_string(err)))
def addFailure(self, test, err):
"""Called when an error has occurred. 'err' is a tuple of values as
returned by sys.exc_info()."""
self.failures.append((test, self._exc_info_to_string(err)))
def addSuccess(self, test):
"Called when a test has completed successfully"
pass
def wasSuccessful(self):
"Tells whether or not this result was a success"
return len(self.failures) == len(self.errors) == 0
def stop(self):
"Indicates that the tests should be aborted"
self.shouldStop = 1
def _exc_info_to_string(self, err):
"""Converts a sys.exc_info()-style tuple of values into a string."""
return string.join(traceback.format_exception(*err), '')
def __repr__(self):
return "<%s run=%i errors=%i failures=%i>" % \
(_strclass(self.__class__), self.testsRun, len(self.errors),
len(self.failures))
class TestCase:
"""A class whose instances are single test cases.
By default, the test code itself should be placed in a method named
'runTest'.
If the fixture may be used for many test cases, create as
many test methods as are needed. When instantiating such a TestCase
subclass, specify in the constructor arguments the name of the test method
that the instance is to execute.
Test authors should subclass TestCase for their own tests. Construction
and deconstruction of the test's environment ('fixture') can be
implemented by overriding the 'setUp' and 'tearDown' methods respectively.
If it is necessary to override the __init__ method, the base class
__init__ method must always be called. It is important that subclasses
should not change the signature of their __init__ method, since instances
of the classes are instantiated automatically by parts of the framework
in order to be run.
"""
# This attribute determines which exception will be raised when
# the instance's assertion methods fail; test methods raising this
# exception will be deemed to have 'failed' rather than 'errored'
failureException = AssertionError
def __init__(self, methodName='runTest'):
"""Create an instance of the class that will use the named test
method when executed. Raises a ValueError if the instance does
not have a method with the specified name.
"""
try:
self.__testMethodName = methodName
testMethod = getattr(self, methodName)
self.__testMethodDoc = testMethod.__doc__
except AttributeError:
raise ValueError, "no such test method in %s: %s" % \
(self.__class__, methodName)
def setUp(self):
"Hook method for setting up the test fixture before exercising it."
pass
def tearDown(self):
"Hook method for deconstructing the test fixture after testing it."
pass
def countTestCases(self):
return 1
def defaultTestResult(self):
return TestResult()
def shortDescription(self):
"""Returns a one-line description of the test, or None if no
description has been provided.
The default implementation of this method returns the first line of
the specified test method's docstring.
"""
doc = self.__testMethodDoc
return doc and string.strip(string.split(doc, "\n")[0]) or None
def id(self):
return "%s.%s" % (_strclass(self.__class__), self.__testMethodName)
def __str__(self):
return "%s (%s)" % (self.__testMethodName, _strclass(self.__class__))
def __repr__(self):
return "<%s testMethod=%s>" % \
(_strclass(self.__class__), self.__testMethodName)
def run(self, result=None):
return self(result)
def __call__(self, result=None):
if result is None: result = self.defaultTestResult()
result.startTest(self)
testMethod = getattr(self, self.__testMethodName)
try:
try:
x = self.setUp()
if x:
print 'skipped (%s)' % x
return
except KeyboardInterrupt:
raise
except:
result.addError(self, self.__exc_info())
return
ok = 0
try:
testMethod()
ok = 1
except self.failureException, e:
result.addFailure(self, self.__exc_info())
except KeyboardInterrupt:
raise
except:
result.addError(self, self.__exc_info())
try:
self.tearDown()
except KeyboardInterrupt:
raise
except:
result.addError(self, self.__exc_info())
ok = 0
if ok: result.addSuccess(self)
finally:
result.stopTest(self)
def debug(self):
"""Run the test without collecting errors in a TestResult"""
self.setUp()
getattr(self, self.__testMethodName)()
self.tearDown()
def __exc_info(self):
"""Return a version of sys.exc_info() with the traceback frame
minimised; usually the top level of the traceback frame is not
needed.
"""
exctype, excvalue, tb = sys.exc_info()
if sys.platform[:4] == 'java': ## tracebacks look different in Jython
return (exctype, excvalue, tb)
newtb = tb.tb_next
if newtb is None:
return (exctype, excvalue, tb)
return (exctype, excvalue, newtb)
def _fail(self, msg):
"""Underlying implementation of failure."""
raise self.failureException, msg
def fail(self, msg=None):
"""Fail immediately, with the given message."""
global asserts
asserts += 1
self._fail(msg)
def failIf(self, expr, msg=None):
"Fail the test if the expression is true."
global asserts
asserts += 1
if expr: self._fail(msg)
def failUnless(self, expr, msg=None):
"""Fail the test unless the expression is true."""
global asserts
asserts += 1
if not expr: self._fail(msg)
def failUnlessRaises(self, excClass, callableObj, *args, **kwargs):
"""Fail unless an exception of class excClass is thrown
by callableObj when invoked with arguments args and keyword
arguments kwargs. If a different type of exception is
thrown, it will not be caught, and the test case will be
deemed to have suffered an error, exactly as for an
unexpected exception.
"""
global asserts
asserts += 1
try:
callableObj(*args, **kwargs)
except excClass:
return
else:
if hasattr(excClass,'__name__'): excName = excClass.__name__
else: excName = str(excClass)
raise self._fail(excName)
def failUnlessEqual(self, first, second, msg=None):
"""Fail if the two objects are unequal as determined by the '=='
operator.
"""
global asserts
asserts += 1
if not first == second:
self._fail(msg or '%s != %s' % (`first`, `second`))
def failIfEqual(self, first, second, msg=None):
"""Fail if the two objects are equal as determined by the '=='
operator.
"""
global asserts
asserts += 1
if first == second:
self._fail(msg or '%s == %s' % (`first`, `second`))
def failUnlessAlmostEqual(self, first, second, places=7, msg=None):
"""Fail if the two objects are unequal as determined by their
difference rounded to the given number of decimal places
(default 7) and comparing to zero.
Note that decimal places (from zero) is usually not the same
as significant digits (measured from the most signficant digit).
"""
global asserts
asserts += 1
if round(second-first, places) != 0:
self._fail(msg or '%s != %s within %s places' % \
(`first`, `second`, `places`))
def failIfAlmostEqual(self, first, second, places=7, msg=None):
"""Fail if the two objects are equal as determined by their
difference rounded to the given number of decimal places
(default 7) and comparing to zero.
Note that decimal places (from zero) is usually not the same
as significant digits (measured from the most signficant digit).
"""
global asserts
asserts += 1
if round(second-first, places) == 0:
self._fail(msg or '%s == %s within %s places' % \
(`first`, `second`, `places`))
assertEqual = assertEquals = failUnlessEqual
assertNotEqual = assertNotEquals = failIfEqual
assertAlmostEqual = assertAlmostEquals = failUnlessAlmostEqual
assertNotAlmostEqual = assertNotAlmostEquals = failIfAlmostEqual
assertRaises = failUnlessRaises
assert_ = failUnless
class TestSuite:
"""A test suite is a composite test consisting of a number of TestCases.
For use, create an instance of TestSuite, then add test case instances.
When all tests have been added, the suite can be passed to a test
runner, such as TextTestRunner. It will run the individual test cases
in the order in which they were added, aggregating the results. When
subclassing, do not forget to call the base class constructor.
"""
def __init__(self, tests=()):
self._tests = []
self.addTests(tests)
def __repr__(self):
return "<%s tests=%s>" % (_strclass(self.__class__), self._tests)
__str__ = __repr__
def countTestCases(self):
cases = 0
for test in self._tests:
cases = cases + test.countTestCases()
return cases
def addTest(self, test):
self._tests.append(test)
def addTests(self, tests):
for test in tests:
self.addTest(test)
def run(self, result):
return self(result)
def __call__(self, result):
for test in self._tests:
if result.shouldStop:
break
test(result)
return result
def debug(self):
"""Run the tests without collecting errors in a TestResult"""
for test in self._tests: test.debug()
class FunctionTestCase(TestCase):
"""A test case that wraps a test function.
This is useful for slipping pre-existing test functions into the
PyUnit framework. Optionally, set-up and tidy-up functions can be
supplied. As with TestCase, the tidy-up ('tearDown') function will
always be called if the set-up ('setUp') function ran successfully.
"""
def __init__(self, testFunc, setUp=None, tearDown=None,
description=None):
TestCase.__init__(self)
self.__setUpFunc = setUp
self.__tearDownFunc = tearDown
self.__testFunc = testFunc
self.__description = description
def setUp(self):
if self.__setUpFunc is not None:
self.__setUpFunc()
def tearDown(self):
if self.__tearDownFunc is not None:
self.__tearDownFunc()
def runTest(self):
self.__testFunc()
def id(self):
return self.__testFunc.__name__
def __str__(self):
return "%s (%s)" % (_strclass(self.__class__), self.__testFunc.__name__)
def __repr__(self):
return "<%s testFunc=%s>" % (_strclass(self.__class__), self.__testFunc)
def shortDescription(self):
if self.__description is not None: return self.__description
doc = self.__testFunc.__doc__
return doc and string.strip(string.split(doc, "\n")[0]) or None
##############################################################################
# Locating and loading tests
##############################################################################
class TestLoader:
"""This class is responsible for loading tests according to various
criteria and returning them wrapped in a Test
"""
testMethodPrefix = 'test'
sortTestMethodsUsing = cmp
suiteClass = TestSuite
def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass"""
return self.suiteClass(map(testCaseClass,
self.getTestCaseNames(testCaseClass)))
def loadTestsFromModule(self, module):
"""Return a suite of all tests cases contained in the given module"""
tests = []
for name in dir(module):
obj = getattr(module, name)
if (isinstance(obj, (type, types.ClassType)) and
issubclass(obj, TestCase)):
tests.append(self.loadTestsFromTestCase(obj))
return self.suiteClass(tests)
def loadTestsFromName(self, name, module=None):
"""Return a suite of all tests cases given a string specifier.
The name may resolve either to a module, a test case class, a
test method within a test case class, or a callable object which
returns a TestCase or TestSuite instance.
The method optionally resolves the names relative to a given module.
"""
parts = string.split(name, '.')
if module is None:
if not parts:
raise ValueError, "incomplete test name: %s" % name
else:
parts_copy = parts[:]
while parts_copy:
try:
module = __import__(string.join(parts_copy,'.'))
break
except ImportError:
del parts_copy[-1]
if not parts_copy: raise
parts = parts[1:]
obj = module
for part in parts:
obj = getattr(obj, part)
import unittest
if type(obj) == types.ModuleType:
return self.loadTestsFromModule(obj)
elif (isinstance(obj, (type, types.ClassType)) and
issubclass(obj, unittest.TestCase)):
return self.loadTestsFromTestCase(obj)
elif type(obj) == types.UnboundMethodType:
return obj.im_class(obj.__name__)
elif callable(obj):
test = obj()
if not isinstance(test, unittest.TestCase) and \
not isinstance(test, unittest.TestSuite):
raise ValueError, \
"calling %s returned %s, not a test" % (obj,test)
return test
else:
raise ValueError, "don't know how to make test from: %s" % obj
def loadTestsFromNames(self, names, module=None):
"""Return a suite of all tests cases found using the given sequence
of string specifiers. See 'loadTestsFromName()'.
"""
suites = []
for name in names:
suites.append(self.loadTestsFromName(name, module))
return self.suiteClass(suites)
def getTestCaseNames(self, testCaseClass):
"""Return a sorted sequence of method names found within testCaseClass
"""
testFnNames = filter(lambda n,p=self.testMethodPrefix: n[:len(p)] == p,
dir(testCaseClass))
for baseclass in testCaseClass.__bases__:
for testFnName in self.getTestCaseNames(baseclass):
if testFnName not in testFnNames: # handle overridden methods
testFnNames.append(testFnName)
if self.sortTestMethodsUsing:
testFnNames.sort(self.sortTestMethodsUsing)
return testFnNames
defaultTestLoader = TestLoader()
##############################################################################
# Patches for old functions: these functions should be considered obsolete
##############################################################################
def _makeLoader(prefix, sortUsing, suiteClass=None):
loader = TestLoader()
loader.sortTestMethodsUsing = sortUsing
loader.testMethodPrefix = prefix
if suiteClass: loader.suiteClass = suiteClass
return loader
def getTestCaseNames(testCaseClass, prefix, sortUsing=cmp):
return _makeLoader(prefix, sortUsing).getTestCaseNames(testCaseClass)
def makeSuite(testCaseClass, prefix='test', sortUsing=cmp, suiteClass=TestSuite):
return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromTestCase(testCaseClass)
def findTestCases(module, prefix='test', sortUsing=cmp, suiteClass=TestSuite):
return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromModule(module)
##############################################################################
# Text UI
##############################################################################
class _WritelnDecorator:
"""Used to decorate file-like objects with a handy 'writeln' method"""
def __init__(self,stream):
self.stream = stream
def __getattr__(self, attr):
return getattr(self.stream,attr)
def writeln(self, *args):
if args: self.write(*args)
self.write('\n') # text-mode streams translate to \r\n if needed
class _TextTestResult(TestResult):
"""A test result class that can print formatted text results to a stream.
Used by TextTestRunner.
"""
separator1 = '=' * 70
separator2 = '-' * 70
def __init__(self, stream, descriptions, verbosity):
TestResult.__init__(self)
self.stream = stream
self.showAll = verbosity > 1
self.dots = verbosity == 1
self.descriptions = descriptions
def getDescription(self, test):
if self.descriptions:
return test.shortDescription() or str(test)
else:
return str(test)
def startTest(self, test):
TestResult.startTest(self, test)
if self.showAll:
self.stream.write(self.getDescription(test))
self.stream.write(" ... ")
def addSuccess(self, test):
TestResult.addSuccess(self, test)
if self.showAll:
self.stream.writeln("ok")
elif self.dots:
self.stream.write('.')
def addError(self, test, err):
TestResult.addError(self, test, err)
if self.showAll:
self.stream.writeln("ERROR")
elif self.dots:
self.stream.write('E')
def addFailure(self, test, err):
TestResult.addFailure(self, test, err)
if self.showAll:
self.stream.writeln("FAIL")
elif self.dots:
self.stream.write('F')
def printErrors(self):
if self.dots or self.showAll:
self.stream.writeln()
self.printErrorList('ERROR', self.errors)
self.printErrorList('FAIL', self.failures)
def printErrorList(self, flavour, errors):
for test, err in errors:
self.stream.writeln(self.separator1)
self.stream.writeln("%s: %s" % (flavour,self.getDescription(test)))
self.stream.writeln(self.separator2)
self.stream.writeln("%s" % err)
class TextTestRunner:
"""A test runner class that displays results in textual form.
It prints out the names of tests as they are run, errors as they
occur, and a summary of the results at the end of the test run.
"""
def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1):
self.stream = _WritelnDecorator(stream)
self.descriptions = descriptions
self.verbosity = verbosity
def _makeResult(self):
return _TextTestResult(self.stream, self.descriptions, self.verbosity)
def run(self, test):
"Run the given test case or test suite."
result = self._makeResult()
startTime = time.time()
test(result)
stopTime = time.time()
timeTaken = float(stopTime - startTime)
result.printErrors()
self.stream.writeln(result.separator2)
run = result.testsRun
self.stream.writeln("Ran %d test%s in %.3fs" %
(run, run != 1 and "s" or "", timeTaken))
self.stream.writeln()
if not result.wasSuccessful():
self.stream.write("FAILED (")
failed, errored = map(len, (result.failures, result.errors))
if failed:
self.stream.write("failures=%d" % failed)
if errored:
if failed: self.stream.write(", ")
self.stream.write("errors=%d" % errored)
self.stream.writeln(")")
else:
self.stream.writeln("OK")
return result
##############################################################################
# Facilities for running tests from the command line
##############################################################################
class TestProgram:
"""A command-line program that runs a set of tests; this is primarily
for making test modules conveniently executable.
"""
USAGE = """\
Usage: %(progName)s [options] [test] [...]
Options:
-h, --help Show this message
-v, --verbose Verbose output
-q, --quiet Minimal output
Examples:
%(progName)s - run default set of tests
%(progName)s MyTestSuite - run suite 'MyTestSuite'
%(progName)s MyTestCase.testSomething - run MyTestCase.testSomething
%(progName)s MyTestCase - run all 'test*' test methods
in MyTestCase
"""
def __init__(self, module='__main__', defaultTest=None,
argv=None, testRunner=None, testLoader=defaultTestLoader):
if type(module) == type(''):
self.module = __import__(module)
for part in string.split(module,'.')[1:]:
self.module = getattr(self.module, part)
else:
self.module = module
if argv is None:
argv = sys.argv
self.verbosity = 1
self.defaultTest = defaultTest
self.testRunner = testRunner
self.testLoader = testLoader
self.progName = os.path.basename(argv[0])
self.parseArgs(argv)
self.runTests()
def usageExit(self, msg=None):
if msg: print msg
print self.USAGE % self.__dict__
sys.exit(2)
def parseArgs(self, argv):
import getopt
try:
options, args = getopt.getopt(argv[1:], 'hHvq',
['help','verbose','quiet'])
for opt, value in options:
if opt in ('-h','-H','--help'):
self.usageExit()
if opt in ('-q','--quiet'):
self.verbosity = 0
if opt in ('-v','--verbose'):
self.verbosity = 2
if len(args) == 0 and self.defaultTest is None:
self.test = self.testLoader.loadTestsFromModule(self.module)
return
if len(args) > 0:
self.testNames = args
else:
self.testNames = (self.defaultTest,)
self.createTests()
except getopt.error, msg:
self.usageExit(msg)
def createTests(self):
self.test = self.testLoader.loadTestsFromNames(self.testNames,
self.module)
def runTests(self):
if self.testRunner is None:
self.testRunner = TextTestRunner(verbosity=self.verbosity)
result = self.testRunner.run(self.test)
sys.exit(not result.wasSuccessful())
main = TestProgram
##############################################################################
# Executing this module from the command line
##############################################################################
if __name__ == "__main__":
main(module=None)

File diff suppressed because it is too large Load Diff

View File

@ -1,302 +0,0 @@
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Allows aliases for other commands.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jemfinch
import supybot.plugins as plugins
import os
import re
import sets
import supybot.conf as conf
import supybot.utils as utils
from supybot.commands import *
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks
import supybot.structures as structures
import supybot.unpreserve as unpreserve
class AliasError(Exception):
pass
class RecursiveAlias(AliasError):
pass
def findAliasCommand(s, alias):
s = re.escape(s)
r = re.compile(r'(?:(^|\[)\s*\b%s\b|\|\s*\b%s\b)' % (s, s))
return bool(r.search(alias))
dollarRe = re.compile(r'\$(\d+)')
def findBiggestDollar(alias):
dollars = dollarRe.findall(alias)
dollars = map(int, dollars)
dollars.sort()
if dollars:
return dollars[-1]
else:
return 0
atRe = re.compile(r'@(\d+)')
def findBiggestAt(alias):
ats = atRe.findall(alias)
ats = map(int, ats)
ats.sort()
if ats:
return ats[-1]
else:
return 0
def makeNewAlias(name, alias):
original = alias
if findAliasCommand(name, alias):
raise RecursiveAlias
biggestDollar = findBiggestDollar(original)
biggestAt = findBiggestAt(original)
wildcard = '$*' in original
if biggestAt and wildcard:
raise AliasError, 'Can\'t mix $* and optional args (@1, etc.)'
if original.count('$*') > 1:
raise AliasError, 'There can be only one $* in an alias.'
testTokens = callbacks.tokenize(original)
if testTokens and isinstance(testTokens[0], list):
raise AliasError, 'Commands may not be the result of nesting.'
def f(self, irc, msg, args):
alias = original.replace('$nick', msg.nick)
if '$channel' in original:
channel = privmsgs.getChannel(msg, args)
alias = alias.replace('$channel', channel)
tokens = callbacks.tokenize(alias)
if not wildcard and biggestDollar or biggestAt:
args = privmsgs.getArgs(args,
required=biggestDollar,
optional=biggestAt)
# Gotta have a mutable sequence (for replace).
if biggestDollar + biggestAt == 1: # We got a string, no tuple.
args = [args]
def regexpReplace(m):
idx = int(m.group(1))
return args[idx-1]
def replace(tokens, replacer):
for (i, token) in enumerate(tokens):
if isinstance(token, list):
replace(token, replacer)
else:
tokens[i] = replacer(token)
replace(tokens, lambda s: dollarRe.sub(regexpReplace, s))
if biggestAt:
assert not wildcard
args = args[biggestDollar:]
replace(tokens, lambda s: atRe.sub(regexpReplace, s))
if wildcard:
assert not biggestAt
# Gotta remove the things that have already been subbed in.
i = biggestDollar
while i:
args.pop(0)
i -= 1
def everythingReplace(tokens):
for (i, token) in enumerate(tokens):
if isinstance(token, list):
if everythingReplace(token):
return
if token == '$*':
tokens[i:i+1] = args
return True
elif '$*' in token:
tokens[i] = token.replace('$*', ' '.join(args))
return True
return False
everythingReplace(tokens)
self.Proxy(irc, msg, tokens)
doc ='<an alias, %s>\n\nAlias for <<%s>>' % \
(utils.nItems('argument', biggestDollar), alias)
f = utils.changeFunctionName(f, name, doc)
return f
conf.registerPlugin('Alias')
conf.registerGroup(conf.supybot.plugins.Alias, 'aliases')
class Alias(callbacks.Privmsg):
def __init__(self):
callbacks.Privmsg.__init__(self)
# Schema: {alias: [command, locked]}
self.aliases = {}
group = conf.supybot.plugins.Alias.aliases
for (name, alias) in registry._cache.iteritems():
name = name.lower()
if name.startswith('supybot.plugins.alias.aliases.'):
name = name[len('supybot.plugins.alias.aliases.'):]
if '.' in name:
continue
conf.registerGlobalValue(group, name, registry.String('', ''))
conf.registerGlobalValue(group.get(name), 'locked',
registry.Boolean(False, ''))
for (name, value) in group.getValues(fullNames=False):
name = name.lower() # Just in case.
command = value()
locked = value.locked()
self.aliases[name] = [command, locked]
def __call__(self, irc, msg):
# Adding the aliases requires an Irc. So the first time we get called
# with an Irc, we add our aliases and then delete ourselves :)
for (alias, (command, locked)) in self.aliases.items():
try:
self.addAlias(irc, alias, command, locked)
except Exception, e:
self.log.exception('Exception when trying to add alias %s. '
'Removing from the Alias database.' % alias)
del self.aliases[alias]
del self.__class__.__call__
callbacks.Privmsg.__call__(self, irc, msg)
def lock(self, irc, msg, args, name):
"""<alias>
Locks an alias so that no one else can change it.
"""
if hasattr(self, name) and self.isCommand(name):
self.aliases[name][1] = True
conf.supybot.plugins.Alias.aliases.get(name).locked.setValue(True)
irc.replySuccess()
else:
irc.error('There is no such alias.')
lock = wrap(lock, [('checkCapability', 'admin'), 'commandName'])
def unlock(self, irc, msg, args, name):
"""<alias>
Unlocks an alias so that people can define new aliases over it.
"""
if hasattr(self, name) and self.isCommand(name):
self.aliases[name][1] = False
conf.supybot.plugins.Alias.aliases.get(name).locked.setValue(False)
irc.replySuccess()
else:
irc.error('There is no such alias.')
unlock = wrap(unlock, [('checkCapability', 'admin'), 'commandName'])
_invalidCharsRe = re.compile(r'[\[\]\s]')
def addAlias(self, irc, name, alias, lock=False):
if self._invalidCharsRe.search(name):
raise AliasError, 'Names cannot contain spaces or square brackets.'
if '|' in name:
raise AliasError, 'Names cannot contain pipes.'
if irc.getCallback(name):
raise AliasError, 'Names cannot coincide with names of plugins.'
realName = callbacks.canonicalName(name)
if name != realName:
s = 'That name isn\'t valid. Try %s instead.' % \
utils.quoted(realName)
raise AliasError, s
name = realName
cbs = callbacks.findCallbackForCommand(irc, name)
if self in cbs:
if hasattr(self, realName) and realName not in self.aliases:
s = 'You can\'t overwrite commands in this plugin.'
raise AliasError, s
if name in self.aliases:
(currentAlias, locked) = self.aliases[name]
if locked and currentAlias != alias:
raise AliasError, 'Alias %s is locked.' % utils.quoted(name)
try:
f = makeNewAlias(name, alias)
except RecursiveAlias:
raise AliasError, 'You can\'t define a recursive alias.'
if name in self.aliases:
# We gotta remove it so its value gets updated.
conf.supybot.plugins.Alias.aliases.unregister(name)
conf.supybot.plugins.Alias.aliases.register(name,
registry.String(alias, ''))
conf.supybot.plugins.Alias.aliases.get(name).register('locked',
registry.Boolean(lock, ''))
setattr(self.__class__, name, f)
self.aliases[name] = [alias, lock]
def removeAlias(self, name, evenIfLocked=False):
name = callbacks.canonicalName(name)
if hasattr(self, name) and self.isCommand(name):
if evenIfLocked or not self.aliases[name][1]:
delattr(self.__class__, name)
del self.aliases[name]
conf.supybot.plugins.Alias.aliases.unregister(name)
else:
raise AliasError, 'That alias is locked.'
else:
raise AliasError, 'There is no such alias.'
def add(self, irc, msg, args, name, alias):
"""<name> <alias>
Defines an alias <name> that executes <alias>. The <alias>
should be in the standard "command argument [nestedcommand argument]"
arguments to the alias; they'll be filled with the first, second, etc.
arguments. $1, $2, etc. can be used for required arguments. @1, @2,
etc. can be used for optional arguments. $* simply means "all
remaining arguments," and cannot be combined with optional arguments.
"""
if ' ' not in alias:
# If it's a single word, they probably want $*.
alias += ' $*'
try:
self.addAlias(irc, name, alias)
self.log.info('Adding alias %s for %s (from %s)' %
(utils.quoted(name), utils.quoted(alias),msg.prefix))
irc.replySuccess()
except AliasError, e:
irc.error(str(e))
add = wrap(add, ['commandName', 'text'])
def remove(self, irc, msg, args, name):
"""<name>
Removes the given alias, if unlocked.
"""
try:
self.removeAlias(name)
self.log.info('Removing alias %s (from %s)' % (utils.quoted(name),
msg.prefix))
irc.replySuccess()
except AliasError, e:
irc.error(str(e))
remove = wrap(remove, ['commandName'])
Class = Alias
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,605 +0,0 @@
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Amazon module, to use Amazon's Web Services.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jamessan
import getopt
import amazon
import supybot.conf as conf
import supybot.utils as utils
from supybot.commands import *
import supybot.plugins as plugins
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks
def configure(advanced):
from supybot.questions import output, expect, anything, something, yn
output('To use Amazon\'s Web Services, you must have a license key.')
if yn('Do you have a license key?'):
key = anything('What is it?')
conf.registerPlugin('Amazon', True)
conf.supybot.plugins.Amazon.licenseKey.set(key)
else:
output("""You'll need to get a key before you can use this plugin.
You can apply for a key at
http://www.amazon.com/webservices/""")
class Region(registry.OnlySomeStrings):
validStrings = ('us', 'uk', 'de', 'jp')
class LicenseKey(registry.String):
def set(self, s):
# In case we decide we need to recover
original = getattr(self, 'value', self._default)
registry.String.set(self, s)
if self.value:
amazon.setLicense(self.value)
conf.registerPlugin('Amazon')
conf.registerChannelValue(conf.supybot.plugins.Amazon, 'bold',
registry.Boolean(True, """Determines whether the results are bolded."""))
conf.registerGlobalValue(conf.supybot.plugins.Amazon, 'licenseKey',
LicenseKey('', """Sets the license key for using Amazon Web Services.
Must be set before any other commands in the plugin are used.""",
private=True))
conf.registerChannelValue(conf.supybot.plugins.Amazon, 'linkSnarfer',
registry.Boolean(False, """Determines whether the bot will reply to
Amazon.com URLs in the channel with a description of the item at the
URL."""))
conf.registerChannelValue(conf.supybot.plugins.Amazon, 'region', Region('us',
"""Determines the region that will be used when performing searches."""))
class Amazon(callbacks.PrivmsgCommandAndRegexp):
threaded = True
callBefore = ['URL']
regexps = ['amzSnarfer']
def __init__(self):
self.__parent = super(Amazon, self)
self.__parent.__init__()
def callCommand(self, name, irc, msg, *L, **kwargs):
try:
self.__parent.callCommand(name, irc, msg, *L, **kwargs)
except amazon.NoLicenseKey, e:
irc.error('You must have a free Amazon web services license key '
'in order to use this command. You can get one at '
'<http://www.amazon.com/webservices>. Once you have '
'one, you can set it with the command '
'"config supybot.plugins.Amazon.licensekey <key>".')
def _genResults(self, reply, attribs, items, url, bold):
results = {}
res = []
bold_item = 'title'
if isinstance(items, amazon.Bag):
items = [items]
for item in items:
try:
for k,v in attribs.iteritems():
results[v] = getattr(item, k, 'unknown')
if isinstance(results[v], amazon.Bag):
results[v] = getattr(results[v], k[:-1], 'unknown')
if not isinstance(results[v], basestring):
results[v] = utils.commaAndify(results[v])
if bold_item in results:
if bold:
results[bold_item] = ircutils.bold(results[bold_item])
else:
results[bold_item] = '"%s"' % results[bold_item]
if not url:
results['url'] = ''
else:
results['url'] = ' <%s>' % results['url']
s = reply % results
if isinstance(s, unicode):
s = s.encode('utf-8')
res.append(str(s))
except amazon.AmazonError, e:
self.log.warning(str(e))
except UnicodeEncodeError, e:
self.log.warning(str(e))
return res
def isbn(self, irc, msg, args, optlist, isbn):
"""[--url] <isbn>
Returns the book matching the given ISBN number. If --url is
specified, a link to amazon.com's page for the book will also be
returned.
"""
url = False
for (option, argument) in optlist:
if option == 'url':
url = True
isbn = isbn.replace('-', '').replace(' ', '')
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'Authors' : 'author',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s, written by %(author)s; published by ' \
'%(publisher)s; price: %(price)s%(url)s'
channel = msg.args[0]
bold = self.registryValue('bold', channel)
region = self.registryValue('region', channel)
try:
book = amazon.searchByKeyword(isbn, locale=region)
res = self._genResults(s, attribs, book, url, bold)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.reply('No book was found with that ISBN.')
isbn = wrap(isbn, [getopts({'url':''}), 'text'])
def books(self, irc, msg, args, optlist, keyword):
"""[--url] <keywords>
Returns the books matching the given <keywords> search. If --url is
specified, a link to amazon.com's page for the book will also be
returned.
"""
url = False
for (option, _) in optlist:
if option == 'url':
url = True
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'Authors' : 'author',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s, written by %(author)s; published by ' \
'%(publisher)s; price: %(price)s%(url)s'
channel = msg.args[0]
region = self.registryValue('region', channel)
bold = self.registryValue('bold', channel)
try:
books = amazon.searchByKeyword(keyword, locale=region)
res = self._genResults(s, attribs, books, url, bold)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.reply('No books were found with that keyword search.')
books = wrap(books, [getopts({'url':''}), 'text'])
def videos(self, irc, msg, args, optlist, keyword):
"""[--url] [--{dvd,vhs}] <keywords>
Returns the videos matching the given <keyword> search. If --url is
specified, a link to amazon.com's page for the video will also be
returned. Search defaults to using --dvd.
"""
url = False
product = 'dvd'
for (option, _) in optlist:
if option == 'url':
url = True
else:
product = option
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'MpaaRating' : 'mpaa',
'Media' : 'media',
'ReleaseDate' : 'date',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s (%(media)s), rated %(mpaa)s; released ' \
'%(date)s; published by %(publisher)s; price: %(price)s%(url)s'
channel = msg.args[0]
region = self.registryValue('region', channel)
bold = self.registryValue('bold', channel)
try:
videos = amazon.searchByKeyword(keyword, product_line=product,
locale=region)
res = self._genResults(s, attribs, videos, url, bold)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.reply('No videos were found with that keyword search.')
videos = wrap(videos, [getopts({'url':'', 'dvd':'', 'vhs':''}), 'text'])
def asin(self, irc, msg, args, optlist, asin):
"""[--url] <asin>
Returns the item matching the given ASIN number. If --url is
specified, a link to amazon.com's page for the item will also be
returned.
"""
url = False
for (option, _) in optlist:
if option == 'url':
url = True
asin = asin.replace('-', '').replace(' ', '')
attribs = {'ProductName' : 'title',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s; price: %(price)s%(url)s'
channel = msg.args[0]
region = self.registryValue('region', channel)
bold = self.registryValue('bold', channel)
try:
item = amazon.searchByASIN(asin, locale=region)
res = self._genResults(s, attribs, item, url, bold)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.reply('No item was found with that ASIN.')
asin = wrap(asin, [getopts({'url':''}), 'text'])
def upc(self, irc, msg, args, optlist, upc):
"""[--url] <upc>
Returns the item matching the given UPC number. If --url is
specified, a link to amazon.com's page for the item will also be
returned. Only items in the following categories may be found via upc
search: music, classical, software, dvd, video, vhs, electronics,
pc-hardware, and photo.
"""
url = False
for (option, _) in optlist:
if option == 'url':
url = True
upc = upc.replace('-', '').replace(' ', '')
attribs = {'ProductName' : 'title',
'Manufacturer' : 'manufacturer',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s %(manufacturer)s; price: %(price)s%(url)s'
channel = msg.args[0]
region = self.registryValue('region', channel)
bold = self.registryValue('bold', channel)
try:
item = amazon.searchByUPC(upc, locale=region)
res = self._genResults(s, attribs, item, url, bold)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.reply('No item was found with that UPC.')
upc = wrap(upc, [getopts({'url':''}), 'text'])
def author(self, irc, msg, args, optlist, author):
"""[--url] <author>
Returns a list of books written by the given author. If --url is
specified, a link to amazon.com's page for the book will also be
returned.
"""
url = False
for (option, argument) in optlist:
if option == 'url':
url = True
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'Authors' : 'author',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s, written by %(author)s; published by ' \
'%(publisher)s; price: %(price)s%(url)s'
channel = msg.args[0]
region = self.registryValue('region', channel)
bold = self.registryValue('bold', channel)
try:
books = amazon.searchByAuthor(author, locale=region)
res = self._genResults(s, attribs, books, url, bold)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.reply('No books were found by that author.')
author = wrap(author, [getopts({'url':''}), 'text'])
# FIXME: Until I get a *good* list of categories (ones that actually work),
# these commands will remain unavailable
'''
_textToNode = {'dvds':'130', 'magazines':'599872', 'music':'301668',
'software':'491286', 'vhs':'404272', 'kitchen':'491864',
'video games':'471280', 'toys':'491290', 'camera':'502394',
'outdoor':'468250', 'computers':'565118', 'tools':'468240',
'electronics':'172282'
}
def categories(self, irc, msg, args):
"""takes no arguments
Returns a list of valid categories to use with the bestsellers
commands.
"""
cats = self._textToNode.keys()
cats.sort()
irc.reply(utils.commaAndify(cats))
categories = wrap(categories)
def bestsellers(self, irc, msg, args, optlist, category):
"""[--url] <category>
Returns a list of best selling items in <category>. The 'categories'
command will return a list of the available categories. If --url
is specified, a link to amazon.com's page for the item will also be
returned.
"""
url = False
for (option, _) in optlist:
if option == 'url':
url = True
if category not in self._textToNode:
irc.error('An invalid category was specified. The categories'
' command will return a list of valid categories')
return
category = self._textToNode[category]
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'URL' : 'url'
}
s = '"%(title)s", from %(publisher)s.%(url)s'
channel = msg.args[0]
region = self.registryValue('region', channel)
bold = self.registryValue('bold', channel)
try:
items = amazon.browseBestSellers(category, locale=region)
res = self._genResults(s, attribs, items, url, bold)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.reply('No items were found on that best seller list.')
bestsellers = wrap(bestsellers, [getopts({'url':''}), rest('lowered')])
'''
def artist(self, irc, msg, args, optlist, artist):
"""[--url] [--{music,classical}] <artist>
Returns a list of items by the given artist. If --url is specified, a
link to amazon.com's page for the match will also be returned. The
search defaults to using --music.
"""
url = False
product = None
for (option, _) in optlist:
if option == 'url':
url = True
else:
product = option
product = product or 'music'
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'Artists' : 'artist',
'Media' : 'media',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s (%(media)s), by %(artist)s; published by ' \
'%(publisher)s; price: %(price)s%(url)s'
channel = msg.args[0]
region = self.registryValue('region', channel)
bold = self.registryValue('bold', channel)
try:
items = amazon.searchByArtist(artist, product_line=product,
locale=region)
res = self._genResults(s, attribs, items, url, bold)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.reply('No items were found by that artist.')
artist = wrap(artist, [getopts({'music':'', 'classical':'', 'url':''}),
'text'])
def actor(self, irc, msg, args, optlist, actor):
"""[--url] [--{dvd,vhs,video}] <actor>
Returns a list of items starring the given actor. If --url is
specified, a link to amazon.com's page for the match will also be
returned. The search defaults to using --dvd.
"""
url = False
product = ''
for (option, _) in optlist:
if option == 'url':
url = True
else:
product = option
product = product or 'dvd'
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'MpaaRating' : 'mpaa',
'Media' : 'media',
'ReleaseDate' : 'date',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s (%(media)s), rated %(mpaa)s; released ' \
'%(date)s; published by %(publisher)s; price: %(price)s%(url)s'
channel = msg.args[0]
region = self.registryValue('region', channel)
bold = self.registryValue('bold', channel)
try:
items = amazon.searchByActor(actor, product_line=product,
locale=region)
res = self._genResults(s, attribs, items, url, bold)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.reply('No items were found starring that actor.')
actor = wrap(actor, [getopts({'dvd': '', 'video': '', 'vhs':'', 'url':''}),
'text'])
def director(self, irc, msg, args, optlist, director):
"""[--url] [--{dvd,vhs,video}] <director>
Returns a list of items by the given director. If --url is
specified, a link to amazon.com's page for the match will also be
returned. The search defaults to using --dvd.
"""
url = False
product = None
for (option, _) in optlist:
if option == 'url':
url = True
else:
product = option
product = product or 'dvd'
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'MpaaRating' : 'mpaa',
'Media' : 'media',
'ReleaseDate' : 'date',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s (%(media)s), rated %(mpaa)s; released ' \
'%(date)s; published by %(publisher)s; price: %(price)s%(url)s'
channel = msg.args[0]
region = self.registryValue('region', channel)
bold = self.registryValue('bold', channel)
try:
items = amazon.searchByDirector(director, product_line=product,
locale=region)
res = self._genResults(s, attribs, items, url, bold)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.reply('No items were found by that director.')
director = wrap(director, [getopts({'dvd': '', 'video': '', 'vhs': '',
'url': ''}),
'text'])
def manufacturer(self, irc, msg, args, optlist, manufacturer):
""" [--url] \
[--{pc-hardware,kitchen,electronics,videogames,software,photo}] \
<manufacturer>
Returns a list of items by the given manufacturer. If --url is
specified, a link to amazon.com's page for the match will also be
returned. The search defaults to using --pc-hardware.
"""
url = False
product = None
for (option, _) in optlist:
if option == 'url':
url = True
else:
product = option
product = product or 'pc-hardware'
attribs = {'ProductName' : 'title',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s; price: %(price)s%(url)s'
channel = msg.args[0]
region = self.registryValue('region', channel)
bold = self.registryValue('bold', channel)
try:
items = amazon.searchByManufacturer(manufacturer,
product_line=product,
locale=region)
res = self._genResults(s, attribs, items, url, bold)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.reply('No items were found by that manufacturer.')
manufacturer = wrap(manufacturer,
[getopts({'url':'', 'electronics':'', 'kitchen':'',
'videogames':'', 'software':'', 'photo':'',
'pc-hardware':'',
}),
'text'])
def amzSnarfer(self, irc, msg, match):
r"http://www.amazon.com/exec/obidos/(?:tg/detail/-/|ASIN/)([^/]+)"
if not self.registryValue('linkSnarfer', msg.args[0]):
return
match = match.group(1)
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'Authors' : 'author',
'MpaaRating' : 'mpaa',
'Media' : 'media',
'ReleaseDate' : 'date',
'OurPrice' : 'price',
'Artists' : 'artist',
}
s = '%(title)s; %(artist)s; %(author)s; %(mpaa)s; %(media)s; '\
'%(date)s; %(publisher)s; price: %(price)s'
channel = msg.args[0]
region = self.registryValue('region', channel)
bold = self.registryValue('bold', channel)
try:
item = amazon.searchByASIN(match, locale=region)
res = self._genResults(s, attribs, item, False, bold)
if res:
res = utils.commaAndify(res)
res = res.replace('; unknown', '')
res = res.replace('; price: unknown', '')
irc.reply(res, prefixName=False)
return
except amazon.AmazonError, e:
pass
self.log.debug('No item was found with that ASIN.')
amzSnarfer = urlSnarfer(amzSnarfer)
Class = Amazon
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,123 +0,0 @@
###
# Copyright (c) 2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Allows folks to talk through the bot anonymously.
"""
__revision__ = "$Id$"
import supybot.plugins as plugins
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircdb as ircdb
from supybot.commands import *
import supybot.ircmsgs as ircmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks
def configure(advanced):
# This will be called by setup.py to configure this module. Advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('Anonymous', True)
conf.registerPlugin('Anonymous')
conf.registerChannelValue(conf.supybot.plugins.Anonymous,
'requirePresenceInChannel', registry.Boolean(True, """Determines whether
the bot should require people trying to use this plugin to be in the
channel they wish to anonymously send to."""))
conf.registerGlobalValue(conf.supybot.plugins.Anonymous, 'requireRegistration',
registry.Boolean(True, """Determines whether the bot should require people
trying to use this plugin to be registered."""))
conf.registerGlobalValue(conf.supybot.plugins.Anonymous, 'requireCapability',
registry.String('', """Determines what capability (if any) the bot should
require people trying to use this plugin to have."""))
conf.registerGlobalValue(conf.supybot.plugins.Anonymous, 'allowPrivateTarget',
registry.Boolean(False, """Determines whether the bot will require targets
of the "say" command to be public (i.e., channels). If this is True, the
bot will allow people to use the "say" command to send private messages to
other users."""))
class Anonymous(callbacks.Privmsg):
private = True
def _preCheck(self, irc, msg, channel):
if self.registryValue('requireRegistration'):
try:
_ = ircdb.users.getUser(msg.prefix)
except KeyError:
irc.errorNotRegistered(Raise=True)
capability = self.registryValue('requireCapability')
if capability:
if not ircdb.checkCapability(msg.prefix, capability):
irc.errorNoCapability(capability, Raise=True)
if self.registryValue('requirePresenceInChannel', channel) and \
msg.nick not in irc.state.channels[channel].users:
irc.error('You must be in %s to "say" in there.' % channel,
Raise=True)
c = ircdb.channels.getChannel(channel)
if c.lobotomized:
irc.error('I\'m lobotomized in %s.' % channel, Raise=True)
if not c._checkCapability(self.name()):
irc.error('That channel has set its capabilities so as to '
'disallow the use of this plugin.', Raise=True)
def say(self, irc, msg, args, channel, text):
"""<channel> <text>
Sends <text> to <channel>.
"""
self._preCheck(irc, msg, channel)
self.log.info('Saying %s in %s due to %s.',
utils.quoted(text), channel, msg.prefix)
irc.queueMsg(ircmsgs.privmsg(channel, text))
irc.noReply()
say = wrap(say, ['inChannel', 'text'])
def do(self, irc, msg, args, channel, text):
"""<channel> <action>
Performs <action> in <channel>.
"""
self._preCheck(irc, msg, channel)
self.log.info('Performing %s in %s due to %s.',
utils.quoted(text), channel, msg.prefix)
irc.queueMsg(ircmsgs.action(channel, text))
irc.noReply()
do = wrap(do, ['inChannel', 'text'])
Class = Anonymous
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,139 +0,0 @@
###
# Copyright (c) 2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Automatically ops, voices, or halfops, or bans people when they join a channel,
according to their capabilities. If you want your bot automatically op users
when they join your channel, this is the plugin to load.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jemfinch
__contributors__ = {}
import time
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircdb as ircdb
import supybot.plugins as plugins
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.schedule as schedule
import supybot.callbacks as callbacks
def configure(advanced):
# This will be called by setup.py to configure this module. Advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('AutoMode', True)
AutoMode = conf.registerPlugin('AutoMode')
conf.registerChannelValue(AutoMode, 'enable',
registry.Boolean(True, """Determines whether this plugin is enabled."""))
conf.registerChannelValue(AutoMode, 'fallthrough',
registry.Boolean(False, """Determines whether the bot will "fall through" to
halfop/voicing when auto-opping is turned off but auto-halfopping/voicing
are turned on."""))
conf.registerChannelValue(AutoMode, 'op',
registry.Boolean(True, """Determines whether the bot will automatically op
people with the <channel>,op capability when they join the channel."""))
conf.registerChannelValue(AutoMode, 'halfop',
registry.Boolean(True, """Determines whether the bot will automatically
halfop people with the <channel>,halfop capability when they join the
channel."""))
conf.registerChannelValue(AutoMode, 'voice',
registry.Boolean(True, """Determines whether the bot will automatically
voice people with the <channel>,voice capability when they join the
channel."""))
conf.registerChannelValue(AutoMode, 'ban',
registry.Boolean(True, """Determines whether the bot will automatically ban
people who join the channel and are on the banlist."""))
conf.registerChannelValue(AutoMode.ban, 'period',
registry.PositiveInteger(86400, """Determines how many seconds the bot will
automatically ban a person when banning."""))
class Continue(Exception):
pass # Used below, look in the "do" function nested in doJoin.
class AutoMode(callbacks.Privmsg):
def doJoin(self, irc, msg):
channel = msg.args[0]
if ircutils.strEqual(irc.nick, msg.nick):
return
if not self.registryValue('enable', channel):
return
fallthrough = self.registryValue('fallthrough', channel)
def do(type):
cap = ircdb.makeChannelCapability(channel, type)
if ircdb.checkCapability(msg.prefix, cap):
if self.registryValue(type, channel):
self.log.info('Sending auto-%s of %s in %s.',
type, msg.prefix, channel)
msgmaker = getattr(ircmsgs, type)
irc.queueMsg(msgmaker(channel, msg.nick))
raise Continue # Even if fallthrough, let's only do one.
elif not fallthrough:
self.log.debug('%s has %s, but supybot.plugins.AutoMode.%s'
' is not enabled in %s, refusing to fall '
'through.', msg.prefix, cap, type, channel)
raise Continue
try:
do('op')
if 'h' in irc.state.supported['prefix']:
do('halfop')
do('voice')
except Continue:
return
c = ircdb.channels.getChannel(channel)
if c.checkBan(msg.prefix) and self.registryValue('ban', channel):
period = self.registryValue('ban.period', channel)
if period:
def unban():
try:
if msg.prefix in irc.state.channels[channel].bans:
irc.queueMsg(ircmsgs.unban(channel, msg.prefix))
except KeyError:
# We're not in the channel anymore.
pass
schedule.addEvent(unban, time.time()+period)
irc.queueMsg(ircmsgs.ban(channel, msg.prefix))
irc.queueMsg(ircmsgs.kick(channel, msg.nick))
Class = AutoMode
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,196 +0,0 @@
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Babelfish-related commands.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jamessan
import sets
import random
from itertools import imap
import babelfish
import supybot.conf as conf
import supybot.utils as utils
from supybot.commands import *
import supybot.registry as registry
import supybot.callbacks as callbacks
class Languages(registry.OnlySomeStrings):
validStrings = tuple(map(str.capitalize, babelfish.available_languages))
normalize = staticmethod(str.capitalize)
class SpaceSeparatedListOfLanguages(registry.SeparatedListOf):
List = sets.Set
Value = Languages
def splitter(self, s):
return s.split()
joiner = ' '.join
conf.registerPlugin('Babelfish')
conf.registerChannelValue(conf.supybot.plugins.Babelfish, 'languages',
SpaceSeparatedListOfLanguages(babelfish.available_languages, """Determines
which languages are available for translation; valid input is a list of
languages separated by spaces."""))
class Babelfish(callbacks.Privmsg):
threaded = True
_abbrevs = utils.abbrev(map(str.lower, babelfish.available_languages))
_abbrevs['de'] = 'german'
_abbrevs['jp'] = 'japanese'
_abbrevs['kr'] = 'korean'
_abbrevs['es'] = 'spanish'
_abbrevs['pt'] = 'portuguese'
_abbrevs['it'] = 'italian'
_abbrevs['zh'] = 'chinese_simple'
_abbrevs['zt'] = 'chinese_traditional'
_abbrevs['nl'] = 'dutch'
_abbrevs['el'] = 'greek'
for language in babelfish.available_languages:
_abbrevs[language] = language
def _getLang(self, fromLang, toLang, chan):
fromLang = self._abbrevs[fromLang.lower()]
toLang = self._abbrevs[toLang.lower()]
languages = map(str.lower, self.registryValue('languages',chan))
if fromLang not in languages:
fromLang = None
if toLang not in languages:
toLang = None
return (fromLang, toLang)
def languages(self, irc, msg, args):
"""takes no arguments
Returns the languages that Babelfish can translate to/from.
"""
irc.reply(utils.commaAndify(babelfish.available_languages))
def translate(self, irc, msg, args, fromLang, toLang, text):
"""<from-language> [to] <to-language> <text>
Returns <text> translated from <from-language> into <to-language>.
Beware that translating to or from languages that use multi-byte
characters may result in some very odd results.
"""
chan = msg.args[0]
try:
(fromLang, toLang) = self._getLang(fromLang, toLang, chan)
if not fromLang or not toLang:
langs = self.registryValue('languages', chan)
if not langs:
irc.error('I do not speak any other languages.')
return
else:
irc.error('I only speak %s.' % utils.commaAndify(langs))
return
translation = babelfish.translate(text, fromLang, toLang)
irc.reply(utils.htmlToText(translation))
except (KeyError, babelfish.LanguageNotAvailableError), e:
languages = self.registryValue('languages', chan)
if languages:
languages = 'Valid languages include %s' % \
utils.commaAndify(sorted(languages))
else:
languages = 'I do not speak any other languages.'
irc.errorInvalid('language', str(e), languages)
except babelfish.BabelizerIOError, e:
irc.error(str(e))
except babelfish.BabelfishChangedError, e:
irc.error('Babelfish has foiled our plans by changing its '
'webpage format.')
translate = wrap(translate, ['something', 'to', 'something', 'text'])
def babelize(self, irc, msg, args, fromLang, toLang, text):
"""<from-language> <to-language> <text>
Translates <text> repeatedly between <from-language> and <to-language>
until it doesn't change anymore or 12 times, whichever is fewer. One
of the languages must be English.
"""
chan = msg.args[0]
try:
(fromLang, toLang) = self._getLang(fromLang, toLang, chan)
if fromLang != 'english' and toLang != 'english':
irc.error('One language in babelize must be English.')
return
if not fromLang or not toLang:
langs = self.registryValue('languages', chan)
if not langs:
irc.error('I do not speak any other languages.')
return
else:
irc.error('I only speak %s.' % utils.commaAndify(langs,
And='or'))
return
translations = babelfish.babelize(text, fromLang, toLang)
irc.reply(utils.htmlToText(translations[-1]))
except (KeyError, babelfish.LanguageNotAvailableError), e:
languages = self.registryValue('languages', chan)
if languages:
languages = 'Valid languages include %s' % \
utils.commaAndify(sorted(languages))
else:
languages = 'I do not speak any other languages.'
irc.errorInvalid('language', str(e), languages)
except babelfish.BabelizerIOError, e:
irc.reply(e)
except babelfish.BabelfishChangedError, e:
irc.reply('Babelfish has foiled our plans by changing its '
'webpage format.')
babelize = wrap(babelize, ['something', 'something', 'text'])
def randomlanguage(self, irc, msg, args, optlist):
"""[--allow-english]
Returns a random language supported by babelfish. If --allow-english
is provided, will include English in the list of possible languages.
"""
allowEnglish = False
for (option, arg) in optlist:
if option == 'allow-english':
allowEnglish = True
languages = self.registryValue('languages', msg.args[0])
if not languages:
irc.error('I can\'t speak any other languages.', Raise=True)
language = random.choice(languages)
while not allowEnglish and language == 'English':
language = random.choice(languages)
irc.reply(language)
randomlanguage = wrap(randomlanguage, [getopts({'allow-english': ''})])
Class = Babelfish
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,194 +0,0 @@
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Filters bad words on outgoing messages from the bot, so the bot can't be made
to say bad words.
"""
__revision__ = "$Id$"
import supybot.plugins as plugins
import re
import math
import sets
import time
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircdb as ircdb
import supybot.ircmsgs as ircmsgs
from supybot.commands import *
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks
def configure(advanced):
from supybot.questions import output, expect, anything, something, yn
conf.registerPlugin('BadWords', True)
if yn('Would you like to add some bad words?'):
words = anything('What words? (separate individual words by spaces)')
conf.supybot.plugins.BadWords.words.set(words)
class LastModifiedSetOfStrings(registry.SpaceSeparatedListOfStrings):
List = sets.Set
lastModified = 0
def setValue(self, v):
self.lastModified = time.time()
registry.SpaceSeparatedListOfStrings.setValue(self, v)
conf.registerPlugin('BadWords')
conf.registerGlobalValue(conf.supybot.plugins.BadWords, 'words',
LastModifiedSetOfStrings([], """Determines what words are
considered to be 'bad' so the bot won't say them."""))
conf.registerGlobalValue(conf.supybot.plugins.BadWords,'requireWordBoundaries',
registry.Boolean(False, """Determines whether the bot will require bad
words to be independent words, or whether it will censor them within other
words. For instance, if 'darn' is a bad word, then if this is true, 'darn'
will be censored, but 'darnit' will not. You probably want this to be
false."""))
class String256(registry.String):
def __call__(self):
s = registry.String.__call__(self)
return s * (1024/len(s))
def __str__(self):
return self.value
conf.registerGlobalValue(conf.supybot.plugins.BadWords, 'nastyChars',
String256('!@#&', """Determines what characters will replace bad words; a
chunk of these characters matching the size of the replaced bad word will
be used to replace the bad words you've configured."""))
class ReplacementMethods(registry.OnlySomeStrings):
validStrings = ('simple', 'nastyCharacters')
conf.registerGlobalValue(conf.supybot.plugins.BadWords, 'replaceMethod',
ReplacementMethods('nastyCharacters', """Determines the manner in which
bad words will be replaced. 'nastyCharacters' (the default) will replace a
bad word with the same number of 'nasty characters' (like those used in
comic books; configurable by supybot.plugins.BadWords.nastyChars).
'simple' will replace a bad word with a simple strings (regardless of the
length of the bad word); this string is configurable via
supybot.plugins.BadWords.simpleReplacement."""))
conf.registerGlobalValue(conf.supybot.plugins.BadWords,'simpleReplacement',
registry.String('[CENSORED]', """Determines what word will replace bad
words if the replacement method is 'simple'."""))
conf.registerGlobalValue(conf.supybot.plugins.BadWords, 'stripFormatting',
registry.Boolean(True, """Determines whether the bot will strip
formatting characters from messages before it checks them for bad words.
If this is False, it will be relatively trivial to circumvent this plugin's
filtering. If it's True, however, it will interact poorly with other
plugins that do coloring or bolding of text."""))
class BadWords(privmsgs.CapabilityCheckingPrivmsg):
priority = 1
capability = 'admin'
def __init__(self):
privmsgs.CapabilityCheckingPrivmsg.__init__(self)
# This is so we can not filter certain outgoing messages (like list,
# which would be kinda useless if it were filtered).
self.filtering = True
self.lastModified = 0
self.words = conf.supybot.plugins.BadWords.words
def sub(self, m):
replaceMethod = self.registryValue('replaceMethod')
if replaceMethod == 'simple':
return self.registryValue('simpleReplacement')
elif replaceMethod == 'nastyCharacters':
return self.registryValue('nastyChars')[:len(m.group(1))]
def inFilter(self, irc, msg):
self.filtering = True
return msg
def outFilter(self, irc, msg):
if self.filtering and msg.command == 'PRIVMSG':
if self.lastModified < self.words.lastModified:
self.makeRegexp(self.words())
self.lastModified = time.time()
s = msg.args[1]
if self.registryValue('stripFormatting'):
s = ircutils.stripFormatting(s)
s = self.regexp.sub(self.sub, s)
msg = ircmsgs.privmsg(msg.args[0], s, msg=msg)
return msg
def makeRegexp(self, iterable):
s = '(%s)' % '|'.join(map(re.escape, iterable))
if self.registryValue('requireWordBoundaries'):
s = r'\b%s\b' % s
self.regexp = re.compile(s, re.I)
def list(self, irc, msg, args):
"""takes no arguments
Returns the list of words being censored.
"""
L = list(self.words())
if L:
self.filtering = False
utils.sortBy(str.lower, L)
irc.reply(utils.commaAndify(L))
else:
irc.reply('I\'m not currently censoring any bad words.')
list = wrap(list)
def add(self, irc, msg, args, words):
"""<word> [<word> ...]
Adds all <word>s to the list of words the bot isn't to say.
"""
set = self.words()
set.update(words)
self.words.setValue(set)
irc.replySuccess()
add = wrap(add, [many('something')])
def remove(self, irc, msg, args, words):
"""<word> [<word> ...]
Removes a <word>s from the list of words the bot isn't to say.
"""
set = self.words()
for word in words:
set.discard(word)
self.words.setValue(set)
irc.replySuccess()
remove = wrap(remove, [many('something')])
Class = BadWords
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,214 +0,0 @@
###
# Copyright (c) 2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Watches for paste-floods in a channel and takes appropriate measures against
violators.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jemfinch
__contributors__ = {}
import supybot.plugins as plugins
import glob
import os.path
import reverend.thomas
from cStringIO import StringIO as sio
import supybot.conf as conf
import supybot.utils as utils
from supybot.commands import *
import supybot.ircutils as ircutils
import supybot.registry as registry
import supybot.callbacks as callbacks
def configure(advanced):
# This will be called by setup.py to configure this module. Advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('Bayes', True)
Bayes = conf.registerPlugin('Bayes')
conf.registerChannelValue(Bayes, 'maximumLines',
registry.NonNegativeInteger(4, """Determines the maximum allowable number
of consecutive messages that classify as a paste. If this value is 0, no
checking will be done."""))
def tokenize(s):
return s.lower().split()
class PickleBayesDB(plugins.DbiChannelDB):
class DB(object):
def __init__(self, filename):
self.filename = filename
self.nickFilename = self.filename.replace('pickle', 'nick.pickle')
self.bayes = reverend.thomas.Bayes(tokenize)
if os.path.exists(self.filename):
try:
self.bayes.load(self.filename)
except (EOFError, EnvironmentError), e:
log.error('Couldn\'t load bayes pickle from %s: %s',
self.filename, utils.exnToString(e))
self.nickBayes = reverend.thomas.Bayes(tokenize)
if os.path.exists(self.nickFilename):
try:
self.nickBayes.load(self.nickFilename)
except (EOFError, EnvironmentError), e:
log.error('Couldn\'t load nickbayes pickle from %s: %s',
self.nickFilename, utils.exnToString(e))
def close(self):
self.bayes.save(self.filename)
self.nickBayes.save(self.nickFilename)
flush = close
def train(self, kind, s):
self.bayes.train(kind, s)
def trainNick(self, nick, s):
self.nickBayes.train(nick, s)
def guess(self, s):
matches = self.bayes.guess(s)
if matches:
if matches[0][1] > 0.5:
if len(matches) > 1 and \
matches[0][1] - matches[1][1] < 0.4:
return None
else:
return matches[0]
else:
self.bayes.train('normal', s)
return None
def guessNick(self, s):
L = [t for t in self.nickBayes.guess(s) if t[1] > 0.01]
if len(L) > 1:
if L[0][1] / L[1][1] > 2:
return [L[0]]
return L
BayesDB = plugins.DB('Bayes', {'pickle': PickleBayesDB})
class Bayes(callbacks.Privmsg):
def __init__(self):
self.__parent = super(Bayes, self)
self.__parent.__init__()
global log
log = self.log
self.db = BayesDB()
def die(self):
self.db.close()
def doPrivmsg(self, irc, msg):
(channel, text) = msg.args
if not ircutils.isChannel(channel) or msg.guessed:
return
kind = self.db.guess(channel, text)
if kind is not None:
(kind, prob) = kind
prob *= 100
text = utils.ellipsisify(text, 30)
self.log.debug('Classified %s as %s. (%.2f%%)',
utils.quoted(text), kind, prob)
self.db.trainNick(channel, msg.nick, text)
def guess(self, irc, msg, args, channel, text):
"""[<channel>] <text>
Guesses how <text> should be classified according to the Bayesian
classifier for <channel>. <channel> is only necessary if the message
isn't sent in the channel itself, and then only if
supybot.databases.plugins.channelSpecific is True.
"""
msg.tag('guessed')
kind = self.db.guess(channel, text)
if kind is not None:
(kind, prob) = kind
prob *= 100
irc.reply('That seems to me to be %s, '
'but I\'m only %.2f certain.' % (kind, prob))
else:
irc.reply('I don\'t know what the heck that is.')
guess = wrap(guess, ['channeldb', 'something'])
def who(self, irc, msg, args, channel, text):
"""[<channel>] <text>
Guesses who might have said <text>. <channel> is only necessary if the
message isn't sent in the channel itself, and then only if
supybot.databases.plugins.channelSpecific is True.
"""
msg.tag('guessed')
kinds = self.db.guessNick(channel, text)
if kinds:
if len(kinds) == 1:
(kind, prob) = kinds.pop()
irc.reply('It seems to me (with %.2f%% certainty) '
'that %s said that.' % (prob*100, kind))
else:
kinds = ['%s (%.2f%%)' % (k, prob*100) for (k, prob) in kinds]
irc.reply('I\'m not quite sure who said that, but it could be '
+ utils.commaAndify(kinds, And='or'))
else:
irc.reply('I have no idea who might\'ve said that.')
who = wrap(who, ['channeldb', 'something'])
def train(self, irc, msg, args, channel, language, pattern):
"""[<channel>] <language> <glob>
Trains the bot to recognize text similar to that contained in the files
matching <glob> as text of the language <language>. <channel> is only
necessary if the message isn't sent in the channel itself, and then
only if supybot.databases.plugins.channelSpecific is True.
"""
filenames = glob.glob(pattern)
if not filenames:
irc.errorInvalid('glob', pattern)
for filename in filenames:
fd = file(filename)
for line in fd:
self.db.train(channel, language, line)
fd.close()
irc.replySuccess()
train = wrap(train, ['channeldb', 'something', 'something'])
Class = Bayes
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,464 +0,0 @@
###
# Copyright (c) 2003, Daniel Berlin
# Based on code from kibot
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Bugzilla bug retriever
"""
__revision__ = "$Id$"
import os
import re
import csv
import getopt
import urllib
import xml.dom.minidom as minidom
from itertools import imap, ifilter
from htmlentitydefs import entitydefs as entities
import supybot.conf as conf
import supybot.utils as utils
import supybot.plugins as plugins
from supybot.commands import *
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.webutils as webutils
import supybot.callbacks as callbacks
import supybot.structures as structures
statusKeys = ['unconfirmed', 'new', 'assigned', 'reopened', 'resolved',
'verified', 'closed']
resolutionKeys = ['fixed', 'invalid', 'worksforme', 'needinfo',
'test-request', 'wontfix', 'cantfix', 'moved', 'duplicate',
'remind', 'later', 'notabug', 'notgnome', 'incomplete',
'gnome1.x', 'moved']
priorityKeys = ['p1', 'p2', 'p3', 'p4', 'p5', 'Low', 'Normal', 'High',
'Immediate', 'Urgent']
severityKeys = ['enhancement', 'trivial', 'minor', 'normal', 'major',
'critical', 'blocker']
class BugzillaError(Exception):
"""A bugzilla error"""
pass
def configure(advanced):
from supybot.questions import output, expect, anything, yn
conf.registerPlugin('Bugzilla', True)
output("""The Bugzilla plugin has the functionality to watch for URLs
that match a specific pattern (we call this a snarfer). When
a Supybot sees such a URL, it will parse the web page for
information and reply with the results.""")
if yn('Do you want this bug snarfer enabled by default?', default=False):
conf.supybot.plugins.Bugzilla.bugSnarfer.setValue(True)
conf.registerPlugin('Bugzilla')
conf.registerChannelValue(conf.supybot.plugins.Bugzilla, 'bugSnarfer',
registry.Boolean(False, """Determines whether the bug snarfer will be
enabled, such that any Bugzilla URLs and bug ### seen in the channel
will have their information reported into the channel."""))
conf.registerChannelValue(conf.supybot.plugins.Bugzilla, 'bold',
registry.Boolean(True, """Determines whether results are bolded."""))
conf.registerChannelValue(conf.supybot.plugins.Bugzilla, 'replyNoBugzilla',
registry.String('I don\'t have a bugzilla %s.', """Determines the phrase
to use when notifying the user that there is no information about that
bugzilla site."""))
conf.registerChannelValue(conf.supybot.plugins.Bugzilla, 'snarfTarget',
registry.String('', """Determines the bugzilla site to query when the
snarf command is triggered"""))
class Bugzillae(registry.SpaceSeparatedListOfStrings):
List = ircutils.IrcSet
conf.registerGlobalValue(conf.supybot.plugins.Bugzilla, 'bugzillae',
Bugzillae([], """Determines what bugzillae will be added to the bot when it
starts."""))
def registerBugzilla(name, url='', description=''):
conf.supybot.plugins.Bugzilla.bugzillae().add(name)
group = conf.registerGroup(conf.supybot.plugins.Bugzilla.bugzillae, name)
URL = conf.registerGlobalValue(group, 'url', registry.String(url, ''))
DESC = conf.registerGlobalValue(group, 'description',
registry.String(description, ''))
if url:
URL.setValue(url)
if description:
DESC.setValue(description)
class Bugzilla(callbacks.PrivmsgCommandAndRegexp):
"""Show a link to a bug report with a brief description"""
threaded = True
callBefore = ['URL']
regexps = ['bzSnarfer', 'bugzSnarf']
def __init__(self):
callbacks.PrivmsgCommandAndRegexp.__init__(self)
self.entre = re.compile('&(\S*?);')
# Schema: {name, [url, description]}
self.db = ircutils.IrcDict()
for name in self.registryValue('bugzillae'):
registerBugzilla(name)
group = self.registryValue('bugzillae.%s' % name, value=False)
self.db[name] = [group.url(), group.description()]
self.shorthand = utils.abbrev(self.db.keys())
def keywords2query(self, keywords):
"""Turn a list of keywords into a URL query string"""
query = []
for k in keywords:
k = k.lower()
if k in statusKeys:
query.append('bug_status=%s' % k.upper())
elif k in resolutionKeys:
query.append('resolution=%s' % k.upper())
elif k in priorityKeys:
query.append('priority=%s' % k.upper())
elif k in severityKeys:
query.append('bug_severity=%s' % k.upper())
query.append('ctype=csv')
return query
def add(self, irc, msg, args, name, url, description):
"""<name> <url> [<description>]
Add a bugzilla <url> to the list of defined bugzillae. <name>
is the name that will be used to reference the bugzilla in all
commands. Unambiguous abbreviations of <name> will be accepted also.
<description> is the common name for the bugzilla and will
be listed with the bugzilla query; if not given, it defaults to <name>.
"""
if not description:
description = name
if url[-1] == '/':
url = url[:-1]
self.db[name] = [url, description]
registerBugzilla(name, url, description)
self.shorthand = utils.abbrev(self.db.keys())
irc.replySuccess()
add = wrap(add, ['something', 'url', additional('text')])
def remove(self, irc, msg, args, name):
"""<abbreviation>
Remove the bugzilla associated with <abbreviation> from the list of
defined bugzillae.
"""
try:
name = self.shorthand[name]
del self.db[name]
self.registryValue('bugzillae').remove(name)
self.shorthand = utils.abbrev(self.db.keys())
irc.replySuccess()
except KeyError:
s = self.registryValue('replyNoBugzilla', msg.args[0])
irc.error(s % name)
remove = wrap(remove, ['text'])
def list(self, irc, msg, args, name):
"""[<abbreviation>]
List defined bugzillae. If <abbreviation> is specified, list the
information for that bugzilla.
"""
if name:
try:
name = self.shorthand[name]
(url, description) = self.db[name]
irc.reply('%s: %s, %s' % (name, description, url))
except KeyError:
s = self.registryValue('replyNoBugzilla', msg.args[0])
irc.error(s % name)
else:
if self.db:
L = self.db.keys()
L.sort()
irc.reply(utils.commaAndify(L))
else:
irc.reply('I have no defined bugzillae.')
list = wrap(list, [additional('text')])
def bugzSnarf(self, irc, msg, match):
r"""\bbug\b(?:id|ids|#)?\s+(?:id|ids|#)?(?P<bug>\d+)"""
snarfTarget = self.registryValue('snarfTarget')
if not snarfTarget:
return
bugid = match.group('bug')
name = self.shorthand[snarfTarget]
try:
(url, description) = self.db[name]
except KeyError:
s = self.registryValue('replyNoBugzilla', name)
irc.error(s % name)
return
if not self.registryValue('bugSnarfer', name):
return
queryurl = '%s/xml.cgi?id=%s' % (url, bugid)
bold = self.registryValue('bold', name)
try:
summary = self._get_short_bug_summary(queryurl,description,bugid)
except BugzillaError, e:
irc.error(str(e))
return
except IOError, e:
s = '%s. Try yourself: %s' % (e, queryurl)
irc.error(s)
report = {}
report['id'] = bugid
report['url'] = str('%s/show_bug.cgi?id=%s' % (url, bugid))
report['title'] = str(summary['title'])
report['summary'] = str(self._mk_summary_string(summary, bold))
report['product'] = str(summary['product'])
s = '%(product)s bug #%(id)s: %(title)s %(summary)s' % report
irc.reply(s, prefixName=False)
def bzSnarfer(self, irc, msg, match):
r"(http://\S+)/show_bug.cgi\?id=([0-9]+)"
if not self.registryValue('bugSnarfer', msg.args[0]):
return
queryurl = '%s/xml.cgi?id=%s' % (match.group(1), match.group(2))
try:
summary = self._get_short_bug_summary(queryurl,
'Snarfed Bugzilla URL',
match.group(2))
except BugzillaError, e:
irc.reply(str(e))
return
except IOError, e:
msgtouser = '%s. Try yourself: %s' % (e, queryurl)
irc.reply(msgtouser)
return
bold = self.registryValue('bold', msg.args[0])
report = {}
report['id'] = match.group(2)
report['url'] = str('%s/show_bug.cgi?id=%s' % (match.group(1),
match.group(2)))
report['title'] = str(summary['title'])
report['summary'] = str(self._mk_summary_string(summary, bold))
report['product'] = str(summary['product'])
s = '%(product)s bug #%(id)s: %(title)s %(summary)s' % report
irc.reply(s, prefixName=False)
bzSnarfer = urlSnarfer(bzSnarfer)
def urlquery2bugslist(self, url, query):
"""Given a URL and query list for a CSV bug list, it'll return
all the bugs in a dict
"""
bugs = {}
try:
url = '%s/buglist.cgi?%s' % (url, '&'.join(query))
u = webutils.getUrlFd(url)
except webutils.WebError, e:
return bugs
# actually read in the file
csvreader = csv.reader(u)
# read header
fields = csvreader.next()
# read the rest of the list
for bug in csvreader:
if isinstance(bug, basestring):
bugid = bug
else:
if bug:
bugid = bug[0]
else:
raise callbacks.Error, 'No bugs found.'
try:
bugid = int(bugid)
except ValueError:
pass
bugs[bugid] = {}
i = 1
for f in fields[1:]:
bugs[bugid][f] = bug[i]
i += 1
u.close()
return bugs
def search(self, irc, msg, args, optlist, name, searchstr):
"""[--keywords=<keyword>] <bugzilla name> <search string in desc>
Look for bugs with <search string in the desc>, also matching
<keywords>. <keywords> can be statuses, severities, priorities, or
resolutions, separated by commas"""
keywords = None
for (option, arguments) in optlist:
if option == 'keywords':
keywords = arguments.split(',')
if not keywords:
keywords = ['UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED']
query = self.keywords2query(keywords)
query.append('short_desc_type=allwordssubstr')
query.append('short_desc=%s' % urllib.quote(searchstr))
query.append('order=Bug+Number')
try:
name = self.shorthand[name]
(url, description) = self.db[name]
except KeyError:
s = self.registryValue('replyNoBugzilla', msg.args[0])
irc.error(s % name)
return
bugs = self.urlquery2bugslist(url, query)
bugids = bugs.keys()
bugids.sort()
if not bugs:
irc.error('I could not find any bugs.')
return
s = '%s match %s (%s): %s.' % \
(utils.nItems('bug', len(bugs)), utils.quoted(searchstr),
' AND '.join(keywords), utils.commaAndify(map(str, bugids)))
irc.reply(s)
search = wrap(search, [getopts({'keywords': 'something'}),
'something', 'text'])
def bug(self, irc, msg, args, name, number):
"""<abbreviation> <number>
Look up bug <number> in the bugzilla associated with <abbreviation>.
"""
try:
name = self.shorthand[name]
(url, description) = self.db[name]
except KeyError:
s = self.registryValue('replyNoBugzilla', msg.args[0])
irc.error(s % utils.quoted(name))
return
queryurl = '%s/xml.cgi?id=%s' % (url, number)
try:
summary = self._get_short_bug_summary(queryurl,description,number)
except BugzillaError, e:
irc.error(str(e))
return
except IOError, e:
s = '%s. Try yourself: %s' % (e, queryurl)
irc.error(s)
bold = self.registryValue('bold', msg.args[0])
report = {}
report['zilla'] = description
report['id'] = number
report['url'] = '%s/show_bug.cgi?id=%s' % (url, number)
report['title'] = str(summary['title'])
report['summary'] = self._mk_summary_string(summary, bold)
s = '%(zilla)s bug #%(id)s: %(title)s %(summary)s %(url)s' % report
irc.reply(s)
bug = wrap(bug, ['something', ('id', 'bug')])
def _mk_summary_string(self, summary, bold):
L = []
if bold:
decorate = lambda s: ircutils.bold(s)
else:
decorate = lambda s: s
if 'product' in summary:
L.append(decorate('Product: ') + summary['product'])
if 'component' in summary:
L.append(decorate('Component: ') + summary['component'])
if 'severity' in summary:
L.append(decorate('Severity: ') + summary['severity'])
if 'assigned to' in summary:
L.append(decorate('Assigned to: ') + summary['assigned to'])
if 'status' in summary:
L.append(decorate('Status: ') + summary['status'])
if 'resolution' in summary:
L.append(decorate('Resolution: ') + summary['resolution'])
return ', '.join(imap(str, L))
def _get_short_bug_summary(self, url, desc, number):
try:
bugxml = self._getbugxml(url, desc)
zilladom = minidom.parseString(bugxml)
except Exception, e:
s = 'Could not parse XML returned by %s bugzilla: %s' % (desc, e)
raise BugzillaError, s
bug_n = zilladom.getElementsByTagName('bug')[0]
if bug_n.hasAttribute('error'):
errtxt = bug_n.getAttribute('error')
s = 'Error getting %s bug #%s: %s' % (desc, number, errtxt)
raise BugzillaError, s
summary = {}
try:
node = bug_n.getElementsByTagName('short_desc')[0]
summary['title'] = self._getnodetxt(node)
node = bug_n.getElementsByTagName('bug_status')[0]
summary['status'] = self._getnodetxt(node)
try:
node = bug_n.getElementsByTagName('resolution')[0]
summary['resolution'] = self._getnodetxt(node)
except:
pass
node = bug_n.getElementsByTagName('assigned_to')[0]
summary['assigned to'] = self._getnodetxt(node)
node = bug_n.getElementsByTagName('product')[0]
summary['product'] = self._getnodetxt(node)
node = bug_n.getElementsByTagName('component')[0]
summary['component'] = self._getnodetxt(node)
node = bug_n.getElementsByTagName('bug_severity')[0]
summary['severity'] = self._getnodetxt(node)
except Exception, e:
s = 'Could not parse XML returned by %s bugzilla: %s' % (desc, e)
raise BugzillaError, s
return summary
def _getbugxml(self, url, desc):
try:
bugxml = webutils.getUrl(url)
except webutils.WebError, e:
raise IOError, 'Connection to %s bugzilla failed' % desc
if not bugxml:
raise IOError, 'Error getting bug content from %s' % desc
return bugxml
def _getnodetxt(self, node):
L = []
for childnode in node.childNodes:
if childnode.nodeType == childnode.TEXT_NODE:
L.append(childnode.data)
val = ''.join(L)
if node.hasAttribute('encoding'):
encoding = node.getAttribute('encoding')
if encoding == 'base64':
try:
val = val.decode('base64')
except:
val = 'Cannot convert bug data from base64.'
while self.entre.search(val):
entity = self.entre.search(val).group(1)
if entity in entities:
val = self.entre.sub(entities[entity], val)
else:
val = self.entre.sub('?', val)
return val
Class = Bugzilla
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

Some files were not shown because too many files have changed in this diff Show More