WebUi Plugin - rev40

This commit is contained in:
Zach Tibbitts 2007-09-28 01:47:12 +00:00
parent d36ec723ac
commit 72d868ff79
56 changed files with 6939 additions and 0 deletions

350
plugins/WebUi/LICENSE Normal file
View File

@ -0,0 +1,350 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General
Public License instead of this License.
In addition, as a special exception, the copyright holders give
permission to link the code of portions of this program with the OpenSSL
library.
You must obey the GNU General Public License in all respects for all of
the code used other than OpenSSL. If you modify file(s) with this
exception, you may extend this exception to your version of the file(s),
but you are not obligated to do so. If you do not wish to do so, delete
this exception statement from your version. If you delete this exception
statement from all source files in the program, then also delete it here.

197
plugins/WebUi/__init__.py Normal file
View File

@ -0,0 +1,197 @@
# -*- coding: utf-8 -*-
# Dbus Ipc for experimental web interface
#
# dbus_interface.py
#
# Copyright (C) Martijn Voncken 2007 <mvoncken@gmail.com>
#
# 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, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
plugin_name = "Web User interface"
plugin_author = "Martijn Voncken"
plugin_version = "rev."
plugin_description = """A Web based User Interface (and dbus-ipc)
beta test version, disclaimer, etc..
"""
import deluge.common, deluge.pref
from dbus_interface import DbusManager
import gtk
import os
from subprocess import Popen
plugin_version += open(os.path.join(os.path.dirname(__file__),'revno')).read()
def deluge_init(deluge_path):
global path
path = deluge_path
def enable(core, interface):
global path
return plugin_WebUi(path, core, interface)
class plugin_WebUi:
def __init__(self, path, deluge_core, deluge_interface):
self.path = path
self.core = deluge_core
self.interface = deluge_interface
self.proc = None
self.config_file = deluge.common.CONFIG_DIR + "/webui.conf"
self.config = deluge.pref.Preferences(self.config_file, False)
try:
self.config.load()
except IOError:
# File does not exist
pass
if not self.config.get('port'): #ugly way to detect new config file.
#set default values:
self.config.set("port", 8112)
self.config.set("user", "deluge")
self.config.set("pwd", "deluge")
#future->use deluge-core setting for download_dir (if it is set)
self.config.set("download_dir", os.path.expanduser("~/"))
self.config.set("torrent_dir", os.path.expanduser("~/"))
self.config.set("auto_refresh", False)
self.config.set("auto_refresh_secs", 4)
self.config.set("template", "deluge")
self.config.save(self.config_file)
self.dbusManager = DbusManager(deluge_core, deluge_interface
, self.config, self.config_file)
self.start_server()
def unload(self):
print 'WebUI:unload..'
self.kill_server()
#self.dbusManager.
def update(self):
pass
## This will be only called if your plugin is configurable
def configure(self,parent_dialog):
d = ConfigDialog(self.config,self)
if d.run() == gtk.RESPONSE_OK:
d.save_config()
d.destroy()
def start_server(self):
self.kill_server()
print 'start Webui..'
path = os.path.dirname(__file__)
server_bin = path + '/run_webserver'
port = str(self.config.get('port'))
self.proc = Popen((server_bin, port),cwd=path)
def kill_server(self):
if self.proc:
print "webserver: kill %i"%self.proc.pid
os.system("kill %i"%self.proc.pid)
self.proc = None
class ConfigDialog(gtk.Dialog):
"""
sorry, can't get used to gui builders.
from what I read glade is better, but i dont want to invest time in them.
"""
def __init__(self, config, plugin):
gtk.Dialog.__init__(self)
self.config = config
self.plugin = plugin
self.vb = gtk.VBox()
self.set_title(_("WebUi Config"))
template_path = os.path.join(os.path.dirname(__file__), 'templates')
self.templates = [dirname for dirname
in os.listdir(template_path)
if os.path.isdir(os.path.join(template_path, dirname))]
self.port = self.add_widget(_('Port Number'), gtk.SpinButton())
self.user = self.add_widget(_('User'), gtk.Entry())
self.pwd = self.add_widget(_('Password'), gtk.Entry())
self.template = self.add_widget(_('Template'), gtk.combo_box_new_text())
self.download_dir = self.add_widget(_('Download Directory'),
gtk.FileChooserButton(_('Download Directory')))
self.torrent_dir = self.add_widget(_('Torrent Directory'),
gtk.FileChooserButton(_('Torrent Directory')))
self.download_dir.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
self.torrent_dir.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
self.port.set_range(80, 65536)
self.port.set_increments(1, 10)
for item in self.templates:
self.template.append_text(item)
if not self.config.get("template") in self.templates:
self.config.set("template","deluge")
self.user.set_text(self.config.get("user"))
self.pwd.set_text(self.config.get("pwd"))
self.port.set_value(int(self.config.get("port")))
self.template.set_active(
self.templates.index(self.config.get("template")))
self.torrent_dir.set_filename(self.config.get("torrent_dir"))
self.download_dir.set_filename(self.config.get("download_dir"))
self.vbox.pack_start(self.vb, True, True, 0)
self.vb.show_all()
self.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL
,gtk.STOCK_OK, gtk.RESPONSE_OK)
def add_widget(self,label,w=None):
hb = gtk.HBox()
lbl = gtk.Label(label)
lbl.set_size_request(200,20)
hb.pack_start(lbl,False,False, 0)
hb.pack_start(w,True,True, 0)
self.vb.pack_start(hb,False,False, 0)
return w
self.add_buttons(dgtk.STOCK_CLOSE, dgtk.RESPONSE_CLOSE)
def save_config(self):
print 'save config'
self.config.set("user", self.user.get_text())
self.config.set("pwd", self.pwd.get_text())
self.config.set("port", int(self.port.get_value()))
self.config.set("template", self.template.get_active_text())
self.config.set("torrent_dir", self.torrent_dir.get_filename())
self.config.set("download_dir",self.download_dir.get_filename())
self.config.save(self.plugin.config_file)
self.plugin.start_server() #restarts server

View File

@ -0,0 +1,238 @@
# -*- coding: utf-8 -*-
# Dbus Ipc for experimental web interface
#
# dbus_interface.py
#
# Copyright (C) Martijn Voncken 2007 <mvoncken@gmail.com>
# Contains copy and pasted code from other parts of deluge,see deluge AUTHORS
#
# 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, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
import os
import gtk
import dbus
import deluge.common as common
from dbus_pythonize import pythonize
import base64
import random
random.seed()
dbus_interface="org.deluge_torrent.dbusplugin"
dbus_service="/org/deluge_torrent/DelugeDbusPlugin"
class DbusManager(dbus.service.Object):
def __init__(self, core, interface,config,config_file):
self.core = core
self.interface = interface
self.config = config
self.config_file = config_file
self.bus = dbus.SessionBus()
bus_name = dbus.service.BusName(dbus_interface,bus=self.bus)
dbus.service.Object.__init__(self, bus_name,dbus_service)
#
#todo : add: get_interface_version in=i,get_config_value in=s out=s
#
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="",out_signature="as")
def get_torrent_state(self):
"""Returns a list of torrent_ids in the session.
same as 0.6, but returns type "as" instead of a pickle
"""
torrent_list = [str(key) for key in self.core.unique_IDs]
return torrent_list
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="sas",out_signature="a{sv}")
def get_torrent_status(self, torrent_id, keys):
"""return torrent metadata of a single torrent as a dict
0.6 returns a pickle, this returns a dbus-type.
+added some more values to the dict
"""
torrent_id = int(torrent_id)
# Convert the array of strings to a python list of strings
nkeys = [str(key) for key in keys]
state = self.core.get_torrent_state(torrent_id)
torrent = self.core.unique_IDs[torrent_id]
status = {
"name": state["name"],
"total_size": state["total_size"],
"num_pieces": state["num_pieces"],
#"state": int(status.state), #?
"paused": self.core.is_user_paused(torrent_id),
"progress": int(state["progress"] * 100),
"next_announce": state["next_announce"],
"total_payload_download":state["total_payload_download"],
"total_payload_upload": state["total_payload_upload"],
"download_payload_rate": state["download_rate"],
"upload_payload_rate": state["upload_rate"],
"num_peers": state["num_peers"],
"num_seeds": state["num_seeds"],
"total_wanted": state["total_wanted"],
"eta": common.estimate_eta(state),
"ratio": self.interface.manager.calc_ratio(torrent_id,state),
#non 0.6 values follow here:
"message": self.interface.get_message_from_state(state),
"tracker_status": state.get("tracker_status","?"),
"uploaded_memory": torrent.uploaded_memory,
}
#more non 0.6 values
for key in ["total_seeds", "total_peers","is_seed", "total_done",
"total_download", "total_upload", "download_rate",
"upload_rate", "num_files", "piece_length", "distributed_copies"
,"next_announce","tracker"]:
status[key] = state[key]
#print 'all_keys:',sorted(status.keys())
status_subset = {}
for key in keys:
if key in status:
status_subset[key] = status[key]
else:
print 'mbus error,no key named:', key
return status_subset
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="s",out_signature="")
def pause_torrent(self, torrent_id):
"""same as 0.6 interface"""
torrent_id = int(torrent_id)
self.core.set_user_pause(torrent_id,True)
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="s", out_signature="")
def resume_torrent(self, torrent_id):
"""same as 0.6 interface"""
torrent_id = int(torrent_id)
self.core.set_user_pause(torrent_id,False)
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="sbb", out_signature="")
def remove_torrent(self, torrent_id, data_also, torrent_also):
"""remove a torrent,and optionally data and torrent
additions compared to 0.6 interface: (data_also, torrent_also)
"""
torrent_id = int(torrent_id)
self.core.remove_torrent(torrent_id, bool(data_also)
,bool( torrent_also))
#this should not be needed:
self.interface.torrent_model_remove(torrent_id)
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="s", out_signature="b")
def add_torrent_url(self, url):
"""not available in deluge 0.6 interface"""
filename = fetch_url(url)
self._add_torrent(filename)
return True
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="ss", out_signature="b")
def add_torrent_filecontent(self, name, filecontent_b64):
"""not available in deluge 0.6 interface"""
#name = fillename without directory
name = name.replace('\\','/')
name = 'deluge_' + str(random.random()) + '_' + name.split('/')[-1]
filename = os.path.join(self.config.get("torrent_dir"),name)
filecontent = base64.b64decode(filecontent_b64)
f = open(filename,"wb") #no with statement, that's py 2.5+
f.write(filecontent)
f.close()
print 'write:',filename
self._add_torrent(filename)
return True
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="s",out_signature="v")
def get_webui_config(self,key):
"""
return data from wevbui config.
not in 0.6
"""
retval = self.config.get(str(key))
print 'get webui config:', str(key), retval
if retval == None:
retval = False #dbus does not accept None :(
return retval
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="sv",out_signature="")
def set_webui_config(self, key, value):
"""
return data from wevbui config.
not in 0.6
"""
print 'set webui config:', str(key), pythonize(value)
self.config.set(str(key), pythonize(value))
self.config.save(self.config_file)
#internal
def _add_torrent(self, filename):
#dbus types break pickle, again.....
filename = unicode(filename)
target = self.config.get("download_dir")
torrent_id = self.core.add_torrent(filename, target,
self.interface.config.get("use_compact_storage"))
#update gtk-ui This should not be needed!!
gtk.gdk.threads_enter()
try:
self.interface.torrent_model_append(torrent_id)
except:
pass
#finally is 2.5 only!
gtk.gdk.threads_leave()
return True
def fetch_url(url):
import urllib
try:
filename, headers = urllib.urlretrieve(url)
except IOError:
raise Exception( "Network error while trying to fetch torrent from %s"
% url)
else:
if (filename.endswith(".torrent") or
headers["content-type"]=="application/x-bittorrent"):
return filename
else:
raise Exception("URL doesn't appear to be a valid torrent file:%s"
% url)
return None

View File

@ -0,0 +1,38 @@
"""
some dbus to python type conversions
-decorator for interface
-wrapper class for proxy
"""
def pythonize(var):
"""translates dbus types back to basic python types."""
if isinstance(var, list):
return [pythonize(value) for value in var]
if isinstance(var, tuple):
return tuple([pythonize(value) for value in var])
if isinstance(var, dict):
return dict(
[(pythonize(key), pythonize(value)) for key, value in var.iteritems()]
)
for klass in [unicode, str, bool, int, float, long]:
if isinstance(var,klass):
return klass(var)
return var
def pythonize_call(func):
def deco(*args,**kwargs):
return pythonize(func(*args, **kwargs))
return deco
def pythonize_interface(func):
def deco(*args, **kwargs):
args = pythonize(args)
kwargs = pythonize(kwargs)
return func(*args, **kwargs)
return deco
class PythonizeProxy(object):
def __init__(self,proxy):
self.proxy = proxy
def __getattr__(self, key):
return pythonize_call(getattr(self.proxy, key))

View File

@ -0,0 +1,391 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# deluge_webserver.py
#
# Copyright (C) Martijn Voncken 2007 <mvoncken@gmail.com>
#
# 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, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
"""
Todo's before beta:
-alternating rows?
-__init__:unload plugin is broken!
-__init__:kill->restart is not waiting for kill to be finished.
-redir is broken.
--later/features:---
-set prio
-clear finished?
-torrent files.
"""
import webpy022 as web
from webpy022.webapi import cookies, setcookie
from webpy022.http import seeother, url
from webpy022.utils import Storage
from webpy022 import template
import dbus
import gettext, os, platform, locale
import random
import base64
from operator import attrgetter
from deluge import common
from deluge.common import INSTALL_PREFIX
#init:
APP = 'deluge'
DIR = os.path.join(INSTALL_PREFIX, 'share', 'locale')
if platform.system() != "Windows":
locale.setlocale(locale.LC_MESSAGES, '')
locale.bindtextdomain(APP, DIR)
locale.textdomain(APP)
else:
locale.setlocale(locale.LC_ALL, '')
gettext.bindtextdomain(APP, DIR)
gettext.textdomain(APP)
gettext.install(APP, DIR)
random.seed()
bus = dbus.SessionBus()
proxy = bus.get_object("org.deluge_torrent.dbusplugin"
, "/org/deluge_torrent/DelugeDbusPlugin")
web.webapi.internalerror = web.debugerror
#/init
#framework:
SESSIONS = {}
def do_redirect():
"""for redirects after a POST"""
vars = web.input(redir = None)
ck = cookies()
if vars.redir:
seeother(vars.redir)
elif ("order" in ck and "sort" in ck):
seeother(url("/index", sort=ck['sort'], order=ck['order']))
else:
seeother(url("/index"))
def deluge_page_noauth(func):
"""
add http headers
print result of func
"""
def deco(self, name=None):
web.header("Content-Type", "text/html; charset=utf-8")
web.header("Cache-Control", "no-cache, must-revalidate")
res = func(self, name)
print res
return deco
def check_session(func):
"""
a decorator
return func if session is valid, else redirect to login page.
"""
def deco(self, name):
ck = cookies()
if ck.has_key("session_id") and ck["session_id"] in SESSIONS:
return func(self, name) #ok, continue..
else:
seeother("/login") #do not continue, and redirect to login page
return deco
def deluge_page(func):
return check_session(deluge_page_noauth(func))
def auto_refreshed(func):
"decorator:adds a refresh header"
def deco(self, name):
if proxy.get_webui_config('auto_refresh'):
web.header("Refresh", "%i ; url=%s" %
(proxy.get_webui_config('auto_refresh_secs'),
web.changequery()))
return func(self, name)
return deco
def error_page(error):
web.header("Content-Type", "text/html; charset=utf-8")
web.header("Cache-Control", "no-cache, must-revalidate")
print render.error(error)
#/framework
#utils:
torrent_keys = ['distributed_copies', 'download_payload_rate',
'download_rate', 'eta', 'is_seed', 'message', 'name', 'next_announce',
'num_files', 'num_peers', 'num_pieces', 'num_seeds', 'paused',
'piece_length','progress', 'ratio', 'total_done', 'total_download',
'total_payload_download', 'total_payload_upload', 'total_peers',
'total_seeds', 'total_size', 'total_upload', 'total_wanted',
'tracker_status', 'upload_payload_rate', 'upload_rate',
'uploaded_memory','tracker']
def get_torrent_status(torrent_id):
"""
helper method.
enhance proxy.get_torrent_status with some extra data
"""
status = proxy.get_torrent_status(torrent_id,torrent_keys)
status["id"] = torrent_id
#for naming the status-images
status["calc_state_str"] = "downloading"
if status["paused"]:
status["calc_state_str"] = "inactive"
elif status["is_seed"]:
status["calc_state_str"] = "seeding"
#action for torrent_pause
if status["calc_state_str"] == "inactive":
status["action"] = "start"
else:
status["action"] = "stop"
#add some pre-calculated values
status.update({
"calc_total_downloaded" : (common.fsize(status["total_done"])
+ " (" + common.fsize(status["total_download"]) + ")"),
"calc_total_uploaded": (common.fsize(status['uploaded_memory']
+ status["total_payload_upload"]) + " ("
+ common.fsize(status["total_upload"]) + ")"),
})
return Storage(status) #Storage for easy templating.
#/utils
#template-defs:
def template_crop(text, end):
if len(text) > end:
return text[0:end - 3] + '...'
return text
def sort_head(id,name):
#got tired of doing these complex things inside templetor..
vars = web.input(sort=None, order=None)
active_up = False
active_down = False
order = 'down'
if vars.sort == id:
if vars.order == 'down':
order = 'up'
active_down = True
else:
active_up = True
return render.sort_column_head(id, name, order, active_up, active_down)
render = template.render('templates/%s/' % proxy.get_webui_config('template'))
template.Template.globals['crop'] = template_crop
template.Template.globals['fspeed'] = common.fspeed
template.Template.globals['fsize'] = common.fsize
template.Template.globals['sorted'] = sorted
template.Template.globals['_'] = _ #gettext/translations
template.Template.globals['deluge_web_version'] = ('rev.'
+ open(os.path.join(os.path.dirname(__file__),'revno')).read())
template.Template.globals['render'] = render #for easy resuse of templates
template.Template.globals['sort_head'] = sort_head
template.Template.globals['get_config'] = proxy.get_webui_config
template.Template.globals['self_url'] = web.changequery
#/template-defs
#routing:
urls = (
"/login(.*)", "login",
"/index(.*)", "index",
"/torrent/info/(.*)", "torrent_info",
"/torrent/pause(.*)", "torrent_pause",
"/torrent/add(.*)", "torrent_add",
"/torrent/delete/(.*)", "torrent_delete",
"/pause_all(.*)", "pause_all",
"/resume_all(.*)", "resume_all",
"/refresh/set(.*)", "refresh_set",
"/refresh/(.*)", "refresh",
"/home(.*)", "home",
#default-pages
"/", "login",
"", "login"
)
#/routing
#pages:
class login:
@deluge_page_noauth
def GET(self, name):
return render.login()
def POST(self, name):
vars = web.input(var = None, pwd = None)
if (vars.user == proxy.get_webui_config('user')
and vars.pwd == proxy.get_webui_config('pwd')):
#start new session
session_id = str(random.random())
SESSIONS[session_id] = {"user":vars.user}
setcookie("session_id", session_id)
do_redirect()
else:
error_page(_("Username or Password is invalid."))
class home:
@check_session
def GET(self, name):
do_redirect()
class index:
"page containing the torrent list."
@auto_refreshed
@deluge_page
def GET(self, name):
vars = web.input(sort=None, order=None)
status_rows = [get_torrent_status(torrent_id)
for torrent_id in proxy.get_torrent_state()]
#sorting:
if vars.sort:
status_rows.sort(key=attrgetter(vars.sort))
if vars.order == 'up':
status_rows = reversed(status_rows)
setcookie("order", vars.order)
setcookie("sort", vars.sort)
return render.index(status_rows)
class torrent_info:
"torrent details"
@auto_refreshed
@deluge_page
def GET(self, torrent_id):
return render.torrent_info(get_torrent_status(torrent_id))
class torrent_pause:
"start/stop a torrent"
@check_session
def POST(self, name):
vars = web.input(stop = None, start = None, redir = None)
if vars.stop:
proxy.pause_torrent(vars.stop)
elif vars.start:
proxy.resume_torrent(vars.start)
do_redirect()
class torrent_add:
@deluge_page
def GET(self, name):
return render.torrent_add()
@check_session
def POST(self, name):
vars = web.input(url = None, torrent = {})
if vars.url and vars.torrent.filename:
error_page(_("Choose an url or a torrent, not both."))
if vars.url:
proxy.add_torrent_url(vars.url)
do_redirect()
elif vars.torrent.filename:
data = vars.torrent.file.read()
data_b64 = base64.b64encode(data)
#b64 because of strange bug-reports related to binary data
proxy.add_torrent_filecontent(vars.torrent.filename,data_b64)
do_redirect()
else:
error_page(_("no data."))
class torrent_delete:
@deluge_page
def GET(self, torrent_id):
return render.torrent_delete(get_torrent_status(torrent_id))
@check_session
def POST(self, name):
torrent_id = name
vars = web.input(data_also = None, torrent_also = None)
data_also = bool(vars.data_also)
torrent_also = bool(vars.torrent_also)
proxy.remove_torrent(torrent_id, data_also, torrent_also)
do_redirect()
class pause_all:
@check_session
def POST(self, name):
for torrent_id in proxy.get_torrent_state():
proxy.pause_torrent(torrent_id)
do_redirect()
class resume_all:
@check_session
def POST(self, name):
for torrent_id in proxy.get_torrent_state():
proxy.resume_torrent(torrent_id)
do_redirect()
class refresh:
@check_session
def POST(self, name):
auto_refresh = {'off':False, 'on':True}[name]
proxy.set_webui_config('auto_refresh', auto_refresh)
do_redirect()
class refresh_set:
@deluge_page
def GET(self, name):
return render.refresh_form()
@check_session
def POST(self, name):
vars = web.input(refresh = 0)
refresh = int(vars.refresh)
if refresh > 0:
proxy.set_webui_config('refresh', refresh)
proxy.set_webui_config('auto_refresh', True)
do_redirect()
else:
error_page(_('refresh must be > 0'))
#/pages
if __name__ == "__main__":
web.run(urls, globals())

1
plugins/WebUi/revno Normal file
View File

@ -0,0 +1 @@
40

3
plugins/WebUi/run_webserver Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env python
from deluge_webserver import *
web.run(urls, globals())

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

View File

@ -0,0 +1,36 @@
/* ----------------------------------------------------------- Theme Name: Simple Theme URI: http://deluge-torrent.org Description: Deluge Theme Version: 1.0 ----------------------------------------------------------- */ BODY { background: #304663 url(images/simple_bg.jpg) repeat-x; font-family: trebuchet ms; font-size: 10pt; margin: 0; } /* GENERIC STYLES */ a img {border: 0px} hr {color: #627082; margin: 15px 0 15px 0;} /* STRUCTURE */ #page { min-width: 800px; margin-left: auto; margin-right: auto; } #main_content { background:url(images/simple_line.jpg) repeat-x; } #simple_logo { background:url(images/simple_logo.jpg) no-repeat; } #main { padding-top: 20px; padding-left: 20px; color: #fff; } #main form table { border: #2a425c 1px solid; } #main form table tr { border: 0px; } #main form table tr th { background: #1f3044; font-size: 16px; border: 0px;
white-space: nowrap; } #main form table tr td{ border: 0px; color: #fff; font-size: 12px; white-space: nowrap; } #main form table tr th a { color: #8fa6c3; font-size: 16px; white-space: nowrap; } #main form table tr th a, a:active, a:visited { color: #8fa6c3; text-decoration: none; } #main form table tr th a:hover {color: #fff; text-decoration: underline;} #main form table tr td a { color: #fff; font-size: 12px; white-space: nowrap; } #main form table tr td a, a:active, a:visited { color: #fff; text-decoration: none;} #main form table tr td a:hover {color: #fff; text-decoration: underline;} #main a { color: #fff; font-size: 12px; } #main a, a:active, a:visited { color: #fff; text-decoration: none;} #main a:hover {color: #fff; text-decoration: underline;} .info { text-align: right; padding: 0 50px 0 0; color: #8fa6c3; font-size: 16px; letter-spacing: 4px; font-weight: bold; } .title { color: #dce4ee; font-size: 32px; padding: 10px 50px 0 0; text-align: right; } .title a, a:active, a:visited { color: #dce4ee; text-decoration: none;} .title a:hover {color: #fff; text-decoration: underline;} #button { border:1px solid #23344b; background: #99acc3; color: #000; font-family:verdana, arial, helvetica, sans-serif; font-size:10px; margin-top:5px; } INPUT{ border:1px solid #23344b; background: #99acc3; color: #000; } TEXTAREA{ border:1px solid #23344b; background: #99acc3; width:480px; } .footertext a { color: #c0c0c0; text-decoration:none;} .footertext a:visited { color: #c0c0c0; text-decoration:none;} .footertext a:active { color: #c0c0c0; text-decoration:none;} .footertext a:hover {color: #fff; text-decoration: underline;} .footertext { text-align: center; padding: 60px 0 0 0; font-size: 8pt; left: -100px; font-family: trebuchet MS; color: #fff; position: relative; } .clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } div.progress_bar{ background-color:#4573a5; /*color:blue;*/ -moz-border-radius:5px; /*ff only setting*/ }
div.progress_bar_outer { /*used in table-view*/
width:150px;
}
td.progress_bar { white-space: nowrap; } td.info_label { font-weight: bold; } td { font-size: 10pt; color: #d1dae5; white-space: nowrap; } tr { font-size: 10pt; color: #d1dae5; }
div.panel {
padding:10px;
width:750px;
background-color: #37506f;
-moz-border-radius:10px; /*ff-only!*/
margin-top:10px;
margin-bottom:10px;
}
/*New styles:*/
div.deluge_button {
display:inline;
}
form.deluge_button {
display:inline;
}
button.deluge_button {
background-color: #37506f;
border:1px solid #23344b;
background: #99acc3;
color: #000;
}
/* Hides from IE-mac \*/ * html .clearfix {height: 1%;} .clearfix {display: block;} /* End hide from IE-mac */

View File

@ -0,0 +1,5 @@
-first layout taken from deluge website
improved by:
-mvoncken
-somedude

View File

@ -0,0 +1,6 @@
$def with (error_msg)
$:header(_('Error'))
<pre class="error">
$error_msg
</pre>
$:footer()

View File

@ -0,0 +1,6 @@
</center>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,25 @@
$def with (title)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/1999/REC-html401-19991224/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Deluge:$title</title>
<link rel="icon" href="/static/images/deluge_icon.gif" type="image/gif" />
<link rel="shortcut icon" href="/static/images/deluge_icon.gif" type="image/gif" />
<link rel="stylesheet" type="text/css" href="/static/simple_site_style.css" />
</head>
<body>
<div id="page">
<a href='/home'>
<div id="simple_logo">
<div class="title">Deluge</div>
<div class="info">$title</div>
</div>
</a>
<div id="main_content">
<div id="main">
<center>

View File

@ -0,0 +1,58 @@
$def with (torrent_list)
$:render.header(_('Torrent list'))
<form action="/torrent/pause" method="POST">
<table class="torrent_list" border=1>
<tr>
$:(sort_head('calc_state_str', 'S'))
$:(sort_head('id', '#'))
$:(sort_head('name', _('Name')))
$:(sort_head('total_size', _('Size')))
$:(sort_head('progress', _('Progress')))
$:(sort_head('num_seeds', _('Seeders')))
$:(sort_head('num_peers', _('Peers')))
$:(sort_head('download_rate', _('Download')))
$:(sort_head('upload_rate', _('Upload')))
$:(sort_head('eta', _('Eta')))
$:(sort_head('distributed_copies', _('Ava')))
$:(sort_head('ratio', _('Ratio')))
</tr>
$#4-space indentation is mandatory for for-loops in templetor!
$for torrent in torrent_list:
<tr>
<td><input type="image"
src="/static/images/$(torrent.calc_state_str)16.png"
name="$torrent.action" value="$torrent.id">
</td>
<td>$torrent.id</td>
<td style="width:100px; overflow:hidden;white-space: nowrap">
<a href="/torrent/info/$torrent.id">$(crop(torrent.name, 40))</a></td>
<td>$fsize(torrent.total_size)</td>
<td class="progress_bar">
<div class="progress_bar_outer">
<div class="progress_bar" style="width:$(torrent.progress)%">
$torrent.message
</div>
</div>
</td>
<td>$torrent.num_seeds ($torrent.total_seeds)</td>
<td>$torrent.num_peers ($torrent.total_peers)</td>
<td>$fspeed(torrent.download_rate)</td>
<td>$fspeed(torrent.upload_rate)</td>
<td>$torrent.eta</td>
<td>$("%.3f" % torrent.distributed_copies)</td>
<td>$("%.3f" % torrent.ratio)</td\>
</tr>
</table>
</form>
<div class="panel">
$:render.part_button('GET', '/torrent/add', _('Add torrent'), 'tango/list-add.png')
$:render.part_button('POST', '/pause_all', _('Pause all'), 'tango/media-playback-pause.png')
$:render.part_button('POST', '/resume_all', _('Resume all'), 'tango/media-playback-start.png')
</div>
$:render.part_refresh()
$:render.footer()

View File

@ -0,0 +1,21 @@
$:render.header(_('Login'))
<div class="panel">
<form method="POST" id="loginform" action='/login'>
<div id="loginpanel">
<div class="form_row">
<span class="form_label">$_('User')</span>
<input type="text" name="user" id="user" class="form_input">
</div>
<div class="form_row">
<span class="form_label">$_('Pass')</span>
<input type="password" name="pwd" id="pwd" class="form_input">
</div>
<div class="form_row">
<span class="form_label"></span>
<input type="submit" name="submit"
id="submit" value="Submit" class="form_input">
</div>
</div>
</form>
</div>
$:render.footer()

View File

@ -0,0 +1,13 @@
$def with (method, url, title, image='')
<div class="deluge_button">
<form method="$method" action="$url" class="deluge_button">
<button type="submit" class="deluge_button">
<input type="hidden" name="redir" value="$self_url()">
$if image:
<image src="/static/images/$image" class="button"/>
$else:
<!--no image-->
$title
</button>
</form>
</div>

View File

@ -0,0 +1,13 @@
<div class="panel">
<div id='refresh'>
$_('Auto refresh:')
$if get_config('auto_refresh'):
($(get_config('refresh')) $_('seconds')) &nbsp;
$:render.part_button('GET', '/refresh/set', _('Set'), 'tango/preferences-system.png')
$:render.part_button('POST', '/refresh/off', _('Disable'), 'tango/process-stop.png')
$else:
$_('Off') &nbsp;
$:render.part_button('POST', '/refresh/on', _('Enable'), 'tango/view-refresh.png')
$#end
</div>
</div>

View File

@ -0,0 +1,10 @@
$:render.header(_('Set Timeout'))
<div class="panel">
<form action="/refresh/set" method="POST">
$_('Refresh page every:')
<input type="text" name="refresh" value="$get_config('refresh')" size="3">
$_('Seconds')
<input type="submit" value="$_('Set')">
</form>
</div>
$:render.footer()

View File

@ -0,0 +1,12 @@
$def with (column_id, column_name, order, active_up, active_down)
<th>
<a href="/index?sort=$column_id&order=$order">
$column_name\
$if active_up:
<img src="/static/images/tango/up.png" />
$if active_down:
<img src="/static/images/tango/down.png" />
</a>
</th>

View File

@ -0,0 +1,21 @@
$:render.header(_("Add Torrent"))
<div class="panel">
<form method="POST" action="/torrent/add" ENCTYPE="multipart/form-data">
<div id="torrent_add">
<div class="form_row">
<span class="form_label">$_('Url')</span>
<input type="text" name="url" class="form_input" size=60>
</div>
<div class="form_row">
<span class="form_label">$_('Upload torrent')</span>
<input type="file" name="torrent" class="form_input" size=50>
</div>
<div class="form_row">
<span class="form_label"></span>
<input type="submit" name="submit"
value="$_('Submit')" class="form_input">
</div>
</div>
</form>
</div>
$:render.footer()

View File

@ -0,0 +1,26 @@
$def with (torrent)
$:render.header(_("Remove %s ") % torrent.name)
<div class="panel">
<form method="POST" action='/torrent/delete/$torrent.id'>
<div id="del_torrent">
$(_("Remove %s?") % torrent.name)
<div class="form_row2">
<span class="form_label2">
<input type="checkbox" name="torrent_also" class="form_input" checked
value="1">$_('Delete .torrent file')</span>
</div>
<div class="form_row2">
<span class="form_label2">
<input type="checkbox" name="data_also" class="form_input"
value="1">$_('Delete downloaded files.')</span>
</div>
<div class="form_row2">
<span class="form_label2"></span>
<input type="submit" name="submit"
value="$_('Remove')" class="form_input">
</div>
</div>
</form>
</div>
$:render.footer()

View File

@ -0,0 +1,118 @@
$def with (torrent)
$:(render.header(torrent.message + '/' + torrent.name))
<div class="panel">
<h3>$_('Details')</h3>
<table width="100%"><tr>
<td colspan=3 style="background-color:#999;-moz-border-radius:5px;">
<div class="progress_bar" style="width:$torrent.progress%;
text-align:center;font-weight:bold;">
$torrent.progress %</div>
</td>
</tr><td width=30%%>
<table>
<tr><td class="info_label">$_('Downloaded'):</td>
<td class="info_value">$torrent.calc_total_downloaded</td></tr>
<tr><td class="info_label">$_('Uploaded'):</td>
<td class="info_value">$torrent.calc_total_uploaded</td>
</tr>
<tr><td class="info_label">$_('Seeders'):</td>
<td class="info_value">$torrent.num_seeds ($torrent.total_seeds )</td></tr>
<tr><td class="info_label">$_('Share Ratio'):</td>
<td class="info_value">$("%.3f" % torrent.ratio)</td></tr>
<tr><td class="info_label">$_('Pieces'):</td>
<td class="info_value">$torrent.num_pieces x $fsize(torrent.piece_length) </td>
</tr>
</table>
</td><td width=30%%>
<table>
<tr><td class="info_label">$_('Speed'):</td><td class="info_value">
$fspeed(torrent.download_rate)</td></td></tr>
<tr><td class="info_label">$_('Speed'):</td>
<td class="info_value">$fspeed(torrent.upload_rate)</td></tr>
<tr><td class="info_label">$_('Peers'):</td>
<td class="info_value">$torrent.num_peers ($torrent.total_peers )</td></tr>
<tr><td class="info_label">$_('ETA'):</td>
<td class="info_value">$torrent.eta </td></tr>
<tr><td class="info_label">$_('Availability'):</td>
<td class="info_value">$("%.3f" % torrent.distributed_copies)</td></td></tr>
</table>
</td><td width=30%%>
<table>
<tr><td class="info_label">$_('Total Size'):</td>
<td class="info_value">$fspeed(torrent.total_size)</td></tr>
<tr><td class="info_label">$_('# Of Files'):</td>
<td class="info_value">$torrent.num_files</td></tr>
<tr><td class="info_label">$_('Tracker'):</td>
<td class="info_value">$(crop(torrent.tracker, 30))</td></tr>
<tr><td class="info_label">$_('Tracker Status'):</td>
<td class="info_value">$torrent.tracker_status </td></tr>
<tr><td class="info_label">$_('Next Announce'):</td>
<td class="info_value">$torrent.next_announce </td></tr>
</table>
</table>
<form action="/torrent/pause?redir=/torrent/info/$torrent.id" method="POST"
class="deluge_button">
<input type="image" src="/static/images/$(torrent.calc_state_str)16.png"
name="$torrent.action" value="$torrent.id">
</form>
<div class="deluge_button">
<form method="GET" action="/torrent/delete/$torrent.id" class="deluge_button">
<button type="submit" class="deluge_button">
<image src="/static/images/tango/user-trash.png" /> $_('Delete')
</button>
</form>
</div>
</form>
<br>
[<a onclick="javascript:toggle_dump()">$_('Debug:Data Dump')</a>]
<pre style="background-color:white;color:black;display:none" id="data_dump">
$for key in sorted(torrent):
$key : $torrent[key]
</pre>
<script language="javascript">
function toggle_dump(){
el = document.getElementById("data_dump");
if (el.style.display == "block"){
el.style.display = "none";
}
else{
el.style.display = "block";
}
}
</script>
</div>
$:render.part_refresh()
$:render.footer()

View File

@ -0,0 +1,27 @@
Quickstart:
Just copy and rename an existing template.
-The settings panel will see all directory's in this folder ,and let you choose your new template.
-Clicking Ok in the settings panel will restart the webserver and reload your template.
Notes:
Please configure your editor to use 4-space indents instead of tabs.
Or use scite and my config: http://mvoncken.sohosted.com/deluge/SciTEUser.properties.txt
template language: http://webpy.org/templetor
Exposed methods and variables (c&p from deluge_webserver):
template.Template.globals['crop'] = template_crop
template.Template.globals['fspeed'] = common.fspeed
template.Template.globals['fsize'] = common.fsize
template.Template.globals['sorted'] = sorted
template.Template.globals['_'] = _ #gettext/translations
template.Template.globals['deluge_web_version'] = '0.3alfa'
template.Template.globals['render'] = render #for easy resuse of templates
template.Template.globals['sort_head'] = sort_head
template.Template.globals['get_config'] = proxy.get_webui_config
I will update this file if there is interest in making templates.

5
plugins/WebUi/version Normal file
View File

@ -0,0 +1,5 @@
revision-id: mvoncken@gmail.com-20070923193628-omqar5yaxatdmfu0
date: 2007-09-23 21:36:28 +0200
build-date: 2007-09-23 21:54:09 +0200
revno: 30
branch-nick: WebUi

View File

@ -0,0 +1 @@
http://webpy.org/

View File

@ -0,0 +1,62 @@
#!/usr/bin/env python
from __future__ import generators
"""web.py: makes web apps (http://webpy.org)"""
__version__ = "0.22"
__revision__ = "$Rev: 183 $"
__author__ = "Aaron Swartz <me@aaronsw.com>"
__license__ = "public domain"
__contributors__ = "see http://webpy.org/changes"
# todo:
# - some sort of accounts system
import utils, db, net, wsgi, http, webapi, request, httpserver, debugerror
import template, form
from utils import *
from db import *
from net import *
from wsgi import *
from http import *
from webapi import *
from request import *
from httpserver import *
from debugerror import *
try:
import cheetah
from cheetah import *
except ImportError:
pass
def main():
import doctest
doctest.testmod(utils)
doctest.testmod(db)
doctest.testmod(net)
doctest.testmod(wsgi)
doctest.testmod(http)
doctest.testmod(webapi)
doctest.testmod(request)
try:
doctest.testmod(cheetah)
except NameError:
pass
template.test()
import sys
urls = ('/web.py', 'source')
class source:
def GET(self):
header('Content-Type', 'text/python')
print open(sys.argv[0]).read()
if listget(sys.argv, 1) != 'test':
run(urls, locals())
if __name__ == "__main__": main()

View File

@ -0,0 +1,6 @@
Commented out some code to enable a relative redirect.
This is not according to HTTP/1.1 Spec
But many deluge users will want to route the webui through firewalls/routers or use apache redirects.

View File

@ -0,0 +1,98 @@
"""
Cheetah API
(from web.py)
"""
__all__ = ["render"]
import re, urlparse, pprint, traceback, sys
from Cheetah.Compiler import Compiler
from Cheetah.Filters import Filter
from utils import re_compile, memoize, dictadd
from net import htmlquote, websafe
from webapi import ctx, header, output, input, cookies, loadhooks
def upvars(level=2):
"""Guido van Rossum sez: don't use this function."""
return dictadd(
sys._getframe(level).f_globals,
sys._getframe(level).f_locals)
r_include = re_compile(r'(?!\\)#include \"(.*?)\"($|#)', re.M)
def __compiletemplate(template, base=None, isString=False):
if isString:
text = template
else:
text = open('templates/'+template).read()
# implement #include at compile-time
def do_include(match):
text = open('templates/'+match.groups()[0]).read()
return text
while r_include.findall(text):
text = r_include.sub(do_include, text)
execspace = _compiletemplate.bases.copy()
tmpl_compiler = Compiler(source=text, mainClassName='GenTemplate')
tmpl_compiler.addImportedVarNames(execspace.keys())
exec str(tmpl_compiler) in execspace
if base:
_compiletemplate.bases[base] = execspace['GenTemplate']
return execspace['GenTemplate']
_compiletemplate = memoize(__compiletemplate)
_compiletemplate.bases = {}
def render(template, terms=None, asTemplate=False, base=None,
isString=False):
"""
Renders a template, caching where it can.
`template` is the name of a file containing the a template in
the `templates/` folder, unless `isString`, in which case it's the
template itself.
`terms` is a dictionary used to fill the template. If it's None, then
the caller's local variables are used instead, plus context, if it's not
already set, is set to `context`.
If asTemplate is False, it `output`s the template directly. Otherwise,
it returns the template object.
If the template is a potential base template (that is, something other templates)
can extend, then base should be a string with the name of the template. The
template will be cached and made available for future calls to `render`.
Requires [Cheetah](http://cheetahtemplate.org/).
"""
# terms=['var1', 'var2'] means grab those variables
if isinstance(terms, list):
new = {}
old = upvars()
for k in terms:
new[k] = old[k]
terms = new
# default: grab all locals
elif terms is None:
terms = {'context': ctx, 'ctx':ctx}
terms.update(sys._getframe(1).f_locals)
# terms=d means use d as the searchList
if not isinstance(terms, tuple):
terms = (terms,)
if 'headers' in ctx and not isString and template.endswith('.html'):
header('Content-Type','text/html; charset=utf-8', unique=True)
if loadhooks.has_key('reloader'):
compiled_tmpl = __compiletemplate(template, base=base, isString=isString)
else:
compiled_tmpl = _compiletemplate(template, base=base, isString=isString)
compiled_tmpl = compiled_tmpl(searchList=terms, filter=WebSafe)
if asTemplate:
return compiled_tmpl
else:
return output(str(compiled_tmpl))
class WebSafe(Filter):
def filter(self, val, **keywords):
return websafe(val)

View File

@ -0,0 +1,703 @@
"""
Database API
(part of web.py)
"""
# todo:
# - test with sqlite
# - a store function?
__all__ = [
"UnknownParamstyle", "UnknownDB",
"sqllist", "sqlors", "aparam", "reparam",
"SQLQuery", "sqlquote",
"SQLLiteral", "sqlliteral",
"connect",
"TransactionError", "transaction", "transact", "commit", "rollback",
"query",
"select", "insert", "update", "delete"
]
import time
try: import datetime
except ImportError: datetime = None
from utils import storage, iters, iterbetter
import webapi as web
try:
from DBUtils import PooledDB
web.config._hasPooling = True
except ImportError:
web.config._hasPooling = False
class _ItplError(ValueError):
def __init__(self, text, pos):
ValueError.__init__(self)
self.text = text
self.pos = pos
def __str__(self):
return "unfinished expression in %s at char %d" % (
repr(self.text), self.pos)
def _interpolate(format):
"""
Takes a format string and returns a list of 2-tuples of the form
(boolean, string) where boolean says whether string should be evaled
or not.
from <http://lfw.org/python/Itpl.py> (public domain, Ka-Ping Yee)
"""
from tokenize import tokenprog
def matchorfail(text, pos):
match = tokenprog.match(text, pos)
if match is None:
raise _ItplError(text, pos)
return match, match.end()
namechars = "abcdefghijklmnopqrstuvwxyz" \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
chunks = []
pos = 0
while 1:
dollar = format.find("$", pos)
if dollar < 0:
break
nextchar = format[dollar + 1]
if nextchar == "{":
chunks.append((0, format[pos:dollar]))
pos, level = dollar + 2, 1
while level:
match, pos = matchorfail(format, pos)
tstart, tend = match.regs[3]
token = format[tstart:tend]
if token == "{":
level = level + 1
elif token == "}":
level = level - 1
chunks.append((1, format[dollar + 2:pos - 1]))
elif nextchar in namechars:
chunks.append((0, format[pos:dollar]))
match, pos = matchorfail(format, dollar + 1)
while pos < len(format):
if format[pos] == "." and \
pos + 1 < len(format) and format[pos + 1] in namechars:
match, pos = matchorfail(format, pos + 1)
elif format[pos] in "([":
pos, level = pos + 1, 1
while level:
match, pos = matchorfail(format, pos)
tstart, tend = match.regs[3]
token = format[tstart:tend]
if token[0] in "([":
level = level + 1
elif token[0] in ")]":
level = level - 1
else:
break
chunks.append((1, format[dollar + 1:pos]))
else:
chunks.append((0, format[pos:dollar + 1]))
pos = dollar + 1 + (nextchar == "$")
if pos < len(format):
chunks.append((0, format[pos:]))
return chunks
class UnknownParamstyle(Exception):
"""
raised for unsupported db paramstyles
(currently supported: qmark, numeric, format, pyformat)
"""
pass
def aparam():
"""
Returns the appropriate string to be used to interpolate
a value with the current `web.ctx.db_module` or simply %s
if there isn't one.
>>> aparam()
'%s'
"""
if hasattr(web.ctx, 'db_module'):
style = web.ctx.db_module.paramstyle
else:
style = 'pyformat'
if style == 'qmark':
return '?'
elif style == 'numeric':
return ':1'
elif style in ['format', 'pyformat']:
return '%s'
raise UnknownParamstyle, style
def reparam(string_, dictionary):
"""
Takes a string and a dictionary and interpolates the string
using values from the dictionary. Returns an `SQLQuery` for the result.
>>> reparam("s = $s", dict(s=True))
<sql: "s = 't'">
"""
vals = []
result = []
for live, chunk in _interpolate(string_):
if live:
result.append(aparam())
vals.append(eval(chunk, dictionary))
else: result.append(chunk)
return SQLQuery(''.join(result), vals)
def sqlify(obj):
"""
converts `obj` to its proper SQL version
>>> sqlify(None)
'NULL'
>>> sqlify(True)
"'t'"
>>> sqlify(3)
'3'
"""
# because `1 == True and hash(1) == hash(True)`
# we have to do this the hard way...
if obj is None:
return 'NULL'
elif obj is True:
return "'t'"
elif obj is False:
return "'f'"
elif datetime and isinstance(obj, datetime.datetime):
return repr(obj.isoformat())
else:
return repr(obj)
class SQLQuery:
"""
You can pass this sort of thing as a clause in any db function.
Otherwise, you can pass a dictionary to the keyword argument `vars`
and the function will call reparam for you.
"""
# tested in sqlquote's docstring
def __init__(self, s='', v=()):
self.s, self.v = str(s), tuple(v)
def __getitem__(self, key): # for backwards-compatibility
return [self.s, self.v][key]
def __add__(self, other):
if isinstance(other, str):
self.s += other
elif isinstance(other, SQLQuery):
self.s += other.s
self.v += other.v
return self
def __radd__(self, other):
if isinstance(other, str):
self.s = other + self.s
return self
else:
return NotImplemented
def __str__(self):
try:
return self.s % tuple([sqlify(x) for x in self.v])
except (ValueError, TypeError):
return self.s
def __repr__(self):
return '<sql: %s>' % repr(str(self))
class SQLLiteral:
"""
Protects a string from `sqlquote`.
>>> insert('foo', time=SQLLiteral('NOW()'), _test=True)
<sql: 'INSERT INTO foo (time) VALUES (NOW())'>
"""
def __init__(self, v):
self.v = v
def __repr__(self):
return self.v
sqlliteral = SQLLiteral
def sqlquote(a):
"""
Ensures `a` is quoted properly for use in a SQL query.
>>> 'WHERE x = ' + sqlquote(True) + ' AND y = ' + sqlquote(3)
<sql: "WHERE x = 't' AND y = 3">
"""
return SQLQuery(aparam(), (a,))
class UnknownDB(Exception):
"""raised for unsupported dbms"""
pass
def connect(dbn, **keywords):
"""
Connects to the specified database.
`dbn` currently must be "postgres", "mysql", or "sqlite".
If DBUtils is installed, connection pooling will be used.
"""
if dbn == "postgres":
try:
import psycopg2 as db
except ImportError:
try:
import psycopg as db
except ImportError:
import pgdb as db
if 'pw' in keywords:
keywords['password'] = keywords['pw']
del keywords['pw']
keywords['database'] = keywords['db']
del keywords['db']
elif dbn == "mysql":
import MySQLdb as db
if 'pw' in keywords:
keywords['passwd'] = keywords['pw']
del keywords['pw']
db.paramstyle = 'pyformat' # it's both, like psycopg
elif dbn == "sqlite":
try:
import sqlite3 as db
db.paramstyle = 'qmark'
except ImportError:
try:
from pysqlite2 import dbapi2 as db
db.paramstyle = 'qmark'
except ImportError:
import sqlite as db
web.config._hasPooling = False
keywords['database'] = keywords['db']
del keywords['db']
elif dbn == "firebird":
import kinterbasdb as db
if 'pw' in keywords:
keywords['passwd'] = keywords['pw']
del keywords['pw']
keywords['database'] = keywords['db']
del keywords['db']
else:
raise UnknownDB, dbn
web.ctx.db_name = dbn
web.ctx.db_module = db
web.ctx.db_transaction = 0
web.ctx.db = keywords
def _PooledDB(db, keywords):
# In DBUtils 0.9.3, `dbapi` argument is renamed as `creator`
# see Bug#122112
if PooledDB.__version__.split('.') < '0.9.3'.split('.'):
return PooledDB.PooledDB(dbapi=db, **keywords)
else:
return PooledDB.PooledDB(creator=db, **keywords)
def db_cursor():
if isinstance(web.ctx.db, dict):
keywords = web.ctx.db
if web.config._hasPooling:
if 'db' not in globals():
globals()['db'] = _PooledDB(db, keywords)
web.ctx.db = globals()['db'].connection()
else:
web.ctx.db = db.connect(**keywords)
return web.ctx.db.cursor()
web.ctx.db_cursor = db_cursor
web.ctx.dbq_count = 0
def db_execute(cur, sql_query, dorollback=True):
"""executes an sql query"""
web.ctx.dbq_count += 1
try:
a = time.time()
out = cur.execute(sql_query.s, sql_query.v)
b = time.time()
except:
if web.config.get('db_printing'):
print >> web.debug, 'ERR:', str(sql_query)
if dorollback: rollback(care=False)
raise
if web.config.get('db_printing'):
print >> web.debug, '%s (%s): %s' % (round(b-a, 2), web.ctx.dbq_count, str(sql_query))
return out
web.ctx.db_execute = db_execute
return web.ctx.db
class TransactionError(Exception): pass
class transaction:
"""
A context that can be used in conjunction with "with" statements
to implement SQL transactions. Starts a transaction on enter,
rolls it back if there's an error; otherwise it commits it at the
end.
"""
def __enter__(self):
transact()
def __exit__(self, exctype, excvalue, traceback):
if exctype is not None:
rollback()
else:
commit()
def transact():
"""Start a transaction."""
if not web.ctx.db_transaction:
# commit everything up to now, so we don't rollback it later
if hasattr(web.ctx.db, 'commit'):
web.ctx.db.commit()
else:
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor,
SQLQuery("SAVEPOINT webpy_sp_%s" % web.ctx.db_transaction))
web.ctx.db_transaction += 1
def commit():
"""Commits a transaction."""
web.ctx.db_transaction -= 1
if web.ctx.db_transaction < 0:
raise TransactionError, "not in a transaction"
if not web.ctx.db_transaction:
if hasattr(web.ctx.db, 'commit'):
web.ctx.db.commit()
else:
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor,
SQLQuery("RELEASE SAVEPOINT webpy_sp_%s" % web.ctx.db_transaction))
def rollback(care=True):
"""Rolls back a transaction."""
web.ctx.db_transaction -= 1
if web.ctx.db_transaction < 0:
web.db_transaction = 0
if care:
raise TransactionError, "not in a transaction"
else:
return
if not web.ctx.db_transaction:
if hasattr(web.ctx.db, 'rollback'):
web.ctx.db.rollback()
else:
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor,
SQLQuery("ROLLBACK TO SAVEPOINT webpy_sp_%s" % web.ctx.db_transaction),
dorollback=False)
def query(sql_query, vars=None, processed=False, _test=False):
"""
Execute SQL query `sql_query` using dictionary `vars` to interpolate it.
If `processed=True`, `vars` is a `reparam`-style list to use
instead of interpolating.
>>> query("SELECT * FROM foo", _test=True)
<sql: 'SELECT * FROM foo'>
>>> query("SELECT * FROM foo WHERE x = $x", vars=dict(x='f'), _test=True)
<sql: "SELECT * FROM foo WHERE x = 'f'">
>>> query("SELECT * FROM foo WHERE x = " + sqlquote('f'), _test=True)
<sql: "SELECT * FROM foo WHERE x = 'f'">
"""
if vars is None: vars = {}
if not processed and not isinstance(sql_query, SQLQuery):
sql_query = reparam(sql_query, vars)
if _test: return sql_query
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor, sql_query)
if db_cursor.description:
names = [x[0] for x in db_cursor.description]
def iterwrapper():
row = db_cursor.fetchone()
while row:
yield storage(dict(zip(names, row)))
row = db_cursor.fetchone()
out = iterbetter(iterwrapper())
if web.ctx.db_name != "sqlite":
out.__len__ = lambda: int(db_cursor.rowcount)
out.list = lambda: [storage(dict(zip(names, x))) \
for x in db_cursor.fetchall()]
else:
out = db_cursor.rowcount
if not web.ctx.db_transaction: web.ctx.db.commit()
return out
def sqllist(lst):
"""
Converts the arguments for use in something like a WHERE clause.
>>> sqllist(['a', 'b'])
'a, b'
>>> sqllist('a')
'a'
"""
if isinstance(lst, str):
return lst
else:
return ', '.join(lst)
def sqlors(left, lst):
"""
`left is a SQL clause like `tablename.arg = `
and `lst` is a list of values. Returns a reparam-style
pair featuring the SQL that ORs together the clause
for each item in the lst.
>>> sqlors('foo = ', [])
<sql: '2+2=5'>
>>> sqlors('foo = ', [1])
<sql: 'foo = 1'>
>>> sqlors('foo = ', 1)
<sql: 'foo = 1'>
>>> sqlors('foo = ', [1,2,3])
<sql: '(foo = 1 OR foo = 2 OR foo = 3)'>
"""
if isinstance(lst, iters):
lst = list(lst)
ln = len(lst)
if ln == 0:
return SQLQuery("2+2=5", [])
if ln == 1:
lst = lst[0]
if isinstance(lst, iters):
return SQLQuery('(' + left +
(' OR ' + left).join([aparam() for param in lst]) + ")", lst)
else:
return SQLQuery(left + aparam(), [lst])
def sqlwhere(dictionary, grouping=' AND '):
"""
Converts a `dictionary` to an SQL WHERE clause `SQLQuery`.
>>> sqlwhere({'cust_id': 2, 'order_id':3})
<sql: 'order_id = 3 AND cust_id = 2'>
>>> sqlwhere({'cust_id': 2, 'order_id':3}, grouping=', ')
<sql: 'order_id = 3, cust_id = 2'>
"""
return SQLQuery(grouping.join([
'%s = %s' % (k, aparam()) for k in dictionary.keys()
]), dictionary.values())
def select(tables, vars=None, what='*', where=None, order=None, group=None,
limit=None, offset=None, _test=False):
"""
Selects `what` from `tables` with clauses `where`, `order`,
`group`, `limit`, and `offset`. Uses vars to interpolate.
Otherwise, each clause can be a SQLQuery.
>>> select('foo', _test=True)
<sql: 'SELECT * FROM foo'>
>>> select(['foo', 'bar'], where="foo.bar_id = bar.id", limit=5, _test=True)
<sql: 'SELECT * FROM foo, bar WHERE foo.bar_id = bar.id LIMIT 5'>
"""
if vars is None: vars = {}
qout = ""
def gen_clause(sql, val):
if isinstance(val, (int, long)):
if sql == 'WHERE':
nout = 'id = ' + sqlquote(val)
else:
nout = SQLQuery(val)
elif isinstance(val, (list, tuple)) and len(val) == 2:
nout = SQLQuery(val[0], val[1]) # backwards-compatibility
elif isinstance(val, SQLQuery):
nout = val
elif val:
nout = reparam(val, vars)
else:
return ""
out = ""
if qout: out += " "
out += sql + " " + nout
return out
if web.ctx.get('db_name') == "firebird":
for (sql, val) in (
('FIRST', limit),
('SKIP', offset)
):
qout += gen_clause(sql, val)
if qout:
SELECT = 'SELECT ' + qout
else:
SELECT = 'SELECT'
qout = ""
sql_clauses = (
(SELECT, what),
('FROM', sqllist(tables)),
('WHERE', where),
('GROUP BY', group),
('ORDER BY', order)
)
else:
sql_clauses = (
('SELECT', what),
('FROM', sqllist(tables)),
('WHERE', where),
('GROUP BY', group),
('ORDER BY', order),
('LIMIT', limit),
('OFFSET', offset)
)
for (sql, val) in sql_clauses:
qout += gen_clause(sql, val)
if _test: return qout
return query(qout, processed=True)
def insert(tablename, seqname=None, _test=False, **values):
"""
Inserts `values` into `tablename`. Returns current sequence ID.
Set `seqname` to the ID if it's not the default, or to `False`
if there isn't one.
>>> insert('foo', joe='bob', a=2, _test=True)
<sql: "INSERT INTO foo (a, joe) VALUES (2, 'bob')">
"""
if values:
sql_query = SQLQuery("INSERT INTO %s (%s) VALUES (%s)" % (
tablename,
", ".join(values.keys()),
', '.join([aparam() for x in values])
), values.values())
else:
sql_query = SQLQuery("INSERT INTO %s DEFAULT VALUES" % tablename)
if _test: return sql_query
db_cursor = web.ctx.db_cursor()
if seqname is False:
pass
elif web.ctx.db_name == "postgres":
if seqname is None:
seqname = tablename + "_id_seq"
sql_query += "; SELECT currval('%s')" % seqname
elif web.ctx.db_name == "mysql":
web.ctx.db_execute(db_cursor, sql_query)
sql_query = SQLQuery("SELECT last_insert_id()")
elif web.ctx.db_name == "sqlite":
web.ctx.db_execute(db_cursor, sql_query)
# not really the same...
sql_query = SQLQuery("SELECT last_insert_rowid()")
web.ctx.db_execute(db_cursor, sql_query)
try:
out = db_cursor.fetchone()[0]
except Exception:
out = None
if not web.ctx.db_transaction: web.ctx.db.commit()
return out
def update(tables, where, vars=None, _test=False, **values):
"""
Update `tables` with clause `where` (interpolated using `vars`)
and setting `values`.
>>> joe = 'Joseph'
>>> update('foo', where='name = $joe', name='bob', age=5,
... vars=locals(), _test=True)
<sql: "UPDATE foo SET age = 5, name = 'bob' WHERE name = 'Joseph'">
"""
if vars is None: vars = {}
if isinstance(where, (int, long)):
where = "id = " + sqlquote(where)
elif isinstance(where, (list, tuple)) and len(where) == 2:
where = SQLQuery(where[0], where[1])
elif isinstance(where, SQLQuery):
pass
else:
where = reparam(where, vars)
query = (
"UPDATE " + sqllist(tables) +
" SET " + sqlwhere(values, ', ') +
" WHERE " + where)
if _test: return query
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor, query)
if not web.ctx.db_transaction: web.ctx.db.commit()
return db_cursor.rowcount
def delete(table, where=None, using=None, vars=None, _test=False):
"""
Deletes from `table` with clauses `where` and `using`.
>>> name = 'Joe'
>>> delete('foo', where='name = $name', vars=locals(), _test=True)
<sql: "DELETE FROM foo WHERE name = 'Joe'">
"""
if vars is None: vars = {}
if isinstance(where, (int, long)):
where = "id = " + sqlquote(where)
elif isinstance(where, (list, tuple)) and len(where) == 2:
where = SQLQuery(where[0], where[1])
elif isinstance(where, SQLQuery):
pass
elif where is None:
pass
else:
where = reparam(where, vars)
q = 'DELETE FROM ' + table
if where:
q += ' WHERE ' + where
if using and web.ctx.get('db_name') != "firebird":
q += ' USING ' + sqllist(using)
if _test: return q
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor, q)
if not web.ctx.db_transaction: web.ctx.db.commit()
return db_cursor.rowcount
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@ -0,0 +1,316 @@
"""
pretty debug errors
(part of web.py)
adapted from Django <djangoproject.com>
Copyright (c) 2005, the Lawrence Journal-World
Used under the modified BSD license:
http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
"""
__all__ = ["debugerror", "djangoerror"]
import sys, urlparse, pprint
from net import websafe
from template import Template
import webapi as web
import os, os.path
whereami = os.path.join(os.getcwd(), __file__)
whereami = os.path.sep.join(whereami.split(os.path.sep)[:-1])
djangoerror_t = """\
$def with (exception_type, exception_value, frames)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="robots" content="NONE,NOARCHIVE" />
<title>$exception_type at $ctx.path</title>
<style type="text/css">
html * { padding:0; margin:0; }
body * { padding:10px 20px; }
body * * { padding:0; }
body { font:small sans-serif; }
body>div { border-bottom:1px solid #ddd; }
h1 { font-weight:normal; }
h2 { margin-bottom:.8em; }
h2 span { font-size:80%; color:#666; font-weight:normal; }
h3 { margin:1em 0 .5em 0; }
h4 { margin:0 0 .5em 0; font-weight: normal; }
table {
border:1px solid #ccc; border-collapse: collapse; background:white; }
tbody td, tbody th { vertical-align:top; padding:2px 3px; }
thead th {
padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
font-weight:normal; font-size:11px; border:1px solid #ddd; }
tbody th { text-align:right; color:#666; padding-right:.5em; }
table.vars { margin:5px 0 2px 40px; }
table.vars td, table.req td { font-family:monospace; }
table td.code { width:100%;}
table td.code div { overflow:hidden; }
table.source th { color:#666; }
table.source td {
font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
ul.traceback { list-style-type:none; }
ul.traceback li.frame { margin-bottom:1em; }
div.context { margin: 10px 0; }
div.context ol {
padding-left:30px; margin:0 10px; list-style-position: inside; }
div.context ol li {
font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
div.context ol.context-line li { color:black; background-color:#ccc; }
div.context ol.context-line li span { float: right; }
div.commands { margin-left: 40px; }
div.commands a { color:black; text-decoration:none; }
#summary { background: #ffc; }
#summary h2 { font-weight: normal; color: #666; }
#explanation { background:#eee; }
#template, #template-not-exist { background:#f6f6f6; }
#template-not-exist ul { margin: 0 0 0 20px; }
#traceback { background:#eee; }
#requestinfo { background:#f6f6f6; padding-left:120px; }
#summary table { border:none; background:transparent; }
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
#requestinfo h3 { margin-bottom:-1em; }
.error { background: #ffc; }
.specific { color:#cc3300; font-weight:bold; }
</style>
<script type="text/javascript">
//<!--
function getElementsByClassName(oElm, strTagName, strClassName){
// Written by Jonathan Snook, http://www.snook.ca/jon;
// Add-ons by Robert Nyman, http://www.robertnyman.com
var arrElements = (strTagName == "*" && document.all)? document.all :
oElm.getElementsByTagName(strTagName);
var arrReturnElements = new Array();
strClassName = strClassName.replace(/\-/g, "\\-");
var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$)");
var oElement;
for(var i=0; i<arrElements.length; i++){
oElement = arrElements[i];
if(oRegExp.test(oElement.className)){
arrReturnElements.push(oElement);
}
}
return (arrReturnElements)
}
function hideAll(elems) {
for (var e = 0; e < elems.length; e++) {
elems[e].style.display = 'none';
}
}
window.onload = function() {
hideAll(getElementsByClassName(document, 'table', 'vars'));
hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
hideAll(getElementsByClassName(document, 'ol', 'post-context'));
}
function toggle() {
for (var i = 0; i < arguments.length; i++) {
var e = document.getElementById(arguments[i]);
if (e) {
e.style.display = e.style.display == 'none' ? 'block' : 'none';
}
}
return false;
}
function varToggle(link, id) {
toggle('v' + id);
var s = link.getElementsByTagName('span')[0];
var uarr = String.fromCharCode(0x25b6);
var darr = String.fromCharCode(0x25bc);
s.innerHTML = s.innerHTML == uarr ? darr : uarr;
return false;
}
//-->
</script>
</head>
<body>
<div id="summary">
<h1>$exception_type at $ctx.path</h1>
<h2>$exception_value</h2>
<table><tr>
<th>Python</th>
<td>$frames[0].filename in $frames[0].function, line $frames[0].lineno</td>
</tr><tr>
<th>Web</th>
<td>$ctx.method $ctx.home$ctx.path</td>
</tr></table>
</div>
<div id="traceback">
<h2>Traceback <span>(innermost first)</span></h2>
<ul class="traceback">
$for frame in frames:
<li class="frame">
<code>$frame.filename</code> in <code>$frame.function</code>
$if frame.context_line:
<div class="context" id="c$frame.id">
$if frame.pre_context:
<ol start="$frame.pre_context_lineno" class="pre-context" id="pre$frame.id">
$for line in frame.pre_context:
<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>
</ol>
<ol start="$frame.lineno" class="context-line"><li onclick="toggle('pre$frame.id', 'post$frame.id')">$frame.context_line <span>...</span></li></ol>
$if frame.post_context:
<ol start='${frame.lineno + 1}' class="post-context" id="post$frame.id">
$for line in frame.post_context:
<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>
</ol>
</div>
$if frame.vars:
<div class="commands">
<a href='#' onclick="return varToggle(this, '$frame.id')"><span>&#x25b6;</span> Local vars</a>
$# $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame))
</div>
$:dicttable(frame.vars, kls='vars', id=('v' + str(frame.id)))
</li>
</ul>
</div>
<div id="requestinfo">
$if ctx.output or ctx.headers:
<h2>Response so far</h2>
<h3>HEADERS</h3>
<p class="req"><code>
$for kv in ctx.headers:
$kv[0]: $kv[1]<br />
$else:
[no headers]
</code></p>
<h3>BODY</h3>
<p class="req" style="padding-bottom: 2em"><code>
$ctx.output
</code></p>
<h2>Request information</h2>
<h3>INPUT</h3>
$:dicttable(web.input())
<h3 id="cookie-info">COOKIES</h3>
$:dicttable(web.cookies())
<h3 id="meta-info">META</h3>
$ newctx = []
$# ) and (k not in ['env', 'output', 'headers', 'environ', 'status', 'db_execute']):
$for k, v in ctx.iteritems():
$if not k.startswith('_') and (k in x):
$newctx.append(kv)
$:dicttable(dict(newctx))
<h3 id="meta-info">ENVIRONMENT</h3>
$:dicttable(ctx.env)
</div>
<div id="explanation">
<p>
You're seeing this error because you have <code>web.internalerror</code>
set to <code>web.debugerror</code>. Change that if you want a different one.
</p>
</div>
</body>
</html>
"""
dicttable_t = r"""$def with (d, kls='req', id=None)
$if d:
<table class="$kls"\
$if id: id="$id"\
><thead><tr><th>Variable</th><th>Value</th></tr></thead>
<tbody>
$ temp = d.items()
$temp.sort()
$for kv in temp:
<tr><td>$kv[0]</td><td class="code"><div>$prettify(kv[1])</div></td></tr>
</tbody>
</table>
$else:
<p>No data.</p>
"""
dicttable_r = Template(dicttable_t, filter=websafe)
djangoerror_r = Template(djangoerror_t, filter=websafe)
def djangoerror():
def _get_lines_from_file(filename, lineno, context_lines):
"""
Returns context_lines before and after lineno from file.
Returns (pre_context_lineno, pre_context, context_line, post_context).
"""
try:
source = open(filename).readlines()
lower_bound = max(0, lineno - context_lines)
upper_bound = lineno + context_lines
pre_context = \
[line.strip('\n') for line in source[lower_bound:lineno]]
context_line = source[lineno].strip('\n')
post_context = \
[line.strip('\n') for line in source[lineno + 1:upper_bound]]
return lower_bound, pre_context, context_line, post_context
except (OSError, IOError):
return None, [], None, []
exception_type, exception_value, tback = sys.exc_info()
frames = []
while tback is not None:
filename = tback.tb_frame.f_code.co_filename
function = tback.tb_frame.f_code.co_name
lineno = tback.tb_lineno - 1
pre_context_lineno, pre_context, context_line, post_context = \
_get_lines_from_file(filename, lineno, 7)
frames.append(web.storage({
'tback': tback,
'filename': filename,
'function': function,
'lineno': lineno,
'vars': tback.tb_frame.f_locals,
'id': id(tback),
'pre_context': pre_context,
'context_line': context_line,
'post_context': post_context,
'pre_context_lineno': pre_context_lineno,
}))
tback = tback.tb_next
frames.reverse()
urljoin = urlparse.urljoin
def prettify(x):
try:
out = pprint.pformat(x)
except Exception, e:
out = '[could not display: <' + e.__class__.__name__ + \
': '+str(e)+'>]'
return out
dt = dicttable_r
dt.globals = {'prettify': prettify}
t = djangoerror_r
t.globals = {'ctx': web.ctx, 'web':web, 'dicttable':dt, 'dict':dict, 'str':str}
return t(exception_type, exception_value, frames)
def debugerror():
"""
A replacement for `internalerror` that presents a nice page with lots
of debug information for the programmer.
(Based on the beautiful 500 page from [Django](http://djangoproject.com/),
designed by [Wilson Miner](http://wilsonminer.com/).)
"""
web.ctx.headers = [('Content-Type', 'text/html')]
web.ctx.output = djangoerror()
if __name__ == "__main__":
urls = (
'/', 'index'
)
class index:
def GET(self):
thisdoesnotexist
web.internalerror = web.debugerror
web.run(urls)

View File

@ -0,0 +1,215 @@
"""
HTML forms
(part of web.py)
"""
import copy, re
import webapi as web
import utils, net
def attrget(obj, attr, value=None):
if hasattr(obj, 'has_key') and obj.has_key(attr): return obj[attr]
if hasattr(obj, attr): return getattr(obj, attr)
return value
class Form:
def __init__(self, *inputs, **kw):
self.inputs = inputs
self.valid = True
self.note = None
self.validators = kw.pop('validators', [])
def __call__(self, x=None):
o = copy.deepcopy(self)
if x: o.validates(x)
return o
def render(self):
out = ''
out += self.rendernote(self.note)
out += '<table>\n'
for i in self.inputs:
out += ' <tr><th><label for="%s">%s</label></th>' % (i.id, i.description)
out += "<td>"+i.pre+i.render()+i.post+"</td>"
out += '<td id="note_%s">%s</td></tr>\n' % (i.id, self.rendernote(i.note))
out += "</table>"
return out
def rendernote(self, note):
if note: return '<strong class="wrong">%s</strong>' % note
else: return ""
def validates(self, source=None, _validate=True, **kw):
source = source or kw or web.input()
out = True
for i in self.inputs:
v = attrget(source, i.name)
if _validate:
out = i.validate(v) and out
else:
i.value = v
if _validate:
out = out and self._validate(source)
self.valid = out
return out
def _validate(self, value):
self.value = value
for v in self.validators:
if not v.valid(value):
self.note = v.msg
return False
return True
def fill(self, source=None, **kw):
return self.validates(source, _validate=False, **kw)
def __getitem__(self, i):
for x in self.inputs:
if x.name == i: return x
raise KeyError, i
def _get_d(self): #@@ should really be form.attr, no?
return utils.storage([(i.name, i.value) for i in self.inputs])
d = property(_get_d)
class Input(object):
def __init__(self, name, *validators, **attrs):
self.description = attrs.pop('description', name)
self.value = attrs.pop('value', None)
self.pre = attrs.pop('pre', "")
self.post = attrs.pop('post', "")
self.id = attrs.setdefault('id', name)
if 'class_' in attrs:
attrs['class'] = attrs['class_']
del attrs['class_']
self.name, self.validators, self.attrs, self.note = name, validators, attrs, None
def validate(self, value):
self.value = value
for v in self.validators:
if not v.valid(value):
self.note = v.msg
return False
return True
def render(self): raise NotImplementedError
def addatts(self):
str = ""
for (n, v) in self.attrs.items():
str += ' %s="%s"' % (n, net.websafe(v))
return str
#@@ quoting
class Textbox(Input):
def render(self):
x = '<input type="text" name="%s"' % net.websafe(self.name)
if self.value: x += ' value="%s"' % net.websafe(self.value)
x += self.addatts()
x += ' />'
return x
class Password(Input):
def render(self):
x = '<input type="password" name="%s"' % net.websafe(self.name)
if self.value: x += ' value="%s"' % net.websafe(self.value)
x += self.addatts()
x += ' />'
return x
class Textarea(Input):
def render(self):
x = '<textarea name="%s"' % net.websafe(self.name)
x += self.addatts()
x += '>'
if self.value is not None: x += net.websafe(self.value)
x += '</textarea>'
return x
class Dropdown(Input):
def __init__(self, name, args, *validators, **attrs):
self.args = args
super(Dropdown, self).__init__(name, *validators, **attrs)
def render(self):
x = '<select name="%s"%s>\n' % (net.websafe(self.name), self.addatts())
for arg in self.args:
if type(arg) == tuple:
value, desc= arg
else:
value, desc = arg, arg
if self.value == value: select_p = ' selected="selected"'
else: select_p = ''
x += ' <option %s value="%s">%s</option>\n' % (select_p, net.websafe(value), net.websafe(desc))
x += '</select>\n'
return x
class Radio(Input):
def __init__(self, name, args, *validators, **attrs):
self.args = args
super(Radio, self).__init__(name, *validators, **attrs)
def render(self):
x = '<span>'
for arg in self.args:
if self.value == arg: select_p = ' checked="checked"'
else: select_p = ''
x += '<input type="radio" name="%s" value="%s"%s%s /> %s ' % (net.websafe(self.name), net.websafe(arg), select_p, self.addatts(), net.websafe(arg))
return x+'</span>'
class Checkbox(Input):
def render(self):
x = '<input name="%s" type="checkbox"' % net.websafe(self.name)
if self.value: x += ' checked="checked"'
x += self.addatts()
x += ' />'
return x
class Button(Input):
def __init__(self, name, *validators, **attrs):
super(Button, self).__init__(name, *validators, **attrs)
self.description = ""
def render(self):
safename = net.websafe(self.name)
x = '<button name="%s"%s>%s</button>' % (safename, self.addatts(), safename)
return x
class Hidden(Input):
def __init__(self, name, *validators, **attrs):
super(Hidden, self).__init__(name, *validators, **attrs)
# it doesnt make sence for a hidden field to have description
self.description = ""
def render(self):
x = '<input type="hidden" name="%s"' % net.websafe(self.name)
if self.value: x += ' value="%s"' % net.websafe(self.value)
x += ' />'
return x
class File(Input):
def render(self):
x = '<input type="file" name="%s"' % net.websafe(self.name)
x += self.addatts()
x += ' />'
return x
class Validator:
def __deepcopy__(self, memo): return copy.copy(self)
def __init__(self, msg, test, jstest=None): utils.autoassign(self, locals())
def valid(self, value):
try: return self.test(value)
except: return False
notnull = Validator("Required", bool)
class regexp(Validator):
def __init__(self, rexp, msg):
self.rexp = re.compile(rexp)
self.msg = msg
def valid(self, value):
return bool(self.rexp.match(value))

View File

@ -0,0 +1,270 @@
"""
HTTP Utilities
(from web.py)
"""
__all__ = [
"expires", "lastmodified",
"prefixurl", "modified",
"redirect", "found", "seeother", "tempredirect",
"write",
"changequery", "url",
"background", "backgrounder",
"Reloader", "reloader", "profiler",
]
import sys, os, threading, urllib, urlparse
try: import datetime
except ImportError: pass
import net, utils, webapi as web
def prefixurl(base=''):
"""
Sorry, this function is really difficult to explain.
Maybe some other time.
"""
url = web.ctx.path.lstrip('/')
for i in xrange(url.count('/')):
base += '../'
if not base:
base = './'
return base
def expires(delta):
"""
Outputs an `Expires` header for `delta` from now.
`delta` is a `timedelta` object or a number of seconds.
"""
if isinstance(delta, (int, long)):
delta = datetime.timedelta(seconds=delta)
date_obj = datetime.datetime.utcnow() + delta
web.header('Expires', net.httpdate(date_obj))
def lastmodified(date_obj):
"""Outputs a `Last-Modified` header for `datetime`."""
web.header('Last-Modified', net.httpdate(date_obj))
def modified(date=None, etag=None):
n = web.ctx.env.get('HTTP_IF_NONE_MATCH')
m = net.parsehttpdate(web.ctx.env.get('HTTP_IF_MODIFIED_SINCE', '').split(';')[0])
validate = False
if etag:
raise NotImplementedError, "no etag support yet"
# should really be a warning
if date and m:
# we subtract a second because
# HTTP dates don't have sub-second precision
if date-datetime.timedelta(seconds=1) <= m:
validate = True
if validate: web.ctx.status = '304 Not Modified'
return not validate
"""
By default, these all return simple error messages that send very short messages
(like "bad request") to the user. They can and should be overridden
to return nicer ones.
"""
def redirect(url, status='301 Moved Permanently'):
"""
Returns a `status` redirect to the new URL.
`url` is joined with the base URL so that things like
`redirect("about") will work properly.
"""
newloc = urlparse.urljoin(web.ctx.path, url)
# if newloc is relative then make it absolute
#mvoncken:Disabled because we don't want to redirect to localhost!
#if newloc.startswith('/'):
# newloc = web.ctx.home + newloc
web.ctx.status = status
web.ctx.output = ''
web.header('Content-Type', 'text/html')
web.header('Location', newloc)
# seems to add a three-second delay for some reason:
# web.output('<a href="'+ newloc + '">moved permanently</a>')
def found(url):
"""A `302 Found` redirect."""
return redirect(url, '302 Found')
def seeother(url):
"""A `303 See Other` redirect."""
return redirect(url, '303 See Other')
def tempredirect(url):
"""A `307 Temporary Redirect` redirect."""
return redirect(url, '307 Temporary Redirect')
def write(cgi_response):
"""
Converts a standard CGI-style string response into `header` and
`output` calls.
"""
cgi_response = str(cgi_response)
cgi_response.replace('\r\n', '\n')
head, body = cgi_response.split('\n\n', 1)
lines = head.split('\n')
for line in lines:
if line.isspace():
continue
hdr, value = line.split(":", 1)
value = value.strip()
if hdr.lower() == "status":
web.ctx.status = value
else:
web.header(hdr, value)
web.output(body)
def urlencode(query):
"""
Same as urllib.urlencode, but supports unicode strings.
>>> urlencode({'text':'foo bar'})
'text=foo+bar'
"""
query = dict([(k, utils.utf8(v)) for k, v in query.items()])
return urllib.urlencode(query)
def changequery(query=None, **kw):
"""
Imagine you're at `/foo?a=1&b=2`. Then `changequery(a=3)` will return
`/foo?a=3&b=2` -- the same URL but with the arguments you requested
changed.
"""
if query is None:
query = web.input(_method='get')
for k, v in kw.iteritems():
if v is None:
query.pop(k, None)
else:
query[k] = v
out = web.ctx.path
if query:
out += '?' + urlencode(query)
return out
def url(path=None, **kw):
"""
Makes url by concatinating web.ctx.homepath and path and the
query string created using the arguments.
"""
if path is None:
path = web.ctx.path
if path.startswith("/"):
out = web.ctx.homepath + path
else:
out = path
if kw:
out += '?' + urlencode(kw)
return out
def background(func):
"""A function decorator to run a long-running function as a background thread."""
def internal(*a, **kw):
web.data() # cache it
tmpctx = web._context[threading.currentThread()]
web._context[threading.currentThread()] = utils.storage(web.ctx.copy())
def newfunc():
web._context[threading.currentThread()] = tmpctx
func(*a, **kw)
myctx = web._context[threading.currentThread()]
for k in myctx.keys():
if k not in ['status', 'headers', 'output']:
try: del myctx[k]
except KeyError: pass
t = threading.Thread(target=newfunc)
background.threaddb[id(t)] = t
t.start()
web.ctx.headers = []
return seeother(changequery(_t=id(t)))
return internal
background.threaddb = {}
def backgrounder(func):
def internal(*a, **kw):
i = web.input(_method='get')
if '_t' in i:
try:
t = background.threaddb[int(i._t)]
except KeyError:
return web.notfound()
web._context[threading.currentThread()] = web._context[t]
return
else:
return func(*a, **kw)
return internal
class Reloader:
"""
Before every request, checks to see if any loaded modules have changed on
disk and, if so, reloads them.
"""
def __init__(self, func):
self.func = func
self.mtimes = {}
# cheetah:
# b = _compiletemplate.bases
# _compiletemplate = globals()['__compiletemplate']
# _compiletemplate.bases = b
web.loadhooks['reloader'] = self.check
# todo:
# - replace relrcheck with a loadhook
#if reloader in middleware:
# relr = reloader(None)
# relrcheck = relr.check
# middleware.remove(reloader)
#else:
# relr = None
# relrcheck = lambda: None
# if relr:
# relr.func = wsgifunc
# return wsgifunc
#
def check(self):
for mod in sys.modules.values():
try:
mtime = os.stat(mod.__file__).st_mtime
except (AttributeError, OSError, IOError):
continue
if mod.__file__.endswith('.pyc') and \
os.path.exists(mod.__file__[:-1]):
mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
if mod not in self.mtimes:
self.mtimes[mod] = mtime
elif self.mtimes[mod] < mtime:
try:
reload(mod)
self.mtimes[mod] = mtime
except ImportError:
pass
return True
def __call__(self, e, o):
self.check()
return self.func(e, o)
reloader = Reloader
def profiler(app):
"""Outputs basic profiling information at the bottom of each response."""
from utils import profile
def profile_internal(e, o):
out, result = profile(app)(e, o)
return out + ['<pre>' + net.websafe(result) + '</pre>']
return profile_internal
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@ -0,0 +1,224 @@
__all__ = ["runsimple"]
import sys, os
import webapi as web
import net
def runbasic(func, server_address=("0.0.0.0", 8080)):
"""
Runs a simple HTTP server hosting WSGI app `func`. The directory `static/`
is hosted statically.
Based on [WsgiServer][ws] from [Colin Stewart][cs].
[ws]: http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html
[cs]: http://www.owlfish.com/
"""
# Copyright (c) 2004 Colin Stewart (http://www.owlfish.com/)
# Modified somewhat for simplicity
# Used under the modified BSD license:
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
import SimpleHTTPServer, SocketServer, BaseHTTPServer, urlparse
import socket, errno
import traceback
class WSGIHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def run_wsgi_app(self):
protocol, host, path, parameters, query, fragment = \
urlparse.urlparse('http://dummyhost%s' % self.path)
# we only use path, query
env = {'wsgi.version': (1, 0)
,'wsgi.url_scheme': 'http'
,'wsgi.input': self.rfile
,'wsgi.errors': sys.stderr
,'wsgi.multithread': 1
,'wsgi.multiprocess': 0
,'wsgi.run_once': 0
,'REQUEST_METHOD': self.command
,'REQUEST_URI': self.path
,'PATH_INFO': path
,'QUERY_STRING': query
,'CONTENT_TYPE': self.headers.get('Content-Type', '')
,'CONTENT_LENGTH': self.headers.get('Content-Length', '')
,'REMOTE_ADDR': self.client_address[0]
,'SERVER_NAME': self.server.server_address[0]
,'SERVER_PORT': str(self.server.server_address[1])
,'SERVER_PROTOCOL': self.request_version
}
for http_header, http_value in self.headers.items():
env ['HTTP_%s' % http_header.replace('-', '_').upper()] = \
http_value
# Setup the state
self.wsgi_sent_headers = 0
self.wsgi_headers = []
try:
# We have there environment, now invoke the application
result = self.server.app(env, self.wsgi_start_response)
try:
try:
for data in result:
if data:
self.wsgi_write_data(data)
finally:
if hasattr(result, 'close'):
result.close()
except socket.error, socket_err:
# Catch common network errors and suppress them
if (socket_err.args[0] in \
(errno.ECONNABORTED, errno.EPIPE)):
return
except socket.timeout, socket_timeout:
return
except:
print >> web.debug, traceback.format_exc(),
if (not self.wsgi_sent_headers):
# We must write out something!
self.wsgi_write_data(" ")
return
do_POST = run_wsgi_app
do_PUT = run_wsgi_app
do_DELETE = run_wsgi_app
def do_GET(self):
if self.path.startswith('/static/'):
SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
else:
self.run_wsgi_app()
def wsgi_start_response(self, response_status, response_headers,
exc_info=None):
if (self.wsgi_sent_headers):
raise Exception \
("Headers already sent and start_response called again!")
# Should really take a copy to avoid changes in the application....
self.wsgi_headers = (response_status, response_headers)
return self.wsgi_write_data
def wsgi_write_data(self, data):
if (not self.wsgi_sent_headers):
status, headers = self.wsgi_headers
# Need to send header prior to data
status_code = status[:status.find(' ')]
status_msg = status[status.find(' ') + 1:]
self.send_response(int(status_code), status_msg)
for header, value in headers:
self.send_header(header, value)
self.end_headers()
self.wsgi_sent_headers = 1
# Send the data
self.wfile.write(data)
class WSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
def __init__(self, func, server_address):
BaseHTTPServer.HTTPServer.__init__(self,
server_address,
WSGIHandler)
self.app = func
self.serverShuttingDown = 0
print "http://%s:%d/" % server_address
WSGIServer(func, server_address).serve_forever()
def runsimple(func, server_address=("0.0.0.0", 8080)):
"""
Runs [CherryPy][cp] WSGI server hosting WSGI app `func`.
The directory `static/` is hosted statically.
[cp]: http://www.cherrypy.org
"""
from wsgiserver import CherryPyWSGIServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
from BaseHTTPServer import BaseHTTPRequestHandler
class StaticApp(SimpleHTTPRequestHandler):
"""WSGI application for serving static files."""
def __init__(self, environ, start_response):
self.headers = []
self.environ = environ
self.start_response = start_response
def send_response(self, status, msg=""):
self.status = str(status) + " " + msg
def send_header(self, name, value):
self.headers.append((name, value))
def end_headers(self):
pass
def log_message(*a): pass
def __iter__(self):
environ = self.environ
self.path = environ.get('PATH_INFO', '')
self.client_address = environ.get('REMOTE_ADDR','-'), \
environ.get('REMOTE_PORT','-')
self.command = environ.get('REQUEST_METHOD', '-')
from cStringIO import StringIO
self.wfile = StringIO() # for capturing error
f = self.send_head()
self.start_response(self.status, self.headers)
if f:
block_size = 16 * 1024
while True:
buf = f.read(block_size)
if not buf:
break
yield buf
f.close()
else:
value = self.wfile.getvalue()
yield value
class WSGIWrapper(BaseHTTPRequestHandler):
"""WSGI wrapper for logging the status and serving static files."""
def __init__(self, app):
self.app = app
self.format = '%s - - [%s] "%s %s %s" - %s'
def __call__(self, environ, start_response):
def xstart_response(status, response_headers, *args):
write = start_response(status, response_headers, *args)
self.log(status, environ)
return write
path = environ.get('PATH_INFO', '')
if path.startswith('/static/'):
return StaticApp(environ, xstart_response)
else:
return self.app(environ, xstart_response)
def log(self, status, environ):
outfile = environ.get('wsgi.errors', web.debug)
req = environ.get('PATH_INFO', '_')
protocol = environ.get('ACTUAL_SERVER_PROTOCOL', '-')
method = environ.get('REQUEST_METHOD', '-')
host = "%s:%s" % (environ.get('REMOTE_ADDR','-'),
environ.get('REMOTE_PORT','-'))
#@@ It is really bad to extend from
#@@ BaseHTTPRequestHandler just for this method
time = self.log_date_time_string()
print >> outfile, self.format % (host, time, protocol,
method, req, status)
func = WSGIWrapper(func)
server = CherryPyWSGIServer(server_address, func, server_name="localhost")
print "http://%s:%d/" % server_address
try:
server.start()
except KeyboardInterrupt:
server.stop()

View File

@ -0,0 +1,155 @@
"""
Network Utilities
(from web.py)
"""
__all__ = [
"validipaddr", "validipport", "validip", "validaddr",
"urlquote",
"httpdate", "parsehttpdate",
"htmlquote", "websafe",
]
import urllib, time
try: import datetime
except ImportError: pass
def validipaddr(address):
"""returns True if `address` is a valid IPv4 address"""
try:
octets = address.split('.')
assert len(octets) == 4
for x in octets:
assert 0 <= int(x) <= 255
except (AssertionError, ValueError):
return False
return True
def validipport(port):
"""returns True if `port` is a valid IPv4 port"""
try:
assert 0 <= int(port) <= 65535
except (AssertionError, ValueError):
return False
return True
def validip(ip, defaultaddr="0.0.0.0", defaultport=8080):
"""returns `(ip_address, port)` from string `ip_addr_port`"""
addr = defaultaddr
port = defaultport
ip = ip.split(":", 1)
if len(ip) == 1:
if not ip[0]:
pass
elif validipaddr(ip[0]):
addr = ip[0]
elif validipport(ip[0]):
port = int(ip[0])
else:
raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
elif len(ip) == 2:
addr, port = ip
if not validipaddr(addr) and validipport(port):
raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
port = int(port)
else:
raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
return (addr, port)
def validaddr(string_):
"""
returns either (ip_address, port) or "/path/to/socket" from string_
>>> validaddr('/path/to/socket')
'/path/to/socket'
>>> validaddr('8000')
('0.0.0.0', 8000)
>>> validaddr('127.0.0.1')
('127.0.0.1', 8080)
>>> validaddr('127.0.0.1:8000')
('127.0.0.1', 8000)
>>> validaddr('fff')
Traceback (most recent call last):
...
ValueError: fff is not a valid IP address/port
"""
if '/' in string_:
return string_
else:
return validip(string_)
def urlquote(val):
"""
Quotes a string for use in a URL.
>>> urlquote('://?f=1&j=1')
'%3A//%3Ff%3D1%26j%3D1'
>>> urlquote(None)
''
>>> urlquote(u'\u203d')
'%E2%80%BD'
"""
if val is None: return ''
if not isinstance(val, unicode): val = str(val)
else: val = val.encode('utf-8')
return urllib.quote(val)
def httpdate(date_obj):
"""
Formats a datetime object for use in HTTP headers.
>>> import datetime
>>> httpdate(datetime.datetime(1970, 1, 1, 1, 1, 1))
'Thu, 01 Jan 1970 01:01:01 GMT'
"""
return date_obj.strftime("%a, %d %b %Y %H:%M:%S GMT")
def parsehttpdate(string_):
"""
Parses an HTTP date into a datetime object.
>>> parsehttpdate('Thu, 01 Jan 1970 01:01:01 GMT')
datetime.datetime(1970, 1, 1, 1, 1, 1)
"""
try:
t = time.strptime(string_, "%a, %d %b %Y %H:%M:%S %Z")
except ValueError:
return None
return datetime.datetime(*t[:6])
def htmlquote(text):
"""
Encodes `text` for raw use in HTML.
>>> htmlquote("<'&\\">")
'&lt;&#39;&amp;&quot;&gt;'
"""
text = text.replace("&", "&amp;") # Must be done first!
text = text.replace("<", "&lt;")
text = text.replace(">", "&gt;")
text = text.replace("'", "&#39;")
text = text.replace('"', "&quot;")
return text
def websafe(val):
"""
Converts `val` so that it's safe for use in UTF-8 HTML.
>>> websafe("<'&\\">")
'&lt;&#39;&amp;&quot;&gt;'
>>> websafe(None)
''
>>> websafe(u'\u203d')
'\\xe2\\x80\\xbd'
"""
if val is None:
return ''
if isinstance(val, unicode):
val = val.encode('utf-8')
val = str(val)
return htmlquote(val)
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@ -0,0 +1,153 @@
"""
Request Delegation
(from web.py)
"""
__all__ = ["handle", "nomethod", "autodelegate", "webpyfunc", "run"]
import sys, re, types, os.path, urllib
import http, wsgi, utils, webapi
import webapi as web
def handle(mapping, fvars=None):
"""
Call the appropriate function based on the url to function mapping in `mapping`.
If no module for the function is specified, look up the function in `fvars`. If
`fvars` is empty, using the caller's context.
`mapping` should be a tuple of paired regular expressions with function name
substitutions. `handle` will import modules as necessary.
"""
for url, ofno in utils.group(mapping, 2):
if isinstance(ofno, tuple):
ofn, fna = ofno[0], list(ofno[1:])
else:
ofn, fna = ofno, []
fn, result = utils.re_subm('^' + url + '$', ofn, web.ctx.path)
if result: # it's a match
if fn.split(' ', 1)[0] == "redirect":
url = fn.split(' ', 1)[1]
if web.ctx.method == "GET":
x = web.ctx.env.get('QUERY_STRING', '')
if x:
url += '?' + x
return http.redirect(url)
elif '.' in fn:
x = fn.split('.')
mod, cls = '.'.join(x[:-1]), x[-1]
mod = __import__(mod, globals(), locals(), [""])
cls = getattr(mod, cls)
else:
cls = fn
mod = fvars
if isinstance(mod, types.ModuleType):
mod = vars(mod)
try:
cls = mod[cls]
except KeyError:
return web.notfound()
meth = web.ctx.method
if meth == "HEAD":
if not hasattr(cls, meth):
meth = "GET"
if not hasattr(cls, meth):
return nomethod(cls)
tocall = getattr(cls(), meth)
args = list(result.groups())
for d in re.findall(r'\\(\d+)', ofn):
args.pop(int(d) - 1)
return tocall(*([x and urllib.unquote(x) for x in args] + fna))
return web.notfound()
def nomethod(cls):
"""Returns a `405 Method Not Allowed` error for `cls`."""
web.ctx.status = '405 Method Not Allowed'
web.header('Content-Type', 'text/html')
web.header('Allow', \
', '.join([method for method in \
['GET', 'HEAD', 'POST', 'PUT', 'DELETE'] \
if hasattr(cls, method)]))
# commented out for the same reason redirect is
# return output('method not allowed')
def autodelegate(prefix=''):
"""
Returns a method that takes one argument and calls the method named prefix+arg,
calling `notfound()` if there isn't one. Example:
urls = ('/prefs/(.*)', 'prefs')
class prefs:
GET = autodelegate('GET_')
def GET_password(self): pass
def GET_privacy(self): pass
`GET_password` would get called for `/prefs/password` while `GET_privacy` for
`GET_privacy` gets called for `/prefs/privacy`.
If a user visits `/prefs/password/change` then `GET_password(self, '/change')`
is called.
"""
def internal(self, arg):
if '/' in arg:
first, rest = arg.split('/', 1)
func = prefix + first
args = ['/' + rest]
else:
func = prefix + arg
args = []
if hasattr(self, func):
try:
return getattr(self, func)(*args)
except TypeError:
return web.notfound()
else:
return web.notfound()
return internal
def webpyfunc(inp, fvars, autoreload=False):
"""If `inp` is a url mapping, returns a function that calls handle."""
if not hasattr(inp, '__call__'):
if autoreload:
def modname():
"""find name of the module name from fvars."""
file, name = fvars['__file__'], fvars['__name__']
if name == '__main__':
# Since the __main__ module can't be reloaded, the module has
# to be imported using its file name.
name = os.path.splitext(os.path.basename(file))[0]
return name
mod = __import__(modname(), None, None, [""])
#@@probably should replace this with some inspect magic
name = utils.dictfind(fvars, inp)
func = lambda: handle(getattr(mod, name), mod)
else:
func = lambda: handle(inp, fvars)
else:
func = inp
return func
def run(inp, fvars, *middleware):
"""
Starts handling requests. If called in a CGI or FastCGI context, it will follow
that protocol. If called from the command line, it will start an HTTP
server on the port named in the first command line argument, or, if there
is no argument, on port 8080.
`input` is a callable, then it's called with no arguments.
Otherwise, it's a `mapping` object to be passed to `handle(...)`.
**Caveat:** So that `reloader` will work correctly, input has to be a variable,
it can't be a tuple passed in directly.
`middleware` is a list of WSGI middleware which is applied to the resulting WSGI
function.
"""
autoreload = http.reloader in middleware
return wsgi.runwsgi(webapi.wsgifunc(webpyfunc(inp, fvars, autoreload), *middleware))

View File

@ -0,0 +1,878 @@
"""
simple, elegant templating
(part of web.py)
"""
import re, glob, os, os.path
from types import FunctionType as function
from utils import storage, group, utf8
from net import websafe
# differences from python:
# - for: has an optional else: that gets called if the loop never runs
# differences to add:
# - you can use the expression inside if, while blocks
# - special for loop attributes, like django?
# - you can check to see if a variable is defined (perhaps w/ get func?)
# all these are probably good ideas for python...
# todo:
# inline tuple
# relax constraints on spacing
# continue, break, etc.
# tracebacks
global_globals = {'None':None, 'False':False, 'True': True}
MAX_ITERS = 100000
WHAT = 0
ARGS = 4
KWARGS = 6
NAME = 2
BODY = 4
CLAUSE = 2
ELIF = 6
ELSE = 8
IN = 6
NAME = 2
EXPR = 4
FILTER = 4
THING = 2
ATTR = 4
ITEM = 4
NEGATE = 4
X = 2
OP = 4
Y = 6
LINENO = -1
# http://docs.python.org/ref/identifiers.html
r_var = '[a-zA-Z_][a-zA-Z0-9_]*'
class ParseError(Exception): pass
class Parser:
def __init__(self, text, name=""):
self.t = text
self.p = 0
self._lock = [False]
self.name = name
def lock(self):
self._lock[-1] = True
def curline(self):
return self.t[:self.p].count('\n')+1
def csome(self):
return repr(self.t[self.p:self.p+5]+'...')
def Error(self, x, y=None):
if y is None: y = self.csome()
raise ParseError, "%s: expected %s, got %s (line %s)" % (self.name, x, y, self.curline())
def q(self, f):
def internal(*a, **kw):
checkp = self.p
self._lock.append(False)
try:
q = f(*a, **kw)
except ParseError:
if self._lock[-1]:
raise
self.p = checkp
self._lock.pop()
return False
self._lock.pop()
return q or True
return internal
def tokr(self, t):
text = self.c(len(t))
if text != t:
self.Error(repr(t), repr(text))
return t
def ltokr(self, *l):
for x in l:
o = self.tokq(x)
if o: return o
self.Error('one of '+repr(l))
def rer(self, r):
x = re.match(r, self.t[self.p:]) #@@re_compile
if not x:
self.Error('r'+repr(r))
return self.tokr(x.group())
def endr(self):
if self.p != len(self.t):
self.Error('EOF')
def c(self, n=1):
out = self.t[self.p:self.p+n]
if out == '' and n != 0:
self.Error('character', 'EOF')
self.p += n
return out
def lookbehind(self, t):
return self.t[self.p-len(t):self.p] == t
def __getattr__(self, a):
if a.endswith('q'):
return self.q(getattr(self, a[:-1]+'r'))
raise AttributeError, a
class TemplateParser(Parser):
def __init__(self, *a, **kw):
Parser.__init__(self, *a, **kw)
self.curws = ''
self.curind = ''
def o(self, *a):
return a+('lineno', self.curline())
def go(self):
# maybe try to do some traceback parsing/hacking
return self.gor()
def gor(self):
header = self.defwithq()
results = self.lines(start=True)
self.endr()
return header, results
def ws(self):
n = 0
while self.tokq(" "): n += 1
return " " * n
def defwithr(self):
self.tokr('$def with ')
self.lock()
self.tokr('(')
args = []
kw = []
x = self.req(r_var)
while x:
if self.tokq('='):
v = self.exprr()
kw.append((x, v))
else:
args.append(x)
x = self.tokq(', ') and self.req(r_var)
self.tokr(')\n')
return self.o('defwith', 'null', None, 'args', args, 'kwargs', kw)
def literalr(self):
o = (
self.req('"[^"]*"') or #@@ no support for escapes
self.req("'[^']*'")
)
if o is False:
o = self.req('\-?[0-9]+(\.[0-9]*)?')
if o is not False:
if '.' in o: o = float(o)
else: o = int(o)
if o is False: self.Error('literal')
return self.o('literal', 'thing', o)
def listr(self):
self.tokr('[')
self.lock()
x = []
if not self.tokq(']'):
while True:
t = self.exprr()
x.append(t)
if not self.tokq(', '): break
self.tokr(']')
return self.o('list', 'thing', x)
def dictr(self):
self.tokr('{')
self.lock()
x = {}
if not self.tokq('}'):
while True:
k = self.exprr()
self.tokr(': ')
v = self.exprr()
x[k] = v
if not self.tokq(', '): break
self.tokr('}')
return self.o('dict', 'thing', x)
def parenr(self):
self.tokr('(')
self.lock()
o = self.exprr() # todo: allow list
self.tokr(')')
return self.o('paren', 'thing', o)
def atomr(self):
"""returns var, literal, paren, dict, or list"""
o = (
self.varq() or
self.parenq() or
self.dictq() or
self.listq() or
self.literalq()
)
if o is False: self.Error('atom')
return o
def primaryr(self):
"""returns getattr, call, or getitem"""
n = self.atomr()
while 1:
if self.tokq('.'):
v = self.req(r_var)
if not v:
self.p -= 1 # get rid of the '.'
break
else:
n = self.o('getattr', 'thing', n, 'attr', v)
elif self.tokq('('):
args = []
kw = []
while 1:
# need to see if we're doing a keyword argument
checkp = self.p
k = self.req(r_var)
if k and self.tokq('='): # yup
v = self.exprr()
kw.append((k, v))
else:
self.p = checkp
x = self.exprq()
if x: # at least it's something
args.append(x)
else:
break
if not self.tokq(', '): break
self.tokr(')')
n = self.o('call', 'thing', n, 'args', args, 'kwargs', kw)
elif self.tokq('['):
v = self.exprr()
self.tokr(']')
n = self.o('getitem', 'thing', n, 'item', v)
else:
break
return n
def exprr(self):
negate = self.tokq('not ')
x = self.primaryr()
if self.tokq(' '):
operator = self.ltokr('not in', 'in', 'is not', 'is', '==', '!=', '>=', '<=', '<', '>', 'and', 'or', '*', '+', '-', '/', '%')
self.tokr(' ')
y = self.exprr()
x = self.o('test', 'x', x, 'op', operator, 'y', y)
return self.o('expr', 'thing', x, 'negate', negate)
def varr(self):
return self.o('var', 'name', self.rer(r_var))
def liner(self):
out = []
o = self.curws
while 1:
c = self.c()
self.lock()
if c == '\n':
self.p -= 1
break
if c == '$':
if self.lookbehind('\\$'):
o = o[:-1] + c
else:
filter = not bool(self.tokq(':'))
if self.tokq('{'):
out.append(o)
out.append(self.o('itpl', 'name', self.exprr(), 'filter', filter))
self.tokr('}')
o = ''
else:
g = self.primaryq()
if g:
out.append(o)
out.append(self.o('itpl', 'name', g, 'filter', filter))
o = ''
else:
o += c
else:
o += c
self.tokr('\n')
if not self.lookbehind('\\\n'):
o += '\n'
else:
o = o[:-1]
out.append(o)
return self.o('line', 'thing', out)
def varsetr(self):
self.tokr('$var ')
self.lock()
what = self.rer(r_var)
self.tokr(':')
body = self.lines()
return self.o('varset', 'name', what, 'body', body)
def ifr(self):
self.tokr("$if ")
self.lock()
expr = self.exprr()
self.tokr(":")
ifc = self.lines()
elifs = []
while self.tokq(self.curws + self.curind + '$elif '):
v = self.exprr()
self.tokr(':')
c = self.lines()
elifs.append(self.o('elif', 'clause', v, 'body', c))
if self.tokq(self.curws + self.curind + "$else:"):
elsec = self.lines()
else:
elsec = None
return self.o('if', 'clause', expr, 'then', ifc, 'elif', elifs, 'else', elsec)
def forr(self):
self.tokr("$for ")
self.lock()
v = self.setabler()
self.tokr(" in ")
g = self.exprr()
self.tokr(":")
l = self.lines()
if self.tokq(self.curws + self.curind + '$else:'):
elsec = self.lines()
else:
elsec = None
return self.o('for', 'name', v, 'body', l, 'in', g, 'else', elsec)
def whiler(self):
self.tokr('$while ')
self.lock()
v = self.exprr()
self.tokr(":")
l = self.lines()
if self.tokq(self.curws + self.curind + '$else:'):
elsec = self.lines()
else:
elsec = None
return self.o('while', 'clause', v, 'body', l, 'null', None, 'else', elsec)
def assignr(self):
self.tokr('$ ')
assign = self.rer(r_var) # NOTE: setable
self.tokr(' = ')
expr = self.exprr()
self.tokr('\n')
return self.o('assign', 'name', assign, 'expr', expr)
def commentr(self):
self.tokr('$#')
self.lock()
while self.c() != '\n': pass
return self.o('comment')
def setabler(self):
out = [self.varr()] #@@ not quite right
while self.tokq(', '):
out.append(self.varr())
return out
def lines(self, start=False):
"""
This function gets called from two places:
1. at the start, where it's matching the document itself
2. after any command, where it matches one line or an indented block
"""
o = []
if not start: # try to match just one line
singleline = self.tokq(' ') and self.lineq()
if singleline:
return [singleline]
else:
self.rer(' *') #@@slurp space?
self.tokr('\n')
oldind = self.curind
self.curind += ' '
while 1:
oldws = self.curws
t = self.tokq(oldws + self.curind)
if not t: break
self.curws += self.ws()
x = t and (
self.varsetq() or
self.ifq() or
self.forq() or
self.whileq() or
self.assignq() or
self.commentq() or
self.lineq())
self.curws = oldws
if not x:
break
elif x[WHAT] == 'comment':
pass
else:
o.append(x)
if not start: self.curind = oldind
return o
class Stowage(storage):
def __str__(self): return self.get('_str')
#@@ edits in place
def __add__(self, other):
if isinstance(other, (unicode, str)):
self._str += other
return self
else:
raise TypeError, 'cannot add'
def __radd__(self, other):
if isinstance(other, (unicode, str)):
self._str = other + self._str
return self
else:
raise TypeError, 'cannot add'
class WTF(AssertionError): pass
class SecurityError(Exception):
"""The template seems to be trying to do something naughty."""
pass
Required = object()
class Template:
globals = {}
content_types = {
'.html' : 'text/html; charset=utf-8',
'.txt' : 'text/plain',
}
def __init__(self, text, filter=None, filename=""):
self.filter = filter
self.filename = filename
# universal newlines:
text = text.replace('\r\n', '\n').replace('\r', '\n').expandtabs()
if not text.endswith('\n'): text += '\n'
header, tree = TemplateParser(text, filename).go()
self.tree = tree
if header:
self.h_defwith(header)
else:
self.args, self.kwargs = (), {}
def __call__(self, *a, **kw):
d = self.globals.copy()
d.update(self._parseargs(a, kw))
f = Fill(self.tree, d=d)
if self.filter: f.filter = self.filter
import webapi as web
if 'headers' in web.ctx and self.filename:
content_type = self.find_content_type()
if content_type:
web.header('Content-Type', content_type, unique=True)
return f.go()
def find_content_type(self):
for ext, content_type in self.content_types.iteritems():
if self.filename.endswith(ext):
return content_type
def _parseargs(self, inargs, inkwargs):
# difference from Python:
# no error on setting a keyword arg twice
d = {}
for arg in self.args:
d[arg] = Required
for kw, val in self.kwargs:
d[kw] = val
for n, val in enumerate(inargs):
if n < len(self.args):
d[self.args[n]] = val
elif n < len(self.args)+len(self.kwargs):
kw = self.kwargs[n - len(self.args)][0]
d[kw] = val
for kw, val in inkwargs.iteritems():
d[kw] = val
unset = []
for k, v in d.iteritems():
if v is Required:
unset.append(k)
if unset:
raise TypeError, 'values for %s are required' % unset
return d
def h_defwith(self, header):
assert header[WHAT] == 'defwith'
f = Fill(self.tree, d={})
self.args = header[ARGS]
self.kwargs = []
for var, valexpr in header[KWARGS]:
self.kwargs.append((var, f.h(valexpr)))
def __repr__(self):
return "<Template: %s>" % self.filename
class Handle:
def __init__(self, parsetree, **kw):
self._funccache = {}
self.parsetree = parsetree
for (k, v) in kw.iteritems(): setattr(self, k, v)
def h(self, item):
return getattr(self, 'h_' + item[WHAT])(item)
class Fill(Handle):
builtins = global_globals
def filter(self, text):
if text is None: return ''
else: return utf8(text)
# often replaced with stuff like net.websafe
def h_literal(self, i):
item = i[THING]
if isinstance(item, (unicode, str)) and item[0] in ['"', "'"]:
item = item[1:-1]
elif isinstance(item, (float, int)):
pass
return item
def h_list(self, i):
x = i[THING]
out = []
for item in x:
out.append(self.h(item))
return out
def h_dict(self, i):
x = i[THING]
out = {}
for k, v in x.iteritems():
out[self.h(k)] = self.h(v)
return out
def h_paren(self, i):
item = i[THING]
if isinstance(item, list):
raise NotImplementedError, 'tuples'
return self.h(item)
def h_getattr(self, i):
thing, attr = i[THING], i[ATTR]
thing = self.h(thing)
if attr.startswith('_') or attr.startswith('func_') or attr.startswith('im_'):
raise SecurityError, 'tried to get ' + attr
try:
if thing in self.builtins:
raise SecurityError, 'tried to getattr on ' + repr(thing)
except TypeError:
pass # raised when testing an unhashable object
try:
return getattr(thing, attr)
except AttributeError:
if isinstance(thing, list) and attr == 'join':
return lambda s: s.join(thing)
else:
raise
def h_call(self, i):
call = self.h(i[THING])
args = [self.h(x) for x in i[ARGS]]
kw = dict([(x, self.h(y)) for (x, y) in i[KWARGS]])
return call(*args, **kw)
def h_getitem(self, i):
thing, item = i[THING], i[ITEM]
thing = self.h(thing)
item = self.h(item)
return thing[item]
def h_expr(self, i):
item = self.h(i[THING])
if i[NEGATE]:
item = not item
return item
def h_test(self, item):
ox, op, oy = item[X], item[OP], item[Y]
# for short-circuiting to work, we can't eval these here
e = self.h
if op == 'is':
return e(ox) is e(oy)
elif op == 'is not':
return e(ox) is not e(oy)
elif op == 'in':
return e(ox) in e(oy)
elif op == 'not in':
return e(ox) not in e(oy)
elif op == '==':
return e(ox) == e(oy)
elif op == '!=':
return e(ox) != e(oy)
elif op == '>':
return e(ox) > e(oy)
elif op == '<':
return e(ox) < e(oy)
elif op == '<=':
return e(ox) <= e(oy)
elif op == '>=':
return e(ox) >= e(oy)
elif op == 'and':
return e(ox) and e(oy)
elif op == 'or':
return e(ox) or e(oy)
elif op == '+':
return e(ox) + e(oy)
elif op == '-':
return e(ox) - e(oy)
elif op == '*':
return e(ox) * e(oy)
elif op == '/':
return e(ox) / e(oy)
elif op == '%':
return e(ox) % e(oy)
else:
raise WTF, 'op ' + op
def h_var(self, i):
v = i[NAME]
if v in self.d:
return self.d[v]
elif v in self.builtins:
return self.builtins[v]
elif v == 'self':
return self.output
else:
raise NameError, 'could not find %s (line %s)' % (repr(i[NAME]), i[LINENO])
def h_line(self, i):
out = []
for x in i[THING]:
#@@ what if x is unicode
if isinstance(x, str):
out.append(x)
elif x[WHAT] == 'itpl':
o = self.h(x[NAME])
if x[FILTER]:
o = self.filter(o)
else:
o = (o is not None and utf8(o)) or ""
out.append(o)
else:
raise WTF, x
return ''.join(out)
def h_varset(self, i):
self.output[i[NAME]] = ''.join(self.h_lines(i[BODY]))
return ''
def h_if(self, i):
expr = self.h(i[CLAUSE])
if expr:
do = i[BODY]
else:
for e in i[ELIF]:
expr = self.h(e[CLAUSE])
if expr:
do = e[BODY]
break
else:
do = i[ELSE]
return ''.join(self.h_lines(do))
def h_for(self, i):
out = []
assert i[IN][WHAT] == 'expr'
invar = self.h(i[IN])
forvar = i[NAME]
if invar:
for nv in invar:
if len(forvar) == 1:
fv = forvar[0]
assert fv[WHAT] == 'var'
self.d[fv[NAME]] = nv # same (lack of) scoping as Python
else:
for x, y in zip(forvar, nv):
assert x[WHAT] == 'var'
self.d[x[NAME]] = y
out.extend(self.h_lines(i[BODY]))
else:
if i[ELSE]:
out.extend(self.h_lines(i[ELSE]))
return ''.join(out)
def h_while(self, i):
out = []
expr = self.h(i[CLAUSE])
if not expr:
return ''.join(self.h_lines(i[ELSE]))
c = 0
while expr:
c += 1
if c >= MAX_ITERS:
raise RuntimeError, 'too many while-loop iterations (line %s)' % i[LINENO]
out.extend(self.h_lines(i[BODY]))
expr = self.h(i[CLAUSE])
return ''.join(out)
def h_assign(self, i):
self.d[i[NAME]] = self.h(i[EXPR])
return ''
def h_comment(self, i): pass
def h_lines(self, lines):
if lines is None: return []
return map(self.h, lines)
def go(self):
self.output = Stowage()
self.output._str = ''.join(map(self.h, self.parsetree))
if self.output.keys() == ['_str']:
self.output = self.output['_str']
return self.output
class render:
def __init__(self, loc='templates/', cache=True):
self.loc = loc
if cache:
self.cache = {}
else:
self.cache = False
def _do(self, name, filter=None):
if self.cache is False or name not in self.cache:
tmplpath = os.path.join(self.loc, name)
p = [f for f in glob.glob(tmplpath + '.*') if not f.endswith('~')] # skip backup files
if not p and os.path.isdir(tmplpath):
return render(tmplpath, cache=self.cache)
elif not p:
raise AttributeError, 'no template named ' + name
p = p[0]
c = Template(open(p).read(), filename=p)
if self.cache is not False: self.cache[name] = (p, c)
if self.cache is not False: p, c = self.cache[name]
if p.endswith('.html') or p.endswith('.xml'):
if not filter: c.filter = websafe
return c
def __getattr__(self, p):
return self._do(p)
def frender(fn, *a, **kw):
return Template(open(fn).read(), *a, **kw)
def test():
import sys
verbose = '-v' in sys.argv
def assertEqual(a, b):
if a == b:
if verbose:
sys.stderr.write('.')
sys.stderr.flush()
else:
assert a == b, "\nexpected: %s\ngot: %s" % (repr(b), repr(a))
from utils import storage, group
class t:
def __init__(self, text):
self.text = text
def __call__(self, *a, **kw):
return TestResult(self.text, Template(self.text)(*a, **kw))
class TestResult:
def __init__(self, source, value):
self.source = source
self.value = value
def __eq__(self, other):
if self.value == other:
if verbose:
sys.stderr.write('.')
else:
print >> sys.stderr, 'FAIL:', repr(self.source), 'expected', repr(other), ', got', repr(self.value)
sys.stderr.flush()
t('1')() == '1\n'
t('$def with ()\n1')() == '1\n'
t('$def with (a)\n$a')(1) == '1\n'
t('$def with (a=0)\n$a')(1) == '1\n'
t('$def with (a=0)\n$a')(a=1) == '1\n'
t('$if 1: 1')() == '1\n'
t('$if 1:\n 1')() == '1\n'
t('$if 0: 0\n$elif 1: 1')() == '1\n'
t('$if 0: 0\n$elif None: 0\n$else: 1')() == '1\n'
t('$if (0 < 1) and (1 < 2): 1')() == '1\n'
t('$for x in [1, 2, 3]: $x')() == '1\n2\n3\n'
t('$for x in []: 0\n$else: 1')() == '1\n'
t('$def with (a)\n$while a and a.pop(): 1')([1, 2, 3]) == '1\n1\n1\n'
t('$while 0: 0\n$else: 1')() == '1\n'
t('$ a = 1\n$a')() == '1\n'
t('$# 0')() == ''
t('$def with (d)\n$for k, v in d.iteritems(): $k')({1: 1}) == '1\n'
t('$def with (a)\n$(a)')(1) == '1\n'
t('$def with (a)\n$a')(1) == '1\n'
t('$def with (a)\n$a.b')(storage(b=1)) == '1\n'
t('$def with (a)\n$a[0]')([1]) == '1\n'
t('${0 or 1}')() == '1\n'
t('$ a = [1]\n$a[0]')() == '1\n'
t('$ a = {1: 1}\n$a.keys()[0]')() == '1\n'
t('$ a = []\n$if not a: 1')() == '1\n'
t('$ a = {}\n$if not a: 1')() == '1\n'
t('$ a = -1\n$a')() == '-1\n'
t('$ a = "1"\n$a')() == '1\n'
t('$if 1 is 1: 1')() == '1\n'
t('$if not 0: 1')() == '1\n'
t('$if 1:\n $if 1: 1')() == '1\n'
t('$ a = 1\n$a')() == '1\n'
t('$ a = 1.\n$a')() == '1.0\n'
t('$({1: 1}.keys()[0])')() == '1\n'
t('$for x in [1, 2, 3]:\n\t$x')() == ' 1\n 2\n 3\n'
t('$def with (a)\n$:a')(1) == '1\n'
t('$def with (a)\n$a')(u'\u203d') == '\xe2\x80\xbd\n'
t(u'$def with (f)\n$:f("x")')(lambda x: x) == 'x\n'
j = Template("$var foo: bar")()
assertEqual(str(j), '')
assertEqual(j.foo, 'bar\n')
if verbose: sys.stderr.write('\n')
if __name__ == "__main__":
test()

View File

@ -0,0 +1,796 @@
"""
General Utilities
(part of web.py)
"""
__all__ = [
"Storage", "storage", "storify",
"iters",
"rstrips", "lstrips", "strips", "utf8",
"TimeoutError", "timelimit",
"Memoize", "memoize",
"re_compile", "re_subm",
"group",
"IterBetter", "iterbetter",
"dictreverse", "dictfind", "dictfindall", "dictincr", "dictadd",
"listget", "intget", "datestr",
"numify", "denumify", "dateify",
"CaptureStdout", "capturestdout", "Profile", "profile",
"tryall",
"ThreadedDict",
"autoassign",
"to36",
"safemarkdown"
]
import re, sys, time, threading
try: import datetime
except ImportError: pass
class Storage(dict):
"""
A Storage object is like a dictionary except `obj.foo` can be used
in addition to `obj['foo']`.
>>> o = storage(a=1)
>>> o.a
1
>>> o['a']
1
>>> o.a = 2
>>> o['a']
2
>>> del o.a
>>> o.a
Traceback (most recent call last):
...
AttributeError: 'a'
"""
def __getattr__(self, key):
try:
return self[key]
except KeyError, k:
raise AttributeError, k
def __setattr__(self, key, value):
self[key] = value
def __delattr__(self, key):
try:
del self[key]
except KeyError, k:
raise AttributeError, k
def __repr__(self):
return '<Storage ' + dict.__repr__(self) + '>'
storage = Storage
def storify(mapping, *requireds, **defaults):
"""
Creates a `storage` object from dictionary `mapping`, raising `KeyError` if
d doesn't have all of the keys in `requireds` and using the default
values for keys found in `defaults`.
For example, `storify({'a':1, 'c':3}, b=2, c=0)` will return the equivalent of
`storage({'a':1, 'b':2, 'c':3})`.
If a `storify` value is a list (e.g. multiple values in a form submission),
`storify` returns the last element of the list, unless the key appears in
`defaults` as a list. Thus:
>>> storify({'a':[1, 2]}).a
2
>>> storify({'a':[1, 2]}, a=[]).a
[1, 2]
>>> storify({'a':1}, a=[]).a
[1]
>>> storify({}, a=[]).a
[]
Similarly, if the value has a `value` attribute, `storify will return _its_
value, unless the key appears in `defaults` as a dictionary.
>>> storify({'a':storage(value=1)}).a
1
>>> storify({'a':storage(value=1)}, a={}).a
<Storage {'value': 1}>
>>> storify({}, a={}).a
{}
"""
def getvalue(x):
if hasattr(x, 'value'):
return x.value
else:
return x
stor = Storage()
for key in requireds + tuple(mapping.keys()):
value = mapping[key]
if isinstance(value, list):
if isinstance(defaults.get(key), list):
value = [getvalue(x) for x in value]
else:
value = value[-1]
if not isinstance(defaults.get(key), dict):
value = getvalue(value)
if isinstance(defaults.get(key), list) and not isinstance(value, list):
value = [value]
setattr(stor, key, value)
for (key, value) in defaults.iteritems():
result = value
if hasattr(stor, key):
result = stor[key]
if value == () and not isinstance(result, tuple):
result = (result,)
setattr(stor, key, result)
return stor
iters = [list, tuple]
import __builtin__
if hasattr(__builtin__, 'set'):
iters.append(set)
try:
from sets import Set
iters.append(Set)
except ImportError:
pass
class _hack(tuple): pass
iters = _hack(iters)
iters.__doc__ = """
A list of iterable items (like lists, but not strings). Includes whichever
of lists, tuples, sets, and Sets are available in this version of Python.
"""
def _strips(direction, text, remove):
if direction == 'l':
if text.startswith(remove):
return text[len(remove):]
elif direction == 'r':
if text.endswith(remove):
return text[:-len(remove)]
else:
raise ValueError, "Direction needs to be r or l."
return text
def rstrips(text, remove):
"""
removes the string `remove` from the right of `text`
>>> rstrips("foobar", "bar")
'foo'
"""
return _strips('r', text, remove)
def lstrips(text, remove):
"""
removes the string `remove` from the left of `text`
>>> lstrips("foobar", "foo")
'bar'
"""
return _strips('l', text, remove)
def strips(text, remove):
"""removes the string `remove` from the both sides of `text`
>>> strips("foobarfoo", "foo")
'bar'
"""
return rstrips(lstrips(text, remove), remove)
def utf8(text):
"""Encodes text in utf-8.
>> utf8(u'\u1234') # doctest doesn't seem to like utf-8
'\xe1\x88\xb4'
>>> utf8('hello')
'hello'
>>> utf8(42)
'42'
"""
if isinstance(text, unicode):
return text.encode('utf-8')
elif isinstance(text, str):
return text
else:
return str(text)
class TimeoutError(Exception): pass
def timelimit(timeout):
"""
A decorator to limit a function to `timeout` seconds, raising `TimeoutError`
if it takes longer.
>>> import time
>>> def meaningoflife():
... time.sleep(.2)
... return 42
>>>
>>> timelimit(.1)(meaningoflife)()
Traceback (most recent call last):
...
TimeoutError: took too long
>>> timelimit(1)(meaningoflife)()
42
_Caveat:_ The function isn't stopped after `timeout` seconds but continues
executing in a separate thread. (There seems to be no way to kill a thread.)
inspired by <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473878>
"""
def _1(function):
def _2(*args, **kw):
class Dispatch(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.result = None
self.error = None
self.setDaemon(True)
self.start()
def run(self):
try:
self.result = function(*args, **kw)
except:
self.error = sys.exc_info()
c = Dispatch()
c.join(timeout)
if c.isAlive():
raise TimeoutError, 'took too long'
if c.error:
raise c.error[0], c.error[1]
return c.result
return _2
return _1
class Memoize:
"""
'Memoizes' a function, caching its return values for each input.
>>> import time
>>> def meaningoflife():
... time.sleep(.2)
... return 42
>>> fastlife = memoize(meaningoflife)
>>> meaningoflife()
42
>>> timelimit(.1)(meaningoflife)()
Traceback (most recent call last):
...
TimeoutError: took too long
>>> fastlife()
42
>>> timelimit(.1)(fastlife)()
42
"""
def __init__(self, func):
self.func = func
self.cache = {}
def __call__(self, *args, **keywords):
key = (args, tuple(keywords.items()))
if key not in self.cache:
self.cache[key] = self.func(*args, **keywords)
return self.cache[key]
memoize = Memoize
re_compile = memoize(re.compile) #@@ threadsafe?
re_compile.__doc__ = """
A memoized version of re.compile.
"""
class _re_subm_proxy:
def __init__(self):
self.match = None
def __call__(self, match):
self.match = match
return ''
def re_subm(pat, repl, string):
"""
Like re.sub, but returns the replacement _and_ the match object.
>>> t, m = re_subm('g(oo+)fball', r'f\\1lish', 'goooooofball')
>>> t
'foooooolish'
>>> m.groups()
('oooooo',)
"""
compiled_pat = re_compile(pat)
proxy = _re_subm_proxy()
compiled_pat.sub(proxy.__call__, string)
return compiled_pat.sub(repl, string), proxy.match
def group(seq, size):
"""
Returns an iterator over a series of lists of length size from iterable.
>>> list(group([1,2,3,4], 2))
[[1, 2], [3, 4]]
"""
if not hasattr(seq, 'next'):
seq = iter(seq)
while True:
yield [seq.next() for i in xrange(size)]
class IterBetter:
"""
Returns an object that can be used as an iterator
but can also be used via __getitem__ (although it
cannot go backwards -- that is, you cannot request
`iterbetter[0]` after requesting `iterbetter[1]`).
>>> import itertools
>>> c = iterbetter(itertools.count())
>>> c[1]
1
>>> c[5]
5
>>> c[3]
Traceback (most recent call last):
...
IndexError: already passed 3
"""
def __init__(self, iterator):
self.i, self.c = iterator, 0
def __iter__(self):
while 1:
yield self.i.next()
self.c += 1
def __getitem__(self, i):
#todo: slices
if i < self.c:
raise IndexError, "already passed "+str(i)
try:
while i > self.c:
self.i.next()
self.c += 1
# now self.c == i
self.c += 1
return self.i.next()
except StopIteration:
raise IndexError, str(i)
iterbetter = IterBetter
def dictreverse(mapping):
"""
>>> dictreverse({1: 2, 3: 4})
{2: 1, 4: 3}
"""
return dict([(value, key) for (key, value) in mapping.iteritems()])
def dictfind(dictionary, element):
"""
Returns a key whose value in `dictionary` is `element`
or, if none exists, None.
>>> d = {1:2, 3:4}
>>> dictfind(d, 4)
3
>>> dictfind(d, 5)
"""
for (key, value) in dictionary.iteritems():
if element is value:
return key
def dictfindall(dictionary, element):
"""
Returns the keys whose values in `dictionary` are `element`
or, if none exists, [].
>>> d = {1:4, 3:4}
>>> dictfindall(d, 4)
[1, 3]
>>> dictfindall(d, 5)
[]
"""
res = []
for (key, value) in dictionary.iteritems():
if element is value:
res.append(key)
return res
def dictincr(dictionary, element):
"""
Increments `element` in `dictionary`,
setting it to one if it doesn't exist.
>>> d = {1:2, 3:4}
>>> dictincr(d, 1)
3
>>> d[1]
3
>>> dictincr(d, 5)
1
>>> d[5]
1
"""
dictionary.setdefault(element, 0)
dictionary[element] += 1
return dictionary[element]
def dictadd(*dicts):
"""
Returns a dictionary consisting of the keys in the argument dictionaries.
If they share a key, the value from the last argument is used.
>>> dictadd({1: 0, 2: 0}, {2: 1, 3: 1})
{1: 0, 2: 1, 3: 1}
"""
result = {}
for dct in dicts:
result.update(dct)
return result
def listget(lst, ind, default=None):
"""
Returns `lst[ind]` if it exists, `default` otherwise.
>>> listget(['a'], 0)
'a'
>>> listget(['a'], 1)
>>> listget(['a'], 1, 'b')
'b'
"""
if len(lst)-1 < ind:
return default
return lst[ind]
def intget(integer, default=None):
"""
Returns `integer` as an int or `default` if it can't.
>>> intget('3')
3
>>> intget('3a')
>>> intget('3a', 0)
0
"""
try:
return int(integer)
except (TypeError, ValueError):
return default
def datestr(then, now=None):
"""
Converts a (UTC) datetime object to a nice string representation.
>>> from datetime import datetime, timedelta
>>> d = datetime(1970, 5, 1)
>>> datestr(d, now=d)
'0 microseconds ago'
>>> for t, v in {
... timedelta(microseconds=1): '1 microsecond ago',
... timedelta(microseconds=2): '2 microseconds ago',
... -timedelta(microseconds=1): '1 microsecond from now',
... -timedelta(microseconds=2): '2 microseconds from now',
... timedelta(microseconds=2000): '2 milliseconds ago',
... timedelta(seconds=2): '2 seconds ago',
... timedelta(seconds=2*60): '2 minutes ago',
... timedelta(seconds=2*60*60): '2 hours ago',
... timedelta(days=2): '2 days ago',
... }.iteritems():
... assert datestr(d, now=d+t) == v
>>> datestr(datetime(1970, 1, 1), now=d)
'January 1'
>>> datestr(datetime(1969, 1, 1), now=d)
'January 1, 1969'
>>> datestr(datetime(1970, 6, 1), now=d)
'June 1, 1970'
"""
def agohence(n, what, divisor=None):
if divisor: n = n // divisor
out = str(abs(n)) + ' ' + what # '2 day'
if abs(n) != 1: out += 's' # '2 days'
out += ' ' # '2 days '
if n < 0:
out += 'from now'
else:
out += 'ago'
return out # '2 days ago'
oneday = 24 * 60 * 60
if not now: now = datetime.datetime.utcnow()
if type(now).__name__ == "DateTime":
now = datetime.datetime.fromtimestamp(now)
if type(then).__name__ == "DateTime":
then = datetime.datetime.fromtimestamp(then)
delta = now - then
deltaseconds = int(delta.days * oneday + delta.seconds + delta.microseconds * 1e-06)
deltadays = abs(deltaseconds) // oneday
if deltaseconds < 0: deltadays *= -1 # fix for oddity of floor
if deltadays:
if abs(deltadays) < 4:
return agohence(deltadays, 'day')
out = then.strftime('%B %e') # e.g. 'June 13'
if then.year != now.year or deltadays < 0:
out += ', %s' % then.year
return out
if int(deltaseconds):
if abs(deltaseconds) > (60 * 60):
return agohence(deltaseconds, 'hour', 60 * 60)
elif abs(deltaseconds) > 60:
return agohence(deltaseconds, 'minute', 60)
else:
return agohence(deltaseconds, 'second')
deltamicroseconds = delta.microseconds
if delta.days: deltamicroseconds = int(delta.microseconds - 1e6) # datetime oddity
if abs(deltamicroseconds) > 1000:
return agohence(deltamicroseconds, 'millisecond', 1000)
return agohence(deltamicroseconds, 'microsecond')
def numify(string):
"""
Removes all non-digit characters from `string`.
>>> numify('800-555-1212')
'8005551212'
>>> numify('800.555.1212')
'8005551212'
"""
return ''.join([c for c in str(string) if c.isdigit()])
def denumify(string, pattern):
"""
Formats `string` according to `pattern`, where the letter X gets replaced
by characters from `string`.
>>> denumify("8005551212", "(XXX) XXX-XXXX")
'(800) 555-1212'
"""
out = []
for c in pattern:
if c == "X":
out.append(string[0])
string = string[1:]
else:
out.append(c)
return ''.join(out)
def dateify(datestring):
"""
Formats a numified `datestring` properly.
"""
return denumify(datestring, "XXXX-XX-XX XX:XX:XX")
class CaptureStdout:
"""
Captures everything `func` prints to stdout and returns it instead.
>>> def idiot():
... print "foo"
>>> capturestdout(idiot)()
'foo\\n'
**WARNING:** Not threadsafe!
"""
def __init__(self, func):
self.func = func
def __call__(self, *args, **keywords):
from cStringIO import StringIO
# Not threadsafe!
out = StringIO()
oldstdout = sys.stdout
sys.stdout = out
try:
self.func(*args, **keywords)
finally:
sys.stdout = oldstdout
return out.getvalue()
capturestdout = CaptureStdout
class Profile:
"""
Profiles `func` and returns a tuple containing its output
and a string with human-readable profiling information.
>>> import time
>>> out, inf = profile(time.sleep)(.001)
>>> out
>>> inf[:10].strip()
'took 0.0'
"""
def __init__(self, func):
self.func = func
def __call__(self, *args): ##, **kw): kw unused
import hotshot, hotshot.stats, tempfile ##, time already imported
temp = tempfile.NamedTemporaryFile()
prof = hotshot.Profile(temp.name)
stime = time.time()
result = prof.runcall(self.func, *args)
stime = time.time() - stime
prof.close()
stats = hotshot.stats.load(temp.name)
stats.strip_dirs()
stats.sort_stats('time', 'calls')
x = '\n\ntook '+ str(stime) + ' seconds\n'
x += capturestdout(stats.print_stats)(40)
x += capturestdout(stats.print_callers)()
return result, x
profile = Profile
import traceback
# hack for compatibility with Python 2.3:
if not hasattr(traceback, 'format_exc'):
from cStringIO import StringIO
def format_exc(limit=None):
strbuf = StringIO()
traceback.print_exc(limit, strbuf)
return strbuf.getvalue()
traceback.format_exc = format_exc
def tryall(context, prefix=None):
"""
Tries a series of functions and prints their results.
`context` is a dictionary mapping names to values;
the value will only be tried if it's callable.
>>> tryall(dict(j=lambda: True))
j: True
----------------------------------------
results:
True: 1
For example, you might have a file `test/stuff.py`
with a series of functions testing various things in it.
At the bottom, have a line:
if __name__ == "__main__": tryall(globals())
Then you can run `python test/stuff.py` and get the results of
all the tests.
"""
context = context.copy() # vars() would update
results = {}
for (key, value) in context.iteritems():
if not hasattr(value, '__call__'):
continue
if prefix and not key.startswith(prefix):
continue
print key + ':',
try:
r = value()
dictincr(results, r)
print r
except:
print 'ERROR'
dictincr(results, 'ERROR')
print ' ' + '\n '.join(traceback.format_exc().split('\n'))
print '-'*40
print 'results:'
for (key, value) in results.iteritems():
print ' '*2, str(key)+':', value
class ThreadedDict:
"""
Takes a dictionary that maps threads to objects.
When a thread tries to get or set an attribute or item
of the threadeddict, it passes it on to the object
for that thread in dictionary.
"""
def __init__(self, dictionary):
self.__dict__['_ThreadedDict__d'] = dictionary
def __getattr__(self, attr):
return getattr(self.__d[threading.currentThread()], attr)
def __getitem__(self, item):
return self.__d[threading.currentThread()][item]
def __setattr__(self, attr, value):
if attr == '__doc__':
self.__dict__[attr] = value
else:
return setattr(self.__d[threading.currentThread()], attr, value)
def __delattr__(self, item):
try:
del self.__d[threading.currentThread()][item]
except KeyError, k:
raise AttributeError, k
def __delitem__(self, item):
del self.__d[threading.currentThread()][item]
def __setitem__(self, item, value):
self.__d[threading.currentThread()][item] = value
def __hash__(self):
return hash(self.__d[threading.currentThread()])
threadeddict = ThreadedDict
def autoassign(self, locals):
"""
Automatically assigns local variables to `self`.
>>> self = storage()
>>> autoassign(self, dict(a=1, b=2))
>>> self
<Storage {'a': 1, 'b': 2}>
Generally used in `__init__` methods, as in:
def __init__(self, foo, bar, baz=1): autoassign(self, locals())
"""
for (key, value) in locals.iteritems():
if key == 'self':
continue
setattr(self, key, value)
def to36(q):
"""
Converts an integer to base 36 (a useful scheme for human-sayable IDs).
>>> to36(35)
'z'
>>> to36(119292)
'2k1o'
>>> int(to36(939387374), 36)
939387374
>>> to36(0)
'0'
>>> to36(-393)
Traceback (most recent call last):
...
ValueError: must supply a positive integer
"""
if q < 0: raise ValueError, "must supply a positive integer"
letters = "0123456789abcdefghijklmnopqrstuvwxyz"
converted = []
while q != 0:
q, r = divmod(q, 36)
converted.insert(0, letters[r])
return "".join(converted) or '0'
r_url = re_compile('(?<!\()(http://(\S+))')
def safemarkdown(text):
"""
Converts text to HTML following the rules of Markdown, but blocking any
outside HTML input, so that only the things supported by Markdown
can be used. Also converts raw URLs to links.
(requires [markdown.py](http://webpy.org/markdown.py))
"""
from markdown import markdown
if text:
text = text.replace('<', '&lt;')
# TODO: automatically get page title?
text = r_url.sub(r'<\1>', text)
text = markdown(text)
return text
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@ -0,0 +1,369 @@
"""
Web API (wrapper around WSGI)
(from web.py)
"""
__all__ = [
"config",
"badrequest", "notfound", "gone", "internalerror",
"header", "output", "flush", "debug",
"input", "data",
"setcookie", "cookies",
"ctx",
"loadhooks", "load", "unloadhooks", "unload", "_loadhooks",
"wsgifunc"
]
import sys, os, cgi, threading, Cookie, pprint, traceback
try: import itertools
except ImportError: pass
from utils import storage, storify, threadeddict, dictadd, intget, lstrips, utf8
config = storage()
config.__doc__ = """
A configuration object for various aspects of web.py.
`db_parameters`
: A dictionary containing the parameters to be passed to `connect`
when `load()` is called.
`db_printing`
: Set to `True` if you would like SQL queries and timings to be
printed to the debug output.
"""
def badrequest():
"""Return a `400 Bad Request` error."""
ctx.status = '400 Bad Request'
header('Content-Type', 'text/html')
return output('bad request')
def notfound():
"""Returns a `404 Not Found` error."""
ctx.status = '404 Not Found'
header('Content-Type', 'text/html')
return output('not found')
def gone():
"""Returns a `410 Gone` error."""
ctx.status = '410 Gone'
header('Content-Type', 'text/html')
return output("gone")
def internalerror():
"""Returns a `500 Internal Server` error."""
ctx.status = "500 Internal Server Error"
ctx.headers = [('Content-Type', 'text/html')]
ctx.output = "internal server error"
def header(hdr, value, unique=False):
"""
Adds the header `hdr: value` with the response.
If `unique` is True and a header with that name already exists,
it doesn't add a new one.
"""
hdr, value = utf8(hdr), utf8(value)
# protection against HTTP response splitting attack
if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value:
raise ValueError, 'invalid characters in header'
if unique is True:
for h, v in ctx.headers:
if h.lower() == hdr.lower(): return
ctx.headers.append((hdr, value))
def output(string_):
"""Appends `string_` to the response."""
if isinstance(string_, unicode): string_ = string_.encode('utf8')
if ctx.get('flush'):
ctx._write(string_)
else:
ctx.output += str(string_)
def flush():
ctx.flush = True
return flush
def input(*requireds, **defaults):
"""
Returns a `storage` object with the GET and POST arguments.
See `storify` for how `requireds` and `defaults` work.
"""
from cStringIO import StringIO
def dictify(fs): return dict([(k, fs[k]) for k in fs.keys()])
_method = defaults.pop('_method', 'both')
e = ctx.env.copy()
a = b = {}
if _method.lower() in ['both', 'post']:
if e['REQUEST_METHOD'] == 'POST':
a = cgi.FieldStorage(fp = StringIO(data()), environ=e,
keep_blank_values=1)
a = dictify(a)
if _method.lower() in ['both', 'get']:
e['REQUEST_METHOD'] = 'GET'
b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
out = dictadd(b, a)
try:
return storify(out, *requireds, **defaults)
except KeyError:
badrequest()
raise StopIteration
def data():
"""Returns the data sent with the request."""
if 'data' not in ctx:
cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
ctx.data = ctx.env['wsgi.input'].read(cl)
return ctx.data
def setcookie(name, value, expires="", domain=None):
"""Sets a cookie."""
if expires < 0:
expires = -1000000000
kargs = {'expires': expires, 'path':'/'}
if domain:
kargs['domain'] = domain
# @@ should we limit cookies to a different path?
cookie = Cookie.SimpleCookie()
cookie[name] = value
for key, val in kargs.iteritems():
cookie[name][key] = val
header('Set-Cookie', cookie.items()[0][1].OutputString())
def cookies(*requireds, **defaults):
"""
Returns a `storage` object with all the cookies in it.
See `storify` for how `requireds` and `defaults` work.
"""
cookie = Cookie.SimpleCookie()
cookie.load(ctx.env.get('HTTP_COOKIE', ''))
try:
return storify(cookie, *requireds, **defaults)
except KeyError:
badrequest()
raise StopIteration
def debug(*args):
"""
Prints a prettyprinted version of `args` to stderr.
"""
try:
out = ctx.environ['wsgi.errors']
except:
out = sys.stderr
for arg in args:
print >> out, pprint.pformat(arg)
return ''
def _debugwrite(x):
try:
out = ctx.environ['wsgi.errors']
except:
out = sys.stderr
out.write(x)
debug.write = _debugwrite
class _outputter:
"""Wraps `sys.stdout` so that print statements go into the response."""
def __init__(self, file): self.file = file
def write(self, string_):
if hasattr(ctx, 'output'):
return output(string_)
else:
self.file.write(string_)
def __getattr__(self, attr): return getattr(self.file, attr)
def __getitem__(self, item): return self.file[item]
def _capturedstdout():
sysstd = sys.stdout
while hasattr(sysstd, 'file'):
if isinstance(sys.stdout, _outputter): return True
sysstd = sysstd.file
if isinstance(sys.stdout, _outputter): return True
return False
if not _capturedstdout():
sys.stdout = _outputter(sys.stdout)
_context = {threading.currentThread(): storage()}
ctx = context = threadeddict(_context)
ctx.__doc__ = """
A `storage` object containing various information about the request:
`environ` (aka `env`)
: A dictionary containing the standard WSGI environment variables.
`host`
: The domain (`Host` header) requested by the user.
`home`
: The base path for the application.
`ip`
: The IP address of the requester.
`method`
: The HTTP method used.
`path`
: The path request.
`query`
: If there are no query arguments, the empty string. Otherwise, a `?` followed
by the query string.
`fullpath`
: The full path requested, including query arguments (`== path + query`).
### Response Data
`status` (default: "200 OK")
: The status code to be used in the response.
`headers`
: A list of 2-tuples to be used in the response.
`output`
: A string to be used as the response.
"""
loadhooks = {}
_loadhooks = {}
def load():
"""
Loads a new context for the thread.
You can ask for a function to be run at loadtime by
adding it to the dictionary `loadhooks`.
"""
_context[threading.currentThread()] = storage()
ctx.status = '200 OK'
ctx.headers = []
if config.get('db_parameters'):
import db
db.connect(**config.db_parameters)
for x in loadhooks.values(): x()
def _load(env):
load()
ctx.output = ''
ctx.environ = ctx.env = env
ctx.host = env.get('HTTP_HOST')
ctx.homedomain = 'http://' + env.get('HTTP_HOST', '[unknown]')
ctx.homepath = os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))
ctx.home = ctx.homedomain + ctx.homepath
ctx.ip = env.get('REMOTE_ADDR')
ctx.method = env.get('REQUEST_METHOD')
ctx.path = env.get('PATH_INFO')
# http://trac.lighttpd.net/trac/ticket/406 requires:
if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'):
ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0],
os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', '')))
if env.get('QUERY_STRING'):
ctx.query = '?' + env.get('QUERY_STRING', '')
else:
ctx.query = ''
ctx.fullpath = ctx.path + ctx.query
for x in _loadhooks.values(): x()
unloadhooks = {}
def unload():
"""
Unloads the context for the thread.
You can ask for a function to be run at loadtime by
adding it ot the dictionary `unloadhooks`.
"""
for x in unloadhooks.values(): x()
# ensures db cursors and such are GCed promptly
del _context[threading.currentThread()]
def _unload():
unload()
def wsgifunc(func, *middleware):
"""Returns a WSGI-compatible function from a webpy-function."""
middleware = list(middleware)
def wsgifunc(env, start_resp):
_load(env)
try:
result = func()
except StopIteration:
result = None
except:
print >> debug, traceback.format_exc()
result = internalerror()
is_generator = result and hasattr(result, 'next')
if is_generator:
# wsgi requires the headers first
# so we need to do an iteration
# and save the result for later
try:
firstchunk = result.next()
except StopIteration:
firstchunk = ''
status, headers, output = ctx.status, ctx.headers, ctx.output
ctx._write = start_resp(status, headers)
# and now, the fun:
def cleanup():
# we insert this little generator
# at the end of our itertools.chain
# so that it unloads the request
# when everything else is done
yield '' # force it to be a generator
_unload()
# result is the output of calling the webpy function
# it could be a generator...
if is_generator:
if firstchunk is flush:
# oh, it's just our special flush mode
# ctx._write is set up, so just continue execution
try:
result.next()
except StopIteration:
pass
_unload()
return []
else:
return itertools.chain([firstchunk], result, cleanup())
# ... but it's usually just None
#
# output is the stuff in ctx.output
# it's usually a string...
if isinstance(output, str): #@@ other stringlikes?
_unload()
return [output]
# it could be a generator...
elif hasattr(output, 'next'):
return itertools.chain(output, cleanup())
else:
_unload()
raise Exception, "Invalid ctx.output"
for mw_func in middleware:
wsgifunc = mw_func(wsgifunc)
return wsgifunc

View File

@ -0,0 +1,54 @@
"""
WSGI Utilities
(from web.py)
"""
import os, sys
import http
import webapi as web
from utils import listget
from net import validaddr, validip
import httpserver
def runfcgi(func, addr=('localhost', 8000)):
"""Runs a WSGI function as a FastCGI server."""
import flup.server.fcgi as flups
return flups.WSGIServer(func, multiplexed=True, bindAddress=addr).run()
def runscgi(func, addr=('localhost', 4000)):
"""Runs a WSGI function as an SCGI server."""
import flup.server.scgi as flups
return flups.WSGIServer(func, bindAddress=addr).run()
def runwsgi(func):
"""
Runs a WSGI-compatible `func` using FCGI, SCGI, or a simple web server,
as appropriate based on context and `sys.argv`.
"""
if os.environ.has_key('SERVER_SOFTWARE'): # cgi
os.environ['FCGI_FORCE_CGI'] = 'Y'
if (os.environ.has_key('PHP_FCGI_CHILDREN') #lighttpd fastcgi
or os.environ.has_key('SERVER_SOFTWARE')):
return runfcgi(func, None)
if 'fcgi' in sys.argv or 'fastcgi' in sys.argv:
args = sys.argv[1:]
if 'fastcgi' in args: args.remove('fastcgi')
elif 'fcgi' in args: args.remove('fcgi')
if args:
return runfcgi(func, validaddr(args[0]))
else:
return runfcgi(func, None)
if 'scgi' in sys.argv:
args = sys.argv[1:]
args.remove('scgi')
if args:
return runscgi(func, validaddr(args[0]))
else:
return runscgi(func)
return httpserver.runsimple(func, validip(listget(sys.argv, 1, '')))

File diff suppressed because it is too large Load Diff