initial commit
This commit is contained in:
Executable
+261
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
AIM class to credit card payment with authorize.net
|
||||
|
||||
Fork of authnet code written by John Conde
|
||||
http://www.johnconde.net/blog/integrate-the-authorizenet-aim-api-with-python-3-2/
|
||||
Unkown license, assuming public domain
|
||||
|
||||
Modifed by Massimo Di Pierro
|
||||
|
||||
- ported from Python 3.x run on Python 2.4+
|
||||
- fixed a couple of bugs
|
||||
- merged with test so single file
|
||||
- namedtuple from http://code.activestate.com/recipes/500261/
|
||||
|
||||
"""
|
||||
|
||||
__all__ = ['AIM']
|
||||
|
||||
from operator import itemgetter
|
||||
import urllib
|
||||
|
||||
_known_tuple_types = {}
|
||||
|
||||
class NamedTupleBase(tuple):
|
||||
"""Base class for named tuples with the __new__ operator set, named tuples
|
||||
yielded by the namedtuple() function will subclass this and add
|
||||
properties."""
|
||||
def __new__(cls, *args, **kws):
|
||||
"""Create a new instance of this fielded tuple"""
|
||||
# May need to unpack named field values here
|
||||
if kws:
|
||||
values = list(args) + [None]*(len(cls._fields) - len(args))
|
||||
fields = dict((val, idx) for idx, val in enumerate(cls._fields))
|
||||
for kw, val in kws.iteritems():
|
||||
assert kw in kws, "%r not in field list" % kw
|
||||
values[fields[kw]] = val
|
||||
args = tuple(values)
|
||||
return tuple.__new__(cls, args)
|
||||
|
||||
def namedtuple(typename, fieldnames):
|
||||
"""
|
||||
>>> import namedtuples
|
||||
>>> tpl = namedtuples.namedtuple(['a', 'b', 'c'])
|
||||
>>> tpl(1, 2, 3)
|
||||
(1, 2, 3)
|
||||
>>> tpl(1, 2, 3).b
|
||||
2
|
||||
>>> tpl(c=1, a=2, b=3)
|
||||
(2, 3, 1)
|
||||
>>> tpl(c=1, a=2, b=3).b
|
||||
3
|
||||
>>> tpl(c='pads with nones')
|
||||
(None, None, 'pads with nones')
|
||||
>>> tpl(b='pads with nones')
|
||||
(None, 'pads with nones', None)
|
||||
>>>
|
||||
"""
|
||||
# Split up a string, some people do this
|
||||
if isinstance(fieldnames, basestring):
|
||||
fieldnames = fieldnames.replace(',', ' ').split()
|
||||
# Convert anything iterable that enumerates fields to a tuple now
|
||||
fieldname_tuple = tuple(str(field) for field in fieldnames)
|
||||
# See if we've cached this
|
||||
if fieldname_tuple in _known_tuple_types:
|
||||
return _known_tuple_types[fieldname_tuple]
|
||||
# Make the type
|
||||
new_tuple_type = type(typename, (NamedTupleBase,), {})
|
||||
# Set the hidden field
|
||||
new_tuple_type._fields = fieldname_tuple
|
||||
# Add the getters
|
||||
for i, field in enumerate(fieldname_tuple):
|
||||
setattr(new_tuple_type, field, property(itemgetter(i)))
|
||||
# Cache
|
||||
_known_tuple_types[fieldname_tuple] = new_tuple_type
|
||||
# Done
|
||||
return new_tuple_type
|
||||
|
||||
class AIM:
|
||||
|
||||
class AIMError(Exception):
|
||||
def __init__(self, value):
|
||||
self.parameter = value
|
||||
def __str__(self):
|
||||
return str(self.parameter)
|
||||
|
||||
def __init__(self, login, transkey, testmode=False):
|
||||
if str(login).strip() == '' or login == None:
|
||||
raise AIM.AIMError('No login name provided')
|
||||
if str(transkey).strip() == '' or transkey == None:
|
||||
raise AIM.AIMError('No transaction key provided')
|
||||
if testmode != True and testmode != False:
|
||||
raise AIM.AIMError('Invalid value for testmode. Must be True or False. "{0}" given.'.format(testmode))
|
||||
|
||||
self.testmode = testmode
|
||||
self.proxy = None;
|
||||
self.delimiter = '|'
|
||||
self.results = []
|
||||
self.error = True
|
||||
self.success = False
|
||||
self.declined = False
|
||||
|
||||
self.parameters = {}
|
||||
self.setParameter('x_delim_data', 'true')
|
||||
self.setParameter('x_delim_char', self.delimiter)
|
||||
self.setParameter('x_relay_response', 'FALSE')
|
||||
self.setParameter('x_url', 'FALSE')
|
||||
self.setParameter('x_version', '3.1')
|
||||
self.setParameter('x_method', 'CC')
|
||||
self.setParameter('x_type', 'AUTH_CAPTURE')
|
||||
self.setParameter('x_login', login)
|
||||
self.setParameter('x_tran_key', transkey)
|
||||
|
||||
def process(self):
|
||||
encoded_args = urllib.urlencode(self.parameters)
|
||||
if self.testmode == True:
|
||||
url = 'https://test.authorize.net/gateway/transact.dll'
|
||||
else:
|
||||
url = 'https://secure.authorize.net/gateway/transact.dll'
|
||||
|
||||
if self.proxy == None:
|
||||
self.results += str(urllib.urlopen(url, encoded_args).read()).split(self.delimiter)
|
||||
else:
|
||||
opener = urllib.FancyURLopener(self.proxy)
|
||||
opened = opener.open(url, encoded_args)
|
||||
try:
|
||||
self.results += str(opened.read()).split(self.delimiter)
|
||||
finally:
|
||||
opened.close()
|
||||
Results = namedtuple('Results', 'ResultResponse ResponseSubcode ResponseCode ResponseText AuthCode \
|
||||
AVSResponse TransactionID InvoiceNumber Description Amount PaymentMethod \
|
||||
TransactionType CustomerID CHFirstName CHLastName Company BillingAddress \
|
||||
BillingCity BillingState BillingZip BillingCountry Phone Fax Email ShippingFirstName \
|
||||
ShippingLastName ShippingCompany ShippingAddress ShippingCity ShippingState \
|
||||
ShippingZip ShippingCountry TaxAmount DutyAmount FreightAmount TaxExemptFlag \
|
||||
PONumber MD5Hash CVVResponse CAVVResponse')
|
||||
self.response = Results(*tuple(r for r in self.results)[0:40])
|
||||
|
||||
if self.getResultResponseFull() == 'Approved':
|
||||
self.error = False
|
||||
self.success = True
|
||||
self.declined = False
|
||||
elif self.getResultResponseFull() == 'Declined':
|
||||
self.error = False
|
||||
self.success = False
|
||||
self.declined = True
|
||||
else:
|
||||
raise AIM.AIMError(self.response.ResponseText)
|
||||
|
||||
def setTransaction(self, creditcard, expiration, total, cvv=None, tax=None, invoice=None):
|
||||
if str(creditcard).strip() == '' or creditcard == None:
|
||||
raise AIM.AIMError('No credit card number passed to setTransaction(): {0}'.format(creditcard))
|
||||
if str(expiration).strip() == '' or expiration == None:
|
||||
raise AIM.AIMError('No expiration number to setTransaction(): {0}'.format(expiration))
|
||||
if str(total).strip() == '' or total == None:
|
||||
raise AIM.AIMError('No total amount passed to setTransaction(): {0}'.format(total))
|
||||
|
||||
self.setParameter('x_card_num', creditcard)
|
||||
self.setParameter('x_exp_date', expiration)
|
||||
self.setParameter('x_amount', total)
|
||||
if cvv != None:
|
||||
self.setParameter('x_card_code', cvv)
|
||||
if tax != None:
|
||||
self.setParameter('x_tax', tax)
|
||||
if invoice != None:
|
||||
self.setParameter('x_invoice_num', invoice)
|
||||
|
||||
def setTransactionType(self, transtype=None):
|
||||
types = ['AUTH_CAPTURE', 'AUTH_ONLY', 'PRIOR_AUTH_CAPTURE', 'CREDIT', 'CAPTURE_ONLY', 'VOID']
|
||||
if transtype.upper() not in types:
|
||||
raise AIM.AIMError('Incorrect Transaction Type passed to setTransactionType(): {0}'.format(transtype))
|
||||
self.setParameter('x_type', transtype.upper())
|
||||
|
||||
def setProxy(self, proxy=None):
|
||||
if str(proxy).strip() == '' or proxy == None:
|
||||
raise AIM.AIMError('No proxy passed to setProxy()')
|
||||
self.proxy = {'http': str(proxy).strip()}
|
||||
|
||||
def setParameter(self, key=None, value=None):
|
||||
if key != None and value != None and str(key).strip() != '' and str(value).strip() != '':
|
||||
self.parameters[key] = str(value).strip()
|
||||
else:
|
||||
raise AIM.AIMError('Incorrect parameters passed to setParameter(): {0}:{1}'.format(key, value))
|
||||
|
||||
def isApproved(self):
|
||||
return self.success
|
||||
|
||||
def isDeclined(self):
|
||||
return self.declined
|
||||
|
||||
def isError(self):
|
||||
return self.error
|
||||
|
||||
def getResultResponseFull(self):
|
||||
responses = ['', 'Approved', 'Declined', 'Error']
|
||||
return responses[int(self.results[0])]
|
||||
|
||||
def process(creditcard,expiration,total,cvv=None,tax=None,invoice=None,
|
||||
login='cnpdev4289', transkey='SR2P8g4jdEn7vFLQ',testmode=True):
|
||||
payment = AIM(login,transkey,testmode)
|
||||
expiration = expiration.replace('/','')
|
||||
payment.setTransaction(creditcard, expiration, total, cvv, tax, invoice)
|
||||
try:
|
||||
payment.process()
|
||||
return payment.isApproved()
|
||||
except AIM.AIMError:
|
||||
return False
|
||||
|
||||
def test():
|
||||
import socket
|
||||
import sys
|
||||
from time import time
|
||||
|
||||
creditcard = '4427802641004797'
|
||||
expiration = '122012'
|
||||
total = '1.00'
|
||||
cvv = '123'
|
||||
tax = '0.00'
|
||||
invoice = str(time())[4:10] # get a random invoice number
|
||||
|
||||
try:
|
||||
payment = AIM('cnpdev4289', 'SR2P8g4jdEn7vFLQ', True)
|
||||
payment.setTransaction(creditcard, expiration, total, cvv, tax, invoice)
|
||||
payment.setParameter('x_duplicate_window', 180) # three minutes duplicate windows
|
||||
payment.setParameter('x_cust_id', '1324') # customer ID
|
||||
payment.setParameter('x_first_name', 'John')
|
||||
payment.setParameter('x_last_name', 'Conde')
|
||||
payment.setParameter('x_company', 'Test Company')
|
||||
payment.setParameter('x_address', '1234 Main Street')
|
||||
payment.setParameter('x_city', 'Townsville')
|
||||
payment.setParameter('x_state', 'NJ')
|
||||
payment.setParameter('x_zip', '12345')
|
||||
payment.setParameter('x_country', 'US')
|
||||
payment.setParameter('x_phone', '800-555-1234')
|
||||
payment.setParameter('x_description', 'Test Transaction')
|
||||
payment.setParameter('x_customer_ip', socket.gethostbyname(socket.gethostname()))
|
||||
payment.setParameter('x_email', 'john@example.com')
|
||||
payment.setParameter('x_email_customer', False)
|
||||
payment.process()
|
||||
if payment.isApproved():
|
||||
print 'Response Code: ', payment.response.ResponseCode
|
||||
print 'Response Text: ', payment.response.ResponseText
|
||||
print 'Response: ', payment.getResultResponseFull()
|
||||
print 'Transaction ID: ', payment.response.TransactionID
|
||||
print 'CVV Result: ', payment.response.CVVResponse
|
||||
print 'Approval Code: ', payment.response.AuthCode
|
||||
print 'AVS Result: ', payment.response.AVSResponse
|
||||
elif payment.isDeclined():
|
||||
print 'Your credit card was declined by your bank'
|
||||
elif payment.isError():
|
||||
raise AIM.AIMError('An uncaught error occurred')
|
||||
except AIM.AIMError, e:
|
||||
print "Exception thrown:", e
|
||||
print 'An error occured'
|
||||
print 'approved',payment.isApproved()
|
||||
print 'declined',payment.isDeclined()
|
||||
print 'error',payment.isError()
|
||||
|
||||
if __name__=='__main__':
|
||||
test()
|
||||
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
DowCommerce class to process credit card payments with DowCommerce.com
|
||||
|
||||
Modifications to support Dow Commerce API from code originally written by John Conde
|
||||
http://www.johnconde.net/blog/integrate-the-authorizenet-aim-api-with-python-3-2/
|
||||
Unkown license, assuming public domain
|
||||
|
||||
Modifed by Dave Stoll dave.stoll@gmail.com
|
||||
|
||||
- modifed to support Dow Commerce API
|
||||
"""
|
||||
|
||||
__all__ = ['DowCommerce']
|
||||
|
||||
from operator import itemgetter
|
||||
import urllib
|
||||
|
||||
class DowCommerce:
|
||||
|
||||
class DowCommerceError(Exception):
|
||||
def __init__(self, value):
|
||||
self.parameter = value
|
||||
def __str__(self):
|
||||
return str(self.parameter)
|
||||
|
||||
def __init__(self, username=None, password=None, demomode=False):
|
||||
if not demomode:
|
||||
if str(username).strip() == '' or username == None:
|
||||
raise DowCommerce.DowCommerceError('No username provided')
|
||||
if str(password).strip() == '' or password == None:
|
||||
raise DowCommerce.DowCommerceError('No password provided')
|
||||
else:
|
||||
username = 'demo'
|
||||
password = 'password'
|
||||
|
||||
self.proxy = None;
|
||||
self.delimiter = '&'
|
||||
self.results = {}
|
||||
self.error = True
|
||||
self.success = False
|
||||
self.declined = False
|
||||
self.url = 'https://secure.dowcommerce.net/api/transact.php'
|
||||
|
||||
self.parameters = {}
|
||||
self.setParameter('username', username)
|
||||
self.setParameter('password', password)
|
||||
|
||||
|
||||
def process(self):
|
||||
encoded_args = urllib.urlencode(self.parameters)
|
||||
|
||||
if self.proxy == None:
|
||||
results = str(urllib.urlopen(self.url, encoded_args).read()).split(self.delimiter)
|
||||
else:
|
||||
opener = urllib.FancyURLopener(self.proxy)
|
||||
opened = opener.open(self.url, encoded_args)
|
||||
try:
|
||||
results += str(opened.read()).split(self.delimiter)
|
||||
finally:
|
||||
opened.close()
|
||||
|
||||
for result in results:
|
||||
(key,val) = result.split('=')
|
||||
self.results[key] = val
|
||||
|
||||
if self.results['response'] == '1':
|
||||
self.error = False
|
||||
self.success = True
|
||||
self.declined = False
|
||||
elif self.results['response'] == '2':
|
||||
self.error = False
|
||||
self.success = False
|
||||
self.declined = True
|
||||
elif self.results['response'] == '3':
|
||||
self.error = True
|
||||
self.success = False
|
||||
self.declined = False
|
||||
else:
|
||||
self.error = True
|
||||
self.success = False
|
||||
self.declined = False
|
||||
raise DowCommerce.DowCommerceError(self.results)
|
||||
|
||||
def setTransaction(self, creditcard, expiration, total, cvv=None, orderid=None, orderdescription=None,
|
||||
ipaddress=None, tax=None, shipping=None,
|
||||
firstname=None, lastname=None, company=None, address1=None, address2=None, city=None, state=None, zipcode=None,
|
||||
country=None, phone=None, fax=None, emailaddress=None, website=None,
|
||||
shipping_firstname=None, shipping_lastname=None, shipping_company=None, shipping_address1=None, shipping_address2=None,
|
||||
shipping_city=None, shipping_state=None, shipping_zipcode = None, shipping_country=None, shipping_emailaddress=None):
|
||||
if str(creditcard).strip() == '' or creditcard == None:
|
||||
raise DowCommerce.DowCommerceError('No credit card number passed to setTransaction(): {0}'.format(creditcard))
|
||||
if str(expiration).strip() == '' or expiration == None:
|
||||
raise DowCommerce.DowCommerceError('No expiration number passed to setTransaction(): {0}'.format(expiration))
|
||||
if str(total).strip() == '' or total == None:
|
||||
raise DowCommerce.DowCommerceError('No total amount passed to setTransaction(): {0}'.format(total))
|
||||
|
||||
self.setParameter('ccnumber', creditcard)
|
||||
self.setParameter('ccexp', expiration)
|
||||
self.setParameter('amount', total)
|
||||
|
||||
if cvv:
|
||||
self.setParameter('cvv', cvv)
|
||||
if orderid:
|
||||
self.setParameter('orderid', orderid)
|
||||
if orderdescription:
|
||||
self.setParameter('orderdescription', orderdescription)
|
||||
if ipaddress:
|
||||
self.setParameter('ipaddress', ipaddress)
|
||||
if tax:
|
||||
self.setParameter('tax', tax)
|
||||
if shipping:
|
||||
self.setParameter('shipping', shipping)
|
||||
|
||||
## billing info
|
||||
if firstname:
|
||||
self.setParameter('firstname', firstname)
|
||||
if lastname:
|
||||
self.setParameter('lastname', lastname)
|
||||
if company:
|
||||
self.setParameter('company', company)
|
||||
if address1:
|
||||
self.setParameter('address1', address1)
|
||||
if address2:
|
||||
self.setParameter('address2', address2)
|
||||
if city:
|
||||
self.setParameter('city', city)
|
||||
if state:
|
||||
self.setParameter('state', state)
|
||||
if zipcode:
|
||||
self.setParameter('zip', zipcode)
|
||||
if country:
|
||||
self.setParameter('country', country)
|
||||
if phone:
|
||||
self.setParameter('phone', phone)
|
||||
if fax:
|
||||
self.setParameter('fax', fax)
|
||||
if emailaddress:
|
||||
self.setParameter('email', emailaddress)
|
||||
if website:
|
||||
self.setParameter('website', website)
|
||||
|
||||
## shipping info
|
||||
if shipping_firstname:
|
||||
self.setParameter('shipping_firstname', shipping_firstname)
|
||||
if shipping_lastname:
|
||||
self.setParameter('shipping_lastname', shipping_lastname)
|
||||
if shipping_company:
|
||||
self.setParameter('shipping_company', shipping_company)
|
||||
if shipping_address1:
|
||||
self.setParameter('shipping_address1', shipping_address1)
|
||||
if shipping_address2:
|
||||
self.setParameter('shipping_address2', shipping_address2)
|
||||
if shipping_city:
|
||||
self.setParameter('shipping_city', shipping_city)
|
||||
if shipping_state:
|
||||
self.setParameter('shipping_state', shipping_state)
|
||||
if shipping_zipcode:
|
||||
self.setParameter('shipping_zip', shipping_zipcode)
|
||||
if shipping_country:
|
||||
self.setParameter('shipping_country', shipping_country)
|
||||
|
||||
def setTransactionType(self, transtype=None):
|
||||
types = ['sale', 'auth', 'credit']
|
||||
if transtype.lower() not in types:
|
||||
raise DowCommerce.DowCommerceError('Incorrect Transaction Type passed to setTransactionType(): {0}'.format(transtype))
|
||||
self.setParameter('type', transtype.lower())
|
||||
|
||||
def setProxy(self, proxy=None):
|
||||
if str(proxy).strip() == '' or proxy == None:
|
||||
raise DowCommerce.DowCommerceError('No proxy passed to setProxy()')
|
||||
self.proxy = {'http': str(proxy).strip()}
|
||||
|
||||
def setParameter(self, key=None, value=None):
|
||||
if key != None and value != None and str(key).strip() != '' and str(value).strip() != '':
|
||||
self.parameters[key] = str(value).strip()
|
||||
else:
|
||||
raise DowCommerce.DowCommerceError('Incorrect parameters passed to setParameter(): {0}:{1}'.format(key, value))
|
||||
|
||||
def isApproved(self):
|
||||
return self.success
|
||||
|
||||
def isDeclined(self):
|
||||
return self.declined
|
||||
|
||||
def isError(self):
|
||||
return self.error
|
||||
|
||||
def getResultResponseShort(self):
|
||||
responses = ['', 'Approved', 'Declined', 'Error']
|
||||
return responses[int(self.results['response'])]
|
||||
|
||||
def getFullResponse(self):
|
||||
return self.results
|
||||
|
||||
def getResponseText(self):
|
||||
return self.results['responsetext']
|
||||
|
||||
def test():
|
||||
import socket
|
||||
import sys
|
||||
from time import time
|
||||
|
||||
## TEST VALUES FROM API DOC:
|
||||
# Visa: 4111111111111111
|
||||
# MasterCard 5431111111111111
|
||||
# DiscoverCard: 6011601160116611
|
||||
# American Express: 341111111111111
|
||||
# Expiration: 10/10
|
||||
# Amount: > 1.00 (( passing less than $1.00 will cause it to be declined ))
|
||||
# CVV: 999
|
||||
creditcard = '4111111111111111'
|
||||
expiration = '1010'
|
||||
total = '1.00'
|
||||
cvv = '999'
|
||||
tax = '0.00'
|
||||
orderid = str(time())[4:10] # get a random invoice number
|
||||
|
||||
try:
|
||||
payment = DowCommerce(demomode=True)
|
||||
payment.setTransaction(creditcard, expiration, total, cvv=cvv, tax=tax, orderid=orderid, orderdescription='Test Transaction',
|
||||
firstname='John', lastname='Doe', company='Acme', address1='123 Min Street', city='Hometown', state='VA',
|
||||
zipcode='12345', country='US', phone='888-555-1212', emailaddress='john@noemail.local', ipaddress='192.168.1.1')
|
||||
|
||||
payment.process()
|
||||
if payment.isApproved():
|
||||
print 'Payment approved!'
|
||||
print payment.getFullResponse()
|
||||
elif payment.isDeclined():
|
||||
print 'Your credit card was declined by your bank'
|
||||
elif payment.isError():
|
||||
raise DowCommerce.DowCommerceError('An uncaught error occurred')
|
||||
except DowCommerce.DowCommerceError, e:
|
||||
print "Exception thrown:", e
|
||||
print 'An error occured'
|
||||
print 'approved',payment.isApproved()
|
||||
print 'declined',payment.isDeclined()
|
||||
print 'error',payment.isError()
|
||||
|
||||
if __name__=='__main__':
|
||||
test()
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
This file is part of the web2py Web Framework
|
||||
Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
|
||||
License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
Attention: Requires Chrome or Safari. For IE of Firefox you need https://github.com/gimite/web-socket-js
|
||||
|
||||
1) install tornado (requires Tornado 2.1)
|
||||
|
||||
easy_install tornado
|
||||
|
||||
2) start this app:
|
||||
|
||||
python gluon/contrib/comet_messaging.py -k mykey -p 8888
|
||||
|
||||
3) from any web2py app you can post messages with
|
||||
|
||||
from gluon.contrib.comet_messaging import comet_send
|
||||
comet_send('http://127.0.0.1:8888','Hello World','mykey','mygroup')
|
||||
|
||||
4) from any template you can receive them with
|
||||
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
if(!web2py_comet('ws://127.0.0.1:8888/realtime/mygroup',function(e){alert(e.data)}))
|
||||
alert("html5 websocket not supported by your browser, try Google Chrome");
|
||||
});
|
||||
</script>
|
||||
|
||||
When the server posts a message, all clients connected to the page will popup an alert message
|
||||
Or if you want to send json messages and store evaluated json in a var called data:
|
||||
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
var data;
|
||||
web2py_comet('ws://127.0.0.1:8888/realtime/mygroup',function(e){data=eval('('+e.data+')')});
|
||||
});
|
||||
</script>
|
||||
|
||||
- All communications between web2py and comet_messaging will be digitally signed with hmac.
|
||||
- All validation is handled on the web2py side and there is no need to modify comet_messaging.py
|
||||
- Multiple web2py instances can talk with one or more comet_messaging servers.
|
||||
- "ws://127.0.0.1:8888/realtime/" must be contain the IP of the comet_messaging server.
|
||||
- Via group='mygroup' name you can support multiple groups of clients (think of many chat-rooms)
|
||||
|
||||
Here is a complete sample web2py action:
|
||||
|
||||
def index():
|
||||
form=LOAD('default','ajax_form',ajax=True)
|
||||
script=SCRIPT('''
|
||||
jQuery(document).ready(function(){
|
||||
var callback=function(e){alert(e.data)};
|
||||
if(!web2py_comet('ws://127.0.0.1:8888/realtime/mygroup',callback))
|
||||
alert("html5 websocket not supported by your browser, try Google Chrome");
|
||||
});
|
||||
''')
|
||||
return dict(form=form, script=script)
|
||||
|
||||
def ajax_form():
|
||||
form=SQLFORM.factory(Field('message'))
|
||||
if form.accepts(request,session):
|
||||
from gluon.contrib.comet_messaging import comet_send
|
||||
comet_send('http://127.0.0.1:8888',form.vars.message,'mykey','mygroup')
|
||||
return form
|
||||
|
||||
Acknowledgements:
|
||||
Tornado code inspired by http://thomas.pelletier.im/2010/08/websocket-tornado-redis/
|
||||
|
||||
"""
|
||||
|
||||
import tornado.httpserver
|
||||
import tornado.websocket
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
import hmac
|
||||
import sys
|
||||
import optparse
|
||||
import urllib
|
||||
import time
|
||||
|
||||
listeners = {}
|
||||
names = {}
|
||||
tokens = {}
|
||||
|
||||
def comet_send(url,message,hmac_key=None,group='default'):
|
||||
sig = hmac_key and hmac.new(hmac_key,message).hexdigest() or ''
|
||||
params = urllib.urlencode({'message': message, 'signature': sig, 'group':group})
|
||||
f = urllib.urlopen(url, params)
|
||||
data= f.read()
|
||||
f.close()
|
||||
return data
|
||||
|
||||
class PostHandler(tornado.web.RequestHandler):
|
||||
"""
|
||||
only authorized parties can post messages
|
||||
"""
|
||||
def post(self):
|
||||
if hmac_key and not 'signature' in self.request.arguments: return 'false'
|
||||
if 'message' in self.request.arguments:
|
||||
message = self.request.arguments['message'][0]
|
||||
group = self.request.arguments.get('group',['default'])[0]
|
||||
print '%s:MESSAGE to %s:%s' % (time.time(), group, message)
|
||||
if hmac_key:
|
||||
signature = self.request.arguments['signature'][0]
|
||||
if not hmac.new(hmac_key,message).hexdigest()==signature: return 'false'
|
||||
for client in listeners.get(group,[]): client.write_message(message)
|
||||
return 'true'
|
||||
return 'false'
|
||||
|
||||
class TokenHandler(tornado.web.RequestHandler):
|
||||
"""
|
||||
if running with -t post a token to allow a client to join using the token
|
||||
the message here is the token (any uuid)
|
||||
allows only authorized parties to joins, for example, a chat
|
||||
"""
|
||||
def post(self):
|
||||
if hmac_key and not 'message' in self.request.arguments: return 'false'
|
||||
if 'message' in self.request.arguments:
|
||||
message = self.request.arguments['message'][0]
|
||||
if hmac_key:
|
||||
signature = self.request.arguments['signature'][0]
|
||||
if not hmac.new(hmac_key,message).hexdigest()==signature: return 'false'
|
||||
tokens[message] = None
|
||||
return 'true'
|
||||
return 'false'
|
||||
|
||||
class DistributeHandler(tornado.websocket.WebSocketHandler):
|
||||
def open(self,params):
|
||||
group,token,name = params.split('/')+[None,None]
|
||||
self.group = group or 'default'
|
||||
self.token = token or 'none'
|
||||
self.name = name or 'anonymous'
|
||||
# only authorized parties can join
|
||||
if DistributeHandler.tokens:
|
||||
if not self.token in tokens or not token[self.token]==None:
|
||||
self.close()
|
||||
else:
|
||||
tokens[self.token] = self
|
||||
if not self.group in listeners: listeners[self.group]=[]
|
||||
# notify clients that a member has joined the groups
|
||||
for client in listeners.get(self.group,[]): client.write_message('+'+self.name)
|
||||
listeners[self.group].append(self)
|
||||
names[self] = self.name
|
||||
print '%s:CONNECT to %s' % (time.time(), self.group)
|
||||
def on_message(self, message):
|
||||
pass
|
||||
def on_close(self):
|
||||
if self.group in listeners: listeners[self.group].remove(self)
|
||||
del names[self]
|
||||
# notify clients that a member has left the groups
|
||||
for client in listeners.get(self.group,[]): client.write_message('-'+self.name)
|
||||
print '%s:DISCONNECT from %s' % (time.time(), self.group)
|
||||
|
||||
if __name__ == "__main__":
|
||||
usage = __doc__
|
||||
version= ""
|
||||
parser = optparse.OptionParser(usage, None, optparse.Option, version)
|
||||
parser.add_option('-p',
|
||||
'--port',
|
||||
default='8888',
|
||||
dest='port',
|
||||
help='socket')
|
||||
parser.add_option('-l',
|
||||
'--listen',
|
||||
default='0.0.0.0',
|
||||
dest='address',
|
||||
help='listener address')
|
||||
parser.add_option('-k',
|
||||
'--hmac_key',
|
||||
default='',
|
||||
dest='hmac_key',
|
||||
help='hmac_key')
|
||||
parser.add_option('-t',
|
||||
'--tokens',
|
||||
action='store_true',
|
||||
default=False,
|
||||
dest='tokens',
|
||||
help='require tockens to join')
|
||||
(options, args) = parser.parse_args()
|
||||
hmac_key = options.hmac_key
|
||||
DistributeHandler.tokens = options.tokens
|
||||
urls=[
|
||||
(r'/', PostHandler),
|
||||
(r'/token', TokenHandler),
|
||||
(r'/realtime/(.*)', DistributeHandler)]
|
||||
application = tornado.web.Application(urls, auto_reload=True)
|
||||
http_server = tornado.httpserver.HTTPServer(application)
|
||||
http_server.listen(int(options.port), address=options.address)
|
||||
tornado.ioloop.IOLoop.instance().start()
|
||||
|
||||
|
||||
Executable
+3908
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Developed by Robin Bhattacharyya (memecache for GAE)
|
||||
Released under the web2py license (LGPL)
|
||||
|
||||
from gluon.contrib.gae_memcache import MemcacheClient
|
||||
cache.ram=cache.disk=MemcacheClient(request)
|
||||
"""
|
||||
|
||||
import time
|
||||
from google.appengine.api.memcache import Client
|
||||
|
||||
|
||||
class MemcacheClient(Client):
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
Client.__init__(self)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
key,
|
||||
f,
|
||||
time_expire=300,
|
||||
):
|
||||
key = '%s/%s' % (self.request.application, key)
|
||||
dt = time_expire
|
||||
value = None
|
||||
obj = self.get(key)
|
||||
if obj and (dt == None or obj[0] > time.time() - dt):
|
||||
value = obj[1]
|
||||
elif f is None:
|
||||
if obj:
|
||||
self.delete(key)
|
||||
else:
|
||||
value = f()
|
||||
self.set(key, (time.time(), value))
|
||||
return value
|
||||
|
||||
def increment(self, key, value=1):
|
||||
key = '%s/%s' % (self.request.application, key)
|
||||
obj = self.get(key)
|
||||
if obj:
|
||||
value = obj[1] + value
|
||||
self.set((time.time(), value))
|
||||
return value
|
||||
|
||||
def clear(self, key):
|
||||
key = '%s/%s' % (self.request.application, key)
|
||||
self.delete(key)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
def autoretry_datastore_timeouts(attempts=5.0, interval=0.1, exponent=2.0):
|
||||
"""
|
||||
Copyright (C) 2009 twitter.com/rcb
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
======================================================================
|
||||
|
||||
This function wraps the AppEngine Datastore API to autoretry
|
||||
datastore timeouts at the lowest accessible level.
|
||||
|
||||
The benefits of this approach are:
|
||||
|
||||
1. Small Footprint: Does not monkey with Model internals
|
||||
which may break in future releases.
|
||||
2. Max Performance: Retrying at this lowest level means
|
||||
serialization and key formatting is not
|
||||
needlessly repeated on each retry.
|
||||
At initialization time, execute this:
|
||||
|
||||
>>> autoretry_datastore_timeouts()
|
||||
|
||||
Should only be called once, subsequent calls have no effect.
|
||||
|
||||
>>> autoretry_datastore_timeouts() # no effect
|
||||
|
||||
Default (5) attempts: .1, .2, .4, .8, 1.6 seconds
|
||||
|
||||
Parameters can each be specified as floats.
|
||||
|
||||
:param attempts: maximum number of times to retry.
|
||||
:param interval: base seconds to sleep between retries.
|
||||
:param exponent: rate of exponential back-off.
|
||||
"""
|
||||
|
||||
import time, logging
|
||||
from google.appengine.api import apiproxy_stub_map
|
||||
from google.appengine.runtime import apiproxy_errors
|
||||
from google.appengine.datastore import datastore_pb
|
||||
|
||||
attempts = float(attempts)
|
||||
interval = float(interval)
|
||||
exponent = float(exponent)
|
||||
wrapped = apiproxy_stub_map.MakeSyncCall
|
||||
errors = {datastore_pb.Error.TIMEOUT:'Timeout',
|
||||
datastore_pb.Error.CONCURRENT_TRANSACTION:'TransactionFailedError'}
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
count = 0.0
|
||||
while True:
|
||||
try:
|
||||
return wrapped(*args, **kwargs)
|
||||
except apiproxy_errors.ApplicationError, err:
|
||||
errno = err.application_error
|
||||
if errno not in errors: raise
|
||||
sleep = (exponent ** count) * interval
|
||||
count += 1.0
|
||||
if count > attempts: raise
|
||||
msg = "Datastore %s: retry #%d in %s seconds.\n%s"
|
||||
vals = ''
|
||||
if count == 1.0:
|
||||
vals = '\n'.join([str(a) for a in args])
|
||||
logging.warning(msg % (errors[errno], count, sleep, vals))
|
||||
time.sleep(sleep)
|
||||
|
||||
setattr(wrapper, '_autoretry_datastore_timeouts', False)
|
||||
if getattr(wrapped, '_autoretry_datastore_timeouts', True):
|
||||
apiproxy_stub_map.MakeSyncCall = wrapper
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
# fix response
|
||||
|
||||
import re
|
||||
import os
|
||||
import cPickle
|
||||
import gluon.serializers
|
||||
from gluon import current
|
||||
from gluon.html import markmin_serializer, TAG, HTML, BODY, UL, XML
|
||||
from gluon.contenttype import contenttype
|
||||
from gluon.contrib.pyfpdf import FPDF, HTMLMixin
|
||||
from gluon.sanitizer import sanitize
|
||||
from gluon.contrib.markmin.markmin2latex import markmin2latex
|
||||
from gluon.contrib.markmin.markmin2pdf import markmin2pdf
|
||||
|
||||
def wrapper(f):
|
||||
def g(data):
|
||||
try:
|
||||
output = f(data)
|
||||
except (TypeError, ValueError):
|
||||
raise HTTP(405, '%s serialization error' % extension.upper())
|
||||
except ImportError:
|
||||
raise HTTP(405, '%s not available' % extension.upper())
|
||||
except:
|
||||
raise HTTP(405, '%s error' % extension.upper())
|
||||
return XML(ouput)
|
||||
return g
|
||||
|
||||
def latex_from_html(html):
|
||||
markmin=TAG(html).element('body').flatten(markmin_serializer)
|
||||
return XML(markmin2latex(markmin))
|
||||
|
||||
def pdflatex_from_html(html):
|
||||
if os.system('which pdflatex > /dev/null')==0:
|
||||
markmin=TAG(html).element('body').flatten(markmin_serializer)
|
||||
out,warning,errors=markmin2pdf(markmin)
|
||||
if errors:
|
||||
current.response.headers['Content-Type']='text/html'
|
||||
raise HTTP(405,HTML(BODY(H1('errors'),
|
||||
LU(*errors),
|
||||
H1('warnings'),
|
||||
LU(*warnings))).xml())
|
||||
else:
|
||||
return XML(out)
|
||||
|
||||
def pyfpdf_from_html(html):
|
||||
request = current.request
|
||||
def image_map(path):
|
||||
if path.startswith('/%s/static/' % request.application):
|
||||
return os.path.join(request.folder,path.split('/',2)[2])
|
||||
return 'http%s://%s%s' % (request.is_https and 's' or '',request.env.http_host, path)
|
||||
class MyFPDF(FPDF, HTMLMixin): pass
|
||||
pdf=MyFPDF()
|
||||
pdf.add_page()
|
||||
html = sanitize(html, escape=False) #### should have better list of allowed tags
|
||||
pdf.write_html(html,image_map=image_map)
|
||||
return XML(pdf.output(dest='S'))
|
||||
|
||||
def pdf_from_html(html):
|
||||
# try use latex and pdflatex
|
||||
if os.system('which pdflatex > /dev/null')==0:
|
||||
return pdflatex_from_html(html)
|
||||
else:
|
||||
return pyfpdf_from_html(html)
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from gluon import XML
|
||||
|
||||
def button(merchant_id="123456789012345",
|
||||
products=[dict(name="shoes",
|
||||
quantity=1,
|
||||
price=23.5,
|
||||
currency='USD',
|
||||
description="running shoes black")]):
|
||||
t = '<input name="item_%(key)s_%(k)s" type="hidden" value="%(value)s"/>'
|
||||
list_products = ''
|
||||
for k,product in enumerate(products):
|
||||
for key,value in product.items():
|
||||
list_products += t % dict(k=k+1,key=key,value=value)
|
||||
button = '<form action="https://checkout.google.com/api/checkout/v2/checkoutForm/Merchant/%s" id="BB_BuyButtonForm" method="post" name="BB_BuyButtonForm" target="_top">%s<input name="_charset_" type="hidden" value="utf-8"/><input alt="" src="https://checkout.google.com/buttons/buy.gif?merchant_id=%s&w=117&h=48&style=white&variant=text&loc=en_US" type="image"/></form>' % (merchant_id, list_products, merchant_id)
|
||||
return XML(button)
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# this file exists for backward compatibility
|
||||
|
||||
__all__ = ['DAL','Field','drivers','gae']
|
||||
|
||||
from gluon.dal import DAL, Field, Table, Query, Set, Expression, Row, Rows, drivers, BaseAdapter, SQLField, SQLTable, SQLXorable, SQLQuery, SQLSet, SQLRows, SQLStorage, SQLDB, GQLDB, SQLALL, SQLCustomType, gae
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import urllib
|
||||
import urllib2
|
||||
import base64
|
||||
|
||||
|
||||
def basic_auth(server="http://127.0.0.1"):
|
||||
"""
|
||||
to use basic login with a different server
|
||||
from gluon.contrib.login_methods.basic_auth import basic_auth
|
||||
auth.settings.login_methods.append(basic_auth('http://server'))
|
||||
"""
|
||||
|
||||
def basic_login_aux(username,
|
||||
password,
|
||||
server=server):
|
||||
key = base64.b64encode(username+':'+password)
|
||||
headers = {'Authorization': 'Basic ' + key}
|
||||
request = urllib2.Request(server, None, headers)
|
||||
try:
|
||||
urllib2.urlopen(request)
|
||||
return True
|
||||
except (urllib2.URLError, urllib2.HTTPError):
|
||||
return False
|
||||
return basic_login_aux
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
This file is part of web2py Web Framework (Copyrighted, 2007-2009).
|
||||
Developed by Massimo Di Pierro <mdipierro@cs.depaul.edu>.
|
||||
License: GPL v2
|
||||
|
||||
Tinkered by Szabolcs Gyuris < szimszo n @ o regpreshaz dot eu>
|
||||
"""
|
||||
|
||||
from gluon import current, redirect
|
||||
|
||||
class CasAuth( object ):
|
||||
"""
|
||||
Login will be done via Web2py's CAS application, instead of web2py's
|
||||
login form.
|
||||
|
||||
Include in your model (eg db.py)::
|
||||
|
||||
from gluon.contrib.login_methods.cas_auth import CasAuth
|
||||
auth.define_tables(username=True)
|
||||
auth.settings.login_form=CasAuth(
|
||||
urlbase = "https://[your CAS provider]/app/default/user/cas",
|
||||
actions=['login','validate','logout'])
|
||||
|
||||
where urlbase is the actual CAS server url without the login,logout...
|
||||
Enjoy.
|
||||
|
||||
###UPDATE###
|
||||
if you want to connect to a CAS version 2 JASIG Server use this:
|
||||
auth.settings.login_form=CasAuth(
|
||||
urlbase = "https://[Your CAS server]/cas",
|
||||
actions = ['login','serviceValidate','logout'],
|
||||
casversion = 2,
|
||||
casusername = "cas:user")
|
||||
|
||||
where casusername is the xml node returned by CAS server which contains
|
||||
user's username.
|
||||
|
||||
"""
|
||||
def __init__(self, g=None, ### g for backward compatibility ###
|
||||
urlbase = "https://web2py.com/cas/cas",
|
||||
actions=['login','check','logout'],
|
||||
maps=dict(username=lambda v:v.get('username',v['user']),
|
||||
email=lambda v:v.get('email',None),
|
||||
user_id=lambda v:v['user']),
|
||||
casversion = 1,
|
||||
casusername = 'cas:user'
|
||||
):
|
||||
self.urlbase=urlbase
|
||||
self.cas_login_url="%s/%s"%(self.urlbase,actions[0])
|
||||
self.cas_check_url="%s/%s"%(self.urlbase,actions[1])
|
||||
self.cas_logout_url="%s/%s"%(self.urlbase,actions[2])
|
||||
self.maps=maps
|
||||
self.casversion = casversion
|
||||
self.casusername = casusername
|
||||
http_host=current.request.env.http_x_forwarded_host
|
||||
if not http_host: http_host=current.request.env.http_host
|
||||
if current.request.env.wsgi_url_scheme in [ 'https', 'HTTPS' ]:
|
||||
scheme = 'https'
|
||||
else:
|
||||
scheme = 'http'
|
||||
self.cas_my_url='%s://%s%s'%( scheme, http_host, current.request.env.path_info )
|
||||
|
||||
def login_url( self, next = "/" ):
|
||||
current.session.token=self._CAS_login()
|
||||
return next
|
||||
def logout_url( self, next = "/" ):
|
||||
current.session.token=None
|
||||
current.session.auth=None
|
||||
self._CAS_logout()
|
||||
return next
|
||||
def get_user( self ):
|
||||
user=current.session.token
|
||||
if user:
|
||||
d = {'source':'web2py cas'}
|
||||
for key in self.maps:
|
||||
d[key]=self.maps[key](user)
|
||||
return d
|
||||
return None
|
||||
def _CAS_login( self ):
|
||||
"""
|
||||
exposed as CAS.login(request)
|
||||
returns a token on success, None on failed authentication
|
||||
"""
|
||||
import urllib
|
||||
self.ticket=current.request.vars.ticket
|
||||
if not current.request.vars.ticket:
|
||||
redirect( "%s?service=%s"% (self.cas_login_url,
|
||||
self.cas_my_url))
|
||||
else:
|
||||
url="%s?service=%s&ticket=%s" % (self.cas_check_url,
|
||||
self.cas_my_url,
|
||||
self.ticket )
|
||||
data=urllib.urlopen( url ).read()
|
||||
if data.startswith('yes') or data.startswith('no'):
|
||||
data = data.split('\n')
|
||||
if data[0]=='yes':
|
||||
a,b,c = data[1].split( ':' )+[None,None]
|
||||
return dict(user=a,email=b,username=c)
|
||||
return None
|
||||
import xml.dom.minidom as dom
|
||||
import xml.parsers.expat as expat
|
||||
try:
|
||||
dxml=dom.parseString(data)
|
||||
envelop = dxml.getElementsByTagName("cas:authenticationSuccess")
|
||||
if len(envelop)>0:
|
||||
res = dict()
|
||||
for x in envelop[0].childNodes:
|
||||
if x.nodeName.startswith('cas:') and len(x.childNodes):
|
||||
key = x.nodeName[4:].encode('utf8')
|
||||
value = x.childNodes[0].nodeValue.encode('utf8')
|
||||
if not key in res:
|
||||
res[key]=value
|
||||
else:
|
||||
if not isinstance(res[key],list):
|
||||
res[key]=[res[key]]
|
||||
res[key].append(value)
|
||||
return res
|
||||
except expat.ExpatError: pass
|
||||
return None # fallback
|
||||
|
||||
|
||||
def _CAS_logout( self ):
|
||||
"""
|
||||
exposed CAS.logout()
|
||||
redirects to the CAS logout page
|
||||
"""
|
||||
import urllib
|
||||
redirect("%s?service=%s" % (self.cas_logout_url,self.cas_my_url))
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf8
|
||||
|
||||
"""
|
||||
Dropbox Authentication for web2py
|
||||
Developed by Massimo Di Pierro (2011)
|
||||
Same License as Web2py License
|
||||
"""
|
||||
|
||||
# mind here session is dropbox session, not current.session
|
||||
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
from dropbox import client, rest, session
|
||||
from gluon import *
|
||||
from gluon.tools import fetch
|
||||
from gluon.storage import Storage
|
||||
import gluon.contrib.simplejson as json
|
||||
|
||||
class DropboxAccount(object):
|
||||
|
||||
"""
|
||||
from gluon.contrib.login_methods.dropbox_account import DropboxAccount
|
||||
auth.settings.actions_disabled=['register','change_password','request_reset_password']
|
||||
auth.settings.login_form = DropboxAccount(request,
|
||||
key="...",
|
||||
secret="...",
|
||||
access_type="...",
|
||||
url = "http://localhost:8000/%s/default/user/login" % request.application)
|
||||
when logged in
|
||||
client = auth.settings.login_form.client
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
request,
|
||||
key = "",
|
||||
secret = "",
|
||||
access_type="app_folder",
|
||||
login_url = "",
|
||||
on_login_failure=None,
|
||||
):
|
||||
|
||||
self.request=request
|
||||
self.key=key
|
||||
self.secret=secret
|
||||
self.access_type=access_type
|
||||
self.login_url = login_url
|
||||
self.on_login_failure = on_login_failure
|
||||
self.sess = session.DropboxSession(
|
||||
self.key,self.secret,self.access_type)
|
||||
|
||||
|
||||
def get_user(self):
|
||||
request = self.request
|
||||
token = current.session.dropbox_token
|
||||
try:
|
||||
access_token = self.sess.obtain_access_token(token)
|
||||
except:
|
||||
access_token = None
|
||||
if access_token:
|
||||
user = Storage()
|
||||
self.client = client.DropboxClient(self.sess)
|
||||
data = self.client.account_info()
|
||||
display_name = data.get('display_name','').split(' ',1)
|
||||
user = dict(email = data.get('email',None),
|
||||
first_name = display_name[0],
|
||||
last_name = display_name[-1],
|
||||
registration_id = data.get('uid',None))
|
||||
if not user['registration_id'] and self.on_login_failure:
|
||||
redirect(self.on_login_failure)
|
||||
return user
|
||||
return None
|
||||
|
||||
def login_form(self):
|
||||
token = self.sess.obtain_request_token()
|
||||
current.session.dropbox_token = token
|
||||
dropbox_url = self.sess.build_authorize_url(token,self.login_url)
|
||||
redirect(dropbox_url)
|
||||
form = IFRAME(_src=dropbox_url,
|
||||
_scrolling="no",
|
||||
_frameborder="no",
|
||||
_style="width:400px;height:240px;")
|
||||
return form
|
||||
def logout_url(self, next = "/"):
|
||||
current.session.dropbox_token=None
|
||||
current.session.auth=None
|
||||
redirect('https://www.dropbox.com/logout')
|
||||
return next
|
||||
def put(self,filename,file):
|
||||
return json.loads(self.client.put_file(filename,file))['bytes']
|
||||
def get(self,filename,file):
|
||||
return self.client.get_file(filename)
|
||||
def dir(self,path):
|
||||
return json.loads(self.client.metadata(path))
|
||||
|
||||
def use_dropbox(auth,filename='private/dropbox.key',**kwargs):
|
||||
path = os.path.join(current.request.folder,filename)
|
||||
if os.path.exists(path):
|
||||
request = current.request
|
||||
key,secret,access_type = open(path,'r').read().strip().split(':')
|
||||
host = current.request.env.http_host
|
||||
login_url = "http://%s/%s/default/user/login" % \
|
||||
(host,request.application)
|
||||
auth.settings.actions_disabled = \
|
||||
['register','change_password','request_reset_password']
|
||||
auth.settings.login_form = DropboxAccount(
|
||||
request,key=key,secret=secret,access_type=access_type,
|
||||
login_url = login_url,**kwargs)
|
||||
@@ -0,0 +1,45 @@
|
||||
import smtplib
|
||||
import logging
|
||||
|
||||
def email_auth(server="smtp.gmail.com:587",
|
||||
domain="@gmail.com",
|
||||
tls_mode=None):
|
||||
"""
|
||||
to use email_login:
|
||||
from gluon.contrib.login_methods.email_auth import email_auth
|
||||
auth.settings.login_methods.append(email_auth("smtp.gmail.com:587",
|
||||
"@gmail.com"))
|
||||
"""
|
||||
|
||||
def email_auth_aux(email,
|
||||
password,
|
||||
server=server,
|
||||
domain=domain,
|
||||
tls_mode=tls_mode):
|
||||
if domain:
|
||||
if not isinstance(domain,(list,tuple)):
|
||||
domain=[str(domain)]
|
||||
if not [d for d in domain if email[-len(d):]==d]:
|
||||
return False
|
||||
(host, port) = server.split(':')
|
||||
if tls_mode is None: # then auto detect
|
||||
tls_mode = port == '587'
|
||||
try:
|
||||
server = None
|
||||
server = smtplib.SMTP(host, port)
|
||||
server.ehlo()
|
||||
if tls_mode:
|
||||
server.starttls()
|
||||
server.ehlo()
|
||||
server.login(email, password)
|
||||
server.quit()
|
||||
return True
|
||||
except:
|
||||
logging.exception('email_auth() failed')
|
||||
if server:
|
||||
try:
|
||||
server.quit()
|
||||
except: # server might already close connection after error
|
||||
pass
|
||||
return False
|
||||
return email_auth_aux
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf8
|
||||
|
||||
"""
|
||||
ExtendedLoginForm is used to extend normal login form in web2py with one more login method.
|
||||
So user can choose the built-in login or extended login methods.
|
||||
"""
|
||||
|
||||
from gluon.html import DIV
|
||||
|
||||
class ExtendedLoginForm(object):
|
||||
"""
|
||||
Put extended_login_form under web2py/gluon/contrib/login_methods folder.
|
||||
Then inside your model where defines the auth:
|
||||
|
||||
auth = Auth(globals(),db) # authentication/authorization
|
||||
...
|
||||
auth.define_tables() # You might like to put the code after auth.define_tables
|
||||
... # if the alt_login_form deals with tables of auth.
|
||||
|
||||
alt_login_form = RPXAccount(request,
|
||||
api_key="...",
|
||||
domain="...",
|
||||
url = "http://localhost:8000/%s/default/user/login" % request.application)
|
||||
extended_login_form = ExtendedLoginForm(auth, alt_login_form, signals=['token'])
|
||||
|
||||
auth.settings.login_form = extended_login_form
|
||||
|
||||
Note:
|
||||
Since rpx_account doesn't create the password for the user, you
|
||||
might need to provide a way for user to create password to do
|
||||
normal login.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
auth,
|
||||
alt_login_form,
|
||||
signals=[],
|
||||
login_arg = 'login'
|
||||
):
|
||||
self.auth = auth
|
||||
self.alt_login_form = alt_login_form
|
||||
self.signals = signals
|
||||
self.login_arg = login_arg
|
||||
|
||||
def get_user(self):
|
||||
"""
|
||||
Delegate the get_user to alt_login_form.get_user.
|
||||
"""
|
||||
if hasattr(self.alt_login_form, 'get_user'):
|
||||
return self.alt_login_form.get_user()
|
||||
return None # let gluon.tools.Auth.get_or_create_user do the rest
|
||||
|
||||
def login_url(self, next):
|
||||
"""
|
||||
Optional implement for alt_login_form.
|
||||
|
||||
In normal case, this should be replaced by get_user, and never get called.
|
||||
"""
|
||||
if hasattr(self.alt_login_form, 'login_url'):
|
||||
return self.alt_login_form.login_url(next)
|
||||
return self.auth.settings.login_url
|
||||
|
||||
def logout_url(self, next):
|
||||
"""
|
||||
Optional implement for alt_login_form.
|
||||
|
||||
Called if bool(alt_login_form.get_user) is True.
|
||||
|
||||
If alt_login_form implemented logout_url function, it will return that function call.
|
||||
"""
|
||||
if hasattr(self.alt_login_form, 'logout_url'):
|
||||
return self.alt_login_form.logout_url(next)
|
||||
return next
|
||||
|
||||
def login_form(self):
|
||||
"""
|
||||
Combine the auth() form with alt_login_form.
|
||||
|
||||
If signals are set and a parameter in request matches any signals,
|
||||
it will return the call of alt_login_form.login_form instead.
|
||||
So alt_login_form can handle some particular situations, for example,
|
||||
multiple steps of OpenID login inside alt_login_form.login_form.
|
||||
|
||||
Otherwise it will render the normal login form combined with
|
||||
alt_login_form.login_form.
|
||||
"""
|
||||
request = self.auth.environment.request
|
||||
args = request.args
|
||||
|
||||
if (self.signals and
|
||||
any([True for signal in self.signals if request.vars.has_key(signal)])
|
||||
):
|
||||
return self.alt_login_form.login_form()
|
||||
|
||||
self.auth.settings.login_form = self.auth
|
||||
form = DIV(self.auth())
|
||||
self.auth.settings.login_form = self
|
||||
|
||||
form.components.append(self.alt_login_form.login_form())
|
||||
return form
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
This file is part of web2py Web Framework (Copyrighted, 2007-2009).
|
||||
Developed by Massimo Di Pierro <mdipierro@cs.depaul.edu>.
|
||||
License: GPL v2
|
||||
|
||||
Thanks to Hans Donner <hans.donner@pobox.com> for GaeGoogleAccount.
|
||||
"""
|
||||
|
||||
from google.appengine.api import users
|
||||
|
||||
class GaeGoogleAccount(object):
|
||||
"""
|
||||
Login will be done via Google's Appengine login object, instead of web2py's
|
||||
login form.
|
||||
|
||||
Include in your model (eg db.py)::
|
||||
|
||||
from gluon.contrib.login_methods.gae_google_account import \
|
||||
GaeGoogleAccount
|
||||
auth.settings.login_form=GaeGoogleAccount()
|
||||
|
||||
"""
|
||||
|
||||
def login_url(self, next="/"):
|
||||
return users.create_login_url(next)
|
||||
|
||||
def logout_url(self, next="/"):
|
||||
return users.create_logout_url(next)
|
||||
|
||||
def get_user(self):
|
||||
user = users.get_current_user()
|
||||
if user:
|
||||
return dict(nickname=user.nickname(), email=user.email(),
|
||||
user_id=user.user_id(), source="google account")
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import sys
|
||||
import logging
|
||||
try:
|
||||
import ldap
|
||||
ldap.set_option(ldap.OPT_REFERRALS, 0)
|
||||
except Exception, e:
|
||||
logging.error('missing ldap, try "easy_install python-ldap"')
|
||||
raise e
|
||||
|
||||
|
||||
def ldap_auth(server='ldap', port=None,
|
||||
base_dn='ou=users,dc=domain,dc=com',
|
||||
mode='uid', secure=False, cert_path=None, bind_dn=None, bind_pw=None, filterstr='objectClass=*'):
|
||||
"""
|
||||
to use ldap login with MS Active Directory::
|
||||
|
||||
from gluon.contrib.login_methods.ldap_auth import ldap_auth
|
||||
auth.settings.login_methods.append(ldap_auth(
|
||||
mode='ad', server='my.domain.controller',
|
||||
base_dn='ou=Users,dc=domain,dc=com'))
|
||||
|
||||
to use ldap login with Notes Domino::
|
||||
|
||||
auth.settings.login_methods.append(ldap_auth(
|
||||
mode='domino',server='my.domino.server'))
|
||||
|
||||
to use ldap login with OpenLDAP::
|
||||
|
||||
auth.settings.login_methods.append(ldap_auth(
|
||||
server='my.ldap.server', base_dn='ou=Users,dc=domain,dc=com'))
|
||||
|
||||
to use ldap login with OpenLDAP and subtree search and (optionally) multiple DNs:
|
||||
|
||||
auth.settings.login_methods.append(ldap_auth(
|
||||
mode='uid_r', server='my.ldap.server',
|
||||
base_dn=['ou=Users,dc=domain,dc=com','ou=Staff,dc=domain,dc=com']))
|
||||
|
||||
or (if using CN)::
|
||||
|
||||
auth.settings.login_methods.append(ldap_auth(
|
||||
mode='cn', server='my.ldap.server',
|
||||
base_dn='ou=Users,dc=domain,dc=com'))
|
||||
|
||||
If using secure ldaps:// pass secure=True and cert_path="..."
|
||||
|
||||
If you need to bind to the directory with an admin account in order to search it then specify bind_dn & bind_pw to use for this.
|
||||
- currently only implemented for Active Directory
|
||||
|
||||
If you need to restrict the set of allowed users (e.g. to members of a department) then specify
|
||||
a rfc4515 search filter string.
|
||||
- currently only implemented for mode in ['ad', 'company', 'uid_r']
|
||||
"""
|
||||
|
||||
def ldap_auth_aux(username,
|
||||
password,
|
||||
ldap_server=server,
|
||||
ldap_port=port,
|
||||
ldap_basedn=base_dn,
|
||||
ldap_mode=mode,
|
||||
ldap_binddn=bind_dn,
|
||||
ldap_bindpw=bind_pw,
|
||||
secure=secure,
|
||||
cert_path=cert_path,
|
||||
filterstr=filterstr):
|
||||
try:
|
||||
if secure:
|
||||
if not ldap_port:
|
||||
ldap_port = 636
|
||||
con = ldap.initialize(
|
||||
"ldaps://" + ldap_server + ":" + str(ldap_port))
|
||||
if cert_path:
|
||||
con.set_option(ldap.OPT_X_TLS_CACERTDIR, cert_path)
|
||||
else:
|
||||
if not ldap_port:
|
||||
ldap_port = 389
|
||||
con = ldap.initialize(
|
||||
"ldap://" + ldap_server + ":" + str(ldap_port))
|
||||
|
||||
if ldap_mode == 'ad':
|
||||
# Microsoft Active Directory
|
||||
if '@' not in username:
|
||||
domain = []
|
||||
for x in ldap_basedn.split(','):
|
||||
if "DC=" in x.upper():
|
||||
domain.append(x.split('=')[-1])
|
||||
username = "%s@%s" % (username, '.'.join(domain))
|
||||
username_bare = username.split("@")[0]
|
||||
con.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
|
||||
# In cases where ForestDnsZones and DomainDnsZones are found,
|
||||
# result will look like the following:
|
||||
# ['ldap://ForestDnsZones.domain.com/DC=ForestDnsZones,DC=domain,DC=com']
|
||||
if not isinstance(result, dict):
|
||||
# result should be a dict in the form {'sAMAccountName': [username_bare]}
|
||||
return False
|
||||
if ldap_binddn:
|
||||
# need to search directory with an admin account 1st
|
||||
con.simple_bind_s(ldap_binddn, ldap_bindpw)
|
||||
else:
|
||||
# credentials should be in the form of username@domain.tld
|
||||
con.simple_bind_s(username, password)
|
||||
# this will throw an index error if the account is not found
|
||||
# in the ldap_basedn
|
||||
result = con.search_ext_s(
|
||||
ldap_basedn, ldap.SCOPE_SUBTREE,
|
||||
"(&(sAMAccountName=%s)(%s))" % (username_bare, filterstr), ["sAMAccountName"])[0][1]
|
||||
if ldap_binddn:
|
||||
# We know the user exists & is in the correct OU
|
||||
# so now we just check the password
|
||||
con.simple_bind_s(username, password)
|
||||
|
||||
if ldap_mode == 'domino':
|
||||
# Notes Domino
|
||||
if "@" in username:
|
||||
username = username.split("@")[0]
|
||||
con.simple_bind_s(username, password)
|
||||
|
||||
if ldap_mode == 'cn':
|
||||
# OpenLDAP (CN)
|
||||
dn = "cn=" + username + "," + ldap_basedn
|
||||
con.simple_bind_s(dn, password)
|
||||
|
||||
if ldap_mode == 'uid':
|
||||
# OpenLDAP (UID)
|
||||
dn = "uid=" + username + "," + ldap_basedn
|
||||
con.simple_bind_s(dn, password)
|
||||
|
||||
if ldap_mode == 'company':
|
||||
# no DNs or password needed to search directory
|
||||
dn = ""
|
||||
pw = ""
|
||||
# bind anonymously
|
||||
con.simple_bind_s(dn, pw)
|
||||
# search by e-mail address
|
||||
filter = '(&(mail=' + username + ')(' + filterstr + '))'
|
||||
# find the uid
|
||||
attrs = ['uid']
|
||||
# perform the actual search
|
||||
company_search_result=con.search_s(ldap_basedn,
|
||||
ldap.SCOPE_SUBTREE,
|
||||
filter, attrs)
|
||||
dn = company_search_result[0][0]
|
||||
# perform the real authentication test
|
||||
con.simple_bind_s(dn, password)
|
||||
|
||||
if ldap_mode == 'uid_r':
|
||||
# OpenLDAP (UID) with subtree search and multiple DNs
|
||||
if type(ldap_basedn) == type([]):
|
||||
basedns = ldap_basedn
|
||||
else:
|
||||
basedns = [ldap_basedn]
|
||||
filter = '(&(uid=%s)(%s))' % (username, filterstr)
|
||||
for basedn in basedns:
|
||||
try:
|
||||
result = con.search_s(basedn, ldap.SCOPE_SUBTREE, filter)
|
||||
if result:
|
||||
user_dn = result[0][0]
|
||||
# Check the password
|
||||
con.simple_bind_s(user_dn, password)
|
||||
con.unbind()
|
||||
return True
|
||||
except ldap.LDAPError, detail:
|
||||
(exc_type, exc_value) = sys.exc_info()[:2]
|
||||
sys.stderr.write("ldap_auth: searching %s for %s resulted in %s: %s\n" %
|
||||
(basedn, filter, exc_type, exc_value))
|
||||
return False
|
||||
|
||||
con.unbind()
|
||||
return True
|
||||
except ldap.LDAPError, e:
|
||||
return False
|
||||
except IndexError, ex: # for AD membership test
|
||||
return False
|
||||
|
||||
if filterstr[0] == '(' and filterstr[-1] == ')': # rfc4515 syntax
|
||||
filterstr = filterstr[1:-1] # parens added again where used
|
||||
return ldap_auth_aux
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
This file is part of web2py Web Framework (Copyrighted, 2007-2009).
|
||||
Developed by Massimo Di Pierro <mdipierro@cs.depaul.edu>.
|
||||
License: GPL v2
|
||||
|
||||
Thanks to Hans Donner <hans.donner@pobox.com> for GaeGoogleAccount.
|
||||
"""
|
||||
|
||||
from gluon.http import HTTP
|
||||
try:
|
||||
import linkedin
|
||||
except ImportError:
|
||||
raise HTTP(400,"linkedin module not found")
|
||||
|
||||
class LinkedInAccount(object):
|
||||
"""
|
||||
Login will be done via Google's Appengine login object, instead of web2py's
|
||||
login form.
|
||||
|
||||
Include in your model (eg db.py)::
|
||||
|
||||
from gluon.contrib.login_methods.linkedin_account import LinkedInAccount
|
||||
auth.settings.login_form=LinkedInAccount(request,KEY,SECRET,RETURN_URL)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self,request,key,secret,return_url):
|
||||
self.request = request
|
||||
self.api = linkedin.LinkedIn(key,secret,return_url)
|
||||
self.token = result = self.api.requestToken()
|
||||
|
||||
def login_url(self, next="/"):
|
||||
return self.api.getAuthorizeURL(self.token)
|
||||
|
||||
def logout_url(self, next="/"):
|
||||
return ''
|
||||
|
||||
def get_user(self):
|
||||
result = self.request.vars.verifier and self.api.accessToken(verifier = self.request.vars.verifier )
|
||||
if result:
|
||||
profile = self.api.GetProfile()
|
||||
profile = self.api.GetProfile(profile).public_url = "http://www.linkedin.com/in/ozgurv"
|
||||
return dict(first_name = profile.first_name,
|
||||
last_name = profile.last_name,
|
||||
username = profile.id)
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Loginza.ru authentication for web2py
|
||||
Developed by Vladimir Dronnikov (Copyright © 2011)
|
||||
Email <dronnikov@gmail.com>
|
||||
"""
|
||||
|
||||
import urllib
|
||||
from gluon.html import *
|
||||
from gluon.tools import fetch
|
||||
from gluon.storage import Storage
|
||||
import gluon.contrib.simplejson as json
|
||||
|
||||
class Loginza(object):
|
||||
|
||||
"""
|
||||
from gluon.contrib.login_methods.loginza import Loginza
|
||||
auth.settings.login_form = Loginza(request,
|
||||
url = "http://localhost:8000/%s/default/user/login" % request.application)
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
request,
|
||||
url = "",
|
||||
embed = True,
|
||||
auth_url = "http://loginza.ru/api/authinfo",
|
||||
language = "en",
|
||||
prompt = "loginza",
|
||||
on_login_failure = None,
|
||||
):
|
||||
|
||||
self.request = request
|
||||
self.token_url = url
|
||||
self.embed = embed
|
||||
self.auth_url = auth_url
|
||||
self.language = language
|
||||
self.prompt = prompt
|
||||
self.profile = None
|
||||
self.on_login_failure = on_login_failure
|
||||
self.mappings = Storage()
|
||||
|
||||
# TODO: profile.photo is the URL to the picture
|
||||
# Howto download and store it locally?
|
||||
# FIXME: what if email is unique=True
|
||||
|
||||
self.mappings["http://twitter.com/"] = lambda profile:\
|
||||
dict(registration_id = profile.get("identity",""),
|
||||
username = profile.get("nickname",""),
|
||||
email = profile.get("email",""),
|
||||
last_name = profile.get("name","").get("full_name",""),
|
||||
#avatar = profile.get("photo",""),
|
||||
)
|
||||
self.mappings["https://www.google.com/accounts/o8/ud"] = lambda profile:\
|
||||
dict(registration_id = profile.get("identity",""),
|
||||
username = profile.get("name","").get("full_name",""),
|
||||
email = profile.get("email",""),
|
||||
first_name = profile.get("name","").get("first_name",""),
|
||||
last_name = profile.get("name","").get("last_name",""),
|
||||
#avatar = profile.get("photo",""),
|
||||
)
|
||||
self.mappings["http://vkontakte.ru/"] = lambda profile:\
|
||||
dict(registration_id=profile.get("identity",""),
|
||||
username = profile.get("name","").get("full_name",""),
|
||||
email = profile.get("email",""),
|
||||
first_name = profile.get("name","").get("first_name",""),
|
||||
last_name = profile.get("name","").get("last_name",""),
|
||||
#avatar = profile.get("photo",""),
|
||||
)
|
||||
self.mappings.default = lambda profile:\
|
||||
dict(registration_id = profile.get("identity",""),
|
||||
username = profile.get("name","").get("full_name"),
|
||||
email = profile.get("email",""),
|
||||
first_name = profile.get("name","").get("first_name",""),
|
||||
last_name = profile.get("name","").get("last_name",""),
|
||||
#avatar = profile.get("photo",""),
|
||||
)
|
||||
|
||||
def get_user(self):
|
||||
request = self.request
|
||||
if request.vars.token:
|
||||
user = Storage()
|
||||
data = urllib.urlencode(dict(token = request.vars.token))
|
||||
auth_info_json = fetch(self.auth_url+'?'+data)
|
||||
#print auth_info_json
|
||||
auth_info = json.loads(auth_info_json)
|
||||
if auth_info["identity"] != None:
|
||||
self.profile = auth_info
|
||||
provider = self.profile["provider"]
|
||||
user = self.mappings.get(provider, self.mappings.default)(self.profile)
|
||||
#user["password"] = ???
|
||||
#user["avatar"] = ???
|
||||
return user
|
||||
elif self.on_login_failure:
|
||||
redirect(self.on_login_failure)
|
||||
return None
|
||||
|
||||
def login_form(self):
|
||||
request = self.request
|
||||
args = request.args
|
||||
LOGINZA_URL = "https://loginza.ru/api/widget?lang=%s&token_url=%s&overlay=loginza"
|
||||
if self.embed:
|
||||
form = IFRAME(_src=LOGINZA_URL % (self.language, self.token_url),
|
||||
_scrolling="no",
|
||||
_frameborder="no",
|
||||
_style="width:359px;height:300px;")
|
||||
else:
|
||||
form = DIV(A(self.prompt, _href=LOGINZA_URL % (self.language, self.token_url), _class="loginza"),
|
||||
SCRIPT(_src="https://s3-eu-west-1.amazonaws.com/s1.loginza.ru/js/widget.js", _type="text/javascript"))
|
||||
return form
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Written by Michele Comitini <mcm@glisco.it>
|
||||
License: GPL v3
|
||||
|
||||
Adds support for OAuth1.0a authentication to web2py.
|
||||
|
||||
Dependencies:
|
||||
- python-oauth2 (http://github.com/simplegeo/python-oauth2)
|
||||
|
||||
"""
|
||||
|
||||
import oauth2 as oauth
|
||||
import cgi
|
||||
|
||||
from urllib2 import urlopen
|
||||
import urllib2
|
||||
from urllib import urlencode
|
||||
|
||||
class OAuthAccount(object):
|
||||
"""
|
||||
Login will be done via OAuth Framework, instead of web2py's
|
||||
login form.
|
||||
|
||||
Include in your model (eg db.py)::
|
||||
# define the auth_table before call to auth.define_tables()
|
||||
auth_table = db.define_table(
|
||||
auth.settings.table_user_name,
|
||||
Field('first_name', length=128, default=""),
|
||||
Field('last_name', length=128, default=""),
|
||||
Field('username', length=128, default="", unique=True),
|
||||
Field('password', 'password', length=256,
|
||||
readable=False, label='Password'),
|
||||
Field('registration_key', length=128, default= "",
|
||||
writable=False, readable=False))
|
||||
|
||||
auth_table.username.requires = IS_NOT_IN_DB(db, auth_table.username)
|
||||
.
|
||||
.
|
||||
.
|
||||
auth.define_tables()
|
||||
.
|
||||
.
|
||||
.
|
||||
|
||||
CLIENT_ID=\"<put your fb application id here>\"
|
||||
CLIENT_SECRET=\"<put your fb application secret here>\"
|
||||
AUTH_URL="..."
|
||||
TOKEN_URL="..."
|
||||
ACCESS_TOKEN_URL="..."
|
||||
from gluon.contrib.login_methods.oauth10a_account import OAuthAccount
|
||||
auth.settings.login_form=OAuthAccount(globals(),CLIENT_ID,CLIENT_SECRET, AUTH_URL, TOKEN_URL, ACCESS_TOKEN_URL)
|
||||
|
||||
"""
|
||||
|
||||
def __redirect_uri(self, next=None):
|
||||
"""Build the uri used by the authenticating server to redirect
|
||||
the client back to the page originating the auth request.
|
||||
Appends the _next action to the generated url so the flows continues.
|
||||
"""
|
||||
r = self.request
|
||||
http_host=r.env.http_x_forwarded_for
|
||||
if not http_host: http_host=r.env.http_host
|
||||
|
||||
url_scheme = r.env.wsgi_url_scheme
|
||||
if next:
|
||||
path_info = next
|
||||
else:
|
||||
path_info = r.env.path_info
|
||||
uri = '%s://%s%s' %(url_scheme, http_host, path_info)
|
||||
if r.get_vars and not next:
|
||||
uri += '?' + urlencode(r.get_vars)
|
||||
return uri
|
||||
|
||||
|
||||
def accessToken(self):
|
||||
"""Return the access token generated by the authenticating server.
|
||||
|
||||
If token is already in the session that one will be used.
|
||||
Otherwise the token is fetched from the auth server.
|
||||
|
||||
"""
|
||||
|
||||
if self.session.access_token:
|
||||
# return the token (TODO: does it expire?)
|
||||
|
||||
return self.session.access_token
|
||||
if self.session.request_token:
|
||||
# Exchange the request token with an authorization token.
|
||||
token = self.session.request_token
|
||||
self.session.request_token = None
|
||||
|
||||
# Build an authorized client
|
||||
# OAuth1.0a put the verifier!
|
||||
token.set_verifier(self.request.vars.oauth_verifier)
|
||||
client = oauth.Client(self.consumer, token)
|
||||
|
||||
|
||||
resp, content = client.request(self.access_token_url, "POST")
|
||||
if str(resp['status']) != '200':
|
||||
self.session.request_token = None
|
||||
self.globals['redirect'](self.globals['URL'](f='user',args='logout'))
|
||||
|
||||
|
||||
self.session.access_token = oauth.Token.from_string(content)
|
||||
|
||||
return self.session.access_token
|
||||
|
||||
self.session.access_token = None
|
||||
return None
|
||||
|
||||
def __init__(self, g, client_id, client_secret, auth_url, token_url, access_token_url):
|
||||
self.globals = g
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self.code = None
|
||||
self.request = g['request']
|
||||
self.session = g['session']
|
||||
self.auth_url = auth_url
|
||||
self.token_url = token_url
|
||||
self.access_token_url = access_token_url
|
||||
|
||||
# consumer init
|
||||
self.consumer = oauth.Consumer(self.client_id, self.client_secret)
|
||||
|
||||
|
||||
def login_url(self, next="/"):
|
||||
self.__oauth_login(next)
|
||||
return next
|
||||
|
||||
def logout_url(self, next="/"):
|
||||
self.session.request_token = None
|
||||
self.session.access_token = None
|
||||
return next
|
||||
|
||||
def get_user(self):
|
||||
'''Get user data.
|
||||
|
||||
Since OAuth does not specify what a user
|
||||
is, this function must be implemented for the specific
|
||||
provider.
|
||||
'''
|
||||
raise NotImplementedError, "Must override get_user()"
|
||||
|
||||
def __oauth_login(self, next):
|
||||
'''This method redirects the user to the authenticating form
|
||||
on authentication server if the authentication code
|
||||
and the authentication token are not available to the
|
||||
application yet.
|
||||
|
||||
Once the authentication code has been received this method is
|
||||
called to set the access token into the session by calling
|
||||
accessToken()
|
||||
'''
|
||||
|
||||
if not self.accessToken():
|
||||
# setup the client
|
||||
client = oauth.Client(self.consumer, None)
|
||||
# Get a request token.
|
||||
# oauth_callback *is REQUIRED* for OAuth1.0a
|
||||
# putting it in the body seems to work.
|
||||
callback_url = self.__redirect_uri(next)
|
||||
data = urlencode(dict(oauth_callback=callback_url))
|
||||
resp, content = client.request(self.token_url, "POST", body=data)
|
||||
if resp['status'] != '200':
|
||||
self.session.request_token = None
|
||||
self.globals['redirect'](self.globals['URL'](f='user',args='logout'))
|
||||
|
||||
# Store the request token in session.
|
||||
request_token = self.session.request_token = oauth.Token.from_string(content)
|
||||
|
||||
# Redirect the user to the authentication URL and pass the callback url.
|
||||
data = urlencode(dict(oauth_token=request_token.key,
|
||||
oauth_callback=callback_url))
|
||||
auth_request_url = self.auth_url + '?' +data
|
||||
|
||||
|
||||
HTTP = self.globals['HTTP']
|
||||
|
||||
|
||||
raise HTTP(307,
|
||||
"You are not authenticated: you are being redirected to the <a href='" + auth_request_url + "'> authentication server</a>",
|
||||
Location=auth_request_url)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Written by Michele Comitini <mcm@glisco.it>
|
||||
License: GPL v3
|
||||
|
||||
Adds support for OAuth 2.0 authentication to web2py.
|
||||
|
||||
OAuth 2.0 Draft: http://tools.ietf.org/html/draft-ietf-oauth-v2-10
|
||||
"""
|
||||
|
||||
import time
|
||||
import cgi
|
||||
|
||||
from urllib2 import urlopen
|
||||
import urllib2
|
||||
from urllib import urlencode
|
||||
|
||||
class OAuthAccount(object):
|
||||
"""
|
||||
Login will be done via OAuth Framework, instead of web2py's
|
||||
login form.
|
||||
|
||||
Include in your model (eg db.py)::
|
||||
# define the auth_table before call to auth.define_tables()
|
||||
auth_table = db.define_table(
|
||||
auth.settings.table_user_name,
|
||||
Field('first_name', length=128, default=""),
|
||||
Field('last_name', length=128, default=""),
|
||||
Field('username', length=128, default="", unique=True),
|
||||
Field('password', 'password', length=256,
|
||||
readable=False, label='Password'),
|
||||
Field('registration_key', length=128, default= "",
|
||||
writable=False, readable=False))
|
||||
|
||||
auth_table.username.requires = IS_NOT_IN_DB(db, auth_table.username)
|
||||
.
|
||||
.
|
||||
.
|
||||
auth.define_tables()
|
||||
.
|
||||
.
|
||||
.
|
||||
|
||||
CLIENT_ID=\"<put your fb application id here>\"
|
||||
CLIENT_SECRET=\"<put your fb application secret here>\"
|
||||
AUTH_URL="http://..."
|
||||
TOKEN_URL="http://..."
|
||||
from gluon.contrib.login_methods.oauth20_account import OAuthAccount
|
||||
auth.settings.login_form=OAuthAccount(globals(),CLIENT_ID,CLIENT_SECRET,AUTH_URL, TOKEN_URL, **args )
|
||||
Any optional arg will be passed as is to remote server for requests.
|
||||
It can be used for the optional "scope" parameters for Facebook.
|
||||
"""
|
||||
def __redirect_uri(self, next=None):
|
||||
"""Build the uri used by the authenticating server to redirect
|
||||
the client back to the page originating the auth request.
|
||||
Appends the _next action to the generated url so the flows continues.
|
||||
"""
|
||||
|
||||
r = self.request
|
||||
http_host=r.env.http_x_forwarded_for
|
||||
if not http_host: http_host=r.env.http_host
|
||||
|
||||
url_scheme = r.env.wsgi_url_scheme
|
||||
if next:
|
||||
path_info = next
|
||||
else:
|
||||
path_info = r.env.path_info
|
||||
uri = '%s://%s%s' %(url_scheme, http_host, path_info)
|
||||
if r.get_vars and not next:
|
||||
uri += '?' + urlencode(r.get_vars)
|
||||
return uri
|
||||
|
||||
|
||||
def __build_url_opener(self, uri):
|
||||
"""Build the url opener for managing HTTP Basic Athentication"""
|
||||
# Create an OpenerDirector with support for Basic HTTP Authentication...
|
||||
auth_handler = urllib2.HTTPBasicAuthHandler()
|
||||
auth_handler.add_password(None,
|
||||
uri,
|
||||
self.client_id,
|
||||
self.client_secret)
|
||||
opener = urllib2.build_opener(auth_handler)
|
||||
return opener
|
||||
|
||||
|
||||
def accessToken(self):
|
||||
"""Return the access token generated by the authenticating server.
|
||||
|
||||
If token is already in the session that one will be used.
|
||||
Otherwise the token is fetched from the auth server.
|
||||
|
||||
"""
|
||||
if self.session.token and self.session.token.has_key('expires'):
|
||||
expires = self.session.token['expires']
|
||||
# reuse token until expiration
|
||||
if expires == 0 or expires > time.time():
|
||||
return self.session.token['access_token']
|
||||
if self.session.code:
|
||||
data = dict(client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
redirect_uri=self.session.redirect_uri,
|
||||
response_type='token', code=self.session.code)
|
||||
|
||||
|
||||
if self.args:
|
||||
data.update(self.args)
|
||||
open_url = None
|
||||
opener = self.__build_url_opener(self.token_url)
|
||||
try:
|
||||
open_url = opener.open(self.token_url, urlencode(data))
|
||||
except urllib2.HTTPError, e:
|
||||
raise Exception(e.read())
|
||||
finally:
|
||||
del self.session.code # throw it away
|
||||
|
||||
if open_url:
|
||||
try:
|
||||
tokendata = cgi.parse_qs(open_url.read())
|
||||
self.session.token = dict([(k,v[-1]) for k,v in tokendata.items()])
|
||||
# set expiration absolute time try to avoid broken
|
||||
# implementations where "expires_in" becomes "expires"
|
||||
if self.session.token.has_key('expires_in'):
|
||||
exps = 'expires_in'
|
||||
else:
|
||||
exps = 'expires'
|
||||
self.session.token['expires'] = int(self.session.token[exps]) + \
|
||||
time.time()
|
||||
finally:
|
||||
opener.close()
|
||||
return self.session.token['access_token']
|
||||
|
||||
self.session.token = None
|
||||
return None
|
||||
|
||||
def __init__(self, g, client_id, client_secret, auth_url, token_url, **args):
|
||||
self.globals = g
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self.request = g['request']
|
||||
self.session = g['session']
|
||||
self.auth_url = auth_url
|
||||
self.token_url = token_url
|
||||
self.args = args
|
||||
|
||||
def login_url(self, next="/"):
|
||||
self.__oauth_login(next)
|
||||
return next
|
||||
|
||||
def logout_url(self, next="/"):
|
||||
del self.session.token
|
||||
return next
|
||||
|
||||
def get_user(self):
|
||||
'''Returns the user using the Graph API.
|
||||
'''
|
||||
raise NotImplementedError, "Must override get_user()"
|
||||
if not self.accessToken():
|
||||
return None
|
||||
|
||||
if not self.graph:
|
||||
self.graph = GraphAPI((self.accessToken()))
|
||||
|
||||
user = None
|
||||
try:
|
||||
user = self.graph.get_object("me")
|
||||
except GraphAPIError:
|
||||
self.session.token = None
|
||||
self.graph = None
|
||||
|
||||
if user:
|
||||
return dict(first_name = user['first_name'],
|
||||
last_name = user['last_name'],
|
||||
username = user['id'])
|
||||
|
||||
|
||||
|
||||
def __oauth_login(self, next):
|
||||
'''This method redirects the user to the authenticating form
|
||||
on authentication server if the authentication code
|
||||
and the authentication token are not available to the
|
||||
application yet.
|
||||
|
||||
Once the authentication code has been received this method is
|
||||
called to set the access token into the session by calling
|
||||
accessToken()
|
||||
'''
|
||||
if not self.accessToken():
|
||||
if not self.request.vars.code:
|
||||
self.session.redirect_uri=self.__redirect_uri(next)
|
||||
data = dict(redirect_uri=self.session.redirect_uri,
|
||||
response_type='code',
|
||||
client_id=self.client_id)
|
||||
if self.args:
|
||||
data.update(self.args)
|
||||
auth_request_url = self.auth_url + "?" +urlencode(data)
|
||||
HTTP = self.globals['HTTP']
|
||||
raise HTTP(307,
|
||||
"You are not authenticated: you are being redirected to the <a href='" + auth_request_url + "'> authentication server</a>",
|
||||
Location=auth_request_url)
|
||||
else:
|
||||
self.session.code = self.request.vars.code
|
||||
self.accessToken()
|
||||
return self.session.code
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,632 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf8
|
||||
|
||||
"""
|
||||
OpenID authentication for web2py
|
||||
|
||||
Allowed using OpenID login together with web2py built-in login.
|
||||
|
||||
By default, to support OpenID login, put this in your db.py
|
||||
|
||||
>>> from gluon.contrib.login_methods.openid_auth import OpenIDAuth
|
||||
>>> auth.settings.login_form = OpenIDAuth(auth)
|
||||
|
||||
To show OpenID list in user profile, you can add the following code
|
||||
before the end of function user() of your_app/controllers/default.py
|
||||
|
||||
+ if (request.args and request.args(0) == "profile"):
|
||||
+ form = DIV(form, openid_login_form.list_user_openids())
|
||||
return dict(form=form, login_form=login_form, register_form=register_form, self_registration=self_registration)
|
||||
|
||||
More detail in the description of the class OpenIDAuth.
|
||||
|
||||
Requirements:
|
||||
python-openid version 2.2.5 or later
|
||||
|
||||
Reference:
|
||||
* w2p openID
|
||||
http://w2popenid.appspot.com/init/default/wiki/w2popenid
|
||||
* RPX and web2py auth module
|
||||
http://www.web2pyslices.com/main/slices/take_slice/28
|
||||
* built-in file: gluon/contrib/login_methods/rpx_account.py
|
||||
* built-in file: gluon/tools.py (Auth class)
|
||||
"""
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from gluon import *
|
||||
from gluon.storage import Storage, Messages
|
||||
|
||||
try:
|
||||
import openid.consumer.consumer
|
||||
from openid.association import Association
|
||||
from openid.store.interface import OpenIDStore
|
||||
from openid.extensions.sreg import SRegRequest, SRegResponse
|
||||
from openid.store import nonce
|
||||
from openid.consumer.discover import DiscoveryFailure
|
||||
except ImportError, err:
|
||||
raise ImportError("OpenIDAuth requires python-openid package")
|
||||
|
||||
DEFAULT = lambda: None
|
||||
|
||||
class OpenIDAuth(object):
|
||||
"""
|
||||
OpenIDAuth
|
||||
|
||||
It supports the logout_url, implementing the get_user and login_form
|
||||
for cas usage of gluon.tools.Auth.
|
||||
|
||||
It also uses the ExtendedLoginForm to allow the OpenIDAuth login_methods
|
||||
combined with the standard logon/register procedure.
|
||||
|
||||
It uses OpenID Consumer when render the form and begins the OpenID
|
||||
authentication.
|
||||
|
||||
Example: (put these code after auth.define_tables() in your models.)
|
||||
|
||||
auth = Auth(globals(), db) # authentication/authorization
|
||||
...
|
||||
auth.define_tables() # creates all needed tables
|
||||
...
|
||||
|
||||
#include in your model after auth has been defined
|
||||
from gluon.contrib.login_methods.openid_auth import OpenIDAuth
|
||||
openid_login_form = OpenIDAuth(request, auth, db)
|
||||
|
||||
from gluon.contrib.login_methods.extended_login_form import ExtendedLoginForm
|
||||
extended_login_form = ExtendedLoginForm(request, auth, openid_login_form,
|
||||
signals=['oid','janrain_nonce'])
|
||||
|
||||
auth.settings.login_form = extended_login_form
|
||||
"""
|
||||
|
||||
def __init__(self, auth):
|
||||
self.auth = auth
|
||||
self.db = auth.db
|
||||
|
||||
request = current.request
|
||||
self.nextvar = '_next'
|
||||
self.realm = 'http://%s' % request.env.http_host
|
||||
self.login_url = URL(r=request, f='user', args=['login'])
|
||||
self.return_to_url = self.realm + self.login_url
|
||||
|
||||
self.table_alt_logins_name = "alt_logins"
|
||||
if not auth.settings.table_user:
|
||||
raise
|
||||
self.table_user = self.auth.settings.table_user
|
||||
self.openid_expiration = 15 #minutes
|
||||
|
||||
self.messages = self._define_messages()
|
||||
|
||||
if not self.table_alt_logins_name in self.db.tables:
|
||||
self._define_alt_login_table()
|
||||
|
||||
def _define_messages(self):
|
||||
messages = Messages(current.T)
|
||||
messages.label_alt_login_username = 'Sign-in with OpenID: '
|
||||
messages.label_add_alt_login_username = 'Add a new OpenID: '
|
||||
messages.submit_button = 'Sign in'
|
||||
messages.submit_button_add = 'Add'
|
||||
messages.a_delete = 'Delete'
|
||||
messages.comment_openid_signin = 'What is OpenID?'
|
||||
messages.comment_openid_help_title = 'Start using your OpenID'
|
||||
messages.comment_openid_help_url = 'http://openid.net/get-an-openid/start-using-your-openid/'
|
||||
messages.openid_fail_discover = 'Failed to discover OpenID service. Check your OpenID or "More about OpenID"?'
|
||||
messages.flash_openid_expired = 'OpenID expired. Please login or authenticate OpenID again. Sorry for the inconvenient.'
|
||||
messages.flash_openid_associated = 'OpenID associated'
|
||||
messages.flash_associate_openid = 'Please login or register an account for this OpenID.'
|
||||
messages.p_openid_not_registered = "This Open ID haven't be registered. " \
|
||||
+ "Please login to associate with it or register an account for it."
|
||||
messages.flash_openid_authenticated = 'OpenID authenticated successfully.'
|
||||
messages.flash_openid_fail_authentication = 'OpenID authentication failed. (Error message: %s)'
|
||||
messages.flash_openid_canceled = 'OpenID authentication canceled by user.'
|
||||
messages.flash_openid_need_setup = 'OpenID authentication needs to be setup by the user with the provider first.'
|
||||
messages.h_openid_login = 'OpenID Login'
|
||||
messages.h_openid_list = 'OpenID List'
|
||||
return messages
|
||||
|
||||
def _define_alt_login_table(self):
|
||||
"""
|
||||
Define the OpenID login table.
|
||||
Note: type is what I used for our project. We're going to support 'fackbook' and
|
||||
'plurk' alternate login methods. Otherwise it's always 'openid' and you
|
||||
may not need it. This should be easy to changed.
|
||||
(Just remove the field of "type" and remove the
|
||||
"and db.alt_logins.type == type_" in _find_matched_openid function)
|
||||
"""
|
||||
db = self.db
|
||||
table = db.define_table(
|
||||
self.table_alt_logins_name,
|
||||
Field('username', length=512, default=''),
|
||||
Field('type', length=128, default='openid', readable=False),
|
||||
Field('user', self.table_user, readable=False),
|
||||
)
|
||||
table.username.requires = IS_NOT_IN_DB(db, table.username)
|
||||
self.table_alt_logins = table
|
||||
|
||||
def logout_url(self, next):
|
||||
"""
|
||||
Delete the w2popenid record in session as logout
|
||||
"""
|
||||
if current.session.w2popenid:
|
||||
del(current.session.w2popenid)
|
||||
return next
|
||||
|
||||
def login_form(self):
|
||||
"""
|
||||
Start to process the OpenID response if 'janrain_nonce' in request parameters
|
||||
and not processed yet. Else return the OpenID form for login.
|
||||
"""
|
||||
request = current.request
|
||||
if request.vars.has_key('janrain_nonce') and not self._processed():
|
||||
self._process_response()
|
||||
return self.auth()
|
||||
return self._form()
|
||||
|
||||
def get_user(self):
|
||||
"""
|
||||
It supports the logout_url, implementing the get_user and login_form
|
||||
for cas usage of gluon.tools.Auth.
|
||||
"""
|
||||
request = current.request
|
||||
args = request.args
|
||||
|
||||
if args[0] == 'logout':
|
||||
return True # Let logout_url got called
|
||||
|
||||
if current.session.w2popenid:
|
||||
w2popenid = current.session.w2popenid
|
||||
db = self.db
|
||||
if (w2popenid.ok is True and w2popenid.oid): # OpenID authenticated
|
||||
if self._w2popenid_expired(w2popenid):
|
||||
del(current.session.w2popenid)
|
||||
flash = self.messages.flash_openid_expired
|
||||
current.session.warning = flash
|
||||
redirect(self.auth.settings.login_url)
|
||||
oid = self._remove_protocol(w2popenid.oid)
|
||||
alt_login = self._find_matched_openid(db, oid)
|
||||
|
||||
nextvar = self.nextvar
|
||||
# This OpenID not in the database. If user logged in then add it
|
||||
# into database, else ask user to login or register.
|
||||
if not alt_login:
|
||||
if self.auth.is_logged_in():
|
||||
# TODO: ask first maybe
|
||||
self._associate_user_openid(self.auth.user, oid)
|
||||
if current.session.w2popenid:
|
||||
del(current.session.w2popenid)
|
||||
current.session.flash = self.messages.flash_openid_associated
|
||||
if request.vars.has_key(nextvar):
|
||||
redirect(request.vars[nextvar])
|
||||
redirect(self.auth.settings.login_next)
|
||||
|
||||
if not request.vars.has_key(nextvar):
|
||||
# no next var, add it and do login again
|
||||
# so if user login or register can go back here to associate the OpenID
|
||||
redirect(URL(r=request,
|
||||
args=['login'],
|
||||
vars={nextvar:self.login_url}))
|
||||
self.login_form = self._form_with_notification()
|
||||
current.session.flash = self.messages.flash_associate_openid
|
||||
return None # need to login or register to associate this openid
|
||||
|
||||
# Get existed OpenID user
|
||||
user = db(self.table_user.id==alt_login.user).select().first()
|
||||
if user:
|
||||
if current.session.w2popenid:
|
||||
del(current.session.w2popenid)
|
||||
if 'username' in self.table_user.fields():
|
||||
username = 'username'
|
||||
elif 'email' in self.table_user.fields():
|
||||
username = 'email'
|
||||
return {username: user[username]} if user else None # login success (almost)
|
||||
|
||||
return None # just start to login
|
||||
|
||||
def _find_matched_openid(self, db, oid, type_='openid'):
|
||||
"""
|
||||
Get the matched OpenID for given
|
||||
"""
|
||||
query = ((db.alt_logins.username == oid) & (db.alt_logins.type == type_))
|
||||
alt_login = db(query).select().first() # Get the OpenID record
|
||||
return alt_login
|
||||
|
||||
def _associate_user_openid(self, user, oid):
|
||||
"""
|
||||
Associate the user logged in with given OpenID
|
||||
"""
|
||||
# print "[DB] %s authenticated" % oid
|
||||
self.db.alt_logins.insert(username=oid, user=user.id)
|
||||
|
||||
def _form_with_notification(self):
|
||||
"""
|
||||
Render the form for normal login with a notice of OpenID authenticated
|
||||
"""
|
||||
form = DIV()
|
||||
# TODO: check when will happen
|
||||
if self.auth.settings.login_form in (self.auth, self):
|
||||
self.auth.settings.login_form = self.auth
|
||||
form = DIV(self.auth())
|
||||
|
||||
register_note = DIV(P(self.messages.p_openid_not_registered))
|
||||
form.components.append(register_note)
|
||||
return lambda: form
|
||||
|
||||
def _remove_protocol(self, oid):
|
||||
"""
|
||||
Remove https:// or http:// from oid url
|
||||
"""
|
||||
protocol = 'https://'
|
||||
if oid.startswith(protocol):
|
||||
oid = oid[len(protocol):]
|
||||
return oid
|
||||
protocol = 'http://'
|
||||
if oid.startswith(protocol):
|
||||
oid = oid[len(protocol):]
|
||||
return oid
|
||||
return oid
|
||||
|
||||
def _init_consumerhelper(self):
|
||||
"""
|
||||
Initialize the ConsumerHelper
|
||||
"""
|
||||
if not hasattr(self, "consumerhelper"):
|
||||
self.consumerhelper = ConsumerHelper(current.session,
|
||||
self.db)
|
||||
return self.consumerhelper
|
||||
|
||||
|
||||
def _form(self, style=None):
|
||||
form = DIV(H3(self.messages.h_openid_login), self._login_form(style))
|
||||
return form
|
||||
|
||||
def _login_form(self,
|
||||
openid_field_label=None,
|
||||
submit_button=None,
|
||||
_next=None,
|
||||
style=None):
|
||||
"""
|
||||
Render the form for OpenID login
|
||||
"""
|
||||
def warning_openid_fail(session):
|
||||
session.warning = messages.openid_fail_discover
|
||||
|
||||
style = style or """
|
||||
background-attachment: scroll;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("http://wiki.openid.net/f/openid-16x16.gif");
|
||||
background-position: 0% 50%;
|
||||
background-color: transparent;
|
||||
padding-left: 18px;
|
||||
width: 400px;
|
||||
"""
|
||||
style = style.replace("\n","")
|
||||
|
||||
request = current.request
|
||||
session = current.session
|
||||
messages = self.messages
|
||||
hidden_next_input = ""
|
||||
if _next == 'profile':
|
||||
profile_url = URL(r=request, f='user', args=['profile'])
|
||||
hidden_next_input = INPUT(_type="hidden", _name="_next", _value=profile_url)
|
||||
form = FORM(openid_field_label or self.messages.label_alt_login_username,
|
||||
INPUT(_type="input", _name="oid",
|
||||
requires=IS_NOT_EMPTY(error_message=messages.openid_fail_discover),
|
||||
_style=style),
|
||||
hidden_next_input,
|
||||
INPUT(_type="submit", _value=submit_button or messages.submit_button),
|
||||
" ",
|
||||
A(messages.comment_openid_signin,
|
||||
_href=messages.comment_openid_help_url,
|
||||
_title=messages.comment_openid_help_title,
|
||||
_class='openid-identifier',
|
||||
_target="_blank"),
|
||||
_action=self.login_url
|
||||
)
|
||||
if form.accepts(request.vars, session):
|
||||
oid = request.vars.oid
|
||||
consumerhelper = self._init_consumerhelper()
|
||||
url = self.login_url
|
||||
return_to_url = self.return_to_url
|
||||
if not oid:
|
||||
warning_openid_fail(session)
|
||||
redirect(url)
|
||||
try:
|
||||
if request.vars.has_key('_next'):
|
||||
return_to_url = self.return_to_url + '?_next=' + request.vars._next
|
||||
url = consumerhelper.begin(oid, self.realm, return_to_url)
|
||||
except DiscoveryFailure:
|
||||
warning_openid_fail(session)
|
||||
redirect(url)
|
||||
return form
|
||||
|
||||
def _processed(self):
|
||||
"""
|
||||
Check if w2popenid authentication is processed.
|
||||
Return True if processed else False.
|
||||
"""
|
||||
processed = (hasattr(current.session, 'w2popenid') and
|
||||
current.session.w2popenid.ok is True)
|
||||
return processed
|
||||
|
||||
def _set_w2popenid_expiration(self, w2popenid):
|
||||
"""
|
||||
Set expiration for OpenID authentication.
|
||||
"""
|
||||
w2popenid.expiration = datetime.now() + timedelta(minutes=self.openid_expiration)
|
||||
|
||||
def _w2popenid_expired(self, w2popenid):
|
||||
"""
|
||||
Check if w2popenid authentication is expired.
|
||||
Return True if expired else False.
|
||||
"""
|
||||
return (not w2popenid.expiration) or (datetime.now() > w2popenid.expiration)
|
||||
|
||||
def _process_response(self):
|
||||
"""
|
||||
Process the OpenID by ConsumerHelper.
|
||||
"""
|
||||
request = current.request
|
||||
request_vars = request.vars
|
||||
consumerhelper = self._init_consumerhelper()
|
||||
process_status = consumerhelper.process_response(request_vars, self.return_to_url)
|
||||
if process_status == "success":
|
||||
w2popenid = current.session.w2popenid
|
||||
user_data = self.consumerhelper.sreg()
|
||||
current.session.w2popenid.ok = True
|
||||
self._set_w2popenid_expiration(w2popenid)
|
||||
w2popenid.user_data = user_data
|
||||
current.session.flash = self.messages.flash_openid_authenticated
|
||||
elif process_status == "failure":
|
||||
flash = self.messages.flash_openid_fail_authentication % consumerhelper.error_message
|
||||
current.session.warning = flash
|
||||
elif process_status == "cancel":
|
||||
current.session.warning = self.messages.flash_openid_canceled
|
||||
elif process_status == "setup_needed":
|
||||
current.session.warning = self.messages.flash_openid_need_setup
|
||||
|
||||
def list_user_openids(self):
|
||||
messages = self.messages
|
||||
request = current.request
|
||||
if request.vars.has_key('delete_openid'):
|
||||
self.remove_openid(request.vars.delete_openid)
|
||||
|
||||
query = self.db.alt_logins.user == self.auth.user.id
|
||||
alt_logins = self.db(query).select()
|
||||
l = []
|
||||
for alt_login in alt_logins:
|
||||
username = alt_login.username
|
||||
delete_href = URL(r=request, f='user',
|
||||
args=['profile'],
|
||||
vars={'delete_openid': username})
|
||||
delete_link = A(messages.a_delete, _href=delete_href)
|
||||
l.append(LI(username, " ", delete_link))
|
||||
|
||||
profile_url = URL(r=request, f='user', args=['profile'])
|
||||
#return_to_url = self.return_to_url + '?' + self.nextvar + '=' + profile_url
|
||||
openid_list = DIV(H3(messages.h_openid_list), UL(l),
|
||||
self._login_form(
|
||||
_next='profile',
|
||||
submit_button=messages.submit_button_add,
|
||||
openid_field_label=messages.label_add_alt_login_username)
|
||||
)
|
||||
return openid_list
|
||||
|
||||
|
||||
def remove_openid(self, openid):
|
||||
query = self.db.alt_logins.username == openid
|
||||
self.db(query).delete()
|
||||
|
||||
class ConsumerHelper(object):
|
||||
"""
|
||||
ConsumerHelper knows the python-openid and
|
||||
"""
|
||||
|
||||
def __init__(self, session, db):
|
||||
self.session = session
|
||||
store = self._init_store(db)
|
||||
self.consumer = openid.consumer.consumer.Consumer(session, store)
|
||||
|
||||
def _init_store(self, db):
|
||||
"""
|
||||
Initialize Web2pyStore
|
||||
"""
|
||||
if not hasattr(self, "store"):
|
||||
store = Web2pyStore(db)
|
||||
session = self.session
|
||||
if not session.has_key('w2popenid'):
|
||||
session.w2popenid = Storage()
|
||||
self.store = store
|
||||
return self.store
|
||||
|
||||
def begin(self, oid, realm, return_to_url):
|
||||
"""
|
||||
Begin the OpenID authentication
|
||||
"""
|
||||
w2popenid = self.session.w2popenid
|
||||
w2popenid.oid = oid
|
||||
auth_req = self.consumer.begin(oid)
|
||||
auth_req.addExtension(SRegRequest(required=['email','nickname']))
|
||||
url = auth_req.redirectURL(return_to=return_to_url, realm=realm)
|
||||
return url
|
||||
|
||||
def process_response(self, request_vars, return_to_url):
|
||||
"""
|
||||
Complete the process and
|
||||
"""
|
||||
resp = self.consumer.complete(request_vars, return_to_url)
|
||||
if resp:
|
||||
if resp.status == openid.consumer.consumer.SUCCESS:
|
||||
self.resp = resp
|
||||
if hasattr(resp, "identity_url"):
|
||||
self.session.w2popenid.oid = resp.identity_url
|
||||
return "success"
|
||||
if resp.status == openid.consumer.consumer.FAILURE:
|
||||
self.error_message = resp.message
|
||||
return "failure"
|
||||
if resp.status == openid.consumer.consumer.CANCEL:
|
||||
return "cancel"
|
||||
if resp.status == openid.consumer.consumer.SETUP_NEEDED:
|
||||
return "setup_needed"
|
||||
return "no resp"
|
||||
|
||||
def sreg(self):
|
||||
"""
|
||||
Try to get OpenID Simple Registation
|
||||
http://openid.net/specs/openid-simple-registration-extension-1_0.html
|
||||
"""
|
||||
if self.resp:
|
||||
resp = self.resp
|
||||
sreg_resp = SRegResponse.fromSuccessResponse(resp)
|
||||
return sreg_resp.data if sreg_resp else None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class Web2pyStore(OpenIDStore):
|
||||
"""
|
||||
Web2pyStore
|
||||
|
||||
This class implements the OpenIDStore interface. OpenID stores take care
|
||||
of persisting nonces and associations. The Janrain Python OpenID library
|
||||
comes with implementations for file and memory storage. Web2pyStore uses
|
||||
the web2py db abstration layer. See the source code docs of OpenIDStore
|
||||
for a comprehensive description of this interface.
|
||||
"""
|
||||
|
||||
def __init__(self, database):
|
||||
self.database = database
|
||||
self.table_oid_associations_name = 'oid_associations'
|
||||
self.table_oid_nonces_name = 'oid_nonces'
|
||||
self._initDB()
|
||||
|
||||
def _initDB(self):
|
||||
|
||||
if self.table_oid_associations_name not in self.database:
|
||||
self.database.define_table(self.table_oid_associations_name,
|
||||
Field('server_url', 'string', length=2047, required=True),
|
||||
Field('handle', 'string', length=255, required=True),
|
||||
Field('secret', 'blob', required=True),
|
||||
Field('issued', 'integer', required=True),
|
||||
Field('lifetime', 'integer', required=True),
|
||||
Field('assoc_type', 'string', length=64, required=True)
|
||||
)
|
||||
if self.table_oid_nonces_name not in self.database:
|
||||
self.database.define_table(self.table_oid_nonces_name,
|
||||
Field('server_url', 'string', length=2047, required=True),
|
||||
Field('timestamp', 'integer', required=True),
|
||||
Field('salt', 'string', length=40, required=True)
|
||||
)
|
||||
|
||||
def storeAssociation(self, server_url, association):
|
||||
"""
|
||||
Store associations. If there already is one with the same
|
||||
server_url and handle in the table replace it.
|
||||
"""
|
||||
|
||||
db = self.database
|
||||
query = (db.oid_associations.server_url == server_url) & (db.oid_associations.handle == association.handle)
|
||||
db(query).delete()
|
||||
db.oid_associations.insert(server_url = server_url,
|
||||
handle = association.handle,
|
||||
secret = association.secret,
|
||||
issued = association.issued,
|
||||
lifetime = association.lifetime,
|
||||
assoc_type = association.assoc_type), 'insert '*10
|
||||
|
||||
def getAssociation(self, server_url, handle=None):
|
||||
"""
|
||||
Return the association for server_url and handle. If handle is
|
||||
not None return the latests associations for that server_url.
|
||||
Return None if no association can be found.
|
||||
"""
|
||||
|
||||
db = self.database
|
||||
query = (db.oid_associations.server_url == server_url)
|
||||
if handle:
|
||||
query &= (db.oid_associations.handle == handle)
|
||||
rows = db(query).select(orderby=db.oid_associations.issued)
|
||||
keep_assoc, _ = self._removeExpiredAssocations(rows)
|
||||
if len(keep_assoc) == 0:
|
||||
return None
|
||||
else:
|
||||
assoc = keep_assoc.pop() # pop the last one as it should be the latest one
|
||||
return Association(assoc['handle'],
|
||||
assoc['secret'],
|
||||
assoc['issued'],
|
||||
assoc['lifetime'],
|
||||
assoc['assoc_type'])
|
||||
|
||||
def removeAssociation(self, server_url, handle):
|
||||
db = self.database
|
||||
query = (db.oid_associations.server_url == server_url) & (db.oid_associations.handle == handle)
|
||||
return db(query).delete() != None
|
||||
|
||||
def useNonce(self, server_url, timestamp, salt):
|
||||
"""
|
||||
This method returns Falase if a nonce has been used before or its
|
||||
timestamp is not current.
|
||||
"""
|
||||
|
||||
db = self.database
|
||||
if abs(timestamp - time.time()) > nonce.SKEW:
|
||||
return False
|
||||
query = (db.oid_nonces.server_url == server_url) & (db.oid_nonces.timestamp == timestamp) & (db.oid_nonces.salt == salt)
|
||||
if db(query).count() > 0:
|
||||
return False
|
||||
else:
|
||||
db.oid_nonces.insert(server_url = server_url,
|
||||
timestamp = timestamp,
|
||||
salt = salt)
|
||||
return True
|
||||
|
||||
def _removeExpiredAssocations(self, rows):
|
||||
"""
|
||||
This helper function is not part of the interface. Given a list of
|
||||
association rows it checks which associations have expired and
|
||||
deletes them from the db. It returns a tuple of the form
|
||||
([valid_assoc], no_of_expired_assoc_deleted).
|
||||
"""
|
||||
|
||||
db = self.database
|
||||
keep_assoc = []
|
||||
remove_assoc = []
|
||||
t1970 = time.time()
|
||||
for r in rows:
|
||||
if r['issued'] + r['lifetime'] < t1970:
|
||||
remove_assoc.append(r)
|
||||
else:
|
||||
keep_assoc.append(r)
|
||||
for r in remove_assoc:
|
||||
del db.oid_associations[r['id']]
|
||||
return (keep_assoc, len(remove_assoc)) # return tuple (list of valid associations, number of deleted associations)
|
||||
|
||||
def cleanupNonces(self):
|
||||
"""
|
||||
Remove expired nonce entries from DB and return the number
|
||||
of entries deleted.
|
||||
"""
|
||||
|
||||
db = self.database
|
||||
query = (db.oid_nonces.timestamp < time.time() - nonce.SKEW)
|
||||
return db(query).delete()
|
||||
|
||||
def cleanupAssociations(self):
|
||||
"""
|
||||
Remove expired associations from db and return the number
|
||||
of entries deleted.
|
||||
"""
|
||||
|
||||
db = self.database
|
||||
query = (db.oid_associations.id > 0)
|
||||
return self._removeExpiredAssocations(db(query).select())[1] #return number of assoc removed
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
This method should be run periodically to free the db from
|
||||
expired nonce and association entries.
|
||||
"""
|
||||
|
||||
return self.cleanupNonces(), self.cleanupAssociations()
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from gluon.contrib.pam import authenticate
|
||||
|
||||
def pam_auth():
|
||||
"""
|
||||
to use pam_login:
|
||||
from gluon.contrib.login_methods.pam_auth import pam_auth
|
||||
auth.settings.login_methods.append(pam_auth())
|
||||
"""
|
||||
|
||||
def pam_auth_aux(username, password):
|
||||
return authenticate(username, password)
|
||||
|
||||
return pam_auth_aux
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf8
|
||||
|
||||
"""
|
||||
RPX Authentication for web2py
|
||||
Developed by Nathan Freeze (Copyright © 2009)
|
||||
Email <nathan@freezable.com>
|
||||
Modified by Massimo Di Pierro
|
||||
|
||||
This file contains code to allow using RPXNow.com (now Jainrain.com)
|
||||
services with web2py
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
from gluon import *
|
||||
from gluon.tools import fetch
|
||||
from gluon.storage import Storage
|
||||
import gluon.contrib.simplejson as json
|
||||
|
||||
class RPXAccount(object):
|
||||
|
||||
"""
|
||||
from gluon.contrib.login_methods.rpx_account import RPXAccount
|
||||
auth.settings.actions_disabled=['register','change_password','request_reset_password']
|
||||
auth.settings.login_form = RPXAccount(request,
|
||||
api_key="...",
|
||||
domain="...",
|
||||
url = "http://localhost:8000/%s/default/user/login" % request.application)
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
request,
|
||||
api_key = "",
|
||||
domain = "",
|
||||
url = "",
|
||||
embed = True,
|
||||
auth_url = "https://rpxnow.com/api/v2/auth_info",
|
||||
language= "en",
|
||||
prompt='rpx',
|
||||
on_login_failure = None,
|
||||
):
|
||||
|
||||
self.request=request
|
||||
self.api_key=api_key
|
||||
self.embed = embed
|
||||
self.auth_url = auth_url
|
||||
self.domain = domain
|
||||
self.token_url = url
|
||||
self.language = language
|
||||
self.profile = None
|
||||
self.prompt = prompt
|
||||
self.on_login_failure = on_login_failure
|
||||
self.mappings = Storage()
|
||||
|
||||
dn = {'givenName':'','familyName':''}
|
||||
self.mappings.Facebook = lambda profile, dn=dn:\
|
||||
dict(registration_id = profile.get("identifier",""),
|
||||
username = profile.get("preferredUsername",""),
|
||||
email = profile.get("email",""),
|
||||
first_name = profile.get("name",dn).get("givenName",""),
|
||||
last_name = profile.get("name",dn).get("familyName",""))
|
||||
self.mappings.Google = lambda profile, dn=dn:\
|
||||
dict(registration_id=profile.get("identifier",""),
|
||||
username=profile.get("preferredUsername",""),
|
||||
email=profile.get("email",""),
|
||||
first_name=profile.get("name",dn).get("givenName",""),
|
||||
last_name=profile.get("name",dn).get("familyName",""))
|
||||
self.mappings.default = lambda profile:\
|
||||
dict(registration_id=profile.get("identifier",""),
|
||||
username=profile.get("preferredUsername",""),
|
||||
email=profile.get("email",""),
|
||||
first_name=profile.get("preferredUsername",""),
|
||||
last_name='')
|
||||
|
||||
def get_user(self):
|
||||
request = self.request
|
||||
if request.vars.token:
|
||||
user = Storage()
|
||||
data = urllib.urlencode(dict(apiKey = self.api_key, token=request.vars.token))
|
||||
auth_info_json = fetch(self.auth_url+'?'+data)
|
||||
auth_info = json.loads(auth_info_json)
|
||||
|
||||
if auth_info['stat'] == 'ok':
|
||||
self.profile = auth_info['profile']
|
||||
provider = re.sub('[^\w\-]','',self.profile['providerName'])
|
||||
user = self.mappings.get(provider,self.mappings.default)(self.profile)
|
||||
return user
|
||||
elif self.on_login_failure:
|
||||
redirect(self.on_login_failure)
|
||||
return None
|
||||
|
||||
def login_form(self):
|
||||
request = self.request
|
||||
args = request.args
|
||||
if self.embed:
|
||||
JANRAIN_URL = \
|
||||
"https://%s.rpxnow.com/openid/embed?token_url=%s&language_preference=%s"
|
||||
rpxform = IFRAME(_src=JANRAIN_URL % (self.domain,self.token_url,self.language),
|
||||
_scrolling="no",
|
||||
_frameborder="no",
|
||||
_style="width:400px;height:240px;")
|
||||
else:
|
||||
JANRAIN_URL = \
|
||||
"https://%s.rpxnow.com/openid/v2/signin?token_url=%s"
|
||||
rpxform = DIV(SCRIPT(_src="https://rpxnow.com/openid/v2/widget",
|
||||
_type="text/javascript"),
|
||||
SCRIPT("RPXNOW.overlay = true;",
|
||||
"RPXNOW.language_preference = '%s';" % self.language,
|
||||
"RPXNOW.realm = '%s';" % self.domain,
|
||||
"RPXNOW.token_url = '%s';" % self.token_url,
|
||||
"RPXNOW.show();",
|
||||
_type="text/javascript"))
|
||||
return rpxform
|
||||
|
||||
def use_janrain(auth,filename='private/janrain.key',**kwargs):
|
||||
path = os.path.join(current.request.folder,filename)
|
||||
if os.path.exists(path):
|
||||
request = current.request
|
||||
domain,key = open(path,'r').read().strip().split(':')
|
||||
host = current.request.env.http_host
|
||||
url = "http://%s/%s/default/user/login" % (host,request.application)
|
||||
auth.settings.actions_disabled = \
|
||||
['register','change_password','request_reset_password']
|
||||
auth.settings.login_form = RPXAccount(
|
||||
request, api_key=key,domain=domain, url = url,**kwargs)
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Written by Michele Comitini <mcm@glisco.it>
|
||||
License: GPL v3
|
||||
|
||||
Adds support for x509 authentication.
|
||||
|
||||
"""
|
||||
|
||||
from gluon.globals import current
|
||||
from gluon.storage import Storage
|
||||
from gluon.http import HTTP,redirect
|
||||
|
||||
#requires M2Crypto
|
||||
from M2Crypto import X509
|
||||
|
||||
|
||||
|
||||
class X509Auth(object):
|
||||
"""
|
||||
Login using x509 cert from client.
|
||||
|
||||
from gluon.contrib.login_methods.x509_auth import X509Account
|
||||
auth.settings.actions_disabled=['register','change_password',
|
||||
'request_reset_password','profile']
|
||||
auth.settings.login_form = X509Account()
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.request = current.request
|
||||
self.ssl_client_raw_cert = self.request.env.ssl_client_raw_cert
|
||||
|
||||
# rebuild the certificate passed by the env
|
||||
# this is double work, but it is the only way
|
||||
# since we cannot access the web server ssl engine directly
|
||||
|
||||
if self.ssl_client_raw_cert:
|
||||
|
||||
x509=X509.load_cert_string(self.ssl_client_raw_cert, X509.FORMAT_PEM)
|
||||
# extract it from the cert
|
||||
self.serial = self.request.env.ssl_client_serial or ('%x' % x509.get_serial_number()).upper()
|
||||
|
||||
|
||||
subject = x509.get_subject()
|
||||
|
||||
# Reordering the subject map to a usable Storage map
|
||||
# this allows us a cleaner syntax:
|
||||
# cn = self.subject.cn
|
||||
self.subject = Storage(filter(None,
|
||||
map(lambda x:
|
||||
(x,map(lambda y:
|
||||
y.get_data().as_text(),
|
||||
subject.get_entries_by_nid(subject.nid[x]))),
|
||||
subject.nid.keys())))
|
||||
|
||||
|
||||
|
||||
def login_form(self, **args):
|
||||
raise HTTP(403,'Login not allowed. No valid x509 crentials')
|
||||
|
||||
|
||||
|
||||
def login_url(self, next="/"):
|
||||
raise HTTP(403,'Login not allowed. No valid x509 crentials')
|
||||
|
||||
|
||||
|
||||
|
||||
def logout_url(self, next="/"):
|
||||
return next
|
||||
|
||||
def get_user(self):
|
||||
'''Returns the user info contained in the certificate.
|
||||
'''
|
||||
|
||||
# We did not get the client cert?
|
||||
if not self.ssl_client_raw_cert:
|
||||
return None
|
||||
|
||||
# Try to reconstruct some useful info for web2py auth machinery
|
||||
|
||||
p = profile = dict()
|
||||
|
||||
username = p['username'] = self.subject.CN or self.subject.commonName
|
||||
p['first_name'] = self.subject.givenName or username
|
||||
p['last_name'] = self.subject.surname
|
||||
p['email'] = self.subject.Email or self.subject.emailAddress
|
||||
|
||||
# IMPORTANT WE USE THE CERT SERIAL AS UNIQUE KEY FOR THE USER
|
||||
p['registration_id'] = self.serial
|
||||
|
||||
# If the auth table has a field certificate it will be used to
|
||||
# save a PEM encoded copy of the user certificate.
|
||||
|
||||
p['certificate'] = self.ssl_client_raw_cert
|
||||
|
||||
return profile
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
markdown2.py is released under MIT license.
|
||||
@@ -0,0 +1,17 @@
|
||||
from markdown2 import *
|
||||
from gluon.html import XML
|
||||
|
||||
def WIKI(text, encoding="utf8", safe_mode='escape', html4tags=False, **attributes):
|
||||
if not text:
|
||||
test = ''
|
||||
if attributes.has_key('extras'):
|
||||
extras = attributes['extras']
|
||||
del attributes['extras']
|
||||
else:
|
||||
extras=None
|
||||
text = text.decode(encoding,'replace')
|
||||
|
||||
return XML(markdown(text,extras=extras,
|
||||
safe_mode=safe_mode, html4tags=html4tags)\
|
||||
.encode(encoding,'xmlcharrefreplace'),**attributes)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<html><body><h1>Markmin markup language</h1><h2>About</h2><p>This is a new markup language that we call markmin designed to produce high quality scientific papers and books and also put them online. We provide serializers for html, latex and pdf. It is implemented in the <code class="">markmin2html</code> function in the <code class="">markmin2html.py</code>.</p><p>Example of usage:</p><pre><code class="">>>> m = "Hello **world** [[link http://web2py.com]]"
|
||||
>>> from markmin2html import markmin2html
|
||||
>>> print markmin2html(m)
|
||||
>>> from markmin2latex import markmin2latex
|
||||
>>> print markmin2latex(m)
|
||||
>>> from markmin2pdf import markmin2pdf # requires pdflatex
|
||||
>>> print markmin2pdf(m)</code></pre><h2>Why?</h2><p>We wanted a markup language with the following requirements:</p><ul><li>less than 100 lines of functional code</li><li>easy to read</li><li>secure</li><li>support table, ul, ol, code</li><li>support html5 video and audio elements (html serialization only)</li><li>can align images and resize them</li><li>can specify class for tables and code elements</li><li>can add anchors</li><li>does not use _ for markup (since it creates odd behavior)</li><li>automatically links urls</li><li>fast</li><li>easy to extend</li><li>supports latex and pdf including references</li><li>allows to describe the markup in the markup (this document is generated from markmin syntax)</li></ul><p>(results depend on text but in average for text ~100K markmin is 30% faster than markdown, for text ~10K it is 10x faster)</p><p>The <a href="http://www.lulu.com/product/paperback/web2py-%283rd-edition%29/12822827">web2py book</a> published by lulu, for example, was entirely generated with markmin2pdf from the online <a href="http://www.web2py.com/book">web2py wiki</a></p><h2>Download</h2><ul><li>http://web2py.googlecode.com/hg/gluon/contrib/markmin/markmin2html.py</li><li>http://web2py.googlecode.com/hg/gluon/contrib/markmin/markmin2latex.py</li><li>http://web2py.googlecode.com/hg/gluon/contrib/markmin/markmin2pdf.py</li></ul><p>markmin2html.py and markmin2latex.py are single files and have no web2py dependence. Their license is BSD.</p><h2>Examples</h2><h3>Bold, italic, code and links</h3><table class=""><tr><td><b>SOURCE</b> </td><td><b>OUTPUT</b></td></tr><tr><td><code class=""># title</code> </td><td><b>title</b></td></tr><tr><td><code class="">## section</code> </td><td><b>section</b></td></tr><tr><td><code class="">### subsection</code> </td><td><b>subsection</b></td></tr><tr><td><code class="">**bold**</code> </td><td><b>bold</b></td></tr><tr><td><code class="">''italic''</code> </td><td><i>italic</i></td></tr><tr><td><code class="">``verbatim``</code> </td><td><code class="">verbatim</code></td></tr><tr><td><code class="">http://google.com</code> </td><td>http://google.com</td></tr><tr><td><code class="">[[click me #myanchor]]</code></td><td><a href="#myanchor">click me</a></td></tr></table>
|
||||
<h3>More on links</h3><p>The format is always <code class="">[[title link]]</code>. Notice you can nest bold, italic and code inside the link title.</p><h3>Anchors <span id="myanchor"><span></h3><p>You can place an anchor anywhere in the text using the syntax <code class="">[[name]]</code> where <i>name</i> is the name of the anchor.
|
||||
You can then link the anchor with <a href="#myanchor">link</a>, i.e. <code class="">[[link #myanchor]]</code>.</p><h3>Images</h3><p><img src="http://www.web2py.com/examples/static/web2py_logo.png" alt="some image" align="right" width="200px" />
|
||||
This paragraph has an image aligned to the right with a width of 200px. Its is placed using the code</p><p><code class="">[[some image http://www.web2py.com/examples/static/web2py_logo.png right 200px]]</code>.</p><h3>Unordered Lists</h3><pre><code class="">- Dog
|
||||
- Cat
|
||||
- Mouse</code></pre><p>is rendered as</p><ul><li>Dog</li><li>Cat</li><li>Mouse</li></ul><p>Two new lines between items break the list in two lists.</p><h3>Ordered Lists</h3><pre><code class="">+ Dog
|
||||
+ Cat
|
||||
+ Mouse</code></pre><p>is rendered as</p><ol><li>Dog</li><li>Cat</li><li>Mouse</li></ol><h3>Tables</h3><p>Something like this
|
||||
<pre><code class="">---------
|
||||
**A** | **B** | **C**
|
||||
0 | 0 | X
|
||||
0 | X | 0
|
||||
X | 0 | 0
|
||||
-----:abc</code></pre>
|
||||
is a table and is rendered as
|
||||
<table class="abc"><tr><td><b>A</b></td><td><b>B</b></td><td><b>C</b></td></tr><tr><td>0</td><td>0</td><td>X</td></tr><tr><td>0</td><td>X</td><td>0</td></tr><tr><td>X</td><td>0</td><td>0</td></tr></table>Four or more dashes delimit the table and | separates the columns.
|
||||
The <code class="">:abc</code> at the end sets the class for the table and it is optional.</p><h3>Blockquote</h3><p>A table with a single cell is rendered as a blockquote:</p><blockquote class="">Hello world</blockquote>
|
||||
<h3>Code, <code class=""><code></code>, escaping and extra stuff</h3><pre><code class="python">def test():
|
||||
return "this is Python code"</code></pre><p>Optionally a ` inside a <code class="">``...``</code> block can be inserted escaped with !`!.
|
||||
The <code class="">:python</code> after the markup is also optional. If present, by default, it is used to set the class of the <code> block.
|
||||
The behavior can be overridden by passing an argument <code class="">extra</code> to the <code class="">render</code> function. For example:</p><pre><code class="python">>>> markmin2html("``aaa``:custom",
|
||||
extra=dict(custom=lambda text: 'x'+text+'x'))</code></pre><p>generates</p><code class="python">'xaaax'</code><p>(the <code class="">``...``:custom</code> block is rendered by the <code class="">custom=lambda</code> function passed to <code class="">render</code>).</p><h3>Html5 support</h3><p>Markmin also supports the <video> and <audio> html5 tags using the notation:
|
||||
<pre><code class="">[[title link video]]
|
||||
[[title link audio]]</code></pre></p><h3>Latex</h3><p>Formulas can be embedded into HTML with <code class="">$</code><code class="">$</code>formula<code class="">$</code><code class="">$</code>.
|
||||
You can use Google charts to render the formula:</p><pre><code class="">>>> LATEX = '<img src="http://chart.apis.google.com/chart?cht=tx&chl=%s" align="center"/>'
|
||||
>>> markmin2html(text,{'latex':lambda code: LATEX % code.replace('"','"')})</code></pre><h3>Citations and References</h3><p>Citations are treated as internal links in html and proper citations in latex if there is a final section called "References". Items like</p><pre><code class="">- [[key]] value</code></pre><p>in the References will be translated into Latex</p><pre><code class="">\bibitem{key} value</code></pre><p>Here is an example of usage:</p><pre><code class="">As shown in Ref.``mdipierro``:cite
|
||||
|
||||
## References
|
||||
- [[mdipierro]] web2py Manual, 3rd Edition, lulu.com</code></pre><h3>Caveats</h3><p><code class=""><ul/></code>, <code class=""><ol/></code>, <code class=""><code/></code>, <code class=""><table/></code>, <code class=""><blockquote/></code>, <code class=""><h1/></code>, ..., <code class=""><h6/></code> do not have <code class=""><p>...</p></code> around them.</p></body></html>
|
||||
Binary file not shown.
Executable
+463
@@ -0,0 +1,463 @@
|
||||
#!/usr/bin/env python
|
||||
# created my Massimo Di Pierro
|
||||
# license MIT/BSD/GPL
|
||||
import re
|
||||
import cgi
|
||||
|
||||
__all__ = ['render', 'markmin2html']
|
||||
|
||||
__doc__ = """
|
||||
# Markmin markup language
|
||||
|
||||
## About
|
||||
|
||||
This is a new markup language that we call markmin designed to produce high quality scientific papers and books and also put them online. We provide serializers for html, latex and pdf. It is implemented in the ``markmin2html`` function in the ``markmin2html.py``.
|
||||
|
||||
Example of usage:
|
||||
|
||||
``
|
||||
m = "Hello **world** [[link http://web2py.com]]"
|
||||
from markmin2html import markmin2html
|
||||
print markmin2html(m)
|
||||
from markmin2latex import markmin2latex
|
||||
print markmin2latex(m)
|
||||
from markmin2pdf import markmin2pdf # requires pdflatex
|
||||
print markmin2pdf(m)
|
||||
``
|
||||
|
||||
## Why?
|
||||
|
||||
We wanted a markup language with the following requirements:
|
||||
- less than 100 lines of functional code
|
||||
- easy to read
|
||||
- secure
|
||||
- support table, ul, ol, code
|
||||
- support html5 video and audio elements (html serialization only)
|
||||
- can align images and resize them
|
||||
- can specify class for tables and code elements
|
||||
- can add anchors
|
||||
- does not use _ for markup (since it creates odd behavior)
|
||||
- automatically links urls
|
||||
- fast
|
||||
- easy to extend
|
||||
- supports latex and pdf including references
|
||||
- allows to describe the markup in the markup (this document is generated from markmin syntax)
|
||||
|
||||
(results depend on text but in average for text ~100K markmin is 30% faster than markdown, for text ~10K it is 10x faster)
|
||||
|
||||
The [[web2py book http://www.lulu.com/product/paperback/web2py-%283rd-edition%29/12822827]] published by lulu, for example, was entirely generated with markmin2pdf from the online [[web2py wiki http://www.web2py.com/book]]
|
||||
|
||||
## Download
|
||||
|
||||
- http://web2py.googlecode.com/hg/gluon/contrib/markmin/markmin2html.py
|
||||
- http://web2py.googlecode.com/hg/gluon/contrib/markmin/markmin2latex.py
|
||||
- http://web2py.googlecode.com/hg/gluon/contrib/markmin/markmin2pdf.py
|
||||
|
||||
markmin2html.py and markmin2latex.py are single files and have no web2py dependence. Their license is BSD.
|
||||
|
||||
## Examples
|
||||
|
||||
### Bold, italic, code and links
|
||||
|
||||
--------------------------------------------------
|
||||
**SOURCE** | **OUTPUT**
|
||||
``# title`` | **title**
|
||||
``## section`` | **section**
|
||||
``### subsection`` | **subsection**
|
||||
``**bold**`` | **bold**
|
||||
``''italic''`` | ''italic''
|
||||
``!`!`verbatim`!`!`` | ``verbatim``
|
||||
``http://google.com`` | http://google.com
|
||||
``[[click me #myanchor]]`` | [[click me #myanchor]]
|
||||
---------------------------------------------------
|
||||
|
||||
### More on links
|
||||
|
||||
The format is always ``[[title link]]``. Notice you can nest bold, italic and code inside the link title.
|
||||
|
||||
### Anchors [[myanchor]]
|
||||
|
||||
You can place an anchor anywhere in the text using the syntax ``[[name]]`` where ''name'' is the name of the anchor.
|
||||
You can then link the anchor with [[link #myanchor]], i.e. ``[[link #myanchor]]``.
|
||||
|
||||
### Images
|
||||
|
||||
[[some image http://www.web2py.com/examples/static/web2py_logo.png right 200px]]
|
||||
This paragraph has an image aligned to the right with a width of 200px. Its is placed using the code
|
||||
|
||||
``[[some image http://www.web2py.com/examples/static/web2py_logo.png right 200px]]``.
|
||||
|
||||
### Unordered Lists
|
||||
|
||||
``
|
||||
- Dog
|
||||
- Cat
|
||||
- Mouse
|
||||
``
|
||||
|
||||
is rendered as
|
||||
- Dog
|
||||
- Cat
|
||||
- Mouse
|
||||
|
||||
Two new lines between items break the list in two lists.
|
||||
|
||||
### Ordered Lists
|
||||
|
||||
``
|
||||
+ Dog
|
||||
+ Cat
|
||||
+ Mouse
|
||||
``
|
||||
|
||||
is rendered as
|
||||
+ Dog
|
||||
+ Cat
|
||||
+ Mouse
|
||||
|
||||
|
||||
### Tables
|
||||
|
||||
Something like this
|
||||
``
|
||||
---------
|
||||
**A** | **B** | **C**
|
||||
0 | 0 | X
|
||||
0 | X | 0
|
||||
X | 0 | 0
|
||||
-----:abc
|
||||
``
|
||||
is a table and is rendered as
|
||||
---------
|
||||
**A** | **B** | **C**
|
||||
0 | 0 | X
|
||||
0 | X | 0
|
||||
X | 0 | 0
|
||||
-----:abc
|
||||
Four or more dashes delimit the table and | separates the columns.
|
||||
The ``:abc`` at the end sets the class for the table and it is optional.
|
||||
|
||||
### Blockquote
|
||||
|
||||
A table with a single cell is rendered as a blockquote:
|
||||
|
||||
-----
|
||||
Hello world
|
||||
-----
|
||||
|
||||
### Code, ``<code>``, escaping and extra stuff
|
||||
|
||||
``
|
||||
def test():
|
||||
return "this is Python code"
|
||||
``:python
|
||||
|
||||
Optionally a ` inside a ``!`!`...`!`!`` block can be inserted escaped with !`!.
|
||||
The ``:python`` after the markup is also optional. If present, by default, it is used to set the class of the <code> block.
|
||||
The behavior can be overridden by passing an argument ``extra`` to the ``render`` function. For example:
|
||||
|
||||
``
|
||||
markmin2html("!`!!`!aaa!`!!`!:custom",
|
||||
extra=dict(custom=lambda text: 'x'+text+'x'))
|
||||
``:python
|
||||
|
||||
generates
|
||||
|
||||
``'xaaax'``:python
|
||||
|
||||
(the ``!`!`...`!`!:custom`` block is rendered by the ``custom=lambda`` function passed to ``render``).
|
||||
|
||||
|
||||
### Html5 support
|
||||
|
||||
Markmin also supports the <video> and <audio> html5 tags using the notation:
|
||||
``
|
||||
[[title link video]]
|
||||
[[title link audio]]
|
||||
``
|
||||
|
||||
### Latex and other extensions
|
||||
|
||||
Formulas can be embedded into HTML with ``$````$``formula``$````$``.
|
||||
You can use Google charts to render the formula:
|
||||
|
||||
``
|
||||
LATEX = '<img src="http://chart.apis.google.com/chart?cht=tx&chl=%s" />'
|
||||
markmin2html(text,{'latex':lambda code: LATEX % code.replace('"','\"')})
|
||||
``
|
||||
|
||||
### Code with syntax highlighting
|
||||
|
||||
This requires a syntax highlighting tool, such as the web2py CODE helper.
|
||||
|
||||
``
|
||||
extra={'code_cpp':lambda text: CODE(text,language='cpp').xml(),
|
||||
'code_java':lambda text: CODE(text,language='java').xml(),
|
||||
'code_python':lambda text: CODE(text,language='python').xml(),
|
||||
'code_html':lambda text: CODE(text,language='html').xml()}
|
||||
markmin2html(text,extra=extra)
|
||||
``
|
||||
|
||||
Code can now be marked up as in this example:
|
||||
|
||||
``
|
||||
!`!`
|
||||
<html><body>example</body></html>
|
||||
!`!`:code_html
|
||||
``
|
||||
|
||||
### Citations and References
|
||||
|
||||
Citations are treated as internal links in html and proper citations in latex if there is a final section called "References". Items like
|
||||
|
||||
``
|
||||
- [[key]] value
|
||||
``
|
||||
|
||||
in the References will be translated into Latex
|
||||
|
||||
``
|
||||
\\bibitem{key} value
|
||||
``
|
||||
|
||||
Here is an example of usage:
|
||||
|
||||
``
|
||||
As shown in Ref.!`!`mdipierro`!`!:cite
|
||||
|
||||
## References
|
||||
- [[mdipierro]] web2py Manual, 3rd Edition, lulu.com
|
||||
``
|
||||
|
||||
### Caveats
|
||||
``<ul/>``, ``<ol/>``, ``<code/>``, ``<table/>``, ``<blockquote/>``, ``<h1/>``, ..., ``<h6/>`` do not have ``<p>...</p>`` around them.
|
||||
|
||||
"""
|
||||
|
||||
META = 'META'
|
||||
LATEX = '<img src="http://chart.apis.google.com/chart?cht=tx&chl=%s" />'
|
||||
regex_newlines = re.compile('(\n\r)|(\r\n)')
|
||||
regex_dd=re.compile('\$\$(?P<latex>.*?)\$\$')
|
||||
regex_code = re.compile('('+META+')|(``(?P<t>.*?)``(:(?P<c>\w+))?)',re.S)
|
||||
regex_maps = [
|
||||
(re.compile('[ \t\r]+\n'),'\n'),
|
||||
(re.compile('[ \t\r]+\n'),'\n'),
|
||||
(re.compile('\*\*(?P<t>[^\s\*]+( +[^\s\*]+)*)\*\*'),'<b>\g<t></b>'),
|
||||
(re.compile("''(?P<t>[^\s']+( +[^\s']+)*)''"),'<i>\g<t></i>'),
|
||||
(re.compile('^#{6} (?P<t>[^\n]+)',re.M),'\n\n<<h6>\g<t></h6>\n'),
|
||||
(re.compile('^#{5} (?P<t>[^\n]+)',re.M),'\n\n<<h5>\g<t></h5>\n'),
|
||||
(re.compile('^#{4} (?P<t>[^\n]+)',re.M),'\n\n<<h4>\g<t></h4>\n'),
|
||||
(re.compile('^#{3} (?P<t>[^\n]+)',re.M),'\n\n<<h3>\g<t></h3>\n'),
|
||||
(re.compile('^#{2} (?P<t>[^\n]+)',re.M),'\n\n<<h2>\g<t></h2>\n'),
|
||||
(re.compile('^#{1} (?P<t>[^\n]+)',re.M),'\n\n<<h1>\g<t></h1>\n'),
|
||||
(re.compile('^\- +(?P<t>.*)',re.M),'<<ul><li>\g<t></li></ul>'),
|
||||
(re.compile('^\+ +(?P<t>.*)',re.M),'<<ol><li>\g<t></li></ol>'),
|
||||
(re.compile('</ol>\n<<ol>'),''),
|
||||
(re.compile('</ul>\n<<ul>'),''),
|
||||
(re.compile('<<'),'\n\n<<'),
|
||||
(re.compile('\n\s+\n'),'\n\n')]
|
||||
regex_table = re.compile('^\-{4,}\n(?P<t>.*?)\n\-{4,}(:(?P<c>\w+))?\n',re.M|re.S)
|
||||
regex_anchor = re.compile('\[\[(?P<t>\S+)\]\]')
|
||||
regex_image_center_width = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+) +center +(?P<w>\d+px)\]\]')
|
||||
regex_image_width = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+) +(?P<p>left|right) +(?P<w>\d+px)\]\]')
|
||||
regex_image_center = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+) +center\]\]')
|
||||
regex_image = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+) +(?P<p>left|right|center)\]\]')
|
||||
regex_video = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+) +video\]\]')
|
||||
regex_audio = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+) +audio\]\]')
|
||||
regex_link = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+)\]\]')
|
||||
regex_link_popup = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+) popup\]\]')
|
||||
regex_link_no_anchor = re.compile('\[\[ +(?P<k>\S+)\]\]')
|
||||
regex_auto = re.compile('(?<!["\w\>])(?P<k>\w+://[\w\.\-\+\?&%\/]+)',re.M)
|
||||
|
||||
def render(text,extra={},allowed={},sep='p'):
|
||||
"""
|
||||
Arguments:
|
||||
- text is the text to be processed
|
||||
- extra is a dict like extra=dict(custom=lambda value: value) that process custom code
|
||||
as in " ``this is custom code``:custom "
|
||||
- allowed is a dictionary of list of allowed classes like
|
||||
allowed = dict(code=('python','cpp','java'))
|
||||
- sep can be 'p' to separate text in <p>...</p>
|
||||
or can be 'br' to separate text using <br />
|
||||
|
||||
|
||||
>>> render('this is\\n# a section\\nparagraph')
|
||||
'<p>this is</p><h1>a section</h1><p>paragraph</p>'
|
||||
>>> render('this is\\n## a subsection\\nparagraph')
|
||||
'<p>this is</p><h2>a subsection</h2><p>paragraph</p>'
|
||||
>>> render('this is\\n### a subsubsection\\nparagraph')
|
||||
'<p>this is</p><h3>a subsubsection</h3><p>paragraph</p>'
|
||||
>>> render('**hello world**')
|
||||
'<p><b>hello world</b></p>'
|
||||
>>> render('``hello world``')
|
||||
'<code class="">hello world</code>'
|
||||
>>> render('``hello world``:python')
|
||||
'<code class="python">hello world</code>'
|
||||
>>> render('``\\nhello\\nworld\\n``:python')
|
||||
'<pre><code class="python">hello\\nworld</code></pre>'
|
||||
>>> render("''hello world''")
|
||||
'<p><i>hello world</i></p>'
|
||||
>>> render('** hello** **world**')
|
||||
'<p>** hello** <b>world</b></p>'
|
||||
|
||||
>>> render('- this\\n- is\\n- a list\\n\\nand this\\n- is\\n- another')
|
||||
'<ul><li>this</li><li>is</li><li>a list</li></ul><p>and this</p><ul><li>is</li><li>another</li></ul>'
|
||||
|
||||
>>> render('+ this\\n+ is\\n+ a list\\n\\nand this\\n+ is\\n+ another')
|
||||
'<ol><li>this</li><li>is</li><li>a list</li></ol><p>and this</p><ol><li>is</li><li>another</li></ol>'
|
||||
|
||||
>>> render("----\\na | b\\nc | d\\n----\\n")
|
||||
'<table class=""><tr><td>a</td><td>b</td></tr><tr><td>c</td><td>d</td></tr></table>'
|
||||
|
||||
>>> render("----\\nhello world\\n----\\n")
|
||||
'<blockquote class="">hello world</blockquote>'
|
||||
|
||||
>>> render('[[this is a link http://example.com]]')
|
||||
'<p><a href="http://example.com">this is a link</a></p>'
|
||||
|
||||
>>> render('[[this is an image http://example.com left]]')
|
||||
'<p><img src="http://example.com" alt="this is an image" align="left" /></p>'
|
||||
>>> render('[[this is an image http://example.com left 200px]]')
|
||||
'<p><img src="http://example.com" alt="this is an image" align="left" width="200px" /></p>'
|
||||
|
||||
>>> render('[[this is an image http://example.com video]]')
|
||||
'<p><video src="http://example.com" controls></video></p>'
|
||||
>>> render('[[this is an image http://example.com audio]]')
|
||||
'<p><audio src="http://example.com" controls></audio></p>'
|
||||
|
||||
>>> render('[[this is a **link** http://example.com]]')
|
||||
'<p><a href="http://example.com">this is a <b>link</b></a></p>'
|
||||
|
||||
>>> render("``aaa``:custom",extra=dict(custom=lambda text: 'x'+text+'x'))
|
||||
'xaaax'
|
||||
|
||||
>>> print render(r"$$\int_a^b sin(x)dx$$")
|
||||
<img src="http://chart.apis.google.com/chart?cht=tx&chl=\\int_a^b sin(x)dx" />
|
||||
"""
|
||||
text = str(text or '')
|
||||
#############################################################
|
||||
# replace all blocks marked with ``...``:class with META
|
||||
# store them into segments they will be treated as code
|
||||
#############################################################
|
||||
segments, i = [], 0
|
||||
text = regex_dd.sub('``\g<latex>``:latex ',text)
|
||||
text = regex_newlines.sub('\n',text)
|
||||
while True:
|
||||
item = regex_code.search(text,i)
|
||||
if not item: break
|
||||
if item.group()==META:
|
||||
segments.append((None,None))
|
||||
text = text[:item.start()]+META+text[item.end():]
|
||||
else:
|
||||
c = item.group('c') or ''
|
||||
if 'code' in allowed and not c in allowed['code']: c = ''
|
||||
code = item.group('t').replace('!`!','`')
|
||||
segments.append((code,c))
|
||||
text = text[:item.start()]+META+text[item.end():]
|
||||
i=item.start()+3
|
||||
|
||||
#############################################################
|
||||
# do h1,h2,h3,h4,h5,h6,b,i,ol,ul and normalize spaces
|
||||
#############################################################
|
||||
text = '\n'.join(t.strip() for t in text.split('\n'))
|
||||
text = cgi.escape(text)
|
||||
for regex, sub in regex_maps:
|
||||
text = regex.sub(sub,text)
|
||||
|
||||
#############################################################
|
||||
# process tables and blockquotes
|
||||
#############################################################
|
||||
while True:
|
||||
item = regex_table.search(text)
|
||||
if not item: break
|
||||
c = item.group('c') or ''
|
||||
if 'table' in allowed and not c in allowed['table']: c = ''
|
||||
content = item.group('t')
|
||||
if ' | ' in content:
|
||||
rows = content.replace('\n','</td></tr><tr><td>').replace(' | ','</td><td>')
|
||||
text = text[:item.start()] + '<<table class="%s"><tr><td>'%c + rows + '</td></tr></table>' + text[item.end():]
|
||||
else:
|
||||
text = text[:item.start()] + '<<blockquote class="%s">'%c + content + '</blockquote>' + text[item.end():]
|
||||
|
||||
#############################################################
|
||||
# deal with images, videos, audios and links
|
||||
#############################################################
|
||||
|
||||
text = regex_anchor.sub('<span id="\g<t>"><span>', text)
|
||||
text = regex_image_center_width.sub('<p align="center"><img src="\g<k>" alt="\g<t>" width="\g<w>" /></p>', text)
|
||||
text = regex_image_width.sub('<img src="\g<k>" alt="\g<t>" align="\g<p>" width="\g<w>" />', text)
|
||||
text = regex_image_center.sub('<p align="center"><img src="\g<k>" alt="\g<t>" /></p>', text)
|
||||
text = regex_image.sub('<img src="\g<k>" alt="\g<t>" align="\g<p>" />', text)
|
||||
text = regex_video.sub('<video src="\g<k>" controls></video>', text)
|
||||
text = regex_audio.sub('<audio src="\g<k>" controls></audio>', text)
|
||||
text = regex_link_popup.sub('<a href="\g<k>" target="_blank">\g<t></a>', text)
|
||||
text = regex_link_no_anchor.sub('<a href="\g<k>">\g<k></a>', text)
|
||||
text = regex_link.sub('<a href="\g<k>">\g<t></a>', text)
|
||||
text = regex_auto.sub('<a href="\g<k>">\g<k></a>', text)
|
||||
|
||||
#############################################################
|
||||
# deal with paragraphs (trick <<ul, <<ol, <<table, <<h1, etc)
|
||||
# the << indicates that there should NOT be a new paragraph
|
||||
# META indicates a code block therefore no new paragraph
|
||||
#############################################################
|
||||
items = [item.strip() for item in text.split('\n\n')]
|
||||
if sep=='p':
|
||||
text = ''.join(
|
||||
(p[:2]!='<<' and p!=META and '<p>%s</p>'%p or '%s'%p) \
|
||||
for p in items if p.strip())
|
||||
elif sep=='br':
|
||||
text = '<br />'.join(items)
|
||||
|
||||
#############################################################
|
||||
# finally get rid of <<
|
||||
#############################################################
|
||||
text=text.replace('<<','<')
|
||||
|
||||
#############################################################
|
||||
# process all code text
|
||||
#############################################################
|
||||
parts = text.split(META)
|
||||
text = parts[0]
|
||||
for i,(code,b) in enumerate(segments):
|
||||
if code==None:
|
||||
html = META
|
||||
else:
|
||||
if b in extra:
|
||||
if code[:1]=='\n': code=code[1:]
|
||||
if code[-1:]=='\n': code=code[:-1]
|
||||
html = extra[b](code)
|
||||
elif b=='cite':
|
||||
html = '['+','.join('<a href="#%s" class="%s">%s</a>' \
|
||||
% (d,b,d) \
|
||||
for d in cgi.escape(code).split(','))+']'
|
||||
elif b=='latex':
|
||||
html = LATEX % code.replace('"','\"').replace('\n',' ')
|
||||
elif code[:1]=='\n' or code[-1:]=='\n':
|
||||
if code[:1]=='\n': code=code[1:]
|
||||
if code[-1:]=='\n': code=code[:-1]
|
||||
html = '<pre><code class="%s">%s</code></pre>' % (b,cgi.escape(code))
|
||||
else:
|
||||
if code[:1]=='\n': code=code[1:]
|
||||
if code[-1:]=='\n': code=code[:-1]
|
||||
html = '<code class="%s">%s</code>' % (b,cgi.escape(code))
|
||||
text = text+html+parts[i+1]
|
||||
return text
|
||||
|
||||
|
||||
def markmin2html(text,extra={},allowed={},sep='p'):
|
||||
return render(text,extra,allowed,sep)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
import doctest
|
||||
if sys.argv[1:2]==['-h']:
|
||||
print '<html><body>'+markmin2html(__doc__)+'</body></html>'
|
||||
elif len(sys.argv)>1:
|
||||
fargv = open(sys.argv[1],'r')
|
||||
try:
|
||||
print '<html><body>'+markmin2html(fargv.read())+'</body></html>'
|
||||
finally:
|
||||
fargv.close()
|
||||
else:
|
||||
doctest.testmod()
|
||||
|
||||
Executable
+285
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python
|
||||
# created my Massimo Di Pierro
|
||||
# license MIT/BSD/GPL
|
||||
import re
|
||||
import cgi
|
||||
import sys
|
||||
import doctest
|
||||
from optparse import OptionParser
|
||||
|
||||
__all__ = ['render','markmin2latex']
|
||||
|
||||
META = 'META'
|
||||
regex_newlines = re.compile('(\n\r)|(\r\n)')
|
||||
regex_dd=re.compile('\$\$(?P<latex>.*?)\$\$')
|
||||
regex_code = re.compile('('+META+')|(``(?P<t>.*?)``(:(?P<c>\w+))?)',re.S)
|
||||
regex_title = re.compile('^#{1} (?P<t>[^\n]+)',re.M)
|
||||
regex_maps = [
|
||||
(re.compile('[ \t\r]+\n'),'\n'),
|
||||
(re.compile('[ \t\r]+\n'),'\n'),
|
||||
(re.compile('\*\*(?P<t>[^\s\*]+( +[^\s\*]+)*)\*\*'),'{\\\\bf \g<t>}'),
|
||||
(re.compile("''(?P<t>[^\s']+( +[^\s']+)*)''"),'{\\it \g<t>}'),
|
||||
(re.compile('^#{6} (?P<t>[^\n]+)',re.M),'\n\n{\\\\bf \g<t>}\n'),
|
||||
(re.compile('^#{5} (?P<t>[^\n]+)',re.M),'\n\n{\\\\bf \g<t>}\n'),
|
||||
(re.compile('^#{4} (?P<t>[^\n]+)',re.M),'\n\n\\\\goodbreak\\subsubsection{\g<t>}\n'),
|
||||
(re.compile('^#{3} (?P<t>[^\n]+)',re.M),'\n\n\\\\goodbreak\\subsection{\g<t>}\n'),
|
||||
(re.compile('^#{2} (?P<t>[^\n]+)',re.M),'\n\n\\\\goodbreak\\section{\g<t>}\n'),
|
||||
(re.compile('^#{1} (?P<t>[^\n]+)',re.M),''),
|
||||
(re.compile('^\- +(?P<t>.*)',re.M),'\\\\begin{itemize}\n\\item \g<t>\n\\end{itemize}'),
|
||||
(re.compile('^\+ +(?P<t>.*)',re.M),'\\\\begin{itemize}\n\\item \g<t>\n\\end{itemize}'),
|
||||
(re.compile('\\\\end\{itemize\}\s+\\\\begin\{itemize\}'),'\n'),
|
||||
(re.compile('\n\s+\n'),'\n\n')]
|
||||
regex_table = re.compile('^\-{4,}\n(?P<t>.*?)\n\-{4,}(:(?P<c>\w+))?\n',re.M|re.S)
|
||||
|
||||
regex_anchor = re.compile('\[\[(?P<t>\S+)\]\]')
|
||||
regex_bibitem = re.compile('\-\s*\[\[(?P<t>\S+)\]\]')
|
||||
regex_image_width = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+) +(?P<p>left|right|center) +(?P<w>\d+px)\]\]')
|
||||
regex_image = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+) +(?P<p>left|right|center)\]\]')
|
||||
#regex_video = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+) +video\]\]')
|
||||
#regex_audio = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+) +audio\]\]')
|
||||
regex_link = re.compile('\[\[(?P<t>[^\]]*?) +(?P<k>\S+)\]\]')
|
||||
regex_auto = re.compile('(?<!["\w])(?P<k>\w+://[\w\.\-\?&%]+)',re.M)
|
||||
regex_commas = re.compile('[ ]+(?P<t>[,;\.])')
|
||||
regex_noindent = re.compile('\n\n(?P<t>[a-z])')
|
||||
regex_quote_left = re.compile('"(?=\w)')
|
||||
regex_quote_right = re.compile('(?=\w\.)"')
|
||||
|
||||
def latex_escape(text,pound=True):
|
||||
text=text.replace('\\','{\\textbackslash}')
|
||||
for c in '^_&$%{}': text=text.replace(c,'\\'+c)
|
||||
text=text.replace('\\{\\textbackslash\\}','{\\textbackslash}')
|
||||
if pound: text=text.replace('#','\\#')
|
||||
return text
|
||||
|
||||
def render(text,extra={},allowed={},sep='p',image_mapper=lambda x:x):
|
||||
#############################################################
|
||||
# replace all blocks marked with ``...``:class with META
|
||||
# store them into segments they will be treated as code
|
||||
#############################################################
|
||||
text = str(text or '')
|
||||
segments, i = [], 0
|
||||
text = regex_dd.sub('``\g<latex>``:latex ',text)
|
||||
text = regex_newlines.sub('\n',text)
|
||||
while True:
|
||||
item = regex_code.search(text,i)
|
||||
if not item: break
|
||||
if item.group()==META:
|
||||
segments.append((None,None))
|
||||
text = text[:item.start()]+META+text[item.end():]
|
||||
else:
|
||||
c = item.group('c') or ''
|
||||
if 'code' in allowed and not c in allowed['code']: c = ''
|
||||
code = item.group('t').replace('!`!','`')
|
||||
segments.append((code,c))
|
||||
text = text[:item.start()]+META+text[item.end():]
|
||||
i=item.start()+3
|
||||
|
||||
|
||||
#############################################################
|
||||
# do h1,h2,h3,h4,h5,h6,b,i,ol,ul and normalize spaces
|
||||
#############################################################
|
||||
|
||||
title = regex_title.search(text)
|
||||
if not title: title='Title'
|
||||
else: title=title.group('t')
|
||||
|
||||
text = latex_escape(text,pound=False)
|
||||
|
||||
texts = text.split('## References',1)
|
||||
text = regex_anchor.sub('\\label{\g<t>}', texts[0])
|
||||
if len(texts)==2:
|
||||
text += '\n\\begin{thebibliography}{999}\n'
|
||||
text += regex_bibitem.sub('\n\\\\bibitem{\g<t>}', texts[1])
|
||||
text += '\n\\end{thebibliography}\n'
|
||||
|
||||
text = '\n'.join(t.strip() for t in text.split('\n'))
|
||||
for regex, sub in regex_maps:
|
||||
text = regex.sub(sub,text)
|
||||
text=text.replace('#','\\#')
|
||||
text=text.replace('`',"'")
|
||||
|
||||
#############################################################
|
||||
# process tables and blockquotes
|
||||
#############################################################
|
||||
while True:
|
||||
item = regex_table.search(text)
|
||||
if not item: break
|
||||
c = item.group('c') or ''
|
||||
if 'table' in allowed and not c in allowed['table']: c = ''
|
||||
content = item.group('t')
|
||||
if ' | ' in content:
|
||||
rows = content.replace('\n','\\\\\n').replace(' | ',' & ')
|
||||
row0,row2 = rows.split('\\\\\n',1)
|
||||
cols=row0.count(' & ')+1
|
||||
cal='{'+''.join('l' for j in range(cols))+'}'
|
||||
tabular = '\\begin{center}\n{\\begin{tabular}'+cal+'\\hline\n' + row0+'\\\\ \\hline\n'+row2 + ' \\\\ \\hline\n\\end{tabular}}\n\\end{center}'
|
||||
if row2.count('\n')>20: tabular='\\newpage\n'+tabular
|
||||
text = text[:item.start()] + tabular + text[item.end():]
|
||||
else:
|
||||
text = text[:item.start()] + '\\begin{quote}' + content + '\\end{quote}' + text[item.end():]
|
||||
|
||||
#############################################################
|
||||
# deal with images, videos, audios and links
|
||||
#############################################################
|
||||
|
||||
def sub(x):
|
||||
f=image_mapper(x.group('k'))
|
||||
if not f: return None
|
||||
return '\n\\begin{center}\\includegraphics[width=8cm]{%s}\\end{center}\n' % (f)
|
||||
text = regex_image_width.sub(sub,text)
|
||||
text = regex_image.sub(sub,text)
|
||||
|
||||
text = regex_link.sub('{\\\\footnotesize\\href{\g<k>}{\g<t>}}', text)
|
||||
text = regex_commas.sub('\g<t>',text)
|
||||
text = regex_noindent.sub('\n\\\\noindent \g<t>',text)
|
||||
|
||||
### fix paths in images
|
||||
regex=re.compile('\\\\_[\w_]*\.(eps|png|jpg|gif)')
|
||||
while True:
|
||||
match=regex.search(text)
|
||||
if not match: break
|
||||
text=text[:match.start()]+text[match.start()+1:]
|
||||
text = regex_quote_left.sub('``',text)
|
||||
text = regex_quote_right.sub("''",text)
|
||||
|
||||
#############################################################
|
||||
# process all code text
|
||||
#############################################################
|
||||
parts = text.split(META)
|
||||
text = parts[0]
|
||||
authors = []
|
||||
for i,(code,b) in enumerate(segments):
|
||||
if code==None:
|
||||
html = META
|
||||
else:
|
||||
if b=='hidden':
|
||||
html=''
|
||||
elif b=='author':
|
||||
author = latex_escape(code.strip())
|
||||
authors.append(author)
|
||||
html=''
|
||||
elif b=='inxx':
|
||||
html='\inxx{%s}' % latex_escape(code)
|
||||
elif b=='cite':
|
||||
html='~\cite{%s}' % latex_escape(code.strip())
|
||||
elif b=='ref':
|
||||
html='~\ref{%s}' % latex_escape(code.strip())
|
||||
elif b=='latex':
|
||||
if '\n' in code:
|
||||
html='\n\\begin{equation}\n%s\n\\end{equation}\n' % code.strip()
|
||||
else:
|
||||
html='$%s$' % code.strip()
|
||||
elif b=='latex_eqnarray':
|
||||
code=code.strip()
|
||||
code='\\\\'.join(x.replace('=','&=&',1) for x in code.split('\\\\'))
|
||||
html='\n\\begin{eqnarray}\n%s\n\\end{eqnarray}\n' % code
|
||||
elif b.startswith('latex_'):
|
||||
key=b[6:]
|
||||
html='\\begin{%s}%s\\end{%s}' % (key,code,key)
|
||||
elif b in extra:
|
||||
if code[:1]=='\n': code=code[1:]
|
||||
if code[-1:]=='\n': code=code[:-1]
|
||||
html = extra[b](code)
|
||||
elif code[:1]=='\n' or code[:-1]=='\n':
|
||||
if code[:1]=='\n': code=code[1:]
|
||||
if code[-1:]=='\n': code=code[:-1]
|
||||
if code.startswith('<') or code.startswith('{{') or code.startswith('http'):
|
||||
html = '\\begin{lstlisting}[keywords={}]\n%s\n\\end{lstlisting}' % code
|
||||
else:
|
||||
html = '\\begin{lstlisting}\n%s\n\\end{lstlisting}' % code
|
||||
else:
|
||||
if code[:1]=='\n': code=code[1:]
|
||||
if code[-1:]=='\n': code=code[:-1]
|
||||
html = '{\\ft %s}' % latex_escape(code)
|
||||
try:
|
||||
text = text+html+parts[i+1]
|
||||
except:
|
||||
text = text + '... WIKI PROCESSING ERROR ...'
|
||||
break
|
||||
text = text.replace(' ~\\cite','~\\cite')
|
||||
return text, title, authors
|
||||
|
||||
WRAPPER = """
|
||||
\\documentclass[12pt]{article}
|
||||
\\usepackage{hyperref}
|
||||
\\usepackage{listings}
|
||||
\\usepackage{upquote}
|
||||
\\usepackage{color}
|
||||
\\usepackage{graphicx}
|
||||
\\usepackage{grffile}
|
||||
\\usepackage[utf8x]{inputenc}
|
||||
\\definecolor{lg}{rgb}{0.9,0.9,0.9}
|
||||
\\definecolor{dg}{rgb}{0.3,0.3,0.3}
|
||||
\\def\\ft{\\small\\tt}
|
||||
\\lstset{
|
||||
basicstyle=\\footnotesize,
|
||||
breaklines=true, basicstyle=\\ttfamily\\color{black}\\footnotesize,
|
||||
keywordstyle=\\bf\\ttfamily,
|
||||
commentstyle=\\it\\ttfamily,
|
||||
stringstyle=\\color{dg}\\it\\ttfamily,
|
||||
numbers=left, numberstyle=\\color{dg}\\tiny, stepnumber=1, numbersep=5pt,
|
||||
backgroundcolor=\\color{lg}, tabsize=4, showspaces=false,
|
||||
showstringspaces=false
|
||||
}
|
||||
\\title{%(title)s}
|
||||
\\author{%(author)s}
|
||||
\\begin{document}
|
||||
\\maketitle
|
||||
\\tableofcontents
|
||||
\\newpage
|
||||
%(body)s
|
||||
\\end{document}
|
||||
"""
|
||||
|
||||
def markmin2latex(data, image_mapper=lambda x:x, extra={},
|
||||
wrapper=WRAPPER):
|
||||
body, title, authors = render(data, extra=extra, image_mapper=image_mapper)
|
||||
author = '\n\\and\n'.join(a.replace('\n','\\\\\n\\footnotesize ') for a in authors)
|
||||
return wrapper % dict(title=title, author=author, body=body)
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = OptionParser()
|
||||
parser.add_option("-i", "--info", dest="info",
|
||||
help="markmin help")
|
||||
parser.add_option("-t", "--test", dest="test", action="store_true",
|
||||
default=False)
|
||||
parser.add_option("-n", "--no_wrapper", dest="no_wrapper",
|
||||
action="store_true",default=False)
|
||||
parser.add_option("-1", "--one", dest="one",action="store_true",
|
||||
default=False,help="switch section for chapter")
|
||||
parser.add_option("-w", "--wrapper", dest="wrapper", default=False,
|
||||
help="latex file containing header and footer")
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
if options.info:
|
||||
import markmin2html
|
||||
markmin2latex(markmin2html.__doc__)
|
||||
elif options.test:
|
||||
doctest.testmod()
|
||||
else:
|
||||
if options.wrapper:
|
||||
fwrapper = open(options.wrapper,'rb')
|
||||
try:
|
||||
wrapper = fwrapper.read()
|
||||
finally:
|
||||
fwrapper.close()
|
||||
elif options.no_wrapper:
|
||||
wrapper = '%(body)s'
|
||||
else:
|
||||
wrapper = WRAPPER
|
||||
for f in args:
|
||||
fargs = open(f,'r')
|
||||
content_data = []
|
||||
try:
|
||||
content_data.append(fargs.read())
|
||||
finally:
|
||||
fargs.close()
|
||||
content = '\n'.join(content_data)
|
||||
output= markmin2latex(content,wrapper=wrapper)
|
||||
if options.one:
|
||||
output=output.replace(r'\section*{',r'\chapter*{')
|
||||
output=output.replace(r'\section{',r'\chapter{')
|
||||
output=output.replace(r'subsection{',r'section{')
|
||||
print output
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Created by Massimo Di Pierro
|
||||
Licese BSD
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
from tempfile import mkstemp, mkdtemp, NamedTemporaryFile
|
||||
from markmin2latex import markmin2latex
|
||||
|
||||
__all__ = ['markmin2pdf']
|
||||
|
||||
def removeall(path):
|
||||
|
||||
ERROR_STR= """Error removing %(path)s, %(error)s """
|
||||
def rmgeneric(path, __func__):
|
||||
try:
|
||||
__func__(path)
|
||||
except OSError, (errno, strerror):
|
||||
print ERROR_STR % {'path' : path, 'error': strerror }
|
||||
|
||||
files=[path]
|
||||
|
||||
while files:
|
||||
file=files[0]
|
||||
if os.path.isfile(file):
|
||||
f=os.remove
|
||||
rmgeneric(file, os.remove)
|
||||
del files[0]
|
||||
elif os.path.isdir(file):
|
||||
nested = os.listdir(file)
|
||||
if not nested:
|
||||
rmgeneric(file, os.rmdir)
|
||||
del files[0]
|
||||
else:
|
||||
files = [os.path.join(file,x) for x in nested] + files
|
||||
|
||||
|
||||
def latex2pdf(latex, pdflatex='pdflatex', passes=3):
|
||||
"""
|
||||
calls pdflatex in a tempfolder
|
||||
|
||||
Arguments:
|
||||
|
||||
- pdflatex: path to the pdflatex command. Default is just 'pdflatex'.
|
||||
- passes: defines how often pdflates should be run in the texfile.
|
||||
"""
|
||||
|
||||
pdflatex=pdflatex
|
||||
passes=passes
|
||||
warnings=[]
|
||||
|
||||
# setup the envoriment
|
||||
tmpdir = mkdtemp()
|
||||
texfile = open(tmpdir+'/test.tex','wb')
|
||||
texfile.write(latex)
|
||||
texfile.seek(0)
|
||||
texfile.close()
|
||||
texfile = os.path.abspath(texfile.name)
|
||||
|
||||
# start doing some work
|
||||
for i in range(0, passes):
|
||||
logfd,logname = mkstemp()
|
||||
outfile=os.fdopen(logfd)
|
||||
try:
|
||||
ret = subprocess.call([pdflatex,
|
||||
'-interaction=nonstopmode',
|
||||
'-output-format', 'pdf',
|
||||
'-output-directory', tmpdir,
|
||||
texfile],
|
||||
cwd=os.path.dirname(texfile), stdout=outfile,
|
||||
stderr=subprocess.PIPE)
|
||||
finally:
|
||||
outfile.close()
|
||||
re_errors=re.compile('^\!(.*)$',re.M)
|
||||
re_warnings=re.compile('^LaTeX Warning\:(.*)$',re.M)
|
||||
flog = open(logname)
|
||||
try:
|
||||
loglines = flog.read()
|
||||
finally:
|
||||
flog.close()
|
||||
errors=re_errors.findall(loglines)
|
||||
warnings=re_warnings.findall(loglines)
|
||||
os.unlink(logname)
|
||||
|
||||
pdffile=texfile.rsplit('.',1)[0]+'.pdf'
|
||||
if os.path.isfile(pdffile):
|
||||
fpdf = open(pdffile, 'rb')
|
||||
try:
|
||||
data = fpdf.read()
|
||||
finally:
|
||||
fpdf.close()
|
||||
else:
|
||||
data = None
|
||||
removeall(tmpdir)
|
||||
return data, warnings, errors
|
||||
|
||||
|
||||
def markmin2pdf(text, image_mapper=lambda x: None, extra={}):
|
||||
return latex2pdf(markmin2latex(text,image_mapper=image_mapper, extra=extra))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
import doctest
|
||||
import markmin2html
|
||||
if sys.argv[1:2]==['-h']:
|
||||
data, warnings, errors = markmin2pdf(markmin2html.__doc__)
|
||||
if errors:
|
||||
print 'ERRORS:'+'\n'.join(errors)
|
||||
print 'WARNGINS:'+'\n'.join(warnings)
|
||||
else:
|
||||
print data
|
||||
elif len(sys.argv)>1:
|
||||
fargv = open(sys.argv[1],'rb')
|
||||
try:
|
||||
data, warnings, errors = markmin2pdf(fargv.read())
|
||||
finally:
|
||||
fargv.close()
|
||||
if errors:
|
||||
print 'ERRORS:'+'\n'.join(errors)
|
||||
print 'WARNGINS:'+'\n'.join(warnings)
|
||||
else:
|
||||
print data
|
||||
else:
|
||||
doctest.testmod()
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
Thu, 02 Apr 2009 13:37:49 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.44
|
||||
|
||||
* Allowing spaces in the key. (Patch provided by xmm on Launchpad)
|
||||
|
||||
* Detecting when the pickler needs a positional argument. (Patch
|
||||
provided by Brad Clements on Launchpad)
|
||||
|
||||
* Moving length check after the compression. (Patch provided by user
|
||||
Tom on Launchpad)
|
||||
|
||||
* Fixing arguments passed to the _Error if invalid read length.
|
||||
|
||||
* Fixing the representation of domain sockets. (Patch provided by user
|
||||
MTB on Launchpad)
|
||||
|
||||
* Changing a typo of dead_until. (Patch provided by Shane R. Spencer)
|
||||
|
||||
* Providing better error messages (patch provided by Johan Euphrosine).
|
||||
|
||||
* Adding get_slabs() function to get stats. (Patch provided
|
||||
by Nick Verbeck)
|
||||
|
||||
Sun, 01 Jun 2008 15:05:11 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.43
|
||||
|
||||
* eliott reported a bug in the 1.42 related to the socket timeout code
|
||||
causing a traceback due to the timeout value not being set.
|
||||
|
||||
Sat, 31 May 2008 02:09:17 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.42
|
||||
|
||||
* Paul Hummer set up a Launchpad project which I'm going to start using
|
||||
to track patches and allow users to set up their own bzr branches and
|
||||
manage marging in the upstream patches with their own.
|
||||
|
||||
https://launchpad.net/python-memcached
|
||||
|
||||
* Patch from Jehiah Czebotar which does: Changing the calls to
|
||||
mark_dead() to make them dereference tuples, reducing timeout on
|
||||
sockets to 3 seconds, settable via setting Host._SOCKET_TIMEOUT.
|
||||
|
||||
* Patches from Steve Schwarz for set_multi() to return the full set of
|
||||
keys if all servers are down. Previously would not report any keys.
|
||||
|
||||
* Fix from Steve Schwarz delete_multi() argument "seconds" not being
|
||||
correctly handled. Changed it to "time" to match all other calls.
|
||||
|
||||
* Patch from Peter Wilkinson to support using unix domain sockets.
|
||||
He reports that tests succeed with with memcached daemons running,
|
||||
the normal and a domain socket started via
|
||||
"memcached -s memcached.socket". I massaged it quite a bit.
|
||||
|
||||
To use domain sockets, use a connect string of "unix:/path/to/socket"
|
||||
Note however that if you are using a host name of "unix", it will now
|
||||
detect "unix:11211" as being a domain socket with the name "11211".
|
||||
In this case, please use "inet:unix:11211".
|
||||
|
||||
Because of this, it is now preferred to use a connect string prefix
|
||||
of "inet:" or "unix:".
|
||||
|
||||
Tue, 29 Apr 2008 21:03:53 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.41
|
||||
|
||||
* Patch from Jehiah Czebotar to catch an additional server disconnect
|
||||
situation.
|
||||
|
||||
* Patch from Andrey Petrov to add the "append" and "replace" commands.
|
||||
|
||||
Tue, 18 Sep 2007 20:52:09 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.40
|
||||
|
||||
* Updated setup.py file that uses distutils provided by Kai Lautaportti.
|
||||
|
||||
* Prevent keys from containing ASCII character 127 as well, patch provided
|
||||
by Philip Neustrom.
|
||||
|
||||
* Added ability to overload the persistent_load/id, patch provided by
|
||||
Steve Schwarz.
|
||||
|
||||
* Fixed ability to pass (server_hash,key) in place of key in Client.set()
|
||||
Reported by Alexander Klyuev.
|
||||
|
||||
Tue, 14 Aug 2007 14:43:27 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.39
|
||||
|
||||
* Michael Krause reports the previous version doesn't work for
|
||||
_val_to_store_info() calls because it's defined as a staticmethod.
|
||||
Removing staticmethod decorator. Also confirmed by Kai Lautaportti,
|
||||
with suggested fix of removing staticmethod.
|
||||
|
||||
Fri, 10 Aug 2007 17:50:13 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.38
|
||||
|
||||
* Matt McClanahan submitted a patch that allow add() to have a
|
||||
min_compress_len argument.
|
||||
|
||||
* Steve Schwarz submitted a patch allowing user-defined picklers.
|
||||
|
||||
* Michael Krause suggested checking the return value to prevent an
|
||||
exception from being raised in _set() when a value is too large to be
|
||||
stored.
|
||||
|
||||
Fri, 27 Jul 2007 01:55:48 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.37
|
||||
|
||||
* Fixing call from add() to _set() with parameter for min_compress_len.
|
||||
Reported by Jeff Fisher.
|
||||
|
||||
Thu, 07 Jun 2007 04:10:31 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.36
|
||||
|
||||
* Patch by Dave St.Germain to make the Client() class sub-class
|
||||
threadlocal to help with multi-threading issues. Only available in
|
||||
Python 2.4 and above.
|
||||
|
||||
* Patch by James Robinson with:
|
||||
1) new set_multi method.
|
||||
2) factored out determining the flags, length, and value to store
|
||||
from set() into method _val_to_store_info() for use by both set()
|
||||
and set_multi().
|
||||
3) send_cmds() method on host which doesn't apply the trailing '\r\n'
|
||||
for use by set_multi.
|
||||
4) check_key() extended a bit to allow for testing the prefix passed
|
||||
to set_multi just once, not once per each key.
|
||||
5) Patch also enables support for auto compression in set, set_multi,
|
||||
and replace.
|
||||
|
||||
* Suggestion by Helge Tesdal, fixes in check_key for non-string keys.
|
||||
|
||||
* NOTE: On a farm of clients with multiple servers, all clients will
|
||||
need to be upgraded to this version. The next patch changes the
|
||||
server hash.
|
||||
|
||||
* Philip Neustrom supplied a patch to change the server hash function to
|
||||
binascii.crc32. The original "hash()" call is not cross-platform, so
|
||||
big and little endian systems accessing the same memcache may end up
|
||||
hitting different servers. Restore the old functionality by calling:
|
||||
"memcached.serverHashFunction = hash" after importing memcache.
|
||||
|
||||
* Philip Neustrom points out that passing Unicode keys or values causes
|
||||
problems because len(key) or len(value) is not equal to the number of
|
||||
bytes that are required to store the key/value. Philip provides a
|
||||
patch which raises an exception in this case. Raises
|
||||
memcache.Client.MemcachedStringEncodingError exception in this case.
|
||||
|
||||
* NOTE: If you recompiled memcached to increase the default 1MB max
|
||||
value size, you will need to call "memcached.MAX_SERVER_VALUE_LENGTH = N"
|
||||
or memcached will not store values larger than the default 1MB.
|
||||
|
||||
* Philip Neustrom includes another patch which checks that the key
|
||||
doesn't exceed the memcache server's max size. If it does, the item
|
||||
is silently not stored.
|
||||
|
||||
* Philip Neustrom added a bunch of sanity checks.
|
||||
|
||||
* Jehiah Czebotar provided a patch to make the add() and replace()
|
||||
functions return 0 when the add or replace fails, similar to how set()
|
||||
works.
|
||||
|
||||
Sat, 16 Sep 2006 18:31:46 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.34
|
||||
|
||||
* In get_multi, if the recv loop reads 0 bytes, raising an EOFError.
|
||||
Identified by Jim Baker.
|
||||
|
||||
Tue, 05 Sep 2006 14:06:50 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.33
|
||||
|
||||
* Including patch from Yoshinori K. Okuji to read in larger chunks for
|
||||
readline() calls. This should dramatically improve performance under
|
||||
some circumstances.
|
||||
|
||||
Sun, 03 Sep 2006 14:02:03 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.32
|
||||
|
||||
* Including patch from Philip Neustrom which checks keys sent to the
|
||||
server for length and bad characters.
|
||||
|
||||
Sat, 20 May 2006 14:51:28 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Version 1.31
|
||||
|
||||
* Rolled version 1.30 since the Danga folks are now listing this
|
||||
version as the official version. Removing the "tummy" from the version
|
||||
number, and incrementing so that it's clear it's more recent than "1.2".
|
||||
|
||||
* Patch applied from Simon Forman for handling of weighted hosts.
|
||||
|
||||
* Added a little more meat to the README.
|
||||
|
||||
Sat, 28 Jan 2006 15:59:50 -0700 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* cludwin at socallocal suggested that the write-combining with
|
||||
sendall() may not be beneficial. After testing on both SMP and non-SMP
|
||||
machines, I can't see a significant benefit to not doing the
|
||||
write-combining, even on large strings. The benefits of write-combining
|
||||
on smaller strings seems to be significant on UP machines in tight loops.
|
||||
Even on strings that are larger than 2MB, there seems to be no benefit to
|
||||
splitting out the writes.
|
||||
|
||||
Sun, 18 Sep 2005 18:56:31 -0600 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Changing a printf to debuglog and catching a pickle exception, patch
|
||||
submitted by Justin Azoff.
|
||||
|
||||
Thu, 14 Jul 2005 11:17:30 -0700 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Alex Stapleton found that the sendall call was slow for writing data
|
||||
larger than several kilobytes. I had him test a change to his patch,
|
||||
which worked as well, but was simpler. The code now does two sendall
|
||||
calls, one for the data and one for the line termination, if the data is
|
||||
larger than 100 bytes.
|
||||
|
||||
Thu, 7 Apr 2005 14:45:44 -0700 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Incorporating some fixes to get_multi() from Bo Yang
|
||||
|
||||
Mon, 13 Dec 2004 02:35:17 -0700 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Simplifying the readline() function and speeding it up ~25%.
|
||||
* Fixing a bug in readline() if the server drops, mark_dead() was not
|
||||
being properly called.
|
||||
|
||||
Sun, 12 Dec 2004 18:56:33 -0700 Sean Reifschneider <jafo@tummy.com>
|
||||
|
||||
* Adding "stats()" and "flush_all()" methods.
|
||||
|
||||
Thu, 10 Aug 2003 12:17:50 -0700 Evan Martin <martine@danga.com>
|
||||
|
||||
* Slightly more verbose self-test output.
|
||||
* Fix mark_dead() to use proper classname.
|
||||
* Make pooltest.py run from the test directory.
|
||||
|
||||
Thu, 07 Aug 2003 16:32:32 -0700 Evan Martin <martine@danga.com>
|
||||
|
||||
* Add incr, decr, and delete.
|
||||
* Better Python (based on comments from Uriah Welcome).
|
||||
* Docs, using epydoc.
|
||||
|
||||
Thu, 07 Aug 2003 14:20:27 -0700 Evan Martin <martine@danga.com>
|
||||
|
||||
* Initial prerelease.
|
||||
@@ -0,0 +1,10 @@
|
||||
Metadata-Version: 1.0
|
||||
Name: python-memcached
|
||||
Version: 1.44
|
||||
Summary: A Python memcached client library.
|
||||
Home-page: http://www.danga.com/memcached/
|
||||
Author: Evan Martin
|
||||
Author-email: martine@danga.com
|
||||
License: Python
|
||||
Description: A Python memcached client library.
|
||||
Platform: UNKNOWN
|
||||
@@ -0,0 +1,7 @@
|
||||
This package was originally written by Evan Martin of Danga.
|
||||
Sean Reifschneider of tummy.com, ltd. has taken over maintenance of it.
|
||||
|
||||
This software is a 100% Python interface to the memcached memory cache
|
||||
daemon. It is the client side software which allows storing values in one
|
||||
or more, possibly remote, memcached servers. Search google for memcached
|
||||
for more information.
|
||||
@@ -0,0 +1,89 @@
|
||||
from gluon.contrib.memcache.memcache import Client
|
||||
from gluon.cache import CacheAbstract
|
||||
import time
|
||||
|
||||
"""
|
||||
examle of usage:
|
||||
|
||||
cache.memcache = MemcacheClient(request,[127.0.0.1:11211],debug=true)
|
||||
"""
|
||||
|
||||
import cPickle as pickle
|
||||
import thread
|
||||
|
||||
locker = thread.allocate_lock()
|
||||
|
||||
def MemcacheClient(*a, **b):
|
||||
locker.acquire()
|
||||
try:
|
||||
if not hasattr(MemcacheClient, '__mc_instance'):
|
||||
MemcacheClient.__mc_instance = _MemcacheClient(*a, **b)
|
||||
finally:
|
||||
locker.release()
|
||||
return MemcacheClient.__mc_instance
|
||||
|
||||
class _MemcacheClient(Client):
|
||||
|
||||
meta_storage = {}
|
||||
|
||||
def __init__(self, request, servers, debug=0, pickleProtocol=0,
|
||||
pickler=pickle.Pickler, unpickler=pickle.Unpickler,
|
||||
pload=None, pid=None):
|
||||
self.request=request
|
||||
if request:
|
||||
app = request.application
|
||||
else:
|
||||
app = ''
|
||||
Client.__init__(self, servers, debug, pickleProtocol,
|
||||
pickler, unpickler, pload, pid)
|
||||
if not app in self.meta_storage:
|
||||
self.storage = self.meta_storage[app] = {
|
||||
CacheAbstract.cache_stats_name: {
|
||||
'hit_total': 0,
|
||||
'misses': 0,
|
||||
}}
|
||||
else:
|
||||
self.storage = self.meta_storage[app]
|
||||
|
||||
|
||||
def __call__(self, key, f, time_expire=300):
|
||||
if time_expire == None:
|
||||
time_expire = 10**10
|
||||
# this must be commented because get and set are redefined
|
||||
# key = self.__keyFormat__(key)
|
||||
value = None
|
||||
obj = self.get(key)
|
||||
if obj:
|
||||
value = obj
|
||||
elif f is None:
|
||||
if obj: self.delete(key)
|
||||
else:
|
||||
value = f()
|
||||
self.set(key, value, time_expire)
|
||||
return value
|
||||
|
||||
def increment(self, key, value=1, time_expire=300):
|
||||
newKey = self.__keyFormat__(key)
|
||||
obj = Client.get(self, newKey)
|
||||
if obj:
|
||||
return Client.incr(self, newKey, value)
|
||||
else:
|
||||
Client.set(self, newKey, value, time_expire)
|
||||
return value
|
||||
|
||||
def set(self, key, value, time_expire=300):
|
||||
newKey = self.__keyFormat__(key)
|
||||
return Client.set(self, newKey, value, time_expire)
|
||||
|
||||
def get(self, key):
|
||||
newKey = self.__keyFormat__(key)
|
||||
return Client.get(self, newKey)
|
||||
|
||||
def delete(self, key):
|
||||
newKey = self.__keyFormat__(key)
|
||||
return Client.delete(self, newKey)
|
||||
|
||||
def __keyFormat__(self, key):
|
||||
return '%s/%s' % (self.request.application, key.replace(' ', '_'))
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,908 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
This file is part of web2py Web Framework (Copyrighted, 2007-2009).
|
||||
Developed by Massimo Di Pierro <mdipierro@cs.depaul.edu> and
|
||||
Robin B <robi123@gmail.com>.
|
||||
License: GPL v2
|
||||
"""
|
||||
|
||||
__all__ = ['MEMDB', 'Field']
|
||||
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
import types
|
||||
import datetime
|
||||
import thread
|
||||
import cStringIO
|
||||
import csv
|
||||
import copy
|
||||
import gluon.validators as validators
|
||||
from gluon.storage import Storage
|
||||
import random
|
||||
|
||||
SQL_DIALECTS = {'memcache': {
|
||||
'boolean': bool,
|
||||
'string': unicode,
|
||||
'text': unicode,
|
||||
'password': unicode,
|
||||
'blob': unicode,
|
||||
'upload': unicode,
|
||||
'integer': long,
|
||||
'double': float,
|
||||
'date': datetime.date,
|
||||
'time': datetime.time,
|
||||
'datetime': datetime.datetime,
|
||||
'id': int,
|
||||
'reference': int,
|
||||
'lower': None,
|
||||
'upper': None,
|
||||
'is null': 'IS NULL',
|
||||
'is not null': 'IS NOT NULL',
|
||||
'extract': None,
|
||||
'left join': None,
|
||||
}}
|
||||
|
||||
|
||||
def cleanup(text):
|
||||
if re.compile('[^0-9a-zA-Z_]').findall(text):
|
||||
raise SyntaxError, \
|
||||
'Can\'t cleanup \'%s\': only [0-9a-zA-Z_] allowed in table and field names' % text
|
||||
return text
|
||||
|
||||
|
||||
def assert_filter_fields(*fields):
|
||||
for field in fields:
|
||||
if isinstance(field, (Field, Expression)) and field.type\
|
||||
in ['text', 'blob']:
|
||||
raise SyntaxError, 'AppEngine does not index by: %s'\
|
||||
% field.type
|
||||
|
||||
|
||||
def dateobj_to_datetime(object):
|
||||
|
||||
# convert dates,times to datetimes for AppEngine
|
||||
|
||||
if isinstance(object, datetime.date):
|
||||
object = datetime.datetime(object.year, object.month,
|
||||
object.day)
|
||||
if isinstance(object, datetime.time):
|
||||
object = datetime.datetime(
|
||||
1970,
|
||||
1,
|
||||
1,
|
||||
object.hour,
|
||||
object.minute,
|
||||
object.second,
|
||||
object.microsecond,
|
||||
)
|
||||
return object
|
||||
|
||||
|
||||
def sqlhtml_validators(field_type, length):
|
||||
v = {
|
||||
'boolean': [],
|
||||
'string': validators.IS_LENGTH(length),
|
||||
'text': [],
|
||||
'password': validators.IS_LENGTH(length),
|
||||
'blob': [],
|
||||
'upload': [],
|
||||
'double': validators.IS_FLOAT_IN_RANGE(-1e100, 1e100),
|
||||
'integer': validators.IS_INT_IN_RANGE(-1e100, 1e100),
|
||||
'date': validators.IS_DATE(),
|
||||
'time': validators.IS_TIME(),
|
||||
'datetime': validators.IS_DATETIME(),
|
||||
'reference': validators.IS_INT_IN_RANGE(0, 1e100),
|
||||
}
|
||||
try:
|
||||
return v[field_type[:9]]
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
|
||||
class DALStorage(dict):
|
||||
|
||||
"""
|
||||
a dictionary that let you do d['a'] as well as d.a
|
||||
"""
|
||||
|
||||
def __getattr__(self, key):
|
||||
return self[key]
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if key in self:
|
||||
raise SyntaxError, 'Object \'%s\'exists and cannot be redefined' % key
|
||||
self[key] = value
|
||||
|
||||
def __repr__(self):
|
||||
return '<DALStorage ' + dict.__repr__(self) + '>'
|
||||
|
||||
|
||||
class SQLCallableList(list):
|
||||
|
||||
def __call__(self):
|
||||
return copy.copy(self)
|
||||
|
||||
|
||||
class MEMDB(DALStorage):
|
||||
|
||||
"""
|
||||
an instance of this class represents a database connection
|
||||
|
||||
Example::
|
||||
|
||||
db=MEMDB(Client())
|
||||
db.define_table('tablename',Field('fieldname1'),
|
||||
Field('fieldname2'))
|
||||
"""
|
||||
|
||||
def __init__(self, client):
|
||||
self._dbname = 'memdb'
|
||||
self['_lastsql'] = ''
|
||||
self.tables = SQLCallableList()
|
||||
self._translator = SQL_DIALECTS['memcache']
|
||||
self.client = client
|
||||
|
||||
def define_table(
|
||||
self,
|
||||
tablename,
|
||||
*fields,
|
||||
**args
|
||||
):
|
||||
tablename = cleanup(tablename)
|
||||
if tablename in dir(self) or tablename[0] == '_':
|
||||
raise SyntaxError, 'invalid table name: %s' % tablename
|
||||
if not tablename in self.tables:
|
||||
self.tables.append(tablename)
|
||||
else:
|
||||
raise SyntaxError, 'table already defined: %s' % tablename
|
||||
t = self[tablename] = Table(self, tablename, *fields)
|
||||
t._create()
|
||||
return t
|
||||
|
||||
def __call__(self, where=''):
|
||||
return Set(self, where)
|
||||
|
||||
|
||||
class SQLALL(object):
|
||||
|
||||
def __init__(self, table):
|
||||
self.table = table
|
||||
|
||||
|
||||
class Table(DALStorage):
|
||||
|
||||
"""
|
||||
an instance of this class represents a database table
|
||||
|
||||
Example::
|
||||
|
||||
db=MEMDB(Client())
|
||||
db.define_table('users',Field('name'))
|
||||
db.users.insert(name='me')
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db,
|
||||
tablename,
|
||||
*fields
|
||||
):
|
||||
self._db = db
|
||||
self._tablename = tablename
|
||||
self.fields = SQLCallableList()
|
||||
self._referenced_by = []
|
||||
fields = list(fields)
|
||||
fields.insert(0, Field('id', 'id'))
|
||||
for field in fields:
|
||||
self.fields.append(field.name)
|
||||
self[field.name] = field
|
||||
field._tablename = self._tablename
|
||||
field._table = self
|
||||
field._db = self._db
|
||||
self.ALL = SQLALL(self)
|
||||
|
||||
def _create(self):
|
||||
fields = []
|
||||
myfields = {}
|
||||
for k in self.fields:
|
||||
field = self[k]
|
||||
attr = {}
|
||||
if not field.type[:9] in ['id', 'reference']:
|
||||
if field.notnull:
|
||||
attr = dict(required=True)
|
||||
if field.type[:2] == 'id':
|
||||
continue
|
||||
if field.type[:9] == 'reference':
|
||||
referenced = field.type[10:].strip()
|
||||
if not referenced:
|
||||
raise SyntaxError, \
|
||||
'Table %s: reference \'%s\' to nothing!' % (self._tablename, k)
|
||||
if not referenced in self._db:
|
||||
raise SyntaxError, \
|
||||
'Table: table %s does not exist' % referenced
|
||||
referee = self._db[referenced]
|
||||
ftype = \
|
||||
self._db._translator[field.type[:9]](
|
||||
self._db[referenced]._tableobj)
|
||||
if self._tablename in referee.fields: # ## THIS IS OK
|
||||
raise SyntaxError, \
|
||||
'Field: table \'%s\' has same name as a field ' \
|
||||
'in referenced table \'%s\'' % (self._tablename, referenced)
|
||||
self._db[referenced]._referenced_by.append((self._tablename,
|
||||
field.name))
|
||||
elif not field.type in self._db._translator\
|
||||
or not self._db._translator[field.type]:
|
||||
raise SyntaxError, 'Field: unkown field type %s' % field.type
|
||||
self._tableobj = self._db.client
|
||||
return None
|
||||
|
||||
def create(self):
|
||||
|
||||
# nothing to do, here for backward compatility
|
||||
|
||||
pass
|
||||
|
||||
def drop(self):
|
||||
|
||||
# nothing to do, here for backward compatibility
|
||||
|
||||
self._db(self.id > 0).delete()
|
||||
|
||||
def insert(self, **fields):
|
||||
id = self._create_id()
|
||||
if self.update(id, **fields):
|
||||
return long(id)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get(self, id):
|
||||
val = self._tableobj.get(self._id_to_key(id))
|
||||
if val:
|
||||
return Storage(val)
|
||||
else:
|
||||
return None
|
||||
|
||||
def update(self, id, **fields):
|
||||
for field in fields:
|
||||
if not field in fields and self[field].default\
|
||||
!= None:
|
||||
fields[field] = self[field].default
|
||||
if field in fields:
|
||||
fields[field] = obj_represent(fields[field],
|
||||
self[field].type, self._db)
|
||||
return self._tableobj.set(self._id_to_key(id), fields)
|
||||
|
||||
def delete(self, id):
|
||||
return self._tableobj.delete(self._id_to_key(id))
|
||||
|
||||
def _shard_key(self, shard):
|
||||
return self._id_to_key('s/%s' % shard)
|
||||
|
||||
def _id_to_key(self, id):
|
||||
return '__memdb__/t/%s/k/%s' % (self._tablename, str(id))
|
||||
|
||||
def _create_id(self):
|
||||
shard = random.randint(10, 99)
|
||||
shard_id = self._shard_key(shard)
|
||||
id = self._tableobj.incr(shard_id)
|
||||
if not id:
|
||||
if self._tableobj.set(shard_id, '0'):
|
||||
id = 0
|
||||
else:
|
||||
raise Exception, 'cannot set memcache'
|
||||
return long(str(shard) + str(id))
|
||||
|
||||
def __str__(self):
|
||||
return self._tablename
|
||||
|
||||
|
||||
class Expression(object):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
type='string',
|
||||
db=None,
|
||||
):
|
||||
(self.name, self.type, self._db) = (name, type, db)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __or__(self, other): # for use in sortby
|
||||
assert_filter_fields(self, other)
|
||||
return Expression(self.name + '|' + other.name, None, None)
|
||||
|
||||
def __invert__(self):
|
||||
assert_filter_fields(self)
|
||||
return Expression('-' + self.name, self.type, None)
|
||||
|
||||
# for use in Query
|
||||
|
||||
def __eq__(self, value):
|
||||
return Query(self, '=', value)
|
||||
|
||||
def __ne__(self, value):
|
||||
return Query(self, '!=', value)
|
||||
|
||||
def __lt__(self, value):
|
||||
return Query(self, '<', value)
|
||||
|
||||
def __le__(self, value):
|
||||
return Query(self, '<=', value)
|
||||
|
||||
def __gt__(self, value):
|
||||
return Query(self, '>', value)
|
||||
|
||||
def __ge__(self, value):
|
||||
return Query(self, '>=', value)
|
||||
|
||||
# def like(self,value): return Query(self,' LIKE ',value)
|
||||
# def belongs(self,value): return Query(self,' IN ',value)
|
||||
# for use in both Query and sortby
|
||||
|
||||
def __add__(self, other):
|
||||
return Expression('%s+%s' % (self, other), 'float', None)
|
||||
|
||||
def __sub__(self, other):
|
||||
return Expression('%s-%s' % (self, other), 'float', None)
|
||||
|
||||
def __mul__(self, other):
|
||||
return Expression('%s*%s' % (self, other), 'float', None)
|
||||
|
||||
def __div__(self, other):
|
||||
return Expression('%s/%s' % (self, other), 'float', None)
|
||||
|
||||
|
||||
class Field(Expression):
|
||||
|
||||
"""
|
||||
an instance of this class represents a database field
|
||||
|
||||
example::
|
||||
|
||||
a = Field(name, 'string', length=32, required=False,
|
||||
default=None, requires=IS_NOT_EMPTY(), notnull=False,
|
||||
unique=False, uploadfield=True)
|
||||
|
||||
to be used as argument of GQLDB.define_table
|
||||
|
||||
allowed field types:
|
||||
string, boolean, integer, double, text, blob,
|
||||
date, time, datetime, upload, password
|
||||
|
||||
strings must have a length or 512 by default.
|
||||
fields should have a default or they will be required in SQLFORMs
|
||||
the requires argument are used to validate the field input in SQLFORMs
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fieldname,
|
||||
type='string',
|
||||
length=None,
|
||||
default=None,
|
||||
required=False,
|
||||
requires=sqlhtml_validators,
|
||||
ondelete='CASCADE',
|
||||
notnull=False,
|
||||
unique=False,
|
||||
uploadfield=True,
|
||||
):
|
||||
|
||||
self.name = cleanup(fieldname)
|
||||
if fieldname in dir(Table) or fieldname[0] == '_':
|
||||
raise SyntaxError, 'Field: invalid field name: %s' % fieldname
|
||||
if isinstance(type, Table):
|
||||
type = 'reference ' + type._tablename
|
||||
if not length:
|
||||
length = 512
|
||||
self.type = type # 'string', 'integer'
|
||||
self.length = length # the length of the string
|
||||
self.default = default # default value for field
|
||||
self.required = required # is this field required
|
||||
self.ondelete = ondelete.upper() # this is for reference fields only
|
||||
self.notnull = notnull
|
||||
self.unique = unique
|
||||
self.uploadfield = uploadfield
|
||||
if requires == sqlhtml_validators:
|
||||
requires = sqlhtml_validators(type, length)
|
||||
elif requires is None:
|
||||
requires = []
|
||||
self.requires = requires # list of validators
|
||||
|
||||
def formatter(self, value):
|
||||
if value is None or not self.requires:
|
||||
return value
|
||||
if not isinstance(self.requires, (list, tuple)):
|
||||
requires = [self.requires]
|
||||
else:
|
||||
requires = copy.copy(self.requires)
|
||||
requires.reverse()
|
||||
for item in requires:
|
||||
if hasattr(item, 'formatter'):
|
||||
value = item.formatter(value)
|
||||
return value
|
||||
|
||||
def __str__(self):
|
||||
return '%s.%s' % (self._tablename, self.name)
|
||||
|
||||
|
||||
MEMDB.Field = Field # ## required by gluon/globals.py session.connect
|
||||
|
||||
|
||||
def obj_represent(object, fieldtype, db):
|
||||
if object != None:
|
||||
if fieldtype == 'date' and not isinstance(object,
|
||||
datetime.date):
|
||||
(y, m, d) = [int(x) for x in str(object).strip().split('-')]
|
||||
object = datetime.date(y, m, d)
|
||||
elif fieldtype == 'time' and not isinstance(object, datetime.time):
|
||||
time_items = [int(x) for x in str(object).strip().split(':')[:3]]
|
||||
if len(time_items) == 3:
|
||||
(h, mi, s) = time_items
|
||||
else:
|
||||
(h, mi, s) = time_items + [0]
|
||||
object = datetime.time(h, mi, s)
|
||||
elif fieldtype == 'datetime' and not isinstance(object,
|
||||
datetime.datetime):
|
||||
(y, m, d) = [int(x) for x in
|
||||
str(object)[:10].strip().split('-')]
|
||||
time_items = [int(x) for x in
|
||||
str(object)[11:].strip().split(':')[:3]]
|
||||
if len(time_items) == 3:
|
||||
(h, mi, s) = time_items
|
||||
else:
|
||||
(h, mi, s) = time_items + [0]
|
||||
object = datetime.datetime(
|
||||
y,
|
||||
m,
|
||||
d,
|
||||
h,
|
||||
mi,
|
||||
s,
|
||||
)
|
||||
elif fieldtype == 'integer' and not isinstance(object, long):
|
||||
object = long(object)
|
||||
|
||||
return object
|
||||
|
||||
|
||||
class QueryException:
|
||||
|
||||
def __init__(self, **a):
|
||||
self.__dict__ = a
|
||||
|
||||
|
||||
class Query(object):
|
||||
|
||||
"""
|
||||
A query object necessary to define a set.
|
||||
It can be stored or can be passed to GQLDB.__call__() to obtain a Set
|
||||
|
||||
Example:
|
||||
query=db.users.name=='Max'
|
||||
set=db(query)
|
||||
records=set.select()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
left,
|
||||
op=None,
|
||||
right=None,
|
||||
):
|
||||
if isinstance(right, (Field, Expression)):
|
||||
raise SyntaxError, \
|
||||
'Query: right side of filter must be a value or entity'
|
||||
if isinstance(left, Field) and left.name == 'id':
|
||||
if op == '=':
|
||||
self.get_one = \
|
||||
QueryException(tablename=left._tablename,
|
||||
id=long(right))
|
||||
return
|
||||
else:
|
||||
raise SyntaxError, 'only equality by id is supported'
|
||||
raise SyntaxError, 'not supported'
|
||||
|
||||
def __str__(self):
|
||||
return str(self.left)
|
||||
|
||||
|
||||
class Set(object):
|
||||
|
||||
"""
|
||||
As Set represents a set of records in the database,
|
||||
the records are identified by the where=Query(...) object.
|
||||
normally the Set is generated by GQLDB.__call__(Query(...))
|
||||
|
||||
given a set, for example
|
||||
set=db(db.users.name=='Max')
|
||||
you can:
|
||||
set.update(db.users.name='Massimo')
|
||||
set.delete() # all elements in the set
|
||||
set.select(orderby=db.users.id,groupby=db.users.name,limitby=(0,10))
|
||||
and take subsets:
|
||||
subset=set(db.users.id<5)
|
||||
"""
|
||||
|
||||
def __init__(self, db, where=None):
|
||||
self._db = db
|
||||
self._tables = []
|
||||
self.filters = []
|
||||
if hasattr(where, 'get_all'):
|
||||
self.where = where
|
||||
self._tables.insert(0, where.get_all)
|
||||
elif hasattr(where, 'get_one') and isinstance(where.get_one,
|
||||
QueryException):
|
||||
self.where = where.get_one
|
||||
else:
|
||||
|
||||
# find out which tables are involved
|
||||
|
||||
if isinstance(where, Query):
|
||||
self.filters = where.left
|
||||
self.where = where
|
||||
self._tables = [field._tablename for (field, op, val) in
|
||||
self.filters]
|
||||
|
||||
def __call__(self, where):
|
||||
if isinstance(self.where, QueryException) or isinstance(where,
|
||||
QueryException):
|
||||
raise SyntaxError, \
|
||||
'neither self.where nor where can be a QueryException instance'
|
||||
if self.where:
|
||||
return Set(self._db, self.where & where)
|
||||
else:
|
||||
return Set(self._db, where)
|
||||
|
||||
def _get_table_or_raise(self):
|
||||
tablenames = list(set(self._tables)) # unique
|
||||
if len(tablenames) < 1:
|
||||
raise SyntaxError, 'Set: no tables selected'
|
||||
if len(tablenames) > 1:
|
||||
raise SyntaxError, 'Set: no join in appengine'
|
||||
return self._db[tablenames[0]]._tableobj
|
||||
|
||||
def _getitem_exception(self):
|
||||
(tablename, id) = (self.where.tablename, self.where.id)
|
||||
fields = self._db[tablename].fields
|
||||
self.colnames = ['%s.%s' % (tablename, t) for t in fields]
|
||||
item = self._db[tablename].get(id)
|
||||
return (item, fields, tablename, id)
|
||||
|
||||
def _select_except(self):
|
||||
(item, fields, tablename, id) = self._getitem_exception()
|
||||
if not item:
|
||||
return []
|
||||
new_item = []
|
||||
for t in fields:
|
||||
if t == 'id':
|
||||
new_item.append(long(id))
|
||||
else:
|
||||
new_item.append(getattr(item, t))
|
||||
r = [new_item]
|
||||
return Rows(self._db, r, *self.colnames)
|
||||
|
||||
def select(self, *fields, **attributes):
|
||||
"""
|
||||
Always returns a Rows object, even if it may be empty
|
||||
"""
|
||||
|
||||
if isinstance(self.where, QueryException):
|
||||
return self._select_except()
|
||||
else:
|
||||
raise SyntaxError, 'select arguments not supported'
|
||||
|
||||
def count(self):
|
||||
return len(self.select())
|
||||
|
||||
def delete(self):
|
||||
if isinstance(self.where, QueryException):
|
||||
(item, fields, tablename, id) = self._getitem_exception()
|
||||
if not item:
|
||||
return
|
||||
self._db[tablename].delete(id)
|
||||
else:
|
||||
raise Exception, 'deletion not implemented'
|
||||
|
||||
def update(self, **update_fields):
|
||||
if isinstance(self.where, QueryException):
|
||||
(item, fields, tablename, id) = self._getitem_exception()
|
||||
if not item:
|
||||
return
|
||||
for (key, value) in update_fields.items():
|
||||
setattr(item, key, value)
|
||||
self._db[tablename].update(id, **item)
|
||||
else:
|
||||
raise Exception, 'update not implemented'
|
||||
|
||||
|
||||
def update_record(
|
||||
t,
|
||||
s,
|
||||
id,
|
||||
a,
|
||||
):
|
||||
item = s.get(id)
|
||||
for (key, value) in a.items():
|
||||
t[key] = value
|
||||
setattr(item, key, value)
|
||||
s.update(id, **item)
|
||||
|
||||
|
||||
class Rows(object):
|
||||
|
||||
"""
|
||||
A wrapper for the return value of a select. It basically represents a table.
|
||||
It has an iterator and each row is represented as a dictionary.
|
||||
"""
|
||||
|
||||
# ## this class still needs some work to care for ID/OID
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db,
|
||||
response,
|
||||
*colnames
|
||||
):
|
||||
self._db = db
|
||||
self.colnames = colnames
|
||||
self.response = response
|
||||
|
||||
def __len__(self):
|
||||
return len(self.response)
|
||||
|
||||
def __getitem__(self, i):
|
||||
if i >= len(self.response) or i < 0:
|
||||
raise SyntaxError, 'Rows: no such row: %i' % i
|
||||
if len(self.response[0]) != len(self.colnames):
|
||||
raise SyntaxError, 'Rows: internal error'
|
||||
row = DALStorage()
|
||||
for j in xrange(len(self.colnames)):
|
||||
value = self.response[i][j]
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode('utf-8')
|
||||
packed = self.colnames[j].split('.')
|
||||
try:
|
||||
(tablename, fieldname) = packed
|
||||
except:
|
||||
if not '_extra' in row:
|
||||
row['_extra'] = DALStorage()
|
||||
row['_extra'][self.colnames[j]] = value
|
||||
continue
|
||||
table = self._db[tablename]
|
||||
field = table[fieldname]
|
||||
if not tablename in row:
|
||||
row[tablename] = DALStorage()
|
||||
if field.type[:9] == 'reference':
|
||||
referee = field.type[10:].strip()
|
||||
rid = value
|
||||
row[tablename][fieldname] = rid
|
||||
elif field.type == 'boolean' and value != None:
|
||||
|
||||
# row[tablename][fieldname]=Set(self._db[referee].id==rid)
|
||||
|
||||
if value == True or value == 'T':
|
||||
row[tablename][fieldname] = True
|
||||
else:
|
||||
row[tablename][fieldname] = False
|
||||
elif field.type == 'date' and value != None\
|
||||
and not isinstance(value, datetime.date):
|
||||
(y, m, d) = [int(x) for x in
|
||||
str(value).strip().split('-')]
|
||||
row[tablename][fieldname] = datetime.date(y, m, d)
|
||||
elif field.type == 'time' and value != None\
|
||||
and not isinstance(value, datetime.time):
|
||||
time_items = [int(x) for x in
|
||||
str(value).strip().split(':')[:3]]
|
||||
if len(time_items) == 3:
|
||||
(h, mi, s) = time_items
|
||||
else:
|
||||
(h, mi, s) = time_items + [0]
|
||||
row[tablename][fieldname] = datetime.time(h, mi, s)
|
||||
elif field.type == 'datetime' and value != None\
|
||||
and not isinstance(value, datetime.datetime):
|
||||
(y, m, d) = [int(x) for x in
|
||||
str(value)[:10].strip().split('-')]
|
||||
time_items = [int(x) for x in
|
||||
str(value)[11:].strip().split(':')[:3]]
|
||||
if len(time_items) == 3:
|
||||
(h, mi, s) = time_items
|
||||
else:
|
||||
(h, mi, s) = time_items + [0]
|
||||
row[tablename][fieldname] = datetime.datetime(
|
||||
y,
|
||||
m,
|
||||
d,
|
||||
h,
|
||||
mi,
|
||||
s,
|
||||
)
|
||||
else:
|
||||
row[tablename][fieldname] = value
|
||||
if fieldname == 'id':
|
||||
id = row[tablename].id
|
||||
row[tablename].update_record = lambda t = row[tablename], \
|
||||
s = self._db[tablename], id = id, **a: update_record(t,
|
||||
s, id, a)
|
||||
for (referee_table, referee_name) in \
|
||||
table._referenced_by:
|
||||
s = self._db[referee_table][referee_name]
|
||||
row[tablename][referee_table] = Set(self._db, s
|
||||
== id)
|
||||
if len(row.keys()) == 1:
|
||||
return row[row.keys()[0]]
|
||||
return row
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
iterator over records
|
||||
"""
|
||||
|
||||
for i in xrange(len(self)):
|
||||
yield self[i]
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
serializes the table into a csv file
|
||||
"""
|
||||
|
||||
s = cStringIO.StringIO()
|
||||
writer = csv.writer(s)
|
||||
writer.writerow(self.colnames)
|
||||
c = len(self.colnames)
|
||||
for i in xrange(len(self)):
|
||||
row = [self.response[i][j] for j in xrange(c)]
|
||||
for k in xrange(c):
|
||||
if isinstance(row[k], unicode):
|
||||
row[k] = row[k].encode('utf-8')
|
||||
writer.writerow(row)
|
||||
return s.getvalue()
|
||||
|
||||
def xml(self):
|
||||
"""
|
||||
serializes the table using sqlhtml.SQLTABLE (if present)
|
||||
"""
|
||||
|
||||
return sqlhtml.SQLTABLE(self).xml()
|
||||
|
||||
|
||||
def test_all():
|
||||
"""
|
||||
How to run from web2py dir:
|
||||
export PYTHONPATH=.:YOUR_PLATFORMS_APPENGINE_PATH
|
||||
python gluon/contrib/memdb.py
|
||||
|
||||
Setup the UTC timezone and database stubs
|
||||
|
||||
>>> import os
|
||||
>>> os.environ['TZ'] = 'UTC'
|
||||
>>> import time
|
||||
>>> if hasattr(time, 'tzset'):
|
||||
... time.tzset()
|
||||
>>>
|
||||
>>> from google.appengine.api import apiproxy_stub_map
|
||||
>>> from google.appengine.api.memcache import memcache_stub
|
||||
>>> apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
|
||||
>>> apiproxy_stub_map.apiproxy.RegisterStub('memcache', memcache_stub.MemcacheServiceStub())
|
||||
|
||||
Create a table with all possible field types
|
||||
>>> from google.appengine.api.memcache import Client
|
||||
>>> db=MEMDB(Client())
|
||||
>>> tmp=db.define_table('users', Field('stringf','string',length=32,required=True), Field('booleanf','boolean',default=False), Field('passwordf','password',notnull=True), Field('blobf','blob'), Field('uploadf','upload'), Field('integerf','integer',unique=True), Field('doublef','double',unique=True,notnull=True), Field('datef','date',default=datetime.date.today()), Field('timef','time'), Field('datetimef','datetime'), migrate='test_user.table')
|
||||
|
||||
Insert a field
|
||||
|
||||
>>> user_id = db.users.insert(stringf='a',booleanf=True,passwordf='p',blobf='0A', uploadf=None, integerf=5,doublef=3.14, datef=datetime.date(2001,1,1), timef=datetime.time(12,30,15), datetimef=datetime.datetime(2002,2,2,12,30,15))
|
||||
>>> user_id != None
|
||||
True
|
||||
|
||||
Select all
|
||||
|
||||
# >>> all = db().select(db.users.ALL)
|
||||
|
||||
Drop the table
|
||||
|
||||
# >>> db.users.drop()
|
||||
|
||||
Select many entities
|
||||
|
||||
>>> tmp = db.define_table(\"posts\", Field('body','text'), Field('total','integer'), Field('created_at','datetime'))
|
||||
>>> many = 20 #2010 # more than 1000 single fetch limit (it can be slow)
|
||||
>>> few = 5
|
||||
>>> most = many - few
|
||||
>>> 0 < few < most < many
|
||||
True
|
||||
>>> for i in range(many):
|
||||
... f=db.posts.insert(body='', total=i,created_at=datetime.datetime(2008, 7, 6, 14, 15, 42, i))
|
||||
>>>
|
||||
|
||||
# test timezones
|
||||
>>> class TZOffset(datetime.tzinfo):
|
||||
... def __init__(self,offset=0):
|
||||
... self.offset = offset
|
||||
... def utcoffset(self, dt): return datetime.timedelta(hours=self.offset)
|
||||
... def dst(self, dt): return datetime.timedelta(0)
|
||||
... def tzname(self, dt): return 'UTC' + str(self.offset)
|
||||
...
|
||||
>>> SERVER_OFFSET = -8
|
||||
>>>
|
||||
>>> stamp = datetime.datetime(2008, 7, 6, 14, 15, 42, 828201)
|
||||
>>> post_id = db.posts.insert(created_at=stamp,body='body1')
|
||||
>>> naive_stamp = db(db.posts.id==post_id).select()[0].created_at
|
||||
>>> utc_stamp=naive_stamp.replace(tzinfo=TZOffset())
|
||||
>>> server_stamp = utc_stamp.astimezone(TZOffset(SERVER_OFFSET))
|
||||
>>> stamp == naive_stamp
|
||||
True
|
||||
>>> utc_stamp == server_stamp
|
||||
True
|
||||
>>> rows = db(db.posts.id==post_id).select()
|
||||
>>> len(rows) == 1
|
||||
True
|
||||
>>> rows[0].body == 'body1'
|
||||
True
|
||||
>>> db(db.posts.id==post_id).delete()
|
||||
>>> rows = db(db.posts.id==post_id).select()
|
||||
>>> len(rows) == 0
|
||||
True
|
||||
|
||||
>>> id = db.posts.insert(total='0') # coerce str to integer
|
||||
>>> rows = db(db.posts.id==id).select()
|
||||
>>> len(rows) == 1
|
||||
True
|
||||
>>> rows[0].total == 0
|
||||
True
|
||||
|
||||
Examples of insert, select, update, delete
|
||||
|
||||
>>> tmp=db.define_table('person', Field('name'), Field('birth','date'), migrate='test_person.table')
|
||||
>>> marco_id=db.person.insert(name=\"Marco\",birth='2005-06-22')
|
||||
>>> person_id=db.person.insert(name=\"Massimo\",birth='1971-12-21')
|
||||
>>> me=db(db.person.id==person_id).select()[0] # test select
|
||||
>>> me.name
|
||||
'Massimo'
|
||||
>>> db(db.person.id==person_id).update(name='massimo') # test update
|
||||
>>> me = db(db.person.id==person_id).select()[0]
|
||||
>>> me.name
|
||||
'massimo'
|
||||
>>> str(me.birth)
|
||||
'1971-12-21'
|
||||
|
||||
# resave date to ensure it comes back the same
|
||||
>>> me=db(db.person.id==person_id).update(birth=me.birth) # test update
|
||||
>>> me = db(db.person.id==person_id).select()[0]
|
||||
>>> me.birth
|
||||
datetime.date(1971, 12, 21)
|
||||
>>> db(db.person.id==marco_id).delete() # test delete
|
||||
>>> len(db(db.person.id==marco_id).select())
|
||||
0
|
||||
|
||||
Update a single record
|
||||
|
||||
>>> me.update_record(name=\"Max\")
|
||||
>>> me.name
|
||||
'Max'
|
||||
>>> me = db(db.person.id == person_id).select()[0]
|
||||
>>> me.name
|
||||
'Max'
|
||||
|
||||
"""
|
||||
|
||||
SQLField = Field
|
||||
SQLTable = Table
|
||||
SQLXorable = Expression
|
||||
SQLQuery = Query
|
||||
SQLSet = Set
|
||||
SQLRows = Rows
|
||||
SQLStorage = DALStorage
|
||||
|
||||
if __name__ == '__main__':
|
||||
import doctest
|
||||
doctest.testmod()
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""`cssmin` - A Python port of the YUI CSS compressor."""
|
||||
|
||||
"""
|
||||
Home page: https://github.com/zacharyvoase/cssmin
|
||||
License: BSD: https://github.com/zacharyvoase/cssmin/blob/master/LICENSE
|
||||
Original author: Zachary Voase
|
||||
Modified for inclusion into web2py by: Ross Peoples <ross.peoples@gmail.com>
|
||||
"""
|
||||
|
||||
|
||||
from StringIO import StringIO # The pure-Python StringIO supports unicode.
|
||||
import re
|
||||
|
||||
|
||||
__version__ = '0.1.4'
|
||||
|
||||
|
||||
def remove_comments(css):
|
||||
"""Remove all CSS comment blocks."""
|
||||
|
||||
iemac = False
|
||||
preserve = False
|
||||
comment_start = css.find("/*")
|
||||
while comment_start >= 0:
|
||||
# Preserve comments that look like `/*!...*/`.
|
||||
# Slicing is used to make sure we don"t get an IndexError.
|
||||
preserve = css[comment_start + 2:comment_start + 3] == "!"
|
||||
|
||||
comment_end = css.find("*/", comment_start + 2)
|
||||
if comment_end < 0:
|
||||
if not preserve:
|
||||
css = css[:comment_start]
|
||||
break
|
||||
elif comment_end >= (comment_start + 2):
|
||||
if css[comment_end - 1] == "\\":
|
||||
# This is an IE Mac-specific comment; leave this one and the
|
||||
# following one alone.
|
||||
comment_start = comment_end + 2
|
||||
iemac = True
|
||||
elif iemac:
|
||||
comment_start = comment_end + 2
|
||||
iemac = False
|
||||
elif not preserve:
|
||||
css = css[:comment_start] + css[comment_end + 2:]
|
||||
else:
|
||||
comment_start = comment_end + 2
|
||||
comment_start = css.find("/*", comment_start)
|
||||
|
||||
return css
|
||||
|
||||
|
||||
def remove_unnecessary_whitespace(css):
|
||||
"""Remove unnecessary whitespace characters."""
|
||||
|
||||
def pseudoclasscolon(css):
|
||||
|
||||
"""
|
||||
Prevents 'p :link' from becoming 'p:link'.
|
||||
|
||||
Translates 'p :link' into 'p ___PSEUDOCLASSCOLON___link'; this is
|
||||
translated back again later.
|
||||
"""
|
||||
|
||||
regex = re.compile(r"(^|\})(([^\{\:])+\:)+([^\{]*\{)")
|
||||
match = regex.search(css)
|
||||
while match:
|
||||
css = ''.join([
|
||||
css[:match.start()],
|
||||
match.group().replace(":", "___PSEUDOCLASSCOLON___"),
|
||||
css[match.end():]])
|
||||
match = regex.search(css)
|
||||
return css
|
||||
|
||||
css = pseudoclasscolon(css)
|
||||
# Remove spaces from before things.
|
||||
css = re.sub(r"\s+([!{};:>+\(\)\],])", r"\1", css)
|
||||
|
||||
# If there is a `@charset`, then only allow one, and move to the beginning.
|
||||
css = re.sub(r"^(.*)(@charset \"[^\"]*\";)", r"\2\1", css)
|
||||
css = re.sub(r"^(\s*@charset [^;]+;\s*)+", r"\1", css)
|
||||
|
||||
# Put the space back in for a few cases, such as `@media screen` and
|
||||
# `(-webkit-min-device-pixel-ratio:0)`.
|
||||
css = re.sub(r"\band\(", "and (", css)
|
||||
|
||||
# Put the colons back.
|
||||
css = css.replace('___PSEUDOCLASSCOLON___', ':')
|
||||
|
||||
# Remove spaces from after things.
|
||||
css = re.sub(r"([!{}:;>+\(\[,])\s+", r"\1", css)
|
||||
|
||||
return css
|
||||
|
||||
|
||||
def remove_unnecessary_semicolons(css):
|
||||
"""Remove unnecessary semicolons."""
|
||||
|
||||
return re.sub(r";+\}", "}", css)
|
||||
|
||||
|
||||
def remove_empty_rules(css):
|
||||
"""Remove empty rules."""
|
||||
|
||||
return re.sub(r"[^\}\{]+\{\}", "", css)
|
||||
|
||||
|
||||
def normalize_rgb_colors_to_hex(css):
|
||||
"""Convert `rgb(51,102,153)` to `#336699`."""
|
||||
|
||||
regex = re.compile(r"rgb\s*\(\s*([0-9,\s]+)\s*\)")
|
||||
match = regex.search(css)
|
||||
while match:
|
||||
colors = map(lambda s: s.strip(), match.group(1).split(","))
|
||||
hexcolor = '#%.2x%.2x%.2x' % tuple(map(int, colors))
|
||||
css = css.replace(match.group(), hexcolor)
|
||||
match = regex.search(css)
|
||||
return css
|
||||
|
||||
|
||||
def condense_zero_units(css):
|
||||
"""Replace `0(px, em, %, etc)` with `0`."""
|
||||
|
||||
return re.sub(r"([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", r"\1\2", css)
|
||||
|
||||
|
||||
def condense_multidimensional_zeros(css):
|
||||
"""Replace `:0 0 0 0;`, `:0 0 0;` etc. with `:0;`."""
|
||||
|
||||
css = css.replace(":0 0 0 0;", ":0;")
|
||||
css = css.replace(":0 0 0;", ":0;")
|
||||
css = css.replace(":0 0;", ":0;")
|
||||
|
||||
# Revert `background-position:0;` to the valid `background-position:0 0;`.
|
||||
css = css.replace("background-position:0;", "background-position:0 0;")
|
||||
|
||||
return css
|
||||
|
||||
|
||||
def condense_floating_points(css):
|
||||
"""Replace `0.6` with `.6` where possible."""
|
||||
|
||||
return re.sub(r"(:|\s)0+\.(\d+)", r"\1.\2", css)
|
||||
|
||||
|
||||
def condense_hex_colors(css):
|
||||
"""Shorten colors from #AABBCC to #ABC where possible."""
|
||||
|
||||
regex = re.compile(r"([^\"'=\s])(\s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])")
|
||||
match = regex.search(css)
|
||||
while match:
|
||||
first = match.group(3) + match.group(5) + match.group(7)
|
||||
second = match.group(4) + match.group(6) + match.group(8)
|
||||
if first.lower() == second.lower():
|
||||
css = css.replace(match.group(), match.group(1) + match.group(2) + '#' + first)
|
||||
match = regex.search(css, match.end() - 3)
|
||||
else:
|
||||
match = regex.search(css, match.end())
|
||||
return css
|
||||
|
||||
|
||||
def condense_whitespace(css):
|
||||
"""Condense multiple adjacent whitespace characters into one."""
|
||||
|
||||
return re.sub(r"\s+", " ", css)
|
||||
|
||||
|
||||
def condense_semicolons(css):
|
||||
"""Condense multiple adjacent semicolon characters into one."""
|
||||
|
||||
return re.sub(r";;+", ";", css)
|
||||
|
||||
|
||||
def wrap_css_lines(css, line_length):
|
||||
"""Wrap the lines of the given CSS to an approximate length."""
|
||||
|
||||
lines = []
|
||||
line_start = 0
|
||||
for i, char in enumerate(css):
|
||||
# It's safe to break after `}` characters.
|
||||
if char == '}' and (i - line_start >= line_length):
|
||||
lines.append(css[line_start:i + 1])
|
||||
line_start = i + 1
|
||||
|
||||
if line_start < len(css):
|
||||
lines.append(css[line_start:])
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def cssmin(css, wrap=None):
|
||||
css = remove_comments(css)
|
||||
css = condense_whitespace(css)
|
||||
# A pseudo class for the Box Model Hack
|
||||
# (see http://tantek.com/CSS/Examples/boxmodelhack.html)
|
||||
css = css.replace('"\\"}\\""', "___PSEUDOCLASSBMH___")
|
||||
css = remove_unnecessary_whitespace(css)
|
||||
css = remove_unnecessary_semicolons(css)
|
||||
css = condense_zero_units(css)
|
||||
css = condense_multidimensional_zeros(css)
|
||||
css = condense_floating_points(css)
|
||||
css = normalize_rgb_colors_to_hex(css)
|
||||
css = condense_hex_colors(css)
|
||||
if wrap is not None:
|
||||
css = wrap_css_lines(css, wrap)
|
||||
css = css.replace("___PSEUDOCLASSBMH___", '"\\"}\\""')
|
||||
css = condense_semicolons(css)
|
||||
return css.strip()
|
||||
|
||||
|
||||
def main():
|
||||
import optparse
|
||||
import sys
|
||||
|
||||
p = optparse.OptionParser(
|
||||
prog="cssmin", version=__version__,
|
||||
usage="%prog [--wrap N]",
|
||||
description="""Reads raw CSS from stdin, and writes compressed CSS to stdout.""")
|
||||
|
||||
p.add_option(
|
||||
'-w', '--wrap', type='int', default=None, metavar='N',
|
||||
help="Wrap output to approximately N chars per line.")
|
||||
|
||||
options, args = p.parse_args()
|
||||
sys.stdout.write(cssmin(sys.stdin.read(), wrap=options.wrap))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,374 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: ascii -*-
|
||||
#
|
||||
# Copyright 2011
|
||||
# Andr\xe9 Malo or his licensors, as applicable
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
r"""
|
||||
=====================
|
||||
Javascript Minifier
|
||||
=====================
|
||||
|
||||
Javascript Minifier based on `jsmin.c by Douglas Crockford`_\.
|
||||
|
||||
This module is a re-implementation based on the semantics of jsmin.c. Usually
|
||||
it produces the same results. It differs in the following ways:
|
||||
|
||||
- there is no error detection: unterminated string, regex and comment
|
||||
literals are treated as regular javascript code and minified as such.
|
||||
- Control characters inside string and regex literals are left untouched; they
|
||||
are not converted to spaces (nor to \n)
|
||||
- Newline characters are not allowed inside string and regex literals, except
|
||||
for line continuations in string literals (ECMA-5).
|
||||
- "return /regex/" is recognized correctly.
|
||||
- rjsmin does not handle streams, but only complete strings. (However, the
|
||||
module provides a "streamy" interface).
|
||||
|
||||
Besides the list above it differs from direct python ports of jsmin.c in
|
||||
speed. Since most parts of the logic are handled by the regex engine it's way
|
||||
faster than the original python port by Baruch Even. The speed factor varies
|
||||
between about 6 and 55 depending on input and python version (it gets faster
|
||||
the more compressed the input already is). Compared to the speed-refactored
|
||||
python port by Dave St.Germain the performance gain is less dramatic but still
|
||||
between 1.2 and 7. See the docs/BENCHMARKS file for details.
|
||||
|
||||
rjsmin.c is a reimplementation of rjsmin.py in C and speeds it up even more.
|
||||
|
||||
Both python 2 and python 3 are supported.
|
||||
|
||||
.. _jsmin.c by Douglas Crockford:
|
||||
http://www.crockford.com/javascript/jsmin.c
|
||||
|
||||
Original author of Python version: Andr\xe9 Malo
|
||||
Home page: http://opensource.perlig.de/rjsmin/
|
||||
Modified by Ross Peoples <ross.peoples@gmail.com> for inclusion into web2py.
|
||||
"""
|
||||
__author__ = "Andr\xe9 Malo"
|
||||
__author__ = getattr(__author__, 'decode', lambda x: __author__)('latin-1')
|
||||
__docformat__ = "restructuredtext en"
|
||||
__license__ = "Apache License, Version 2.0"
|
||||
__version__ = '1.0.2'
|
||||
__all__ = ['jsmin', 'jsmin_for_posers']
|
||||
|
||||
import re as _re
|
||||
|
||||
|
||||
def _make_jsmin(extended=True, python_only=True):
|
||||
"""
|
||||
Generate JS minifier based on `jsmin.c by Douglas Crockford`_
|
||||
|
||||
.. _jsmin.c by Douglas Crockford:
|
||||
http://www.crockford.com/javascript/jsmin.c
|
||||
|
||||
:Parameters:
|
||||
`extended` : ``bool``
|
||||
Extended Regexps? (using lookahead and lookbehind). This is faster,
|
||||
because it can be optimized way more. The regexps used with `extended`
|
||||
being false are only left here to allow easier porting to platforms
|
||||
without extended regex features (and for my own reference...)
|
||||
|
||||
`python_only` : ``bool``
|
||||
Use only the python variant. If true, the c extension is not even
|
||||
tried to be loaded.
|
||||
|
||||
:Return: Minifier
|
||||
:Rtype: ``callable``
|
||||
"""
|
||||
# pylint: disable = R0912, R0914, W0612
|
||||
if not python_only:
|
||||
try:
|
||||
import _rjsmin
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
return _rjsmin.jsmin
|
||||
try:
|
||||
xrange
|
||||
except NameError:
|
||||
xrange = range # pylint: disable = W0622
|
||||
|
||||
space_chars = r'[\000-\011\013\014\016-\040]'
|
||||
|
||||
line_comment = r'(?://[^\r\n]*)'
|
||||
space_comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
|
||||
string1 = \
|
||||
r'(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|\r)[^\047\\\r\n]*)*\047)'
|
||||
string2 = r'(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|\r)[^"\\\r\n]*)*")'
|
||||
strings = r'(?:%s|%s)' % (string1, string2)
|
||||
|
||||
charclass = r'(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\])'
|
||||
nospecial = r'[^/\\\[\r\n]'
|
||||
if extended:
|
||||
regex = r'(?:/(?![\r\n/*])%s*(?:(?:\\[^\r\n]|%s)%s*)*/)' % (
|
||||
nospecial, charclass, nospecial
|
||||
)
|
||||
else:
|
||||
regex = (
|
||||
r'(?:/(?:[^*/\\\r\n\[]|%s|\\[^\r\n])%s*(?:(?:\\[^\r\n]|%s)%s*)*/)'
|
||||
)
|
||||
regex = regex % (charclass, nospecial, charclass, nospecial)
|
||||
|
||||
space = r'(?:%s|%s)' % (space_chars, space_comment)
|
||||
newline = r'(?:%s?[\r\n])' % line_comment
|
||||
|
||||
def fix_charclass(result):
|
||||
""" Fixup string of chars to fit into a regex char class """
|
||||
pos = result.find('-')
|
||||
if pos >= 0:
|
||||
result = r'%s%s-' % (result[:pos], result[pos + 1:])
|
||||
|
||||
def sequentize(string):
|
||||
"""
|
||||
Notate consecutive characters as sequence
|
||||
|
||||
(1-4 instead of 1234)
|
||||
"""
|
||||
first, last, result = None, None, []
|
||||
for char in map(ord, string):
|
||||
if last is None:
|
||||
first = last = char
|
||||
elif last + 1 == char:
|
||||
last = char
|
||||
else:
|
||||
result.append((first, last))
|
||||
first = last = char
|
||||
if last is not None:
|
||||
result.append((first, last))
|
||||
return ''.join(['%s%s%s' % (
|
||||
chr(first),
|
||||
last > first + 1 and '-' or '',
|
||||
last != first and chr(last) or ''
|
||||
) for first, last in result])
|
||||
|
||||
return _re.sub(r'([\000-\040\047])', # for better portability
|
||||
lambda m: '\\%03o' % ord(m.group(1)), (sequentize(result)
|
||||
.replace('\\', '\\\\')
|
||||
.replace('[', '\\[')
|
||||
.replace(']', '\\]')
|
||||
)
|
||||
)
|
||||
|
||||
def id_literal_(what):
|
||||
""" Make id_literal like char class """
|
||||
match = _re.compile(what).match
|
||||
result = ''.join([
|
||||
chr(c) for c in xrange(127) if not match(chr(c))
|
||||
])
|
||||
return '[^%s]' % fix_charclass(result)
|
||||
|
||||
def not_id_literal_(keep):
|
||||
""" Make negated id_literal like char class """
|
||||
match = _re.compile(id_literal_(keep)).match
|
||||
result = ''.join([
|
||||
chr(c) for c in xrange(127) if not match(chr(c))
|
||||
])
|
||||
return r'[%s]' % fix_charclass(result)
|
||||
|
||||
not_id_literal = not_id_literal_(r'[a-zA-Z0-9_$]')
|
||||
preregex1 = r'[(,=:\[!&|?{};\r\n]'
|
||||
preregex2 = r'%(not_id_literal)sreturn' % locals()
|
||||
|
||||
if extended:
|
||||
id_literal = id_literal_(r'[a-zA-Z0-9_$]')
|
||||
id_literal_open = id_literal_(r'[a-zA-Z0-9_${\[(+-]')
|
||||
id_literal_close = id_literal_(r'[a-zA-Z0-9_$}\])"\047+-]')
|
||||
|
||||
space_sub = _re.compile((
|
||||
r'([^\047"/\000-\040]+)'
|
||||
r'|(%(strings)s[^\047"/\000-\040]*)'
|
||||
r'|(?:(?<=%(preregex1)s)%(space)s*(%(regex)s[^\047"/\000-\040]*))'
|
||||
r'|(?:(?<=%(preregex2)s)%(space)s*(%(regex)s[^\047"/\000-\040]*))'
|
||||
r'|(?<=%(id_literal_close)s)'
|
||||
r'%(space)s*(?:(%(newline)s)%(space)s*)+'
|
||||
r'(?=%(id_literal_open)s)'
|
||||
r'|(?<=%(id_literal)s)(%(space)s)+(?=%(id_literal)s)'
|
||||
r'|%(space)s+'
|
||||
r'|(?:%(newline)s%(space)s*)+'
|
||||
) % locals()).sub
|
||||
def space_subber(match):
|
||||
""" Substitution callback """
|
||||
# pylint: disable = C0321, R0911
|
||||
groups = match.groups()
|
||||
if groups[0]: return groups[0]
|
||||
elif groups[1]: return groups[1]
|
||||
elif groups[2]: return groups[2]
|
||||
elif groups[3]: return groups[3]
|
||||
elif groups[4]: return '\n'
|
||||
elif groups[5]: return ' '
|
||||
else: return ''
|
||||
|
||||
def jsmin(script): # pylint: disable = W0621
|
||||
r"""
|
||||
Minify javascript based on `jsmin.c by Douglas Crockford`_\.
|
||||
|
||||
Instead of parsing the stream char by char, it uses a regular
|
||||
expression approach which minifies the whole script with one big
|
||||
substitution regex.
|
||||
|
||||
.. _jsmin.c by Douglas Crockford:
|
||||
http://www.crockford.com/javascript/jsmin.c
|
||||
|
||||
:Parameters:
|
||||
`script` : ``str``
|
||||
Script to minify
|
||||
|
||||
:Return: Minified script
|
||||
:Rtype: ``str``
|
||||
"""
|
||||
return space_sub(space_subber, '\n%s\n' % script).strip()
|
||||
|
||||
else:
|
||||
pre_regex = r'(?:%(preregex1)s|%(preregex2)s)' % locals()
|
||||
not_id_literal_open = not_id_literal_(r'[a-zA-Z0-9_${\[(+-]')
|
||||
not_id_literal_close = not_id_literal_(r'[a-zA-Z0-9_$}\])"\047+-]')
|
||||
|
||||
space_norm_sub = _re.compile((
|
||||
r'(%(strings)s)'
|
||||
r'|(?:(%(pre_regex)s)%(space)s*(%(regex)s))'
|
||||
r'|(%(space)s)+'
|
||||
r'|(?:(%(newline)s)%(space)s*)+'
|
||||
) % locals()).sub
|
||||
def space_norm_subber(match):
|
||||
""" Substitution callback """
|
||||
# pylint: disable = C0321
|
||||
groups = match.groups()
|
||||
if groups[0]: return groups[0]
|
||||
elif groups[1]: return groups[1].replace('\r', '\n') + groups[2]
|
||||
elif groups[3]: return ' '
|
||||
elif groups[4]: return '\n'
|
||||
|
||||
space_sub1 = _re.compile((
|
||||
r'[\040\n]?(%(strings)s|%(pre_regex)s%(regex)s)'
|
||||
r'|\040(%(not_id_literal)s)'
|
||||
r'|\n(%(not_id_literal_open)s)'
|
||||
) % locals()).sub
|
||||
def space_subber1(match):
|
||||
""" Substitution callback """
|
||||
groups = match.groups()
|
||||
return groups[0] or groups[1] or groups[2]
|
||||
|
||||
space_sub2 = _re.compile((
|
||||
r'(%(strings)s)\040?'
|
||||
r'|(%(pre_regex)s%(regex)s)[\040\n]?'
|
||||
r'|(%(not_id_literal)s)\040'
|
||||
r'|(%(not_id_literal_close)s)\n'
|
||||
) % locals()).sub
|
||||
def space_subber2(match):
|
||||
""" Substitution callback """
|
||||
groups = match.groups()
|
||||
return groups[0] or groups[1] or groups[2] or groups[3]
|
||||
|
||||
def jsmin(script):
|
||||
r"""
|
||||
Minify javascript based on `jsmin.c by Douglas Crockford`_\.
|
||||
|
||||
Instead of parsing the stream char by char, it uses a regular
|
||||
expression approach. The script is minified with three passes:
|
||||
|
||||
normalization
|
||||
Control character are mapped to spaces, spaces and newlines
|
||||
are squeezed and comments are stripped.
|
||||
space removal 1
|
||||
Spaces before certain tokens are removed
|
||||
space removal 2
|
||||
Spaces after certain tokens are remove
|
||||
|
||||
.. _jsmin.c by Douglas Crockford:
|
||||
http://www.crockford.com/javascript/jsmin.c
|
||||
|
||||
:Parameters:
|
||||
`script` : ``str``
|
||||
Script to minify
|
||||
|
||||
:Return: Minified script
|
||||
:Rtype: ``str``
|
||||
"""
|
||||
return space_sub2(space_subber2,
|
||||
space_sub1(space_subber1,
|
||||
space_norm_sub(space_norm_subber, '\n%s\n' % script)
|
||||
)
|
||||
).strip()
|
||||
return jsmin
|
||||
|
||||
jsmin = _make_jsmin()
|
||||
|
||||
#####################
|
||||
# EXAMPLE USAGE #
|
||||
#####################
|
||||
#
|
||||
# import jsmin
|
||||
# jsmin.jsmin(script)
|
||||
#
|
||||
|
||||
def jsmin_for_posers(script):
|
||||
r"""
|
||||
Minify javascript based on `jsmin.c by Douglas Crockford`_\.
|
||||
|
||||
Instead of parsing the stream char by char, it uses a regular
|
||||
expression approach which minifies the whole script with one big
|
||||
substitution regex.
|
||||
|
||||
.. _jsmin.c by Douglas Crockford:
|
||||
http://www.crockford.com/javascript/jsmin.c
|
||||
|
||||
:Warning: This function is the digest of a _make_jsmin() call. It just
|
||||
utilizes the resulting regex. It's just for fun here and may
|
||||
vanish any time. Use the `jsmin` function instead.
|
||||
|
||||
:Parameters:
|
||||
`script` : ``str``
|
||||
Script to minify
|
||||
|
||||
:Return: Minified script
|
||||
:Rtype: ``str``
|
||||
"""
|
||||
def subber(match):
|
||||
""" Substitution callback """
|
||||
groups = match.groups()
|
||||
return (
|
||||
groups[0] or
|
||||
groups[1] or
|
||||
groups[2] or
|
||||
groups[3] or
|
||||
(groups[4] and '\n') or
|
||||
(groups[5] and ' ') or
|
||||
''
|
||||
)
|
||||
|
||||
return _re.sub(
|
||||
r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
|
||||
r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
|
||||
r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
|
||||
r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
|
||||
r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
|
||||
r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
|
||||
r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
|
||||
r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
|
||||
r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
|
||||
r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
|
||||
r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
|
||||
r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
|
||||
r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-#%-\04'
|
||||
r'7)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-\011'
|
||||
r'\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^\000-'
|
||||
r'#%-,./:-@\[-^`{-~-])|(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*'
|
||||
r'+(?:[^/*][^*]*\*+)*/))+|(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011'
|
||||
r'\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+',
|
||||
subber, '\n%s\n' % script
|
||||
).strip()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys as _sys
|
||||
_sys.stdout.write(jsmin(_sys.stdin.read()))
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
High-level CSS and JS minification class for web2py.
|
||||
Called by response.include_files()
|
||||
Created by: Ross Peoples <ross.peoples@gmail.com>
|
||||
Modified by: Massimo Di Pierro <massimo.dipierro@gmail.com>
|
||||
"""
|
||||
|
||||
import cssmin
|
||||
import jsmin
|
||||
import os
|
||||
|
||||
def read_binary_file(filename):
|
||||
f = open(filename,'rb')
|
||||
data = f.read()
|
||||
f.close()
|
||||
return data
|
||||
|
||||
def write_binary_file(filename,data):
|
||||
f =open(filename,'wb')
|
||||
f.write(data)
|
||||
f.close()
|
||||
|
||||
def fix_links(css,static_path):
|
||||
return css.replace('../',static_path+'/')
|
||||
|
||||
def minify(files, path_info, folder, optimize_css, optimize_js,
|
||||
ignore_concat = [],
|
||||
ignore_minity = ['/jquery.js', '/anytime.js']):
|
||||
|
||||
"""
|
||||
Input:
|
||||
files: is a list of URLs to JS and CSS files (not repeated)
|
||||
path_info: is the URL of a temp static folder
|
||||
folder: is the application folder
|
||||
optimize_css: is a string of the form 'concat|minify|inline'
|
||||
optimize_js: is a string of the form 'concat|minify|inline'
|
||||
(minify requires concat, inline requires concat also)
|
||||
|
||||
Returns a new list of:
|
||||
- filename (absolute or relative, css or js, actual or temporary) or
|
||||
- ('css:inline','...css..')
|
||||
- ('js:inline','...js..')
|
||||
"""
|
||||
concat_css = 'concat' in optimize_css
|
||||
minify_css = 'minify' in optimize_css
|
||||
inline_css = 'inline' in optimize_css
|
||||
concat_js = 'concat' in optimize_js
|
||||
minify_js = 'minify' in optimize_js
|
||||
inline_js = 'inline' in optimize_js
|
||||
static_path,temp = path_info.rsplit('/',1)
|
||||
new_files = []
|
||||
css = []
|
||||
js = []
|
||||
for k,filename in enumerate(files):
|
||||
if not filename.startswith('/') or \
|
||||
any(filename.endswith(x) for x in ignore_concat):
|
||||
new_files.append(filename)
|
||||
continue
|
||||
|
||||
abs_filename = os.path.join(folder,'static',
|
||||
filename[len(static_path)+1:])
|
||||
|
||||
if filename.lower().endswith('.css'):
|
||||
if concat_css:
|
||||
contents = read_binary_file(abs_filename)
|
||||
if minify_css:
|
||||
css.append(cssmin.cssmin(contents))
|
||||
else:
|
||||
css.append(contents)
|
||||
else:
|
||||
css.append(filename)
|
||||
elif filename.lower().endswith('.js'):
|
||||
if concat_js:
|
||||
contents = read_binary_file(abs_filename)
|
||||
if minify_js and not filename.endswith('.min.js') and \
|
||||
not any(filename.endswith(x) for x in ignore_minify):
|
||||
js.append(jsmin.jsmin(contents))
|
||||
else:
|
||||
js.append(contents)
|
||||
else:
|
||||
js.append(filename)
|
||||
if css and concat_css:
|
||||
css = '\n\n'.join(contents for contents in css)
|
||||
if inline_css:
|
||||
css = ('css:inline',fix_links(css,static_path))
|
||||
else:
|
||||
temppath = os.path.join(folder,'static',temp)
|
||||
if not os.path.exists(temppath): os.mkdir(temppath)
|
||||
tempfile = os.path.join(temppath,'compressed.css')
|
||||
write_binary_file(tempfile,css)
|
||||
css = path_info+'/compressed.css'
|
||||
new_files.append(css)
|
||||
else:
|
||||
new_files += css
|
||||
if js and concat_js:
|
||||
js = '\n'.join(contents for contents in js)
|
||||
if inline_js:
|
||||
js = ('js:inline',js)
|
||||
else:
|
||||
temppath = os.path.join(folder,'static',temp)
|
||||
if not os.path.exists(temppath): os.mkdir(temppath)
|
||||
tempfile = os.path.join(folder,'static',temp,'compressed.js')
|
||||
write_binary_file(tempfile,js)
|
||||
js = path_info+'/compressed.js'
|
||||
new_files.append(js)
|
||||
else:
|
||||
new_files += js
|
||||
return new_files
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# (c) 2007 Chris AtLee <chris@atlee.ca>
|
||||
# Licensed under the MIT license:
|
||||
# http://www.opensource.org/licenses/mit-license.php
|
||||
"""
|
||||
PAM module for python
|
||||
|
||||
Provides an authenticate function that will allow the caller to authenticate
|
||||
a user against the Pluggable Authentication Modules (PAM) on the system.
|
||||
|
||||
Implemented using ctypes, so no compilation is necessary.
|
||||
"""
|
||||
__all__ = ['authenticate']
|
||||
|
||||
from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, pointer, sizeof
|
||||
from ctypes import c_void_p, c_uint, c_char_p, c_char, c_int
|
||||
from ctypes.util import find_library
|
||||
|
||||
LIBPAM = CDLL(find_library("pam"))
|
||||
LIBC = CDLL(find_library("c"))
|
||||
|
||||
CALLOC = LIBC.calloc
|
||||
CALLOC.restype = c_void_p
|
||||
CALLOC.argtypes = [c_uint, c_uint]
|
||||
|
||||
STRDUP = LIBC.strdup
|
||||
STRDUP.argstypes = [c_char_p]
|
||||
STRDUP.restype = POINTER(c_char) # NOT c_char_p !!!!
|
||||
|
||||
# Various constants
|
||||
PAM_PROMPT_ECHO_OFF = 1
|
||||
PAM_PROMPT_ECHO_ON = 2
|
||||
PAM_ERROR_MSG = 3
|
||||
PAM_TEXT_INFO = 4
|
||||
|
||||
class PamHandle(Structure):
|
||||
"""wrapper class for pam_handle_t"""
|
||||
_fields_ = [
|
||||
("handle", c_void_p)
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
Structure.__init__(self)
|
||||
self.handle = 0
|
||||
|
||||
class PamMessage(Structure):
|
||||
"""wrapper class for pam_message structure"""
|
||||
_fields_ = [
|
||||
("msg_style", c_int),
|
||||
("msg", c_char_p),
|
||||
]
|
||||
|
||||
def __repr__(self):
|
||||
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
|
||||
|
||||
class PamResponse(Structure):
|
||||
"""wrapper class for pam_response structure"""
|
||||
_fields_ = [
|
||||
("resp", c_char_p),
|
||||
("resp_retcode", c_int),
|
||||
]
|
||||
|
||||
def __repr__(self):
|
||||
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
|
||||
|
||||
CONV_FUNC = CFUNCTYPE(c_int,
|
||||
c_int, POINTER(POINTER(PamMessage)),
|
||||
POINTER(POINTER(PamResponse)), c_void_p)
|
||||
|
||||
class PamConv(Structure):
|
||||
"""wrapper class for pam_conv structure"""
|
||||
_fields_ = [
|
||||
("conv", CONV_FUNC),
|
||||
("appdata_ptr", c_void_p)
|
||||
]
|
||||
|
||||
PAM_START = LIBPAM.pam_start
|
||||
PAM_START.restype = c_int
|
||||
PAM_START.argtypes = [c_char_p, c_char_p, POINTER(PamConv),
|
||||
POINTER(PamHandle)]
|
||||
|
||||
PAM_AUTHENTICATE = LIBPAM.pam_authenticate
|
||||
PAM_AUTHENTICATE.restype = c_int
|
||||
PAM_AUTHENTICATE.argtypes = [PamHandle, c_int]
|
||||
|
||||
def authenticate(username, password, service='login'):
|
||||
"""Returns True if the given username and password authenticate for the
|
||||
given service. Returns False otherwise
|
||||
|
||||
``username``: the username to authenticate
|
||||
|
||||
``password``: the password in plain text
|
||||
|
||||
``service``: the PAM service to authenticate against.
|
||||
Defaults to 'login'"""
|
||||
@CONV_FUNC
|
||||
def my_conv(n_messages, messages, p_response, app_data):
|
||||
"""Simple conversation function that responds to any
|
||||
prompt where the echo is off with the supplied password"""
|
||||
# Create an array of n_messages response objects
|
||||
addr = CALLOC(n_messages, sizeof(PamResponse))
|
||||
p_response[0] = cast(addr, POINTER(PamResponse))
|
||||
for i in range(n_messages):
|
||||
if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF:
|
||||
pw_copy = STRDUP(str(password))
|
||||
p_response.contents[i].resp = cast(pw_copy, c_char_p)
|
||||
p_response.contents[i].resp_retcode = 0
|
||||
return 0
|
||||
|
||||
handle = PamHandle()
|
||||
conv = PamConv(my_conv, 0)
|
||||
retval = PAM_START(service, username, pointer(conv), pointer(handle))
|
||||
|
||||
if retval != 0:
|
||||
# TODO: This is not an authentication error, something
|
||||
# has gone wrong starting up PAM
|
||||
return False
|
||||
|
||||
retval = PAM_AUTHENTICATE(handle, 0)
|
||||
return retval == 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
import getpass
|
||||
print authenticate(getpass.getuser(), getpass.getpass())
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
Read more about this http://code.google.com/p/pyfpdf
|
||||
@@ -0,0 +1,5 @@
|
||||
from fpdf import FPDF
|
||||
from html import HTMLMixin
|
||||
from template import Template
|
||||
|
||||
|
||||
@@ -0,0 +1,736 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: latin-1 -*-
|
||||
# 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 3, 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 MERCHANTIBILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
# for more details.
|
||||
|
||||
"Visual Template designer for PyFPDF (using wxPython OGL library)"
|
||||
|
||||
__author__ = "Mariano Reingart <reingart@gmail.com>"
|
||||
__copyright__ = "Copyright (C) 2011 Mariano Reingart"
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.01a"
|
||||
|
||||
# Based on:
|
||||
# * pySjetch.py wxPython sample application
|
||||
# * OGL.py and other wxPython demo modules
|
||||
|
||||
|
||||
import os, sys
|
||||
import wx
|
||||
import wx.lib.ogl as ogl
|
||||
from wx.lib.wordwrap import wordwrap
|
||||
|
||||
DEBUG = True
|
||||
|
||||
|
||||
class CustomDialog(wx.Dialog):
|
||||
"A dinamyc dialog to ask user about arbitrary fields"
|
||||
|
||||
def __init__(
|
||||
self, parent, ID, title, size=wx.DefaultSize, pos=wx.DefaultPosition,
|
||||
style=wx.DEFAULT_DIALOG_STYLE, fields=None, data=None,
|
||||
):
|
||||
|
||||
wx.Dialog.__init__ (self, parent, ID, title, pos, size, style)
|
||||
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
self.textctrls = {}
|
||||
for field in fields:
|
||||
box = wx.BoxSizer(wx.HORIZONTAL)
|
||||
label = wx.StaticText(self, -1, field)
|
||||
label.SetHelpText("This is the help text for the label")
|
||||
box.Add(label, 1, wx.ALIGN_CENTRE|wx.ALL, 5)
|
||||
text = wx.TextCtrl(self, -1, "", size=(80,-1))
|
||||
text.SetHelpText("Here's some help text for field #1")
|
||||
if field in data:
|
||||
text.SetValue(repr(data[field]))
|
||||
box.Add(text, 1, wx.ALIGN_CENTRE|wx.ALL, 1)
|
||||
sizer.Add(box, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 1)
|
||||
self.textctrls[field] = text
|
||||
|
||||
line = wx.StaticLine(self, -1, size=(20,-1), style=wx.LI_HORIZONTAL)
|
||||
sizer.Add(line, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.RIGHT|wx.TOP, 5)
|
||||
|
||||
btnsizer = wx.StdDialogButtonSizer()
|
||||
|
||||
btn = wx.Button(self, wx.ID_OK)
|
||||
btn.SetHelpText("The OK button completes the dialog")
|
||||
btn.SetDefault()
|
||||
btnsizer.AddButton(btn)
|
||||
|
||||
btn = wx.Button(self, wx.ID_CANCEL)
|
||||
btn.SetHelpText("The Cancel button cancels the dialog. (Cool, huh?)")
|
||||
btnsizer.AddButton(btn)
|
||||
btnsizer.Realize()
|
||||
|
||||
sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
|
||||
|
||||
self.SetSizer(sizer)
|
||||
sizer.Fit(self)
|
||||
|
||||
@classmethod
|
||||
def do_input(Class, parent, title, fields, data):
|
||||
dlg = Class(parent, -1, title, size=(350, 200),
|
||||
style=wx.DEFAULT_DIALOG_STYLE, # & ~wx.CLOSE_BOX,
|
||||
fields=fields, data=data
|
||||
)
|
||||
dlg.CenterOnScreen()
|
||||
while 1:
|
||||
val = dlg.ShowModal()
|
||||
if val == wx.ID_OK:
|
||||
values = {}
|
||||
for field in fields:
|
||||
try:
|
||||
values[field] = eval(dlg.textctrls[field].GetValue())
|
||||
except Exception, e:
|
||||
msg = wx.MessageDialog(parent, unicode(e),
|
||||
"Error in field %s" % field,
|
||||
wx.OK | wx.ICON_INFORMATION
|
||||
)
|
||||
msg.ShowModal()
|
||||
msg.Destroy()
|
||||
break
|
||||
else:
|
||||
return dict([(field, values[field]) for field in fields])
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class MyEvtHandler(ogl.ShapeEvtHandler):
|
||||
"Custom Event Handler for Shapes"
|
||||
def __init__(self, callback):
|
||||
ogl.ShapeEvtHandler.__init__(self)
|
||||
self.callback = callback
|
||||
|
||||
def OnLeftClick(self, x, y, keys=0, attachment=0):
|
||||
shape = self.GetShape()
|
||||
canvas = shape.GetCanvas()
|
||||
dc = wx.ClientDC(canvas)
|
||||
canvas.PrepareDC(dc)
|
||||
|
||||
if shape.Selected() and keys & ogl.KEY_SHIFT:
|
||||
shape.Select(False, dc)
|
||||
#canvas.Redraw(dc)
|
||||
canvas.Refresh(False)
|
||||
else:
|
||||
redraw = False
|
||||
shapeList = canvas.GetDiagram().GetShapeList()
|
||||
toUnselect = []
|
||||
|
||||
for s in shapeList:
|
||||
if s.Selected() and not keys & ogl.KEY_SHIFT:
|
||||
# If we unselect it now then some of the objects in
|
||||
# shapeList will become invalid (the control points are
|
||||
# shapes too!) and bad things will happen...
|
||||
toUnselect.append(s)
|
||||
|
||||
shape.Select(True, dc)
|
||||
|
||||
if toUnselect:
|
||||
for s in toUnselect:
|
||||
s.Select(False, dc)
|
||||
##canvas.Redraw(dc)
|
||||
canvas.Refresh(False)
|
||||
|
||||
self.callback()
|
||||
|
||||
def OnEndDragLeft(self, x, y, keys=0, attachment=0):
|
||||
shape = self.GetShape()
|
||||
ogl.ShapeEvtHandler.OnEndDragLeft(self, x, y, keys, attachment)
|
||||
|
||||
if not shape.Selected():
|
||||
self.OnLeftClick(x, y, keys, attachment)
|
||||
|
||||
self.callback()
|
||||
|
||||
def OnSizingEndDragLeft(self, pt, x, y, keys, attch):
|
||||
ogl.ShapeEvtHandler.OnSizingEndDragLeft(self, pt, x, y, keys, attch)
|
||||
self.callback()
|
||||
|
||||
def OnMovePost(self, dc, x, y, oldX, oldY, display):
|
||||
shape = self.GetShape()
|
||||
ogl.ShapeEvtHandler.OnMovePost(self, dc, x, y, oldX, oldY, display)
|
||||
self.callback()
|
||||
if "wxMac" in wx.PlatformInfo:
|
||||
shape.GetCanvas().Refresh(False)
|
||||
|
||||
def OnLeftDoubleClick(self, x, y, keys = 0, attachment = 0):
|
||||
self.callback("LeftDoubleClick")
|
||||
|
||||
def OnRightClick(self, *dontcare):
|
||||
self.callback("RightClick")
|
||||
|
||||
|
||||
class Element(object):
|
||||
"Visual class that represent a placeholder in the template"
|
||||
|
||||
fields = ['name', 'type',
|
||||
'x1', 'y1', 'x2', 'y2',
|
||||
'font', 'size',
|
||||
'bold', 'italic', 'underline',
|
||||
'foreground', 'background',
|
||||
'align', 'text', 'priority',]
|
||||
|
||||
def __init__(self, canvas=None, frame=None, zoom=5.0, static=False, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
self.zoom = zoom
|
||||
self.frame = frame
|
||||
self.canvas = canvas
|
||||
self.static = static
|
||||
|
||||
name = kwargs['name']
|
||||
kwargs['type']
|
||||
type = kwargs['type']
|
||||
|
||||
x, y, w, h = self.set_coordinates(kwargs['x1'], kwargs['y1'], kwargs['x2'], kwargs['y2'])
|
||||
|
||||
text = kwargs['text']
|
||||
|
||||
shape = self.shape = ogl.RectangleShape(w, h)
|
||||
|
||||
if not static:
|
||||
shape.SetDraggable(True, True)
|
||||
|
||||
shape.SetX(x)
|
||||
shape.SetY(y)
|
||||
#if pen: shape.SetPen(pen)
|
||||
#if brush: shape.SetBrush(brush)
|
||||
shape.SetBrush(wx.TRANSPARENT_BRUSH)
|
||||
|
||||
if type not in ('L', 'B', 'BC'):
|
||||
if not static:
|
||||
pen = wx.LIGHT_GREY_PEN
|
||||
else:
|
||||
pen = wx.RED_PEN
|
||||
shape.SetPen(pen)
|
||||
|
||||
self.text = kwargs['text']
|
||||
|
||||
evthandler = MyEvtHandler(self.evt_callback)
|
||||
evthandler.SetShape(shape)
|
||||
evthandler.SetPreviousHandler(shape.GetEventHandler())
|
||||
shape.SetEventHandler(evthandler)
|
||||
shape.SetCentreResize(False)
|
||||
shape.SetMaintainAspectRatio(False)
|
||||
|
||||
canvas.AddShape( shape )
|
||||
|
||||
@classmethod
|
||||
def new(Class, parent):
|
||||
data = dict(name='some_name', type='T',
|
||||
x1=5.0, y1=5.0, x2=100.0, y2=10.0,
|
||||
font="Arial", size=12,
|
||||
bold=False, italic=False, underline=False,
|
||||
foreground= 0x000000, background=0xFFFFFF,
|
||||
align="L", text="", priority=0)
|
||||
data = CustomDialog.do_input(parent, 'New element', Class.fields, data)
|
||||
if data:
|
||||
return Class(canvas=parent.canvas, frame=parent, **data)
|
||||
|
||||
def edit(self):
|
||||
"Edit current element (show a dialog box with all fields)"
|
||||
data = self.kwargs.copy()
|
||||
x1, y1, x2, y2 = self.get_coordinates()
|
||||
data.update(dict(name=self.name,
|
||||
text=self.text,
|
||||
x1=x1, y1=y1, x2=x2, y2=y2,
|
||||
))
|
||||
data = CustomDialog.do_input(self.frame, 'Edit element', self.fields, data)
|
||||
if data:
|
||||
self.kwargs.update(data)
|
||||
self.name = data['name']
|
||||
self.text = data['text']
|
||||
x,y, w, h = self.set_coordinates(data['x1'], data['y1'], data['x2'], data['y2'])
|
||||
self.shape.SetX(x)
|
||||
self.shape.SetY(y)
|
||||
self.shape.SetWidth(w)
|
||||
self.shape.SetHeight(h)
|
||||
self.canvas.Refresh(False)
|
||||
self.canvas.GetDiagram().ShowAll(1)
|
||||
|
||||
def edit_text(self):
|
||||
"Allow text edition (i.e. for doubleclick)"
|
||||
dlg = wx.TextEntryDialog(
|
||||
self.frame, 'Text for %s' % self.name,
|
||||
'Edit Text', '')
|
||||
if self.text:
|
||||
dlg.SetValue(self.text)
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
self.text = dlg.GetValue().encode("latin1")
|
||||
dlg.Destroy()
|
||||
|
||||
def copy(self):
|
||||
"Return an identical duplicate"
|
||||
kwargs = self.as_dict()
|
||||
element = Element(canvas=self.canvas, frame=self.frame, zoom=self.zoom, static=self.static, **kwargs)
|
||||
return element
|
||||
|
||||
def remove(self):
|
||||
"Erases visual shape from OGL canvas (element must be deleted manually)"
|
||||
self.canvas.RemoveShape(self.shape)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"Change pdf coordinates (converting to wx internal values)"
|
||||
x1, y1, x2, y2 = self.get_coordinates()
|
||||
x1 += dx
|
||||
x2 += dx
|
||||
y1 += dy
|
||||
y2 += dy
|
||||
x, y, w, h = self.set_coordinates(x1, y1, x2, y2)
|
||||
self.shape.SetX(x)
|
||||
self.shape.SetY(y)
|
||||
|
||||
def evt_callback(self, evt_type=None):
|
||||
"Event dispatcher"
|
||||
if evt_type=="LeftDoubleClick":
|
||||
self.edit_text()
|
||||
if evt_type=='RightClick':
|
||||
self.edit()
|
||||
|
||||
# update the status bar
|
||||
x1, y1, x2, y2 = self.get_coordinates()
|
||||
self.frame.SetStatusText("%s (%0.2f, %0.2f) - (%0.2f, %0.2f)" %
|
||||
(self.name, x1, y1, x2, y2))
|
||||
|
||||
def get_coordinates(self):
|
||||
"Convert from wx to pdf coordinates"
|
||||
x, y = self.shape.GetX(), self.shape.GetY()
|
||||
w, h = self.shape.GetBoundingBoxMax()
|
||||
w -= 1
|
||||
h -= 1
|
||||
x1 = x/self.zoom - w/self.zoom/2.0
|
||||
x2 = x/self.zoom + w/self.zoom/2.0
|
||||
y1 = y/self.zoom - h/self.zoom/2.0
|
||||
y2 = y/self.zoom + h/self.zoom/2.0
|
||||
return x1, y1, x2, y2
|
||||
|
||||
def set_coordinates(self, x1, y1, x2, y2):
|
||||
"Convert from pdf to wx coordinates"
|
||||
x1 = x1 * self.zoom
|
||||
x2 = x2 * self.zoom
|
||||
y1 = y1 * self.zoom
|
||||
y2 = y2 * self.zoom
|
||||
|
||||
# shapes seems to be centred, pdf coord not
|
||||
w = max(x1, x2) - min(x1, x2) + 1
|
||||
h = max(y1, y2) - min(y1, y2) + 1
|
||||
x = (min(x1, x2) + w/2.0)
|
||||
y = (min(y1, y2) + h/2.0)
|
||||
return x, y, w, h
|
||||
|
||||
def text(self, txt=None):
|
||||
if txt is not None:
|
||||
if not isinstance(txt,str):
|
||||
txt = str(txt)
|
||||
self.kwargs['text'] = txt
|
||||
self.shape.ClearText()
|
||||
for line in txt.split('\n'):
|
||||
self.shape.AddText(unicode(line, "latin1"))
|
||||
self.canvas.Refresh(False)
|
||||
return self.kwargs['text']
|
||||
text = property(text, text)
|
||||
|
||||
def set_x(self, x):
|
||||
self.shape.SetX(x)
|
||||
self.canvas.Refresh(False)
|
||||
self.evt_callback()
|
||||
def set_y(self, y):
|
||||
self.shape.SetY(y)
|
||||
self.canvas.Refresh(False)
|
||||
self.evt_callback()
|
||||
def get_x(self):
|
||||
return self.shape.GetX()
|
||||
def get_y(self):
|
||||
return self.shape.GetY()
|
||||
|
||||
x = property(get_x, set_x)
|
||||
y = property(get_y, set_y)
|
||||
|
||||
def selected(self, sel=None):
|
||||
if sel is not None:
|
||||
print "Setting Select(%s)" % sel
|
||||
self.shape.Select(sel)
|
||||
return self.shape.Selected()
|
||||
selected = property(selected, selected)
|
||||
|
||||
def name(self, name=None):
|
||||
if name is not None:
|
||||
self.kwargs['name'] = name
|
||||
return self.kwargs['name']
|
||||
name = property(name, name)
|
||||
|
||||
def __contains__(self, k):
|
||||
"Implement in keyword for searchs"
|
||||
return k in self.name.lower() or self.text and k in self.text.lower()
|
||||
|
||||
def as_dict(self):
|
||||
"Return a dictionary representation, used by pyfpdf"
|
||||
d = self.kwargs
|
||||
x1, y1, x2, y2 = self.get_coordinates()
|
||||
d.update({
|
||||
'x1': x1, 'y1': y1, 'x2': x2, 'y2': y2,
|
||||
'text': self.text})
|
||||
return d
|
||||
|
||||
|
||||
class AppFrame(wx.Frame):
|
||||
"OGL Designer main window"
|
||||
title = "PyFPDF Template Designer (wx OGL)"
|
||||
|
||||
def __init__(self):
|
||||
wx.Frame.__init__( self,
|
||||
None, -1, self.title,
|
||||
size=(640,480),
|
||||
style=wx.DEFAULT_FRAME_STYLE )
|
||||
sys.excepthook = self.except_hook
|
||||
self.filename = ""
|
||||
# Create a toolbar:
|
||||
tsize = (16,16)
|
||||
self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT)
|
||||
|
||||
artBmp = wx.ArtProvider.GetBitmap
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_NEW, artBmp(wx.ART_NEW, wx.ART_TOOLBAR, tsize), "New")
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_OPEN, artBmp(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, tsize), "Open")
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_SAVE, artBmp(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, tsize), "Save")
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_SAVEAS, artBmp(wx.ART_FILE_SAVE_AS, wx.ART_TOOLBAR, tsize),
|
||||
"Save As...")
|
||||
#-------
|
||||
self.toolbar.AddSeparator()
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_UNDO, artBmp(wx.ART_UNDO, wx.ART_TOOLBAR, tsize), "Undo")
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_REDO, artBmp(wx.ART_REDO, wx.ART_TOOLBAR, tsize), "Redo")
|
||||
self.toolbar.AddSeparator()
|
||||
#-------
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_CUT, artBmp(wx.ART_CUT, wx.ART_TOOLBAR, tsize), "Remove")
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_COPY, artBmp(wx.ART_COPY, wx.ART_TOOLBAR, tsize), "Duplicate")
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_PASTE, artBmp(wx.ART_PASTE, wx.ART_TOOLBAR, tsize), "Insert")
|
||||
self.toolbar.AddSeparator()
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_FIND, artBmp(wx.ART_FIND, wx.ART_TOOLBAR, tsize), "Find")
|
||||
self.toolbar.AddSeparator()
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_PRINT, artBmp(wx.ART_PRINT, wx.ART_TOOLBAR, tsize), "Print")
|
||||
self.toolbar.AddSimpleTool(
|
||||
wx.ID_ABOUT, artBmp(wx.ART_HELP, wx.ART_TOOLBAR, tsize), "About")
|
||||
|
||||
self.toolbar.Realize()
|
||||
|
||||
self.toolbar.EnableTool(wx.ID_SAVEAS, False)
|
||||
self.toolbar.EnableTool(wx.ID_UNDO, False)
|
||||
self.toolbar.EnableTool(wx.ID_REDO, False)
|
||||
|
||||
menu_handlers = [
|
||||
(wx.ID_NEW, self.do_new),
|
||||
(wx.ID_OPEN, self.do_open),
|
||||
(wx.ID_SAVE, self.do_save),
|
||||
(wx.ID_PRINT, self.do_print),
|
||||
(wx.ID_FIND, self.do_find),
|
||||
(wx.ID_CUT, self.do_cut),
|
||||
(wx.ID_COPY, self.do_copy),
|
||||
(wx.ID_PASTE, self.do_paste),
|
||||
(wx.ID_ABOUT, self.do_about),
|
||||
]
|
||||
for menu_id, handler in menu_handlers:
|
||||
self.Bind(wx.EVT_MENU, handler, id = menu_id)
|
||||
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
# put stuff into sizer
|
||||
|
||||
self.CreateStatusBar()
|
||||
|
||||
canvas = self.canvas = ogl.ShapeCanvas( self )
|
||||
maxWidth = 1500
|
||||
maxHeight = 2000
|
||||
canvas.SetScrollbars(20, 20, maxWidth/20, maxHeight/20)
|
||||
sizer.Add( canvas, 1, wx.GROW )
|
||||
|
||||
canvas.SetBackgroundColour("WHITE") #
|
||||
|
||||
diagram = self.diagram = ogl.Diagram()
|
||||
canvas.SetDiagram( diagram )
|
||||
diagram.SetCanvas( canvas )
|
||||
diagram.SetSnapToGrid( False )
|
||||
|
||||
# apply sizer
|
||||
self.SetSizer(sizer)
|
||||
self.SetAutoLayout(1)
|
||||
self.Show(1)
|
||||
|
||||
self.Bind(wx.EVT_CHAR_HOOK, self.on_key_event)
|
||||
self.elements = []
|
||||
|
||||
def on_key_event(self, event):
|
||||
""" Respond to a keypress event.
|
||||
|
||||
We make the arrow keys move the selected object(s) by one pixel in
|
||||
the given direction.
|
||||
"""
|
||||
step = 1
|
||||
if event.ControlDown():
|
||||
step = 20
|
||||
|
||||
if event.GetKeyCode() == wx.WXK_UP:
|
||||
self.move_elements(0, -step)
|
||||
elif event.GetKeyCode() == wx.WXK_DOWN:
|
||||
self.move_elements(0, step)
|
||||
elif event.GetKeyCode() == wx.WXK_LEFT:
|
||||
self.move_elements(-step, 0)
|
||||
elif event.GetKeyCode() == wx.WXK_RIGHT:
|
||||
self.move_elements(step, 0)
|
||||
elif event.GetKeyCode() == wx.WXK_DELETE:
|
||||
self.do_cut()
|
||||
else:
|
||||
event.Skip()
|
||||
|
||||
def do_new(self, evt=None):
|
||||
for element in self.elements:
|
||||
element.remove()
|
||||
self.elements = []
|
||||
# draw paper size guides
|
||||
for k, (w, h) in [('legal', (216, 356)), ('A4', (210, 297)), ('letter', (216, 279))]:
|
||||
self.create_elements(
|
||||
k, 'R', 0, 0, w, h,
|
||||
size=70, foreground=0x808080, priority=-100,
|
||||
canvas=self.canvas, frame=self, static=True)
|
||||
self.diagram.ShowAll( 1 )
|
||||
|
||||
def do_open(self, evt):
|
||||
dlg = wx.FileDialog(
|
||||
self, message="Choose a file",
|
||||
defaultDir=os.getcwd(),
|
||||
defaultFile="invoice.csv",
|
||||
wildcard="CSV Files (*.csv)|*.csv",
|
||||
style=wx.OPEN
|
||||
)
|
||||
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
# This returns a Python list of files that were selected.
|
||||
self.filename = dlg.GetPaths()[0]
|
||||
|
||||
dlg.Destroy()
|
||||
self.SetTitle(self.filename + " - " + self.title)
|
||||
|
||||
self.do_new()
|
||||
tmp = []
|
||||
f = open(self.filename)
|
||||
try:
|
||||
filedata = f.readlines()
|
||||
finally:
|
||||
f.close()
|
||||
for lno, linea in enumerate(filedata):
|
||||
if DEBUG: print "processing line", lno, linea
|
||||
args = []
|
||||
for i,v in enumerate(linea.split(";")):
|
||||
if not v.startswith("'"):
|
||||
v = v.replace(",",".")
|
||||
else:
|
||||
v = v#.decode('latin1')
|
||||
if v.strip()=='':
|
||||
v = None
|
||||
else:
|
||||
v = eval(v.strip())
|
||||
args.append(v)
|
||||
tmp.append(args)
|
||||
|
||||
# sort by z-order (priority)
|
||||
for args in sorted(tmp, key=lambda t: t[-1]):
|
||||
if DEBUG: print args
|
||||
self.create_elements(*args)
|
||||
self.diagram.ShowAll( 1 ) #
|
||||
|
||||
return True
|
||||
|
||||
def do_save(self, evt, filename=None):
|
||||
try:
|
||||
from time import gmtime, strftime
|
||||
ts = strftime("%Y%m%d%H%M%S", gmtime())
|
||||
os.rename(self.filename, self.filename + ts + ".bak")
|
||||
except Exception, e:
|
||||
if DEBUG: print e
|
||||
pass
|
||||
|
||||
def csv_repr(v, decimal_sep="."):
|
||||
if isinstance(v, float):
|
||||
return ("%0.2f" % v).replace(".", decimal_sep)
|
||||
else:
|
||||
return repr(v)
|
||||
|
||||
f = open(self.filename, "w")
|
||||
try:
|
||||
for element in sorted(self.elements, key=lambda e:e.name):
|
||||
if element.static:
|
||||
continue
|
||||
d = element.as_dict()
|
||||
l = [d['name'], d['type'],
|
||||
d['x1'], d['y1'], d['x2'], d['y2'],
|
||||
d['font'], d['size'],
|
||||
d['bold'], d['italic'], d['underline'],
|
||||
d['foreground'], d['background'],
|
||||
d['align'], d['text'], d['priority'],
|
||||
]
|
||||
f.write(";".join([csv_repr(v) for v in l]))
|
||||
f.write("\n")
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def do_print(self, evt):
|
||||
# genero el renderizador con propiedades del PDF
|
||||
from template import Template
|
||||
t = Template(elements=[e.as_dict() for e in self.elements if not e.static])
|
||||
t.add_page()
|
||||
if not t['logo'] or not os.path.exists(t['logo']):
|
||||
# put a default logo so it doesn't trow an exception
|
||||
logo = os.path.join(os.path.dirname(__file__), 'tutorial','logo.png')
|
||||
t.set('logo', logo)
|
||||
try:
|
||||
t.render(self.filename +".pdf")
|
||||
except:
|
||||
if DEBUG and False:
|
||||
import pdb;
|
||||
pdb.pm()
|
||||
else:
|
||||
raise
|
||||
if sys.platform=="linux2":
|
||||
os.system("evince ""%s""" % self.filename +".pdf")
|
||||
else:
|
||||
os.startfile(self.filename +".pdf")
|
||||
|
||||
def do_find(self, evt):
|
||||
# busco nombre o texto
|
||||
dlg = wx.TextEntryDialog(
|
||||
self, 'Enter text to search for',
|
||||
'Find Text', '')
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
txt = dlg.GetValue().encode("latin1").lower()
|
||||
for element in self.elements:
|
||||
if txt in element:
|
||||
element.selected = True
|
||||
print "Found:", element.name
|
||||
self.canvas.Refresh(False)
|
||||
dlg.Destroy()
|
||||
|
||||
def do_cut(self, evt=None):
|
||||
"Delete selected elements"
|
||||
new_elements = []
|
||||
for element in self.elements:
|
||||
if element.selected:
|
||||
print "Erasing:", element.name
|
||||
element.selected = False
|
||||
self.canvas.Refresh(False)
|
||||
element.remove()
|
||||
else:
|
||||
new_elements.append(element)
|
||||
self.elements = new_elements
|
||||
self.canvas.Refresh(False)
|
||||
self.diagram.ShowAll( 1 )
|
||||
|
||||
def do_copy(self, evt):
|
||||
"Duplicate selected elements"
|
||||
fields = ['qty', 'dx', 'dy']
|
||||
data = {'qty': 1, 'dx': 0.0, 'dy': 5.0}
|
||||
data = CustomDialog.do_input(self, 'Copy elements', fields, data)
|
||||
if data:
|
||||
new_elements = []
|
||||
for i in range(1, data['qty']+1):
|
||||
for element in self.elements:
|
||||
if element.selected:
|
||||
print "Copying:", element.name
|
||||
new_element = element.copy()
|
||||
name = new_element.name
|
||||
if len(name)>2 and name[-2:].isdigit():
|
||||
new_element.name = name[:-2] + "%02d" % (int(name[-2:])+i)
|
||||
else:
|
||||
new_element.name = new_element.name + "_copy"
|
||||
new_element.selected = False
|
||||
new_element.move(data['dx']*i, data['dy']*i)
|
||||
new_elements.append(new_element)
|
||||
self.elements.extend(new_elements)
|
||||
self.canvas.Refresh(False)
|
||||
self.diagram.ShowAll( 1 )
|
||||
|
||||
def do_paste(self, evt):
|
||||
"Insert new elements"
|
||||
element = Element.new(self)
|
||||
if element:
|
||||
self.canvas.Refresh(False)
|
||||
self.elements.append(element)
|
||||
self.diagram.ShowAll( 1 )
|
||||
|
||||
def create_elements(self, name, type, x1, y1, x2, y2,
|
||||
font="Arial", size=12,
|
||||
bold=False, italic=False, underline=False,
|
||||
foreground= 0x000000, background=0xFFFFFF,
|
||||
align="L", text="", priority=0, canvas=None, frame=None, static=False,
|
||||
**kwargs):
|
||||
element = Element(name=name, type=type, x1=x1, y1=y1, x2=x2, y2=y2,
|
||||
font=font, size=size,
|
||||
bold=bold, italic=italic, underline=underline,
|
||||
foreground= foreground, background=background,
|
||||
align=align, text=text, priority=priority,
|
||||
canvas=canvas or self.canvas, frame=frame or self,
|
||||
static=static)
|
||||
self.elements.append(element)
|
||||
|
||||
def move_elements(self, x, y):
|
||||
for element in self.elements:
|
||||
if element.selected:
|
||||
print "moving", element.name, x, y
|
||||
element.x = element.x + x
|
||||
element.y = element.y + y
|
||||
|
||||
def do_about(self, evt):
|
||||
info = wx.AboutDialogInfo()
|
||||
info.Name = self.title
|
||||
info.Version = __version__
|
||||
info.Copyright = __copyright__
|
||||
info.Description = (
|
||||
"Visual Template designer for PyFPDF (using wxPython OGL library)\n"
|
||||
"Input files are CSV format describing the layout, separated by ;\n"
|
||||
"Use toolbar buttons to open, save, print (preview) your template, "
|
||||
"and there are buttons to find, add, remove or duplicate elements.\n"
|
||||
"Over an element, a double left click opens edit text dialog, "
|
||||
"and a right click opens edit properties dialog. \n"
|
||||
"Multiple element can be selected with shift left click. \n"
|
||||
"Use arrow keys or drag-and-drop to move elements.\n"
|
||||
"For further information see project webpage:"
|
||||
)
|
||||
info.WebSite = ("http://code.google.com/p/pyfpdf/wiki/Templates",
|
||||
"pyfpdf Google Code Project")
|
||||
info.Developers = [ __author__, ]
|
||||
|
||||
info.License = wordwrap(__license__, 500, wx.ClientDC(self))
|
||||
|
||||
# Then we call wx.AboutBox giving it that info object
|
||||
wx.AboutBox(info)
|
||||
|
||||
def except_hook(self, type, value, trace):
|
||||
import traceback
|
||||
exc = traceback.format_exception(type, value, trace)
|
||||
for e in exc: wx.LogError(e)
|
||||
wx.LogError('Unhandled Error: %s: %s'%(str(type), str(value)))
|
||||
|
||||
|
||||
app = wx.PySimpleApp()
|
||||
ogl.OGLInitialize()
|
||||
frame = AppFrame()
|
||||
app.MainLoop()
|
||||
app.Destroy()
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,459 @@
|
||||
# -*- coding: latin-1 -*-
|
||||
|
||||
"HTML Renderer for FPDF.py"
|
||||
|
||||
__author__ = "Mariano Reingart <reingart@gmail.com>"
|
||||
__copyright__ = "Copyright (C) 2010 Mariano Reingart"
|
||||
__license__ = "LGPL 3.0"
|
||||
|
||||
# Inspired by tuto5.py and several examples from fpdf.org, html2fpdf, etc.
|
||||
|
||||
from fpdf import FPDF
|
||||
from HTMLParser import HTMLParser
|
||||
|
||||
DEBUG = False
|
||||
|
||||
def px2mm(px):
|
||||
return int(px)*25.4/72.0
|
||||
|
||||
def hex2dec(color = "#000000"):
|
||||
if color:
|
||||
r = int(color[1:3], 16)
|
||||
g = int(color[3:5], 16)
|
||||
b = int(color[5:7], 16)
|
||||
return r, g, b
|
||||
|
||||
class HTML2FPDF(HTMLParser):
|
||||
"Render basic HTML to FPDF"
|
||||
|
||||
def __init__(self, pdf, image_map, **kwargs):
|
||||
HTMLParser.__init__(self)
|
||||
self.image_map = image_map
|
||||
self.style = {}
|
||||
self.pre = False
|
||||
self.href = ''
|
||||
self.align = ''
|
||||
self.page_links = {}
|
||||
self.font_list = ("times","courier", "helvetica")
|
||||
self.pdf = pdf
|
||||
self.r = self.g = self.b = 0
|
||||
self.indent = 0
|
||||
self.bullet = []
|
||||
self.font_face="times" # initialize font
|
||||
self.color=0 # initialize font color
|
||||
self.set_font(kwargs.get("font","times"), kwargs.get("fontsize",12))
|
||||
self.table = None # table attributes
|
||||
self.table_col_width = None # column (header) widths
|
||||
self.table_col_index = None # current column index
|
||||
self.td = None # cell attributes
|
||||
self.th = False # header enabled
|
||||
self.tr = None
|
||||
self.theader = None # table header cells
|
||||
self.tfooter = None # table footer cells
|
||||
self.thead = None
|
||||
self.tfoot = None
|
||||
self.theader_out = self.tfooter_out = False
|
||||
|
||||
def width2mm(self, length):
|
||||
if length[-1]=='%':
|
||||
total = self.pdf.w - self.pdf.r_margin - self.pdf.l_margin
|
||||
if self.table['width'][-1]=='%':
|
||||
total *= int(self.table['width'][:-1])/100.0
|
||||
return int(length[:-1]) * total / 101.0
|
||||
else:
|
||||
return int(length) / 6.0
|
||||
|
||||
def handle_data(self, txt):
|
||||
if self.td is not None: # drawing a table?
|
||||
if 'width' not in self.td and 'colspan' not in self.td:
|
||||
l = [self.table_col_width[self.table_col_index]]
|
||||
elif 'colspan' in self.td:
|
||||
i = self.table_col_index
|
||||
colspan = int(self.td['colspan'])
|
||||
l = self.table_col_width[i:i+colspan]
|
||||
else:
|
||||
l = [self.td.get('width','240')]
|
||||
w = sum([self.width2mm(lenght) for lenght in l])
|
||||
h = int(self.td.get('height', 0)) / 4 or self.h*1.30
|
||||
self.table_h = h
|
||||
border = int(self.table.get('border', 0))
|
||||
if not self.th:
|
||||
align = self.td.get('align', 'L')[0].upper()
|
||||
border = border and 'LR'
|
||||
else:
|
||||
self.set_style('B',True)
|
||||
border = border or 'B'
|
||||
align = 'C'
|
||||
bgcolor = hex2dec(self.td.get('bgcolor', self.tr.get('bgcolor', '')))
|
||||
# parsing table header/footer (drawn later):
|
||||
if self.thead is not None:
|
||||
self.theader.append(((w,h,txt,border,0,align), bgcolor))
|
||||
if self.tfoot is not None:
|
||||
self.tfooter.append(((w,h,txt,border,0,align), bgcolor))
|
||||
# check if reached end of page, add table footer and header:
|
||||
height = h + (self.tfooter and self.tfooter[0][0][1] or 0)
|
||||
if self.pdf.y+height>self.pdf.page_break_trigger and not self.th:
|
||||
self.output_table_footer()
|
||||
self.pdf.add_page()
|
||||
self.theader_out = self.tfooter_out = False
|
||||
if self.tfoot is None and self.thead is None:
|
||||
if not self.theader_out:
|
||||
self.output_table_header()
|
||||
self.box_shadow(w, h, bgcolor)
|
||||
if DEBUG: print "td cell", self.pdf.x, w, txt, "*"
|
||||
self.pdf.cell(w,h,txt,border,0,align)
|
||||
elif self.table is not None:
|
||||
# ignore anything else than td inside a table
|
||||
pass
|
||||
elif self.align:
|
||||
if DEBUG: print "cell", txt, "*"
|
||||
self.pdf.cell(0,self.h,txt,0,1,self.align[0].upper(), self.href)
|
||||
else:
|
||||
txt = txt.replace("\n"," ")
|
||||
if self.href:
|
||||
self.put_link(self.href,txt)
|
||||
else:
|
||||
if DEBUG: print "write", txt, "*"
|
||||
self.pdf.write(self.h,txt)
|
||||
|
||||
def box_shadow(self, w, h, bgcolor):
|
||||
if DEBUG: print "box_shadow", w, h, bgcolor
|
||||
if bgcolor:
|
||||
fill_color = self.pdf.fill_color
|
||||
self.pdf.set_fill_color(*bgcolor)
|
||||
self.pdf.rect(self.pdf.x, self.pdf.y, w, h, 'F')
|
||||
self.pdf.fill_color = fill_color
|
||||
|
||||
def output_table_header(self):
|
||||
if self.theader:
|
||||
b = self.b
|
||||
x = self.pdf.x
|
||||
self.pdf.set_x(self.table_offset)
|
||||
self.set_style('B',True)
|
||||
for cell, bgcolor in self.theader:
|
||||
self.box_shadow(cell[0], cell[1], bgcolor)
|
||||
self.pdf.cell(*cell)
|
||||
self.set_style('B',b)
|
||||
self.pdf.ln(self.theader[0][0][1])
|
||||
self.pdf.set_x(self.table_offset)
|
||||
#self.pdf.set_x(x)
|
||||
self.theader_out = True
|
||||
|
||||
def output_table_footer(self):
|
||||
if self.tfooter:
|
||||
x = self.pdf.x
|
||||
self.pdf.set_x(self.table_offset)
|
||||
#TODO: self.output_table_sep()
|
||||
for cell, bgcolor in self.tfooter:
|
||||
self.box_shadow(cell[0], cell[1], bgcolor)
|
||||
self.pdf.cell(*cell)
|
||||
self.pdf.ln(self.tfooter[0][0][1])
|
||||
self.pdf.set_x(x)
|
||||
if int(self.table.get('border', 0)):
|
||||
self.output_table_sep()
|
||||
self.tfooter_out = True
|
||||
|
||||
def output_table_sep(self):
|
||||
self.pdf.set_x(self.table_offset)
|
||||
x1 = self.pdf.x
|
||||
y1 = self.pdf.y
|
||||
w = sum([self.width2mm(lenght) for lenght in self.table_col_width])
|
||||
self.pdf.line(x1,y1,x1+w,y1)
|
||||
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attrs = dict(attrs)
|
||||
if DEBUG: print "STARTTAG", tag, attrs
|
||||
if tag=='b' or tag=='i' or tag=='u':
|
||||
self.set_style(tag,1)
|
||||
if tag=='a':
|
||||
self.href=attrs['href']
|
||||
if tag=='br':
|
||||
self.pdf.ln(5)
|
||||
if tag=='p':
|
||||
self.pdf.ln(5)
|
||||
if attrs:
|
||||
self.align=attrs['align'].lower()
|
||||
if tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
|
||||
k = (2, 1.5, 1.17, 1, 0.83, 0.67)[int(tag[1])]
|
||||
self.pdf.ln(5*k)
|
||||
self.pdf.set_text_color(150,0,0)
|
||||
self.pdf.set_font_size(12 * k)
|
||||
if attrs: self.align = attrs.get('align')
|
||||
if tag=='hr':
|
||||
self.put_line()
|
||||
if tag=='pre':
|
||||
self.pdf.set_font('Courier','',11)
|
||||
self.pdf.set_font_size(11)
|
||||
self.set_style('B',False)
|
||||
self.set_style('I',False)
|
||||
self.pre = True
|
||||
if tag=='blockquote':
|
||||
self.set_text_color(100,0,45)
|
||||
self.pdf.ln(3)
|
||||
if tag=='ul':
|
||||
self.indent+=1
|
||||
self.bullet.append('\x95')
|
||||
if tag=='ol':
|
||||
self.indent+=1
|
||||
self.bullet.append(0)
|
||||
if tag=='li':
|
||||
self.pdf.ln(self.h+2)
|
||||
self.pdf.set_text_color(190,0,0)
|
||||
bullet = self.bullet[self.indent-1]
|
||||
if not isinstance(bullet, basestring):
|
||||
bullet += 1
|
||||
self.bullet[self.indent-1] = bullet
|
||||
bullet = "%s. " % bullet
|
||||
self.pdf.write(self.h,'%s%s ' % (' '*5*self.indent, bullet))
|
||||
self.set_text_color()
|
||||
if tag=='font':
|
||||
if 'color' in attrs:
|
||||
self.color = hex2dec(attrs['color'])
|
||||
self.set_text_color(*color)
|
||||
self.color = color
|
||||
if 'face' in attrs and attrs['face'].lower() in self.font_list:
|
||||
face = attrs.get('face').lower()
|
||||
self.pdf.set_font(face)
|
||||
self.font_face = face
|
||||
if 'size' in attrs:
|
||||
size = int(attrs.get('size'))
|
||||
self.pdf.set_font(self.font_face, size=int(size))
|
||||
self.font_size = size
|
||||
if tag=='table':
|
||||
self.table = dict([(k.lower(), v) for k,v in attrs.items()])
|
||||
if not 'width' in self.table:
|
||||
self.table['width'] = '100%'
|
||||
if self.table['width'][-1]=='%':
|
||||
w = self.pdf.w - self.pdf.r_margin - self.pdf.l_margin
|
||||
w *= int(self.table['width'][:-1])/100.0
|
||||
self.table_offset = (self.pdf.w-w)/2.0
|
||||
self.table_col_width = []
|
||||
self.theader_out = self.tfooter_out = False
|
||||
self.theader = []
|
||||
self.tfooter = []
|
||||
self.thead = None
|
||||
self.tfoot = None
|
||||
self.pdf.ln()
|
||||
if tag=='tr':
|
||||
self.tr = dict([(k.lower(), v) for k,v in attrs.items()])
|
||||
self.table_col_index = 0
|
||||
self.pdf.set_x(self.table_offset)
|
||||
if tag=='td':
|
||||
self.td = dict([(k.lower(), v) for k,v in attrs.items()])
|
||||
if tag=='th':
|
||||
self.td = dict([(k.lower(), v) for k,v in attrs.items()])
|
||||
self.th = True
|
||||
if self.td['width']:
|
||||
self.table_col_width.append(self.td['width'])
|
||||
if tag=='thead':
|
||||
self.thead = {}
|
||||
if tag=='tfoot':
|
||||
self.tfoot = {}
|
||||
if tag=='img':
|
||||
if 'src' in attrs:
|
||||
x = self.pdf.get_x()
|
||||
y = self.pdf.get_y()
|
||||
w = px2mm(attrs.get('width', 0))
|
||||
h = px2mm(attrs.get('height',0))
|
||||
if self.align and self.align[0].upper() == 'C':
|
||||
x = (self.pdf.w-x)/2.0 - w/2.0
|
||||
self.pdf.image(self.image_map(attrs['src']),
|
||||
x, y, w, h, link=self.href)
|
||||
self.pdf.set_x(x+w)
|
||||
self.pdf.set_y(y+h)
|
||||
if tag=='b' or tag=='i' or tag=='u':
|
||||
self.set_style(tag, True)
|
||||
if tag=='center':
|
||||
self.align = 'Center'
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
#Closing tag
|
||||
if DEBUG: print "ENDTAG", tag
|
||||
if tag=='h1' or tag=='h2' or tag=='h3' or tag=='h4':
|
||||
self.pdf.ln(6)
|
||||
self.set_font()
|
||||
self.set_style()
|
||||
self.align = None
|
||||
if tag=='pre':
|
||||
self.pdf.set_font(self.font or 'Times','',12)
|
||||
self.pdf.set_font_size(12)
|
||||
self.pre=False
|
||||
if tag=='blockquote':
|
||||
self.set_text_color(0,0,0)
|
||||
self.pdf.ln(3)
|
||||
if tag=='strong':
|
||||
tag='b'
|
||||
if tag=='em':
|
||||
tag='i'
|
||||
if tag=='b' or tag=='i' or tag=='u':
|
||||
self.set_style(tag, False)
|
||||
if tag=='a':
|
||||
self.href=''
|
||||
if tag=='p':
|
||||
self.align=''
|
||||
if tag in ('ul', 'ol'):
|
||||
self.indent-=1
|
||||
self.bullet.pop()
|
||||
if tag=='table':
|
||||
if not self.tfooter_out:
|
||||
self.output_table_footer()
|
||||
self.table = None
|
||||
self.th = False
|
||||
self.theader = None
|
||||
self.tfooter = None
|
||||
self.pdf.ln()
|
||||
if tag=='thead':
|
||||
self.thead = None
|
||||
if tag=='tfoot':
|
||||
self.tfoot = None
|
||||
if tag=='tbody':
|
||||
# draw a line separator between table bodies
|
||||
self.pdf.set_x(self.table_offset)
|
||||
self.output_table_sep()
|
||||
if tag=='tr':
|
||||
h = self.table_h
|
||||
if self.tfoot is None:
|
||||
self.pdf.ln(h)
|
||||
self.tr = None
|
||||
if tag=='td' or tag=='th':
|
||||
if self.th:
|
||||
if DEBUG: print "revert style"
|
||||
self.set_style('B', False) # revert style
|
||||
self.table_col_index += int(self.td.get('colspan','1'))
|
||||
self.td = None
|
||||
self.th = False
|
||||
if tag=='font':
|
||||
if self.color:
|
||||
self.pdf.set_text_color(0,0,0)
|
||||
self.color = None
|
||||
if self.font_face:
|
||||
self.set_font('Times',12)
|
||||
|
||||
if tag=='center':
|
||||
self.align = None
|
||||
|
||||
def set_font(self, face=None, size=None):
|
||||
if face:
|
||||
self.font_face = face
|
||||
if size:
|
||||
self.font_size = size
|
||||
self.h = size / 72.0*25.4
|
||||
if DEBUG: print "H", self.h
|
||||
self.pdf.set_font(self.font_face or 'times','',12)
|
||||
self.pdf.set_font_size(self.font_size or 12)
|
||||
self.set_style('u', False)
|
||||
self.set_style('b', False)
|
||||
self.set_style('i', False)
|
||||
self.set_text_color()
|
||||
|
||||
def set_style(self, tag=None, enable=None):
|
||||
#Modify style and select corresponding font
|
||||
if tag:
|
||||
t = self.style.get(tag.lower())
|
||||
self.style[tag.lower()] = enable
|
||||
style=''
|
||||
for s in ('b','i','u'):
|
||||
if self.style.get(s):
|
||||
style+=s
|
||||
if DEBUG: print "SET_FONT_STYLE", style
|
||||
self.pdf.set_font('',style)
|
||||
|
||||
def set_text_color(self, r=None, g=0, b=0):
|
||||
if r is None:
|
||||
self.pdf.set_text_color(self.r,self.g,self.b)
|
||||
else:
|
||||
self.pdf.set_text_color(r, g, b)
|
||||
self.r = r
|
||||
self.g = g
|
||||
self.b = b
|
||||
|
||||
def put_link(self, url, txt):
|
||||
#Put a hyperlink
|
||||
self.set_text_color(0,0,255)
|
||||
self.set_style('u', True)
|
||||
self.pdf.write(5,txt,url)
|
||||
self.set_style('u', False)
|
||||
self.set_text_color(0)
|
||||
|
||||
def put_line(self):
|
||||
self.pdf.ln(2)
|
||||
self.pdf.line(self.pdf.get_x(),self.pdf.get_y(),self.pdf.get_x()+187,self.pdf.get_y())
|
||||
self.pdf.ln(3)
|
||||
|
||||
class HTMLMixin():
|
||||
def write_html(self, text, image_map=lambda x:x, **kwargs):
|
||||
"Parse HTML and convert it to PDF"
|
||||
h2p = HTML2FPDF(self,image_map=image_map,**kwargs)
|
||||
h2p.feed(text)
|
||||
|
||||
if __name__=='__main__':
|
||||
html="""
|
||||
<H1 align="center">html2fpdf</H1>
|
||||
<h2>Basic usage</h2>
|
||||
<p>You can now easily print text mixing different
|
||||
styles : <B>bold</B>, <I>italic</I>, <U>underlined</U>, or
|
||||
<B><I><U>all at once</U></I></B>!<BR>You can also insert links
|
||||
on text, such as <A HREF="http://www.fpdf.org">www.fpdf.org</A>,
|
||||
or on an image: click on the logo.<br>
|
||||
<center>
|
||||
<A HREF="http://www.fpdf.org"><img src="tutorial/logo.png" width="104" height="71"></A>
|
||||
</center>
|
||||
<h3>Sample List</h3>
|
||||
<ul><li>option 1</li>
|
||||
<ol><li>option 2</li></ol>
|
||||
<li>option 3</li></ul>
|
||||
|
||||
<table border="0" align="center" width="50%">
|
||||
<thead><tr><th width="30%">Header 1</th><th width="70%">header 2</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>cell 1</td><td>cell 2</td></tr>
|
||||
<tr><td>cell 2</td><td>cell 3</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<table border="1">
|
||||
<thead><tr bgcolor="#A0A0A0"><th width="30%">Header 1</th><th width="70%">header 2</th></tr></thead>
|
||||
<tfoot><tr bgcolor="#E0E0E0"><td>footer 1</td><td>footer 2</td></tr></tfoot>
|
||||
<tbody>
|
||||
<tr><td>cell 1</td><td>cell 2</td></tr>
|
||||
<tr>
|
||||
<td width="30%">cell 1</td><td width="70%" bgcolor="#D0D0FF" align='right'>cell 2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody><tr><td colspan="2">cell spanned</td></tr></tbody>
|
||||
<tbody>
|
||||
""" + """<tr bgcolor="#F0F0F0">
|
||||
<td>cell 3</td><td>cell 4</td>
|
||||
</tr><tr bgcolor="#FFFFFF">
|
||||
<td>cell 5</td><td>cell 6</td>
|
||||
</tr>""" * 200 + """
|
||||
</tbody>
|
||||
</table>
|
||||
"""
|
||||
|
||||
class MyFPDF(FPDF, HTMLMixin):
|
||||
def header(self):
|
||||
self.image('tutorial/logo_pb.png',10,8,33)
|
||||
self.set_font('Arial','B',15)
|
||||
self.cell(80)
|
||||
self.cell(30,10,'Title',1,0,'C')
|
||||
self.ln(20)
|
||||
|
||||
def footer(self):
|
||||
self.set_y(-15)
|
||||
self.set_font('Arial','I',8)
|
||||
txt = 'Page %s of %s' % (self.page_no(), self.alias_nb_pages())
|
||||
self.cell(0,10,txt,0,0,'C')
|
||||
|
||||
pdf=MyFPDF()
|
||||
#First page
|
||||
pdf.add_page()
|
||||
pdf.write_html(html)
|
||||
pdf.output('html.pdf','F')
|
||||
|
||||
import os
|
||||
os.system("evince html.pdf")
|
||||
|
||||
|
||||
Executable
+278
@@ -0,0 +1,278 @@
|
||||
# -*- coding: iso-8859-1 -*-
|
||||
|
||||
"PDF Template Helper for FPDF.py"
|
||||
|
||||
__author__ = "Mariano Reingart <reingart@gmail.com>"
|
||||
__copyright__ = "Copyright (C) 2010 Mariano Reingart"
|
||||
__license__ = "LGPL 3.0"
|
||||
|
||||
import sys,os,csv
|
||||
from fpdf import FPDF
|
||||
|
||||
def rgb(col):
|
||||
return (col // 65536), (col // 256 % 256), (col% 256)
|
||||
|
||||
class Template:
|
||||
def __init__(self, infile=None, elements=None, format='A4', orientation='portrait',
|
||||
title='', author='', subject='', creator='', keywords=''):
|
||||
if elements:
|
||||
self.elements = dict([(v['name'].lower(),v) for v in elements])
|
||||
self.handlers = {'T': self.text, 'L': self.line, 'I': self.image,
|
||||
'B': self.rect, 'BC': self.barcode, }
|
||||
self.pg_no = 0
|
||||
self.texts = {}
|
||||
pdf = self.pdf = FPDF(format=format,orientation=orientation, unit="mm")
|
||||
pdf.set_title(title)
|
||||
pdf.set_author(author)
|
||||
pdf.set_creator(creator)
|
||||
pdf.set_subject(subject)
|
||||
pdf.set_keywords(keywords)
|
||||
|
||||
def parse_csv(self, infile, delimiter=",", decimal_sep="."):
|
||||
"Parse template format csv file and create elements dict"
|
||||
keys = ('name','type','x1','y1','x2','y2','font','size',
|
||||
'bold','italic','underline','foreground','background',
|
||||
'align','text','priority')
|
||||
self.elements = {}
|
||||
f = open(infile, 'rb')
|
||||
try:
|
||||
for row in csv.reader(f, delimiter=delimiter):
|
||||
kargs = {}
|
||||
for i,v in enumerate(row):
|
||||
if not v.startswith("'") and decimal_sep!=".":
|
||||
v = v.replace(decimal_sep,".")
|
||||
else:
|
||||
v = v
|
||||
if v=='':
|
||||
v = None
|
||||
else:
|
||||
v = eval(v.strip())
|
||||
kargs[keys[i]] = v
|
||||
self.elements[kargs['name'].lower()] = kargs
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def add_page(self):
|
||||
self.pg_no += 1
|
||||
self.texts[self.pg_no] = {}
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
if name.lower() in self.elements:
|
||||
if isinstance(value,unicode):
|
||||
value = value.encode("latin1","ignore")
|
||||
else:
|
||||
value = str(value)
|
||||
self.texts[self.pg_no][name.lower()] = value
|
||||
|
||||
# setitem shortcut (may be further extended)
|
||||
set = __setitem__
|
||||
|
||||
def __getitem__(self, name):
|
||||
if name.lower() in self.elements:
|
||||
return self.texts[self.pg_no].get(name.lower(), self.elements[name.lower()]['text'])
|
||||
|
||||
def split_multicell(self, text, element_name):
|
||||
"Divide (\n) a string using a given element width"
|
||||
pdf = self.pdf
|
||||
element = self.elements[element_name.lower()]
|
||||
style = ""
|
||||
if element['bold']: style += "B"
|
||||
if element['italic']: style += "I"
|
||||
if element['underline']: style += "U"
|
||||
pdf.set_font(element['font'],style,element['size'])
|
||||
align = {'L':'L','R':'R','I':'L','D':'R','C':'C','':''}.get(element['align']) # D/I in spanish
|
||||
if isinstance(text, unicode):
|
||||
text = text.encode("latin1","ignore")
|
||||
else:
|
||||
text = str(text)
|
||||
return pdf.multi_cell(w=element['x2']-element['x1'],
|
||||
h=element['y2']-element['y1'],
|
||||
txt=text,align=align,split_only=True)
|
||||
|
||||
def render(self, outfile, dest="F"):
|
||||
pdf = self.pdf
|
||||
for pg in range(1, self.pg_no+1):
|
||||
pdf.add_page()
|
||||
pdf.set_font('Arial','B',16)
|
||||
pdf.set_auto_page_break(False,margin=0)
|
||||
|
||||
for element in sorted(self.elements.values(),key=lambda x: x['priority']):
|
||||
#print "dib",element['type'], element['name'], element['x1'], element['y1'], element['x2'], element['y2']
|
||||
element = element.copy()
|
||||
element['text'] = self.texts[pg].get(element['name'].lower(), element['text'])
|
||||
if 'rotate' in element:
|
||||
pdf.rotate(element['rotate'], element['x1'], element['y1'])
|
||||
self.handlers[element['type'].upper()](pdf, **element)
|
||||
if 'rotate' in element:
|
||||
pdf.rotate(0)
|
||||
|
||||
return pdf.output(outfile, dest)
|
||||
|
||||
def text(self, pdf, x1=0, y1=0, x2=0, y2=0, text='', font="arial", size=10,
|
||||
bold=False, italic=False, underline=False, align="",
|
||||
foreground=0, backgroud=65535,
|
||||
*args, **kwargs):
|
||||
if text:
|
||||
if pdf.text_color!=rgb(foreground):
|
||||
pdf.set_text_color(*rgb(foreground))
|
||||
if pdf.fill_color!=rgb(backgroud):
|
||||
pdf.set_fill_color(*rgb(backgroud))
|
||||
|
||||
font = font.strip().lower()
|
||||
if font == 'arial black':
|
||||
font = 'arial'
|
||||
style = ""
|
||||
for tag in 'B', 'I', 'U':
|
||||
if (text.startswith("<%s>" % tag) and text.endswith("</%s>" %tag)):
|
||||
text = text[3:-4]
|
||||
style += tag
|
||||
if bold: style += "B"
|
||||
if italic: style += "I"
|
||||
if underline: style += "U"
|
||||
align = {'L':'L','R':'R','I':'L','D':'R','C':'C','':''}.get(align) # D/I in spanish
|
||||
pdf.set_font(font,style,size)
|
||||
##m_k = 72 / 2.54
|
||||
##h = (size/m_k)
|
||||
pdf.set_xy(x1,y1)
|
||||
pdf.cell(w=x2-x1,h=y2-y1,txt=text,border=0,ln=0,align=align)
|
||||
#pdf.Text(x=x1,y=y1,txt=text)
|
||||
|
||||
def line(self, pdf, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, *args, **kwargs):
|
||||
if pdf.draw_color!=rgb(foreground):
|
||||
#print "SetDrawColor", hex(foreground)
|
||||
pdf.set_draw_color(*rgb(foreground))
|
||||
#print "SetLineWidth", size
|
||||
pdf.set_line_width(size)
|
||||
pdf.line(x1, y1, x2, y2)
|
||||
|
||||
def rect(self, pdf, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, backgroud=65535, *args, **kwargs):
|
||||
if pdf.draw_color!=rgb(foreground):
|
||||
pdf.set_draw_color(*rgb(foreground))
|
||||
if pdf.fill_color!=rgb(backgroud):
|
||||
pdf.set_fill_color(*rgb(backgroud))
|
||||
pdf.set_line_width(size)
|
||||
pdf.rect(x1, y1, x2-x1, y2-y1)
|
||||
|
||||
def image(self, pdf, x1=0, y1=0, x2=0, y2=0, text='', *args,**kwargs):
|
||||
pdf.image(text,x1,y1,w=x2-x1,h=y2-y1,type='',link='')
|
||||
|
||||
def barcode(self, pdf, x1=0, y1=0, x2=0, y2=0, text='', font="arial", size=1,
|
||||
foreground=0, *args, **kwargs):
|
||||
if pdf.draw_color!=rgb(foreground):
|
||||
pdf.set_draw_color(*rgb(foreground))
|
||||
font = font.lower().strip()
|
||||
if font == 'interleaved 2of5 nt':
|
||||
pdf.interleaved2of5(text,x1,y1,w=size,h=y2-y1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# generate sample invoice (according Argentina's regulations)
|
||||
|
||||
import random
|
||||
from decimal import Decimal
|
||||
|
||||
f = Template(format="A4",
|
||||
title="Sample Invoice", author="Sample Company",
|
||||
subject="Sample Customer", keywords="Electronic TAX Invoice")
|
||||
f.parse_csv(infile="invoice.csv", delimiter=";", decimal_sep=",")
|
||||
|
||||
detail = "Lorem ipsum dolor sit amet, consectetur. " * 30
|
||||
items = []
|
||||
for i in range(1, 30):
|
||||
ds = "Sample product %s" % i
|
||||
qty = random.randint(1,10)
|
||||
price = round(random.random()*100,3)
|
||||
code = "%s%s%02d" % (chr(random.randint(65,90)), chr(random.randint(65,90)),i)
|
||||
items.append(dict(code=code, unit='u',
|
||||
qty=qty, price=price,
|
||||
amount=qty*price,
|
||||
ds="%s: %s" % (i,ds)))
|
||||
|
||||
# divide and count lines
|
||||
lines = 0
|
||||
li_items = []
|
||||
for it in items:
|
||||
qty = it['qty']
|
||||
code = it['code']
|
||||
unit = it['unit']
|
||||
for ds in f.split_multicell(it['ds'], 'item_description01'):
|
||||
# add item description line (without price nor amount)
|
||||
li_items.append(dict(code=code, ds=ds, qty=qty, unit=unit, price=None, amount=None))
|
||||
# clean qty and code (show only at first)
|
||||
unit = qty = code = None
|
||||
# set last item line price and amount
|
||||
li_items[-1].update(amount = it['amount'],
|
||||
price = it['price'])
|
||||
|
||||
obs="\n<U>Detail:</U>\n\n" + detail
|
||||
for ds in f.split_multicell(obs, 'item_description01'):
|
||||
li_items.append(dict(code=code, ds=ds, qty=qty, unit=unit, price=None, amount=None))
|
||||
|
||||
# calculate pages:
|
||||
lines = len(li_items)
|
||||
max_lines_per_page = 24
|
||||
pages = lines / (max_lines_per_page - 1)
|
||||
if lines % (max_lines_per_page - 1): pages = pages + 1
|
||||
|
||||
# completo campos y hojas
|
||||
for page in range(1, pages+1):
|
||||
f.add_page()
|
||||
f['page'] = 'Page %s of %s' % (page, pages)
|
||||
if pages>1 and page<pages:
|
||||
s = 'Continues on page %s' % (page+1)
|
||||
else:
|
||||
s = ''
|
||||
f['item_description%02d' % (max_lines_per_page+1)] = s
|
||||
|
||||
f["company_name"] = "Sample Company"
|
||||
f["company_logo"] = "tutorial/logo.png"
|
||||
f["company_header1"] = "Some Address - somewhere -"
|
||||
f["company_header2"] = "http://www.example.com"
|
||||
f["company_footer1"] = "Tax Code ..."
|
||||
f["company_footer2"] = "Tax/VAT ID ..."
|
||||
f['number'] = '0001-00001234'
|
||||
f['issue_date'] = '2010-09-10'
|
||||
f['due_date'] = '2099-09-10'
|
||||
f['customer_name'] = "Sample Client"
|
||||
f['customer_address'] = "Siempreviva 1234"
|
||||
|
||||
# print line item...
|
||||
li = 0
|
||||
k = 0
|
||||
total = Decimal("0.00")
|
||||
for it in li_items:
|
||||
k = k + 1
|
||||
if k > page * (max_lines_per_page - 1):
|
||||
break
|
||||
if it['amount']:
|
||||
total += Decimal("%.6f" % it['amount'])
|
||||
if k > (page - 1) * (max_lines_per_page - 1):
|
||||
li += 1
|
||||
if it['qty'] is not None:
|
||||
f['item_quantity%02d' % li] = it['qty']
|
||||
if it['code'] is not None:
|
||||
f['item_code%02d' % li] = it['code']
|
||||
if it['unit'] is not None:
|
||||
f['item_unit%02d' % li] = it['unit']
|
||||
f['item_description%02d' % li] = it['ds']
|
||||
if it['price'] is not None:
|
||||
f['item_price%02d' % li] = "%0.3f" % it['price']
|
||||
if it['amount'] is not None:
|
||||
f['item_amount%02d' % li] = "%0.2f" % it['amount']
|
||||
|
||||
if pages == page:
|
||||
f['net'] = "%0.2f" % (total/Decimal("1.21"))
|
||||
f['vat'] = "%0.2f" % (total*(1-1/Decimal("1.21")))
|
||||
f['total_label'] = 'Total:'
|
||||
else:
|
||||
f['total_label'] = 'SubTotal:'
|
||||
f['total'] = "%0.2f" % total
|
||||
|
||||
f.render("./invoice.pdf")
|
||||
if sys.platform.startswith("linux"):
|
||||
os.system("evince ./invoice.pdf")
|
||||
else:
|
||||
os.system("./invoice.pdf")
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2010 PyMySQL contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,37 @@
|
||||
====================
|
||||
PyMySQL Installation
|
||||
====================
|
||||
|
||||
.. contents::
|
||||
..
|
||||
This package contains a pure-Python MySQL client library.
|
||||
Documentation on the MySQL client/server protocol can be found here:
|
||||
http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol
|
||||
If you would like to run the test suite, create a ~/.my.cnf file and
|
||||
a database called "test_pymysql". The goal of pymysql is to be a drop-in
|
||||
replacement for MySQLdb and work on CPython 2.3+, Jython, IronPython, PyPy
|
||||
and Python 3. We test for compatibility by simply changing the import
|
||||
statements in the Django MySQL backend and running its unit tests as well
|
||||
as running it against the MySQLdb and myconnpy unit tests.
|
||||
|
||||
Requirements
|
||||
-------------
|
||||
|
||||
+ Python 2.4 or higher
|
||||
|
||||
* http://www.python.org/
|
||||
|
||||
* 2.6 is the primary test environment.
|
||||
|
||||
* MySQL 4.1 or higher
|
||||
|
||||
* protocol41 support, experimental 4.0 support
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
# easy_install pymysql
|
||||
# ... or ...
|
||||
# python setup.py install
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
'''
|
||||
PyMySQL: A pure-Python drop-in replacement for MySQLdb.
|
||||
|
||||
Copyright (c) 2010 PyMySQL contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
'''
|
||||
|
||||
VERSION = (0, 4, None)
|
||||
|
||||
from constants import FIELD_TYPE
|
||||
from converters import escape_dict, escape_sequence, escape_string
|
||||
from err import Warning, Error, InterfaceError, DataError, \
|
||||
DatabaseError, OperationalError, IntegrityError, InternalError, \
|
||||
NotSupportedError, ProgrammingError
|
||||
from times import Date, Time, Timestamp, \
|
||||
DateFromTicks, TimeFromTicks, TimestampFromTicks
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
frozenset
|
||||
except NameError:
|
||||
from sets import ImmutableSet as frozenset
|
||||
try:
|
||||
from sets import BaseSet as set
|
||||
except ImportError:
|
||||
from sets import Set as set
|
||||
|
||||
threadsafety = 1
|
||||
apilevel = "2.0"
|
||||
paramstyle = "format"
|
||||
|
||||
class DBAPISet(frozenset):
|
||||
|
||||
|
||||
def __ne__(self, other):
|
||||
if isinstance(other, set):
|
||||
return super(DBAPISet, self).__ne__(self, other)
|
||||
else:
|
||||
return other not in self
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, frozenset):
|
||||
return frozenset.__eq__(self, other)
|
||||
else:
|
||||
return other in self
|
||||
|
||||
def __hash__(self):
|
||||
return frozenset.__hash__(self)
|
||||
|
||||
|
||||
STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING,
|
||||
FIELD_TYPE.VAR_STRING])
|
||||
BINARY = DBAPISet([FIELD_TYPE.BLOB, FIELD_TYPE.LONG_BLOB,
|
||||
FIELD_TYPE.MEDIUM_BLOB, FIELD_TYPE.TINY_BLOB])
|
||||
NUMBER = DBAPISet([FIELD_TYPE.DECIMAL, FIELD_TYPE.DOUBLE, FIELD_TYPE.FLOAT,
|
||||
FIELD_TYPE.INT24, FIELD_TYPE.LONG, FIELD_TYPE.LONGLONG,
|
||||
FIELD_TYPE.TINY, FIELD_TYPE.YEAR])
|
||||
DATE = DBAPISet([FIELD_TYPE.DATE, FIELD_TYPE.NEWDATE])
|
||||
TIME = DBAPISet([FIELD_TYPE.TIME])
|
||||
TIMESTAMP = DBAPISet([FIELD_TYPE.TIMESTAMP, FIELD_TYPE.DATETIME])
|
||||
DATETIME = TIMESTAMP
|
||||
ROWID = DBAPISet()
|
||||
|
||||
def Binary(x):
|
||||
"""Return x as a binary type."""
|
||||
return str(x)
|
||||
|
||||
def Connect(*args, **kwargs):
|
||||
"""
|
||||
Connect to the database; see connections.Connection.__init__() for
|
||||
more information.
|
||||
"""
|
||||
from connections import Connection
|
||||
return Connection(*args, **kwargs)
|
||||
|
||||
def get_client_info(): # for MySQLdb compatibility
|
||||
return '%s.%s.%s' % VERSION
|
||||
|
||||
connect = Connection = Connect
|
||||
|
||||
# we include a doctored version_info here for MySQLdb compatibility
|
||||
version_info = (1,2,2,"final",0)
|
||||
|
||||
NULL = "NULL"
|
||||
|
||||
__version__ = get_client_info()
|
||||
|
||||
def thread_safe():
|
||||
return True # match MySQLdb.thread_safe()
|
||||
|
||||
def install_as_MySQLdb():
|
||||
"""
|
||||
After this function is called, any application that imports MySQLdb or
|
||||
_mysql will unwittingly actually use
|
||||
"""
|
||||
sys.modules["MySQLdb"] = sys.modules["_mysql"] = sys.modules["pymysql"]
|
||||
|
||||
__all__ = [
|
||||
'BINARY', 'Binary', 'Connect', 'Connection', 'DATE', 'Date',
|
||||
'Time', 'Timestamp', 'DateFromTicks', 'TimeFromTicks', 'TimestampFromTicks',
|
||||
'DataError', 'DatabaseError', 'Error', 'FIELD_TYPE', 'IntegrityError',
|
||||
'InterfaceError', 'InternalError', 'MySQLError', 'NULL', 'NUMBER',
|
||||
'NotSupportedError', 'DBAPISet', 'OperationalError', 'ProgrammingError',
|
||||
'ROWID', 'STRING', 'TIME', 'TIMESTAMP', 'Warning', 'apilevel', 'connect',
|
||||
'connections', 'constants', 'converters', 'cursors', 'debug', 'escape',
|
||||
'escape_dict', 'escape_sequence', 'escape_string', 'get_client_info',
|
||||
'paramstyle', 'string_literal', 'threadsafety', 'version_info',
|
||||
|
||||
"install_as_MySQLdb",
|
||||
|
||||
"NULL","__version__",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
MBLENGTH = {
|
||||
8:1,
|
||||
33:3,
|
||||
88:2,
|
||||
91:2
|
||||
}
|
||||
|
||||
class Charset:
|
||||
def __init__(self, id, name, collation, is_default):
|
||||
self.id, self.name, self.collation = id, name, collation
|
||||
self.is_default = is_default == 'Yes'
|
||||
|
||||
class Charsets:
|
||||
def __init__(self):
|
||||
self._by_id = {}
|
||||
|
||||
def add(self, c):
|
||||
self._by_id[c.id] = c
|
||||
|
||||
def by_id(self, id):
|
||||
return self._by_id[id]
|
||||
|
||||
def by_name(self, name):
|
||||
for c in self._by_id.values():
|
||||
if c.name == name and c.is_default:
|
||||
return c
|
||||
|
||||
_charsets = Charsets()
|
||||
"""
|
||||
Generated with:
|
||||
|
||||
mysql -N -s -e "select id, character_set_name, collation_name, is_default
|
||||
from information_schema.collations order by id;" | python -c "import sys
|
||||
for l in sys.stdin.readlines():
|
||||
id, name, collation, is_default = l.split(chr(9))
|
||||
print '_charsets.add(Charset(%s, \'%s\', \'%s\', \'%s\'))' \
|
||||
% (id, name, collation, is_default.strip())
|
||||
"
|
||||
|
||||
"""
|
||||
_charsets.add(Charset(1, 'big5', 'big5_chinese_ci', 'Yes'))
|
||||
_charsets.add(Charset(2, 'latin2', 'latin2_czech_cs', ''))
|
||||
_charsets.add(Charset(3, 'dec8', 'dec8_swedish_ci', 'Yes'))
|
||||
_charsets.add(Charset(4, 'cp850', 'cp850_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(5, 'latin1', 'latin1_german1_ci', ''))
|
||||
_charsets.add(Charset(6, 'hp8', 'hp8_english_ci', 'Yes'))
|
||||
_charsets.add(Charset(7, 'koi8r', 'koi8r_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(8, 'latin1', 'latin1_swedish_ci', 'Yes'))
|
||||
_charsets.add(Charset(9, 'latin2', 'latin2_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(10, 'swe7', 'swe7_swedish_ci', 'Yes'))
|
||||
_charsets.add(Charset(11, 'ascii', 'ascii_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(12, 'ujis', 'ujis_japanese_ci', 'Yes'))
|
||||
_charsets.add(Charset(13, 'sjis', 'sjis_japanese_ci', 'Yes'))
|
||||
_charsets.add(Charset(14, 'cp1251', 'cp1251_bulgarian_ci', ''))
|
||||
_charsets.add(Charset(15, 'latin1', 'latin1_danish_ci', ''))
|
||||
_charsets.add(Charset(16, 'hebrew', 'hebrew_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(18, 'tis620', 'tis620_thai_ci', 'Yes'))
|
||||
_charsets.add(Charset(19, 'euckr', 'euckr_korean_ci', 'Yes'))
|
||||
_charsets.add(Charset(20, 'latin7', 'latin7_estonian_cs', ''))
|
||||
_charsets.add(Charset(21, 'latin2', 'latin2_hungarian_ci', ''))
|
||||
_charsets.add(Charset(22, 'koi8u', 'koi8u_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(23, 'cp1251', 'cp1251_ukrainian_ci', ''))
|
||||
_charsets.add(Charset(24, 'gb2312', 'gb2312_chinese_ci', 'Yes'))
|
||||
_charsets.add(Charset(25, 'greek', 'greek_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(26, 'cp1250', 'cp1250_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(27, 'latin2', 'latin2_croatian_ci', ''))
|
||||
_charsets.add(Charset(28, 'gbk', 'gbk_chinese_ci', 'Yes'))
|
||||
_charsets.add(Charset(29, 'cp1257', 'cp1257_lithuanian_ci', ''))
|
||||
_charsets.add(Charset(30, 'latin5', 'latin5_turkish_ci', 'Yes'))
|
||||
_charsets.add(Charset(31, 'latin1', 'latin1_german2_ci', ''))
|
||||
_charsets.add(Charset(32, 'armscii8', 'armscii8_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(33, 'utf8', 'utf8_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(34, 'cp1250', 'cp1250_czech_cs', ''))
|
||||
_charsets.add(Charset(35, 'ucs2', 'ucs2_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(36, 'cp866', 'cp866_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(37, 'keybcs2', 'keybcs2_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(38, 'macce', 'macce_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(39, 'macroman', 'macroman_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(40, 'cp852', 'cp852_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(41, 'latin7', 'latin7_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(42, 'latin7', 'latin7_general_cs', ''))
|
||||
_charsets.add(Charset(43, 'macce', 'macce_bin', ''))
|
||||
_charsets.add(Charset(44, 'cp1250', 'cp1250_croatian_ci', ''))
|
||||
_charsets.add(Charset(47, 'latin1', 'latin1_bin', ''))
|
||||
_charsets.add(Charset(48, 'latin1', 'latin1_general_ci', ''))
|
||||
_charsets.add(Charset(49, 'latin1', 'latin1_general_cs', ''))
|
||||
_charsets.add(Charset(50, 'cp1251', 'cp1251_bin', ''))
|
||||
_charsets.add(Charset(51, 'cp1251', 'cp1251_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(52, 'cp1251', 'cp1251_general_cs', ''))
|
||||
_charsets.add(Charset(53, 'macroman', 'macroman_bin', ''))
|
||||
_charsets.add(Charset(57, 'cp1256', 'cp1256_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(58, 'cp1257', 'cp1257_bin', ''))
|
||||
_charsets.add(Charset(59, 'cp1257', 'cp1257_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(63, 'binary', 'binary', 'Yes'))
|
||||
_charsets.add(Charset(64, 'armscii8', 'armscii8_bin', ''))
|
||||
_charsets.add(Charset(65, 'ascii', 'ascii_bin', ''))
|
||||
_charsets.add(Charset(66, 'cp1250', 'cp1250_bin', ''))
|
||||
_charsets.add(Charset(67, 'cp1256', 'cp1256_bin', ''))
|
||||
_charsets.add(Charset(68, 'cp866', 'cp866_bin', ''))
|
||||
_charsets.add(Charset(69, 'dec8', 'dec8_bin', ''))
|
||||
_charsets.add(Charset(70, 'greek', 'greek_bin', ''))
|
||||
_charsets.add(Charset(71, 'hebrew', 'hebrew_bin', ''))
|
||||
_charsets.add(Charset(72, 'hp8', 'hp8_bin', ''))
|
||||
_charsets.add(Charset(73, 'keybcs2', 'keybcs2_bin', ''))
|
||||
_charsets.add(Charset(74, 'koi8r', 'koi8r_bin', ''))
|
||||
_charsets.add(Charset(75, 'koi8u', 'koi8u_bin', ''))
|
||||
_charsets.add(Charset(77, 'latin2', 'latin2_bin', ''))
|
||||
_charsets.add(Charset(78, 'latin5', 'latin5_bin', ''))
|
||||
_charsets.add(Charset(79, 'latin7', 'latin7_bin', ''))
|
||||
_charsets.add(Charset(80, 'cp850', 'cp850_bin', ''))
|
||||
_charsets.add(Charset(81, 'cp852', 'cp852_bin', ''))
|
||||
_charsets.add(Charset(82, 'swe7', 'swe7_bin', ''))
|
||||
_charsets.add(Charset(83, 'utf8', 'utf8_bin', ''))
|
||||
_charsets.add(Charset(84, 'big5', 'big5_bin', ''))
|
||||
_charsets.add(Charset(85, 'euckr', 'euckr_bin', ''))
|
||||
_charsets.add(Charset(86, 'gb2312', 'gb2312_bin', ''))
|
||||
_charsets.add(Charset(87, 'gbk', 'gbk_bin', ''))
|
||||
_charsets.add(Charset(88, 'sjis', 'sjis_bin', ''))
|
||||
_charsets.add(Charset(89, 'tis620', 'tis620_bin', ''))
|
||||
_charsets.add(Charset(90, 'ucs2', 'ucs2_bin', ''))
|
||||
_charsets.add(Charset(91, 'ujis', 'ujis_bin', ''))
|
||||
_charsets.add(Charset(92, 'geostd8', 'geostd8_general_ci', 'Yes'))
|
||||
_charsets.add(Charset(93, 'geostd8', 'geostd8_bin', ''))
|
||||
_charsets.add(Charset(94, 'latin1', 'latin1_spanish_ci', ''))
|
||||
_charsets.add(Charset(95, 'cp932', 'cp932_japanese_ci', 'Yes'))
|
||||
_charsets.add(Charset(96, 'cp932', 'cp932_bin', ''))
|
||||
_charsets.add(Charset(97, 'eucjpms', 'eucjpms_japanese_ci', 'Yes'))
|
||||
_charsets.add(Charset(98, 'eucjpms', 'eucjpms_bin', ''))
|
||||
_charsets.add(Charset(99, 'cp1250', 'cp1250_polish_ci', ''))
|
||||
_charsets.add(Charset(128, 'ucs2', 'ucs2_unicode_ci', ''))
|
||||
_charsets.add(Charset(129, 'ucs2', 'ucs2_icelandic_ci', ''))
|
||||
_charsets.add(Charset(130, 'ucs2', 'ucs2_latvian_ci', ''))
|
||||
_charsets.add(Charset(131, 'ucs2', 'ucs2_romanian_ci', ''))
|
||||
_charsets.add(Charset(132, 'ucs2', 'ucs2_slovenian_ci', ''))
|
||||
_charsets.add(Charset(133, 'ucs2', 'ucs2_polish_ci', ''))
|
||||
_charsets.add(Charset(134, 'ucs2', 'ucs2_estonian_ci', ''))
|
||||
_charsets.add(Charset(135, 'ucs2', 'ucs2_spanish_ci', ''))
|
||||
_charsets.add(Charset(136, 'ucs2', 'ucs2_swedish_ci', ''))
|
||||
_charsets.add(Charset(137, 'ucs2', 'ucs2_turkish_ci', ''))
|
||||
_charsets.add(Charset(138, 'ucs2', 'ucs2_czech_ci', ''))
|
||||
_charsets.add(Charset(139, 'ucs2', 'ucs2_danish_ci', ''))
|
||||
_charsets.add(Charset(140, 'ucs2', 'ucs2_lithuanian_ci', ''))
|
||||
_charsets.add(Charset(141, 'ucs2', 'ucs2_slovak_ci', ''))
|
||||
_charsets.add(Charset(142, 'ucs2', 'ucs2_spanish2_ci', ''))
|
||||
_charsets.add(Charset(143, 'ucs2', 'ucs2_roman_ci', ''))
|
||||
_charsets.add(Charset(144, 'ucs2', 'ucs2_persian_ci', ''))
|
||||
_charsets.add(Charset(145, 'ucs2', 'ucs2_esperanto_ci', ''))
|
||||
_charsets.add(Charset(146, 'ucs2', 'ucs2_hungarian_ci', ''))
|
||||
_charsets.add(Charset(192, 'utf8', 'utf8_unicode_ci', ''))
|
||||
_charsets.add(Charset(193, 'utf8', 'utf8_icelandic_ci', ''))
|
||||
_charsets.add(Charset(194, 'utf8', 'utf8_latvian_ci', ''))
|
||||
_charsets.add(Charset(195, 'utf8', 'utf8_romanian_ci', ''))
|
||||
_charsets.add(Charset(196, 'utf8', 'utf8_slovenian_ci', ''))
|
||||
_charsets.add(Charset(197, 'utf8', 'utf8_polish_ci', ''))
|
||||
_charsets.add(Charset(198, 'utf8', 'utf8_estonian_ci', ''))
|
||||
_charsets.add(Charset(199, 'utf8', 'utf8_spanish_ci', ''))
|
||||
_charsets.add(Charset(200, 'utf8', 'utf8_swedish_ci', ''))
|
||||
_charsets.add(Charset(201, 'utf8', 'utf8_turkish_ci', ''))
|
||||
_charsets.add(Charset(202, 'utf8', 'utf8_czech_ci', ''))
|
||||
_charsets.add(Charset(203, 'utf8', 'utf8_danish_ci', ''))
|
||||
_charsets.add(Charset(204, 'utf8', 'utf8_lithuanian_ci', ''))
|
||||
_charsets.add(Charset(205, 'utf8', 'utf8_slovak_ci', ''))
|
||||
_charsets.add(Charset(206, 'utf8', 'utf8_spanish2_ci', ''))
|
||||
_charsets.add(Charset(207, 'utf8', 'utf8_roman_ci', ''))
|
||||
_charsets.add(Charset(208, 'utf8', 'utf8_persian_ci', ''))
|
||||
_charsets.add(Charset(209, 'utf8', 'utf8_esperanto_ci', ''))
|
||||
_charsets.add(Charset(210, 'utf8', 'utf8_hungarian_ci', ''))
|
||||
|
||||
def charset_by_name(name):
|
||||
return _charsets.by_name(name)
|
||||
|
||||
def charset_by_id(id):
|
||||
return _charsets.by_id(id)
|
||||
|
||||
|
||||
@@ -0,0 +1,933 @@
|
||||
# Python implementation of the MySQL client-server protocol
|
||||
# http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol
|
||||
|
||||
try:
|
||||
import hashlib
|
||||
sha_new = lambda *args, **kwargs: hashlib.new("sha1", *args, **kwargs)
|
||||
except ImportError:
|
||||
import sha
|
||||
sha_new = sha.new
|
||||
|
||||
import socket
|
||||
try:
|
||||
import ssl
|
||||
SSL_ENABLED = True
|
||||
except ImportError:
|
||||
SSL_ENABLED = False
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
import ConfigParser
|
||||
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
except ImportError:
|
||||
import StringIO
|
||||
|
||||
from charset import MBLENGTH, charset_by_name, charset_by_id
|
||||
from cursors import Cursor
|
||||
from constants import FIELD_TYPE, FLAG
|
||||
from constants import SERVER_STATUS
|
||||
from constants.CLIENT import *
|
||||
from constants.COMMAND import *
|
||||
from util import join_bytes, byte2int, int2byte
|
||||
from converters import escape_item, encoders, decoders
|
||||
from err import raise_mysql_exception, Warning, Error, \
|
||||
InterfaceError, DataError, DatabaseError, OperationalError, \
|
||||
IntegrityError, InternalError, NotSupportedError, ProgrammingError
|
||||
|
||||
DEBUG = False
|
||||
|
||||
NULL_COLUMN = 251
|
||||
UNSIGNED_CHAR_COLUMN = 251
|
||||
UNSIGNED_SHORT_COLUMN = 252
|
||||
UNSIGNED_INT24_COLUMN = 253
|
||||
UNSIGNED_INT64_COLUMN = 254
|
||||
UNSIGNED_CHAR_LENGTH = 1
|
||||
UNSIGNED_SHORT_LENGTH = 2
|
||||
UNSIGNED_INT24_LENGTH = 3
|
||||
UNSIGNED_INT64_LENGTH = 8
|
||||
|
||||
DEFAULT_CHARSET = 'latin1'
|
||||
MAX_PACKET_LENGTH = 256*256*256-1
|
||||
|
||||
|
||||
def dump_packet(data):
|
||||
|
||||
def is_ascii(data):
|
||||
if byte2int(data) >= 65 and byte2int(data) <= 122: #data.isalnum():
|
||||
return data
|
||||
return '.'
|
||||
print "packet length %d" % len(data)
|
||||
print "method call[1]: %s" % sys._getframe(1).f_code.co_name
|
||||
print "method call[2]: %s" % sys._getframe(2).f_code.co_name
|
||||
print "method call[3]: %s" % sys._getframe(3).f_code.co_name
|
||||
print "method call[4]: %s" % sys._getframe(4).f_code.co_name
|
||||
print "method call[5]: %s" % sys._getframe(5).f_code.co_name
|
||||
print "-" * 88
|
||||
dump_data = [data[i:i+16] for i in xrange(len(data)) if i%16 == 0]
|
||||
for d in dump_data:
|
||||
print ' '.join(map(lambda x:"%02X" % byte2int(x), d)) + \
|
||||
' ' * (16 - len(d)) + ' ' * 2 + \
|
||||
' '.join(map(lambda x:"%s" % is_ascii(x), d))
|
||||
print "-" * 88
|
||||
print ""
|
||||
|
||||
def _scramble(password, message):
|
||||
if password == None or len(password) == 0:
|
||||
return int2byte(0)
|
||||
if DEBUG: print 'password=' + password
|
||||
stage1 = sha_new(password).digest()
|
||||
stage2 = sha_new(stage1).digest()
|
||||
s = sha_new()
|
||||
s.update(message)
|
||||
s.update(stage2)
|
||||
result = s.digest()
|
||||
return _my_crypt(result, stage1)
|
||||
|
||||
def _my_crypt(message1, message2):
|
||||
length = len(message1)
|
||||
result = struct.pack('B', length)
|
||||
for i in xrange(length):
|
||||
x = (struct.unpack('B', message1[i:i+1])[0] ^ \
|
||||
struct.unpack('B', message2[i:i+1])[0])
|
||||
result += struct.pack('B', x)
|
||||
return result
|
||||
|
||||
# old_passwords support ported from libmysql/password.c
|
||||
SCRAMBLE_LENGTH_323 = 8
|
||||
|
||||
class RandStruct_323(object):
|
||||
def __init__(self, seed1, seed2):
|
||||
self.max_value = 0x3FFFFFFFL
|
||||
self.seed1 = seed1 % self.max_value
|
||||
self.seed2 = seed2 % self.max_value
|
||||
|
||||
def my_rnd(self):
|
||||
self.seed1 = (self.seed1 * 3L + self.seed2) % self.max_value
|
||||
self.seed2 = (self.seed1 + self.seed2 + 33L) % self.max_value
|
||||
return float(self.seed1) / float(self.max_value)
|
||||
|
||||
def _scramble_323(password, message):
|
||||
hash_pass = _hash_password_323(password)
|
||||
hash_message = _hash_password_323(message[:SCRAMBLE_LENGTH_323])
|
||||
hash_pass_n = struct.unpack(">LL", hash_pass)
|
||||
hash_message_n = struct.unpack(">LL", hash_message)
|
||||
|
||||
rand_st = RandStruct_323(hash_pass_n[0] ^ hash_message_n[0],
|
||||
hash_pass_n[1] ^ hash_message_n[1])
|
||||
outbuf = StringIO.StringIO()
|
||||
for _ in xrange(min(SCRAMBLE_LENGTH_323, len(message))):
|
||||
outbuf.write(int2byte(int(rand_st.my_rnd() * 31) + 64))
|
||||
extra = int2byte(int(rand_st.my_rnd() * 31))
|
||||
out = outbuf.getvalue()
|
||||
outbuf = StringIO.StringIO()
|
||||
for c in out:
|
||||
outbuf.write(int2byte(byte2int(c) ^ byte2int(extra)))
|
||||
return outbuf.getvalue()
|
||||
|
||||
def _hash_password_323(password):
|
||||
nr = 1345345333L
|
||||
add = 7L
|
||||
nr2 = 0x12345671L
|
||||
|
||||
for c in [byte2int(x) for x in password if x not in (' ', '\t')]:
|
||||
nr^= (((nr & 63)+add)*c)+ (nr << 8) & 0xFFFFFFFF
|
||||
nr2= (nr2 + ((nr2 << 8) ^ nr)) & 0xFFFFFFFF
|
||||
add= (add + c) & 0xFFFFFFFF
|
||||
|
||||
r1 = nr & ((1L << 31) - 1L) # kill sign bits
|
||||
r2 = nr2 & ((1L << 31) - 1L)
|
||||
|
||||
# pack
|
||||
return struct.pack(">LL", r1, r2)
|
||||
|
||||
def pack_int24(n):
|
||||
return struct.pack('BBB', n&0xFF, (n>>8)&0xFF, (n>>16)&0xFF)
|
||||
|
||||
def unpack_uint16(n):
|
||||
return struct.unpack('<H', n[0:2])[0]
|
||||
|
||||
|
||||
# TODO: stop using bit-shifting in these functions...
|
||||
# TODO: rename to "uint" to make it clear they're unsigned...
|
||||
def unpack_int24(n):
|
||||
return struct.unpack('B',n[0])[0] + (struct.unpack('B', n[1])[0] << 8) +\
|
||||
(struct.unpack('B',n[2])[0] << 16)
|
||||
|
||||
def unpack_int32(n):
|
||||
return struct.unpack('B',n[0])[0] + (struct.unpack('B', n[1])[0] << 8) +\
|
||||
(struct.unpack('B',n[2])[0] << 16) + (struct.unpack('B', n[3])[0] << 24)
|
||||
|
||||
def unpack_int64(n):
|
||||
return struct.unpack('B',n[0])[0] + (struct.unpack('B', n[1])[0]<<8) +\
|
||||
(struct.unpack('B',n[2])[0] << 16) + (struct.unpack('B',n[3])[0]<<24)+\
|
||||
(struct.unpack('B',n[4])[0] << 32) + (struct.unpack('B',n[5])[0]<<40)+\
|
||||
(struct.unpack('B',n[6])[0] << 48) + (struct.unpack('B',n[7])[0]<<56)
|
||||
|
||||
def defaulterrorhandler(connection, cursor, errorclass, errorvalue):
|
||||
err = errorclass, errorvalue
|
||||
if DEBUG:
|
||||
raise
|
||||
|
||||
if cursor:
|
||||
cursor.messages.append(err)
|
||||
else:
|
||||
connection.messages.append(err)
|
||||
del cursor
|
||||
del connection
|
||||
|
||||
if not issubclass(errorclass, Error):
|
||||
raise Error(errorclass, errorvalue)
|
||||
else:
|
||||
raise errorclass, errorvalue
|
||||
|
||||
|
||||
class MysqlPacket(object):
|
||||
"""Representation of a MySQL response packet. Reads in the packet
|
||||
from the network socket, removes packet header and provides an interface
|
||||
for reading/parsing the packet results."""
|
||||
|
||||
def __init__(self, socket):
|
||||
self.__position = 0
|
||||
self.__recv_packet(socket)
|
||||
del socket
|
||||
|
||||
def __recv_packet(self, socket):
|
||||
"""Parse the packet header and read entire packet payload into buffer."""
|
||||
packet_header = socket.recv(4)
|
||||
while len(packet_header) < 4:
|
||||
d = socket.recv(4 - len(packet_header))
|
||||
if len(d) == 0:
|
||||
raise OperationalError(2013, "Lost connection to MySQL server during query")
|
||||
packet_header += d
|
||||
|
||||
if DEBUG: dump_packet(packet_header)
|
||||
packet_length_bin = packet_header[:3]
|
||||
self.__packet_number = byte2int(packet_header[3])
|
||||
# TODO: check packet_num is correct (+1 from last packet)
|
||||
|
||||
bin_length = packet_length_bin + int2byte(0) # pad little-endian number
|
||||
bytes_to_read = struct.unpack('<I', bin_length)[0]
|
||||
|
||||
payload_buff = [] # this is faster than cStringIO
|
||||
while bytes_to_read > 0:
|
||||
recv_data = socket.recv(bytes_to_read)
|
||||
if len(recv_data) == 0:
|
||||
raise OperationalError(2013, "Lost connection to MySQL server during query")
|
||||
if DEBUG: dump_packet(recv_data)
|
||||
payload_buff.append(recv_data)
|
||||
bytes_to_read -= len(recv_data)
|
||||
self.__data = join_bytes(payload_buff)
|
||||
|
||||
def packet_number(self): return self.__packet_number
|
||||
|
||||
def get_all_data(self): return self.__data
|
||||
|
||||
def read(self, size):
|
||||
"""Read the first 'size' bytes in packet and advance cursor past them."""
|
||||
result = self.peek(size)
|
||||
self.advance(size)
|
||||
return result
|
||||
|
||||
def read_all(self):
|
||||
"""Read all remaining data in the packet.
|
||||
|
||||
(Subsequent read() or peek() will return errors.)
|
||||
"""
|
||||
result = self.__data[self.__position:]
|
||||
self.__position = None # ensure no subsequent read() or peek()
|
||||
return result
|
||||
|
||||
def advance(self, length):
|
||||
"""Advance the cursor in data buffer 'length' bytes."""
|
||||
new_position = self.__position + length
|
||||
if new_position < 0 or new_position > len(self.__data):
|
||||
raise Exception('Invalid advance amount (%s) for cursor. '
|
||||
'Position=%s' % (length, new_position))
|
||||
self.__position = new_position
|
||||
|
||||
def rewind(self, position=0):
|
||||
"""Set the position of the data buffer cursor to 'position'."""
|
||||
if position < 0 or position > len(self.__data):
|
||||
raise Exception("Invalid position to rewind cursor to: %s." % position)
|
||||
self.__position = position
|
||||
|
||||
def peek(self, size):
|
||||
"""Look at the first 'size' bytes in packet without moving cursor."""
|
||||
result = self.__data[self.__position:(self.__position+size)]
|
||||
if len(result) != size:
|
||||
error = ('Result length not requested length:\n'
|
||||
'Expected=%s. Actual=%s. Position: %s. Data Length: %s'
|
||||
% (size, len(result), self.__position, len(self.__data)))
|
||||
if DEBUG:
|
||||
print error
|
||||
self.dump()
|
||||
raise AssertionError(error)
|
||||
return result
|
||||
|
||||
def get_bytes(self, position, length=1):
|
||||
"""Get 'length' bytes starting at 'position'.
|
||||
|
||||
Position is start of payload (first four packet header bytes are not
|
||||
included) starting at index '0'.
|
||||
|
||||
No error checking is done. If requesting outside end of buffer
|
||||
an empty string (or string shorter than 'length') may be returned!
|
||||
"""
|
||||
return self.__data[position:(position+length)]
|
||||
|
||||
def read_length_coded_binary(self):
|
||||
"""Read a 'Length Coded Binary' number from the data buffer.
|
||||
|
||||
Length coded numbers can be anywhere from 1 to 9 bytes depending
|
||||
on the value of the first byte.
|
||||
"""
|
||||
c = byte2int(self.read(1))
|
||||
if c == NULL_COLUMN:
|
||||
return None
|
||||
if c < UNSIGNED_CHAR_COLUMN:
|
||||
return c
|
||||
elif c == UNSIGNED_SHORT_COLUMN:
|
||||
return unpack_uint16(self.read(UNSIGNED_SHORT_LENGTH))
|
||||
elif c == UNSIGNED_INT24_COLUMN:
|
||||
return unpack_int24(self.read(UNSIGNED_INT24_LENGTH))
|
||||
elif c == UNSIGNED_INT64_COLUMN:
|
||||
# TODO: what was 'longlong'? confirm it wasn't used?
|
||||
return unpack_int64(self.read(UNSIGNED_INT64_LENGTH))
|
||||
|
||||
def read_length_coded_string(self):
|
||||
"""Read a 'Length Coded String' from the data buffer.
|
||||
|
||||
A 'Length Coded String' consists first of a length coded
|
||||
(unsigned, positive) integer represented in 1-9 bytes followed by
|
||||
that many bytes of binary data. (For example "cat" would be "3cat".)
|
||||
"""
|
||||
length = self.read_length_coded_binary()
|
||||
if length is None:
|
||||
return None
|
||||
return self.read(length)
|
||||
|
||||
def is_ok_packet(self):
|
||||
return byte2int(self.get_bytes(0)) == 0
|
||||
|
||||
def is_eof_packet(self):
|
||||
return byte2int(self.get_bytes(0)) == 254 # 'fe'
|
||||
|
||||
def is_resultset_packet(self):
|
||||
field_count = byte2int(self.get_bytes(0))
|
||||
return field_count >= 1 and field_count <= 250
|
||||
|
||||
def is_error_packet(self):
|
||||
return byte2int(self.get_bytes(0)) == 255
|
||||
|
||||
def check_error(self):
|
||||
if self.is_error_packet():
|
||||
self.rewind()
|
||||
self.advance(1) # field_count == error (we already know that)
|
||||
errno = unpack_uint16(self.read(2))
|
||||
if DEBUG: print "errno = %d" % errno
|
||||
raise_mysql_exception(self.__data)
|
||||
|
||||
def dump(self):
|
||||
dump_packet(self.__data)
|
||||
|
||||
|
||||
class FieldDescriptorPacket(MysqlPacket):
|
||||
"""A MysqlPacket that represents a specific column's metadata in the result.
|
||||
|
||||
Parsing is automatically done and the results are exported via public
|
||||
attributes on the class such as: db, table_name, name, length, type_code.
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
MysqlPacket.__init__(self, *args)
|
||||
self.__parse_field_descriptor()
|
||||
|
||||
def __parse_field_descriptor(self):
|
||||
"""Parse the 'Field Descriptor' (Metadata) packet.
|
||||
|
||||
This is compatible with MySQL 4.1+ (not compatible with MySQL 4.0).
|
||||
"""
|
||||
self.catalog = self.read_length_coded_string()
|
||||
self.db = self.read_length_coded_string()
|
||||
self.table_name = self.read_length_coded_string()
|
||||
self.org_table = self.read_length_coded_string()
|
||||
self.name = self.read_length_coded_string()
|
||||
self.org_name = self.read_length_coded_string()
|
||||
self.advance(1) # non-null filler
|
||||
self.charsetnr = struct.unpack('<H', self.read(2))[0]
|
||||
self.length = struct.unpack('<I', self.read(4))[0]
|
||||
self.type_code = byte2int(self.read(1))
|
||||
self.flags = struct.unpack('<H', self.read(2))[0]
|
||||
self.scale = byte2int(self.read(1)) # "decimals"
|
||||
self.advance(2) # filler (always 0x00)
|
||||
|
||||
# 'default' is a length coded binary and is still in the buffer?
|
||||
# not used for normal result sets...
|
||||
|
||||
def description(self):
|
||||
"""Provides a 7-item tuple compatible with the Python PEP249 DB Spec."""
|
||||
desc = []
|
||||
desc.append(self.name)
|
||||
desc.append(self.type_code)
|
||||
desc.append(None) # TODO: display_length; should this be self.length?
|
||||
desc.append(self.get_column_length()) # 'internal_size'
|
||||
desc.append(self.get_column_length()) # 'precision' # TODO: why!?!?
|
||||
desc.append(self.scale)
|
||||
|
||||
# 'null_ok' -- can this be True/False rather than 1/0?
|
||||
# if so just do: desc.append(bool(self.flags % 2 == 0))
|
||||
if self.flags % 2 == 0:
|
||||
desc.append(1)
|
||||
else:
|
||||
desc.append(0)
|
||||
return tuple(desc)
|
||||
|
||||
def get_column_length(self):
|
||||
if self.type_code == FIELD_TYPE.VAR_STRING:
|
||||
mblen = MBLENGTH.get(self.charsetnr, 1)
|
||||
return self.length // mblen
|
||||
return self.length
|
||||
|
||||
def __str__(self):
|
||||
return ('%s %s.%s.%s, type=%s'
|
||||
% (self.__class__, self.db, self.table_name, self.name,
|
||||
self.type_code))
|
||||
|
||||
|
||||
class Connection(object):
|
||||
"""
|
||||
Representation of a socket with a mysql server.
|
||||
|
||||
The proper way to get an instance of this class is to call
|
||||
connect()."""
|
||||
errorhandler = defaulterrorhandler
|
||||
|
||||
def __init__(self, host="localhost", user=None, passwd="",
|
||||
db=None, port=3306, unix_socket=None,
|
||||
charset='', sql_mode=None,
|
||||
read_default_file=None, conv=decoders, use_unicode=None,
|
||||
client_flag=0, cursorclass=Cursor, init_command=None,
|
||||
connect_timeout=None, ssl=None, read_default_group=None,
|
||||
compress=None, named_pipe=None):
|
||||
"""
|
||||
Establish a connection to the MySQL database. Accepts several
|
||||
arguments:
|
||||
|
||||
host: Host where the database server is located
|
||||
user: Username to log in as
|
||||
passwd: Password to use.
|
||||
db: Database to use, None to not use a particular one.
|
||||
port: MySQL port to use, default is usually OK.
|
||||
unix_socket: Optionally, you can use a unix socket rather than TCP/IP.
|
||||
charset: Charset you want to use.
|
||||
sql_mode: Default SQL_MODE to use.
|
||||
read_default_file: Specifies my.cnf file to read these parameters from under the [client] section.
|
||||
conv: Decoders dictionary to use instead of the default one. This is used to provide custom marshalling of types. See converters.
|
||||
use_unicode: Whether or not to default to unicode strings. This option defaults to true for Py3k.
|
||||
client_flag: Custom flags to send to MySQL. Find potential values in constants.CLIENT.
|
||||
cursorclass: Custom cursor class to use.
|
||||
init_command: Initial SQL statement to run when connection is established.
|
||||
connect_timeout: Timeout before throwing an exception when connecting.
|
||||
ssl: A dict of arguments similar to mysql_ssl_set()'s parameters. For now the capath and cipher arguments are not supported.
|
||||
read_default_group: Group to read from in the configuration file.
|
||||
compress; Not supported
|
||||
named_pipe: Not supported
|
||||
"""
|
||||
|
||||
if use_unicode is None and sys.version_info[0] > 2:
|
||||
use_unicode = True
|
||||
|
||||
if compress or named_pipe:
|
||||
raise NotImplementedError, "compress and named_pipe arguments are not supported"
|
||||
|
||||
if ssl and (ssl.has_key('capath') or ssl.has_key('cipher')):
|
||||
raise NotImplementedError, 'ssl options capath and cipher are not supported'
|
||||
|
||||
self.ssl = False
|
||||
if ssl:
|
||||
if not SSL_ENABLED:
|
||||
raise NotImplementedError, "ssl module not found"
|
||||
self.ssl = True
|
||||
client_flag |= SSL
|
||||
for k in ('key', 'cert', 'ca'):
|
||||
v = None
|
||||
if ssl.has_key(k):
|
||||
v = ssl[k]
|
||||
setattr(self, k, v)
|
||||
|
||||
if read_default_group and not read_default_file:
|
||||
if sys.platform.startswith("win"):
|
||||
read_default_file = "c:\\my.ini"
|
||||
else:
|
||||
read_default_file = "/etc/my.cnf"
|
||||
|
||||
if read_default_file:
|
||||
if not read_default_group:
|
||||
read_default_group = "client"
|
||||
|
||||
cfg = ConfigParser.RawConfigParser()
|
||||
cfg.read(os.path.expanduser(read_default_file))
|
||||
|
||||
def _config(key, default):
|
||||
try:
|
||||
return cfg.get(read_default_group,key)
|
||||
except:
|
||||
return default
|
||||
|
||||
user = _config("user",user)
|
||||
passwd = _config("password",passwd)
|
||||
host = _config("host", host)
|
||||
db = _config("db",db)
|
||||
unix_socket = _config("socket",unix_socket)
|
||||
port = _config("port", port)
|
||||
charset = _config("default-character-set", charset)
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.user = user
|
||||
self.password = passwd
|
||||
self.db = db
|
||||
self.unix_socket = unix_socket
|
||||
if charset:
|
||||
self.charset = charset
|
||||
self.use_unicode = True
|
||||
else:
|
||||
self.charset = DEFAULT_CHARSET
|
||||
self.use_unicode = False
|
||||
|
||||
if use_unicode:
|
||||
self.use_unicode = use_unicode
|
||||
|
||||
client_flag |= CAPABILITIES
|
||||
client_flag |= MULTI_STATEMENTS
|
||||
if self.db:
|
||||
client_flag |= CONNECT_WITH_DB
|
||||
self.client_flag = client_flag
|
||||
|
||||
self.cursorclass = cursorclass
|
||||
self.connect_timeout = connect_timeout
|
||||
|
||||
self._connect()
|
||||
|
||||
self.messages = []
|
||||
self.set_charset(charset)
|
||||
self.encoders = encoders
|
||||
self.decoders = conv
|
||||
|
||||
self._affected_rows = 0
|
||||
self.host_info = "Not connected"
|
||||
|
||||
self.autocommit(False)
|
||||
|
||||
if sql_mode is not None:
|
||||
c = self.cursor()
|
||||
c.execute("SET sql_mode=%s", (sql_mode,))
|
||||
|
||||
self.commit()
|
||||
|
||||
if init_command is not None:
|
||||
c = self.cursor()
|
||||
c.execute(init_command)
|
||||
|
||||
self.commit()
|
||||
|
||||
|
||||
def close(self):
|
||||
''' Send the quit message and close the socket '''
|
||||
send_data = struct.pack('<i',1) + int2byte(COM_QUIT)
|
||||
self.socket.send(send_data)
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def autocommit(self, value):
|
||||
''' Set whether or not to commit after every execute() '''
|
||||
try:
|
||||
self._execute_command(COM_QUERY, "SET AUTOCOMMIT = %s" % \
|
||||
self.escape(value))
|
||||
self.read_packet()
|
||||
except:
|
||||
exc,value,tb = sys.exc_info()
|
||||
self.errorhandler(None, exc, value)
|
||||
|
||||
def commit(self):
|
||||
''' Commit changes to stable storage '''
|
||||
try:
|
||||
self._execute_command(COM_QUERY, "COMMIT")
|
||||
self.read_packet()
|
||||
except:
|
||||
exc,value,tb = sys.exc_info()
|
||||
self.errorhandler(None, exc, value)
|
||||
|
||||
def rollback(self):
|
||||
''' Roll back the current transaction '''
|
||||
try:
|
||||
self._execute_command(COM_QUERY, "ROLLBACK")
|
||||
self.read_packet()
|
||||
except:
|
||||
exc,value,tb = sys.exc_info()
|
||||
self.errorhandler(None, exc, value)
|
||||
|
||||
def escape(self, obj):
|
||||
''' Escape whatever value you pass to it '''
|
||||
return escape_item(obj, self.charset)
|
||||
|
||||
def literal(self, obj):
|
||||
''' Alias for escape() '''
|
||||
return escape_item(obj, self.charset)
|
||||
|
||||
def cursor(self):
|
||||
''' Create a new cursor to execute queries with '''
|
||||
return self.cursorclass(self)
|
||||
|
||||
def __enter__(self):
|
||||
''' Context manager that returns a Cursor '''
|
||||
return self.cursor()
|
||||
|
||||
def __exit__(self, exc, value, traceback):
|
||||
''' On successful exit, commit. On exception, rollback. '''
|
||||
if exc:
|
||||
self.rollback()
|
||||
else:
|
||||
self.commit()
|
||||
|
||||
# The following methods are INTERNAL USE ONLY (called from Cursor)
|
||||
def query(self, sql):
|
||||
self._execute_command(COM_QUERY, sql)
|
||||
self._affected_rows = self._read_query_result()
|
||||
return self._affected_rows
|
||||
|
||||
def next_result(self):
|
||||
self._affected_rows = self._read_query_result()
|
||||
return self._affected_rows
|
||||
|
||||
def affected_rows(self):
|
||||
return self._affected_rows
|
||||
|
||||
def kill(self, thread_id):
|
||||
arg = struct.pack('<I', thread_id)
|
||||
try:
|
||||
self._execute_command(COM_PROCESS_KILL, arg)
|
||||
except:
|
||||
exc,value,tb = sys.exc_info()
|
||||
self.errorhandler(None, exc, value)
|
||||
return
|
||||
pkt = self.read_packet()
|
||||
return pkt.is_ok_packet()
|
||||
|
||||
def ping(self, reconnect=True):
|
||||
''' Check if the server is alive '''
|
||||
try:
|
||||
self._execute_command(COM_PING, "")
|
||||
except:
|
||||
if reconnect:
|
||||
self._connect()
|
||||
return self.ping(False)
|
||||
else:
|
||||
exc,value,tb = sys.exc_info()
|
||||
self.errorhandler(None, exc, value)
|
||||
return
|
||||
|
||||
pkt = self.read_packet()
|
||||
return pkt.is_ok_packet()
|
||||
|
||||
def set_charset(self, charset):
|
||||
try:
|
||||
if charset:
|
||||
self._execute_command(COM_QUERY, "SET NAMES %s" %
|
||||
self.escape(charset))
|
||||
self.read_packet()
|
||||
self.charset = charset
|
||||
except:
|
||||
exc,value,tb = sys.exc_info()
|
||||
self.errorhandler(None, exc, value)
|
||||
|
||||
def _connect(self):
|
||||
try:
|
||||
if self.unix_socket and (self.host == 'localhost' or self.host == '127.0.0.1'):
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
t = sock.gettimeout()
|
||||
sock.settimeout(self.connect_timeout)
|
||||
sock.connect(self.unix_socket)
|
||||
sock.settimeout(t)
|
||||
self.host_info = "Localhost via UNIX socket"
|
||||
if DEBUG: print 'connected using unix_socket'
|
||||
else:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
t = sock.gettimeout()
|
||||
sock.settimeout(self.connect_timeout)
|
||||
sock.connect((self.host, self.port))
|
||||
sock.settimeout(t)
|
||||
self.host_info = "socket %s:%d" % (self.host, self.port)
|
||||
if DEBUG: print 'connected using socket'
|
||||
self.socket = sock
|
||||
self._get_server_information()
|
||||
self._request_authentication()
|
||||
except socket.error, e:
|
||||
raise OperationalError(2003, "Can't connect to MySQL server on %r (%d)" % (self.host, e.args[0]))
|
||||
|
||||
def read_packet(self, packet_type=MysqlPacket):
|
||||
"""Read an entire "mysql packet" in its entirety from the network
|
||||
and return a MysqlPacket type that represents the results."""
|
||||
|
||||
# TODO: is socket.recv(small_number) significantly slower than
|
||||
# socket.recv(large_number)? if so, maybe we should buffer
|
||||
# the socket.recv() (though that obviously makes memory management
|
||||
# more complicated.
|
||||
packet = packet_type(self.socket)
|
||||
packet.check_error()
|
||||
return packet
|
||||
|
||||
def _read_query_result(self):
|
||||
result = MySQLResult(self)
|
||||
result.read()
|
||||
self._result = result
|
||||
return result.affected_rows
|
||||
|
||||
def _send_command(self, command, sql):
|
||||
#send_data = struct.pack('<i', len(sql) + 1) + command + sql
|
||||
# could probably be more efficient, at least it's correct
|
||||
if not self.socket:
|
||||
self.errorhandler(None, InterfaceError, "(0, '')")
|
||||
|
||||
if isinstance(sql, unicode):
|
||||
sql = sql.encode(self.charset)
|
||||
|
||||
buf = int2byte(command) + sql
|
||||
pckt_no = 0
|
||||
while len(buf) >= MAX_PACKET_LENGTH:
|
||||
header = struct.pack('<i', MAX_PACKET_LENGTH)[:-1]+int2byte(pckt_no)
|
||||
send_data = header + buf[:MAX_PACKET_LENGTH]
|
||||
self.socket.send(send_data)
|
||||
if DEBUG: dump_packet(send_data)
|
||||
buf = buf[MAX_PACKET_LENGTH:]
|
||||
pckt_no += 1
|
||||
header = struct.pack('<i', len(buf))[:-1]+int2byte(pckt_no)
|
||||
self.socket.send(header+buf)
|
||||
|
||||
|
||||
#sock = self.socket
|
||||
#sock.send(send_data)
|
||||
|
||||
#
|
||||
|
||||
def _execute_command(self, command, sql):
|
||||
self._send_command(command, sql)
|
||||
|
||||
def _request_authentication(self):
|
||||
self._send_authentication()
|
||||
|
||||
def _send_authentication(self):
|
||||
sock = self.socket
|
||||
self.client_flag |= CAPABILITIES
|
||||
if self.server_version.startswith('5'):
|
||||
self.client_flag |= MULTI_RESULTS
|
||||
|
||||
if self.user is None:
|
||||
raise ValueError, "Did not specify a username"
|
||||
|
||||
charset_id = charset_by_name(self.charset).id
|
||||
self.user = self.user.encode(self.charset)
|
||||
|
||||
data_init = struct.pack('<i', self.client_flag) + struct.pack("<I", 1) + \
|
||||
int2byte(charset_id) + int2byte(0)*23
|
||||
|
||||
next_packet = 1
|
||||
|
||||
if self.ssl:
|
||||
data = pack_int24(len(data_init)) + int2byte(next_packet) + data_init
|
||||
next_packet += 1
|
||||
|
||||
if DEBUG: dump_packet(data)
|
||||
|
||||
sock.send(data)
|
||||
sock = self.socket = ssl.wrap_socket(sock, keyfile=self.key,
|
||||
certfile=self.cert,
|
||||
ssl_version=ssl.PROTOCOL_TLSv1,
|
||||
cert_reqs=ssl.CERT_REQUIRED,
|
||||
ca_certs=self.ca)
|
||||
|
||||
data = data_init + self.user+int2byte(0) + _scramble(self.password.encode(self.charset), self.salt)
|
||||
|
||||
if self.db:
|
||||
self.db = self.db.encode(self.charset)
|
||||
data += self.db + int2byte(0)
|
||||
|
||||
data = pack_int24(len(data)) + int2byte(next_packet) + data
|
||||
next_packet += 2
|
||||
|
||||
if DEBUG: dump_packet(data)
|
||||
|
||||
sock.send(data)
|
||||
|
||||
auth_packet = MysqlPacket(sock)
|
||||
auth_packet.check_error()
|
||||
if DEBUG: auth_packet.dump()
|
||||
|
||||
# if old_passwords is enabled the packet will be 1 byte long and
|
||||
# have the octet 254
|
||||
|
||||
if auth_packet.is_eof_packet():
|
||||
# send legacy handshake
|
||||
#raise NotImplementedError, "old_passwords are not supported. Check to see if mysqld was started with --old-passwords, if old-passwords=1 in a my.cnf file, or if there are some short hashes in your mysql.user table."
|
||||
# TODO: is this the correct charset?
|
||||
data = _scramble_323(self.password.encode(self.charset), self.salt.encode(self.charset)) + int2byte(0)
|
||||
data = pack_int24(len(data)) + int2byte(next_packet) + data
|
||||
|
||||
sock.send(data)
|
||||
auth_packet = MysqlPacket(sock)
|
||||
auth_packet.check_error()
|
||||
if DEBUG: auth_packet.dump()
|
||||
|
||||
|
||||
# _mysql support
|
||||
def thread_id(self):
|
||||
return self.server_thread_id[0]
|
||||
|
||||
def character_set_name(self):
|
||||
return self.charset
|
||||
|
||||
def get_host_info(self):
|
||||
return self.host_info
|
||||
|
||||
def get_proto_info(self):
|
||||
return self.protocol_version
|
||||
|
||||
def _get_server_information(self):
|
||||
sock = self.socket
|
||||
i = 0
|
||||
packet = MysqlPacket(sock)
|
||||
data = packet.get_all_data()
|
||||
|
||||
if DEBUG: dump_packet(data)
|
||||
#packet_len = byte2int(data[i:i+1])
|
||||
#i += 4
|
||||
self.protocol_version = byte2int(data[i:i+1])
|
||||
|
||||
i += 1
|
||||
server_end = data.find(int2byte(0), i)
|
||||
# TODO: is this the correct charset? should it be default_charset?
|
||||
self.server_version = data[i:server_end].decode(self.charset)
|
||||
|
||||
i = server_end + 1
|
||||
self.server_thread_id = struct.unpack('<h', data[i:i+2])
|
||||
|
||||
i += 4
|
||||
self.salt = data[i:i+8]
|
||||
|
||||
i += 9
|
||||
if len(data) >= i + 1:
|
||||
i += 1
|
||||
|
||||
self.server_capabilities = struct.unpack('<h', data[i:i+2])[0]
|
||||
|
||||
i += 1
|
||||
self.server_language = byte2int(data[i:i+1])
|
||||
self.server_charset = charset_by_id(self.server_language).name
|
||||
|
||||
i += 16
|
||||
if len(data) >= i+12-1:
|
||||
rest_salt = data[i:i+12]
|
||||
self.salt += rest_salt
|
||||
|
||||
def get_server_info(self):
|
||||
return self.server_version
|
||||
|
||||
Warning = Warning
|
||||
Error = Error
|
||||
InterfaceError = InterfaceError
|
||||
DatabaseError = DatabaseError
|
||||
DataError = DataError
|
||||
OperationalError = OperationalError
|
||||
IntegrityError = IntegrityError
|
||||
InternalError = InternalError
|
||||
ProgrammingError = ProgrammingError
|
||||
NotSupportedError = NotSupportedError
|
||||
|
||||
# TODO: move OK and EOF packet parsing/logic into a proper subclass
|
||||
# of MysqlPacket like has been done with FieldDescriptorPacket.
|
||||
class MySQLResult(object):
|
||||
|
||||
def __init__(self, connection):
|
||||
from weakref import proxy
|
||||
self.connection = proxy(connection)
|
||||
self.affected_rows = None
|
||||
self.insert_id = None
|
||||
self.server_status = 0
|
||||
self.warning_count = 0
|
||||
self.message = None
|
||||
self.field_count = 0
|
||||
self.description = None
|
||||
self.rows = None
|
||||
self.has_next = None
|
||||
|
||||
def read(self):
|
||||
self.first_packet = self.connection.read_packet()
|
||||
|
||||
# TODO: use classes for different packet types?
|
||||
if self.first_packet.is_ok_packet():
|
||||
self._read_ok_packet()
|
||||
else:
|
||||
self._read_result_packet()
|
||||
|
||||
def _read_ok_packet(self):
|
||||
self.first_packet.advance(1) # field_count (always '0')
|
||||
self.affected_rows = self.first_packet.read_length_coded_binary()
|
||||
self.insert_id = self.first_packet.read_length_coded_binary()
|
||||
self.server_status = struct.unpack('<H', self.first_packet.read(2))[0]
|
||||
self.warning_count = struct.unpack('<H', self.first_packet.read(2))[0]
|
||||
self.message = self.first_packet.read_all()
|
||||
|
||||
def _read_result_packet(self):
|
||||
self.field_count = byte2int(self.first_packet.read(1))
|
||||
self._get_descriptions()
|
||||
self._read_rowdata_packet()
|
||||
|
||||
# TODO: implement this as an iteratable so that it is more
|
||||
# memory efficient and lower-latency to client...
|
||||
def _read_rowdata_packet(self):
|
||||
"""Read a rowdata packet for each data row in the result set."""
|
||||
rows = []
|
||||
while True:
|
||||
packet = self.connection.read_packet()
|
||||
if packet.is_eof_packet():
|
||||
self.warning_count = packet.read(2)
|
||||
server_status = struct.unpack('<h', packet.read(2))[0]
|
||||
self.has_next = (server_status
|
||||
& SERVER_STATUS.SERVER_MORE_RESULTS_EXISTS)
|
||||
break
|
||||
|
||||
row = []
|
||||
for field in self.fields:
|
||||
if field.type_code in self.connection.decoders:
|
||||
converter = self.connection.decoders[field.type_code]
|
||||
|
||||
if DEBUG: print "DEBUG: field=%s, converter=%s" % (field, converter)
|
||||
data = packet.read_length_coded_string()
|
||||
converted = None
|
||||
if data != None:
|
||||
converted = converter(self.connection, field, data)
|
||||
|
||||
row.append(converted)
|
||||
|
||||
rows.append(tuple(row))
|
||||
|
||||
self.affected_rows = len(rows)
|
||||
self.rows = tuple(rows)
|
||||
if DEBUG: self.rows
|
||||
|
||||
def _get_descriptions(self):
|
||||
"""Read a column descriptor packet for each column in the result."""
|
||||
self.fields = []
|
||||
description = []
|
||||
for i in xrange(self.field_count):
|
||||
field = self.connection.read_packet(FieldDescriptorPacket)
|
||||
self.fields.append(field)
|
||||
description.append(field.description())
|
||||
|
||||
eof_packet = self.connection.read_packet()
|
||||
assert eof_packet.is_eof_packet(), 'Protocol error, expecting EOF'
|
||||
self.description = tuple(description)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
LONG_PASSWORD = 1
|
||||
FOUND_ROWS = 1 << 1
|
||||
LONG_FLAG = 1 << 2
|
||||
CONNECT_WITH_DB = 1 << 3
|
||||
NO_SCHEMA = 1 << 4
|
||||
COMPRESS = 1 << 5
|
||||
ODBC = 1 << 6
|
||||
LOCAL_FILES = 1 << 7
|
||||
IGNORE_SPACE = 1 << 8
|
||||
PROTOCOL_41 = 1 << 9
|
||||
INTERACTIVE = 1 << 10
|
||||
SSL = 1 << 11
|
||||
IGNORE_SIGPIPE = 1 << 12
|
||||
TRANSACTIONS = 1 << 13
|
||||
SECURE_CONNECTION = 1 << 15
|
||||
MULTI_STATEMENTS = 1 << 16
|
||||
MULTI_RESULTS = 1 << 17
|
||||
CAPABILITIES = LONG_PASSWORD|LONG_FLAG|TRANSACTIONS| \
|
||||
PROTOCOL_41|SECURE_CONNECTION
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
COM_SLEEP = 0x00
|
||||
COM_QUIT = 0x01
|
||||
COM_INIT_DB = 0x02
|
||||
COM_QUERY = 0x03
|
||||
COM_FIELD_LIST = 0x04
|
||||
COM_CREATE_DB = 0x05
|
||||
COM_DROP_DB = 0x06
|
||||
COM_REFRESH = 0x07
|
||||
COM_SHUTDOWN = 0x08
|
||||
COM_STATISTICS = 0x09
|
||||
COM_PROCESS_INFO = 0x0a
|
||||
COM_CONNECT = 0x0b
|
||||
COM_PROCESS_KILL = 0x0c
|
||||
COM_DEBUG = 0x0d
|
||||
COM_PING = 0x0e
|
||||
COM_TIME = 0x0f
|
||||
COM_DELAYED_INSERT = 0x10
|
||||
COM_CHANGE_USER = 0x11
|
||||
COM_BINLOG_DUMP = 0x12
|
||||
COM_TABLE_DUMP = 0x13
|
||||
COM_CONNECT_OUT = 0x14
|
||||
COM_REGISTER_SLAVE = 0x15
|
||||
@@ -0,0 +1,472 @@
|
||||
|
||||
ERROR_FIRST = 1000
|
||||
HASHCHK = 1000
|
||||
NISAMCHK = 1001
|
||||
NO = 1002
|
||||
YES = 1003
|
||||
CANT_CREATE_FILE = 1004
|
||||
CANT_CREATE_TABLE = 1005
|
||||
CANT_CREATE_DB = 1006
|
||||
DB_CREATE_EXISTS = 1007
|
||||
DB_DROP_EXISTS = 1008
|
||||
DB_DROP_DELETE = 1009
|
||||
DB_DROP_RMDIR = 1010
|
||||
CANT_DELETE_FILE = 1011
|
||||
CANT_FIND_SYSTEM_REC = 1012
|
||||
CANT_GET_STAT = 1013
|
||||
CANT_GET_WD = 1014
|
||||
CANT_LOCK = 1015
|
||||
CANT_OPEN_FILE = 1016
|
||||
FILE_NOT_FOUND = 1017
|
||||
CANT_READ_DIR = 1018
|
||||
CANT_SET_WD = 1019
|
||||
CHECKREAD = 1020
|
||||
DISK_FULL = 1021
|
||||
DUP_KEY = 1022
|
||||
ERROR_ON_CLOSE = 1023
|
||||
ERROR_ON_READ = 1024
|
||||
ERROR_ON_RENAME = 1025
|
||||
ERROR_ON_WRITE = 1026
|
||||
FILE_USED = 1027
|
||||
FILSORT_ABORT = 1028
|
||||
FORM_NOT_FOUND = 1029
|
||||
GET_ERRNO = 1030
|
||||
ILLEGAL_HA = 1031
|
||||
KEY_NOT_FOUND = 1032
|
||||
NOT_FORM_FILE = 1033
|
||||
NOT_KEYFILE = 1034
|
||||
OLD_KEYFILE = 1035
|
||||
OPEN_AS_READONLY = 1036
|
||||
OUTOFMEMORY = 1037
|
||||
OUT_OF_SORTMEMORY = 1038
|
||||
UNEXPECTED_EOF = 1039
|
||||
CON_COUNT_ERROR = 1040
|
||||
OUT_OF_RESOURCES = 1041
|
||||
BAD_HOST_ERROR = 1042
|
||||
HANDSHAKE_ERROR = 1043
|
||||
DBACCESS_DENIED_ERROR = 1044
|
||||
ACCESS_DENIED_ERROR = 1045
|
||||
NO_DB_ERROR = 1046
|
||||
UNKNOWN_COM_ERROR = 1047
|
||||
BAD_NULL_ERROR = 1048
|
||||
BAD_DB_ERROR = 1049
|
||||
TABLE_EXISTS_ERROR = 1050
|
||||
BAD_TABLE_ERROR = 1051
|
||||
NON_UNIQ_ERROR = 1052
|
||||
SERVER_SHUTDOWN = 1053
|
||||
BAD_FIELD_ERROR = 1054
|
||||
WRONG_FIELD_WITH_GROUP = 1055
|
||||
WRONG_GROUP_FIELD = 1056
|
||||
WRONG_SUM_SELECT = 1057
|
||||
WRONG_VALUE_COUNT = 1058
|
||||
TOO_LONG_IDENT = 1059
|
||||
DUP_FIELDNAME = 1060
|
||||
DUP_KEYNAME = 1061
|
||||
DUP_ENTRY = 1062
|
||||
WRONG_FIELD_SPEC = 1063
|
||||
PARSE_ERROR = 1064
|
||||
EMPTY_QUERY = 1065
|
||||
NONUNIQ_TABLE = 1066
|
||||
INVALID_DEFAULT = 1067
|
||||
MULTIPLE_PRI_KEY = 1068
|
||||
TOO_MANY_KEYS = 1069
|
||||
TOO_MANY_KEY_PARTS = 1070
|
||||
TOO_LONG_KEY = 1071
|
||||
KEY_COLUMN_DOES_NOT_EXITS = 1072
|
||||
BLOB_USED_AS_KEY = 1073
|
||||
TOO_BIG_FIELDLENGTH = 1074
|
||||
WRONG_AUTO_KEY = 1075
|
||||
READY = 1076
|
||||
NORMAL_SHUTDOWN = 1077
|
||||
GOT_SIGNAL = 1078
|
||||
SHUTDOWN_COMPLETE = 1079
|
||||
FORCING_CLOSE = 1080
|
||||
IPSOCK_ERROR = 1081
|
||||
NO_SUCH_INDEX = 1082
|
||||
WRONG_FIELD_TERMINATORS = 1083
|
||||
BLOBS_AND_NO_TERMINATED = 1084
|
||||
TEXTFILE_NOT_READABLE = 1085
|
||||
FILE_EXISTS_ERROR = 1086
|
||||
LOAD_INFO = 1087
|
||||
ALTER_INFO = 1088
|
||||
WRONG_SUB_KEY = 1089
|
||||
CANT_REMOVE_ALL_FIELDS = 1090
|
||||
CANT_DROP_FIELD_OR_KEY = 1091
|
||||
INSERT_INFO = 1092
|
||||
UPDATE_TABLE_USED = 1093
|
||||
NO_SUCH_THREAD = 1094
|
||||
KILL_DENIED_ERROR = 1095
|
||||
NO_TABLES_USED = 1096
|
||||
TOO_BIG_SET = 1097
|
||||
NO_UNIQUE_LOGFILE = 1098
|
||||
TABLE_NOT_LOCKED_FOR_WRITE = 1099
|
||||
TABLE_NOT_LOCKED = 1100
|
||||
BLOB_CANT_HAVE_DEFAULT = 1101
|
||||
WRONG_DB_NAME = 1102
|
||||
WRONG_TABLE_NAME = 1103
|
||||
TOO_BIG_SELECT = 1104
|
||||
UNKNOWN_ERROR = 1105
|
||||
UNKNOWN_PROCEDURE = 1106
|
||||
WRONG_PARAMCOUNT_TO_PROCEDURE = 1107
|
||||
WRONG_PARAMETERS_TO_PROCEDURE = 1108
|
||||
UNKNOWN_TABLE = 1109
|
||||
FIELD_SPECIFIED_TWICE = 1110
|
||||
INVALID_GROUP_FUNC_USE = 1111
|
||||
UNSUPPORTED_EXTENSION = 1112
|
||||
TABLE_MUST_HAVE_COLUMNS = 1113
|
||||
RECORD_FILE_FULL = 1114
|
||||
UNKNOWN_CHARACTER_SET = 1115
|
||||
TOO_MANY_TABLES = 1116
|
||||
TOO_MANY_FIELDS = 1117
|
||||
TOO_BIG_ROWSIZE = 1118
|
||||
STACK_OVERRUN = 1119
|
||||
WRONG_OUTER_JOIN = 1120
|
||||
NULL_COLUMN_IN_INDEX = 1121
|
||||
CANT_FIND_UDF = 1122
|
||||
CANT_INITIALIZE_UDF = 1123
|
||||
UDF_NO_PATHS = 1124
|
||||
UDF_EXISTS = 1125
|
||||
CANT_OPEN_LIBRARY = 1126
|
||||
CANT_FIND_DL_ENTRY = 1127
|
||||
FUNCTION_NOT_DEFINED = 1128
|
||||
HOST_IS_BLOCKED = 1129
|
||||
HOST_NOT_PRIVILEGED = 1130
|
||||
PASSWORD_ANONYMOUS_USER = 1131
|
||||
PASSWORD_NOT_ALLOWED = 1132
|
||||
PASSWORD_NO_MATCH = 1133
|
||||
UPDATE_INFO = 1134
|
||||
CANT_CREATE_THREAD = 1135
|
||||
WRONG_VALUE_COUNT_ON_ROW = 1136
|
||||
CANT_REOPEN_TABLE = 1137
|
||||
INVALID_USE_OF_NULL = 1138
|
||||
REGEXP_ERROR = 1139
|
||||
MIX_OF_GROUP_FUNC_AND_FIELDS = 1140
|
||||
NONEXISTING_GRANT = 1141
|
||||
TABLEACCESS_DENIED_ERROR = 1142
|
||||
COLUMNACCESS_DENIED_ERROR = 1143
|
||||
ILLEGAL_GRANT_FOR_TABLE = 1144
|
||||
GRANT_WRONG_HOST_OR_USER = 1145
|
||||
NO_SUCH_TABLE = 1146
|
||||
NONEXISTING_TABLE_GRANT = 1147
|
||||
NOT_ALLOWED_COMMAND = 1148
|
||||
SYNTAX_ERROR = 1149
|
||||
DELAYED_CANT_CHANGE_LOCK = 1150
|
||||
TOO_MANY_DELAYED_THREADS = 1151
|
||||
ABORTING_CONNECTION = 1152
|
||||
NET_PACKET_TOO_LARGE = 1153
|
||||
NET_READ_ERROR_FROM_PIPE = 1154
|
||||
NET_FCNTL_ERROR = 1155
|
||||
NET_PACKETS_OUT_OF_ORDER = 1156
|
||||
NET_UNCOMPRESS_ERROR = 1157
|
||||
NET_READ_ERROR = 1158
|
||||
NET_READ_INTERRUPTED = 1159
|
||||
NET_ERROR_ON_WRITE = 1160
|
||||
NET_WRITE_INTERRUPTED = 1161
|
||||
TOO_LONG_STRING = 1162
|
||||
TABLE_CANT_HANDLE_BLOB = 1163
|
||||
TABLE_CANT_HANDLE_AUTO_INCREMENT = 1164
|
||||
DELAYED_INSERT_TABLE_LOCKED = 1165
|
||||
WRONG_COLUMN_NAME = 1166
|
||||
WRONG_KEY_COLUMN = 1167
|
||||
WRONG_MRG_TABLE = 1168
|
||||
DUP_UNIQUE = 1169
|
||||
BLOB_KEY_WITHOUT_LENGTH = 1170
|
||||
PRIMARY_CANT_HAVE_NULL = 1171
|
||||
TOO_MANY_ROWS = 1172
|
||||
REQUIRES_PRIMARY_KEY = 1173
|
||||
NO_RAID_COMPILED = 1174
|
||||
UPDATE_WITHOUT_KEY_IN_SAFE_MODE = 1175
|
||||
KEY_DOES_NOT_EXITS = 1176
|
||||
CHECK_NO_SUCH_TABLE = 1177
|
||||
CHECK_NOT_IMPLEMENTED = 1178
|
||||
CANT_DO_THIS_DURING_AN_TRANSACTION = 1179
|
||||
ERROR_DURING_COMMIT = 1180
|
||||
ERROR_DURING_ROLLBACK = 1181
|
||||
ERROR_DURING_FLUSH_LOGS = 1182
|
||||
ERROR_DURING_CHECKPOINT = 1183
|
||||
NEW_ABORTING_CONNECTION = 1184
|
||||
DUMP_NOT_IMPLEMENTED = 1185
|
||||
FLUSH_MASTER_BINLOG_CLOSED = 1186
|
||||
INDEX_REBUILD = 1187
|
||||
MASTER = 1188
|
||||
MASTER_NET_READ = 1189
|
||||
MASTER_NET_WRITE = 1190
|
||||
FT_MATCHING_KEY_NOT_FOUND = 1191
|
||||
LOCK_OR_ACTIVE_TRANSACTION = 1192
|
||||
UNKNOWN_SYSTEM_VARIABLE = 1193
|
||||
CRASHED_ON_USAGE = 1194
|
||||
CRASHED_ON_REPAIR = 1195
|
||||
WARNING_NOT_COMPLETE_ROLLBACK = 1196
|
||||
TRANS_CACHE_FULL = 1197
|
||||
SLAVE_MUST_STOP = 1198
|
||||
SLAVE_NOT_RUNNING = 1199
|
||||
BAD_SLAVE = 1200
|
||||
MASTER_INFO = 1201
|
||||
SLAVE_THREAD = 1202
|
||||
TOO_MANY_USER_CONNECTIONS = 1203
|
||||
SET_CONSTANTS_ONLY = 1204
|
||||
LOCK_WAIT_TIMEOUT = 1205
|
||||
LOCK_TABLE_FULL = 1206
|
||||
READ_ONLY_TRANSACTION = 1207
|
||||
DROP_DB_WITH_READ_LOCK = 1208
|
||||
CREATE_DB_WITH_READ_LOCK = 1209
|
||||
WRONG_ARGUMENTS = 1210
|
||||
NO_PERMISSION_TO_CREATE_USER = 1211
|
||||
UNION_TABLES_IN_DIFFERENT_DIR = 1212
|
||||
LOCK_DEADLOCK = 1213
|
||||
TABLE_CANT_HANDLE_FT = 1214
|
||||
CANNOT_ADD_FOREIGN = 1215
|
||||
NO_REFERENCED_ROW = 1216
|
||||
ROW_IS_REFERENCED = 1217
|
||||
CONNECT_TO_MASTER = 1218
|
||||
QUERY_ON_MASTER = 1219
|
||||
ERROR_WHEN_EXECUTING_COMMAND = 1220
|
||||
WRONG_USAGE = 1221
|
||||
WRONG_NUMBER_OF_COLUMNS_IN_SELECT = 1222
|
||||
CANT_UPDATE_WITH_READLOCK = 1223
|
||||
MIXING_NOT_ALLOWED = 1224
|
||||
DUP_ARGUMENT = 1225
|
||||
USER_LIMIT_REACHED = 1226
|
||||
SPECIFIC_ACCESS_DENIED_ERROR = 1227
|
||||
LOCAL_VARIABLE = 1228
|
||||
GLOBAL_VARIABLE = 1229
|
||||
NO_DEFAULT = 1230
|
||||
WRONG_VALUE_FOR_VAR = 1231
|
||||
WRONG_TYPE_FOR_VAR = 1232
|
||||
VAR_CANT_BE_READ = 1233
|
||||
CANT_USE_OPTION_HERE = 1234
|
||||
NOT_SUPPORTED_YET = 1235
|
||||
MASTER_FATAL_ERROR_READING_BINLOG = 1236
|
||||
SLAVE_IGNORED_TABLE = 1237
|
||||
INCORRECT_GLOBAL_LOCAL_VAR = 1238
|
||||
WRONG_FK_DEF = 1239
|
||||
KEY_REF_DO_NOT_MATCH_TABLE_REF = 1240
|
||||
OPERAND_COLUMNS = 1241
|
||||
SUBQUERY_NO_1_ROW = 1242
|
||||
UNKNOWN_STMT_HANDLER = 1243
|
||||
CORRUPT_HELP_DB = 1244
|
||||
CYCLIC_REFERENCE = 1245
|
||||
AUTO_CONVERT = 1246
|
||||
ILLEGAL_REFERENCE = 1247
|
||||
DERIVED_MUST_HAVE_ALIAS = 1248
|
||||
SELECT_REDUCED = 1249
|
||||
TABLENAME_NOT_ALLOWED_HERE = 1250
|
||||
NOT_SUPPORTED_AUTH_MODE = 1251
|
||||
SPATIAL_CANT_HAVE_NULL = 1252
|
||||
COLLATION_CHARSET_MISMATCH = 1253
|
||||
SLAVE_WAS_RUNNING = 1254
|
||||
SLAVE_WAS_NOT_RUNNING = 1255
|
||||
TOO_BIG_FOR_UNCOMPRESS = 1256
|
||||
ZLIB_Z_MEM_ERROR = 1257
|
||||
ZLIB_Z_BUF_ERROR = 1258
|
||||
ZLIB_Z_DATA_ERROR = 1259
|
||||
CUT_VALUE_GROUP_CONCAT = 1260
|
||||
WARN_TOO_FEW_RECORDS = 1261
|
||||
WARN_TOO_MANY_RECORDS = 1262
|
||||
WARN_NULL_TO_NOTNULL = 1263
|
||||
WARN_DATA_OUT_OF_RANGE = 1264
|
||||
WARN_DATA_TRUNCATED = 1265
|
||||
WARN_USING_OTHER_HANDLER = 1266
|
||||
CANT_AGGREGATE_2COLLATIONS = 1267
|
||||
DROP_USER = 1268
|
||||
REVOKE_GRANTS = 1269
|
||||
CANT_AGGREGATE_3COLLATIONS = 1270
|
||||
CANT_AGGREGATE_NCOLLATIONS = 1271
|
||||
VARIABLE_IS_NOT_STRUCT = 1272
|
||||
UNKNOWN_COLLATION = 1273
|
||||
SLAVE_IGNORED_SSL_PARAMS = 1274
|
||||
SERVER_IS_IN_SECURE_AUTH_MODE = 1275
|
||||
WARN_FIELD_RESOLVED = 1276
|
||||
BAD_SLAVE_UNTIL_COND = 1277
|
||||
MISSING_SKIP_SLAVE = 1278
|
||||
UNTIL_COND_IGNORED = 1279
|
||||
WRONG_NAME_FOR_INDEX = 1280
|
||||
WRONG_NAME_FOR_CATALOG = 1281
|
||||
WARN_QC_RESIZE = 1282
|
||||
BAD_FT_COLUMN = 1283
|
||||
UNKNOWN_KEY_CACHE = 1284
|
||||
WARN_HOSTNAME_WONT_WORK = 1285
|
||||
UNKNOWN_STORAGE_ENGINE = 1286
|
||||
WARN_DEPRECATED_SYNTAX = 1287
|
||||
NON_UPDATABLE_TABLE = 1288
|
||||
FEATURE_DISABLED = 1289
|
||||
OPTION_PREVENTS_STATEMENT = 1290
|
||||
DUPLICATED_VALUE_IN_TYPE = 1291
|
||||
TRUNCATED_WRONG_VALUE = 1292
|
||||
TOO_MUCH_AUTO_TIMESTAMP_COLS = 1293
|
||||
INVALID_ON_UPDATE = 1294
|
||||
UNSUPPORTED_PS = 1295
|
||||
GET_ERRMSG = 1296
|
||||
GET_TEMPORARY_ERRMSG = 1297
|
||||
UNKNOWN_TIME_ZONE = 1298
|
||||
WARN_INVALID_TIMESTAMP = 1299
|
||||
INVALID_CHARACTER_STRING = 1300
|
||||
WARN_ALLOWED_PACKET_OVERFLOWED = 1301
|
||||
CONFLICTING_DECLARATIONS = 1302
|
||||
SP_NO_RECURSIVE_CREATE = 1303
|
||||
SP_ALREADY_EXISTS = 1304
|
||||
SP_DOES_NOT_EXIST = 1305
|
||||
SP_DROP_FAILED = 1306
|
||||
SP_STORE_FAILED = 1307
|
||||
SP_LILABEL_MISMATCH = 1308
|
||||
SP_LABEL_REDEFINE = 1309
|
||||
SP_LABEL_MISMATCH = 1310
|
||||
SP_UNINIT_VAR = 1311
|
||||
SP_BADSELECT = 1312
|
||||
SP_BADRETURN = 1313
|
||||
SP_BADSTATEMENT = 1314
|
||||
UPDATE_LOG_DEPRECATED_IGNORED = 1315
|
||||
UPDATE_LOG_DEPRECATED_TRANSLATED = 1316
|
||||
QUERY_INTERRUPTED = 1317
|
||||
SP_WRONG_NO_OF_ARGS = 1318
|
||||
SP_COND_MISMATCH = 1319
|
||||
SP_NORETURN = 1320
|
||||
SP_NORETURNEND = 1321
|
||||
SP_BAD_CURSOR_QUERY = 1322
|
||||
SP_BAD_CURSOR_SELECT = 1323
|
||||
SP_CURSOR_MISMATCH = 1324
|
||||
SP_CURSOR_ALREADY_OPEN = 1325
|
||||
SP_CURSOR_NOT_OPEN = 1326
|
||||
SP_UNDECLARED_VAR = 1327
|
||||
SP_WRONG_NO_OF_FETCH_ARGS = 1328
|
||||
SP_FETCH_NO_DATA = 1329
|
||||
SP_DUP_PARAM = 1330
|
||||
SP_DUP_VAR = 1331
|
||||
SP_DUP_COND = 1332
|
||||
SP_DUP_CURS = 1333
|
||||
SP_CANT_ALTER = 1334
|
||||
SP_SUBSELECT_NYI = 1335
|
||||
STMT_NOT_ALLOWED_IN_SF_OR_TRG = 1336
|
||||
SP_VARCOND_AFTER_CURSHNDLR = 1337
|
||||
SP_CURSOR_AFTER_HANDLER = 1338
|
||||
SP_CASE_NOT_FOUND = 1339
|
||||
FPARSER_TOO_BIG_FILE = 1340
|
||||
FPARSER_BAD_HEADER = 1341
|
||||
FPARSER_EOF_IN_COMMENT = 1342
|
||||
FPARSER_ERROR_IN_PARAMETER = 1343
|
||||
FPARSER_EOF_IN_UNKNOWN_PARAMETER = 1344
|
||||
VIEW_NO_EXPLAIN = 1345
|
||||
FRM_UNKNOWN_TYPE = 1346
|
||||
WRONG_OBJECT = 1347
|
||||
NONUPDATEABLE_COLUMN = 1348
|
||||
VIEW_SELECT_DERIVED = 1349
|
||||
VIEW_SELECT_CLAUSE = 1350
|
||||
VIEW_SELECT_VARIABLE = 1351
|
||||
VIEW_SELECT_TMPTABLE = 1352
|
||||
VIEW_WRONG_LIST = 1353
|
||||
WARN_VIEW_MERGE = 1354
|
||||
WARN_VIEW_WITHOUT_KEY = 1355
|
||||
VIEW_INVALID = 1356
|
||||
SP_NO_DROP_SP = 1357
|
||||
SP_GOTO_IN_HNDLR = 1358
|
||||
TRG_ALREADY_EXISTS = 1359
|
||||
TRG_DOES_NOT_EXIST = 1360
|
||||
TRG_ON_VIEW_OR_TEMP_TABLE = 1361
|
||||
TRG_CANT_CHANGE_ROW = 1362
|
||||
TRG_NO_SUCH_ROW_IN_TRG = 1363
|
||||
NO_DEFAULT_FOR_FIELD = 1364
|
||||
DIVISION_BY_ZERO = 1365
|
||||
TRUNCATED_WRONG_VALUE_FOR_FIELD = 1366
|
||||
ILLEGAL_VALUE_FOR_TYPE = 1367
|
||||
VIEW_NONUPD_CHECK = 1368
|
||||
VIEW_CHECK_FAILED = 1369
|
||||
PROCACCESS_DENIED_ERROR = 1370
|
||||
RELAY_LOG_FAIL = 1371
|
||||
PASSWD_LENGTH = 1372
|
||||
UNKNOWN_TARGET_BINLOG = 1373
|
||||
IO_ERR_LOG_INDEX_READ = 1374
|
||||
BINLOG_PURGE_PROHIBITED = 1375
|
||||
FSEEK_FAIL = 1376
|
||||
BINLOG_PURGE_FATAL_ERR = 1377
|
||||
LOG_IN_USE = 1378
|
||||
LOG_PURGE_UNKNOWN_ERR = 1379
|
||||
RELAY_LOG_INIT = 1380
|
||||
NO_BINARY_LOGGING = 1381
|
||||
RESERVED_SYNTAX = 1382
|
||||
WSAS_FAILED = 1383
|
||||
DIFF_GROUPS_PROC = 1384
|
||||
NO_GROUP_FOR_PROC = 1385
|
||||
ORDER_WITH_PROC = 1386
|
||||
LOGGING_PROHIBIT_CHANGING_OF = 1387
|
||||
NO_FILE_MAPPING = 1388
|
||||
WRONG_MAGIC = 1389
|
||||
PS_MANY_PARAM = 1390
|
||||
KEY_PART_0 = 1391
|
||||
VIEW_CHECKSUM = 1392
|
||||
VIEW_MULTIUPDATE = 1393
|
||||
VIEW_NO_INSERT_FIELD_LIST = 1394
|
||||
VIEW_DELETE_MERGE_VIEW = 1395
|
||||
CANNOT_USER = 1396
|
||||
XAER_NOTA = 1397
|
||||
XAER_INVAL = 1398
|
||||
XAER_RMFAIL = 1399
|
||||
XAER_OUTSIDE = 1400
|
||||
XAER_RMERR = 1401
|
||||
XA_RBROLLBACK = 1402
|
||||
NONEXISTING_PROC_GRANT = 1403
|
||||
PROC_AUTO_GRANT_FAIL = 1404
|
||||
PROC_AUTO_REVOKE_FAIL = 1405
|
||||
DATA_TOO_LONG = 1406
|
||||
SP_BAD_SQLSTATE = 1407
|
||||
STARTUP = 1408
|
||||
LOAD_FROM_FIXED_SIZE_ROWS_TO_VAR = 1409
|
||||
CANT_CREATE_USER_WITH_GRANT = 1410
|
||||
WRONG_VALUE_FOR_TYPE = 1411
|
||||
TABLE_DEF_CHANGED = 1412
|
||||
SP_DUP_HANDLER = 1413
|
||||
SP_NOT_VAR_ARG = 1414
|
||||
SP_NO_RETSET = 1415
|
||||
CANT_CREATE_GEOMETRY_OBJECT = 1416
|
||||
FAILED_ROUTINE_BREAK_BINLOG = 1417
|
||||
BINLOG_UNSAFE_ROUTINE = 1418
|
||||
BINLOG_CREATE_ROUTINE_NEED_SUPER = 1419
|
||||
EXEC_STMT_WITH_OPEN_CURSOR = 1420
|
||||
STMT_HAS_NO_OPEN_CURSOR = 1421
|
||||
COMMIT_NOT_ALLOWED_IN_SF_OR_TRG = 1422
|
||||
NO_DEFAULT_FOR_VIEW_FIELD = 1423
|
||||
SP_NO_RECURSION = 1424
|
||||
TOO_BIG_SCALE = 1425
|
||||
TOO_BIG_PRECISION = 1426
|
||||
M_BIGGER_THAN_D = 1427
|
||||
WRONG_LOCK_OF_SYSTEM_TABLE = 1428
|
||||
CONNECT_TO_FOREIGN_DATA_SOURCE = 1429
|
||||
QUERY_ON_FOREIGN_DATA_SOURCE = 1430
|
||||
FOREIGN_DATA_SOURCE_DOESNT_EXIST = 1431
|
||||
FOREIGN_DATA_STRING_INVALID_CANT_CREATE = 1432
|
||||
FOREIGN_DATA_STRING_INVALID = 1433
|
||||
CANT_CREATE_FEDERATED_TABLE = 1434
|
||||
TRG_IN_WRONG_SCHEMA = 1435
|
||||
STACK_OVERRUN_NEED_MORE = 1436
|
||||
TOO_LONG_BODY = 1437
|
||||
WARN_CANT_DROP_DEFAULT_KEYCACHE = 1438
|
||||
TOO_BIG_DISPLAYWIDTH = 1439
|
||||
XAER_DUPID = 1440
|
||||
DATETIME_FUNCTION_OVERFLOW = 1441
|
||||
CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG = 1442
|
||||
VIEW_PREVENT_UPDATE = 1443
|
||||
PS_NO_RECURSION = 1444
|
||||
SP_CANT_SET_AUTOCOMMIT = 1445
|
||||
MALFORMED_DEFINER = 1446
|
||||
VIEW_FRM_NO_USER = 1447
|
||||
VIEW_OTHER_USER = 1448
|
||||
NO_SUCH_USER = 1449
|
||||
FORBID_SCHEMA_CHANGE = 1450
|
||||
ROW_IS_REFERENCED_2 = 1451
|
||||
NO_REFERENCED_ROW_2 = 1452
|
||||
SP_BAD_VAR_SHADOW = 1453
|
||||
TRG_NO_DEFINER = 1454
|
||||
OLD_FILE_FORMAT = 1455
|
||||
SP_RECURSION_LIMIT = 1456
|
||||
SP_PROC_TABLE_CORRUPT = 1457
|
||||
SP_WRONG_NAME = 1458
|
||||
TABLE_NEEDS_UPGRADE = 1459
|
||||
SP_NO_AGGREGATE = 1460
|
||||
MAX_PREPARED_STMT_COUNT_REACHED = 1461
|
||||
VIEW_RECURSIVE = 1462
|
||||
NON_GROUPING_FIELD_USED = 1463
|
||||
TABLE_CANT_HANDLE_SPKEYS = 1464
|
||||
NO_TRIGGERS_ON_SYSTEM_SCHEMA = 1465
|
||||
USERNAME = 1466
|
||||
HOSTNAME = 1467
|
||||
WRONG_STRING_LENGTH = 1468
|
||||
ERROR_LAST = 1468
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
|
||||
DECIMAL = 0
|
||||
TINY = 1
|
||||
SHORT = 2
|
||||
LONG = 3
|
||||
FLOAT = 4
|
||||
DOUBLE = 5
|
||||
NULL = 6
|
||||
TIMESTAMP = 7
|
||||
LONGLONG = 8
|
||||
INT24 = 9
|
||||
DATE = 10
|
||||
TIME = 11
|
||||
DATETIME = 12
|
||||
YEAR = 13
|
||||
NEWDATE = 14
|
||||
VARCHAR = 15
|
||||
BIT = 16
|
||||
NEWDECIMAL = 246
|
||||
ENUM = 247
|
||||
SET = 248
|
||||
TINY_BLOB = 249
|
||||
MEDIUM_BLOB = 250
|
||||
LONG_BLOB = 251
|
||||
BLOB = 252
|
||||
VAR_STRING = 253
|
||||
STRING = 254
|
||||
GEOMETRY = 255
|
||||
|
||||
CHAR = TINY
|
||||
INTERVAL = ENUM
|
||||
@@ -0,0 +1,15 @@
|
||||
NOT_NULL = 1
|
||||
PRI_KEY = 2
|
||||
UNIQUE_KEY = 4
|
||||
MULTIPLE_KEY = 8
|
||||
BLOB = 16
|
||||
UNSIGNED = 32
|
||||
ZEROFILL = 64
|
||||
BINARY = 128
|
||||
ENUM = 256
|
||||
AUTO_INCREMENT = 512
|
||||
TIMESTAMP = 1024
|
||||
SET = 2048
|
||||
PART_KEY = 16384
|
||||
GROUP = 32767
|
||||
UNIQUE = 65536
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
SERVER_STATUS_IN_TRANS = 1
|
||||
SERVER_STATUS_AUTOCOMMIT = 2
|
||||
SERVER_MORE_RESULTS_EXISTS = 8
|
||||
SERVER_QUERY_NO_GOOD_INDEX_USED = 16
|
||||
SERVER_QUERY_NO_INDEX_USED = 32
|
||||
SERVER_STATUS_CURSOR_EXISTS = 64
|
||||
SERVER_STATUS_LAST_ROW_SENT = 128
|
||||
SERVER_STATUS_DB_DROPPED = 256
|
||||
SERVER_STATUS_NO_BACKSLASH_ESCAPES = 512
|
||||
SERVER_STATUS_METADATA_CHANGED = 1024
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
import re
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from constants import FIELD_TYPE, FLAG
|
||||
from charset import charset_by_id
|
||||
|
||||
try:
|
||||
set
|
||||
except NameError:
|
||||
try:
|
||||
from sets import BaseSet as set
|
||||
except ImportError:
|
||||
from sets import Set as set
|
||||
|
||||
ESCAPE_REGEX = re.compile(r"[\0\n\r\032\'\"\\]")
|
||||
ESCAPE_MAP = {'\0': '\\0', '\n': '\\n', '\r': '\\r', '\032': '\\Z',
|
||||
'\'': '\\\'', '"': '\\"', '\\': '\\\\'}
|
||||
|
||||
def escape_item(val, charset):
|
||||
if type(val) in [tuple, list, set]:
|
||||
return escape_sequence(val, charset)
|
||||
if type(val) is dict:
|
||||
return escape_dict(val, charset)
|
||||
if hasattr(val, "decode") and not isinstance(val, unicode):
|
||||
# deal with py3k bytes
|
||||
val = val.decode(charset)
|
||||
encoder = encoders[type(val)]
|
||||
val = encoder(val)
|
||||
if type(val) is str:
|
||||
return val
|
||||
val = val.encode(charset)
|
||||
return val
|
||||
|
||||
def escape_dict(val, charset):
|
||||
n = {}
|
||||
for k, v in val.items():
|
||||
quoted = escape_item(v, charset)
|
||||
n[k] = quoted
|
||||
return n
|
||||
|
||||
def escape_sequence(val, charset):
|
||||
n = []
|
||||
for item in val:
|
||||
quoted = escape_item(item, charset)
|
||||
n.append(quoted)
|
||||
return tuple(n)
|
||||
|
||||
def escape_set(val, charset):
|
||||
val = map(lambda x: escape_item(x, charset), val)
|
||||
return ','.join(val)
|
||||
|
||||
def escape_bool(value):
|
||||
return str(int(value))
|
||||
|
||||
def escape_object(value):
|
||||
return str(value)
|
||||
|
||||
escape_int = escape_long = escape_object
|
||||
|
||||
def escape_float(value):
|
||||
return ('%.15g' % value)
|
||||
|
||||
def escape_string(value):
|
||||
return ("'%s'" % ESCAPE_REGEX.sub(
|
||||
lambda match: ESCAPE_MAP.get(match.group(0)), value))
|
||||
|
||||
def escape_unicode(value):
|
||||
return escape_string(value)
|
||||
|
||||
def escape_None(value):
|
||||
return 'NULL'
|
||||
|
||||
def escape_timedelta(obj):
|
||||
seconds = int(obj.seconds) % 60
|
||||
minutes = int(obj.seconds // 60) % 60
|
||||
hours = int(obj.seconds // 3600) % 24 + int(obj.days) * 24
|
||||
return escape_string('%02d:%02d:%02d' % (hours, minutes, seconds))
|
||||
|
||||
def escape_time(obj):
|
||||
s = "%02d:%02d:%02d" % (int(obj.hour), int(obj.minute),
|
||||
int(obj.second))
|
||||
if obj.microsecond:
|
||||
s += ".%f" % obj.microsecond
|
||||
|
||||
return escape_string(s)
|
||||
|
||||
def escape_datetime(obj):
|
||||
return escape_string(obj.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
def escape_date(obj):
|
||||
return escape_string(obj.strftime("%Y-%m-%d"))
|
||||
|
||||
def escape_struct_time(obj):
|
||||
return escape_datetime(datetime.datetime(*obj[:6]))
|
||||
|
||||
def convert_datetime(connection, field, obj):
|
||||
"""Returns a DATETIME or TIMESTAMP column value as a datetime object:
|
||||
|
||||
>>> datetime_or_None('2007-02-25 23:06:20')
|
||||
datetime.datetime(2007, 2, 25, 23, 6, 20)
|
||||
>>> datetime_or_None('2007-02-25T23:06:20')
|
||||
datetime.datetime(2007, 2, 25, 23, 6, 20)
|
||||
|
||||
Illegal values are returned as None:
|
||||
|
||||
>>> datetime_or_None('2007-02-31T23:06:20') is None
|
||||
True
|
||||
>>> datetime_or_None('0000-00-00 00:00:00') is None
|
||||
True
|
||||
|
||||
"""
|
||||
if not isinstance(obj, unicode):
|
||||
obj = obj.decode(connection.charset)
|
||||
if ' ' in obj:
|
||||
sep = ' '
|
||||
elif 'T' in obj:
|
||||
sep = 'T'
|
||||
else:
|
||||
return convert_date(connection, field, obj)
|
||||
|
||||
try:
|
||||
ymd, hms = obj.split(sep, 1)
|
||||
return datetime.datetime(*[ int(x) for x in ymd.split('-')+hms.split(':') ])
|
||||
except ValueError:
|
||||
return convert_date(connection, field, obj)
|
||||
|
||||
def convert_timedelta(connection, field, obj):
|
||||
"""Returns a TIME column as a timedelta object:
|
||||
|
||||
>>> timedelta_or_None('25:06:17')
|
||||
datetime.timedelta(1, 3977)
|
||||
>>> timedelta_or_None('-25:06:17')
|
||||
datetime.timedelta(-2, 83177)
|
||||
|
||||
Illegal values are returned as None:
|
||||
|
||||
>>> timedelta_or_None('random crap') is None
|
||||
True
|
||||
|
||||
Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but
|
||||
can accept values as (+|-)DD HH:MM:SS. The latter format will not
|
||||
be parsed correctly by this function.
|
||||
"""
|
||||
from math import modf
|
||||
try:
|
||||
if not isinstance(obj, unicode):
|
||||
obj = obj.decode(connection.charset)
|
||||
hours, minutes, seconds = tuple([int(x) for x in obj.split(':')])
|
||||
tdelta = datetime.timedelta(
|
||||
hours = int(hours),
|
||||
minutes = int(minutes),
|
||||
seconds = int(seconds),
|
||||
microseconds = int(modf(float(seconds))[0]*1000000),
|
||||
)
|
||||
return tdelta
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def convert_time(connection, field, obj):
|
||||
"""Returns a TIME column as a time object:
|
||||
|
||||
>>> time_or_None('15:06:17')
|
||||
datetime.time(15, 6, 17)
|
||||
|
||||
Illegal values are returned as None:
|
||||
|
||||
>>> time_or_None('-25:06:17') is None
|
||||
True
|
||||
>>> time_or_None('random crap') is None
|
||||
True
|
||||
|
||||
Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but
|
||||
can accept values as (+|-)DD HH:MM:SS. The latter format will not
|
||||
be parsed correctly by this function.
|
||||
|
||||
Also note that MySQL's TIME column corresponds more closely to
|
||||
Python's timedelta and not time. However if you want TIME columns
|
||||
to be treated as time-of-day and not a time offset, then you can
|
||||
use set this function as the converter for FIELD_TYPE.TIME.
|
||||
"""
|
||||
from math import modf
|
||||
try:
|
||||
hour, minute, second = obj.split(':')
|
||||
return datetime.time(hour=int(hour), minute=int(minute),
|
||||
second=int(second),
|
||||
microsecond=int(modf(float(second))[0]*1000000))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def convert_date(connection, field, obj):
|
||||
"""Returns a DATE column as a date object:
|
||||
|
||||
>>> date_or_None('2007-02-26')
|
||||
datetime.date(2007, 2, 26)
|
||||
|
||||
Illegal values are returned as None:
|
||||
|
||||
>>> date_or_None('2007-02-31') is None
|
||||
True
|
||||
>>> date_or_None('0000-00-00') is None
|
||||
True
|
||||
|
||||
"""
|
||||
try:
|
||||
if not isinstance(obj, unicode):
|
||||
obj = obj.decode(connection.charset)
|
||||
return datetime.date(*[ int(x) for x in obj.split('-', 2) ])
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def convert_mysql_timestamp(connection, field, timestamp):
|
||||
"""Convert a MySQL TIMESTAMP to a Timestamp object.
|
||||
|
||||
MySQL >= 4.1 returns TIMESTAMP in the same format as DATETIME:
|
||||
|
||||
>>> mysql_timestamp_converter('2007-02-25 22:32:17')
|
||||
datetime.datetime(2007, 2, 25, 22, 32, 17)
|
||||
|
||||
MySQL < 4.1 uses a big string of numbers:
|
||||
|
||||
>>> mysql_timestamp_converter('20070225223217')
|
||||
datetime.datetime(2007, 2, 25, 22, 32, 17)
|
||||
|
||||
Illegal values are returned as None:
|
||||
|
||||
>>> mysql_timestamp_converter('2007-02-31 22:32:17') is None
|
||||
True
|
||||
>>> mysql_timestamp_converter('00000000000000') is None
|
||||
True
|
||||
|
||||
"""
|
||||
if not isinstance(timestamp, unicode):
|
||||
timestamp = timestamp.decode(connection.charset)
|
||||
|
||||
if timestamp[4] == '-':
|
||||
return convert_datetime(connection, field, timestamp)
|
||||
timestamp += "0"*(14-len(timestamp)) # padding
|
||||
year, month, day, hour, minute, second = \
|
||||
int(timestamp[:4]), int(timestamp[4:6]), int(timestamp[6:8]), \
|
||||
int(timestamp[8:10]), int(timestamp[10:12]), int(timestamp[12:14])
|
||||
try:
|
||||
return datetime.datetime(year, month, day, hour, minute, second)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def convert_set(s):
|
||||
return set(s.split(","))
|
||||
|
||||
def convert_bit(connection, field, b):
|
||||
#b = "\x00" * (8 - len(b)) + b # pad w/ zeroes
|
||||
#return struct.unpack(">Q", b)[0]
|
||||
#
|
||||
# the snippet above is right, but MySQLdb doesn't process bits,
|
||||
# so we shouldn't either
|
||||
return b
|
||||
|
||||
def convert_characters(connection, field, data):
|
||||
field_charset = charset_by_id(field.charsetnr).name
|
||||
if field.flags & FLAG.SET:
|
||||
return convert_set(data.decode(field_charset))
|
||||
if field.flags & FLAG.BINARY:
|
||||
return data
|
||||
|
||||
if connection.use_unicode:
|
||||
data = data.decode(field_charset)
|
||||
elif connection.charset != field_charset:
|
||||
data = data.decode(field_charset)
|
||||
data = data.encode(connection.charset)
|
||||
else:
|
||||
data = data.decode(connection.charset)
|
||||
return data
|
||||
|
||||
def convert_int(connection, field, data):
|
||||
return int(data)
|
||||
|
||||
def convert_long(connection, field, data):
|
||||
return long(data)
|
||||
|
||||
def convert_float(connection, field, data):
|
||||
return float(data)
|
||||
|
||||
encoders = {
|
||||
bool: escape_bool,
|
||||
int: escape_int,
|
||||
long: escape_long,
|
||||
float: escape_float,
|
||||
str: escape_string,
|
||||
unicode: escape_unicode,
|
||||
tuple: escape_sequence,
|
||||
list:escape_sequence,
|
||||
set:escape_sequence,
|
||||
dict:escape_dict,
|
||||
type(None):escape_None,
|
||||
datetime.date: escape_date,
|
||||
datetime.datetime : escape_datetime,
|
||||
datetime.timedelta : escape_timedelta,
|
||||
datetime.time : escape_time,
|
||||
time.struct_time : escape_struct_time,
|
||||
}
|
||||
|
||||
decoders = {
|
||||
FIELD_TYPE.BIT: convert_bit,
|
||||
FIELD_TYPE.TINY: convert_int,
|
||||
FIELD_TYPE.SHORT: convert_int,
|
||||
FIELD_TYPE.LONG: convert_long,
|
||||
FIELD_TYPE.FLOAT: convert_float,
|
||||
FIELD_TYPE.DOUBLE: convert_float,
|
||||
FIELD_TYPE.DECIMAL: convert_float,
|
||||
FIELD_TYPE.NEWDECIMAL: convert_float,
|
||||
FIELD_TYPE.LONGLONG: convert_long,
|
||||
FIELD_TYPE.INT24: convert_int,
|
||||
FIELD_TYPE.YEAR: convert_int,
|
||||
FIELD_TYPE.TIMESTAMP: convert_mysql_timestamp,
|
||||
FIELD_TYPE.DATETIME: convert_datetime,
|
||||
FIELD_TYPE.TIME: convert_timedelta,
|
||||
FIELD_TYPE.DATE: convert_date,
|
||||
FIELD_TYPE.SET: convert_set,
|
||||
FIELD_TYPE.BLOB: convert_characters,
|
||||
FIELD_TYPE.TINY_BLOB: convert_characters,
|
||||
FIELD_TYPE.MEDIUM_BLOB: convert_characters,
|
||||
FIELD_TYPE.LONG_BLOB: convert_characters,
|
||||
FIELD_TYPE.STRING: convert_characters,
|
||||
FIELD_TYPE.VAR_STRING: convert_characters,
|
||||
FIELD_TYPE.VARCHAR: convert_characters,
|
||||
#FIELD_TYPE.BLOB: str,
|
||||
#FIELD_TYPE.STRING: str,
|
||||
#FIELD_TYPE.VAR_STRING: str,
|
||||
#FIELD_TYPE.VARCHAR: str
|
||||
}
|
||||
conversions = decoders # for MySQLdb compatibility
|
||||
|
||||
try:
|
||||
# python version > 2.3
|
||||
from decimal import Decimal
|
||||
def convert_decimal(connection, field, data):
|
||||
return Decimal(data)
|
||||
decoders[FIELD_TYPE.DECIMAL] = convert_decimal
|
||||
decoders[FIELD_TYPE.NEWDECIMAL] = convert_decimal
|
||||
|
||||
def escape_decimal(obj):
|
||||
return unicode(obj)
|
||||
encoders[Decimal] = escape_decimal
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import struct
|
||||
import re
|
||||
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
except ImportError:
|
||||
import StringIO
|
||||
|
||||
from err import Warning, Error, InterfaceError, DataError, \
|
||||
DatabaseError, OperationalError, IntegrityError, InternalError, \
|
||||
NotSupportedError, ProgrammingError
|
||||
|
||||
insert_values = re.compile(r'\svalues\s*(\(.+\))', re.IGNORECASE)
|
||||
|
||||
class Cursor(object):
|
||||
'''
|
||||
This is the object you use to interact with the database.
|
||||
'''
|
||||
def __init__(self, connection):
|
||||
'''
|
||||
Do not create an instance of a Cursor yourself. Call
|
||||
connections.Connection.cursor().
|
||||
'''
|
||||
from weakref import proxy
|
||||
self.connection = proxy(connection)
|
||||
self.description = None
|
||||
self.rownumber = 0
|
||||
self.rowcount = -1
|
||||
self.arraysize = 1
|
||||
self._executed = None
|
||||
self.messages = []
|
||||
self.errorhandler = connection.errorhandler
|
||||
self._has_next = None
|
||||
self._rows = ()
|
||||
|
||||
def __del__(self):
|
||||
'''
|
||||
When this gets GC'd close it.
|
||||
'''
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
'''
|
||||
Closing a cursor just exhausts all remaining data.
|
||||
'''
|
||||
if not self.connection:
|
||||
return
|
||||
try:
|
||||
while self.nextset():
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
self.connection = None
|
||||
|
||||
def _get_db(self):
|
||||
if not self.connection:
|
||||
self.errorhandler(self, ProgrammingError, "cursor closed")
|
||||
return self.connection
|
||||
|
||||
def _check_executed(self):
|
||||
if not self._executed:
|
||||
self.errorhandler(self, ProgrammingError, "execute() first")
|
||||
|
||||
def setinputsizes(self, *args):
|
||||
"""Does nothing, required by DB API."""
|
||||
|
||||
def setoutputsizes(self, *args):
|
||||
"""Does nothing, required by DB API."""
|
||||
|
||||
def nextset(self):
|
||||
''' Get the next query set '''
|
||||
if self._executed:
|
||||
self.fetchall()
|
||||
del self.messages[:]
|
||||
|
||||
if not self._has_next:
|
||||
return None
|
||||
connection = self._get_db()
|
||||
connection.next_result()
|
||||
self._do_get_result()
|
||||
return True
|
||||
|
||||
def execute(self, query, args=None):
|
||||
''' Execute a query '''
|
||||
from sys import exc_info
|
||||
|
||||
conn = self._get_db()
|
||||
charset = conn.charset
|
||||
del self.messages[:]
|
||||
|
||||
# TODO: make sure that conn.escape is correct
|
||||
|
||||
if args is not None:
|
||||
query = query % conn.escape(args)
|
||||
|
||||
if isinstance(query, unicode):
|
||||
query = query.encode(charset)
|
||||
|
||||
result = 0
|
||||
try:
|
||||
result = self._query(query)
|
||||
except:
|
||||
exc, value, tb = exc_info()
|
||||
del tb
|
||||
self.messages.append((exc,value))
|
||||
self.errorhandler(self, exc, value)
|
||||
|
||||
self._executed = query
|
||||
return result
|
||||
|
||||
def executemany(self, query, args):
|
||||
''' Run several data against one query '''
|
||||
del self.messages[:]
|
||||
conn = self._get_db()
|
||||
if not args:
|
||||
return
|
||||
charset = conn.charset
|
||||
if isinstance(query, unicode):
|
||||
query = query.encode(charset)
|
||||
|
||||
self.rowcount = sum([ self.execute(query, arg) for arg in args ])
|
||||
return self.rowcount
|
||||
|
||||
|
||||
def callproc(self, procname, args=()):
|
||||
"""Execute stored procedure procname with args
|
||||
|
||||
procname -- string, name of procedure to execute on server
|
||||
|
||||
args -- Sequence of parameters to use with procedure
|
||||
|
||||
Returns the original args.
|
||||
|
||||
Compatibility warning: PEP-249 specifies that any modified
|
||||
parameters must be returned. This is currently impossible
|
||||
as they are only available by storing them in a server
|
||||
variable and then retrieved by a query. Since stored
|
||||
procedures return zero or more result sets, there is no
|
||||
reliable way to get at OUT or INOUT parameters via callproc.
|
||||
The server variables are named @_procname_n, where procname
|
||||
is the parameter above and n is the position of the parameter
|
||||
(from zero). Once all result sets generated by the procedure
|
||||
have been fetched, you can issue a SELECT @_procname_0, ...
|
||||
query using .execute() to get any OUT or INOUT values.
|
||||
|
||||
Compatibility warning: The act of calling a stored procedure
|
||||
itself creates an empty result set. This appears after any
|
||||
result sets generated by the procedure. This is non-standard
|
||||
behavior with respect to the DB-API. Be sure to use nextset()
|
||||
to advance through all result sets; otherwise you may get
|
||||
disconnected.
|
||||
"""
|
||||
conn = self._get_db()
|
||||
for index, arg in enumerate(args):
|
||||
q = "SET @_%s_%d=%s" % (procname, index, conn.escape(arg))
|
||||
if isinstance(q, unicode):
|
||||
q = q.encode(conn.charset)
|
||||
self._query(q)
|
||||
self.nextset()
|
||||
|
||||
q = "CALL %s(%s)" % (procname,
|
||||
','.join(['@_%s_%d' % (procname, i)
|
||||
for i in range(len(args))]))
|
||||
if isinstance(q, unicode):
|
||||
q = q.encode(conn.charset)
|
||||
self._query(q)
|
||||
self._executed = q
|
||||
|
||||
return args
|
||||
|
||||
def fetchone(self):
|
||||
''' Fetch the next row '''
|
||||
self._check_executed()
|
||||
if self._rows is None or self.rownumber >= len(self._rows):
|
||||
return None
|
||||
result = self._rows[self.rownumber]
|
||||
self.rownumber += 1
|
||||
return result
|
||||
|
||||
def fetchmany(self, size=None):
|
||||
''' Fetch several rows '''
|
||||
self._check_executed()
|
||||
end = self.rownumber + (size or self.arraysize)
|
||||
result = self._rows[self.rownumber:end]
|
||||
if self._rows is None:
|
||||
return None
|
||||
self.rownumber = min(end, len(self._rows))
|
||||
return result
|
||||
|
||||
def fetchall(self):
|
||||
''' Fetch all the rows '''
|
||||
self._check_executed()
|
||||
if self._rows is None:
|
||||
return None
|
||||
if self.rownumber:
|
||||
result = self._rows[self.rownumber:]
|
||||
else:
|
||||
result = self._rows
|
||||
self.rownumber = len(self._rows)
|
||||
return result
|
||||
|
||||
def scroll(self, value, mode='relative'):
|
||||
self._check_executed()
|
||||
if mode == 'relative':
|
||||
r = self.rownumber + value
|
||||
elif mode == 'absolute':
|
||||
r = value
|
||||
else:
|
||||
self.errorhandler(self, ProgrammingError,
|
||||
"unknown scroll mode %s" % mode)
|
||||
|
||||
if r < 0 or r >= len(self._rows):
|
||||
self.errorhandler(self, IndexError, "out of range")
|
||||
self.rownumber = r
|
||||
|
||||
def _query(self, q):
|
||||
conn = self._get_db()
|
||||
self._last_executed = q
|
||||
conn.query(q)
|
||||
self._do_get_result()
|
||||
return self.rowcount
|
||||
|
||||
def _do_get_result(self):
|
||||
conn = self._get_db()
|
||||
self.rowcount = conn._result.affected_rows
|
||||
|
||||
self.rownumber = 0
|
||||
self.description = conn._result.description
|
||||
self.lastrowid = conn._result.insert_id
|
||||
self._rows = conn._result.rows
|
||||
self._has_next = conn._result.has_next
|
||||
conn._result = None
|
||||
|
||||
def __iter__(self):
|
||||
self._check_executed()
|
||||
result = self.rownumber and self._rows[self.rownumber:] or self._rows
|
||||
return iter(result)
|
||||
|
||||
Warning = Warning
|
||||
Error = Error
|
||||
InterfaceError = InterfaceError
|
||||
DatabaseError = DatabaseError
|
||||
DataError = DataError
|
||||
OperationalError = OperationalError
|
||||
IntegrityError = IntegrityError
|
||||
InternalError = InternalError
|
||||
ProgrammingError = ProgrammingError
|
||||
NotSupportedError = NotSupportedError
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import struct
|
||||
|
||||
|
||||
try:
|
||||
Exception, Warning
|
||||
except ImportError:
|
||||
try:
|
||||
from exceptions import Exception, Warning
|
||||
except ImportError:
|
||||
import sys
|
||||
e = sys.modules['exceptions']
|
||||
Exception = e.Exception
|
||||
Warning = e.Warning
|
||||
|
||||
from constants import ER
|
||||
|
||||
class MySQLError(Exception):
|
||||
|
||||
"""Exception related to operation with MySQL."""
|
||||
|
||||
|
||||
class Warning(Warning, MySQLError):
|
||||
|
||||
"""Exception raised for important warnings like data truncations
|
||||
while inserting, etc."""
|
||||
|
||||
class Error(MySQLError):
|
||||
|
||||
"""Exception that is the base class of all other error exceptions
|
||||
(not Warning)."""
|
||||
|
||||
|
||||
class InterfaceError(Error):
|
||||
|
||||
"""Exception raised for errors that are related to the database
|
||||
interface rather than the database itself."""
|
||||
|
||||
|
||||
class DatabaseError(Error):
|
||||
|
||||
"""Exception raised for errors that are related to the
|
||||
database."""
|
||||
|
||||
|
||||
class DataError(DatabaseError):
|
||||
|
||||
"""Exception raised for errors that are due to problems with the
|
||||
processed data like division by zero, numeric value out of range,
|
||||
etc."""
|
||||
|
||||
|
||||
class OperationalError(DatabaseError):
|
||||
|
||||
"""Exception raised for errors that are related to the database's
|
||||
operation and not necessarily under the control of the programmer,
|
||||
e.g. an unexpected disconnect occurs, the data source name is not
|
||||
found, a transaction could not be processed, a memory allocation
|
||||
error occurred during processing, etc."""
|
||||
|
||||
|
||||
class IntegrityError(DatabaseError):
|
||||
|
||||
"""Exception raised when the relational integrity of the database
|
||||
is affected, e.g. a foreign key check fails, duplicate key,
|
||||
etc."""
|
||||
|
||||
|
||||
class InternalError(DatabaseError):
|
||||
|
||||
"""Exception raised when the database encounters an internal
|
||||
error, e.g. the cursor is not valid anymore, the transaction is
|
||||
out of sync, etc."""
|
||||
|
||||
|
||||
class ProgrammingError(DatabaseError):
|
||||
|
||||
"""Exception raised for programming errors, e.g. table not found
|
||||
or already exists, syntax error in the SQL statement, wrong number
|
||||
of parameters specified, etc."""
|
||||
|
||||
|
||||
class NotSupportedError(DatabaseError):
|
||||
|
||||
"""Exception raised in case a method or database API was used
|
||||
which is not supported by the database, e.g. requesting a
|
||||
.rollback() on a connection that does not support transaction or
|
||||
has transactions turned off."""
|
||||
|
||||
|
||||
error_map = {}
|
||||
|
||||
def _map_error(exc, *errors):
|
||||
for error in errors:
|
||||
error_map[error] = exc
|
||||
|
||||
_map_error(ProgrammingError, ER.DB_CREATE_EXISTS, ER.SYNTAX_ERROR,
|
||||
ER.PARSE_ERROR, ER.NO_SUCH_TABLE, ER.WRONG_DB_NAME,
|
||||
ER.WRONG_TABLE_NAME, ER.FIELD_SPECIFIED_TWICE,
|
||||
ER.INVALID_GROUP_FUNC_USE, ER.UNSUPPORTED_EXTENSION,
|
||||
ER.TABLE_MUST_HAVE_COLUMNS, ER.CANT_DO_THIS_DURING_AN_TRANSACTION)
|
||||
_map_error(DataError, ER.WARN_DATA_TRUNCATED, ER.WARN_NULL_TO_NOTNULL,
|
||||
ER.WARN_DATA_OUT_OF_RANGE, ER.NO_DEFAULT, ER.PRIMARY_CANT_HAVE_NULL,
|
||||
ER.DATA_TOO_LONG, ER.DATETIME_FUNCTION_OVERFLOW)
|
||||
_map_error(IntegrityError, ER.DUP_ENTRY, ER.NO_REFERENCED_ROW,
|
||||
ER.NO_REFERENCED_ROW_2, ER.ROW_IS_REFERENCED, ER.ROW_IS_REFERENCED_2,
|
||||
ER.CANNOT_ADD_FOREIGN)
|
||||
_map_error(NotSupportedError, ER.WARNING_NOT_COMPLETE_ROLLBACK,
|
||||
ER.NOT_SUPPORTED_YET, ER.FEATURE_DISABLED, ER.UNKNOWN_STORAGE_ENGINE)
|
||||
|
||||
del _map_error, ER
|
||||
|
||||
|
||||
def _get_error_info(data):
|
||||
errno = struct.unpack('<h', data[1:3])[0]
|
||||
if data[3] == "#":
|
||||
# version 4.1
|
||||
sqlstate = data[4:9].decode("utf8")
|
||||
errorvalue = data[9:].decode("utf8")
|
||||
return (errno, sqlstate, errorvalue)
|
||||
else:
|
||||
# version 4.0
|
||||
return (errno, None, data[3:].decode("utf8"))
|
||||
|
||||
def _check_mysql_exception(errinfo):
|
||||
errno, sqlstate, errorvalue = errinfo
|
||||
errorclass = error_map.get(errno, None)
|
||||
if errorclass:
|
||||
raise errorclass, (errno,errorvalue)
|
||||
|
||||
# couldn't find the right error number
|
||||
raise InternalError, (errno, errorvalue)
|
||||
|
||||
def raise_mysql_exception(data):
|
||||
errinfo = _get_error_info(data)
|
||||
_check_mysql_exception(errinfo)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from pymysql.tests.test_issues import *
|
||||
from pymysql.tests.test_example import *
|
||||
from pymysql.tests.test_basic import *
|
||||
|
||||
if __name__ == "__main__":
|
||||
import unittest
|
||||
unittest.main()
|
||||
@@ -0,0 +1,19 @@
|
||||
import pymysql
|
||||
import unittest
|
||||
|
||||
class PyMySQLTestCase(unittest.TestCase):
|
||||
databases = [
|
||||
{"host":"localhost","user":"root",
|
||||
"passwd":"","db":"test_pymysql", "use_unicode": True},
|
||||
{"host":"localhost","user":"root","passwd":"","db":"test_pymysql2"}]
|
||||
|
||||
def setUp(self):
|
||||
self.connections = []
|
||||
|
||||
for params in self.databases:
|
||||
self.connections.append(pymysql.connect(**params))
|
||||
|
||||
def tearDown(self):
|
||||
for connection in self.connections:
|
||||
connection.close()
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
from pymysql.tests import base
|
||||
from pymysql import util
|
||||
|
||||
import time
|
||||
import datetime
|
||||
|
||||
class TestConversion(base.PyMySQLTestCase):
|
||||
def test_datatypes(self):
|
||||
""" test every data type """
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
c.execute("create table test_datatypes (b bit, i int, l bigint, f real, s varchar(32), u varchar(32), bb blob, d date, dt datetime, ts timestamp, td time, t time, st datetime)")
|
||||
try:
|
||||
# insert values
|
||||
v = (True, -3, 123456789012, 5.7, "hello'\" world", u"Espa\xc3\xb1ol", "binary\x00data".encode(conn.charset), datetime.date(1988,2,2), datetime.datetime.now(), datetime.timedelta(5,6), datetime.time(16,32), time.localtime())
|
||||
c.execute("insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", v)
|
||||
c.execute("select b,i,l,f,s,u,bb,d,dt,td,t,st from test_datatypes")
|
||||
r = c.fetchone()
|
||||
self.assertEqual(util.int2byte(1), r[0])
|
||||
self.assertEqual(v[1:8], r[1:8])
|
||||
# mysql throws away microseconds so we need to check datetimes
|
||||
# specially. additionally times are turned into timedeltas.
|
||||
self.assertEqual(datetime.datetime(*v[8].timetuple()[:6]), r[8])
|
||||
self.assertEqual(v[9], r[9]) # just timedeltas
|
||||
self.assertEqual(datetime.timedelta(0, 60 * (v[10].hour * 60 + v[10].minute)), r[10])
|
||||
self.assertEqual(datetime.datetime(*v[-1][:6]), r[-1])
|
||||
|
||||
c.execute("delete from test_datatypes")
|
||||
|
||||
# check nulls
|
||||
c.execute("insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", [None] * 12)
|
||||
c.execute("select b,i,l,f,s,u,bb,d,dt,td,t,st from test_datatypes")
|
||||
r = c.fetchone()
|
||||
self.assertEqual(tuple([None] * 12), r)
|
||||
|
||||
c.execute("delete from test_datatypes")
|
||||
|
||||
# check sequence type
|
||||
c.execute("insert into test_datatypes (i, l) values (2,4), (6,8), (10,12)")
|
||||
c.execute("select l from test_datatypes where i in %s order by i", ((2,6),))
|
||||
r = c.fetchall()
|
||||
self.assertEqual(((4,),(8,)), r)
|
||||
finally:
|
||||
c.execute("drop table test_datatypes")
|
||||
|
||||
def test_dict(self):
|
||||
""" test dict escaping """
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
c.execute("create table test_dict (a integer, b integer, c integer)")
|
||||
try:
|
||||
c.execute("insert into test_dict (a,b,c) values (%(a)s, %(b)s, %(c)s)", {"a":1,"b":2,"c":3})
|
||||
c.execute("select a,b,c from test_dict")
|
||||
self.assertEqual((1,2,3), c.fetchone())
|
||||
finally:
|
||||
c.execute("drop table test_dict")
|
||||
|
||||
def test_big_blob(self):
|
||||
""" test tons of data """
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
c.execute("create table test_big_blob (b blob)")
|
||||
try:
|
||||
data = "pymysql" * 1024
|
||||
c.execute("insert into test_big_blob (b) values (%s)", (data,))
|
||||
c.execute("select b from test_big_blob")
|
||||
self.assertEqual(data.encode(conn.charset), c.fetchone()[0])
|
||||
finally:
|
||||
c.execute("drop table test_big_blob")
|
||||
|
||||
class TestCursor(base.PyMySQLTestCase):
|
||||
# this test case does not work quite right yet, however,
|
||||
# we substitute in None for the erroneous field which is
|
||||
# compatible with the DB-API 2.0 spec and has not broken
|
||||
# any unit tests for anything we've tried.
|
||||
|
||||
#def test_description(self):
|
||||
# """ test description attribute """
|
||||
# # result is from MySQLdb module
|
||||
# r = (('Host', 254, 11, 60, 60, 0, 0),
|
||||
# ('User', 254, 16, 16, 16, 0, 0),
|
||||
# ('Password', 254, 41, 41, 41, 0, 0),
|
||||
# ('Select_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Insert_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Update_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Delete_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Create_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Drop_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Reload_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Shutdown_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Process_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('File_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Grant_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('References_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Index_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Alter_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Show_db_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Super_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Create_tmp_table_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Lock_tables_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Execute_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Repl_slave_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Repl_client_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Create_view_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Show_view_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Create_routine_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Alter_routine_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Create_user_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Event_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('Trigger_priv', 254, 1, 1, 1, 0, 0),
|
||||
# ('ssl_type', 254, 0, 9, 9, 0, 0),
|
||||
# ('ssl_cipher', 252, 0, 65535, 65535, 0, 0),
|
||||
# ('x509_issuer', 252, 0, 65535, 65535, 0, 0),
|
||||
# ('x509_subject', 252, 0, 65535, 65535, 0, 0),
|
||||
# ('max_questions', 3, 1, 11, 11, 0, 0),
|
||||
# ('max_updates', 3, 1, 11, 11, 0, 0),
|
||||
# ('max_connections', 3, 1, 11, 11, 0, 0),
|
||||
# ('max_user_connections', 3, 1, 11, 11, 0, 0))
|
||||
# conn = self.connections[0]
|
||||
# c = conn.cursor()
|
||||
# c.execute("select * from mysql.user")
|
||||
#
|
||||
# self.assertEqual(r, c.description)
|
||||
|
||||
def test_fetch_no_result(self):
|
||||
""" test a fetchone() with no rows """
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
c.execute("create table test_nr (b varchar(32))")
|
||||
try:
|
||||
data = "pymysql"
|
||||
c.execute("insert into test_nr (b) values (%s)", (data,))
|
||||
self.assertEqual(None, c.fetchone())
|
||||
finally:
|
||||
c.execute("drop table test_nr")
|
||||
|
||||
__all__ = ["TestConversion","TestCursor"]
|
||||
|
||||
if __name__ == "__main__":
|
||||
import unittest
|
||||
unittest.main()
|
||||
@@ -0,0 +1,32 @@
|
||||
import pymysql
|
||||
from pymysql.tests import base
|
||||
|
||||
class TestExample(base.PyMySQLTestCase):
|
||||
def test_example(self):
|
||||
conn = pymysql.connect(host='127.0.0.1', port=3306, user='root', passwd='', db='mysql')
|
||||
|
||||
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("SELECT Host,User FROM user")
|
||||
|
||||
# print cur.description
|
||||
|
||||
# r = cur.fetchall()
|
||||
# print r
|
||||
# ...or...
|
||||
u = False
|
||||
|
||||
for r in cur.fetchall():
|
||||
u = u or conn.user in r
|
||||
|
||||
self.assertTrue(u)
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
__all__ = ["TestExample"]
|
||||
|
||||
if __name__ == "__main__":
|
||||
import unittest
|
||||
unittest.main()
|
||||
@@ -0,0 +1,240 @@
|
||||
import pymysql
|
||||
from pymysql.tests import base
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
import imp
|
||||
reload = imp.reload
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
import datetime
|
||||
|
||||
class TestOldIssues(base.PyMySQLTestCase):
|
||||
def test_issue_3(self):
|
||||
""" undefined methods datetime_or_None, date_or_None """
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
c.execute("create table issue3 (d date, t time, dt datetime, ts timestamp)")
|
||||
try:
|
||||
c.execute("insert into issue3 (d, t, dt, ts) values (%s,%s,%s,%s)", (None, None, None, None))
|
||||
c.execute("select d from issue3")
|
||||
self.assertEqual(None, c.fetchone()[0])
|
||||
c.execute("select t from issue3")
|
||||
self.assertEqual(None, c.fetchone()[0])
|
||||
c.execute("select dt from issue3")
|
||||
self.assertEqual(None, c.fetchone()[0])
|
||||
c.execute("select ts from issue3")
|
||||
self.assertTrue(isinstance(c.fetchone()[0], datetime.datetime))
|
||||
finally:
|
||||
c.execute("drop table issue3")
|
||||
|
||||
def test_issue_4(self):
|
||||
""" can't retrieve TIMESTAMP fields """
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
c.execute("create table issue4 (ts timestamp)")
|
||||
try:
|
||||
c.execute("insert into issue4 (ts) values (now())")
|
||||
c.execute("select ts from issue4")
|
||||
self.assertTrue(isinstance(c.fetchone()[0], datetime.datetime))
|
||||
finally:
|
||||
c.execute("drop table issue4")
|
||||
|
||||
def test_issue_5(self):
|
||||
""" query on information_schema.tables fails """
|
||||
con = self.connections[0]
|
||||
cur = con.cursor()
|
||||
cur.execute("select * from information_schema.tables")
|
||||
|
||||
def test_issue_6(self):
|
||||
""" exception: TypeError: ord() expected a character, but string of length 0 found """
|
||||
conn = pymysql.connect(host="localhost",user="root",passwd="",db="mysql")
|
||||
c = conn.cursor()
|
||||
c.execute("select * from user")
|
||||
conn.close()
|
||||
|
||||
def test_issue_8(self):
|
||||
""" Primary Key and Index error when selecting data """
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
c.execute("""CREATE TABLE `test` (`station` int(10) NOT NULL DEFAULT '0', `dh`
|
||||
datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `echeance` int(1) NOT NULL
|
||||
DEFAULT '0', `me` double DEFAULT NULL, `mo` double DEFAULT NULL, PRIMARY
|
||||
KEY (`station`,`dh`,`echeance`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;""")
|
||||
try:
|
||||
self.assertEqual(0, c.execute("SELECT * FROM test"))
|
||||
c.execute("ALTER TABLE `test` ADD INDEX `idx_station` (`station`)")
|
||||
self.assertEqual(0, c.execute("SELECT * FROM test"))
|
||||
finally:
|
||||
c.execute("drop table test")
|
||||
|
||||
def test_issue_9(self):
|
||||
""" sets DeprecationWarning in Python 2.6 """
|
||||
try:
|
||||
reload(pymysql)
|
||||
except DeprecationWarning:
|
||||
self.fail()
|
||||
|
||||
def test_issue_10(self):
|
||||
""" Allocate a variable to return when the exception handler is permissive """
|
||||
conn = self.connections[0]
|
||||
conn.errorhandler = lambda cursor, errorclass, errorvalue: None
|
||||
cur = conn.cursor()
|
||||
cur.execute( "create table t( n int )" )
|
||||
cur.execute( "create table t( n int )" )
|
||||
|
||||
def test_issue_13(self):
|
||||
""" can't handle large result fields """
|
||||
conn = self.connections[0]
|
||||
cur = conn.cursor()
|
||||
cur.execute("create table issue13 (t text)")
|
||||
try:
|
||||
# ticket says 18k
|
||||
size = 18*1024
|
||||
cur.execute("insert into issue13 (t) values (%s)", ("x" * size,))
|
||||
cur.execute("select t from issue13")
|
||||
# use assert_ so that obscenely huge error messages don't print
|
||||
r = cur.fetchone()[0]
|
||||
self.assert_("x" * size == r)
|
||||
finally:
|
||||
cur.execute("drop table issue13")
|
||||
|
||||
def test_issue_14(self):
|
||||
""" typo in converters.py """
|
||||
self.assertEqual('1', pymysql.converters.escape_item(1, "utf8"))
|
||||
self.assertEqual('1', pymysql.converters.escape_item(1L, "utf8"))
|
||||
|
||||
self.assertEqual('1', pymysql.converters.escape_object(1))
|
||||
self.assertEqual('1', pymysql.converters.escape_object(1L))
|
||||
|
||||
def test_issue_15(self):
|
||||
""" query should be expanded before perform character encoding """
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
c.execute("create table issue15 (t varchar(32))")
|
||||
try:
|
||||
c.execute("insert into issue15 (t) values (%s)", (u'\xe4\xf6\xfc'))
|
||||
c.execute("select t from issue15")
|
||||
self.assertEqual(u'\xe4\xf6\xfc', c.fetchone()[0])
|
||||
finally:
|
||||
c.execute("drop table issue15")
|
||||
|
||||
def test_issue_16(self):
|
||||
""" Patch for string and tuple escaping """
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
c.execute("create table issue16 (name varchar(32) primary key, email varchar(32))")
|
||||
try:
|
||||
c.execute("insert into issue16 (name, email) values ('pete', 'floydophone')")
|
||||
c.execute("select email from issue16 where name=%s", ("pete",))
|
||||
self.assertEqual("floydophone", c.fetchone()[0])
|
||||
finally:
|
||||
c.execute("drop table issue16")
|
||||
|
||||
def test_issue_17(self):
|
||||
""" could not connect mysql use passwod """
|
||||
conn = self.connections[0]
|
||||
host = self.databases[0]["host"]
|
||||
db = self.databases[0]["db"]
|
||||
c = conn.cursor()
|
||||
# grant access to a table to a user with a password
|
||||
try:
|
||||
c.execute("create table issue17 (x varchar(32) primary key)")
|
||||
c.execute("insert into issue17 (x) values ('hello, world!')")
|
||||
c.execute("grant all privileges on %s.issue17 to 'issue17user'@'%%' identified by '1234'" % db)
|
||||
conn.commit()
|
||||
|
||||
conn2 = pymysql.connect(host=host, user="issue17user", passwd="1234", db=db)
|
||||
c2 = conn2.cursor()
|
||||
c2.execute("select x from issue17")
|
||||
self.assertEqual("hello, world!", c2.fetchone()[0])
|
||||
finally:
|
||||
c.execute("drop table issue17")
|
||||
|
||||
def _uni(s, e):
|
||||
# hack for py3
|
||||
if sys.version_info[0] > 2:
|
||||
return unicode(bytes(s, sys.getdefaultencoding()), e)
|
||||
else:
|
||||
return unicode(s, e)
|
||||
|
||||
class TestNewIssues(base.PyMySQLTestCase):
|
||||
def test_issue_34(self):
|
||||
try:
|
||||
pymysql.connect(host="localhost", port=1237, user="root")
|
||||
self.fail()
|
||||
except pymysql.OperationalError, e:
|
||||
self.assertEqual(2003, e.args[0])
|
||||
except:
|
||||
self.fail()
|
||||
|
||||
def test_issue_33(self):
|
||||
conn = pymysql.connect(host="localhost", user="root", db=self.databases[0]["db"], charset="utf8")
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute(_uni("create table hei\xc3\x9fe (name varchar(32))", "utf8"))
|
||||
c.execute(_uni("insert into hei\xc3\x9fe (name) values ('Pi\xc3\xb1ata')", "utf8"))
|
||||
c.execute(_uni("select name from hei\xc3\x9fe", "utf8"))
|
||||
self.assertEqual(_uni("Pi\xc3\xb1ata","utf8"), c.fetchone()[0])
|
||||
finally:
|
||||
c.execute(_uni("drop table hei\xc3\x9fe", "utf8"))
|
||||
|
||||
# Will fail without manual intervention:
|
||||
#def test_issue_35(self):
|
||||
#
|
||||
# conn = self.connections[0]
|
||||
# c = conn.cursor()
|
||||
# print "sudo killall -9 mysqld within the next 10 seconds"
|
||||
# try:
|
||||
# c.execute("select sleep(10)")
|
||||
# self.fail()
|
||||
# except pymysql.OperationalError, e:
|
||||
# self.assertEqual(2013, e.args[0])
|
||||
|
||||
def test_issue_36(self):
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
# kill connections[0]
|
||||
original_count = c.execute("show processlist")
|
||||
kill_id = None
|
||||
for id,user,host,db,command,time,state,info in c.fetchall():
|
||||
if info == "show processlist":
|
||||
kill_id = id
|
||||
break
|
||||
# now nuke the connection
|
||||
conn.kill(kill_id)
|
||||
# make sure this connection has broken
|
||||
try:
|
||||
c.execute("show tables")
|
||||
self.fail()
|
||||
except:
|
||||
pass
|
||||
# check the process list from the other connection
|
||||
self.assertEqual(original_count - 1, self.connections[1].cursor().execute("show processlist"))
|
||||
del self.connections[0]
|
||||
|
||||
def test_issue_37(self):
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
self.assertEqual(1, c.execute("SELECT @foo"))
|
||||
self.assertEqual((None,), c.fetchone())
|
||||
self.assertEqual(0, c.execute("SET @foo = 'bar'"))
|
||||
c.execute("set @foo = 'bar'")
|
||||
|
||||
def test_issue_38(self):
|
||||
conn = self.connections[0]
|
||||
c = conn.cursor()
|
||||
datum = "a" * 1024 * 1023 # reduced size for most default mysql installs
|
||||
|
||||
try:
|
||||
c.execute("create table issue38 (id integer, data mediumblob)")
|
||||
c.execute("insert into issue38 values (1, %s)", datum)
|
||||
finally:
|
||||
c.execute("drop table issue38")
|
||||
__all__ = ["TestOldIssues", "TestNewIssues"]
|
||||
|
||||
if __name__ == "__main__":
|
||||
import unittest
|
||||
unittest.main()
|
||||
@@ -0,0 +1,17 @@
|
||||
from time import localtime
|
||||
from datetime import date, datetime, time, timedelta
|
||||
|
||||
Date = date
|
||||
Time = time
|
||||
TimeDelta = timedelta
|
||||
Timestamp = datetime
|
||||
|
||||
def DateFromTicks(ticks):
|
||||
return date(*localtime(ticks)[:3])
|
||||
|
||||
def TimeFromTicks(ticks):
|
||||
return time(*localtime(ticks)[3:6])
|
||||
|
||||
def TimestampFromTicks(ticks):
|
||||
return datetime(*localtime(ticks)[:6])
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import struct
|
||||
|
||||
def byte2int(b):
|
||||
if isinstance(b, int):
|
||||
return b
|
||||
else:
|
||||
return struct.unpack("!B", b)[0]
|
||||
|
||||
def int2byte(i):
|
||||
return struct.pack("!B", i)
|
||||
|
||||
def join_bytes(bs):
|
||||
if len(bs) == 0:
|
||||
return ""
|
||||
else:
|
||||
rv = bs[0]
|
||||
for b in bs[1:]:
|
||||
rv += b
|
||||
return rv
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
class ViewKind :
|
||||
"""An integer (0-5) that represents the view mode of the document."""
|
||||
|
||||
NONE = 0
|
||||
PageLayout = 1
|
||||
Outline = 2
|
||||
MasterDocument = 3
|
||||
Normal = 4
|
||||
OnlineLayout = 5
|
||||
|
||||
DEFAULT = PageLayout
|
||||
|
||||
def _IsValid( cls, value ) :
|
||||
return value in [ 0, 1, 2, 3, 4, 5 ]
|
||||
IsValid = classmethod( _IsValid )
|
||||
|
||||
class ViewScale :
|
||||
"""Zoom level of the document; the N argument is a value representing a percentage (the default is 100)."""
|
||||
|
||||
def _IsValid( cls, value ) :
|
||||
return value is None or (0 < value < 101)
|
||||
IsValid = classmethod( _IsValid )
|
||||
|
||||
class ViewZoomKind :
|
||||
"""An integer (0 to 2) that represents the zoom kind of the document."""
|
||||
|
||||
NONE = 0
|
||||
FullPage = 1
|
||||
BestFit = 2
|
||||
|
||||
def _IsValid( cls, value ) :
|
||||
return value in [ None, 0, 1, 2 ]
|
||||
IsValid = classmethod( _IsValid )
|
||||
|
||||
|
||||
class Languages :
|
||||
NoLanguage = 1024
|
||||
Albanian = 1052
|
||||
Arabic = 1025
|
||||
Bahasa = 1057
|
||||
BelgianDutch = 2067
|
||||
BelgianFrench = 2060
|
||||
BrazilianPortuguese = 1046
|
||||
Bulgarian = 1026
|
||||
Catalan = 1027
|
||||
CroatoSerbianLatin = 1050
|
||||
Czech = 1029
|
||||
Danish = 1030
|
||||
Dutch = 1043
|
||||
EnglishAustralian = 3081
|
||||
EnglishUK = 2057
|
||||
EnglishUS = 1033
|
||||
Finnish = 1035
|
||||
French = 1036
|
||||
FrenchCanadian = 3084
|
||||
German = 1031
|
||||
Greek = 1032
|
||||
Hebrew = 1037
|
||||
Hungarian = 1038
|
||||
Icelandic = 1039
|
||||
Italian = 1040
|
||||
Japanese = 1041
|
||||
Korean = 1042
|
||||
NorwegianBokmal = 1044
|
||||
NorwegianNynorsk = 2068
|
||||
Polish = 1045
|
||||
Portuguese = 2070
|
||||
RhaetoRomanic = 1047
|
||||
Romanian = 1048
|
||||
Russian = 1049
|
||||
SerboCroatianCyrillic = 2074
|
||||
SimplifiedChinese = 2052
|
||||
Slovak = 1051
|
||||
SpanishCastilian = 1034
|
||||
SpanishMexican = 2058
|
||||
Swedish = 1053
|
||||
SwissFrench = 4108
|
||||
SwissGerman = 2055
|
||||
SwissItalian = 2064
|
||||
Thai = 1054
|
||||
TraditionalChinese = 1028
|
||||
Turkish = 1055
|
||||
Urdu = 1056
|
||||
SesothoSotho = 1072
|
||||
Afrikaans = 1078
|
||||
Zulu = 1077
|
||||
Xhosa = 1076
|
||||
Venda = 1075
|
||||
Tswana = 1074
|
||||
Tsonga = 1073
|
||||
FarsiPersian = 1065
|
||||
|
||||
Codes = [ 1024,
|
||||
1052,
|
||||
1025,
|
||||
1057,
|
||||
2067,
|
||||
2060,
|
||||
1046,
|
||||
1026,
|
||||
1027,
|
||||
1050,
|
||||
1029,
|
||||
1030,
|
||||
1043,
|
||||
3081,
|
||||
2057,
|
||||
1033,
|
||||
1035,
|
||||
1036,
|
||||
3084,
|
||||
1031,
|
||||
1032,
|
||||
1037,
|
||||
1038,
|
||||
1039,
|
||||
1040,
|
||||
1041,
|
||||
1042,
|
||||
1044,
|
||||
2068,
|
||||
1045,
|
||||
2070,
|
||||
1047,
|
||||
1048,
|
||||
1049,
|
||||
2074,
|
||||
2052,
|
||||
1051,
|
||||
1034,
|
||||
2058,
|
||||
1053,
|
||||
4108,
|
||||
2055,
|
||||
2064,
|
||||
1054,
|
||||
1028,
|
||||
1055,
|
||||
1056,
|
||||
1072,
|
||||
1078,
|
||||
1077,
|
||||
1076,
|
||||
1075,
|
||||
1074,
|
||||
1073,
|
||||
1065 ]
|
||||
|
||||
# make it Australian as that is what I use most of the time
|
||||
DEFAULT = EnglishAustralian
|
||||
|
||||
def _IsValid( cls, value ) :
|
||||
return value in cls.Codes
|
||||
IsValid = classmethod( _IsValid )
|
||||
|
||||
if __name__ == '__main__' :
|
||||
PrintHexTable()
|
||||
|
||||
@@ -0,0 +1,757 @@
|
||||
from types import IntType, FloatType, LongType, StringTypes
|
||||
from copy import deepcopy
|
||||
from binascii import hexlify
|
||||
|
||||
from Constants import *
|
||||
from Styles import *
|
||||
|
||||
class UnhandledParamError( Exception ) :
|
||||
def __init__( self, param ) :
|
||||
Exception.__init__( self, "Don't know what to do with param %s" % param )
|
||||
|
||||
# red green blue
|
||||
StandardColours = Colours()
|
||||
StandardColours.append( Colour( 'Black', 0, 0, 0 ) )
|
||||
StandardColours.append( Colour( 'Blue', 0, 0, 255 ) )
|
||||
StandardColours.append( Colour( 'Turquoise', 0, 255, 255 ) )
|
||||
StandardColours.append( Colour( 'Green', 0, 255, 0 ) )
|
||||
StandardColours.append( Colour( 'Pink', 255, 0, 255 ) )
|
||||
StandardColours.append( Colour( 'Red', 255, 0, 0 ) )
|
||||
StandardColours.append( Colour( 'Yellow', 255, 255, 0 ) )
|
||||
StandardColours.append( Colour( 'White', 255, 255, 255 ) )
|
||||
StandardColours.append( Colour( 'Blue Dark', 0, 0, 128 ) )
|
||||
StandardColours.append( Colour( 'Teal', 0, 128, 128 ) )
|
||||
StandardColours.append( Colour( 'Green Dark', 0, 128, 0 ) )
|
||||
StandardColours.append( Colour( 'Violet', 128, 0, 128 ) )
|
||||
StandardColours.append( Colour( 'Red Dark', 128, 0, 0 ) )
|
||||
StandardColours.append( Colour( 'Yellow Dark', 128, 128, 0 ) )
|
||||
StandardColours.append( Colour( 'Grey Dark', 128, 128, 128 ) )
|
||||
StandardColours.append( Colour( 'Grey', 192, 192, 192 ) )
|
||||
|
||||
StandardFonts = Fonts()
|
||||
StandardFonts.append( Font( 'Arial' , 'swiss' , 0, 2, '020b0604020202020204' ) )
|
||||
StandardFonts.append( Font( 'Arial Black' , 'swiss' , 0, 2, '020b0a04020102020204' ) )
|
||||
StandardFonts.append( Font( 'Arial Narrow' , 'swiss' , 0, 2, '020b0506020202030204' ) )
|
||||
StandardFonts.append( Font( 'Bitstream Vera Sans Mono', 'modern', 0, 1, '020b0609030804020204' ) )
|
||||
StandardFonts.append( Font( 'Bitstream Vera Sans' , 'swiss' , 0, 2, '020b0603030804020204' ) )
|
||||
StandardFonts.append( Font( 'Bitstream Vera Serif' , 'roman' , 0, 2, '02060603050605020204' ) )
|
||||
StandardFonts.append( Font( 'Book Antiqua' , 'roman' , 0, 2, '02040602050305030304' ) )
|
||||
StandardFonts.append( Font( 'Bookman Old Style' , 'roman' , 0, 2, '02050604050505020204' ) )
|
||||
StandardFonts.append( Font( 'Castellar' , 'roman' , 0, 2, '020a0402060406010301' ) )
|
||||
StandardFonts.append( Font( 'Century Gothic' , 'swiss' , 0, 2, '020b0502020202020204' ) )
|
||||
StandardFonts.append( Font( 'Comic Sans MS' , 'script', 0, 2, '030f0702030302020204' ) )
|
||||
StandardFonts.append( Font( 'Courier New' , 'modern', 0, 1, '02070309020205020404' ) )
|
||||
StandardFonts.append( Font( 'Franklin Gothic Medium' , 'swiss' , 0, 2, '020b0603020102020204' ) )
|
||||
StandardFonts.append( Font( 'Garamond' , 'roman' , 0, 2, '02020404030301010803' ) )
|
||||
StandardFonts.append( Font( 'Georgia' , 'roman' , 0, 2, '02040502050405020303' ) )
|
||||
StandardFonts.append( Font( 'Haettenschweiler' , 'swiss' , 0, 2, '020b0706040902060204' ) )
|
||||
StandardFonts.append( Font( 'Impact' , 'swiss' , 0, 2, '020b0806030902050204' ) )
|
||||
StandardFonts.append( Font( 'Lucida Console' , 'modern', 0, 1, '020b0609040504020204' ) )
|
||||
StandardFonts.append( Font( 'Lucida Sans Unicode' , 'swiss' , 0, 2, '020b0602030504020204' ) )
|
||||
StandardFonts.append( Font( 'Microsoft Sans Serif' , 'swiss' , 0, 2, '020b0604020202020204' ) )
|
||||
StandardFonts.append( Font( 'Monotype Corsiva' , 'script', 0, 2, '03010101010201010101' ) )
|
||||
StandardFonts.append( Font( 'Palatino Linotype' , 'roman' , 0, 2, '02040502050505030304' ) )
|
||||
StandardFonts.append( Font( 'Papyrus' , 'script', 0, 2, '03070502060502030205' ) )
|
||||
StandardFonts.append( Font( 'Sylfaen' , 'roman' , 0, 2, '010a0502050306030303' ) )
|
||||
StandardFonts.append( Font( 'Symbol' , 'roman' , 2, 2, '05050102010706020507' ) )
|
||||
StandardFonts.append( Font( 'Tahoma' , 'swiss' , 0, 2, '020b0604030504040204' ) )
|
||||
StandardFonts.append( Font( 'Times New Roman' , 'roman' , 0, 2, '02020603050405020304' ) )
|
||||
StandardFonts.append( Font( 'Trebuchet MS' , 'swiss' , 0, 2, '020b0603020202020204' ) )
|
||||
StandardFonts.append( Font( 'Verdana' , 'swiss' , 0, 2, '020b0604030504040204' ) )
|
||||
|
||||
StandardFonts.Castellar.SetAlternate( StandardFonts.Georgia )
|
||||
|
||||
"""
|
||||
Found the following definition at http://www.pbdr.com/vbtips/gen/convtwip.htm
|
||||
|
||||
Twips are screen-independent units used to ensure that the placement and
|
||||
proportion of screen elements in your screen application are the same on all
|
||||
display systems. A twip is a unit of screen measurement equal to 1/20 of a
|
||||
printer's point. The conversion between twips and
|
||||
inches/centimeters/millimeters is as follows:
|
||||
|
||||
There are approximately 1440 twips to a inch (the length of a screen item
|
||||
measuring one inch when printed).
|
||||
|
||||
As there are 2.54 centimeters to 1 inch, then there are approximately 567
|
||||
twips to a centimeter (the length of a screen item measuring one centimeter
|
||||
when printed).
|
||||
|
||||
Or in millimeters, as there are 25.4 millimeters to 1 inch, therefore there
|
||||
are approximately 56.7 twips to a millimeter (the length of a screen item
|
||||
measuring one millimeter when printed)."""
|
||||
|
||||
# Width default is 12240, Height default is 15840
|
||||
StandardPaper = Papers()
|
||||
StandardPaper.append( Paper( 'LETTER' , 1, 'Letter 8 1/2 x 11 in' , 12240, 15840 ) )
|
||||
StandardPaper.append( Paper( 'LETTERSMALL' , 2, 'Letter Small 8 1/2 x 11 in' , 12240, 15840 ) )
|
||||
StandardPaper.append( Paper( 'TABLOID' , 3, 'Tabloid 11 x 17 in' , 15840, 24480 ) )
|
||||
StandardPaper.append( Paper( 'LEDGER' , 4, 'Ledger 17 x 11 in' , 24480, 15840 ) )
|
||||
StandardPaper.append( Paper( 'LEGAL' , 5, 'Legal 8 1/2 x 14 in' , 12240, 20160 ) )
|
||||
StandardPaper.append( Paper( 'STATEMENT' , 6, 'Statement 5 1/2 x 8 1/2 in' , 7920, 12240 ) )
|
||||
StandardPaper.append( Paper( 'EXECUTIVE' , 7, 'Executive 7 1/4 x 10 1/2 in' , 10440, 15120 ) )
|
||||
StandardPaper.append( Paper( 'A3' , 8, 'A3 297 x 420 mm' , 16838, 23811 ) )
|
||||
StandardPaper.append( Paper( 'A4' , 9, 'A4 210 x 297 mm' , 11907, 16838 ) )
|
||||
StandardPaper.append( Paper( 'A4SMALL' , 10, 'A4 Small 210 x 297 mm' , 11907, 16838 ) )
|
||||
StandardPaper.append( Paper( 'A5' , 11, 'A5 148 x 210 mm' , 8391, 11907 ) )
|
||||
StandardPaper.append( Paper( 'B4' , 12, 'B4 (JIS) 250 x 354' , 14175, 20072 ) )
|
||||
StandardPaper.append( Paper( 'B5' , 13, 'B5 (JIS) 182 x 257 mm' , 10319, 14572 ) )
|
||||
StandardPaper.append( Paper( 'FOLIO' , 14, 'Folio 8 1/2 x 13 in' , 12240, 18720 ) )
|
||||
StandardPaper.append( Paper( 'QUARTO' , 15, 'Quarto 215 x 275 mm' , 12191, 15593 ) )
|
||||
StandardPaper.append( Paper( '10X14' , 16, '10x14 in' , 14400, 20160 ) )
|
||||
StandardPaper.append( Paper( '11X17' , 17, '11x17 in' , 15840, 24480 ) )
|
||||
StandardPaper.append( Paper( 'NOTE' , 18, 'Note 8 1/2 x 11 in' , 12240, 15840 ) )
|
||||
StandardPaper.append( Paper( 'ENV_9' , 19, 'Envelope #9 3 7/8 x 8 7/8' , 5580, 12780 ) )
|
||||
StandardPaper.append( Paper( 'ENV_10' , 20, 'Envelope #10 4 1/8 x 9 1/2' , 5940, 13680 ) )
|
||||
StandardPaper.append( Paper( 'ENV_11' , 21, 'Envelope #11 4 1/2 x 10 3/8' , 6480, 14940 ) )
|
||||
StandardPaper.append( Paper( 'ENV_12' , 22, 'Envelope #12 4 3/4 x 11' , 6840, 15840 ) )
|
||||
StandardPaper.append( Paper( 'ENV_14' , 23, 'Envelope #14 5 x 11 1/2' , 7200, 16560 ) )
|
||||
StandardPaper.append( Paper( 'CSHEET' , 24, 'C size sheet 18 x 24 in' , 29520, 34560 ) )
|
||||
StandardPaper.append( Paper( 'DSHEET' , 25, 'D size sheet 22 x 34 in' , 31680, 48960 ) )
|
||||
StandardPaper.append( Paper( 'ESHEET' , 26, 'E size sheet 34 x 44 in' , 48960, 63360 ) )
|
||||
StandardPaper.append( Paper( 'ENV_DL' , 27, 'Envelope DL 110 x 220mm' , 6237, 12474 ) )
|
||||
StandardPaper.append( Paper( 'ENV_C5' , 28, 'Envelope C5 162 x 229 mm' , 9185, 12984 ) )
|
||||
StandardPaper.append( Paper( 'ENV_C3' , 29, 'Envelope C3 324 x 458 mm' , 18371, 25969 ) )
|
||||
StandardPaper.append( Paper( 'ENV_C4' , 30, 'Envelope C4 229 x 324 mm' , 12984, 18371 ) )
|
||||
StandardPaper.append( Paper( 'ENV_C6' , 31, 'Envelope C6 114 x 162 mm' , 6464, 9185 ) )
|
||||
StandardPaper.append( Paper( 'ENV_C65' , 32, 'Envelope C65 114 x 229 mm' , 6464, 12984 ) )
|
||||
StandardPaper.append( Paper( 'ENV_B4' , 33, 'Envelope B4 250 x 353 mm' , 14175, 20015 ) )
|
||||
StandardPaper.append( Paper( 'ENV_B5' , 34, 'Envelope B5 176 x 250 mm' , 9979, 14175 ) )
|
||||
StandardPaper.append( Paper( 'ENV_B6' , 35, 'Envelope B6 176 x 125 mm' , 9979, 7088 ) )
|
||||
StandardPaper.append( Paper( 'ENV_ITALY' , 36, 'Envelope 110 x 230 mm' , 6237, 13041 ) )
|
||||
StandardPaper.append( Paper( 'ENV_MONARCH' , 37, 'Envelope Monarch 3.875 x 7.5 in' , 5580, 10800 ) )
|
||||
StandardPaper.append( Paper( 'ENV_PERSONAL' , 38, '6 3/4 Envelope 3 5/8 x 6 1/2 in' , 5220, 9360 ) )
|
||||
StandardPaper.append( Paper( 'FANFOLD_US' , 39, 'US Std Fanfold 14 7/8 x 11 in' , 21420, 15840 ) )
|
||||
StandardPaper.append( Paper( 'FANFOLD_STD_GERMAN' , 40, 'German Std Fanfold 8 1/2 x 12 in' , 12240, 17280 ) )
|
||||
StandardPaper.append( Paper( 'FANFOLD_LGL_GERMAN' , 41, 'German Legal Fanfold 8 1/2 x 13 in' , 12240, 18720 ) )
|
||||
|
||||
#
|
||||
# Finally a StyleSheet in which all of this stuff is put together
|
||||
#
|
||||
class StyleSheet :
|
||||
def __init__( self, colours=None, fonts=None ) :
|
||||
|
||||
self.Colours = colours or deepcopy( StandardColours )
|
||||
self.Fonts = fonts or deepcopy( StandardFonts )
|
||||
|
||||
self.TextStyles = AttributedList()
|
||||
self.ParagraphStyles = AttributedList()
|
||||
|
||||
class Section( list ) :
|
||||
NONE = 1
|
||||
COLUMN = 2
|
||||
PAGE = 3
|
||||
EVEN = 4
|
||||
ODD = 5
|
||||
BREAK_TYPES = [ NONE, COLUMN, PAGE, EVEN, ODD ]
|
||||
|
||||
def __init__( self, paper=None, margins=None, break_type=None, headery=None, footery=None, landscape=None, first_page_number=None ) :
|
||||
super( Section, self ).__init__()
|
||||
|
||||
self.Paper = paper or StandardPaper.A4
|
||||
self.SetMargins( margins )
|
||||
|
||||
self.Header = []
|
||||
self.Footer = []
|
||||
self.FirstHeader = []
|
||||
self.FirstFooter = []
|
||||
|
||||
self.SetBreakType( break_type or self.NONE )
|
||||
self.SetHeaderY( headery )
|
||||
self.SetFooterY( footery )
|
||||
self.SetLandscape( landscape )
|
||||
self.SetFirstPageNumber( first_page_number )
|
||||
|
||||
def TwipsToRightMargin( self ) :
|
||||
return self.Paper.Width - ( self.Margins.Left + self.Margins.Right )
|
||||
|
||||
def SetMargins( self, value ) :
|
||||
self.Margins = value or MarginsPropertySet( top=1000, left=1200, bottom=1000, right=1200 )
|
||||
self.Width = self.Paper.Width - ( self.Margins.Left + self.Margins.Right )
|
||||
|
||||
def SetBreakType( self, value ) :
|
||||
assert value in self.BREAK_TYPES
|
||||
self.BreakType = value
|
||||
return self
|
||||
|
||||
def SetHeaderY( self, value ) :
|
||||
self.HeaderY = value
|
||||
return self
|
||||
|
||||
def SetFooterY( self, value ) :
|
||||
self.FooterY = value
|
||||
return self
|
||||
|
||||
def SetLandscape( self, value ) :
|
||||
self.Landscape = False
|
||||
if value : self.Landscape = True
|
||||
return self
|
||||
|
||||
def SetFirstPageNumber( self, value ) :
|
||||
self.FirstPageNumber = value
|
||||
return self
|
||||
|
||||
def MakeDefaultStyleSheet( ) :
|
||||
result = StyleSheet()
|
||||
|
||||
NormalText = TextStyle( TextPropertySet( result.Fonts.Arial, 22 ) )
|
||||
|
||||
ps = ParagraphStyle( 'Normal',
|
||||
NormalText.Copy(),
|
||||
ParagraphPropertySet( space_before = 60,
|
||||
space_after = 60 ) )
|
||||
result.ParagraphStyles.append( ps )
|
||||
|
||||
ps = ParagraphStyle( 'Normal Short',
|
||||
NormalText.Copy() )
|
||||
result.ParagraphStyles.append( ps )
|
||||
|
||||
NormalText.TextPropertySet.SetSize( 32 )
|
||||
ps = ParagraphStyle( 'Heading 1',
|
||||
NormalText.Copy(),
|
||||
ParagraphPropertySet( space_before = 240,
|
||||
space_after = 60 ) )
|
||||
result.ParagraphStyles.append( ps )
|
||||
|
||||
NormalText.TextPropertySet.SetSize( 24 ).SetBold( True )
|
||||
ps = ParagraphStyle( 'Heading 2',
|
||||
NormalText.Copy(),
|
||||
ParagraphPropertySet( space_before = 240,
|
||||
space_after = 60 ) )
|
||||
result.ParagraphStyles.append( ps )
|
||||
|
||||
# Add some more in that are based on the normal template but that
|
||||
# have some indenting set that makes them suitable for doing numbered
|
||||
normal_numbered = result.ParagraphStyles.Normal.Copy()
|
||||
normal_numbered.SetName( 'Normal Numbered' )
|
||||
normal_numbered.ParagraphPropertySet.SetFirstLineIndent( TabPropertySet.DEFAULT_WIDTH * -1 )
|
||||
normal_numbered.ParagraphPropertySet.SetLeftIndent ( TabPropertySet.DEFAULT_WIDTH )
|
||||
|
||||
result.ParagraphStyles.append( normal_numbered )
|
||||
|
||||
normal_numbered2 = result.ParagraphStyles.Normal.Copy()
|
||||
normal_numbered2.SetName( 'Normal Numbered 2' )
|
||||
normal_numbered2.ParagraphPropertySet.SetFirstLineIndent( TabPropertySet.DEFAULT_WIDTH * -1 )
|
||||
normal_numbered2.ParagraphPropertySet.SetLeftIndent ( TabPropertySet.DEFAULT_WIDTH * 2 )
|
||||
|
||||
result.ParagraphStyles.append( normal_numbered2 )
|
||||
|
||||
## LIST STYLES
|
||||
for idx, indent in [ (1, TabPS.DEFAULT_WIDTH ),
|
||||
(2, TabPS.DEFAULT_WIDTH * 2),
|
||||
(3, TabPS.DEFAULT_WIDTH * 3) ] :
|
||||
indent = TabPropertySet.DEFAULT_WIDTH
|
||||
ps = ParagraphStyle( 'List %s' % idx,
|
||||
TextStyle( TextPropertySet( result.Fonts.Arial, 22 ) ),
|
||||
ParagraphPropertySet( space_before = 60,
|
||||
space_after = 60,
|
||||
first_line_indent = -indent,
|
||||
left_indent = indent) )
|
||||
result.ParagraphStyles.append( ps )
|
||||
|
||||
return result
|
||||
|
||||
class TAB : pass
|
||||
class LINE : pass
|
||||
|
||||
class RawCode :
|
||||
def __init__( self, data ) :
|
||||
self.Data = data
|
||||
|
||||
PAGE_NUMBER = RawCode( r'{\field{\fldinst page}}' )
|
||||
TOTAL_PAGES = RawCode( r'{\field{\fldinst numpages}}' )
|
||||
SECTION_PAGES = RawCode( r'{\field{\fldinst sectionpages}}' )
|
||||
ARIAL_BULLET = RawCode( r'{\f2\'95}' )
|
||||
|
||||
def _get_jpg_dimensions( fin ):
|
||||
"""
|
||||
converted from: http://dev.w3.org/cvsweb/Amaya/libjpeg/rdjpgcom.c?rev=1.2
|
||||
"""
|
||||
|
||||
M_SOF0 = chr( 0xC0 ) # /* Start Of Frame N */
|
||||
M_SOF1 = chr( 0xC1 ) # /* N indicates which compression process */
|
||||
M_SOF2 = chr( 0xC2 ) # /* Only SOF0-SOF2 are now in common use */
|
||||
M_SOF3 = chr( 0xC3 ) #
|
||||
M_SOF5 = chr( 0xC5 ) # /* NB: codes C4 and CC are NOT SOF markers */
|
||||
M_SOF6 = chr( 0xC6 ) #
|
||||
M_SOF7 = chr( 0xC7 ) #
|
||||
M_SOF9 = chr( 0xC9 ) #
|
||||
M_SOF10 = chr( 0xCA ) #
|
||||
M_SOF11 = chr( 0xCB ) #
|
||||
M_SOF13 = chr( 0xCD ) #
|
||||
M_SOF14 = chr( 0xCE ) #
|
||||
M_SOF15 = chr( 0xCF ) #
|
||||
M_SOI = chr( 0xD8 ) # /* Start Of Image (beginning of datastream) */
|
||||
M_EOI = chr( 0xD9 ) # /* End Of Image (end of datastream) */
|
||||
|
||||
M_FF = chr( 0xFF )
|
||||
|
||||
MARKERS = [ M_SOF0, M_SOF1, M_SOF2, M_SOF3,
|
||||
M_SOF5, M_SOF6, M_SOF7, M_SOF9,
|
||||
M_SOF10,M_SOF11, M_SOF13, M_SOF14,
|
||||
M_SOF15 ]
|
||||
|
||||
def get_length() :
|
||||
b1 = fin.read( 1 )
|
||||
b2 = fin.read( 1 )
|
||||
return (ord(b1) << 8) + ord(b2)
|
||||
|
||||
def next_marker() :
|
||||
# markers come straight after an 0xFF so skip everything
|
||||
# up to the first 0xFF that we find
|
||||
while fin.read(1) != M_FF :
|
||||
pass
|
||||
|
||||
# there can be more than one 0xFF as they can be used
|
||||
# for padding so we are now looking for the first byte
|
||||
# that isn't an 0xFF, this will be the marker
|
||||
while True :
|
||||
result = fin.read(1)
|
||||
if result != M_FF :
|
||||
return result
|
||||
|
||||
raise Exception( 'Invalid JPEG' )
|
||||
|
||||
# BODY OF THE FUNCTION
|
||||
if not ((fin.read(1) == M_FF) and (fin.read(1) == M_SOI)) :
|
||||
raise Exception( 'Invalid Jpeg' )
|
||||
|
||||
while True :
|
||||
marker = next_marker()
|
||||
|
||||
# the marker is always followed by two bytes representing the length of the data field
|
||||
length = get_length ()
|
||||
if length < 2 : raise Exception( "Erroneous JPEG marker length" )
|
||||
|
||||
# if it is a compression process marker then it will contain the dimension of the image
|
||||
if marker in MARKERS :
|
||||
# the next byte is the data precision, just skip it
|
||||
fin.read(1)
|
||||
|
||||
# bingo
|
||||
image_height = get_length()
|
||||
image_width = get_length()
|
||||
return image_width, image_height
|
||||
|
||||
# just skip whatever data it contains
|
||||
fin.read( length - 2 )
|
||||
|
||||
raise Exception( 'Invalid JPEG, end of stream reached' )
|
||||
|
||||
|
||||
_PNG_HEADER = '\x89\x50\x4e'
|
||||
def _get_png_dimensions( data ) :
|
||||
if data[0:3] != _PNG_HEADER :
|
||||
raise Exception( 'Invalid PNG image' )
|
||||
|
||||
width = (ord(data[18]) * 256) + (ord(data[19]))
|
||||
height = (ord(data[22]) * 256) + (ord(data[23]))
|
||||
return width, height
|
||||
|
||||
def _get_emf_dimensions( fin ):
|
||||
import struct
|
||||
def get_DWORD():
|
||||
return struct.unpack("<L",fin.read(4))[0]
|
||||
def get_LONG():
|
||||
return struct.unpack("<l",fin.read(4))[0]
|
||||
def get_WORD():
|
||||
return struct.unpack("<H",fin.read(2))[0]
|
||||
class Empty:
|
||||
pass
|
||||
header = Empty()
|
||||
header.RecordType = get_DWORD() # Record type
|
||||
header.RecordSize = get_DWORD() # Size of the record in bytes
|
||||
header.BoundsLeft = get_LONG() # Left inclusive bounds
|
||||
header.BoundsTop = get_LONG() # Top inclusive bounds
|
||||
header.BoundsRight = get_LONG() # Right inclusive bounds
|
||||
header.BoundsBottom = get_LONG() # Bottom inclusive bounds
|
||||
header.FrameLeft = get_LONG() # Left side of inclusive picture frame
|
||||
header.FrameTop = get_LONG() # Top side of inclusive picture frame
|
||||
header.FrameRight = get_LONG() # Right side of inclusive picture frame
|
||||
header.FrameBottom = get_LONG() # Bottom side of inclusive picture frame
|
||||
header.Signature = get_DWORD() # Signature ID (always 0x464D4520)
|
||||
header.Version = get_DWORD() # Version of the metafile
|
||||
header.Size = get_DWORD() # Size of the metafile in bytes
|
||||
header.NumOfRecords = get_DWORD() # Number of records in the metafile
|
||||
header.NumOfHandles = get_WORD() # Number of handles in the handle table
|
||||
header.Reserved = get_WORD() # Not used (always 0)
|
||||
header.SizeOfDescrip = get_DWORD() # Size of description string in WORDs
|
||||
header.OffsOfDescrip = get_DWORD() # Offset of description string in metafile
|
||||
header.NumPalEntries = get_DWORD() # Number of color palette entries
|
||||
header.WidthDevPixels = get_LONG() # Width of reference device in pixels
|
||||
header.HeightDevPixels = get_LONG() # Height of reference device in pixels
|
||||
header.WidthDevMM = get_LONG() # Width of reference device in millimeters
|
||||
header.HeightDevMM = get_LONG() # Height of reference device in millimeters
|
||||
|
||||
if 0:
|
||||
klist = header.__dict__.keys()
|
||||
klist.sort()
|
||||
for k in klist:
|
||||
print "%20s:%s" % (k,header.__dict__[k])
|
||||
|
||||
dw = header.FrameRight-header.FrameLeft
|
||||
dh = header.FrameBottom-header.FrameTop
|
||||
|
||||
# convert from 0.01mm units to 1/72in units
|
||||
return int(dw * 72.0/2540.0), int(dh * 72.0/2540.0)
|
||||
|
||||
class Image( RawCode ) :
|
||||
|
||||
# Need to add in the width and height in twips as it crashes
|
||||
# word xp with these values. Still working out the most
|
||||
# efficient way of getting these values.
|
||||
# \picscalex100\picscaley100\piccropl0\piccropr0\piccropt0\piccropb0
|
||||
# picwgoal900\pichgoal281
|
||||
|
||||
PNG_LIB = 'pngblip'
|
||||
JPG_LIB = 'jpegblip'
|
||||
EMF_LIB = 'emfblip'
|
||||
PICT_TYPES = { 'png' : PNG_LIB,
|
||||
'jpg' : JPG_LIB,
|
||||
'emf' : EMF_LIB}
|
||||
|
||||
def __init__( self, infile, **kwargs ) :
|
||||
|
||||
if hasattr( infile, 'read' ):
|
||||
fin = infile
|
||||
if 'datatype' not in kwargs.keys():
|
||||
msg = "If passing in a file object, you must also specify type='xxx' where xxx is one of %s" % self.PICT_TYPES.keys()
|
||||
raise ValueError,msg
|
||||
file_name = kwargs.pop('datatype')
|
||||
else:
|
||||
fin = file( infile, 'rb' )
|
||||
file_name = infile
|
||||
|
||||
pict_type = self.PICT_TYPES[ file_name[ -3 : ].lower() ]
|
||||
if pict_type == self.PNG_LIB :
|
||||
width, height = _get_png_dimensions( fin.read( 100 ) )
|
||||
elif pict_type == self.JPG_LIB :
|
||||
width, height = _get_jpg_dimensions( fin )
|
||||
elif pict_type == self.EMF_LIB :
|
||||
width, height = _get_emf_dimensions( fin )
|
||||
|
||||
|
||||
# if user specified height or width but not both, then
|
||||
# scale unspecified dimension to maintain aspect ratio
|
||||
|
||||
if ('width' in kwargs) and ('height' not in kwargs):
|
||||
height = int(height * float(kwargs['width'])/width)
|
||||
elif ('height' in kwargs) and ('width' not in kwargs):
|
||||
width = int(width * float(kwargs['height'])/height)
|
||||
|
||||
width = kwargs.pop('width',width)
|
||||
height = kwargs.pop('height', height)
|
||||
|
||||
codes = [ pict_type,
|
||||
'picwgoal%s' % (width * 20),
|
||||
'pichgoal%s' % (height * 20) ]
|
||||
# let user specify global scaling
|
||||
scale = kwargs.pop('scale',100)
|
||||
|
||||
for kwarg, code, default in [ ( 'scale_x', 'scalex', scale ),
|
||||
( 'scale_y', 'scaley', scale ),
|
||||
( 'crop_left', 'cropl', '0' ),
|
||||
( 'crop_right', 'cropr', '0' ),
|
||||
( 'crop_top', 'cropt', '0' ),
|
||||
( 'crop_bottom', 'cropb', '0' ) ] :
|
||||
codes.append( 'pic%s%s' % ( code, kwargs.pop( kwarg, default ) ) )
|
||||
|
||||
|
||||
# reset back to the start of the file to get all of it and now
|
||||
# turn it into hex.
|
||||
fin.seek( 0, 0 )
|
||||
image = hexlify( fin.read() )
|
||||
fin.close()
|
||||
data = []
|
||||
for i in range( 0, len( image ), 128 ) :
|
||||
data.append( image[ i : i + 128 ] )
|
||||
|
||||
data = r'{\pict{\%s}%s}' % ( '\\'.join( codes ), '\n'.join( data ) )
|
||||
RawCode.__init__( self, data )
|
||||
|
||||
def ToRawCode( self, var_name ) :
|
||||
return '%s = RawCode( """%s""" )' % ( var_name, self.Data )
|
||||
|
||||
class Text :
|
||||
def __init__( self, *params ) :
|
||||
self.Data = None
|
||||
self.Style = None
|
||||
self.Properties = None
|
||||
self.Shading = None
|
||||
|
||||
for param in params :
|
||||
if isinstance( param, TextStyle ) : self.Style = param
|
||||
elif isinstance( param, TextPS ) : self.Properties = param
|
||||
elif isinstance( param, ShadingPS ) : self.Shading = param
|
||||
else :
|
||||
# otherwise let the rendering custom handler sort it out itself
|
||||
self.Data = param
|
||||
|
||||
def SetData( self, value ) :
|
||||
self.Data = value
|
||||
|
||||
class Inline( list ) :
|
||||
def __init__( self, *params ) :
|
||||
super( Inline, self ).__init__()
|
||||
|
||||
self.Style = None
|
||||
self.Properties = None
|
||||
self.Shading = None
|
||||
|
||||
self._append = super( Inline, self ).append
|
||||
|
||||
for param in params :
|
||||
if isinstance( param, TextStyle ) : self.Style = param
|
||||
elif isinstance( param, TextPS ) : self.Properties = param
|
||||
elif isinstance( param, ShadingPS ) : self.Shading = param
|
||||
else :
|
||||
# otherwise we add to it to our list of elements and let
|
||||
# the rendering custom handler sort it out itself.
|
||||
self.append( param )
|
||||
|
||||
def append( self, *params ) :
|
||||
# filter out any that are explicitly None
|
||||
[ self._append( param ) for param in params if param is not None ]
|
||||
|
||||
class Paragraph( list ) :
|
||||
def __init__( self, *params ) :
|
||||
super( Paragraph, self ).__init__()
|
||||
|
||||
self.Style = None
|
||||
self.Properties = None
|
||||
self.Frame = None
|
||||
self.Shading = None
|
||||
|
||||
self._append = super( Paragraph, self ).append
|
||||
|
||||
for param in params :
|
||||
if isinstance( param, ParagraphStyle ) : self.Style = param
|
||||
elif isinstance( param, ParagraphPS ) : self.Properties = param
|
||||
elif isinstance( param, FramePS ) : self.Frame = param
|
||||
elif isinstance( param, ShadingPS ) : self.Shading = param
|
||||
else :
|
||||
# otherwise we add to it to our list of elements and let
|
||||
# the rendering custom handler sort it out itself.
|
||||
self.append( param )
|
||||
|
||||
def append( self, *params ) :
|
||||
# filter out any that are explicitly None
|
||||
[ self._append( param ) for param in params if param is not None ]
|
||||
|
||||
def insert( self, index, value ) :
|
||||
if value is not None :
|
||||
super( Paragraph, self ).insert( index, value )
|
||||
|
||||
class Table :
|
||||
LEFT = 1
|
||||
RIGHT = 2
|
||||
CENTER = 3
|
||||
ALIGNMENT = [ LEFT, RIGHT, CENTER ]
|
||||
|
||||
NO_WRAPPING = 1
|
||||
WRAP_AROUND = 2
|
||||
WRAPPING = [ NO_WRAPPING, WRAP_AROUND ]
|
||||
|
||||
# trrh height of row, 0 means automatically adjust, use negative for an absolute
|
||||
# trgaph is half of the space between a table cell in width, reduce this one
|
||||
# to get a really tiny column
|
||||
|
||||
def __init__( self, *column_widths, **kwargs ) :
|
||||
|
||||
self.Rows = []
|
||||
|
||||
self.SetAlignment ( kwargs.pop( 'alignment', self.LEFT ) )
|
||||
self.SetLeftOffset ( kwargs.pop( 'left_offset', None ) )
|
||||
self.SetGapBetweenCells( kwargs.pop( 'gap_between_cells', None ) )
|
||||
self.SetColumnWidths ( *column_widths )
|
||||
|
||||
assert not kwargs, 'invalid keyword args %s' % kwargs
|
||||
|
||||
def SetAlignment( self, value ) :
|
||||
assert value is None or value in self.ALIGNMENT
|
||||
self.Alignment = value or self.LEFT
|
||||
return self
|
||||
|
||||
def SetLeftOffset( self, value ) :
|
||||
self.LeftOffset = value
|
||||
return self
|
||||
|
||||
def SetGapBetweenCells( self, value ) :
|
||||
self.GapBetweenCells = value
|
||||
return self
|
||||
|
||||
def SetColumnWidths( self, *column_widths ) :
|
||||
self.ColumnWidths = column_widths
|
||||
self.ColumnCount = len( column_widths )
|
||||
return self
|
||||
|
||||
def AddRow( self, *cells ) :
|
||||
height = None
|
||||
if isinstance( cells[ 0 ], (IntType, FloatType, LongType) ):
|
||||
height = int( cells[ 0 ] )
|
||||
cells = cells[ 1 : ]
|
||||
|
||||
# make sure all of the spans add up to the number of columns
|
||||
# otherwise the table will get corrupted
|
||||
if self.ColumnCount != sum( [ cell.Span for cell in cells ] ) :
|
||||
raise Exception( 'ColumnCount != the total of this row\'s cell.Spans.' )
|
||||
|
||||
self.Rows.append( ( height, cells ) )
|
||||
|
||||
append = AddRow
|
||||
|
||||
class Cell( list ) :
|
||||
|
||||
"""
|
||||
\clvertalt Text is top-aligned in cell (the default).
|
||||
\clvertalc Text is centered vertically in cell.
|
||||
\clvertalb Text is bottom-aligned in cell.
|
||||
\cltxlrtb Vertical text aligned left (direction bottom up).
|
||||
\cltxtbrl Vertical text aligned right (direction top down).
|
||||
"""
|
||||
|
||||
ALIGN_TOP = 1
|
||||
ALIGN_CENTER = 2
|
||||
ALIGN_BOTTOM = 3
|
||||
|
||||
FLOW_LR_TB = 1
|
||||
FLOW_RL_TB = 2
|
||||
FLOW_LR_BT = 3
|
||||
FLOW_VERTICAL_LR_TB = 4
|
||||
FLOW_VERTICAL_TB_RL = 5
|
||||
|
||||
def __init__( self, *params, **kwargs ) :
|
||||
super( Cell, self ).__init__()
|
||||
|
||||
self.SetFrame ( None )
|
||||
self.SetMargins( None )
|
||||
|
||||
self.SetAlignment( kwargs.get( 'alignment', self.ALIGN_TOP ) )
|
||||
self.SetFlow ( kwargs.get( 'flow' , self.FLOW_LR_TB ) )
|
||||
self.SetSpan ( kwargs.get( 'span', 1 ) )
|
||||
|
||||
self.SetStartVerticalMerge( kwargs.get( 'start_vertical_merge', False ) )
|
||||
self.SetVerticalMerge ( kwargs.get( 'vertical_merge', False ) )
|
||||
|
||||
self._append = super( Cell, self ).append
|
||||
|
||||
for param in params :
|
||||
if isinstance( param, StringType ) : self.append ( param )
|
||||
elif isinstance( param, Paragraph ) : self.append ( param )
|
||||
elif isinstance( param, FramePS ) : self.SetFrame ( param )
|
||||
elif isinstance( param, MarginsPS ) : self.SetMargins( param )
|
||||
|
||||
def SetFrame( self, value ) :
|
||||
self.Frame = value
|
||||
return self
|
||||
|
||||
def SetMargins( self, value ) :
|
||||
self.Margins = value
|
||||
return self
|
||||
|
||||
def SetAlignment( self, value ) :
|
||||
assert value in [ self.ALIGN_TOP, self.ALIGN_CENTER, self.ALIGN_BOTTOM ] #, self.ALIGN_TEXT_TOP_DOWN, self.ALIGN_TEXT_BOTTOM_UP ]
|
||||
self.Alignment = value
|
||||
|
||||
def SetFlow( self, value ) :
|
||||
assert value in [ self.FLOW_LR_TB, self.FLOW_RL_TB, self.FLOW_LR_BT, self.FLOW_VERTICAL_LR_TB, self.FLOW_VERTICAL_TB_RL ]
|
||||
self.Flow = value
|
||||
|
||||
def SetSpan( self, value ) :
|
||||
# must be a positive integer
|
||||
self.Span = int( max( value, 1 ) )
|
||||
return self
|
||||
|
||||
def SetStartVerticalMerge( self, value ) :
|
||||
self.StartVerticalMerge = False
|
||||
if value :
|
||||
self.StartVerticalMerge = True
|
||||
return self
|
||||
|
||||
def SetVerticalMerge( self, value ) :
|
||||
self.VerticalMerge = False
|
||||
if value :
|
||||
self.VerticalMerge = True
|
||||
return self
|
||||
|
||||
def append( self, *params ) :
|
||||
[ self._append( param ) for param in params ]
|
||||
|
||||
class Document :
|
||||
def __init__( self, style_sheet=None, default_language=None, view_kind=None, view_zoom_kind=None, view_scale=None ) :
|
||||
self.StyleSheet = style_sheet or MakeDefaultStyleSheet()
|
||||
self.Sections = AttributedList( Section )
|
||||
|
||||
self.SetTitle( None )
|
||||
|
||||
self.DefaultLanguage = default_language or Languages.DEFAULT
|
||||
self.ViewKind = view_kind or ViewKind.DEFAULT
|
||||
self.ViewZoomKind = view_zoom_kind
|
||||
self.ViewScale = view_scale
|
||||
|
||||
def NewSection( self, *params, **kwargs ) :
|
||||
result = Section( *params, **kwargs )
|
||||
self.Sections.append( result )
|
||||
return result
|
||||
|
||||
def SetTitle( self, value ) :
|
||||
self.Title = value
|
||||
return self
|
||||
|
||||
def Copy( self ) :
|
||||
result = Document( style_sheet = self.StyleSheet.Copy(),
|
||||
default_language = self.DefaultLanguage,
|
||||
view_kind = self.ViewKind,
|
||||
view_zoom_kind = self.ViewZoomKind,
|
||||
view_scale = self.ViewScale )
|
||||
result.SetTitle( self.Title )
|
||||
result.Sections = self.Sections.Copy()
|
||||
|
||||
return result
|
||||
|
||||
def TEXT( *params, **kwargs ) :
|
||||
text_props = TextPropertySet()
|
||||
text_props.SetFont ( kwargs.get( 'font', None ) )
|
||||
text_props.SetSize ( kwargs.get( 'size', None ) )
|
||||
text_props.SetBold ( kwargs.get( 'bold', False ) )
|
||||
text_props.SetItalic ( kwargs.get( 'italic', False ) )
|
||||
text_props.SetUnderline( kwargs.get( 'underline', False ) )
|
||||
text_props.SetColour ( kwargs.get( 'colour', None ) )
|
||||
|
||||
if len( params ) == 1 :
|
||||
return Text( params[ 0 ], text_props )
|
||||
|
||||
result = Inline( text_props )
|
||||
apply( result.append, params )
|
||||
return result
|
||||
|
||||
def B( *params ) :
|
||||
text_props = TextPropertySet( bold=True )
|
||||
|
||||
if len( params ) == 1 :
|
||||
return Text( params[ 0 ], text_props )
|
||||
|
||||
result = Inline( text_props )
|
||||
apply( result.append, params )
|
||||
return result
|
||||
|
||||
def I( *params ) :
|
||||
text_props = TextPropertySet( italic=True )
|
||||
|
||||
if len( params ) == 1 :
|
||||
return Text( params[ 0 ], text_props )
|
||||
|
||||
result = Inline( text_props )
|
||||
apply( result.append, params )
|
||||
return result
|
||||
|
||||
def U( *params ) :
|
||||
text_props = TextPropertySet( underline=True )
|
||||
|
||||
if len( params ) == 1 :
|
||||
return Text( params[ 0 ], text_props )
|
||||
|
||||
result = Inline( text_props )
|
||||
apply( result.append, params )
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
"""
|
||||
PropertySets group common attributes together, each property set is used to control a specific part of the rendering.
|
||||
|
||||
PropertySets can be used in different elements of the document.
|
||||
|
||||
For example the FramePropertySet is used in paragraphs, tables, cells, etc.
|
||||
|
||||
The TextPropertySet can be used for text or in a Paragraph Style.
|
||||
|
||||
"""
|
||||
|
||||
from types import StringType
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
#
|
||||
# We need some basic Type like fonts, colours and paper definitions
|
||||
#
|
||||
def MakeAttributeName( value ) :
|
||||
assert value and type( value ) is StringType
|
||||
value = value.replace( ' ', '' )
|
||||
return value
|
||||
|
||||
class AttributedList( list ) :
|
||||
def __init__( self, accepted_type=None ) :
|
||||
super( AttributedList, self ).__init__()
|
||||
self.AcceptedType = accepted_type
|
||||
self._append = super( AttributedList, self ).append
|
||||
|
||||
def append( self, *values ) :
|
||||
for value in values :
|
||||
if self.AcceptedType : assert isinstance( value, self.AcceptedType )
|
||||
|
||||
self._append( value )
|
||||
|
||||
name = getattr( value, 'Name', None )
|
||||
if name :
|
||||
name = MakeAttributeName( value.Name )
|
||||
setattr( self, name, value )
|
||||
|
||||
def __deepcopy__( self, memo ) :
|
||||
result = self.__class__()
|
||||
result.append( *self[:] )
|
||||
return result
|
||||
|
||||
class Colour :
|
||||
def __init__( self, name, red, green, blue ) :
|
||||
self.SetName ( name )
|
||||
self.SetRed ( red )
|
||||
self.SetGreen( green )
|
||||
self.SetBlue ( blue )
|
||||
|
||||
def SetName( self, value ) :
|
||||
self.Name = value
|
||||
return self
|
||||
|
||||
def SetRed( self, value ) :
|
||||
self.Red = value
|
||||
return self
|
||||
|
||||
def SetGreen( self, value ) :
|
||||
self.Green = value
|
||||
return self
|
||||
|
||||
def SetBlue( self, value ) :
|
||||
self.Blue = value
|
||||
return self
|
||||
|
||||
class Colours( AttributedList ) :
|
||||
def __init__( self ) :
|
||||
super( Colours, self ).__init__( Colour )
|
||||
|
||||
class Font :
|
||||
def __init__( self, name, family, character_set = 0, pitch = None, panose = None, alternate = None ) :
|
||||
self.SetName ( name )
|
||||
self.SetFamily ( family )
|
||||
self.SetCharacterSet( character_set )
|
||||
self.SetPitch ( pitch )
|
||||
self.SetPanose ( panose )
|
||||
self.SetAlternate ( alternate )
|
||||
|
||||
def SetName( self, value ) :
|
||||
self.Name = value
|
||||
return self
|
||||
|
||||
def SetFamily( self, value ) :
|
||||
self.Family = value
|
||||
return self
|
||||
|
||||
def SetCharacterSet( self, value ) :
|
||||
self.CharacterSet = value
|
||||
return self
|
||||
|
||||
def SetPitch( self, value ) :
|
||||
self.Pitch = value
|
||||
return self
|
||||
|
||||
def SetPanose( self, value ) :
|
||||
self.Panose = value
|
||||
return self
|
||||
|
||||
def SetAlternate( self, value ) :
|
||||
self.Alternate = value
|
||||
return self
|
||||
|
||||
class Fonts( AttributedList ) :
|
||||
def __init__( self ) :
|
||||
super( Fonts, self ).__init__( Font )
|
||||
|
||||
class Paper :
|
||||
def __init__( self, name, code, description, width, height ) :
|
||||
self.SetName ( name )
|
||||
self.SetCode ( code )
|
||||
self.SetDescription( description )
|
||||
self.SetWidth ( width )
|
||||
self.SetHeight ( height )
|
||||
|
||||
def SetName( self, value ) :
|
||||
self.Name = value
|
||||
return self
|
||||
|
||||
def SetCode( self, value ) :
|
||||
self.Code = value
|
||||
return self
|
||||
|
||||
def SetDescription( self, value ) :
|
||||
self.Description = value
|
||||
return self
|
||||
|
||||
def SetWidth( self, value ) :
|
||||
self.Width = value
|
||||
return self
|
||||
|
||||
def SetHeight( self, value ) :
|
||||
self.Height = value
|
||||
return self
|
||||
|
||||
class Papers( AttributedList ) :
|
||||
def __init__( self ) :
|
||||
super( Papers, self ).__init__( Paper )
|
||||
|
||||
#
|
||||
# Then we have property sets which represent different aspects of Styles
|
||||
#
|
||||
class MarginsPropertySet :
|
||||
def __init__( self, top=None, left=None, bottom=None, right=None ) :
|
||||
self.SetTop ( top )
|
||||
self.SetLeft ( left )
|
||||
self.SetBottom( bottom )
|
||||
self.SetRight ( right )
|
||||
|
||||
def SetTop( self, value ) :
|
||||
self.Top = value
|
||||
return self
|
||||
|
||||
def SetLeft( self, value ) :
|
||||
self.Left = value
|
||||
return self
|
||||
|
||||
def SetBottom( self, value ) :
|
||||
self.Bottom = value
|
||||
return self
|
||||
|
||||
def SetRight( self, value ) :
|
||||
self.Right = value
|
||||
return self
|
||||
|
||||
class ShadingPropertySet :
|
||||
HORIZONTAL = 1
|
||||
VERTICAL = 2
|
||||
FORWARD_DIAGONAL = 3
|
||||
BACKWARD_DIAGONAL = 4
|
||||
VERTICAL_CROSS = 5
|
||||
DIAGONAL_CROSS = 6
|
||||
DARK_HORIZONTAL = 7
|
||||
DARK_VERTICAL = 8
|
||||
DARK_FORWARD_DIAGONAL = 9
|
||||
DARK_BACKWARD_DIAGONAL = 10
|
||||
DARK_VERTICAL_CROSS = 11
|
||||
DARK_DIAGONAL_CROSS = 12
|
||||
PATTERNS = [ HORIZONTAL,
|
||||
VERTICAL,
|
||||
FORWARD_DIAGONAL,
|
||||
BACKWARD_DIAGONAL,
|
||||
VERTICAL_CROSS,
|
||||
DIAGONAL_CROSS,
|
||||
DARK_HORIZONTAL,
|
||||
DARK_VERTICAL,
|
||||
DARK_FORWARD_DIAGONAL,
|
||||
DARK_BACKWARD_DIAGONAL,
|
||||
DARK_VERTICAL_CROSS,
|
||||
DARK_DIAGONAL_CROSS ]
|
||||
|
||||
def __init__( self, shading=None, pattern=None, foreground=None, background=None ) :
|
||||
self.SetShading ( shading )
|
||||
self.SetForeground( foreground )
|
||||
self.SetBackground( background )
|
||||
self.SetPattern ( pattern )
|
||||
|
||||
def __deepcopy__( self, memo ) :
|
||||
return ShadingPropertySet( self.Shading,
|
||||
self.Foreground,
|
||||
self.Background,
|
||||
self.Pattern )
|
||||
|
||||
def SetShading( self, value ) :
|
||||
self.Shading = value
|
||||
return self
|
||||
|
||||
def SetPattern( self, value ) :
|
||||
assert value is None or value in self.PATTERNS
|
||||
self.Pattern = value
|
||||
return self
|
||||
|
||||
def SetForeground( self, value ) :
|
||||
assert not value or isinstance( value, Colour )
|
||||
self.Foreground = value
|
||||
return self
|
||||
|
||||
def SetBackground( self, value ) :
|
||||
assert not value or isinstance( value, Colour )
|
||||
self.Background = value
|
||||
return self
|
||||
|
||||
|
||||
class BorderPropertySet :
|
||||
SINGLE = 1
|
||||
DOUBLE = 2
|
||||
SHADOWED = 3
|
||||
DOUBLED = 4
|
||||
DOTTED = 5
|
||||
DASHED = 6
|
||||
HAIRLINE = 7
|
||||
STYLES = [ SINGLE, DOUBLE, SHADOWED, DOUBLED, DOTTED, DASHED, HAIRLINE ]
|
||||
|
||||
def __init__( self, width=None, style=None, colour=None, spacing=None ) :
|
||||
self.SetWidth ( width )
|
||||
self.SetStyle ( style or self.SINGLE )
|
||||
self.SetColour ( colour )
|
||||
self.SetSpacing( spacing )
|
||||
|
||||
def SetWidth( self, value ) :
|
||||
self.Width = value
|
||||
return self
|
||||
|
||||
def SetStyle( self, value ) :
|
||||
assert value is None or value in self.STYLES
|
||||
self.Style = value
|
||||
return self
|
||||
|
||||
def SetColour( self, value ) :
|
||||
assert value is None or isinstance( value, Colour )
|
||||
self.Colour = value
|
||||
return self
|
||||
|
||||
def SetSpacing( self, value ) :
|
||||
self.Spacing = value
|
||||
return self
|
||||
|
||||
class FramePropertySet :
|
||||
def __init__( self, top=None, left=None, bottom=None, right=None ) :
|
||||
self.SetTop ( top )
|
||||
self.SetLeft ( left )
|
||||
self.SetBottom( bottom )
|
||||
self.SetRight ( right )
|
||||
|
||||
def SetTop( self, value ) :
|
||||
assert value is None or isinstance( value, BorderPropertySet )
|
||||
self.Top = value
|
||||
return self
|
||||
|
||||
def SetLeft( self, value ) :
|
||||
assert value is None or isinstance( value, BorderPropertySet )
|
||||
self.Left = value
|
||||
return self
|
||||
|
||||
def SetBottom( self, value ) :
|
||||
assert value is None or isinstance( value, BorderPropertySet )
|
||||
self.Bottom = value
|
||||
return self
|
||||
|
||||
def SetRight( self, value ) :
|
||||
assert value is None or isinstance( value, BorderPropertySet )
|
||||
self.Right = value
|
||||
return self
|
||||
|
||||
class TabPropertySet :
|
||||
DEFAULT_WIDTH = 720
|
||||
|
||||
LEFT = 1
|
||||
RIGHT = 2
|
||||
CENTER = 3
|
||||
DECIMAL = 4
|
||||
ALIGNMENT = [ LEFT, RIGHT, CENTER, DECIMAL ]
|
||||
|
||||
DOTS = 1
|
||||
HYPHENS = 2
|
||||
UNDERLINE = 3
|
||||
THICK_LINE = 4
|
||||
EQUAL_SIGN = 5
|
||||
LEADERS = [ DOTS, HYPHENS, UNDERLINE, THICK_LINE, EQUAL_SIGN ]
|
||||
|
||||
def __init__( self, width=None, alignment=None, leader=None ) :
|
||||
self.SetWidth ( width )
|
||||
self.SetAlignment( alignment or self.LEFT )
|
||||
self.SetLeader ( leader )
|
||||
|
||||
def SetWidth( self, value ) :
|
||||
self.Width = value
|
||||
return self
|
||||
|
||||
def SetAlignment( self, value ) :
|
||||
assert value in self.ALIGNMENT
|
||||
self.Alignment = value
|
||||
return self
|
||||
|
||||
def SetLeader( self, value ) :
|
||||
assert not value or value in self.LEADERS
|
||||
self.Leader = value
|
||||
return self
|
||||
|
||||
class TextPropertySet :
|
||||
|
||||
def __init__( self, font=None, size=None, bold=None, italic=None, underline=None, colour=None, frame=None, expansion=None ) :
|
||||
self.SetFont ( font )
|
||||
self.SetSize ( size )
|
||||
|
||||
self.SetBold ( bold or False )
|
||||
self.SetItalic ( italic or False )
|
||||
self.SetUnderline ( underline or False )
|
||||
|
||||
self.SetColour( colour )
|
||||
self.SetFrame ( frame )
|
||||
|
||||
self.SetStrikeThrough ( False )
|
||||
self.SetDottedUnderline( False )
|
||||
self.SetDoubleUnderline( False )
|
||||
self.SetWordUnderline ( False )
|
||||
self.SetExpansion ( expansion )
|
||||
|
||||
def Copy( self ) :
|
||||
return deepcopy( self )
|
||||
|
||||
def __deepcopy__( self, memo ) :
|
||||
# the font must remain a reference to the same font that we are looking at
|
||||
# so we want to stop the recursiveness at this point and return an object
|
||||
# with the right references.
|
||||
result = TextPropertySet( self.Font,
|
||||
self.Size,
|
||||
self.Bold,
|
||||
self.Italic,
|
||||
self.Underline,
|
||||
self.Colour,
|
||||
deepcopy( self.Frame, memo ) )
|
||||
result.SetStrikeThrough( self.StrikeThrough )
|
||||
return result
|
||||
|
||||
def SetFont( self, value ) :
|
||||
assert not value or isinstance( value, Font )
|
||||
self.Font = value
|
||||
return self
|
||||
|
||||
def SetSize( self, value ) :
|
||||
self.Size = value
|
||||
return self
|
||||
|
||||
def SetBold( self, value ) :
|
||||
self.Bold = False
|
||||
if value : self.Bold = True
|
||||
return self
|
||||
|
||||
def SetItalic( self, value ) :
|
||||
self.Italic = False
|
||||
if value : self.Italic = True
|
||||
return self
|
||||
|
||||
def SetUnderline( self, value ) :
|
||||
self.Underline = False
|
||||
if value : self.Underline = True
|
||||
return self
|
||||
|
||||
def SetColour( self, value ) :
|
||||
assert value is None or isinstance( value, Colour )
|
||||
self.Colour = value
|
||||
return self
|
||||
|
||||
def SetFrame( self, value ) :
|
||||
assert value is None or isinstance( value, BorderPropertySet )
|
||||
self.Frame = value
|
||||
return self
|
||||
|
||||
def SetStrikeThrough( self, value ) :
|
||||
self.StrikeThrough = False
|
||||
if value : self.StrikeThrough = True
|
||||
return self
|
||||
|
||||
def SetDottedUnderline( self, value ) :
|
||||
self.DottedUnderline = False
|
||||
if value : self.DottedUnderline = True
|
||||
return self
|
||||
|
||||
def SetDoubleUnderline( self, value ) :
|
||||
self.DoubleUnderline = False
|
||||
if value : self.DoubleUnderline = True
|
||||
return self
|
||||
|
||||
def SetWordUnderline( self, value ) :
|
||||
self.WordUnderline = False
|
||||
if value : self.WordUnderline = True
|
||||
return self
|
||||
|
||||
def SetExpansion( self, value ) :
|
||||
self.Expansion = value
|
||||
return self
|
||||
|
||||
class ParagraphPropertySet :
|
||||
LEFT = 1
|
||||
RIGHT = 2
|
||||
CENTER = 3
|
||||
JUSTIFY = 4
|
||||
DISTRIBUTE = 5
|
||||
ALIGNMENT = [ LEFT, RIGHT, CENTER, JUSTIFY, DISTRIBUTE ]
|
||||
|
||||
def __init__( self, alignment=None, space_before=None, space_after=None, tabs=None, first_line_indent=None, left_indent=None, right_indent=None, page_break_before=None ) :
|
||||
self.SetAlignment ( alignment or self.LEFT )
|
||||
self.SetSpaceBefore( space_before )
|
||||
self.SetSpaceAfter ( space_after )
|
||||
|
||||
self.Tabs = []
|
||||
if tabs : apply( self.SetTabs, tabs )
|
||||
|
||||
self.SetFirstLineIndent( first_line_indent or None )
|
||||
self.SetLeftIndent ( left_indent or None )
|
||||
self.SetRightIndent ( right_indent or None )
|
||||
|
||||
self.SetPageBreakBefore( page_break_before )
|
||||
|
||||
self.SetSpaceBetweenLines( None )
|
||||
|
||||
def Copy( self ) :
|
||||
return deepcopy( self )
|
||||
|
||||
def SetAlignment( self, value ) :
|
||||
assert not value or value in self.ALIGNMENT
|
||||
self.Alignment = value or self.LEFT
|
||||
return self
|
||||
|
||||
def SetSpaceBefore( self, value ) :
|
||||
self.SpaceBefore = value
|
||||
return self
|
||||
|
||||
def SetSpaceAfter( self, value ) :
|
||||
self.SpaceAfter = value
|
||||
return self
|
||||
|
||||
def SetTabs( self, *params ) :
|
||||
self.Tabs = params
|
||||
return self
|
||||
|
||||
def SetFirstLineIndent( self, value ) :
|
||||
self.FirstLineIndent = value
|
||||
return self
|
||||
|
||||
def SetLeftIndent( self, value ) :
|
||||
self.LeftIndent = value
|
||||
return self
|
||||
|
||||
def SetRightIndent( self, value ) :
|
||||
self.RightIndent = value
|
||||
return self
|
||||
|
||||
def SetSpaceBetweenLines( self, value ) :
|
||||
self.SpaceBetweenLines = value
|
||||
return self
|
||||
|
||||
def SetPageBreakBefore( self, value ) :
|
||||
self.PageBreakBefore = False
|
||||
if value : self.PageBreakBefore = True
|
||||
return self
|
||||
|
||||
# Some short cuts to make the code a bit easier to read
|
||||
MarginsPS = MarginsPropertySet
|
||||
ShadingPS = ShadingPropertySet
|
||||
BorderPS = BorderPropertySet
|
||||
FramePS = FramePropertySet
|
||||
TabPS = TabPropertySet
|
||||
TextPS = TextPropertySet
|
||||
ParagraphPS = ParagraphPropertySet
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Version 0.46
|
||||
|
||||
Added EMF support.
|
||||
|
||||
Added more sophisticated scaling options.
|
||||
|
||||
See examples2.py for both.
|
||||
|
||||
Grant Edwards, grante@users.sourceforge.net
|
||||
|
||||
|
||||
|
||||
Version 0.45
|
||||
|
||||
Finally, image support!!! Handles PNGs and JPGs.
|
||||
|
||||
See examples2.py for the gory details.
|
||||
|
||||
Simon Cusack, scusack@sourceforge.net
|
||||
@@ -0,0 +1,639 @@
|
||||
from types import StringType, ListType, TupleType
|
||||
from copy import deepcopy
|
||||
from Elements import *
|
||||
|
||||
DEFAULT_TAB_WIDTH = 720
|
||||
|
||||
ParagraphAlignmentMap = { ParagraphPropertySet.LEFT : 'ql',
|
||||
ParagraphPropertySet.RIGHT : 'qr',
|
||||
ParagraphPropertySet.CENTER : 'qc',
|
||||
ParagraphPropertySet.JUSTIFY : 'qj',
|
||||
ParagraphPropertySet.DISTRIBUTE : 'qd' }
|
||||
|
||||
TabAlignmentMap = { TabPropertySet.LEFT : '',
|
||||
TabPropertySet.RIGHT : 'tqr',
|
||||
TabPropertySet.CENTER : 'tqc',
|
||||
TabPropertySet.DECIMAL : 'tqdec' }
|
||||
|
||||
TableAlignmentMap = { Table.LEFT : 'trql',
|
||||
Table.RIGHT : 'trqr',
|
||||
Table.CENTER : 'trqc' }
|
||||
|
||||
CellAlignmentMap = { Cell.ALIGN_TOP : '', # clvertalt
|
||||
Cell.ALIGN_CENTER : 'clvertalc',
|
||||
Cell.ALIGN_BOTTOM : 'clvertalb' }
|
||||
|
||||
CellFlowMap = { Cell.FLOW_LR_TB : '', # cltxlrtb, Text in a cell flows from left to right and top to bottom (default)
|
||||
Cell.FLOW_RL_TB : 'cltxtbrl', # Text in a cell flows right to left and top to bottom
|
||||
Cell.FLOW_LR_BT : 'cltxbtlr', # Text in a cell flows left to right and bottom to top
|
||||
Cell.FLOW_VERTICAL_LR_TB : 'cltxlrtbv', # Text in a cell flows left to right and top to bottom, vertical
|
||||
Cell.FLOW_VERTICAL_TB_RL : 'cltxtbrlv' } # Text in a cell flows top to bottom and right to left, vertical
|
||||
|
||||
ShadingPatternMap = { ShadingPropertySet.HORIZONTAL : 'bghoriz',
|
||||
ShadingPropertySet.VERTICAL : 'bgvert',
|
||||
ShadingPropertySet.FORWARD_DIAGONAL : 'bgfdiag',
|
||||
ShadingPropertySet.BACKWARD_DIAGONAL : 'bgbdiag',
|
||||
ShadingPropertySet.VERTICAL_CROSS : 'bgcross',
|
||||
ShadingPropertySet.DIAGONAL_CROSS : 'bgdcross',
|
||||
ShadingPropertySet.DARK_HORIZONTAL : 'bgdkhoriz',
|
||||
ShadingPropertySet.DARK_VERTICAL : 'bgdkvert',
|
||||
ShadingPropertySet.DARK_FORWARD_DIAGONAL : 'bgdkfdiag',
|
||||
ShadingPropertySet.DARK_BACKWARD_DIAGONAL : 'bgdkbdiag',
|
||||
ShadingPropertySet.DARK_VERTICAL_CROSS : 'bgdkcross',
|
||||
ShadingPropertySet.DARK_DIAGONAL_CROSS : 'bgdkdcross' }
|
||||
|
||||
TabLeaderMap = { TabPropertySet.DOTS : 'tldot',
|
||||
TabPropertySet.HYPHENS : 'tlhyph',
|
||||
TabPropertySet.UNDERLINE : 'tlul',
|
||||
TabPropertySet.THICK_LINE : 'tlth',
|
||||
TabPropertySet.EQUAL_SIGN : 'tleq' }
|
||||
|
||||
BorderStyleMap = { BorderPropertySet.SINGLE : 'brdrs',
|
||||
BorderPropertySet.DOUBLE : 'brdrth',
|
||||
BorderPropertySet.SHADOWED : 'brdrsh',
|
||||
BorderPropertySet.DOUBLED : 'brdrdb',
|
||||
BorderPropertySet.DOTTED : 'brdrdot',
|
||||
BorderPropertySet.DASHED : 'brdrdash',
|
||||
BorderPropertySet.HAIRLINE : 'brdrhair' }
|
||||
|
||||
SectionBreakTypeMap = { Section.NONE : 'sbknone',
|
||||
Section.COLUMN : 'sbkcol',
|
||||
Section.PAGE : 'sbkpage',
|
||||
Section.EVEN : 'sbkeven',
|
||||
Section.ODD : 'sbkodd' }
|
||||
|
||||
class Settings( list ) :
|
||||
def __init__( self ) :
|
||||
super( Settings, self ).__init__()
|
||||
self._append = super( Settings, self ).append
|
||||
|
||||
def append( self, value, mask=None, fallback=None ) :
|
||||
if (value is not 0) and value in [ False, None, '' ] :
|
||||
if fallback : self._append( self, fallback )
|
||||
|
||||
else :
|
||||
if mask :
|
||||
if value is True :
|
||||
value = mask
|
||||
else :
|
||||
value = mask % value
|
||||
self._append( value )
|
||||
|
||||
def Join( self ) :
|
||||
if self : return r'\%s' % '\\'.join( self )
|
||||
return ''
|
||||
|
||||
def __repr__( self ) :
|
||||
return self.Join()
|
||||
|
||||
class Renderer :
|
||||
def __init__( self, write_custom_element_callback=None ) :
|
||||
self.character_style_map = {}
|
||||
self.paragraph_style_map = {}
|
||||
self.WriteCustomElement = write_custom_element_callback
|
||||
|
||||
#
|
||||
# All of the Rend* Functions populate a Settings object with values
|
||||
#
|
||||
def _RendPageProperties( self, section, settings, in_section ) :
|
||||
# this one is different from the others as it takes the settings from a
|
||||
if in_section :
|
||||
#paper_size_code = 'psz%s'
|
||||
paper_width_code = 'pgwsxn%s'
|
||||
paper_height_code = 'pghsxn%s'
|
||||
landscape = 'lndscpsxn'
|
||||
margin_suffix = 'sxn'
|
||||
|
||||
else :
|
||||
#paper_size_code = 'psz%s'
|
||||
paper_width_code = 'paperw%s'
|
||||
paper_height_code = 'paperh%s'
|
||||
landscape = 'landscape'
|
||||
margin_suffix = ''
|
||||
|
||||
#settings.append( section.Paper.Code, paper_size_code )
|
||||
settings.append( section.Paper.Width, paper_width_code )
|
||||
settings.append( section.Paper.Height, paper_height_code )
|
||||
|
||||
if section.Landscape :
|
||||
settings.append( landscape )
|
||||
|
||||
if section.FirstPageNumber :
|
||||
settings.append( section.FirstPageNumber, 'pgnstarts%s' )
|
||||
settings.append( 'pgnrestart' )
|
||||
|
||||
self._RendMarginsPropertySet( section.Margins, settings, margin_suffix )
|
||||
|
||||
def _RendShadingPropertySet( self, shading_props, settings, prefix='' ) :
|
||||
if not shading_props : return
|
||||
|
||||
settings.append( shading_props.Shading, prefix + 'shading%s' )
|
||||
settings.append( ShadingPatternMap.get( shading_props.Pattern, False ) )
|
||||
|
||||
settings.append( self._colour_map.get( shading_props.Foreground, False ), prefix + 'cfpat%s' )
|
||||
settings.append( self._colour_map.get( shading_props.Background, False ), prefix + 'cbpat%s' )
|
||||
|
||||
def _RendBorderPropertySet( self, edge_props, settings ) :
|
||||
settings.append( BorderStyleMap[ edge_props.Style ] )
|
||||
settings.append( edge_props.Width , 'brdrw%s' )
|
||||
settings.append( self._colour_map.get( edge_props.Colour, False ), 'brdrcf%s' )
|
||||
settings.append( edge_props.Spacing or False , 'brsp%s' )
|
||||
|
||||
def _RendFramePropertySet( self, frame_props, settings, tag_prefix='' ) :
|
||||
if not frame_props : return
|
||||
|
||||
if frame_props.Top :
|
||||
settings.append( tag_prefix + 'brdrt' )
|
||||
self._RendBorderPropertySet( frame_props.Top, settings )
|
||||
|
||||
if frame_props.Left :
|
||||
settings.append( tag_prefix + 'brdrl' )
|
||||
self._RendBorderPropertySet( frame_props.Left, settings )
|
||||
|
||||
if frame_props.Bottom :
|
||||
settings.append( tag_prefix + 'brdrb' )
|
||||
self._RendBorderPropertySet( frame_props.Bottom, settings )
|
||||
|
||||
if frame_props.Right :
|
||||
settings.append( tag_prefix + 'brdrr' )
|
||||
self._RendBorderPropertySet( frame_props.Right, settings )
|
||||
|
||||
def _RendMarginsPropertySet( self, margin_props, settings, suffix='' ) :
|
||||
if not margin_props : return
|
||||
|
||||
settings.append( margin_props.Top, 'margt' + suffix + '%s' )
|
||||
settings.append( margin_props.Left, 'margl' + suffix + '%s' )
|
||||
settings.append( margin_props.Bottom, 'margb' + suffix + '%s' )
|
||||
settings.append( margin_props.Right, 'margr' + suffix + '%s' )
|
||||
|
||||
def _RendParagraphPropertySet( self, paragraph_props, settings ) :
|
||||
if not paragraph_props : return
|
||||
settings.append( ParagraphAlignmentMap[ paragraph_props.Alignment ] )
|
||||
|
||||
settings.append( paragraph_props.SpaceBefore, 'sb%s' )
|
||||
settings.append( paragraph_props.SpaceAfter, 'sa%s' )
|
||||
|
||||
# then we have to find out all of the tabs
|
||||
width = 0
|
||||
for tab in paragraph_props.Tabs :
|
||||
settings.append( TabAlignmentMap[ tab.Alignment ] )
|
||||
settings.append( TabLeaderMap.get( tab.Leader, '' ) )
|
||||
|
||||
width += tab.Width or DEFAULT_TAB_WIDTH
|
||||
settings.append( 'tx%s' % width )
|
||||
|
||||
settings.append( paragraph_props.PageBreakBefore, 'pagebb' )
|
||||
|
||||
settings.append( paragraph_props.FirstLineIndent, 'fi%s' )
|
||||
settings.append( paragraph_props.LeftIndent, 'li%s' )
|
||||
settings.append( paragraph_props.RightIndent, 'ri%s' )
|
||||
|
||||
if paragraph_props.SpaceBetweenLines :
|
||||
if paragraph_props.SpaceBetweenLines < 0 :
|
||||
settings.append( paragraph_props.SpaceBetweenLines, r'sl%s\slmult0' )
|
||||
else :
|
||||
settings.append( paragraph_props.SpaceBetweenLines, r'sl%s\slmult1' )
|
||||
|
||||
def _RendTextPropertySet( self, text_props, settings ) :
|
||||
if not text_props : return
|
||||
|
||||
if text_props.Expansion :
|
||||
settings.append( text_props.Expansion, 'expndtw%s' )
|
||||
|
||||
settings.append( text_props.Bold, 'b' )
|
||||
settings.append( text_props.Italic, 'i' )
|
||||
settings.append( text_props.Underline, 'ul' )
|
||||
settings.append( text_props.DottedUnderline, 'uld' )
|
||||
settings.append( text_props.DoubleUnderline, 'uldb' )
|
||||
settings.append( text_props.WordUnderline, 'ulw' )
|
||||
|
||||
settings.append( self._font_map.get( text_props.Font, False ), 'f%s' )
|
||||
settings.append( text_props.Size, 'fs%s' )
|
||||
settings.append( self._colour_map.get( text_props.Colour, False ), 'cf%s' )
|
||||
|
||||
if text_props.Frame :
|
||||
frame = text_props.Frame
|
||||
settings.append( 'chbrdr' )
|
||||
settings.append( BorderStyleMap[ frame.Style ] )
|
||||
settings.append( frame.Width , 'brdrw%s' )
|
||||
settings.append( self._colour_map.get( frame.Colour, False ), 'brdrcf%s' )
|
||||
|
||||
#
|
||||
# All of the Write* functions will write to the internal file object
|
||||
#
|
||||
# the _ ones probably don't need to be used by anybody outside
|
||||
# but the other ones like WriteTextElement could be used in the Custom
|
||||
# callback.
|
||||
def Write( self, document, fout ) :
|
||||
# write all of the standard stuff based upon the first document
|
||||
self._doc = document
|
||||
self._fout = fout
|
||||
self._WriteDocument ()
|
||||
self._WriteColours ()
|
||||
self._WriteFonts ()
|
||||
self._WriteStyleSheet()
|
||||
|
||||
settings = Settings()
|
||||
self._RendPageProperties( self._doc.Sections[ 0 ], settings, in_section=False )
|
||||
self._write( repr( settings ) )
|
||||
|
||||
# handle the simplest case first, we don't need to do anymore mucking around
|
||||
# with section headers, etc we can just rip the document out
|
||||
if len( document.Sections ) == 1 :
|
||||
self._WriteSection( document.Sections[ 0 ],
|
||||
is_first = True,
|
||||
add_header = False )
|
||||
|
||||
else :
|
||||
for section_idx, section in enumerate( document.Sections ) :
|
||||
is_first = section_idx == 0
|
||||
add_header = True
|
||||
self._WriteSection( section, is_first, add_header )
|
||||
|
||||
self._write( '}' )
|
||||
|
||||
del self._fout, self._doc, self._CurrentStyle
|
||||
|
||||
def _write( self, data, *params ) :
|
||||
#----------------------------------
|
||||
# begin modification
|
||||
# by Herbert Weinhandl
|
||||
# to convert accented characters
|
||||
# to their rtf-compatible form
|
||||
#for c in range( 128, 256 ) :
|
||||
# data = data.replace( chr(c), "\'%x" % c)
|
||||
# end modification
|
||||
#
|
||||
# This isn't the right place for this as it is going to do
|
||||
# this loop for all sorts of writes, including settings, control codes, etc.
|
||||
#
|
||||
# I will create a def _WriteText (or something) method that is used when the
|
||||
# actual string that is to be viewed in the document is written, this can then
|
||||
# do the final accented character check.
|
||||
#
|
||||
# I left it here so that I remember to do the right thing when I have time
|
||||
#----------------------------------
|
||||
|
||||
if params : data = data % params
|
||||
self._fout.write( data )
|
||||
|
||||
def _WriteDocument( self ) :
|
||||
settings = Settings()
|
||||
|
||||
assert Languages.IsValid ( self._doc.DefaultLanguage )
|
||||
assert ViewKind.IsValid ( self._doc.ViewKind )
|
||||
assert ViewZoomKind.IsValid( self._doc.ViewZoomKind )
|
||||
assert ViewScale.IsValid ( self._doc.ViewScale )
|
||||
|
||||
settings.append( self._doc.DefaultLanguage, 'deflang%s' )
|
||||
settings.append( self._doc.ViewKind , 'viewkind%s' )
|
||||
settings.append( self._doc.ViewZoomKind , 'viewzk%s' )
|
||||
settings.append( self._doc.ViewScale , 'viewscale%s' )
|
||||
|
||||
self._write( "{\\rtf1\\ansi\\ansicpg1252\\deff0%s\n" % settings )
|
||||
|
||||
def _WriteColours( self ) :
|
||||
self._write( r"{\colortbl ;" )
|
||||
|
||||
self._colour_map = {}
|
||||
offset = 0
|
||||
for colour in self._doc.StyleSheet.Colours :
|
||||
self._write( r'\red%s\green%s\blue%s;', colour.Red, colour.Green, colour.Blue )
|
||||
self._colour_map[ colour ] = offset + 1
|
||||
offset += 1
|
||||
self._write( "}\n" )
|
||||
|
||||
def _WriteFonts( self ) :
|
||||
self._write( r'{\fonttbl' )
|
||||
|
||||
self._font_map = {}
|
||||
offset = 0
|
||||
for font in self._doc.StyleSheet.Fonts :
|
||||
pitch = ''
|
||||
panose = ''
|
||||
alternate = ''
|
||||
if font.Pitch : pitch = r'\fprq%s' % font.Pitch
|
||||
if font.Panose : panose = r'{\*\panose %s}' % font.Panose
|
||||
if font.Alternate : alternate = r'{\*\falt %s}' % font.Alternate.Name
|
||||
|
||||
self._write( r'{\f%s\f%s%s\fcharset%s%s %s%s;}',
|
||||
offset,
|
||||
font.Family,
|
||||
pitch,
|
||||
font.CharacterSet,
|
||||
panose,
|
||||
font.Name,
|
||||
alternate )
|
||||
|
||||
self._font_map[ font ] = offset
|
||||
offset += 1
|
||||
|
||||
self._write( "}\n" )
|
||||
|
||||
def _WriteStyleSheet( self ) :
|
||||
self._write( r"{\stylesheet" )
|
||||
|
||||
# TO DO: character styles, does anybody actually use them?
|
||||
|
||||
offset_map = {}
|
||||
for idx, style in enumerate( self._doc.StyleSheet.ParagraphStyles ) :
|
||||
offset_map[ style ] = idx
|
||||
|
||||
# paragraph styles
|
||||
self.paragraph_style_map = {}
|
||||
for idx, style in enumerate( self._doc.StyleSheet.ParagraphStyles ) :
|
||||
|
||||
if idx == 0 :
|
||||
default = style
|
||||
else :
|
||||
self._write( '\n' )
|
||||
|
||||
settings = Settings()
|
||||
|
||||
# paragraph properties
|
||||
self._RendParagraphPropertySet( style.ParagraphPropertySet, settings )
|
||||
self._RendFramePropertySet ( style.FramePropertySet, settings )
|
||||
self._RendShadingPropertySet ( style.ShadingPropertySet, settings )
|
||||
|
||||
# text properties
|
||||
self._RendTextPropertySet ( style.TextStyle.TextPropertySet, settings )
|
||||
self._RendShadingPropertySet( style.TextStyle.ShadingPropertySet, settings )
|
||||
|
||||
# have to take
|
||||
based_on = '\\sbasedon%s' % offset_map.get( style.BasedOn, 0 )
|
||||
next = '\\snext%s' % offset_map.get( style.Next, 0 )
|
||||
|
||||
inln = '\\s%s%s' % ( idx, settings )
|
||||
self._write( "{%s%s%s %s;}", inln, based_on, next, style.Name )
|
||||
|
||||
self.paragraph_style_map[ style ] = inln
|
||||
|
||||
# if now style is specified for the first paragraph to be written, this one
|
||||
# will be used
|
||||
self._CurrentStyle = self.paragraph_style_map[ default ]
|
||||
|
||||
self._write( "}\n" )
|
||||
|
||||
def _WriteSection( self, section, is_first, add_header ) :
|
||||
|
||||
def WriteHF( hf, rtfword ) :
|
||||
#if not hf : return
|
||||
|
||||
# if we don't have anything in the header/footer then include
|
||||
# a blank paragraph, this stops it from picking up the header/footer
|
||||
# from the previous section
|
||||
# if not hf : hf = [ Paragraph( '' ) ]
|
||||
if not hf : hf = []
|
||||
|
||||
self._write( '{\\%s' % rtfword )
|
||||
self._WriteElements( hf )
|
||||
self._write( '}\n' )
|
||||
|
||||
settings = Settings()
|
||||
|
||||
if not is_first :
|
||||
# we need to finish off the preceding section
|
||||
# and reset all of our defaults back to standard
|
||||
settings.append( 'sect' )
|
||||
|
||||
# reset to our defaults
|
||||
settings.append( 'sectd' )
|
||||
|
||||
if add_header :
|
||||
settings.append( SectionBreakTypeMap[ section.BreakType ] )
|
||||
self._RendPageProperties( section, settings, in_section=True )
|
||||
|
||||
settings.append( section.HeaderY, 'headery%s' )
|
||||
settings.append( section.FooterY, 'footery%s' )
|
||||
|
||||
# write all of these out now as we need to do a write elements in the
|
||||
# next section
|
||||
self._write( repr( settings ) )
|
||||
|
||||
# finally after all that has settled down we can do the
|
||||
# headers and footers
|
||||
if section.FirstHeader or section.FirstFooter :
|
||||
# include the titlepg flag if the first page has a special format
|
||||
self._write( r'\titlepg' )
|
||||
WriteHF( section.FirstHeader, 'headerf' )
|
||||
WriteHF( section.FirstFooter, 'footerf' )
|
||||
|
||||
WriteHF( section.Header, 'header' )
|
||||
WriteHF( section.Footer, 'footer' )
|
||||
|
||||
# and at last the contents of the section that actually appear on the page
|
||||
self._WriteElements( section )
|
||||
|
||||
def _WriteElements( self, elements ) :
|
||||
new_line = ''
|
||||
for element in elements :
|
||||
self._write( new_line )
|
||||
new_line = '\n'
|
||||
|
||||
clss = element.__class__
|
||||
|
||||
if clss == Paragraph :
|
||||
self.WriteParagraphElement( element )
|
||||
|
||||
elif clss == Table :
|
||||
self.WriteTableElement( element )
|
||||
|
||||
elif clss == StringType :
|
||||
self.WriteParagraphElement( Paragraph( element ) )
|
||||
|
||||
elif clss in [ RawCode, Image ] :
|
||||
self.WriteRawCode( element )
|
||||
|
||||
#elif clss == List :
|
||||
# self._HandleListElement( element )
|
||||
|
||||
elif self.WriteCustomElement :
|
||||
self.WriteCustomElement( self, element )
|
||||
|
||||
else :
|
||||
raise Exception( "Don't know how to handle elements of type %s" % clss )
|
||||
|
||||
def WriteParagraphElement( self, paragraph_elem, tag_prefix='', tag_suffix=r'\par', opening='{', closing='}' ) :
|
||||
|
||||
# the tag_prefix and the tag_suffix take care of paragraphs in tables. A
|
||||
# paragraph in a table requires and extra tag at the front (intbl) and we
|
||||
# don't want the ending tag everytime. We want it for all paragraphs but
|
||||
# the last.
|
||||
|
||||
overrides = Settings()
|
||||
self._RendParagraphPropertySet( paragraph_elem.Properties, overrides )
|
||||
self._RendFramePropertySet ( paragraph_elem.Frame, overrides )
|
||||
self._RendShadingPropertySet ( paragraph_elem.Shading, overrides )
|
||||
|
||||
# when writing the RTF the style is carried from the previous paragraph to the next,
|
||||
# so if the currently written paragraph has a style then make it the current one,
|
||||
# otherwise leave it as it was
|
||||
self._CurrentStyle = self.paragraph_style_map.get( paragraph_elem.Style, self._CurrentStyle )
|
||||
|
||||
self._write( r'%s\pard\plain%s %s%s ' % ( opening, tag_prefix, self._CurrentStyle, overrides ) )
|
||||
|
||||
for element in paragraph_elem :
|
||||
|
||||
if isinstance( element, StringType ) :
|
||||
self._write( element )
|
||||
|
||||
elif isinstance( element, RawCode ) :
|
||||
self._write( element.Data )
|
||||
|
||||
elif isinstance( element, Text ) :
|
||||
self.WriteTextElement( element )
|
||||
|
||||
elif isinstance( element, Inline ) :
|
||||
self.WriteInlineElement( element )
|
||||
|
||||
elif element == TAB :
|
||||
self._write( r'\tab ' )
|
||||
|
||||
elif element == LINE :
|
||||
self._write( r'\line ' )
|
||||
|
||||
elif self.WriteCustomElement :
|
||||
self.WriteCustomElement( self, element )
|
||||
|
||||
else :
|
||||
raise Exception( 'Don\'t know how to handle %s' % element )
|
||||
|
||||
self._write( tag_suffix + closing )
|
||||
|
||||
def WriteRawCode( self, raw_elem ) :
|
||||
self._write( raw_elem.Data )
|
||||
|
||||
def WriteTextElement( self, text_elem ) :
|
||||
overrides = Settings()
|
||||
|
||||
self._RendTextPropertySet ( text_elem.Properties, overrides )
|
||||
self._RendShadingPropertySet( text_elem.Shading, overrides, 'ch' )
|
||||
|
||||
# write the wrapper and then let the custom handler have a go
|
||||
if overrides : self._write( '{%s ' % repr( overrides ) )
|
||||
|
||||
# if the data is just a string then we can now write it
|
||||
if isinstance( text_elem.Data, StringType ) :
|
||||
self._write( text_elem.Data or '' )
|
||||
|
||||
elif text_elem.Data == TAB :
|
||||
self._write( r'\tab ' )
|
||||
|
||||
else :
|
||||
self.WriteCustomElement( self, text_elem.Data )
|
||||
|
||||
if overrides : self._write( '}' )
|
||||
|
||||
def WriteInlineElement( self, inline_elem ) :
|
||||
overrides = Settings()
|
||||
|
||||
self._RendTextPropertySet ( inline_elem.Properties, overrides )
|
||||
self._RendShadingPropertySet( inline_elem.Shading, overrides, 'ch' )
|
||||
|
||||
# write the wrapper and then let the custom handler have a go
|
||||
if overrides : self._write( '{%s ' % repr( overrides ) )
|
||||
|
||||
for element in inline_elem :
|
||||
# if the data is just a string then we can now write it
|
||||
if isinstance( element, StringType ) :
|
||||
self._write( element )
|
||||
|
||||
elif isinstance( element, RawCode ) :
|
||||
self._write( element.Data )
|
||||
|
||||
elif element == TAB :
|
||||
self._write( r'\tab ' )
|
||||
|
||||
elif element == LINE :
|
||||
self._write( r'\line ' )
|
||||
|
||||
else :
|
||||
self.WriteCustomElement( self, element )
|
||||
|
||||
if overrides : self._write( '}' )
|
||||
|
||||
def WriteText( self, text ) :
|
||||
self._write( text or '' )
|
||||
|
||||
def WriteTableElement( self, table_elem ) :
|
||||
|
||||
vmerge = [ False ] * table_elem.ColumnCount
|
||||
for height, cells in table_elem.Rows :
|
||||
|
||||
# calculate the right hand edge of the cells taking into account the spans
|
||||
offset = table_elem.LeftOffset or 0
|
||||
cellx = []
|
||||
cell_idx = 0
|
||||
for cell in cells :
|
||||
cellx.append( offset + sum( table_elem.ColumnWidths[ : cell_idx + cell.Span ] ) )
|
||||
cell_idx += cell.Span
|
||||
|
||||
self._write( r'{\trowd' )
|
||||
|
||||
settings = Settings()
|
||||
|
||||
# the spec says that this value is mandatory and I think that 108 is the default value
|
||||
# so I'll take care of it here
|
||||
settings.append( table_elem.GapBetweenCells or 108, 'trgaph%s' )
|
||||
settings.append( TableAlignmentMap[ table_elem.Alignment ] )
|
||||
settings.append( height, 'trrh%s' )
|
||||
settings.append( table_elem.LeftOffset, 'trleft%s' )
|
||||
|
||||
width = table_elem.LeftOffset or 0
|
||||
for idx, cell in enumerate( cells ) :
|
||||
self._RendFramePropertySet ( cell.Frame, settings, 'cl' )
|
||||
|
||||
# cells don't have margins so I don't know why I was doing this
|
||||
# I think it might have an affect in some versions of some WPs.
|
||||
#self._RendMarginsPropertySet( cell.Margins, settings, 'cl' )
|
||||
|
||||
# if we are starting to merge or if this one is the first in what is
|
||||
# probably a series of merges then start the vertical merging
|
||||
if cell.StartVerticalMerge or (cell.VerticalMerge and not vmerge[ idx ]) :
|
||||
settings.append( 'clvmgf' )
|
||||
vmerge[ idx ] = True
|
||||
|
||||
elif cell.VerticalMerge :
|
||||
#..continuing a merge
|
||||
settings.append( 'clvmrg' )
|
||||
|
||||
else :
|
||||
#..no merging going on so make sure that it is off
|
||||
vmerge[ idx ] = False
|
||||
|
||||
# for any cell in the next row that is covered by this span we
|
||||
# need to run off the vertical merging as we don't want them
|
||||
# merging up into this spanned cell
|
||||
for vmerge_idx in range( idx + 1, idx + cell.Span - 1 ) :
|
||||
vmerge[ vmerge_idx ] = False
|
||||
|
||||
settings.append( CellAlignmentMap[ cell.Alignment ] )
|
||||
settings.append( CellFlowMap[ cell.Flow ] )
|
||||
|
||||
# this terminates the definition of a cell and represents the right most edge of the cell from the left margin
|
||||
settings.append( cellx[ idx ], 'cellx%s' )
|
||||
|
||||
self._write( repr( settings ) )
|
||||
|
||||
for cell in cells :
|
||||
if len( cell ) :
|
||||
last_idx = len( cell ) - 1
|
||||
for element_idx, element in enumerate( cell ) :
|
||||
# wrap plain strings in paragraph tags
|
||||
if isinstance( element, StringType ) :
|
||||
element = Paragraph( element )
|
||||
|
||||
# don't forget the prefix or else word crashes and does all sorts of strange things
|
||||
if element_idx == last_idx :
|
||||
self.WriteParagraphElement( element, tag_prefix=r'\intbl', tag_suffix='', opening='', closing='' )
|
||||
|
||||
else :
|
||||
self.WriteParagraphElement( element, tag_prefix=r'\intbl', opening='', closing='' )
|
||||
|
||||
self._write( r'\cell' )
|
||||
|
||||
else :
|
||||
self._write( r'\pard\intbl\cell' )
|
||||
|
||||
self._write( '\\row}\n' )
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
A Styles is a collection of PropertySets that can be applied to a particular RTF element.
|
||||
|
||||
At present there are only two, Text and Paragraph but ListStyles will be added soon too.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
from PropertySets import *
|
||||
|
||||
class TextStyle :
|
||||
def __init__( self, text_props, name=None, shading_props=None ) :
|
||||
self.SetTextPropertySet ( text_props )
|
||||
self.SetName ( name )
|
||||
self.SetShadingPropertySet( shading_props )
|
||||
|
||||
def Copy( self ) :
|
||||
return deepcopy( self )
|
||||
|
||||
def SetName( self, value ) :
|
||||
self.Name = value
|
||||
return self
|
||||
|
||||
def SetTextPropertySet( self, value ) :
|
||||
assert isinstance( value, TextPropertySet )
|
||||
self.TextPropertySet = value
|
||||
return self
|
||||
|
||||
def SetShadingPropertySet( self, value ) :
|
||||
assert value is None or isinstance( value, ShadingPropertySet )
|
||||
self.ShadingPropertySet = value or ShadingPropertySet()
|
||||
return self
|
||||
|
||||
class ParagraphStyle :
|
||||
def __init__( self, name, text_style, paragraph_props=None, frame_props=None, shading_props=None ) :
|
||||
|
||||
# A style must have Font and a Font Size but the Text property set doesn't
|
||||
# make these mandatory so that they can be used for overrides so at this point
|
||||
# we need to make sure that that we have these values set
|
||||
if not text_style.TextPropertySet.Font : raise Exception( 'Paragraph Styles must have a Font specified.' )
|
||||
if not text_style.TextPropertySet.Size : raise Exception( 'Paragraph Styles must have a Font Size specified.' )
|
||||
|
||||
self.SetName ( name )
|
||||
self.SetTextStyle ( text_style )
|
||||
self.SetParagraphPropertySet( paragraph_props )
|
||||
self.SetFramePropertySet ( frame_props )
|
||||
self.SetShadingPropertySet ( shading_props )
|
||||
|
||||
self.SetBasedOn( None )
|
||||
self.SetNext ( None )
|
||||
|
||||
def Copy( self ) :
|
||||
return deepcopy( self )
|
||||
|
||||
def SetName( self, value ) :
|
||||
self.Name = value
|
||||
return self
|
||||
|
||||
def SetTextStyle( self, value ) :
|
||||
assert isinstance( value, TextStyle )
|
||||
self.TextStyle = value
|
||||
return self
|
||||
|
||||
def SetParagraphPropertySet( self, value ) :
|
||||
assert value is None or isinstance( value, ParagraphPropertySet )
|
||||
self.ParagraphPropertySet = value or ParagraphPropertySet()
|
||||
return self
|
||||
|
||||
def SetFramePropertySet( self, value ) :
|
||||
assert value is None or isinstance( value, FramePropertySet )
|
||||
self.FramePropertySet = value or FramePropertySet()
|
||||
return self
|
||||
|
||||
def SetShadingPropertySet( self, value ) :
|
||||
"""Set the background shading for the paragraph."""
|
||||
|
||||
assert value is None or isinstance( value, ShadingPropertySet )
|
||||
self.ShadingPropertySet = value or ShadingPropertySet()
|
||||
return self
|
||||
|
||||
def SetBasedOn( self, value ) :
|
||||
"""Set the Paragraph Style that this one is based on."""
|
||||
|
||||
assert not value or isinstance( value, ParagraphStyle )
|
||||
self.BasedOn = value
|
||||
return self
|
||||
|
||||
def SetNext( self, value ) :
|
||||
"""Set the Paragraph Style that should follow this one."""
|
||||
|
||||
assert not value or isinstance( value, ParagraphStyle )
|
||||
self.Next = value
|
||||
return self
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from PropertySets import *
|
||||
from Elements import *
|
||||
from Styles import *
|
||||
from Renderer import *
|
||||
|
||||
def dumps(doc):
|
||||
import cStringIO
|
||||
s=cStringIO.StringIO()
|
||||
r=Renderer()
|
||||
r.Write(doc,s)
|
||||
return s.getvalue()
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"Contributed modules"
|
||||
|
||||
Executable
+689
@@ -0,0 +1,689 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: latin-1 -*-
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by the
|
||||
# Free Software Foundation; either version 3, 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 MERCHANTIBILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
# for more details.
|
||||
|
||||
"Pythonic simple SOAP Client implementation"
|
||||
|
||||
__author__ = "Mariano Reingart (reingart@gmail.com)"
|
||||
__copyright__ = "Copyright (C) 2008 Mariano Reingart"
|
||||
__license__ = "LGPL 3.0"
|
||||
__version__ = "1.02c"
|
||||
|
||||
import urllib
|
||||
try:
|
||||
import httplib2
|
||||
Http = httplib2.Http
|
||||
except ImportError:
|
||||
import urllib2
|
||||
class Http(): # wrapper to use when httplib2 not available
|
||||
def request(self, url, method, body, headers):
|
||||
f = urllib2.urlopen(urllib2.Request(url, body, headers))
|
||||
return f.info(), f.read()
|
||||
|
||||
|
||||
from simplexml import SimpleXMLElement, TYPE_MAP, OrderedDict
|
||||
|
||||
class SoapFault(RuntimeError):
|
||||
def __init__(self,faultcode,faultstring):
|
||||
self.faultcode = faultcode
|
||||
self.faultstring = faultstring
|
||||
|
||||
# soap protocol specification & namespace
|
||||
soap_namespaces = dict(
|
||||
soap11="http://schemas.xmlsoap.org/soap/envelope/",
|
||||
soap="http://schemas.xmlsoap.org/soap/envelope/",
|
||||
soapenv="http://schemas.xmlsoap.org/soap/envelope/",
|
||||
soap12="http://www.w3.org/2003/05/soap-env",
|
||||
)
|
||||
|
||||
class SoapClient(object):
|
||||
"Simple SOAP Client (s�mil PHP)"
|
||||
def __init__(self, location = None, action = None, namespace = None,
|
||||
cert = None, trace = False, exceptions = True, proxy = None, ns=False,
|
||||
soap_ns=None, wsdl = None, cache = False):
|
||||
self.certssl = cert
|
||||
self.keyssl = None
|
||||
self.location = location # server location (url)
|
||||
self.action = action # SOAP base action
|
||||
self.namespace = namespace # message
|
||||
self.trace = trace # show debug messages
|
||||
self.exceptions = exceptions # lanzar execpiones? (Soap Faults)
|
||||
self.xml_request = self.xml_response = ''
|
||||
if not soap_ns and not ns:
|
||||
self.__soap_ns = 'soap' # 1.1
|
||||
elif not soap_ns and ns:
|
||||
self.__soap_ns = 'soapenv' # 1.2
|
||||
else:
|
||||
self.__soap_ns = soap_ns
|
||||
|
||||
# parse wsdl url
|
||||
self.services = wsdl and self.wsdl(wsdl, debug=trace, cache=cache)
|
||||
self.service_port = None # service port for late binding
|
||||
|
||||
if not proxy:
|
||||
self.http = Http()
|
||||
else:
|
||||
import socks
|
||||
##httplib2.debuglevel=4
|
||||
self.http = httplib2.Http(proxy_info = httplib2.ProxyInfo(
|
||||
proxy_type=socks.PROXY_TYPE_HTTP, **proxy))
|
||||
#if self.certssl: # esto funciona para validar al server?
|
||||
# self.http.add_certificate(self.keyssl, self.keyssl, self.certssl)
|
||||
self.__ns = ns # namespace prefix or False to not use it
|
||||
if not ns:
|
||||
self.__xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<%(soap_ns)s:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:%(soap_ns)s="%(soap_uri)s">
|
||||
<%(soap_ns)s:Body>
|
||||
<%(method)s xmlns="%(namespace)s">
|
||||
</%(method)s>
|
||||
</%(soap_ns)s:Body>
|
||||
</%(soap_ns)s:Envelope>"""
|
||||
else:
|
||||
self.__xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s" xmlns:%(ns)s="%(namespace)s">
|
||||
<%(soap_ns)s:Header/>
|
||||
<%(soap_ns)s:Body>
|
||||
<%(ns)s:%(method)s>
|
||||
</%(ns)s:%(method)s>
|
||||
</%(soap_ns)s:Body>
|
||||
</%(soap_ns)s:Envelope>"""
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"Return a pseudo-method that can be called"
|
||||
if not self.services: # not using WSDL?
|
||||
return lambda self=self, *args, **kwargs: self.call(attr,*args,**kwargs)
|
||||
else: # using WSDL:
|
||||
return lambda self=self, *args, **kwargs: self.wsdl_call(attr,*args,**kwargs)
|
||||
|
||||
def call(self, method, *args, **kwargs):
|
||||
"Prepare xml request and make SOAP call, returning a SimpleXMLElement"
|
||||
#TODO: method != input_message
|
||||
# Basic SOAP request:
|
||||
xml = self.__xml % dict(method=method, namespace=self.namespace, ns=self.__ns,
|
||||
soap_ns=self.__soap_ns, soap_uri=soap_namespaces[self.__soap_ns])
|
||||
request = SimpleXMLElement(xml,namespace=self.__ns and self.namespace, prefix=self.__ns)
|
||||
# serialize parameters
|
||||
if kwargs:
|
||||
parameters = kwargs.items()
|
||||
else:
|
||||
parameters = args
|
||||
if parameters and isinstance(parameters[0], SimpleXMLElement):
|
||||
# merge xmlelement parameter ("raw" - already marshalled)
|
||||
for param in parameters[0].children():
|
||||
getattr(request,method).import_node(param)
|
||||
else:
|
||||
# marshall parameters:
|
||||
for k,v in parameters: # dict: tag=valor
|
||||
getattr(request,method).marshall(k,v)
|
||||
self.xml_request = request.as_xml()
|
||||
self.xml_response = self.send(method, self.xml_request)
|
||||
response = SimpleXMLElement(self.xml_response, namespace=self.namespace)
|
||||
if self.exceptions and response("Fault", ns=soap_namespaces.values(), error=False):
|
||||
raise SoapFault(unicode(response.faultcode), unicode(response.faultstring))
|
||||
return response
|
||||
|
||||
def send(self, method, xml):
|
||||
"Send SOAP request using HTTP"
|
||||
if self.location == 'test': return
|
||||
location = "%s" % self.location #?op=%s" % (self.location, method)
|
||||
if self.services:
|
||||
soap_action = self.action
|
||||
else:
|
||||
soap_action = self.action+method
|
||||
headers={
|
||||
'Content-type': 'text/xml; charset="UTF-8"',
|
||||
'Content-length': str(len(xml)),
|
||||
"SOAPAction": "\"%s\"" % (soap_action)
|
||||
}
|
||||
if self.trace:
|
||||
print "-"*80
|
||||
print "POST %s" % location
|
||||
print '\n'.join(["%s: %s" % (k,v) for k,v in headers.items()])
|
||||
print u"\n%s" % xml.decode("utf8","ignore")
|
||||
response, content = self.http.request(
|
||||
location,"POST", body=xml, headers=headers )
|
||||
self.response = response
|
||||
self.content = content
|
||||
if self.trace:
|
||||
print
|
||||
print '\n'.join(["%s: %s" % (k,v) for k,v in response.items()])
|
||||
print content#.decode("utf8","ignore")
|
||||
print "="*80
|
||||
return content
|
||||
|
||||
def get_operation(self, method):
|
||||
# try to find operation in wsdl file
|
||||
soap_ver = self.__soap_ns == 'soap12' and 'soap12' or 'soap11'
|
||||
if not self.service_port:
|
||||
for service_name, service in self.services.items():
|
||||
for port_name, port in [port for port in service['ports'].items()]:
|
||||
if port['soap_ver'] == soap_ver:
|
||||
self.service_port = service_name, port_name
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Cannot determine service in WSDL: "
|
||||
"SOAP version: %s" % soap_ver)
|
||||
else:
|
||||
port = self.services[self.service_port[0]]['ports'][self.service_port[1]]
|
||||
self.location = port['location']
|
||||
operation = port['operations'].get(unicode(method))
|
||||
if not operation:
|
||||
raise RuntimeError("Operation %s not found in WSDL: "
|
||||
"Service/Port Type: %s" %
|
||||
(method, self.service_port))
|
||||
return operation
|
||||
|
||||
def wsdl_call(self, method, *args, **kwargs):
|
||||
"Pre and post process SOAP call, input and output parameters using WSDL"
|
||||
soap_uri = soap_namespaces[self.__soap_ns]
|
||||
operation = self.get_operation(method)
|
||||
# get i/o type declarations:
|
||||
input = operation['input']
|
||||
output = operation['output']
|
||||
if 'action' in operation:
|
||||
self.action = operation['action']
|
||||
# sort parameters (same order as xsd:sequence)
|
||||
def sort_dict(od, d):
|
||||
if isinstance(od, dict):
|
||||
ret = OrderedDict()
|
||||
for k in od.keys():
|
||||
v = d.get(k)
|
||||
if v:
|
||||
if isinstance(v, dict):
|
||||
v = sort_dict(od[k], v)
|
||||
elif isinstance(v, list):
|
||||
v = [sort_dict(od[k][0], v1)
|
||||
for v1 in v]
|
||||
ret[str(k)] = v
|
||||
return ret
|
||||
else:
|
||||
return d
|
||||
if input and kwargs:
|
||||
params = sort_dict(input.values()[0], kwargs).items()
|
||||
method = input.keys()[0]
|
||||
#elif not input:
|
||||
#TODO: no message! (see wsmtxca.dummy)
|
||||
else:
|
||||
params = kwargs and kwargs.items()
|
||||
# call remote procedure
|
||||
response = self.call(method, *params)
|
||||
# parse results:
|
||||
resp = response('Body',ns=soap_uri).children().unmarshall(output)
|
||||
return resp and resp.values()[0] # pass Response tag children
|
||||
|
||||
def help(self, method):
|
||||
"Return operation documentation and invocation/returned value example"
|
||||
operation = self.get_operation(method)
|
||||
input = operation['input'].values()
|
||||
input = input and input[0]
|
||||
output = operation['output'].values()[0]
|
||||
return u"%s(%s)\n -> %s:\n\n%s" % (
|
||||
method,
|
||||
input and ", ".join("%s=%s" % (k,repr(v)) for k,v
|
||||
in input.items()) or "",
|
||||
output and output or "",
|
||||
operation.get("documentation",""),
|
||||
)
|
||||
|
||||
def wsdl(self, url, debug=False, cache=False):
|
||||
"Parse Web Service Description v1.1"
|
||||
soap_ns = {
|
||||
"http://schemas.xmlsoap.org/wsdl/soap/": 'soap11',
|
||||
"http://schemas.xmlsoap.org/wsdl/soap12/": 'soap12',
|
||||
}
|
||||
wsdl_uri="http://schemas.xmlsoap.org/wsdl/"
|
||||
xsd_uri="http://www.w3.org/2001/XMLSchema"
|
||||
xsi_uri="http://www.w3.org/2001/XMLSchema-instance"
|
||||
|
||||
get_local_name = lambda s: str((':' in s) and s.split(':')[1] or s)
|
||||
|
||||
REVERSE_TYPE_MAP = dict([(v,k) for k,v in TYPE_MAP.items()])
|
||||
|
||||
def fetch(url):
|
||||
"Fetch a document from a URL, save it locally if cache enabled"
|
||||
import os, hashlib
|
||||
# make md5 hash of the url for caching...
|
||||
filename = "%s.xml" % hashlib.md5(url).hexdigest()
|
||||
if isinstance(cache, basestring):
|
||||
filename = os.path.join(cache, filename)
|
||||
if cache and os.path.exists(filename):
|
||||
if debug: print "Reading file %s" % (filename, )
|
||||
f = open(filename, "r")
|
||||
xml = f.read()
|
||||
f.close()
|
||||
else:
|
||||
if debug: print "Fetching url %s" % (url, )
|
||||
f = urllib.urlopen(url)
|
||||
xml = f.read()
|
||||
if cache:
|
||||
if debug: print "Writing file %s" % (filename, )
|
||||
f = open(filename, "w")
|
||||
f.write(xml)
|
||||
f.close()
|
||||
return xml
|
||||
|
||||
# Open uri and read xml:
|
||||
xml = fetch(url)
|
||||
# Parse WSDL XML:
|
||||
wsdl = SimpleXMLElement(xml, namespace=wsdl_uri)
|
||||
|
||||
# detect soap prefix and uri (xmlns attributes of <definitions>)
|
||||
xsd_ns = None
|
||||
soap_uris = {}
|
||||
for k, v in wsdl[:]:
|
||||
if v in soap_ns and k.startswith("xmlns:"):
|
||||
soap_uris[get_local_name(k)] = v
|
||||
if v== xsd_uri and k.startswith("xmlns:"):
|
||||
xsd_ns = get_local_name(k)
|
||||
|
||||
# Extract useful data:
|
||||
self.namespace = wsdl['targetNamespace']
|
||||
self.documentation = unicode(wsdl('documentation', error=False) or '')
|
||||
|
||||
services = {}
|
||||
bindings = {} # binding_name: binding
|
||||
operations = {} # operation_name: operation
|
||||
port_type_bindings = {} # port_type_name: binding
|
||||
messages = {} # message: element
|
||||
elements = {} # element: type def
|
||||
|
||||
for service in wsdl.service:
|
||||
service_name=service['name']
|
||||
if not service_name:
|
||||
continue # empty service?
|
||||
if debug: print "Processing service", service_name
|
||||
serv = services.setdefault(service_name, {'ports': {}})
|
||||
serv['documentation']=service['documentation'] or ''
|
||||
for port in service.port:
|
||||
binding_name = get_local_name(port['binding'])
|
||||
address = port('address', ns=soap_uris.values(), error=False)
|
||||
location = address and address['location'] or None
|
||||
soap_uri = address and soap_uris.get(address.get_prefix())
|
||||
soap_ver = soap_uri and soap_ns.get(soap_uri)
|
||||
bindings[binding_name] = {'service_name': service_name,
|
||||
'location': location,
|
||||
'soap_uri': soap_uri, 'soap_ver': soap_ver,
|
||||
}
|
||||
serv['ports'][port['name']] = bindings[binding_name]
|
||||
|
||||
for binding in wsdl.binding:
|
||||
binding_name = binding['name']
|
||||
if debug: print "Processing binding", service_name
|
||||
soap_binding = binding('binding', ns=soap_uris.values(), error=False)
|
||||
transport = soap_binding and soap_binding['transport'] or None
|
||||
port_type_name = get_local_name(binding['type'])
|
||||
bindings[binding_name].update({
|
||||
'port_type_name': port_type_name,
|
||||
'transport': transport, 'operations': {},
|
||||
})
|
||||
port_type_bindings[port_type_name] = bindings[binding_name]
|
||||
for operation in binding.operation:
|
||||
op_name = operation['name']
|
||||
op = operation('operation',ns=soap_uris.values(), error=False)
|
||||
action = op and op['soapAction']
|
||||
d = operations.setdefault(op_name, {})
|
||||
bindings[binding_name]['operations'][op_name] = d
|
||||
d.update({'name': op_name})
|
||||
#if action: #TODO: separe operation_binding from operation
|
||||
if action:
|
||||
d["action"] = action
|
||||
|
||||
#TODO: cleanup element/schema/types parsing:
|
||||
def process_element(element_name, node):
|
||||
"Parse and define simple element types"
|
||||
if debug: print "Processing element", element_name
|
||||
for tag in node:
|
||||
if tag.get_local_name() in ("annotation", "documentation"):
|
||||
continue
|
||||
elif tag.get_local_name() in ('element', 'restriction'):
|
||||
if debug: print element_name,"has not children!",tag
|
||||
children = tag # element "alias"?
|
||||
alias = True
|
||||
elif tag.children():
|
||||
children = tag.children()
|
||||
alias = False
|
||||
else:
|
||||
if debug: print element_name,"has not children!",tag
|
||||
continue #TODO: abstract?
|
||||
d = OrderedDict()
|
||||
for e in children:
|
||||
t = e['type']
|
||||
if not t:
|
||||
t = e['base'] # complexContent (extension)!
|
||||
if not t:
|
||||
t = 'anyType' # no type given!
|
||||
t = t.split(":")
|
||||
if len(t)>1:
|
||||
ns, type_name = t
|
||||
else:
|
||||
ns, type_name = None, t[0]
|
||||
if element_name == type_name:
|
||||
continue # prevent infinite recursion
|
||||
uri = ns and e.get_namespace_uri(ns) or xsd_uri
|
||||
if uri==xsd_uri:
|
||||
# look for the type, None == any
|
||||
fn = REVERSE_TYPE_MAP.get(unicode(type_name), None)
|
||||
else:
|
||||
# complex type, postprocess later
|
||||
fn = elements.setdefault(unicode(type_name), OrderedDict())
|
||||
if e['name'] is not None and not alias:
|
||||
e_name = unicode(e['name'])
|
||||
d[e_name] = fn
|
||||
else:
|
||||
if debug: print "complexConent/simpleType/element", element_name, "=", type_name
|
||||
d[None] = fn
|
||||
if e['maxOccurs']=="unbounded":
|
||||
# it's an array... TODO: compound arrays?
|
||||
d.array = True
|
||||
if e is not None and e.get_local_name() == 'extension' and e.children():
|
||||
# extend base element:
|
||||
process_element(element_name, e.children())
|
||||
elements.setdefault(element_name, OrderedDict()).update(d)
|
||||
|
||||
# check axis2 namespace at schema types attributes
|
||||
self.namespace = dict(wsdl.types("schema", ns=xsd_uri)[:]).get('targetNamespace', self.namespace)
|
||||
|
||||
imported_schemas = {}
|
||||
|
||||
def preprocess_schema(schema):
|
||||
"Find schema elements and complex types"
|
||||
for element in schema.children():
|
||||
if element.get_local_name() in ('import', ):
|
||||
schema_namespace = element['namespace']
|
||||
schema_location = element['schemaLocation']
|
||||
if schema_location is None:
|
||||
if debug: print "Schema location not provided for %s!" % (schema_namespace, )
|
||||
continue
|
||||
if schema_location in imported_schemas:
|
||||
if debug: print "Schema %s already imported!" % (schema_location, )
|
||||
continue
|
||||
imported_schemas[schema_location] = schema_namespace
|
||||
if debug: print "Importing schema %s from %s" % (schema_namespace, schema_location)
|
||||
# Open uri and read xml:
|
||||
xml = fetch(schema_location)
|
||||
# Parse imported XML schema (recursively):
|
||||
imported_schema = SimpleXMLElement(xml, namespace=xsd_uri)
|
||||
preprocess_schema(imported_schema)
|
||||
|
||||
if element.get_local_name() in ('element', 'complexType', "simpleType"):
|
||||
element_name = unicode(element['name'])
|
||||
if debug: print "Parsing Element %s: %s" % (element.get_local_name(),element_name)
|
||||
if element.get_local_name() == 'complexType':
|
||||
children = element.children()
|
||||
elif element.get_local_name() == 'simpleType':
|
||||
children = element("restriction", ns=xsd_uri)
|
||||
elif element.get_local_name() == 'element' and element['type']:
|
||||
children = element
|
||||
else:
|
||||
children = element.children()
|
||||
if children:
|
||||
children = children.children()
|
||||
elif element.get_local_name() == 'element':
|
||||
children = element
|
||||
if children:
|
||||
process_element(element_name, children)
|
||||
|
||||
def postprocess_element(elements):
|
||||
"Fix unresolved references (elements referenced before its definition, thanks .net)"
|
||||
for k,v in elements.items():
|
||||
if isinstance(v, OrderedDict):
|
||||
if v.array:
|
||||
elements[k] = [v] # convert arrays to python lists
|
||||
if v!=elements: #TODO: fix recursive elements
|
||||
postprocess_element(v)
|
||||
if None in v and v[None]: # extension base?
|
||||
if isinstance(v[None], dict):
|
||||
for i, kk in enumerate(v[None]):
|
||||
# extend base -keep orginal order-
|
||||
elements[k].insert(kk, v[None][kk], i)
|
||||
del v[None]
|
||||
else: # "alias", just replace
|
||||
if debug: print "Replacing ", k , " = ", v[None]
|
||||
elements[k] = v[None]
|
||||
#break
|
||||
if isinstance(v, list):
|
||||
for n in v: # recurse list
|
||||
postprocess_element(n)
|
||||
|
||||
|
||||
# process current wsdl schema:
|
||||
for schema in wsdl.types("schema", ns=xsd_uri):
|
||||
preprocess_schema(schema)
|
||||
|
||||
postprocess_element(elements)
|
||||
|
||||
for message in wsdl.message:
|
||||
if debug: print "Processing message", message['name']
|
||||
part = message('part', error=False)
|
||||
element = {}
|
||||
if part:
|
||||
element_name = part['element']
|
||||
if not element_name:
|
||||
element_name = part['type'] # some uses type instead
|
||||
element_name = get_local_name(element_name)
|
||||
element = {element_name: elements.get(element_name)}
|
||||
messages[message['name']] = element
|
||||
|
||||
for port_type in wsdl.portType:
|
||||
port_type_name = port_type['name']
|
||||
if debug: print "Processing port type", port_type_name
|
||||
binding = port_type_bindings[port_type_name]
|
||||
|
||||
for operation in port_type.operation:
|
||||
op_name = operation['name']
|
||||
op = operations[op_name]
|
||||
op['documentation'] = unicode(operation('documentation', error=False) or '')
|
||||
if binding['soap_ver']:
|
||||
#TODO: separe operation_binding from operation (non SOAP?)
|
||||
input = get_local_name(operation.input['message'])
|
||||
output = get_local_name(operation.output['message'])
|
||||
op['input'] = messages[input]
|
||||
op['output'] = messages[output]
|
||||
|
||||
if debug:
|
||||
import pprint
|
||||
pprint.pprint(services)
|
||||
|
||||
return services
|
||||
|
||||
def parse_proxy(proxy_str):
|
||||
"Parses proxy address user:pass@host:port into a dict suitable for httplib2"
|
||||
proxy_dict = {}
|
||||
if proxy_str is None:
|
||||
return
|
||||
if "@" in proxy_str:
|
||||
user_pass, host_port = proxy_str.split("@")
|
||||
else:
|
||||
user_pass, host_port = "", proxy_str
|
||||
if ":" in host_port:
|
||||
host, port = host_port.split(":")
|
||||
proxy_dict['proxy_host'], proxy_dict['proxy_port'] = host, int(port)
|
||||
if ":" in user_pass:
|
||||
proxy_dict['proxy_user'], proxy_dict['proxy_pass'] = user_pass.split(":")
|
||||
return proxy_dict
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
import sys
|
||||
|
||||
if '--web2py' in sys.argv:
|
||||
# test local sample webservice exposed by web2py
|
||||
from client import SoapClient
|
||||
if not '--wsdl' in sys.argv:
|
||||
client = SoapClient(
|
||||
location = "http://127.0.0.1:8000/webservices/sample/call/soap",
|
||||
action = 'http://127.0.0.1:8000/webservices/sample/call/soap', # SOAPAction
|
||||
namespace = "http://127.0.0.1:8000/webservices/sample/call/soap",
|
||||
soap_ns='soap', trace = True, ns = False, exceptions=True)
|
||||
else:
|
||||
client = SoapClient(wsdl="http://127.0.0.1:8000/webservices/sample/call/soap?WSDL",trace=True)
|
||||
response = client.Dummy()
|
||||
print 'dummy', response
|
||||
response = client.Echo(value='hola')
|
||||
print 'echo', repr(response)
|
||||
response = client.AddIntegers(a=1,b=2)
|
||||
if not '--wsdl' in sys.argv:
|
||||
result = response.AddResult # manully convert returned type
|
||||
print int(result)
|
||||
else:
|
||||
result = response['AddResult']
|
||||
print result, type(result), "auto-unmarshalled"
|
||||
|
||||
if '--raw' in sys.argv:
|
||||
# raw (unmarshalled parameter) local sample webservice exposed by web2py
|
||||
from client import SoapClient
|
||||
client = SoapClient(
|
||||
location = "http://127.0.0.1:8000/webservices/sample/call/soap",
|
||||
action = 'http://127.0.0.1:8000/webservices/sample/call/soap', # SOAPAction
|
||||
namespace = "http://127.0.0.1:8000/webservices/sample/call/soap",
|
||||
soap_ns='soap', trace = True, ns = False)
|
||||
params = SimpleXMLElement("""<?xml version="1.0" encoding="UTF-8"?><AddIntegers><a>3</a><b>2</b></AddIntegers>""") # manully convert returned type
|
||||
response = client.call('AddIntegers',params)
|
||||
result = response.AddResult
|
||||
print int(result) # manully convert returned type
|
||||
|
||||
if '--ctg' in sys.argv:
|
||||
# test AFIP Agriculture webservice
|
||||
client = SoapClient(
|
||||
location = "https://fwshomo.afip.gov.ar/wsctg/services/CTGService",
|
||||
action = 'http://impl.service.wsctg.afip.gov.ar/CTGService/', # SOAPAction
|
||||
namespace = "http://impl.service.wsctg.afip.gov.ar/CTGService/",
|
||||
trace = True,
|
||||
ns = True)
|
||||
response = client.dummy()
|
||||
result = response.dummyResponse
|
||||
print str(result.appserver)
|
||||
print str(result.dbserver)
|
||||
print str(result.authserver)
|
||||
|
||||
if '--wsfe' in sys.argv:
|
||||
# Demo & Test (AFIP Electronic Invoice):
|
||||
ta_file = open("TA.xml")
|
||||
try:
|
||||
ta_string = ta_file.read() # read access ticket (wsaa.py)
|
||||
finally:
|
||||
ta_file.close()
|
||||
ta = SimpleXMLElement(ta_string)
|
||||
token = str(ta.credentials.token)
|
||||
sign = str(ta.credentials.sign)
|
||||
cuit = long(20267565393)
|
||||
id = 1234
|
||||
cbte =199
|
||||
client = SoapClient(
|
||||
location = "https://wswhomo.afip.gov.ar/wsfe/service.asmx",
|
||||
action = 'http://ar.gov.afip.dif.facturaelectronica/', # SOAPAction
|
||||
namespace = "http://ar.gov.afip.dif.facturaelectronica/",
|
||||
trace = True)
|
||||
results = client.FERecuperaQTYRequest(
|
||||
argAuth= {"Token": token, "Sign": sign, "cuit":long(cuit)}
|
||||
)
|
||||
if int(results.FERecuperaQTYRequestResult.RError.percode) != 0:
|
||||
print "Percode: %s" % results.FERecuperaQTYRequestResult.RError.percode
|
||||
print "MSGerror: %s" % results.FERecuperaQTYRequestResult.RError.perrmsg
|
||||
else:
|
||||
print int(results.FERecuperaQTYRequestResult.qty.value)
|
||||
|
||||
if '--feriados' in sys.argv:
|
||||
# Demo & Test: Argentina Holidays (Ministerio del Interior):
|
||||
# this webservice seems disabled
|
||||
from datetime import datetime, timedelta
|
||||
client = SoapClient(
|
||||
location = "http://webservices.mininterior.gov.ar/Feriados/Service.svc",
|
||||
action = 'http://tempuri.org/IMyService/', # SOAPAction
|
||||
namespace = "http://tempuri.org/FeriadoDS.xsd",
|
||||
trace = True)
|
||||
dt1 = datetime.today() - timedelta(days=60)
|
||||
dt2 = datetime.today() + timedelta(days=60)
|
||||
feriadosXML = client.FeriadosEntreFechasas_xml(dt1=dt1.isoformat(), dt2=dt2.isoformat());
|
||||
print feriadosXML
|
||||
|
||||
if '--wsdl-parse' in sys.argv:
|
||||
client = SoapClient()
|
||||
# Test PySimpleSOAP WSDL
|
||||
client.wsdl("file:C:/test.wsdl", debug=True)
|
||||
# Test Java Axis WSDL:
|
||||
client.wsdl('https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl',debug=True)
|
||||
# Test .NET 2.0 WSDL:
|
||||
client.wsdl('https://wswhomo.afip.gov.ar/wsfe/service.asmx?WSDL',debug=True)
|
||||
client.wsdl('https://wswhomo.afip.gov.ar/wsfex/service.asmx?WSDL',debug=True)
|
||||
client.wsdl('https://testdia.afip.gov.ar/Dia/Ws/wDigDepFiel/wDigDepFiel.asmx?WSDL',debug=True)
|
||||
# Test JBoss WSDL:
|
||||
client.wsdl('https://fwshomo.afip.gov.ar/wsctg/services/CTGService?wsdl',debug=True)
|
||||
client.wsdl('https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl',debug=True)
|
||||
|
||||
if '--wsdl-client' in sys.argv:
|
||||
client = SoapClient(wsdl='https://wswhomo.afip.gov.ar/wsfex/service.asmx?WSDL',trace=True)
|
||||
results = client.FEXDummy()
|
||||
print results['FEXDummyResult']['AppServer']
|
||||
print results['FEXDummyResult']['DbServer']
|
||||
print results['FEXDummyResult']['AuthServer']
|
||||
ta_file = open("TA.xml")
|
||||
try:
|
||||
ta_string = ta_file.read() # read access ticket (wsaa.py)
|
||||
finally:
|
||||
ta_file.close()
|
||||
ta = SimpleXMLElement(ta_string)
|
||||
token = str(ta.credentials.token)
|
||||
sign = str(ta.credentials.sign)
|
||||
response = client.FEXGetCMP(
|
||||
Auth={"Token": token, "Sign": sign, "Cuit": 20267565393},
|
||||
Cmp={"Tipo_cbte": 19, "Punto_vta": 1, "Cbte_nro": 1})
|
||||
result = response['FEXGetCMPResult']
|
||||
if False: print result
|
||||
if 'FEXErr' in result:
|
||||
print "FEXError:", result['FEXErr']['ErrCode'], result['FEXErr']['ErrCode']
|
||||
cbt = result['FEXResultGet']
|
||||
print cbt['Cae']
|
||||
FEX_event = result['FEXEvents']
|
||||
print FEX_event['EventCode'], FEX_event['EventMsg']
|
||||
|
||||
if '--wsdl-ctg' in sys.argv:
|
||||
client = SoapClient(wsdl='https://fwshomo.afip.gov.ar/wsctg/services/CTGService?wsdl',
|
||||
trace=True, ns = "ctg")
|
||||
results = client.dummy()
|
||||
print results
|
||||
print results['DummyResponse']['appserver']
|
||||
print results['DummyResponse']['dbserver']
|
||||
print results['DummyResponse']['authserver']
|
||||
ta_file = open("TA.xml")
|
||||
try:
|
||||
ta_string = ta_file.read() # read access ticket (wsaa.py)
|
||||
finally:
|
||||
ta_file.close()
|
||||
ta = SimpleXMLElement(ta_string)
|
||||
token = str(ta.credentials.token)
|
||||
sign = str(ta.credentials.sign)
|
||||
print client.help("obtenerProvincias")
|
||||
response = client.obtenerProvincias(auth={"token":token, "sign":sign, "cuitRepresentado":20267565393})
|
||||
print "response=",response
|
||||
for ret in response:
|
||||
print ret['return']['codigoProvincia'], ret['return']['descripcionProvincia'].encode("latin1")
|
||||
prueba = dict(numeroCartaDePorte=512345678, codigoEspecie=23,
|
||||
cuitRemitenteComercial=20267565393, cuitDestino=20267565393, cuitDestinatario=20267565393,
|
||||
codigoLocalidadOrigen=3058, codigoLocalidadDestino=3059,
|
||||
codigoCosecha='0910', pesoNetoCarga=1000, cantHoras=1,
|
||||
patenteVehiculo='CZO985', cuitTransportista=20267565393,
|
||||
numeroCTG="43816783", transaccion='10000001681', observaciones='',
|
||||
)
|
||||
|
||||
response = client.solicitarCTG(
|
||||
auth={"token": token, "sign": sign, "cuitRepresentado": 20267565393},
|
||||
solicitarCTGRequest= prueba)
|
||||
|
||||
print response['return']['numeroCTG']
|
||||
|
||||
##print parse_proxy(None)
|
||||
##print parse_proxy("host:1234")
|
||||
##print parse_proxy("user:pass@host:1234")
|
||||
##sys.exit(0)
|
||||
|
||||
Executable
+455
@@ -0,0 +1,455 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: latin-1 -*-
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by the
|
||||
# Free Software Foundation; either version 3, 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 MERCHANTIBILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
# for more details.
|
||||
|
||||
"Simple SOAP Server implementation"
|
||||
|
||||
__author__ = "Mariano Reingart (reingart@gmail.com)"
|
||||
__copyright__ = "Copyright (C) 2010 Mariano Reingart"
|
||||
__license__ = "LGPL 3.0"
|
||||
__version__ = "1.02c"
|
||||
|
||||
from simplexml import SimpleXMLElement, TYPE_MAP, DateTime, Date, Decimal
|
||||
|
||||
DEBUG = False
|
||||
|
||||
|
||||
class SoapDispatcher(object):
|
||||
"Simple Dispatcher for SOAP Server"
|
||||
|
||||
def __init__(self, name, documentation='', action='', location='',
|
||||
namespace=None, prefix=False,
|
||||
soap_uri="http://schemas.xmlsoap.org/soap/envelope/",
|
||||
soap_ns='soap',
|
||||
**kwargs):
|
||||
self.methods = {}
|
||||
self.name = name
|
||||
self.documentation = documentation
|
||||
self.action = action # base SoapAction
|
||||
self.location = location
|
||||
self.namespace = namespace # targetNamespace
|
||||
self.prefix = prefix
|
||||
self.soap_ns = soap_ns
|
||||
self.soap_uri = soap_uri
|
||||
|
||||
def register_function(self, name, fn, returns=None, args=None, doc=None):
|
||||
self.methods[name] = fn, returns, args, doc or getattr(fn,"__doc__","")
|
||||
|
||||
def dispatch(self, xml, action=None):
|
||||
"Receive and proccess SOAP call"
|
||||
# default values:
|
||||
prefix = self.prefix
|
||||
ret = fault = None
|
||||
soap_ns, soap_uri = self.soap_ns, self.soap_uri
|
||||
soap_fault_code = 'VersionMismatch'
|
||||
|
||||
try:
|
||||
request = SimpleXMLElement(xml, namespace=self.namespace)
|
||||
|
||||
# detect soap prefix and uri (xmlns attributes of Envelope)
|
||||
for k, v in request[:]:
|
||||
if v in ("http://schemas.xmlsoap.org/soap/envelope/",
|
||||
"http://www.w3.org/2003/05/soap-env",):
|
||||
soap_ns = request.attributes()[k].localName
|
||||
soap_uri = request.attributes()[k].value
|
||||
|
||||
soap_fault_code = 'Client'
|
||||
|
||||
# parse request message and get local method
|
||||
method = request('Body', ns=soap_uri).children()(0)
|
||||
if action:
|
||||
# method name = action
|
||||
name = action[len(self.action)+1:-1]
|
||||
prefix = self.prefix
|
||||
if not action or not name:
|
||||
# method name = input message name
|
||||
name = method.get_local_name()
|
||||
prefix = method.get_prefix()
|
||||
|
||||
if DEBUG: print "dispatch method", name
|
||||
function, returns_types, args_types, doc = self.methods[name]
|
||||
|
||||
# de-serialize parameters (if type definitions given)
|
||||
if args_types:
|
||||
args = method.children().unmarshall(args_types)
|
||||
elif args_types is None:
|
||||
args = {'request':method} # send raw request
|
||||
else:
|
||||
args = {} # no parameters
|
||||
|
||||
soap_fault_code = 'Server'
|
||||
# execute function
|
||||
ret = function(**args)
|
||||
if DEBUG: print ret
|
||||
|
||||
except Exception, e:
|
||||
import sys
|
||||
etype, evalue, etb = sys.exc_info()
|
||||
if DEBUG:
|
||||
import traceback
|
||||
detail = ''.join(traceback.format_exception(etype, evalue, etb))
|
||||
detail += '\n\nXML REQUEST\n\n' + xml
|
||||
else:
|
||||
detail = None
|
||||
fault = {'faultcode': "%s.%s" % (soap_fault_code, etype.__name__),
|
||||
'faultstring': unicode(evalue),
|
||||
'detail': detail}
|
||||
|
||||
# build response message
|
||||
if not prefix:
|
||||
xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s"/>"""
|
||||
else:
|
||||
xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s"
|
||||
xmlns:%(prefix)s="%(namespace)s"/>"""
|
||||
|
||||
xml = xml % {'namespace': self.namespace, 'prefix': prefix,
|
||||
'soap_ns': soap_ns, 'soap_uri': soap_uri}
|
||||
|
||||
response = SimpleXMLElement(xml, namespace=self.namespace,
|
||||
prefix=prefix)
|
||||
|
||||
response['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
response['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema"
|
||||
|
||||
body = response.add_child("%s:Body" % soap_ns, ns=False)
|
||||
if fault:
|
||||
# generate a Soap Fault (with the python exception)
|
||||
body.marshall("%s:Fault" % soap_ns, fault, ns=False)
|
||||
else:
|
||||
# return normal value
|
||||
res = body.add_child("%sResponse" % name, ns=prefix)
|
||||
if not prefix:
|
||||
res['xmlns'] = self.namespace # add target namespace
|
||||
|
||||
# serialize returned values (response) if type definition available
|
||||
if returns_types:
|
||||
if not isinstance(ret, dict):
|
||||
res.marshall(returns_types.keys()[0], ret, )
|
||||
else:
|
||||
for k,v in ret.items():
|
||||
res.marshall(k, v)
|
||||
elif returns_types is None:
|
||||
# merge xmlelement returned
|
||||
res.import_node(ret)
|
||||
|
||||
return response.as_xml()
|
||||
|
||||
# Introspection functions:
|
||||
|
||||
def list_methods(self):
|
||||
"Return a list of aregistered operations"
|
||||
return [(method, doc) for method, (function, returns, args, doc) in self.methods.items()]
|
||||
|
||||
def help(self, method=None):
|
||||
"Generate sample request and response messages"
|
||||
(function, returns, args, doc) = self.methods[method]
|
||||
xml = """
|
||||
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap:Body><%(method)s xmlns="%(namespace)s"/></soap:Body>
|
||||
</soap:Envelope>""" % {'method':method, 'namespace':self.namespace}
|
||||
request = SimpleXMLElement(xml, namespace=self.namespace, prefix=self.prefix)
|
||||
if args:
|
||||
items = args.items()
|
||||
elif args is None:
|
||||
items = [('value', None)]
|
||||
else:
|
||||
items = []
|
||||
for k,v in items:
|
||||
request(method).marshall(k, v, add_comments=True, ns=False)
|
||||
|
||||
xml = """
|
||||
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap:Body><%(method)sResponse xmlns="%(namespace)s"/></soap:Body>
|
||||
</soap:Envelope>""" % {'method':method, 'namespace':self.namespace}
|
||||
response = SimpleXMLElement(xml, namespace=self.namespace, prefix=self.prefix)
|
||||
if returns:
|
||||
items = returns.items()
|
||||
elif args is None:
|
||||
items = [('value', None)]
|
||||
else:
|
||||
items = []
|
||||
for k,v in items:
|
||||
response('%sResponse'%method).marshall(k, v, add_comments=True, ns=False)
|
||||
|
||||
return request.as_xml(pretty=True), response.as_xml(pretty=True), doc
|
||||
|
||||
|
||||
def wsdl(self):
|
||||
"Generate Web Service Description v1.1"
|
||||
xml = """<?xml version="1.0"?>
|
||||
<wsdl:definitions name="%(name)s"
|
||||
targetNamespace="%(namespace)s"
|
||||
xmlns:tns="%(namespace)s"
|
||||
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
|
||||
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<wsdl:documentation xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">%(documentation)s</wsdl:documentation>
|
||||
|
||||
<wsdl:types>
|
||||
<xsd:schema targetNamespace="%(namespace)s"
|
||||
elementFormDefault="qualified"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
</xsd:schema>
|
||||
</wsdl:types>
|
||||
|
||||
</wsdl:definitions>
|
||||
""" % {'namespace': self.namespace, 'name': self.name, 'documentation': self.documentation}
|
||||
wsdl = SimpleXMLElement(xml)
|
||||
|
||||
for method, (function, returns, args, doc) in self.methods.items():
|
||||
# create elements:
|
||||
|
||||
def parse_element(name, values, array=False, complex=False):
|
||||
if not complex:
|
||||
element = wsdl('wsdl:types')('xsd:schema').add_child('xsd:element')
|
||||
complex = element.add_child("xsd:complexType")
|
||||
else:
|
||||
complex = wsdl('wsdl:types')('xsd:schema').add_child('xsd:complexType')
|
||||
element = complex
|
||||
element['name'] = name
|
||||
if values:
|
||||
items = values
|
||||
elif values is None:
|
||||
items = [('value', None)]
|
||||
else:
|
||||
items = []
|
||||
if not array and items:
|
||||
all = complex.add_child("xsd:all")
|
||||
elif items:
|
||||
all = complex.add_child("xsd:sequence")
|
||||
for k,v in items:
|
||||
e = all.add_child("xsd:element")
|
||||
e['name'] = k
|
||||
if array:
|
||||
e[:]={'minOccurs': "0", 'maxOccurs': "unbounded"}
|
||||
if v in TYPE_MAP.keys():
|
||||
t='xsd:%s' % TYPE_MAP[v]
|
||||
elif v is None:
|
||||
t='xsd:anyType'
|
||||
elif isinstance(v, list):
|
||||
n="ArrayOf%s%s" % (name, k)
|
||||
l = []
|
||||
for d in v:
|
||||
l.extend(d.items())
|
||||
parse_element(n, l, array=True, complex=True)
|
||||
t = "tns:%s" % n
|
||||
elif isinstance(v, dict):
|
||||
n="%s%s" % (name, k)
|
||||
parse_element(n, v.items(), complex=True)
|
||||
t = "tns:%s" % n
|
||||
e.add_attribute('type', t)
|
||||
|
||||
parse_element("%s" % method, args and args.items())
|
||||
parse_element("%sResponse" % method, returns and returns.items())
|
||||
|
||||
# create messages:
|
||||
for m,e in ('Input',''), ('Output','Response'):
|
||||
message = wsdl.add_child('wsdl:message')
|
||||
message['name'] = "%s%s" % (method, m)
|
||||
part = message.add_child("wsdl:part")
|
||||
part[:] = {'name': 'parameters',
|
||||
'element': 'tns:%s%s' % (method,e)}
|
||||
|
||||
# create ports
|
||||
portType = wsdl.add_child('wsdl:portType')
|
||||
portType['name'] = "%sPortType" % self.name
|
||||
for method, (function, returns, args, doc) in self.methods.items():
|
||||
op = portType.add_child('wsdl:operation')
|
||||
op['name'] = method
|
||||
if doc:
|
||||
op.add_child("wsdl:documentation", doc)
|
||||
input = op.add_child("wsdl:input")
|
||||
input['message'] = "tns:%sInput" % method
|
||||
output = op.add_child("wsdl:output")
|
||||
output['message'] = "tns:%sOutput" % method
|
||||
|
||||
# create bindings
|
||||
binding = wsdl.add_child('wsdl:binding')
|
||||
binding['name'] = "%sBinding" % self.name
|
||||
binding['type'] = "tns:%sPortType" % self.name
|
||||
soapbinding = binding.add_child('soap:binding')
|
||||
soapbinding['style'] = "document"
|
||||
soapbinding['transport'] = "http://schemas.xmlsoap.org/soap/http"
|
||||
for method in self.methods.keys():
|
||||
op = binding.add_child('wsdl:operation')
|
||||
op['name'] = method
|
||||
soapop = op.add_child('soap:operation')
|
||||
soapop['soapAction'] = self.action + method
|
||||
soapop['style'] = 'document'
|
||||
input = op.add_child("wsdl:input")
|
||||
##input.add_attribute('name', "%sInput" % method)
|
||||
soapbody = input.add_child("soap:body")
|
||||
soapbody["use"] = "literal"
|
||||
output = op.add_child("wsdl:output")
|
||||
##output.add_attribute('name', "%sOutput" % method)
|
||||
soapbody = output.add_child("soap:body")
|
||||
soapbody["use"] = "literal"
|
||||
|
||||
service = wsdl.add_child('wsdl:service')
|
||||
service["name"] = "%sService" % self.name
|
||||
service.add_child('wsdl:documentation', text=self.documentation)
|
||||
port=service.add_child('wsdl:port')
|
||||
port["name"] = "%s" % self.name
|
||||
port["binding"] = "tns:%sBinding" % self.name
|
||||
soapaddress = port.add_child('soap:address')
|
||||
soapaddress["location"] = self.location
|
||||
return wsdl.as_xml(pretty=True)
|
||||
|
||||
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
class SOAPHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
"User viewable help information and wsdl"
|
||||
args = self.path[1:].split("?")
|
||||
print "serving", args
|
||||
if self.path != "/" and args[0] not in self.server.dispatcher.methods.keys():
|
||||
self.send_error(404, "Method not found: %s" % args[0])
|
||||
else:
|
||||
if self.path == "/":
|
||||
# return wsdl if no method supplied
|
||||
response = self.server.dispatcher.wsdl()
|
||||
else:
|
||||
# return supplied method help (?request or ?response messages)
|
||||
req, res, doc = self.server.dispatcher.help(args[0])
|
||||
if len(args)==1 or args[1]=="request":
|
||||
response = req
|
||||
else:
|
||||
response = res
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/xml")
|
||||
self.end_headers()
|
||||
self.wfile.write(response)
|
||||
|
||||
def do_POST(self):
|
||||
"SOAP POST gateway"
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/xml")
|
||||
self.end_headers()
|
||||
request = self.rfile.read(int(self.headers.getheader('content-length')))
|
||||
response = self.server.dispatcher.dispatch(request)
|
||||
self.wfile.write(response)
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
import sys
|
||||
|
||||
dispatcher = SoapDispatcher(
|
||||
name = "PySimpleSoapSample",
|
||||
location = "http://localhost:8008/",
|
||||
action = 'http://localhost:8008/', # SOAPAction
|
||||
namespace = "http://example.com/pysimplesoapsamle/", prefix="ns0",
|
||||
documentation = 'Example soap service using PySimpleSoap',
|
||||
trace = True,
|
||||
ns = True)
|
||||
|
||||
def adder(p,c, dt=None):
|
||||
"Add several values"
|
||||
print c[0]['d'],c[1]['d'],
|
||||
import datetime
|
||||
dt = dt + datetime.timedelta(365)
|
||||
return {'ab': p['a']+p['b'], 'dd': c[0]['d']+c[1]['d'], 'dt': dt}
|
||||
|
||||
def dummy(in0):
|
||||
"Just return input"
|
||||
return in0
|
||||
|
||||
def echo(request):
|
||||
"Copy request->response (generic, any type)"
|
||||
return request.value
|
||||
|
||||
dispatcher.register_function('Adder', adder,
|
||||
returns={'AddResult': {'ab': int, 'dd': str } },
|
||||
args={'p': {'a': int,'b': int}, 'dt': Date, 'c': [{'d': Decimal}]})
|
||||
|
||||
dispatcher.register_function('Dummy', dummy,
|
||||
returns={'out0': str},
|
||||
args={'in0': str})
|
||||
|
||||
dispatcher.register_function('Echo', echo)
|
||||
|
||||
if '--local' in sys.argv:
|
||||
|
||||
wsdl=dispatcher.wsdl()
|
||||
print wsdl
|
||||
testfile = open("C:/test.wsdl","w")
|
||||
try:
|
||||
testfile.write(wsdl)
|
||||
finally:
|
||||
testfile.close()
|
||||
# dummy local test (clasic soap dialect)
|
||||
xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap:Body>
|
||||
<Adder xmlns="http://example.com/sample.wsdl">
|
||||
<p><a>1</a><b>2</b></p><c><d>5000000.1</d><d>.2</d></c><dt>20100724</dt>
|
||||
</Adder>
|
||||
</soap:Body>
|
||||
</soap:Envelope>"""
|
||||
|
||||
print dispatcher.dispatch(xml)
|
||||
|
||||
# dummy local test (modern soap dialect, SoapUI)
|
||||
xml = """
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:pys="http://example.com/pysimplesoapsamle/">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<pys:Adder>
|
||||
<pys:p><pys:a>9</pys:a><pys:b>3</pys:b></pys:p>
|
||||
<pys:dt>19690720<!--1969-07-20T21:28:00--></pys:dt>
|
||||
<pys:c><pys:d>10.001</pys:d><pys:d>5.02</pys:d></pys:c>
|
||||
</pys:Adder>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
"""
|
||||
print dispatcher.dispatch(xml)
|
||||
|
||||
# echo local test (generic soap service)
|
||||
xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<soap:Body>
|
||||
<Echo xmlns="http://example.com/sample.wsdl">
|
||||
<value xsi:type="xsd:string">Hello world</value>
|
||||
</Echo>
|
||||
</soap:Body>
|
||||
</soap:Envelope>"""
|
||||
|
||||
print dispatcher.dispatch(xml)
|
||||
|
||||
|
||||
for method, doc in dispatcher.list_methods():
|
||||
request, response, doc = dispatcher.help(method)
|
||||
##print request
|
||||
##print response
|
||||
|
||||
if '--serve' in sys.argv:
|
||||
print "Starting server..."
|
||||
httpd = HTTPServer(("", 8008), SOAPHandler)
|
||||
httpd.dispatcher = dispatcher
|
||||
httpd.serve_forever()
|
||||
|
||||
if '--consume' in sys.argv:
|
||||
from client import SoapClient
|
||||
client = SoapClient(
|
||||
location = "http://localhost:8008/",
|
||||
action = 'http://localhost:8008/', # SOAPAction
|
||||
namespace = "http://example.com/sample.wsdl",
|
||||
soap_ns='soap',
|
||||
trace = True,
|
||||
ns = False)
|
||||
response = client.Adder(p={'a':1,'b':2},dt='20100724',c=[{'d':'1.20'},{'d':'2.01'}])
|
||||
result = response.AddResult
|
||||
print int(result.ab)
|
||||
print str(result.dd)
|
||||
|
||||
|
||||
|
||||
Executable
+416
@@ -0,0 +1,416 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: latin-1 -*-
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by the
|
||||
# Free Software Foundation; either version 3, 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 MERCHANTIBILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
# for more details.
|
||||
|
||||
"Simple XML manipulation"
|
||||
|
||||
__author__ = "Mariano Reingart (reingart@gmail.com)"
|
||||
__copyright__ = "Copyright (C) 2008/009 Mariano Reingart"
|
||||
__license__ = "LGPL 3.0"
|
||||
__version__ = "1.02c"
|
||||
|
||||
import xml.dom.minidom
|
||||
from decimal import Decimal
|
||||
import datetime
|
||||
import time
|
||||
|
||||
DEBUG = False
|
||||
|
||||
# Functions to serialize/unserialize special immutable types:
|
||||
datetime_u = lambda s: datetime.datetime.strptime(s, "%Y-%m-%dT%H:%M:%S")
|
||||
datetime_m = lambda dt: dt.isoformat('T')
|
||||
date_u = lambda s: datetime.datetime.strptime(s[0:10], "%Y-%m-%d").date()
|
||||
date_m = lambda d: d.strftime("%Y-%m-%d")
|
||||
time_u = lambda s: datetime.datetime.strptime(s, "%H:%M:%S").time()
|
||||
time_m = lambda d: d.strftime("%H%M%S")
|
||||
bool_u = lambda s: {'0':False, 'false': False, '1': True, 'true': True}[s]
|
||||
|
||||
# aliases:
|
||||
class Alias():
|
||||
def __init__(self, py_type, xml_type):
|
||||
self.py_type, self.xml_type = py_type, xml_type
|
||||
def __call__(self, value):
|
||||
return self.py_type(value)
|
||||
def __repr__(self):
|
||||
return "<alias '%s' for '%s'>" % (self.xml_type, self.py_type)
|
||||
|
||||
byte = Alias(str,'byte')
|
||||
short = Alias(int,'short')
|
||||
double = Alias(float,'double')
|
||||
integer = Alias(long,'integer')
|
||||
DateTime = datetime.datetime
|
||||
Date = datetime.date
|
||||
Time = datetime.time
|
||||
|
||||
# Define convertion function (python type): xml schema type
|
||||
TYPE_MAP = {str:'string',unicode:'string',
|
||||
bool:'boolean', short:'short', byte:'byte',
|
||||
int:'int', long:'long', integer:'integer',
|
||||
float:'float', double:'double',
|
||||
Decimal:'decimal',
|
||||
datetime.datetime:'dateTime', datetime.date:'date',
|
||||
}
|
||||
TYPE_MARSHAL_FN = {datetime.datetime:datetime_m, datetime.date:date_m,}
|
||||
TYPE_UNMARSHAL_FN = {datetime.datetime:datetime_u, datetime.date:date_u,
|
||||
bool:bool_u,
|
||||
}
|
||||
|
||||
|
||||
class OrderedDict(dict):
|
||||
"Minimal ordered dictionary for xsd:sequences"
|
||||
def __init__(self):
|
||||
self.__keys = []
|
||||
self.array = False
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self.__keys:
|
||||
self.__keys.append(key)
|
||||
dict.__setitem__(self, key, value)
|
||||
def insert(self, key, value, index=0):
|
||||
if key not in self.__keys:
|
||||
self.__keys.insert(index, key)
|
||||
dict.__setitem__(self, key, value)
|
||||
def __delitem__(self, key):
|
||||
if key in self.__keys:
|
||||
self.__keys.remove(key)
|
||||
dict.__delitem__(self, key)
|
||||
def __iter__(self):
|
||||
return iter(self.__keys)
|
||||
def keys(self):
|
||||
return self.__keys
|
||||
def items(self):
|
||||
return [(key, self[key]) for key in self.__keys]
|
||||
def update(self, other):
|
||||
for k,v in other.items():
|
||||
self[k] = v
|
||||
if isinstance(other, OrderedDict):
|
||||
self.array = other.array
|
||||
def __str__(self):
|
||||
return "*%s*" % dict.__str__(self)
|
||||
def __repr__(self):
|
||||
s= "*{%s}*" % ", ".join(['%s: %s' % (repr(k),repr(v)) for k,v in self.items()])
|
||||
if self.array and False:
|
||||
s = "[%s]" % s
|
||||
return s
|
||||
|
||||
|
||||
class SimpleXMLElement(object):
|
||||
"Simple XML manipulation (simil PHP)"
|
||||
|
||||
def __init__(self, text = None, elements = None, document = None, namespace = None, prefix=None):
|
||||
self.__ns = namespace
|
||||
self.__prefix = prefix
|
||||
if text:
|
||||
try:
|
||||
self.__document = xml.dom.minidom.parseString(text)
|
||||
except:
|
||||
if DEBUG: print text
|
||||
raise
|
||||
self.__elements = [self.__document.documentElement]
|
||||
else:
|
||||
self.__elements = elements
|
||||
self.__document = document
|
||||
|
||||
def add_child(self,name,text=None,ns=True):
|
||||
"Adding a child tag to a node"
|
||||
if not ns or not self.__ns:
|
||||
if DEBUG: print "adding %s" % (name)
|
||||
element = self.__document.createElement(name)
|
||||
else:
|
||||
if DEBUG: print "adding %s ns %s %s" % (name, self.__ns,ns)
|
||||
if self.__prefix:
|
||||
element = self.__document.createElementNS(self.__ns, "%s:%s" % (self.__prefix, name))
|
||||
else:
|
||||
element = self.__document.createElementNS(self.__ns, name)
|
||||
if text:
|
||||
if isinstance(text, unicode):
|
||||
element.appendChild(self.__document.createTextNode(text))
|
||||
else:
|
||||
element.appendChild(self.__document.createTextNode(str(text)))
|
||||
self._element.appendChild(element)
|
||||
return SimpleXMLElement(
|
||||
elements=[element],
|
||||
document=self.__document,
|
||||
namespace=self.__ns,
|
||||
prefix=self.__prefix)
|
||||
|
||||
def __setattr__(self, tag, text):
|
||||
"Add text child tag node (short form)"
|
||||
if tag.startswith("_"):
|
||||
object.__setattr__(self, tag, text)
|
||||
else:
|
||||
if DEBUG: print "__setattr__(%s,%s)" % (tag, text)
|
||||
self.add_child(tag,text)
|
||||
|
||||
def add_comment(self, data):
|
||||
"Add an xml comment to this child"
|
||||
comment = self.__document.createComment(data)
|
||||
self._element.appendChild(comment)
|
||||
|
||||
def as_xml(self,filename=None,pretty=False):
|
||||
"Return the XML representation of the document"
|
||||
if not pretty:
|
||||
return self.__document.toxml('UTF-8')
|
||||
else:
|
||||
return self.__document.toprettyxml(encoding='UTF-8')
|
||||
|
||||
def __repr__(self):
|
||||
"Return the XML representation of this tag"
|
||||
return self._element.toxml('UTF-8')
|
||||
|
||||
def get_name(self):
|
||||
"Return the tag name of this node"
|
||||
return self._element.tagName
|
||||
|
||||
def get_local_name(self):
|
||||
"Return the tag loca name (prefix:name) of this node"
|
||||
return self._element.localName
|
||||
|
||||
def get_prefix(self):
|
||||
"Return the namespace prefix of this node"
|
||||
return self._element.prefix
|
||||
|
||||
def get_namespace_uri(self, ns):
|
||||
"Return the namespace uri for a prefix"
|
||||
v = self.__document.documentElement.attributes['xmlns:%s' % ns]
|
||||
return v.value
|
||||
|
||||
def attributes(self):
|
||||
"Return a dict of attributes for this tag"
|
||||
#TODO: use slice syntax [:]?
|
||||
return self._element.attributes
|
||||
|
||||
def __getitem__(self, item):
|
||||
"Return xml tag attribute value or a slice of attributes (iter)"
|
||||
if DEBUG: print "__getitem__(%s)" % item
|
||||
if isinstance(item,basestring):
|
||||
if self._element.hasAttribute(item):
|
||||
return self._element.attributes[item].value
|
||||
elif isinstance(item, slice):
|
||||
# return a list with name:values
|
||||
return self._element.attributes.items()[item]
|
||||
else:
|
||||
# return element by index (position)
|
||||
element = self.__elements[item]
|
||||
return SimpleXMLElement(
|
||||
elements=[element],
|
||||
document=self.__document,
|
||||
namespace=self.__ns,
|
||||
prefix=self.__prefix)
|
||||
|
||||
def add_attribute(self, name, value):
|
||||
"Set an attribute value from a string"
|
||||
self._element.setAttribute(name, value)
|
||||
|
||||
def __setitem__(self, item, value):
|
||||
"Set an attribute value"
|
||||
if isinstance(item,basestring):
|
||||
self.add_attribute(item, value)
|
||||
elif isinstance(item, slice):
|
||||
# set multiple attributes at once
|
||||
for k, v in value.items():
|
||||
self.add_attribute(k, v)
|
||||
|
||||
def __call__(self, tag=None, ns=None, children=False, error=True):
|
||||
"Search (even in child nodes) and return a child tag by name"
|
||||
try:
|
||||
if tag is None:
|
||||
# if no name given, iterate over siblings (same level)
|
||||
return self.__iter__()
|
||||
if children:
|
||||
# future: filter children? by ns?
|
||||
return self.children()
|
||||
elements = None
|
||||
if isinstance(tag, int):
|
||||
# return tag by index
|
||||
elements=[self.__elements[tag]]
|
||||
if ns and not elements:
|
||||
for ns_uri in isinstance(ns, (tuple, list)) and ns or (ns, ):
|
||||
if DEBUG: print "searching %s by ns=%s" % (tag,ns_uri)
|
||||
elements = self._element.getElementsByTagNameNS(ns_uri, tag)
|
||||
if elements:
|
||||
break
|
||||
if self.__ns and not elements:
|
||||
if DEBUG: print "searching %s by ns=%s" % (tag, self.__ns)
|
||||
elements = self._element.getElementsByTagNameNS(self.__ns, tag)
|
||||
if not elements:
|
||||
if DEBUG: print "searching %s " % (tag)
|
||||
elements = self._element.getElementsByTagName(tag)
|
||||
if not elements:
|
||||
if DEBUG: print self._element.toxml()
|
||||
if error:
|
||||
raise AttributeError("No elements found")
|
||||
else:
|
||||
return
|
||||
return SimpleXMLElement(
|
||||
elements=elements,
|
||||
document=self.__document,
|
||||
namespace=self.__ns,
|
||||
prefix=self.__prefix)
|
||||
except AttributeError, e:
|
||||
raise AttributeError("Tag not found: %s (%s)" % (tag, str(e)))
|
||||
|
||||
def __getattr__(self, tag):
|
||||
"Shortcut for __call__"
|
||||
return self.__call__(tag)
|
||||
|
||||
def __iter__(self):
|
||||
"Iterate over xml tags at this level"
|
||||
try:
|
||||
for __element in self.__elements:
|
||||
yield SimpleXMLElement(
|
||||
elements=[__element],
|
||||
document=self.__document,
|
||||
namespace=self.__ns,
|
||||
prefix=self.__prefix)
|
||||
except:
|
||||
raise
|
||||
|
||||
def __dir__(self):
|
||||
"List xml children tags names"
|
||||
return [node.tagName for node
|
||||
in self._element.childNodes
|
||||
if node.nodeType != node.TEXT_NODE]
|
||||
|
||||
def children(self):
|
||||
"Return xml children tags element"
|
||||
elements=[__element for __element in self._element.childNodes
|
||||
if __element.nodeType == __element.ELEMENT_NODE]
|
||||
if not elements:
|
||||
return None
|
||||
#raise IndexError("Tag %s has no children" % self._element.tagName)
|
||||
return SimpleXMLElement(
|
||||
elements=elements,
|
||||
document=self.__document,
|
||||
namespace=self.__ns,
|
||||
prefix=self.__prefix)
|
||||
|
||||
def __len__(self):
|
||||
"Return elements count"
|
||||
return len(self.__elements)
|
||||
|
||||
def __contains__( self, item):
|
||||
"Search for a tag name in this element or child nodes"
|
||||
return self._element.getElementsByTagName(item)
|
||||
|
||||
def __unicode__(self):
|
||||
"Returns the unicode text nodes of the current element"
|
||||
if self._element.childNodes:
|
||||
rc = u""
|
||||
for node in self._element.childNodes:
|
||||
if node.nodeType == node.TEXT_NODE:
|
||||
rc = rc + node.data
|
||||
return rc
|
||||
return ''
|
||||
|
||||
def __str__(self):
|
||||
"Returns the str text nodes of the current element"
|
||||
return unicode(self).encode("utf8","ignore")
|
||||
|
||||
def __int__(self):
|
||||
"Returns the integer value of the current element"
|
||||
return int(self.__str__())
|
||||
|
||||
def __float__(self):
|
||||
"Returns the float value of the current element"
|
||||
try:
|
||||
return float(self.__str__())
|
||||
except:
|
||||
raise IndexError(self._element.toxml())
|
||||
|
||||
_element = property(lambda self: self.__elements[0])
|
||||
|
||||
def unmarshall(self, types):
|
||||
"Convert to python values the current serialized xml element"
|
||||
# types is a dict of {tag name: convertion function}
|
||||
# example: types={'p': {'a': int,'b': int}, 'c': [{'d':str}]}
|
||||
# expected xml: <p><a>1</a><b>2</b></p><c><d>hola</d><d>chau</d>
|
||||
# returnde value: {'p': {'a':1,'b':2}, `'c':[{'d':'hola'},{'d':'chau'}]}
|
||||
d = {}
|
||||
for node in self():
|
||||
name = str(node.get_local_name())
|
||||
try:
|
||||
fn = types[name]
|
||||
except (KeyError, ), e:
|
||||
raise TypeError("Tag: %s invalid" % (name,))
|
||||
if isinstance(fn,list):
|
||||
value = []
|
||||
children = node.children()
|
||||
for child in children and children() or []:
|
||||
value.append(child.unmarshall(fn[0]))
|
||||
elif isinstance(fn,dict):
|
||||
children = node.children()
|
||||
value = children and children.unmarshall(fn)
|
||||
else:
|
||||
if fn is None: # xsd:anyType not unmarshalled
|
||||
value = node
|
||||
elif str(node) or fn == str:
|
||||
try:
|
||||
# get special desserialization function (if any)
|
||||
fn = TYPE_UNMARSHAL_FN.get(fn,fn)
|
||||
value = fn(unicode(node))
|
||||
except (ValueError, TypeError), e:
|
||||
raise ValueError("Tag: %s: %s" % (name, unicode(e)))
|
||||
else:
|
||||
value = None
|
||||
d[name] = value
|
||||
return d
|
||||
|
||||
def marshall(self, name, value, add_child=True, add_comments=False, ns=False):
|
||||
"Analize python value and add the serialized XML element using tag name"
|
||||
if isinstance(value, dict): # serialize dict (<key>value</key>)
|
||||
child = add_child and self.add_child(name,ns=ns) or self
|
||||
for k,v in value.items():
|
||||
child.marshall(k, v, add_comments=add_comments, ns=ns)
|
||||
elif isinstance(value, tuple): # serialize tuple (<key>value</key>)
|
||||
child = add_child and self.add_child(name,ns=ns) or self
|
||||
for k,v in value:
|
||||
getattr(self,name).marshall(k, v, add_comments=add_comments, ns=ns)
|
||||
elif isinstance(value, list): # serialize lists
|
||||
child=self.add_child(name,ns=ns)
|
||||
if add_comments:
|
||||
child.add_comment("Repetitive array of:")
|
||||
for t in value:
|
||||
child.marshall(name,t, False, add_comments=add_comments, ns=ns)
|
||||
elif isinstance(value, basestring): # do not convert strings or unicodes
|
||||
self.add_child(name,value,ns=ns)
|
||||
elif value is None: # sent a empty tag?
|
||||
self.add_child(name,ns=ns)
|
||||
elif value in TYPE_MAP.keys():
|
||||
# add commented placeholders for simple tipes (for examples/help only)
|
||||
child = self.add_child(name,ns=ns)
|
||||
child.add_comment(TYPE_MAP[value])
|
||||
else: # the rest of object types are converted to string
|
||||
# get special serialization function (if any)
|
||||
fn = TYPE_MARSHAL_FN.get(type(value),str)
|
||||
self.add_child(name,fn(value),ns=ns)
|
||||
|
||||
def import_node(self, other):
|
||||
x = self.__document.importNode(other._element, True) # deep copy
|
||||
self._element.appendChild(x)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
span = SimpleXMLElement('<span><a href="python.org.ar">pyar</a><prueba><i>1</i><float>1.5</float></prueba></span>')
|
||||
assert str(span.a)==str(span('a'))==str(span.a(0))=="pyar"
|
||||
assert span.a['href']=="python.org.ar"
|
||||
assert int(span.prueba.i)==1 and float(span.prueba.float)==1.5
|
||||
span1 = SimpleXMLElement('<span><a href="google.com">google</a><a>yahoo</a><a>hotmail</a></span>')
|
||||
assert [str(a) for a in span1.a()] == ['google', 'yahoo', 'hotmail']
|
||||
span1.add_child('a','altavista')
|
||||
span1.b = "ex msn"
|
||||
d = {'href':'http://www.bing.com/', 'alt': 'Bing'}
|
||||
span1.b[:] = d
|
||||
assert sorted([(k,v) for k,v in span1.b[:]]) == sorted(d.items())
|
||||
print span1.as_xml()
|
||||
assert 'b' in span1
|
||||
span.import_node(span1)
|
||||
print span.as_xml()
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
Developed by 616d41631bff906704951934ffe4015e
|
||||
Released under web2py license because includes gluon/cache.py source code
|
||||
"""
|
||||
|
||||
import redis
|
||||
from gluon import current
|
||||
from gluon.cache import CacheAbstract
|
||||
import cPickle as pickle
|
||||
import time
|
||||
import re
|
||||
|
||||
import thread
|
||||
|
||||
locker = thread.allocate_lock()
|
||||
|
||||
def RedisCache(*args, **vars):
|
||||
"""
|
||||
Usage example: put in models
|
||||
|
||||
from gluon.contrib.redis import RedisCache
|
||||
cache.redis = RedisCache('localhost:6379',db=None, debug=True)
|
||||
|
||||
cache.redis.stats()
|
||||
|
||||
return a dictionary with statistics of Redis server
|
||||
with one additional key ('w2p_keys') showing all keys currently set
|
||||
from web2py with their TTL
|
||||
if debug=True additional tracking is activate and another key is added
|
||||
('w2p_stats') showing total_hits and misses
|
||||
"""
|
||||
|
||||
locker.acquire()
|
||||
try:
|
||||
if not hasattr(RedisCache, 'redis_instance'):
|
||||
RedisCache.redis_instance = RedisClient(*args, **vars)
|
||||
finally:
|
||||
locker.release()
|
||||
return RedisCache.redis_instance
|
||||
|
||||
|
||||
class RedisClient(object):
|
||||
|
||||
meta_storage = {}
|
||||
|
||||
def __init__(self, server='localhost:6379', db=None, debug=False):
|
||||
host,port = (address.split(':')+['6379'])[:2]
|
||||
port = int(port)
|
||||
self.request=current.request
|
||||
self.debug = debug
|
||||
if request:
|
||||
app = request.application
|
||||
else:
|
||||
app = ''
|
||||
|
||||
if not app in self.meta_storage:
|
||||
self.storage = self.meta_storage[app] = {
|
||||
CacheAbstract.cache_stats_name: {
|
||||
'hit_total': 0,
|
||||
'misses': 0,
|
||||
}}
|
||||
else:
|
||||
self.storage = self.meta_storage[app]
|
||||
|
||||
self.r_server = redis.Redis(host=host, port=port, db=db or 0)
|
||||
|
||||
def __call__(self, key, f, time_expire=300):
|
||||
if time_expire == None:
|
||||
time_expire = 10**10
|
||||
key = self.__keyFormat__(key)
|
||||
value = None
|
||||
obj = self.r_server.get(key)
|
||||
if obj:
|
||||
if self.debug:
|
||||
self.r_server.incr('web2py_cache_statistics:hit_total')
|
||||
value = pickle.loads(obj)
|
||||
elif f is None:
|
||||
if obj: self.r_server.delete(key)
|
||||
else:
|
||||
if self.debug:
|
||||
self.r_server.incr('web2py_cache_statistics:misses')
|
||||
value = f()
|
||||
self.r_server.setex(key, pickle.dumps(value), time_expire)
|
||||
return value
|
||||
|
||||
def increment(self, key, value=1, time_expire=300):
|
||||
newKey = self.__keyFormat__(key)
|
||||
obj = self.r_server.get(newKey)
|
||||
if obj:
|
||||
return self.r_server.incr(newKey, value)
|
||||
else:
|
||||
self.r_server.setex(newKey, value, time_expire)
|
||||
return value
|
||||
|
||||
def clear(self, regex):
|
||||
"""
|
||||
Auxiliary function called by `clear` to search and
|
||||
clear cache entries
|
||||
"""
|
||||
r = re.compile(regex)
|
||||
prefix = "w2p:%s:" % (self.request.application)
|
||||
pipe = self.r_server.pipeline()
|
||||
for a in self.r_server.keys("%s*" % \
|
||||
(prefix)):
|
||||
if r.match(str(a).replace(prefix, '', 1)):
|
||||
pipe.delete(a)
|
||||
pipe.execute()
|
||||
|
||||
def stats(self):
|
||||
statscollector = self.r_server.info()
|
||||
if self.debug:
|
||||
statscollector['w2p_stats'] = dict(
|
||||
hit_total = self.r_server.get(
|
||||
'web2py_cache_statistics:hit_total'),
|
||||
misses=self.r_server.get('web2py_cache_statistics:misses')
|
||||
)
|
||||
statscollector['w2p_keys'] = dict()
|
||||
for a in self.r_server.keys("w2p:%s:*" % (
|
||||
self.request.application)):
|
||||
statscollector['w2p_keys']["%s_expire_in_sec" % (a)] = \
|
||||
self.r_server.ttl(a)
|
||||
return statscollector
|
||||
|
||||
def __keyFormat__(self, key):
|
||||
return 'w2p:%s:%s' % (self.request.application,
|
||||
key.replace(' ', '_'))
|
||||
@@ -0,0 +1,590 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""PyRSS2Gen - A Python library for generating RSS 2.0 feeds."""
|
||||
|
||||
__name__ = 'PyRSS2Gen'
|
||||
__version__ = (1, 0, 0)
|
||||
__author__ = 'Andrew Dalke <dalke@dalkescientific.com>'
|
||||
|
||||
_generator_name = __name__ + '-' + '.'.join(map(str, __version__))
|
||||
|
||||
import datetime
|
||||
import cStringIO
|
||||
|
||||
# Could make this the base class; will need to add 'publish'
|
||||
|
||||
|
||||
class WriteXmlMixin:
|
||||
|
||||
def write_xml(self, outfile, encoding='iso-8859-1'):
|
||||
from xml.sax import saxutils
|
||||
handler = saxutils.XMLGenerator(outfile, encoding)
|
||||
handler.startDocument()
|
||||
self.publish(handler)
|
||||
handler.endDocument()
|
||||
|
||||
def to_xml(self, encoding='iso-8859-1'):
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
except ImportError:
|
||||
import StringIO
|
||||
f = StringIO.StringIO()
|
||||
self.write_xml(f, encoding)
|
||||
return f.getvalue()
|
||||
|
||||
|
||||
def _element(
|
||||
handler,
|
||||
name,
|
||||
obj,
|
||||
d={},
|
||||
):
|
||||
if isinstance(obj, basestring) or obj is None:
|
||||
|
||||
# special-case handling to make the API easier
|
||||
# to use for the common case.
|
||||
|
||||
handler.startElement(name, d)
|
||||
if obj is not None:
|
||||
handler.characters(obj)
|
||||
handler.endElement(name)
|
||||
else:
|
||||
|
||||
# It better know how to emit the correct XML.
|
||||
|
||||
obj.publish(handler)
|
||||
|
||||
|
||||
def _opt_element(handler, name, obj):
|
||||
if obj is None:
|
||||
return
|
||||
_element(handler, name, obj)
|
||||
|
||||
|
||||
def _format_date(dt):
|
||||
"""convert a datetime into an RFC 822 formatted date
|
||||
|
||||
Input date must be in GMT.
|
||||
"""
|
||||
|
||||
# Looks like:
|
||||
# Sat, 07 Sep 2002 00:00:01 GMT
|
||||
# Can't use strftime because that's locale dependent
|
||||
#
|
||||
# Isn't there a standard way to do this for Python? The
|
||||
# rfc822 and email.Utils modules assume a timestamp. The
|
||||
# following is based on the rfc822 module.
|
||||
|
||||
return '%s, %02d %s %04d %02d:%02d:%02d GMT' % (
|
||||
[
|
||||
'Mon',
|
||||
'Tue',
|
||||
'Wed',
|
||||
'Thu',
|
||||
'Fri',
|
||||
'Sat',
|
||||
'Sun',
|
||||
][dt.weekday()],
|
||||
dt.day,
|
||||
[
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
][dt.month - 1],
|
||||
dt.year,
|
||||
dt.hour,
|
||||
dt.minute,
|
||||
dt.second,
|
||||
)
|
||||
|
||||
|
||||
##
|
||||
# A couple simple wrapper objects for the fields which
|
||||
# take a simple value other than a string.
|
||||
|
||||
|
||||
class IntElement:
|
||||
|
||||
"""implements the 'publish' API for integers
|
||||
|
||||
Takes the tag name and the integer value to publish.
|
||||
|
||||
(Could be used for anything which uses str() to be published
|
||||
to text for XML.)
|
||||
"""
|
||||
|
||||
element_attrs = {}
|
||||
|
||||
def __init__(self, name, val):
|
||||
self.name = name
|
||||
self.val = val
|
||||
|
||||
def publish(self, handler):
|
||||
handler.startElement(self.name, self.element_attrs)
|
||||
handler.characters(str(self.val))
|
||||
handler.endElement(self.name)
|
||||
|
||||
|
||||
class DateElement:
|
||||
|
||||
"""implements the 'publish' API for a datetime.datetime
|
||||
|
||||
Takes the tag name and the datetime to publish.
|
||||
|
||||
Converts the datetime to RFC 2822 timestamp (4-digit year).
|
||||
"""
|
||||
|
||||
def __init__(self, name, dt):
|
||||
self.name = name
|
||||
self.dt = dt
|
||||
|
||||
def publish(self, handler):
|
||||
_element(handler, self.name, _format_date(self.dt))
|
||||
|
||||
|
||||
# ###
|
||||
|
||||
|
||||
class Category:
|
||||
|
||||
"""Publish a category element"""
|
||||
|
||||
def __init__(self, category, domain=None):
|
||||
self.category = category
|
||||
self.domain = domain
|
||||
|
||||
def publish(self, handler):
|
||||
d = {}
|
||||
if self.domain is not None:
|
||||
d['domain'] = self.domain
|
||||
_element(handler, 'category', self.category, d)
|
||||
|
||||
|
||||
class Cloud:
|
||||
|
||||
"""Publish a cloud"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
domain,
|
||||
port,
|
||||
path,
|
||||
registerProcedure,
|
||||
protocol,
|
||||
):
|
||||
self.domain = domain
|
||||
self.port = port
|
||||
self.path = path
|
||||
self.registerProcedure = registerProcedure
|
||||
self.protocol = protocol
|
||||
|
||||
def publish(self, handler):
|
||||
_element(handler, 'cloud', None, {
|
||||
'domain': self.domain,
|
||||
'port': str(self.port),
|
||||
'path': self.path,
|
||||
'registerProcedure': self.registerProcedure,
|
||||
'protocol': self.protocol,
|
||||
})
|
||||
|
||||
|
||||
class Image:
|
||||
|
||||
"""Publish a channel Image"""
|
||||
|
||||
element_attrs = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url,
|
||||
title,
|
||||
link,
|
||||
width=None,
|
||||
height=None,
|
||||
description=None,
|
||||
):
|
||||
self.url = url
|
||||
self.title = title
|
||||
self.link = link
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.description = description
|
||||
|
||||
def publish(self, handler):
|
||||
handler.startElement('image', self.element_attrs)
|
||||
|
||||
_element(handler, 'url', self.url)
|
||||
_element(handler, 'title', self.title)
|
||||
_element(handler, 'link', self.link)
|
||||
|
||||
width = self.width
|
||||
if isinstance(width, int):
|
||||
width = IntElement('width', width)
|
||||
_opt_element(handler, 'width', width)
|
||||
|
||||
height = self.height
|
||||
if isinstance(height, int):
|
||||
height = IntElement('height', height)
|
||||
_opt_element(handler, 'height', height)
|
||||
|
||||
_opt_element(handler, 'description', self.description)
|
||||
|
||||
handler.endElement('image')
|
||||
|
||||
|
||||
class Guid:
|
||||
|
||||
"""Publish a guid
|
||||
|
||||
Defaults to being a permalink, which is the assumption if it's
|
||||
omitted. Hence strings are always permalinks.
|
||||
"""
|
||||
|
||||
def __init__(self, guid, isPermaLink=1):
|
||||
self.guid = guid
|
||||
self.isPermaLink = isPermaLink
|
||||
|
||||
def publish(self, handler):
|
||||
d = {}
|
||||
if self.isPermaLink:
|
||||
d['isPermaLink'] = 'true'
|
||||
else:
|
||||
d['isPermaLink'] = 'false'
|
||||
_element(handler, 'guid', self.guid, d)
|
||||
|
||||
|
||||
class TextInput:
|
||||
|
||||
"""Publish a textInput
|
||||
|
||||
Apparently this is rarely used.
|
||||
"""
|
||||
|
||||
element_attrs = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title,
|
||||
description,
|
||||
name,
|
||||
link,
|
||||
):
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.name = name
|
||||
self.link = link
|
||||
|
||||
def publish(self, handler):
|
||||
handler.startElement('textInput', self.element_attrs)
|
||||
_element(handler, 'title', self.title)
|
||||
_element(handler, 'description', self.description)
|
||||
_element(handler, 'name', self.name)
|
||||
_element(handler, 'link', self.link)
|
||||
handler.endElement('textInput')
|
||||
|
||||
|
||||
class Enclosure:
|
||||
|
||||
"""Publish an enclosure"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url,
|
||||
length,
|
||||
type,
|
||||
):
|
||||
self.url = url
|
||||
self.length = length
|
||||
self.type = type
|
||||
|
||||
def publish(self, handler):
|
||||
_element(handler, 'enclosure', None,
|
||||
{'url': self.url, 'length': str(self.length), 'type': self.type})
|
||||
|
||||
|
||||
class Source:
|
||||
|
||||
"""Publish the item's original source, used by aggregators"""
|
||||
|
||||
def __init__(self, name, url):
|
||||
self.name = name
|
||||
self.url = url
|
||||
|
||||
def publish(self, handler):
|
||||
_element(handler, 'source', self.name, {'url': self.url})
|
||||
|
||||
|
||||
class SkipHours:
|
||||
|
||||
"""Publish the skipHours
|
||||
|
||||
This takes a list of hours, as integers.
|
||||
"""
|
||||
|
||||
element_attrs = {}
|
||||
|
||||
def __init__(self, hours):
|
||||
self.hours = hours
|
||||
|
||||
def publish(self, handler):
|
||||
if self.hours:
|
||||
handler.startElement('skipHours', self.element_attrs)
|
||||
for hour in self.hours:
|
||||
_element(handler, 'hour', str(hour))
|
||||
handler.endElement('skipHours')
|
||||
|
||||
|
||||
class SkipDays:
|
||||
|
||||
"""Publish the skipDays
|
||||
|
||||
This takes a list of days as strings.
|
||||
"""
|
||||
|
||||
element_attrs = {}
|
||||
|
||||
def __init__(self, days):
|
||||
self.days = days
|
||||
|
||||
def publish(self, handler):
|
||||
if self.days:
|
||||
handler.startElement('skipDays', self.element_attrs)
|
||||
for day in self.days:
|
||||
_element(handler, 'day', day)
|
||||
handler.endElement('skipDays')
|
||||
|
||||
|
||||
class RSS2(WriteXmlMixin):
|
||||
|
||||
"""The main RSS class.
|
||||
|
||||
Stores the channel attributes, with the \"category\" elements under
|
||||
\".categories\" and the RSS items under \".items\".
|
||||
"""
|
||||
|
||||
rss_attrs = {'version': '2.0'}
|
||||
element_attrs = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title,
|
||||
link,
|
||||
description,
|
||||
language=None,
|
||||
copyright=None,
|
||||
managingEditor=None,
|
||||
webMaster=None,
|
||||
pubDate=None,
|
||||
lastBuildDate=None,
|
||||
categories=None,
|
||||
generator=_generator_name,
|
||||
docs='http://blogs.law.harvard.edu/tech/rss',
|
||||
cloud=None,
|
||||
ttl=None,
|
||||
image=None,
|
||||
rating=None,
|
||||
textInput=None,
|
||||
skipHours=None,
|
||||
skipDays=None,
|
||||
items=None,
|
||||
):
|
||||
|
||||
self.title = title
|
||||
self.link = link
|
||||
self.description = description
|
||||
self.language = language
|
||||
self.copyright = copyright
|
||||
self.managingEditor = managingEditor
|
||||
|
||||
self.webMaster = webMaster
|
||||
self.pubDate = pubDate
|
||||
self.lastBuildDate = lastBuildDate
|
||||
|
||||
if categories is None:
|
||||
categories = []
|
||||
self.categories = categories
|
||||
self.generator = generator
|
||||
self.docs = docs
|
||||
self.cloud = cloud
|
||||
self.ttl = ttl
|
||||
self.image = image
|
||||
self.rating = rating
|
||||
self.textInput = textInput
|
||||
self.skipHours = skipHours
|
||||
self.skipDays = skipDays
|
||||
|
||||
if items is None:
|
||||
items = []
|
||||
self.items = items
|
||||
|
||||
def publish(self, handler):
|
||||
handler.startElement('rss', self.rss_attrs)
|
||||
handler.startElement('channel', self.element_attrs)
|
||||
_element(handler, 'title', self.title)
|
||||
_element(handler, 'link', self.link)
|
||||
_element(handler, 'description', self.description)
|
||||
|
||||
self.publish_extensions(handler)
|
||||
|
||||
_opt_element(handler, 'language', self.language)
|
||||
_opt_element(handler, 'copyright', self.copyright)
|
||||
_opt_element(handler, 'managingEditor', self.managingEditor)
|
||||
_opt_element(handler, 'webMaster', self.webMaster)
|
||||
|
||||
pubDate = self.pubDate
|
||||
if isinstance(pubDate, datetime.datetime):
|
||||
pubDate = DateElement('pubDate', pubDate)
|
||||
_opt_element(handler, 'pubDate', pubDate)
|
||||
|
||||
lastBuildDate = self.lastBuildDate
|
||||
if isinstance(lastBuildDate, datetime.datetime):
|
||||
lastBuildDate = DateElement('lastBuildDate', lastBuildDate)
|
||||
_opt_element(handler, 'lastBuildDate', lastBuildDate)
|
||||
|
||||
for category in self.categories:
|
||||
if isinstance(category, basestring):
|
||||
category = Category(category)
|
||||
category.publish(handler)
|
||||
|
||||
_opt_element(handler, 'generator', self.generator)
|
||||
_opt_element(handler, 'docs', self.docs)
|
||||
|
||||
if self.cloud is not None:
|
||||
self.cloud.publish(handler)
|
||||
|
||||
ttl = self.ttl
|
||||
if isinstance(self.ttl, int):
|
||||
ttl = IntElement('ttl', ttl)
|
||||
_opt_element(handler, 'tt', ttl)
|
||||
|
||||
if self.image is not None:
|
||||
self.image.publish(handler)
|
||||
|
||||
_opt_element(handler, 'rating', self.rating)
|
||||
if self.textInput is not None:
|
||||
self.textInput.publish(handler)
|
||||
if self.skipHours is not None:
|
||||
self.skipHours.publish(handler)
|
||||
if self.skipDays is not None:
|
||||
self.skipDays.publish(handler)
|
||||
|
||||
for item in self.items:
|
||||
item.publish(handler)
|
||||
|
||||
handler.endElement('channel')
|
||||
handler.endElement('rss')
|
||||
|
||||
def publish_extensions(self, handler):
|
||||
|
||||
# Derived classes can hook into this to insert
|
||||
# output after the three required fields.
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RSSItem(WriteXmlMixin):
|
||||
|
||||
"""Publish an RSS Item"""
|
||||
|
||||
element_attrs = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title=None,
|
||||
link=None,
|
||||
description=None,
|
||||
author=None,
|
||||
categories=None,
|
||||
comments=None,
|
||||
enclosure=None,
|
||||
guid=None,
|
||||
pubDate=None,
|
||||
source=None,
|
||||
):
|
||||
|
||||
if title is None and description is None:
|
||||
raise TypeError(
|
||||
"RSSItem must define at least one of 'title' or 'description'")
|
||||
self.title = title
|
||||
self.link = link
|
||||
self.description = description
|
||||
self.author = author
|
||||
if categories is None:
|
||||
categories = []
|
||||
self.categories = categories
|
||||
self.comments = comments
|
||||
self.enclosure = enclosure
|
||||
self.guid = guid
|
||||
self.pubDate = pubDate
|
||||
self.source = source
|
||||
|
||||
# It sure does get tedious typing these names three times...
|
||||
|
||||
def publish(self, handler):
|
||||
handler.startElement('item', self.element_attrs)
|
||||
_opt_element(handler, 'title', self.title)
|
||||
_opt_element(handler, 'link', self.link)
|
||||
self.publish_extensions(handler)
|
||||
_opt_element(handler, 'description', self.description)
|
||||
_opt_element(handler, 'author', self.author)
|
||||
|
||||
for category in self.categories:
|
||||
if isinstance(category, basestring):
|
||||
category = Category(category)
|
||||
category.publish(handler)
|
||||
|
||||
_opt_element(handler, 'comments', self.comments)
|
||||
if self.enclosure is not None:
|
||||
self.enclosure.publish(handler)
|
||||
_opt_element(handler, 'guid', self.guid)
|
||||
|
||||
pubDate = self.pubDate
|
||||
if isinstance(pubDate, datetime.datetime):
|
||||
pubDate = DateElement('pubDate', pubDate)
|
||||
_opt_element(handler, 'pubDate', pubDate)
|
||||
|
||||
if self.source is not None:
|
||||
self.source.publish(handler)
|
||||
|
||||
handler.endElement('item')
|
||||
|
||||
def publish_extensions(self, handler):
|
||||
|
||||
# Derived classes can hook into this to insert
|
||||
# output after the title and link elements
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def dumps(rss, encoding='utf-8'):
|
||||
s = cStringIO.StringIO()
|
||||
rss.write_xml(s, encoding)
|
||||
return s.getvalue()
|
||||
|
||||
|
||||
def test():
|
||||
rss = RSS2(title='web2py feed', link='http://www.web2py.com',
|
||||
description='About web2py',
|
||||
lastBuildDate=datetime.datetime.now(),
|
||||
items=[RSSItem(title='web2py and PyRSS2Gen-0.0',
|
||||
link='http://www.web2py.com/examples/simple_examples/getrss',
|
||||
description='web2py can now make rss feeds!',
|
||||
guid=Guid('http://www.web2py.com/'),
|
||||
pubDate=datetime.datetime(2007, 11, 14, 10, 30))])
|
||||
return dumps(rss)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print test()
|
||||
|
||||
|
||||
Executable
+268
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright 2007 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# Modified by Massimo Di Pierro so it works with and without GAE with web2py
|
||||
# the modified version of this file is still released under the original Apache license
|
||||
# and it is not released under the web2py license.
|
||||
#
|
||||
# This should be compatible with the Apache license since it states:
|
||||
# "For the purposes of this License, Derivative Works shall not include works
|
||||
# that remain separable from, or merely link (or bind by name) to the interfaces of,
|
||||
# the Work and Derivative Works thereof."
|
||||
#
|
||||
# In fact this file is Apache-licensed and it is separable from the rest of web2py.
|
||||
|
||||
|
||||
"""
|
||||
An interactive, stateful AJAX shell that runs Python code on the server.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import new
|
||||
import os
|
||||
import cPickle
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
import wsgiref.handlers
|
||||
import StringIO
|
||||
import threading
|
||||
locker = threading.RLock()
|
||||
|
||||
# Set to True if stack traces should be shown in the browser, etc.
|
||||
_DEBUG = True
|
||||
|
||||
# The entity kind for shell historys. Feel free to rename to suit your app.
|
||||
_HISTORY_KIND = '_Shell_History'
|
||||
|
||||
# Types that can't be pickled.
|
||||
UNPICKLABLE_TYPES = (
|
||||
types.ModuleType,
|
||||
types.TypeType,
|
||||
types.ClassType,
|
||||
types.FunctionType,
|
||||
)
|
||||
|
||||
# Unpicklable statements to seed new historys with.
|
||||
INITIAL_UNPICKLABLES = [
|
||||
'import logging',
|
||||
'import os',
|
||||
'import sys',
|
||||
]
|
||||
|
||||
|
||||
class History:
|
||||
"""A shell history. Stores the history's globals.
|
||||
|
||||
Each history globals is stored in one of two places:
|
||||
|
||||
If the global is picklable, it's stored in the parallel globals and
|
||||
global_names list properties. (They're parallel lists to work around the
|
||||
unfortunate fact that the datastore can't store dictionaries natively.)
|
||||
|
||||
If the global is not picklable (e.g. modules, classes, and functions), or if
|
||||
it was created by the same statement that created an unpicklable global,
|
||||
it's not stored directly. Instead, the statement is stored in the
|
||||
unpicklables list property. On each request, before executing the current
|
||||
statement, the unpicklable statements are evaluated to recreate the
|
||||
unpicklable globals.
|
||||
|
||||
The unpicklable_names property stores all of the names of globals that were
|
||||
added by unpicklable statements. When we pickle and store the globals after
|
||||
executing a statement, we skip the ones in unpicklable_names.
|
||||
|
||||
Using Text instead of string is an optimization. We don't query on any of
|
||||
these properties, so they don't need to be indexed.
|
||||
"""
|
||||
global_names = []
|
||||
globals = []
|
||||
unpicklable_names = []
|
||||
unpicklables = []
|
||||
|
||||
def set_global(self, name, value):
|
||||
"""Adds a global, or updates it if it already exists.
|
||||
|
||||
Also removes the global from the list of unpicklable names.
|
||||
|
||||
Args:
|
||||
name: the name of the global to remove
|
||||
value: any picklable value
|
||||
"""
|
||||
blob = cPickle.dumps(value)
|
||||
|
||||
if name in self.global_names:
|
||||
index = self.global_names.index(name)
|
||||
self.globals[index] = blob
|
||||
else:
|
||||
self.global_names.append(name)
|
||||
self.globals.append(blob)
|
||||
|
||||
self.remove_unpicklable_name(name)
|
||||
|
||||
def remove_global(self, name):
|
||||
"""Removes a global, if it exists.
|
||||
|
||||
Args:
|
||||
name: string, the name of the global to remove
|
||||
"""
|
||||
if name in self.global_names:
|
||||
index = self.global_names.index(name)
|
||||
del self.global_names[index]
|
||||
del self.globals[index]
|
||||
|
||||
def globals_dict(self):
|
||||
"""Returns a dictionary view of the globals.
|
||||
"""
|
||||
return dict((name, cPickle.loads(val))
|
||||
for name, val in zip(self.global_names, self.globals))
|
||||
|
||||
def add_unpicklable(self, statement, names):
|
||||
"""Adds a statement and list of names to the unpicklables.
|
||||
|
||||
Also removes the names from the globals.
|
||||
|
||||
Args:
|
||||
statement: string, the statement that created new unpicklable global(s).
|
||||
names: list of strings; the names of the globals created by the statement.
|
||||
"""
|
||||
self.unpicklables.append(statement)
|
||||
|
||||
for name in names:
|
||||
self.remove_global(name)
|
||||
if name not in self.unpicklable_names:
|
||||
self.unpicklable_names.append(name)
|
||||
|
||||
def remove_unpicklable_name(self, name):
|
||||
"""Removes a name from the list of unpicklable names, if it exists.
|
||||
|
||||
Args:
|
||||
name: string, the name of the unpicklable global to remove
|
||||
"""
|
||||
if name in self.unpicklable_names:
|
||||
self.unpicklable_names.remove(name)
|
||||
|
||||
def represent(obj):
|
||||
"""Returns a string representing the given object's value, which should allow the
|
||||
code below to determine whether the object changes over time.
|
||||
"""
|
||||
try:
|
||||
return cPickle.dumps(obj)
|
||||
except:
|
||||
return repr(obj)
|
||||
|
||||
def run(history, statement, env={}):
|
||||
"""
|
||||
Evaluates a python statement in a given history and returns the result.
|
||||
"""
|
||||
history.unpicklables = INITIAL_UNPICKLABLES
|
||||
|
||||
# extract the statement to be run
|
||||
if not statement:
|
||||
return ''
|
||||
|
||||
# the python compiler doesn't like network line endings
|
||||
statement = statement.replace('\r\n', '\n')
|
||||
|
||||
# add a couple newlines at the end of the statement. this makes
|
||||
# single-line expressions such as 'class Foo: pass' evaluate happily.
|
||||
statement += '\n\n'
|
||||
|
||||
|
||||
# log and compile the statement up front
|
||||
try:
|
||||
logging.info('Compiling and evaluating:\n%s' % statement)
|
||||
compiled = compile(statement, '<string>', 'single')
|
||||
except:
|
||||
return str(traceback.format_exc())
|
||||
|
||||
# create a dedicated module to be used as this statement's __main__
|
||||
statement_module = new.module('__main__')
|
||||
|
||||
# use this request's __builtin__, since it changes on each request.
|
||||
# this is needed for import statements, among other things.
|
||||
import __builtin__
|
||||
statement_module.__builtins__ = __builtin__
|
||||
|
||||
# load the history from the datastore
|
||||
history = History()
|
||||
|
||||
# swap in our custom module for __main__. then unpickle the history
|
||||
# globals, run the statement, and re-pickle the history globals, all
|
||||
# inside it.
|
||||
old_main = sys.modules.get('__main__')
|
||||
output = StringIO.StringIO()
|
||||
try:
|
||||
sys.modules['__main__'] = statement_module
|
||||
statement_module.__name__ = '__main__'
|
||||
statement_module.__dict__.update(env)
|
||||
|
||||
# re-evaluate the unpicklables
|
||||
for code in history.unpicklables:
|
||||
exec code in statement_module.__dict__
|
||||
|
||||
# re-initialize the globals
|
||||
for name, val in history.globals_dict().items():
|
||||
try:
|
||||
statement_module.__dict__[name] = val
|
||||
except:
|
||||
msg = 'Dropping %s since it could not be unpickled.\n' % name
|
||||
output.write(msg)
|
||||
logging.warning(msg + traceback.format_exc())
|
||||
history.remove_global(name)
|
||||
|
||||
# run!
|
||||
old_globals = dict((key,represent(value)) for key,value in statement_module.__dict__.items())
|
||||
try:
|
||||
old_stdout, old_stderr = sys.stdout, sys.stderr
|
||||
try:
|
||||
sys.stderr = sys.stdout = output
|
||||
locker.acquire()
|
||||
exec compiled in statement_module.__dict__
|
||||
finally:
|
||||
locker.release()
|
||||
sys.stdout, sys.stderr = old_stdout, old_stderr
|
||||
except:
|
||||
output.write(str(traceback.format_exc()))
|
||||
return output.getvalue()
|
||||
|
||||
# extract the new globals that this statement added
|
||||
new_globals = {}
|
||||
for name, val in statement_module.__dict__.items():
|
||||
if name not in old_globals or represent(val) != old_globals[name]:
|
||||
new_globals[name] = val
|
||||
|
||||
if True in [isinstance(val, UNPICKLABLE_TYPES)
|
||||
for val in new_globals.values()]:
|
||||
# this statement added an unpicklable global. store the statement and
|
||||
# the names of all of the globals it added in the unpicklables.
|
||||
history.add_unpicklable(statement, new_globals.keys())
|
||||
logging.debug('Storing this statement as an unpicklable.')
|
||||
else:
|
||||
# this statement didn't add any unpicklables. pickle and store the
|
||||
# new globals back into the datastore.
|
||||
for name, val in new_globals.items():
|
||||
if not name.startswith('__'):
|
||||
history.set_global(name, val)
|
||||
|
||||
finally:
|
||||
sys.modules['__main__'] = old_main
|
||||
return output.getvalue()
|
||||
|
||||
if __name__=='__main__':
|
||||
history=History()
|
||||
while True: print run(history, raw_input('>>> ')).rstrip()
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2006 Bob Ippolito
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,440 @@
|
||||
r"""JSON (JavaScript Object Notation) <http://json.org> is a subset of
|
||||
JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data
|
||||
interchange format.
|
||||
|
||||
:mod:`simplejson` exposes an API familiar to users of the standard library
|
||||
:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained
|
||||
version of the :mod:`json` library contained in Python 2.6, but maintains
|
||||
compatibility with Python 2.4 and Python 2.5 and (currently) has
|
||||
significant performance advantages, even without using the optional C
|
||||
extension for speedups.
|
||||
|
||||
Encoding basic Python object hierarchies::
|
||||
|
||||
>>> import simplejson as json
|
||||
>>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
|
||||
'["foo", {"bar": ["baz", null, 1.0, 2]}]'
|
||||
>>> print json.dumps("\"foo\bar")
|
||||
"\"foo\bar"
|
||||
>>> print json.dumps(u'\u1234')
|
||||
"\u1234"
|
||||
>>> print json.dumps('\\')
|
||||
"\\"
|
||||
>>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True)
|
||||
{"a": 0, "b": 0, "c": 0}
|
||||
>>> from StringIO import StringIO
|
||||
>>> io = StringIO()
|
||||
>>> json.dump(['streaming API'], io)
|
||||
>>> io.getvalue()
|
||||
'["streaming API"]'
|
||||
|
||||
Compact encoding::
|
||||
|
||||
>>> import simplejson as json
|
||||
>>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':'))
|
||||
'[1,2,3,{"4":5,"6":7}]'
|
||||
|
||||
Pretty printing::
|
||||
|
||||
>>> import simplejson as json
|
||||
>>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=' ')
|
||||
>>> print '\n'.join([l.rstrip() for l in s.splitlines()])
|
||||
{
|
||||
"4": 5,
|
||||
"6": 7
|
||||
}
|
||||
|
||||
Decoding JSON::
|
||||
|
||||
>>> import simplejson as json
|
||||
>>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}]
|
||||
>>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj
|
||||
True
|
||||
>>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar'
|
||||
True
|
||||
>>> from StringIO import StringIO
|
||||
>>> io = StringIO('["streaming API"]')
|
||||
>>> json.load(io)[0] == 'streaming API'
|
||||
True
|
||||
|
||||
Specializing JSON object decoding::
|
||||
|
||||
>>> import simplejson as json
|
||||
>>> def as_complex(dct):
|
||||
... if '__complex__' in dct:
|
||||
... return complex(dct['real'], dct['imag'])
|
||||
... return dct
|
||||
...
|
||||
>>> json.loads('{"__complex__": true, "real": 1, "imag": 2}',
|
||||
... object_hook=as_complex)
|
||||
(1+2j)
|
||||
>>> from decimal import Decimal
|
||||
>>> json.loads('1.1', parse_float=Decimal) == Decimal('1.1')
|
||||
True
|
||||
|
||||
Specializing JSON object encoding::
|
||||
|
||||
>>> import simplejson as json
|
||||
>>> def encode_complex(obj):
|
||||
... if isinstance(obj, complex):
|
||||
... return [obj.real, obj.imag]
|
||||
... raise TypeError(repr(o) + " is not JSON serializable")
|
||||
...
|
||||
>>> json.dumps(2 + 1j, default=encode_complex)
|
||||
'[2.0, 1.0]'
|
||||
>>> json.JSONEncoder(default=encode_complex).encode(2 + 1j)
|
||||
'[2.0, 1.0]'
|
||||
>>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j))
|
||||
'[2.0, 1.0]'
|
||||
|
||||
|
||||
Using simplejson.tool from the shell to validate and pretty-print::
|
||||
|
||||
$ echo '{"json":"obj"}' | python -m simplejson.tool
|
||||
{
|
||||
"json": "obj"
|
||||
}
|
||||
$ echo '{ 1.2:3.4}' | python -m simplejson.tool
|
||||
Expecting property name: line 1 column 2 (char 2)
|
||||
"""
|
||||
__version__ = '2.1.3'
|
||||
__all__ = [
|
||||
'dump', 'dumps', 'load', 'loads',
|
||||
'JSONDecoder', 'JSONDecodeError', 'JSONEncoder',
|
||||
'OrderedDict',
|
||||
]
|
||||
|
||||
__author__ = 'Bob Ippolito <bob@redivi.com>'
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from decoder import JSONDecoder, JSONDecodeError
|
||||
from encoder import JSONEncoder
|
||||
def _import_OrderedDict():
|
||||
import collections
|
||||
try:
|
||||
return collections.OrderedDict
|
||||
except AttributeError:
|
||||
import ordered_dict
|
||||
return ordered_dict.OrderedDict
|
||||
OrderedDict = _import_OrderedDict()
|
||||
|
||||
def _import_c_make_encoder():
|
||||
try:
|
||||
raise ImportError # because assumes simplejson in path
|
||||
from simplejson._speedups import make_encoder
|
||||
return make_encoder
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
_default_encoder = JSONEncoder(
|
||||
skipkeys=False,
|
||||
ensure_ascii=True,
|
||||
check_circular=True,
|
||||
allow_nan=True,
|
||||
indent=None,
|
||||
separators=None,
|
||||
encoding='utf-8',
|
||||
default=None,
|
||||
use_decimal=False,
|
||||
)
|
||||
|
||||
def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
|
||||
allow_nan=True, cls=None, indent=None, separators=None,
|
||||
encoding='utf-8', default=None, use_decimal=False, **kw):
|
||||
"""Serialize ``obj`` as a JSON formatted stream to ``fp`` (a
|
||||
``.write()``-supporting file-like object).
|
||||
|
||||
If ``skipkeys`` is true then ``dict`` keys that are not basic types
|
||||
(``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``)
|
||||
will be skipped instead of raising a ``TypeError``.
|
||||
|
||||
If ``ensure_ascii`` is false, then the some chunks written to ``fp``
|
||||
may be ``unicode`` instances, subject to normal Python ``str`` to
|
||||
``unicode`` coercion rules. Unless ``fp.write()`` explicitly
|
||||
understands ``unicode`` (as in ``codecs.getwriter()``) this is likely
|
||||
to cause an error.
|
||||
|
||||
If ``check_circular`` is false, then the circular reference check
|
||||
for container types will be skipped and a circular reference will
|
||||
result in an ``OverflowError`` (or worse).
|
||||
|
||||
If ``allow_nan`` is false, then it will be a ``ValueError`` to
|
||||
serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``)
|
||||
in strict compliance of the JSON specification, instead of using the
|
||||
JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
|
||||
|
||||
If *indent* is a string, then JSON array elements and object members
|
||||
will be pretty-printed with a newline followed by that string repeated
|
||||
for each level of nesting. ``None`` (the default) selects the most compact
|
||||
representation without any newlines. For backwards compatibility with
|
||||
versions of simplejson earlier than 2.1.0, an integer is also accepted
|
||||
and is converted to a string with that many spaces.
|
||||
|
||||
If ``separators`` is an ``(item_separator, dict_separator)`` tuple
|
||||
then it will be used instead of the default ``(', ', ': ')`` separators.
|
||||
``(',', ':')`` is the most compact JSON representation.
|
||||
|
||||
``encoding`` is the character encoding for str instances, default is UTF-8.
|
||||
|
||||
``default(obj)`` is a function that should return a serializable version
|
||||
of obj or raise TypeError. The default simply raises TypeError.
|
||||
|
||||
If *use_decimal* is true (default: ``False``) then decimal.Decimal
|
||||
will be natively serialized to JSON with full precision.
|
||||
|
||||
To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the
|
||||
``.default()`` method to serialize additional types), specify it with
|
||||
the ``cls`` kwarg.
|
||||
|
||||
"""
|
||||
# cached encoder
|
||||
if (not skipkeys and ensure_ascii and
|
||||
check_circular and allow_nan and
|
||||
cls is None and indent is None and separators is None and
|
||||
encoding == 'utf-8' and default is None and not use_decimal
|
||||
and not kw):
|
||||
iterable = _default_encoder.iterencode(obj)
|
||||
else:
|
||||
if cls is None:
|
||||
cls = JSONEncoder
|
||||
iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii,
|
||||
check_circular=check_circular, allow_nan=allow_nan, indent=indent,
|
||||
separators=separators, encoding=encoding,
|
||||
default=default, use_decimal=use_decimal, **kw).iterencode(obj)
|
||||
# could accelerate with writelines in some versions of Python, at
|
||||
# a debuggability cost
|
||||
for chunk in iterable:
|
||||
fp.write(chunk)
|
||||
|
||||
|
||||
def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
|
||||
allow_nan=True, cls=None, indent=None, separators=None,
|
||||
encoding='utf-8', default=None, use_decimal=False, **kw):
|
||||
"""Serialize ``obj`` to a JSON formatted ``str``.
|
||||
|
||||
If ``skipkeys`` is false then ``dict`` keys that are not basic types
|
||||
(``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``)
|
||||
will be skipped instead of raising a ``TypeError``.
|
||||
|
||||
If ``ensure_ascii`` is false, then the return value will be a
|
||||
``unicode`` instance subject to normal Python ``str`` to ``unicode``
|
||||
coercion rules instead of being escaped to an ASCII ``str``.
|
||||
|
||||
If ``check_circular`` is false, then the circular reference check
|
||||
for container types will be skipped and a circular reference will
|
||||
result in an ``OverflowError`` (or worse).
|
||||
|
||||
If ``allow_nan`` is false, then it will be a ``ValueError`` to
|
||||
serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in
|
||||
strict compliance of the JSON specification, instead of using the
|
||||
JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
|
||||
|
||||
If ``indent`` is a string, then JSON array elements and object members
|
||||
will be pretty-printed with a newline followed by that string repeated
|
||||
for each level of nesting. ``None`` (the default) selects the most compact
|
||||
representation without any newlines. For backwards compatibility with
|
||||
versions of simplejson earlier than 2.1.0, an integer is also accepted
|
||||
and is converted to a string with that many spaces.
|
||||
|
||||
If ``separators`` is an ``(item_separator, dict_separator)`` tuple
|
||||
then it will be used instead of the default ``(', ', ': ')`` separators.
|
||||
``(',', ':')`` is the most compact JSON representation.
|
||||
|
||||
``encoding`` is the character encoding for str instances, default is UTF-8.
|
||||
|
||||
``default(obj)`` is a function that should return a serializable version
|
||||
of obj or raise TypeError. The default simply raises TypeError.
|
||||
|
||||
If *use_decimal* is true (default: ``False``) then decimal.Decimal
|
||||
will be natively serialized to JSON with full precision.
|
||||
|
||||
To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the
|
||||
``.default()`` method to serialize additional types), specify it with
|
||||
the ``cls`` kwarg.
|
||||
|
||||
"""
|
||||
# cached encoder
|
||||
if (not skipkeys and ensure_ascii and
|
||||
check_circular and allow_nan and
|
||||
cls is None and indent is None and separators is None and
|
||||
encoding == 'utf-8' and default is None and not use_decimal
|
||||
and not kw):
|
||||
return _default_encoder.encode(obj)
|
||||
if cls is None:
|
||||
cls = JSONEncoder
|
||||
return cls(
|
||||
skipkeys=skipkeys, ensure_ascii=ensure_ascii,
|
||||
check_circular=check_circular, allow_nan=allow_nan, indent=indent,
|
||||
separators=separators, encoding=encoding, default=default,
|
||||
use_decimal=use_decimal, **kw).encode(obj)
|
||||
|
||||
|
||||
_default_decoder = JSONDecoder(encoding=None, object_hook=None,
|
||||
object_pairs_hook=None)
|
||||
|
||||
|
||||
def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None,
|
||||
parse_int=None, parse_constant=None, object_pairs_hook=None,
|
||||
use_decimal=False, **kw):
|
||||
"""Deserialize ``fp`` (a ``.read()``-supporting file-like object containing
|
||||
a JSON document) to a Python object.
|
||||
|
||||
*encoding* determines the encoding used to interpret any
|
||||
:class:`str` objects decoded by this instance (``'utf-8'`` by
|
||||
default). It has no effect when decoding :class:`unicode` objects.
|
||||
|
||||
Note that currently only encodings that are a superset of ASCII work,
|
||||
strings of other encodings should be passed in as :class:`unicode`.
|
||||
|
||||
*object_hook*, if specified, will be called with the result of every
|
||||
JSON object decoded and its return value will be used in place of the
|
||||
given :class:`dict`. This can be used to provide custom
|
||||
deserializations (e.g. to support JSON-RPC class hinting).
|
||||
|
||||
*object_pairs_hook* is an optional function that will be called with
|
||||
the result of any object literal decode with an ordered list of pairs.
|
||||
The return value of *object_pairs_hook* will be used instead of the
|
||||
:class:`dict`. This feature can be used to implement custom decoders
|
||||
that rely on the order that the key and value pairs are decoded (for
|
||||
example, :func:`collections.OrderedDict` will remember the order of
|
||||
insertion). If *object_hook* is also defined, the *object_pairs_hook*
|
||||
takes priority.
|
||||
|
||||
*parse_float*, if specified, will be called with the string of every
|
||||
JSON float to be decoded. By default, this is equivalent to
|
||||
``float(num_str)``. This can be used to use another datatype or parser
|
||||
for JSON floats (e.g. :class:`decimal.Decimal`).
|
||||
|
||||
*parse_int*, if specified, will be called with the string of every
|
||||
JSON int to be decoded. By default, this is equivalent to
|
||||
``int(num_str)``. This can be used to use another datatype or parser
|
||||
for JSON integers (e.g. :class:`float`).
|
||||
|
||||
*parse_constant*, if specified, will be called with one of the
|
||||
following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This
|
||||
can be used to raise an exception if invalid JSON numbers are
|
||||
encountered.
|
||||
|
||||
If *use_decimal* is true (default: ``False``) then it implies
|
||||
parse_float=decimal.Decimal for parity with ``dump``.
|
||||
|
||||
To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
|
||||
kwarg.
|
||||
|
||||
"""
|
||||
return loads(fp.read(),
|
||||
encoding=encoding, cls=cls, object_hook=object_hook,
|
||||
parse_float=parse_float, parse_int=parse_int,
|
||||
parse_constant=parse_constant, object_pairs_hook=object_pairs_hook,
|
||||
use_decimal=use_decimal, **kw)
|
||||
|
||||
|
||||
def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None,
|
||||
parse_int=None, parse_constant=None, object_pairs_hook=None,
|
||||
use_decimal=False, **kw):
|
||||
"""Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON
|
||||
document) to a Python object.
|
||||
|
||||
*encoding* determines the encoding used to interpret any
|
||||
:class:`str` objects decoded by this instance (``'utf-8'`` by
|
||||
default). It has no effect when decoding :class:`unicode` objects.
|
||||
|
||||
Note that currently only encodings that are a superset of ASCII work,
|
||||
strings of other encodings should be passed in as :class:`unicode`.
|
||||
|
||||
*object_hook*, if specified, will be called with the result of every
|
||||
JSON object decoded and its return value will be used in place of the
|
||||
given :class:`dict`. This can be used to provide custom
|
||||
deserializations (e.g. to support JSON-RPC class hinting).
|
||||
|
||||
*object_pairs_hook* is an optional function that will be called with
|
||||
the result of any object literal decode with an ordered list of pairs.
|
||||
The return value of *object_pairs_hook* will be used instead of the
|
||||
:class:`dict`. This feature can be used to implement custom decoders
|
||||
that rely on the order that the key and value pairs are decoded (for
|
||||
example, :func:`collections.OrderedDict` will remember the order of
|
||||
insertion). If *object_hook* is also defined, the *object_pairs_hook*
|
||||
takes priority.
|
||||
|
||||
*parse_float*, if specified, will be called with the string of every
|
||||
JSON float to be decoded. By default, this is equivalent to
|
||||
``float(num_str)``. This can be used to use another datatype or parser
|
||||
for JSON floats (e.g. :class:`decimal.Decimal`).
|
||||
|
||||
*parse_int*, if specified, will be called with the string of every
|
||||
JSON int to be decoded. By default, this is equivalent to
|
||||
``int(num_str)``. This can be used to use another datatype or parser
|
||||
for JSON integers (e.g. :class:`float`).
|
||||
|
||||
*parse_constant*, if specified, will be called with one of the
|
||||
following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This
|
||||
can be used to raise an exception if invalid JSON numbers are
|
||||
encountered.
|
||||
|
||||
If *use_decimal* is true (default: ``False``) then it implies
|
||||
parse_float=decimal.Decimal for parity with ``dump``.
|
||||
|
||||
To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
|
||||
kwarg.
|
||||
|
||||
"""
|
||||
if (cls is None and encoding is None and object_hook is None and
|
||||
parse_int is None and parse_float is None and
|
||||
parse_constant is None and object_pairs_hook is None
|
||||
and not use_decimal and not kw):
|
||||
return _default_decoder.decode(s)
|
||||
if cls is None:
|
||||
cls = JSONDecoder
|
||||
if object_hook is not None:
|
||||
kw['object_hook'] = object_hook
|
||||
if object_pairs_hook is not None:
|
||||
kw['object_pairs_hook'] = object_pairs_hook
|
||||
if parse_float is not None:
|
||||
kw['parse_float'] = parse_float
|
||||
if parse_int is not None:
|
||||
kw['parse_int'] = parse_int
|
||||
if parse_constant is not None:
|
||||
kw['parse_constant'] = parse_constant
|
||||
if use_decimal:
|
||||
if parse_float is not None:
|
||||
raise TypeError("use_decimal=True implies parse_float=Decimal")
|
||||
kw['parse_float'] = Decimal
|
||||
return cls(encoding=encoding, **kw).decode(s)
|
||||
|
||||
|
||||
def _toggle_speedups(enabled):
|
||||
import decoder as dec
|
||||
import encoder as enc
|
||||
import scanner as scan
|
||||
c_make_encoder = _import_c_make_encoder()
|
||||
if enabled:
|
||||
dec.scanstring = dec.c_scanstring or dec.py_scanstring
|
||||
enc.c_make_encoder = c_make_encoder
|
||||
enc.encode_basestring_ascii = (enc.c_encode_basestring_ascii or
|
||||
enc.py_encode_basestring_ascii)
|
||||
scan.make_scanner = scan.c_make_scanner or scan.py_make_scanner
|
||||
else:
|
||||
dec.scanstring = dec.py_scanstring
|
||||
enc.c_make_encoder = None
|
||||
enc.encode_basestring_ascii = enc.py_encode_basestring_ascii
|
||||
scan.make_scanner = scan.py_make_scanner
|
||||
dec.make_scanner = scan.make_scanner
|
||||
global _default_decoder
|
||||
_default_decoder = JSONDecoder(
|
||||
encoding=None,
|
||||
object_hook=None,
|
||||
object_pairs_hook=None,
|
||||
)
|
||||
global _default_encoder
|
||||
_default_encoder = JSONEncoder(
|
||||
skipkeys=False,
|
||||
ensure_ascii=True,
|
||||
check_circular=True,
|
||||
allow_nan=True,
|
||||
indent=None,
|
||||
separators=None,
|
||||
encoding='utf-8',
|
||||
default=None,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
"""Implementation of JSONDecoder
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
import struct
|
||||
|
||||
from scanner import make_scanner
|
||||
def _import_c_scanstring():
|
||||
try:
|
||||
raise ImportError # because assumes simplejson in path
|
||||
from simplejson._speedups import scanstring
|
||||
return scanstring
|
||||
except ImportError:
|
||||
return None
|
||||
c_scanstring = _import_c_scanstring()
|
||||
|
||||
__all__ = ['JSONDecoder']
|
||||
|
||||
FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
|
||||
|
||||
def _floatconstants():
|
||||
_BYTES = '7FF80000000000007FF0000000000000'.decode('hex')
|
||||
# The struct module in Python 2.4 would get frexp() out of range here
|
||||
# when an endian is specified in the format string. Fixed in Python 2.5+
|
||||
if sys.byteorder != 'big':
|
||||
_BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1]
|
||||
nan, inf = struct.unpack('dd', _BYTES)
|
||||
return nan, inf, -inf
|
||||
|
||||
NaN, PosInf, NegInf = _floatconstants()
|
||||
|
||||
|
||||
class JSONDecodeError(ValueError):
|
||||
"""Subclass of ValueError with the following additional properties:
|
||||
|
||||
msg: The unformatted error message
|
||||
doc: The JSON document being parsed
|
||||
pos: The start index of doc where parsing failed
|
||||
end: The end index of doc where parsing failed (may be None)
|
||||
lineno: The line corresponding to pos
|
||||
colno: The column corresponding to pos
|
||||
endlineno: The line corresponding to end (may be None)
|
||||
endcolno: The column corresponding to end (may be None)
|
||||
|
||||
"""
|
||||
def __init__(self, msg, doc, pos, end=None):
|
||||
ValueError.__init__(self, errmsg(msg, doc, pos, end=end))
|
||||
self.msg = msg
|
||||
self.doc = doc
|
||||
self.pos = pos
|
||||
self.end = end
|
||||
self.lineno, self.colno = linecol(doc, pos)
|
||||
if end is not None:
|
||||
self.endlineno, self.endcolno = linecol(doc, end)
|
||||
else:
|
||||
self.endlineno, self.endcolno = None, None
|
||||
|
||||
|
||||
def linecol(doc, pos):
|
||||
lineno = doc.count('\n', 0, pos) + 1
|
||||
if lineno == 1:
|
||||
colno = pos
|
||||
else:
|
||||
colno = pos - doc.rindex('\n', 0, pos)
|
||||
return lineno, colno
|
||||
|
||||
|
||||
def errmsg(msg, doc, pos, end=None):
|
||||
# Note that this function is called from _speedups
|
||||
lineno, colno = linecol(doc, pos)
|
||||
if end is None:
|
||||
#fmt = '{0}: line {1} column {2} (char {3})'
|
||||
#return fmt.format(msg, lineno, colno, pos)
|
||||
fmt = '%s: line %d column %d (char %d)'
|
||||
return fmt % (msg, lineno, colno, pos)
|
||||
endlineno, endcolno = linecol(doc, end)
|
||||
#fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})'
|
||||
#return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end)
|
||||
fmt = '%s: line %d column %d - line %d column %d (char %d - %d)'
|
||||
return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end)
|
||||
|
||||
|
||||
_CONSTANTS = {
|
||||
'-Infinity': NegInf,
|
||||
'Infinity': PosInf,
|
||||
'NaN': NaN,
|
||||
}
|
||||
|
||||
STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS)
|
||||
BACKSLASH = {
|
||||
'"': u'"', '\\': u'\\', '/': u'/',
|
||||
'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t',
|
||||
}
|
||||
|
||||
DEFAULT_ENCODING = "utf-8"
|
||||
|
||||
def py_scanstring(s, end, encoding=None, strict=True,
|
||||
_b=BACKSLASH, _m=STRINGCHUNK.match):
|
||||
"""Scan the string s for a JSON string. End is the index of the
|
||||
character in s after the quote that started the JSON string.
|
||||
Unescapes all valid JSON string escape sequences and raises ValueError
|
||||
on attempt to decode an invalid string. If strict is False then literal
|
||||
control characters are allowed in the string.
|
||||
|
||||
Returns a tuple of the decoded string and the index of the character in s
|
||||
after the end quote."""
|
||||
if encoding is None:
|
||||
encoding = DEFAULT_ENCODING
|
||||
chunks = []
|
||||
_append = chunks.append
|
||||
begin = end - 1
|
||||
while 1:
|
||||
chunk = _m(s, end)
|
||||
if chunk is None:
|
||||
raise JSONDecodeError(
|
||||
"Unterminated string starting at", s, begin)
|
||||
end = chunk.end()
|
||||
content, terminator = chunk.groups()
|
||||
# Content is contains zero or more unescaped string characters
|
||||
if content:
|
||||
if not isinstance(content, unicode):
|
||||
content = unicode(content, encoding)
|
||||
_append(content)
|
||||
# Terminator is the end of string, a literal control character,
|
||||
# or a backslash denoting that an escape sequence follows
|
||||
if terminator == '"':
|
||||
break
|
||||
elif terminator != '\\':
|
||||
if strict:
|
||||
msg = "Invalid control character %r at" % (terminator,)
|
||||
#msg = "Invalid control character {0!r} at".format(terminator)
|
||||
raise JSONDecodeError(msg, s, end)
|
||||
else:
|
||||
_append(terminator)
|
||||
continue
|
||||
try:
|
||||
esc = s[end]
|
||||
except IndexError:
|
||||
raise JSONDecodeError(
|
||||
"Unterminated string starting at", s, begin)
|
||||
# If not a unicode escape sequence, must be in the lookup table
|
||||
if esc != 'u':
|
||||
try:
|
||||
char = _b[esc]
|
||||
except KeyError:
|
||||
msg = "Invalid \\escape: " + repr(esc)
|
||||
raise JSONDecodeError(msg, s, end)
|
||||
end += 1
|
||||
else:
|
||||
# Unicode escape sequence
|
||||
esc = s[end + 1:end + 5]
|
||||
next_end = end + 5
|
||||
if len(esc) != 4:
|
||||
msg = "Invalid \\uXXXX escape"
|
||||
raise JSONDecodeError(msg, s, end)
|
||||
uni = int(esc, 16)
|
||||
# Check for surrogate pair on UCS-4 systems
|
||||
if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535:
|
||||
msg = "Invalid \\uXXXX\\uXXXX surrogate pair"
|
||||
if not s[end + 5:end + 7] == '\\u':
|
||||
raise JSONDecodeError(msg, s, end)
|
||||
esc2 = s[end + 7:end + 11]
|
||||
if len(esc2) != 4:
|
||||
raise JSONDecodeError(msg, s, end)
|
||||
uni2 = int(esc2, 16)
|
||||
uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00))
|
||||
next_end += 6
|
||||
char = unichr(uni)
|
||||
end = next_end
|
||||
# Append the unescaped character
|
||||
_append(char)
|
||||
return u''.join(chunks), end
|
||||
|
||||
|
||||
# Use speedup if available
|
||||
scanstring = c_scanstring or py_scanstring
|
||||
|
||||
WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS)
|
||||
WHITESPACE_STR = ' \t\n\r'
|
||||
|
||||
def JSONObject((s, end), encoding, strict, scan_once, object_hook,
|
||||
object_pairs_hook, memo=None,
|
||||
_w=WHITESPACE.match, _ws=WHITESPACE_STR):
|
||||
# Backwards compatibility
|
||||
if memo is None:
|
||||
memo = {}
|
||||
memo_get = memo.setdefault
|
||||
pairs = []
|
||||
# Use a slice to prevent IndexError from being raised, the following
|
||||
# check will raise a more specific ValueError if the string is empty
|
||||
nextchar = s[end:end + 1]
|
||||
# Normally we expect nextchar == '"'
|
||||
if nextchar != '"':
|
||||
if nextchar in _ws:
|
||||
end = _w(s, end).end()
|
||||
nextchar = s[end:end + 1]
|
||||
# Trivial empty object
|
||||
if nextchar == '}':
|
||||
if object_pairs_hook is not None:
|
||||
result = object_pairs_hook(pairs)
|
||||
return result, end + 1
|
||||
pairs = {}
|
||||
if object_hook is not None:
|
||||
pairs = object_hook(pairs)
|
||||
return pairs, end + 1
|
||||
elif nextchar != '"':
|
||||
raise JSONDecodeError("Expecting property name", s, end)
|
||||
end += 1
|
||||
while True:
|
||||
key, end = scanstring(s, end, encoding, strict)
|
||||
key = memo_get(key, key)
|
||||
|
||||
# To skip some function call overhead we optimize the fast paths where
|
||||
# the JSON key separator is ": " or just ":".
|
||||
if s[end:end + 1] != ':':
|
||||
end = _w(s, end).end()
|
||||
if s[end:end + 1] != ':':
|
||||
raise JSONDecodeError("Expecting : delimiter", s, end)
|
||||
|
||||
end += 1
|
||||
|
||||
try:
|
||||
if s[end] in _ws:
|
||||
end += 1
|
||||
if s[end] in _ws:
|
||||
end = _w(s, end + 1).end()
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
try:
|
||||
value, end = scan_once(s, end)
|
||||
except StopIteration:
|
||||
raise JSONDecodeError("Expecting object", s, end)
|
||||
pairs.append((key, value))
|
||||
|
||||
try:
|
||||
nextchar = s[end]
|
||||
if nextchar in _ws:
|
||||
end = _w(s, end + 1).end()
|
||||
nextchar = s[end]
|
||||
except IndexError:
|
||||
nextchar = ''
|
||||
end += 1
|
||||
|
||||
if nextchar == '}':
|
||||
break
|
||||
elif nextchar != ',':
|
||||
raise JSONDecodeError("Expecting , delimiter", s, end - 1)
|
||||
|
||||
try:
|
||||
nextchar = s[end]
|
||||
if nextchar in _ws:
|
||||
end += 1
|
||||
nextchar = s[end]
|
||||
if nextchar in _ws:
|
||||
end = _w(s, end + 1).end()
|
||||
nextchar = s[end]
|
||||
except IndexError:
|
||||
nextchar = ''
|
||||
|
||||
end += 1
|
||||
if nextchar != '"':
|
||||
raise JSONDecodeError("Expecting property name", s, end - 1)
|
||||
|
||||
if object_pairs_hook is not None:
|
||||
result = object_pairs_hook(pairs)
|
||||
return result, end
|
||||
pairs = dict(pairs)
|
||||
if object_hook is not None:
|
||||
pairs = object_hook(pairs)
|
||||
return pairs, end
|
||||
|
||||
def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR):
|
||||
values = []
|
||||
nextchar = s[end:end + 1]
|
||||
if nextchar in _ws:
|
||||
end = _w(s, end + 1).end()
|
||||
nextchar = s[end:end + 1]
|
||||
# Look-ahead for trivial empty array
|
||||
if nextchar == ']':
|
||||
return values, end + 1
|
||||
_append = values.append
|
||||
while True:
|
||||
try:
|
||||
value, end = scan_once(s, end)
|
||||
except StopIteration:
|
||||
raise JSONDecodeError("Expecting object", s, end)
|
||||
_append(value)
|
||||
nextchar = s[end:end + 1]
|
||||
if nextchar in _ws:
|
||||
end = _w(s, end + 1).end()
|
||||
nextchar = s[end:end + 1]
|
||||
end += 1
|
||||
if nextchar == ']':
|
||||
break
|
||||
elif nextchar != ',':
|
||||
raise JSONDecodeError("Expecting , delimiter", s, end)
|
||||
|
||||
try:
|
||||
if s[end] in _ws:
|
||||
end += 1
|
||||
if s[end] in _ws:
|
||||
end = _w(s, end + 1).end()
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return values, end
|
||||
|
||||
class JSONDecoder(object):
|
||||
"""Simple JSON <http://json.org> decoder
|
||||
|
||||
Performs the following translations in decoding by default:
|
||||
|
||||
+---------------+-------------------+
|
||||
| JSON | Python |
|
||||
+===============+===================+
|
||||
| object | dict |
|
||||
+---------------+-------------------+
|
||||
| array | list |
|
||||
+---------------+-------------------+
|
||||
| string | unicode |
|
||||
+---------------+-------------------+
|
||||
| number (int) | int, long |
|
||||
+---------------+-------------------+
|
||||
| number (real) | float |
|
||||
+---------------+-------------------+
|
||||
| true | True |
|
||||
+---------------+-------------------+
|
||||
| false | False |
|
||||
+---------------+-------------------+
|
||||
| null | None |
|
||||
+---------------+-------------------+
|
||||
|
||||
It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as
|
||||
their corresponding ``float`` values, which is outside the JSON spec.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, encoding=None, object_hook=None, parse_float=None,
|
||||
parse_int=None, parse_constant=None, strict=True,
|
||||
object_pairs_hook=None):
|
||||
"""
|
||||
*encoding* determines the encoding used to interpret any
|
||||
:class:`str` objects decoded by this instance (``'utf-8'`` by
|
||||
default). It has no effect when decoding :class:`unicode` objects.
|
||||
|
||||
Note that currently only encodings that are a superset of ASCII work,
|
||||
strings of other encodings should be passed in as :class:`unicode`.
|
||||
|
||||
*object_hook*, if specified, will be called with the result of every
|
||||
JSON object decoded and its return value will be used in place of the
|
||||
given :class:`dict`. This can be used to provide custom
|
||||
deserializations (e.g. to support JSON-RPC class hinting).
|
||||
|
||||
*object_pairs_hook* is an optional function that will be called with
|
||||
the result of any object literal decode with an ordered list of pairs.
|
||||
The return value of *object_pairs_hook* will be used instead of the
|
||||
:class:`dict`. This feature can be used to implement custom decoders
|
||||
that rely on the order that the key and value pairs are decoded (for
|
||||
example, :func:`collections.OrderedDict` will remember the order of
|
||||
insertion). If *object_hook* is also defined, the *object_pairs_hook*
|
||||
takes priority.
|
||||
|
||||
*parse_float*, if specified, will be called with the string of every
|
||||
JSON float to be decoded. By default, this is equivalent to
|
||||
``float(num_str)``. This can be used to use another datatype or parser
|
||||
for JSON floats (e.g. :class:`decimal.Decimal`).
|
||||
|
||||
*parse_int*, if specified, will be called with the string of every
|
||||
JSON int to be decoded. By default, this is equivalent to
|
||||
``int(num_str)``. This can be used to use another datatype or parser
|
||||
for JSON integers (e.g. :class:`float`).
|
||||
|
||||
*parse_constant*, if specified, will be called with one of the
|
||||
following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This
|
||||
can be used to raise an exception if invalid JSON numbers are
|
||||
encountered.
|
||||
|
||||
*strict* controls the parser's behavior when it encounters an
|
||||
invalid control character in a string. The default setting of
|
||||
``True`` means that unescaped control characters are parse errors, if
|
||||
``False`` then control characters will be allowed in strings.
|
||||
|
||||
"""
|
||||
self.encoding = encoding
|
||||
self.object_hook = object_hook
|
||||
self.object_pairs_hook = object_pairs_hook
|
||||
self.parse_float = parse_float or float
|
||||
self.parse_int = parse_int or int
|
||||
self.parse_constant = parse_constant or _CONSTANTS.__getitem__
|
||||
self.strict = strict
|
||||
self.parse_object = JSONObject
|
||||
self.parse_array = JSONArray
|
||||
self.parse_string = scanstring
|
||||
self.memo = {}
|
||||
self.scan_once = make_scanner(self)
|
||||
|
||||
def decode(self, s, _w=WHITESPACE.match):
|
||||
"""Return the Python representation of ``s`` (a ``str`` or ``unicode``
|
||||
instance containing a JSON document)
|
||||
|
||||
"""
|
||||
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
|
||||
end = _w(s, end).end()
|
||||
if end != len(s):
|
||||
raise JSONDecodeError("Extra data", s, end, len(s))
|
||||
return obj
|
||||
|
||||
def raw_decode(self, s, idx=0):
|
||||
"""Decode a JSON document from ``s`` (a ``str`` or ``unicode``
|
||||
beginning with a JSON document) and return a 2-tuple of the Python
|
||||
representation and the index in ``s`` where the document ended.
|
||||
|
||||
This can be used to decode a JSON document from a string that may
|
||||
have extraneous data at the end.
|
||||
|
||||
"""
|
||||
try:
|
||||
obj, end = self.scan_once(s, idx)
|
||||
except StopIteration:
|
||||
raise JSONDecodeError("No JSON object could be decoded", s, idx)
|
||||
return obj, end
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
"""Implementation of JSONEncoder
|
||||
"""
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
def _import_speedups():
|
||||
try:
|
||||
raise ImportError # because assumes simplejson in path
|
||||
from simplejson import _speedups
|
||||
return _speedups.encode_basestring_ascii, _speedups.make_encoder
|
||||
except ImportError:
|
||||
return None, None
|
||||
c_encode_basestring_ascii, c_make_encoder = _import_speedups()
|
||||
|
||||
from decoder import PosInf
|
||||
|
||||
ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]')
|
||||
ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])')
|
||||
HAS_UTF8 = re.compile(r'[\x80-\xff]')
|
||||
ESCAPE_DCT = {
|
||||
'\\': '\\\\',
|
||||
'"': '\\"',
|
||||
'\b': '\\b',
|
||||
'\f': '\\f',
|
||||
'\n': '\\n',
|
||||
'\r': '\\r',
|
||||
'\t': '\\t',
|
||||
}
|
||||
for i in range(0x20):
|
||||
#ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i))
|
||||
ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,))
|
||||
|
||||
FLOAT_REPR = repr
|
||||
|
||||
def encode_basestring(s):
|
||||
"""Return a JSON representation of a Python string
|
||||
|
||||
"""
|
||||
if isinstance(s, str) and HAS_UTF8.search(s) is not None:
|
||||
s = s.decode('utf-8')
|
||||
def replace(match):
|
||||
return ESCAPE_DCT[match.group(0)]
|
||||
return u'"' + ESCAPE.sub(replace, s) + u'"'
|
||||
|
||||
|
||||
def py_encode_basestring_ascii(s):
|
||||
"""Return an ASCII-only JSON representation of a Python string
|
||||
|
||||
"""
|
||||
if isinstance(s, str) and HAS_UTF8.search(s) is not None:
|
||||
s = s.decode('utf-8')
|
||||
def replace(match):
|
||||
s = match.group(0)
|
||||
try:
|
||||
return ESCAPE_DCT[s]
|
||||
except KeyError:
|
||||
n = ord(s)
|
||||
if n < 0x10000:
|
||||
#return '\\u{0:04x}'.format(n)
|
||||
return '\\u%04x' % (n,)
|
||||
else:
|
||||
# surrogate pair
|
||||
n -= 0x10000
|
||||
s1 = 0xd800 | ((n >> 10) & 0x3ff)
|
||||
s2 = 0xdc00 | (n & 0x3ff)
|
||||
#return '\\u{0:04x}\\u{1:04x}'.format(s1, s2)
|
||||
return '\\u%04x\\u%04x' % (s1, s2)
|
||||
return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"'
|
||||
|
||||
|
||||
encode_basestring_ascii = (
|
||||
c_encode_basestring_ascii or py_encode_basestring_ascii)
|
||||
|
||||
class JSONEncoder(object):
|
||||
"""Extensible JSON <http://json.org> encoder for Python data structures.
|
||||
|
||||
Supports the following objects and types by default:
|
||||
|
||||
+-------------------+---------------+
|
||||
| Python | JSON |
|
||||
+===================+===============+
|
||||
| dict | object |
|
||||
+-------------------+---------------+
|
||||
| list, tuple | array |
|
||||
+-------------------+---------------+
|
||||
| str, unicode | string |
|
||||
+-------------------+---------------+
|
||||
| int, long, float | number |
|
||||
+-------------------+---------------+
|
||||
| True | true |
|
||||
+-------------------+---------------+
|
||||
| False | false |
|
||||
+-------------------+---------------+
|
||||
| None | null |
|
||||
+-------------------+---------------+
|
||||
|
||||
To extend this to recognize other objects, subclass and implement a
|
||||
``.default()`` method with another method that returns a serializable
|
||||
object for ``o`` if possible, otherwise it should call the superclass
|
||||
implementation (to raise ``TypeError``).
|
||||
|
||||
"""
|
||||
item_separator = ', '
|
||||
key_separator = ': '
|
||||
def __init__(self, skipkeys=False, ensure_ascii=True,
|
||||
check_circular=True, allow_nan=True, sort_keys=False,
|
||||
indent=None, separators=None, encoding='utf-8', default=None,
|
||||
use_decimal=False):
|
||||
"""Constructor for JSONEncoder, with sensible defaults.
|
||||
|
||||
If skipkeys is false, then it is a TypeError to attempt
|
||||
encoding of keys that are not str, int, long, float or None. If
|
||||
skipkeys is True, such items are simply skipped.
|
||||
|
||||
If ensure_ascii is true, the output is guaranteed to be str
|
||||
objects with all incoming unicode characters escaped. If
|
||||
ensure_ascii is false, the output will be unicode object.
|
||||
|
||||
If check_circular is true, then lists, dicts, and custom encoded
|
||||
objects will be checked for circular references during encoding to
|
||||
prevent an infinite recursion (which would cause an OverflowError).
|
||||
Otherwise, no such check takes place.
|
||||
|
||||
If allow_nan is true, then NaN, Infinity, and -Infinity will be
|
||||
encoded as such. This behavior is not JSON specification compliant,
|
||||
but is consistent with most JavaScript based encoders and decoders.
|
||||
Otherwise, it will be a ValueError to encode such floats.
|
||||
|
||||
If sort_keys is true, then the output of dictionaries will be
|
||||
sorted by key; this is useful for regression tests to ensure
|
||||
that JSON serializations can be compared on a day-to-day basis.
|
||||
|
||||
If indent is a string, then JSON array elements and object members
|
||||
will be pretty-printed with a newline followed by that string repeated
|
||||
for each level of nesting. ``None`` (the default) selects the most compact
|
||||
representation without any newlines. For backwards compatibility with
|
||||
versions of simplejson earlier than 2.1.0, an integer is also accepted
|
||||
and is converted to a string with that many spaces.
|
||||
|
||||
If specified, separators should be a (item_separator, key_separator)
|
||||
tuple. The default is (', ', ': '). To get the most compact JSON
|
||||
representation you should specify (',', ':') to eliminate whitespace.
|
||||
|
||||
If specified, default is a function that gets called for objects
|
||||
that can't otherwise be serialized. It should return a JSON encodable
|
||||
version of the object or raise a ``TypeError``.
|
||||
|
||||
If encoding is not None, then all input strings will be
|
||||
transformed into unicode using that encoding prior to JSON-encoding.
|
||||
The default is UTF-8.
|
||||
|
||||
If use_decimal is true (not the default), ``decimal.Decimal`` will
|
||||
be supported directly by the encoder. For the inverse, decode JSON
|
||||
with ``parse_float=decimal.Decimal``.
|
||||
|
||||
"""
|
||||
|
||||
self.skipkeys = skipkeys
|
||||
self.ensure_ascii = ensure_ascii
|
||||
self.check_circular = check_circular
|
||||
self.allow_nan = allow_nan
|
||||
self.sort_keys = sort_keys
|
||||
self.use_decimal = use_decimal
|
||||
if isinstance(indent, (int, long)):
|
||||
indent = ' ' * indent
|
||||
self.indent = indent
|
||||
if separators is not None:
|
||||
self.item_separator, self.key_separator = separators
|
||||
if default is not None:
|
||||
self.default = default
|
||||
self.encoding = encoding
|
||||
|
||||
def default(self, o):
|
||||
"""Implement this method in a subclass such that it returns
|
||||
a serializable object for ``o``, or calls the base implementation
|
||||
(to raise a ``TypeError``).
|
||||
|
||||
For example, to support arbitrary iterators, you could
|
||||
implement default like this::
|
||||
|
||||
def default(self, o):
|
||||
try:
|
||||
iterable = iter(o)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
return list(iterable)
|
||||
return JSONEncoder.default(self, o)
|
||||
|
||||
"""
|
||||
raise TypeError(repr(o) + " is not JSON serializable")
|
||||
|
||||
def encode(self, o):
|
||||
"""Return a JSON string representation of a Python data structure.
|
||||
|
||||
>>> from simplejson import JSONEncoder
|
||||
>>> JSONEncoder().encode({"foo": ["bar", "baz"]})
|
||||
'{"foo": ["bar", "baz"]}'
|
||||
|
||||
"""
|
||||
# This is for extremely simple cases and benchmarks.
|
||||
if isinstance(o, basestring):
|
||||
if isinstance(o, str):
|
||||
_encoding = self.encoding
|
||||
if (_encoding is not None
|
||||
and not (_encoding == 'utf-8')):
|
||||
o = o.decode(_encoding)
|
||||
if self.ensure_ascii:
|
||||
return encode_basestring_ascii(o)
|
||||
else:
|
||||
return encode_basestring(o)
|
||||
# This doesn't pass the iterator directly to ''.join() because the
|
||||
# exceptions aren't as detailed. The list call should be roughly
|
||||
# equivalent to the PySequence_Fast that ''.join() would do.
|
||||
chunks = self.iterencode(o, _one_shot=True)
|
||||
if not isinstance(chunks, (list, tuple)):
|
||||
chunks = list(chunks)
|
||||
if self.ensure_ascii:
|
||||
return ''.join(chunks)
|
||||
else:
|
||||
return u''.join(chunks)
|
||||
|
||||
def iterencode(self, o, _one_shot=False):
|
||||
"""Encode the given object and yield each string
|
||||
representation as available.
|
||||
|
||||
For example::
|
||||
|
||||
for chunk in JSONEncoder().iterencode(bigobject):
|
||||
mysocket.write(chunk)
|
||||
|
||||
"""
|
||||
if self.check_circular:
|
||||
markers = {}
|
||||
else:
|
||||
markers = None
|
||||
if self.ensure_ascii:
|
||||
_encoder = encode_basestring_ascii
|
||||
else:
|
||||
_encoder = encode_basestring
|
||||
if self.encoding != 'utf-8':
|
||||
def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding):
|
||||
if isinstance(o, str):
|
||||
o = o.decode(_encoding)
|
||||
return _orig_encoder(o)
|
||||
|
||||
def floatstr(o, allow_nan=self.allow_nan,
|
||||
_repr=FLOAT_REPR, _inf=PosInf, _neginf=-PosInf):
|
||||
# Check for specials. Note that this type of test is processor
|
||||
# and/or platform-specific, so do tests which don't depend on
|
||||
# the internals.
|
||||
|
||||
if o != o:
|
||||
text = 'NaN'
|
||||
elif o == _inf:
|
||||
text = 'Infinity'
|
||||
elif o == _neginf:
|
||||
text = '-Infinity'
|
||||
else:
|
||||
return _repr(o)
|
||||
|
||||
if not allow_nan:
|
||||
raise ValueError(
|
||||
"Out of range float values are not JSON compliant: " +
|
||||
repr(o))
|
||||
|
||||
return text
|
||||
|
||||
|
||||
key_memo = {}
|
||||
if (_one_shot and c_make_encoder is not None
|
||||
and self.indent is None):
|
||||
_iterencode = c_make_encoder(
|
||||
markers, self.default, _encoder, self.indent,
|
||||
self.key_separator, self.item_separator, self.sort_keys,
|
||||
self.skipkeys, self.allow_nan, key_memo, self.use_decimal)
|
||||
else:
|
||||
_iterencode = _make_iterencode(
|
||||
markers, self.default, _encoder, self.indent, floatstr,
|
||||
self.key_separator, self.item_separator, self.sort_keys,
|
||||
self.skipkeys, _one_shot, self.use_decimal)
|
||||
try:
|
||||
return _iterencode(o, 0)
|
||||
finally:
|
||||
key_memo.clear()
|
||||
|
||||
|
||||
class JSONEncoderForHTML(JSONEncoder):
|
||||
"""An encoder that produces JSON safe to embed in HTML.
|
||||
|
||||
To embed JSON content in, say, a script tag on a web page, the
|
||||
characters &, < and > should be escaped. They cannot be escaped
|
||||
with the usual entities (e.g. &) because they are not expanded
|
||||
within <script> tags.
|
||||
"""
|
||||
|
||||
def encode(self, o):
|
||||
# Override JSONEncoder.encode because it has hacks for
|
||||
# performance that make things more complicated.
|
||||
chunks = self.iterencode(o, True)
|
||||
if self.ensure_ascii:
|
||||
return ''.join(chunks)
|
||||
else:
|
||||
return u''.join(chunks)
|
||||
|
||||
def iterencode(self, o, _one_shot=False):
|
||||
chunks = super(JSONEncoderForHTML, self).iterencode(o, _one_shot)
|
||||
for chunk in chunks:
|
||||
chunk = chunk.replace('&', '\\u0026')
|
||||
chunk = chunk.replace('<', '\\u003c')
|
||||
chunk = chunk.replace('>', '\\u003e')
|
||||
yield chunk
|
||||
|
||||
|
||||
def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
|
||||
_key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot,
|
||||
_use_decimal,
|
||||
## HACK: hand-optimized bytecode; turn globals into locals
|
||||
False=False,
|
||||
True=True,
|
||||
ValueError=ValueError,
|
||||
basestring=basestring,
|
||||
Decimal=Decimal,
|
||||
dict=dict,
|
||||
float=float,
|
||||
id=id,
|
||||
int=int,
|
||||
isinstance=isinstance,
|
||||
list=list,
|
||||
long=long,
|
||||
str=str,
|
||||
tuple=tuple,
|
||||
):
|
||||
|
||||
def _iterencode_list(lst, _current_indent_level):
|
||||
if not lst:
|
||||
yield '[]'
|
||||
return
|
||||
if markers is not None:
|
||||
markerid = id(lst)
|
||||
if markerid in markers:
|
||||
raise ValueError("Circular reference detected")
|
||||
markers[markerid] = lst
|
||||
buf = '['
|
||||
if _indent is not None:
|
||||
_current_indent_level += 1
|
||||
newline_indent = '\n' + (_indent * _current_indent_level)
|
||||
separator = _item_separator + newline_indent
|
||||
buf += newline_indent
|
||||
else:
|
||||
newline_indent = None
|
||||
separator = _item_separator
|
||||
first = True
|
||||
for value in lst:
|
||||
if first:
|
||||
first = False
|
||||
else:
|
||||
buf = separator
|
||||
if isinstance(value, basestring):
|
||||
yield buf + _encoder(value)
|
||||
elif value is None:
|
||||
yield buf + 'null'
|
||||
elif value is True:
|
||||
yield buf + 'true'
|
||||
elif value is False:
|
||||
yield buf + 'false'
|
||||
elif isinstance(value, (int, long)):
|
||||
yield buf + str(value)
|
||||
elif isinstance(value, float):
|
||||
yield buf + _floatstr(value)
|
||||
elif _use_decimal and isinstance(value, Decimal):
|
||||
yield buf + str(value)
|
||||
else:
|
||||
yield buf
|
||||
if isinstance(value, (list, tuple)):
|
||||
chunks = _iterencode_list(value, _current_indent_level)
|
||||
elif isinstance(value, dict):
|
||||
chunks = _iterencode_dict(value, _current_indent_level)
|
||||
else:
|
||||
chunks = _iterencode(value, _current_indent_level)
|
||||
for chunk in chunks:
|
||||
yield chunk
|
||||
if newline_indent is not None:
|
||||
_current_indent_level -= 1
|
||||
yield '\n' + (_indent * _current_indent_level)
|
||||
yield ']'
|
||||
if markers is not None:
|
||||
del markers[markerid]
|
||||
|
||||
def _iterencode_dict(dct, _current_indent_level):
|
||||
if not dct:
|
||||
yield '{}'
|
||||
return
|
||||
if markers is not None:
|
||||
markerid = id(dct)
|
||||
if markerid in markers:
|
||||
raise ValueError("Circular reference detected")
|
||||
markers[markerid] = dct
|
||||
yield '{'
|
||||
if _indent is not None:
|
||||
_current_indent_level += 1
|
||||
newline_indent = '\n' + (_indent * _current_indent_level)
|
||||
item_separator = _item_separator + newline_indent
|
||||
yield newline_indent
|
||||
else:
|
||||
newline_indent = None
|
||||
item_separator = _item_separator
|
||||
first = True
|
||||
if _sort_keys:
|
||||
items = dct.items()
|
||||
items.sort(key=lambda kv: kv[0])
|
||||
else:
|
||||
items = dct.iteritems()
|
||||
for key, value in items:
|
||||
if isinstance(key, basestring):
|
||||
pass
|
||||
# JavaScript is weakly typed for these, so it makes sense to
|
||||
# also allow them. Many encoders seem to do something like this.
|
||||
elif isinstance(key, float):
|
||||
key = _floatstr(key)
|
||||
elif key is True:
|
||||
key = 'true'
|
||||
elif key is False:
|
||||
key = 'false'
|
||||
elif key is None:
|
||||
key = 'null'
|
||||
elif isinstance(key, (int, long)):
|
||||
key = str(key)
|
||||
elif _skipkeys:
|
||||
continue
|
||||
else:
|
||||
raise TypeError("key " + repr(key) + " is not a string")
|
||||
if first:
|
||||
first = False
|
||||
else:
|
||||
yield item_separator
|
||||
yield _encoder(key)
|
||||
yield _key_separator
|
||||
if isinstance(value, basestring):
|
||||
yield _encoder(value)
|
||||
elif value is None:
|
||||
yield 'null'
|
||||
elif value is True:
|
||||
yield 'true'
|
||||
elif value is False:
|
||||
yield 'false'
|
||||
elif isinstance(value, (int, long)):
|
||||
yield str(value)
|
||||
elif isinstance(value, float):
|
||||
yield _floatstr(value)
|
||||
elif _use_decimal and isinstance(value, Decimal):
|
||||
yield str(value)
|
||||
else:
|
||||
if isinstance(value, (list, tuple)):
|
||||
chunks = _iterencode_list(value, _current_indent_level)
|
||||
elif isinstance(value, dict):
|
||||
chunks = _iterencode_dict(value, _current_indent_level)
|
||||
else:
|
||||
chunks = _iterencode(value, _current_indent_level)
|
||||
for chunk in chunks:
|
||||
yield chunk
|
||||
if newline_indent is not None:
|
||||
_current_indent_level -= 1
|
||||
yield '\n' + (_indent * _current_indent_level)
|
||||
yield '}'
|
||||
if markers is not None:
|
||||
del markers[markerid]
|
||||
|
||||
def _iterencode(o, _current_indent_level):
|
||||
if isinstance(o, basestring):
|
||||
yield _encoder(o)
|
||||
elif o is None:
|
||||
yield 'null'
|
||||
elif o is True:
|
||||
yield 'true'
|
||||
elif o is False:
|
||||
yield 'false'
|
||||
elif isinstance(o, (int, long)):
|
||||
yield str(o)
|
||||
elif isinstance(o, float):
|
||||
yield _floatstr(o)
|
||||
elif isinstance(o, (list, tuple)):
|
||||
for chunk in _iterencode_list(o, _current_indent_level):
|
||||
yield chunk
|
||||
elif isinstance(o, dict):
|
||||
for chunk in _iterencode_dict(o, _current_indent_level):
|
||||
yield chunk
|
||||
elif _use_decimal and isinstance(o, Decimal):
|
||||
yield str(o)
|
||||
else:
|
||||
if markers is not None:
|
||||
markerid = id(o)
|
||||
if markerid in markers:
|
||||
raise ValueError("Circular reference detected")
|
||||
markers[markerid] = o
|
||||
o = _default(o)
|
||||
for chunk in _iterencode(o, _current_indent_level):
|
||||
yield chunk
|
||||
if markers is not None:
|
||||
del markers[markerid]
|
||||
|
||||
return _iterencode
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Drop-in replacement for collections.OrderedDict by Raymond Hettinger
|
||||
|
||||
http://code.activestate.com/recipes/576693/
|
||||
|
||||
"""
|
||||
from UserDict import DictMixin
|
||||
|
||||
# Modified from original to support Python 2.4, see
|
||||
# http://code.google.com/p/simplejson/issues/detail?id=53
|
||||
try:
|
||||
all
|
||||
except NameError:
|
||||
def all(seq):
|
||||
for elem in seq:
|
||||
if not elem:
|
||||
return False
|
||||
return True
|
||||
|
||||
class OrderedDict(dict, DictMixin):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
if len(args) > 1:
|
||||
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||
try:
|
||||
self.__end
|
||||
except AttributeError:
|
||||
self.clear()
|
||||
self.update(*args, **kwds)
|
||||
|
||||
def clear(self):
|
||||
self.__end = end = []
|
||||
end += [None, end, end] # sentinel node for doubly linked list
|
||||
self.__map = {} # key --> [key, prev, next]
|
||||
dict.clear(self)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self:
|
||||
end = self.__end
|
||||
curr = end[1]
|
||||
curr[2] = end[1] = self.__map[key] = [key, curr, end]
|
||||
dict.__setitem__(self, key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
key, prev, next = self.__map.pop(key)
|
||||
prev[2] = next
|
||||
next[1] = prev
|
||||
|
||||
def __iter__(self):
|
||||
end = self.__end
|
||||
curr = end[2]
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[2]
|
||||
|
||||
def __reversed__(self):
|
||||
end = self.__end
|
||||
curr = end[1]
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[1]
|
||||
|
||||
def popitem(self, last=True):
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
# Modified from original to support Python 2.4, see
|
||||
# http://code.google.com/p/simplejson/issues/detail?id=53
|
||||
if last:
|
||||
key = reversed(self).next()
|
||||
else:
|
||||
key = iter(self).next()
|
||||
value = self.pop(key)
|
||||
return key, value
|
||||
|
||||
def __reduce__(self):
|
||||
items = [[k, self[k]] for k in self]
|
||||
tmp = self.__map, self.__end
|
||||
del self.__map, self.__end
|
||||
inst_dict = vars(self).copy()
|
||||
self.__map, self.__end = tmp
|
||||
if inst_dict:
|
||||
return (self.__class__, (items,), inst_dict)
|
||||
return self.__class__, (items,)
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
setdefault = DictMixin.setdefault
|
||||
update = DictMixin.update
|
||||
pop = DictMixin.pop
|
||||
values = DictMixin.values
|
||||
items = DictMixin.items
|
||||
iterkeys = DictMixin.iterkeys
|
||||
itervalues = DictMixin.itervalues
|
||||
iteritems = DictMixin.iteritems
|
||||
|
||||
def __repr__(self):
|
||||
if not self:
|
||||
return '%s()' % (self.__class__.__name__,)
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self)
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
d = cls()
|
||||
for key in iterable:
|
||||
d[key] = value
|
||||
return d
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, OrderedDict):
|
||||
return len(self)==len(other) and \
|
||||
all(p==q for p, q in zip(self.items(), other.items()))
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""JSON token scanner
|
||||
"""
|
||||
import re
|
||||
def _import_c_make_scanner():
|
||||
try:
|
||||
raise ImportError # because assumes simplejson in path
|
||||
from simplejson._speedups import make_scanner
|
||||
return make_scanner
|
||||
except ImportError:
|
||||
return None
|
||||
c_make_scanner = _import_c_make_scanner()
|
||||
|
||||
__all__ = ['make_scanner']
|
||||
|
||||
NUMBER_RE = re.compile(
|
||||
r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?',
|
||||
(re.VERBOSE | re.MULTILINE | re.DOTALL))
|
||||
|
||||
def py_make_scanner(context):
|
||||
parse_object = context.parse_object
|
||||
parse_array = context.parse_array
|
||||
parse_string = context.parse_string
|
||||
match_number = NUMBER_RE.match
|
||||
encoding = context.encoding
|
||||
strict = context.strict
|
||||
parse_float = context.parse_float
|
||||
parse_int = context.parse_int
|
||||
parse_constant = context.parse_constant
|
||||
object_hook = context.object_hook
|
||||
object_pairs_hook = context.object_pairs_hook
|
||||
memo = context.memo
|
||||
|
||||
def _scan_once(string, idx):
|
||||
try:
|
||||
nextchar = string[idx]
|
||||
except IndexError:
|
||||
raise StopIteration
|
||||
|
||||
if nextchar == '"':
|
||||
return parse_string(string, idx + 1, encoding, strict)
|
||||
elif nextchar == '{':
|
||||
return parse_object((string, idx + 1), encoding, strict,
|
||||
_scan_once, object_hook, object_pairs_hook, memo)
|
||||
elif nextchar == '[':
|
||||
return parse_array((string, idx + 1), _scan_once)
|
||||
elif nextchar == 'n' and string[idx:idx + 4] == 'null':
|
||||
return None, idx + 4
|
||||
elif nextchar == 't' and string[idx:idx + 4] == 'true':
|
||||
return True, idx + 4
|
||||
elif nextchar == 'f' and string[idx:idx + 5] == 'false':
|
||||
return False, idx + 5
|
||||
|
||||
m = match_number(string, idx)
|
||||
if m is not None:
|
||||
integer, frac, exp = m.groups()
|
||||
if frac or exp:
|
||||
res = parse_float(integer + (frac or '') + (exp or ''))
|
||||
else:
|
||||
res = parse_int(integer)
|
||||
return res, m.end()
|
||||
elif nextchar == 'N' and string[idx:idx + 3] == 'NaN':
|
||||
return parse_constant('NaN'), idx + 3
|
||||
elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity':
|
||||
return parse_constant('Infinity'), idx + 8
|
||||
elif nextchar == '-' and string[idx:idx + 9] == '-Infinity':
|
||||
return parse_constant('-Infinity'), idx + 9
|
||||
else:
|
||||
raise StopIteration
|
||||
|
||||
def scan_once(string, idx):
|
||||
try:
|
||||
return _scan_once(string, idx)
|
||||
finally:
|
||||
memo.clear()
|
||||
|
||||
return scan_once
|
||||
|
||||
make_scanner = c_make_scanner or py_make_scanner
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
r"""Command-line tool to validate and pretty-print JSON
|
||||
|
||||
Usage::
|
||||
|
||||
$ echo '{"json":"obj"}' | python -m simplejson.tool
|
||||
{
|
||||
"json": "obj"
|
||||
}
|
||||
$ echo '{ 1.2:3.4}' | python -m simplejson.tool
|
||||
Expecting property name: line 1 column 2 (char 2)
|
||||
|
||||
"""
|
||||
import sys
|
||||
import simplejson as json
|
||||
|
||||
def main():
|
||||
if len(sys.argv) == 1:
|
||||
infile = sys.stdin
|
||||
outfile = sys.stdout
|
||||
elif len(sys.argv) == 2:
|
||||
infile = open(sys.argv[1], 'rb')
|
||||
outfile = sys.stdout
|
||||
elif len(sys.argv) == 3:
|
||||
infile = open(sys.argv[1], 'rb')
|
||||
outfile = open(sys.argv[2], 'wb')
|
||||
else:
|
||||
raise SystemExit(sys.argv[0] + " [infile [outfile]]")
|
||||
try:
|
||||
try:
|
||||
obj = json.load(infile,
|
||||
object_pairs_hook=json.OrderedDict,
|
||||
use_decimal=True)
|
||||
except ValueError, e:
|
||||
raise SystemExit(e)
|
||||
json.dump(obj, outfile, sort_keys=True, indent=' ', use_decimal=True)
|
||||
outfile.write('\n')
|
||||
finally:
|
||||
infile.close()
|
||||
outfile.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by the
|
||||
# Free Software Foundation; either version 3, 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 MERCHANTIBILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
# for more details.
|
||||
|
||||
"Pythonic simple JSON RPC Client implementation"
|
||||
|
||||
__author__ = "Mariano Reingart (reingart@gmail.com)"
|
||||
__copyright__ = "Copyright (C) 2011 Mariano Reingart"
|
||||
__license__ = "LGPL 3.0"
|
||||
__version__ = "0.04"
|
||||
|
||||
|
||||
import urllib
|
||||
from xmlrpclib import Transport, SafeTransport
|
||||
from cStringIO import StringIO
|
||||
import random
|
||||
import sys
|
||||
try:
|
||||
import gluon.contrib.simplejson as json # try web2py json serializer
|
||||
except ImportError:
|
||||
try:
|
||||
import json # try stdlib (py2.6)
|
||||
except:
|
||||
import simplejson as json # try external module
|
||||
|
||||
|
||||
class JSONRPCError(RuntimeError):
|
||||
"Error object for remote procedure call fail"
|
||||
def __init__(self, code, message):
|
||||
self.code = code
|
||||
self.message = message
|
||||
def __unicode__(self):
|
||||
return u"%s: %s" % (self.code, self.message)
|
||||
def __str__(self):
|
||||
return self.__unicode__().encode("ascii","ignore")
|
||||
|
||||
|
||||
class JSONDummyParser:
|
||||
"json wrapper for xmlrpclib parser interfase"
|
||||
def __init__(self):
|
||||
self.buf = StringIO()
|
||||
def feed(self, data):
|
||||
self.buf.write(data)
|
||||
def close(self):
|
||||
return self.buf.getvalue()
|
||||
|
||||
|
||||
class JSONTransportMixin:
|
||||
"json wrapper for xmlrpclib transport interfase"
|
||||
|
||||
def send_content(self, connection, request_body):
|
||||
connection.putheader("Content-Type", "application/json")
|
||||
connection.putheader("Content-Length", str(len(request_body)))
|
||||
connection.endheaders()
|
||||
if request_body:
|
||||
connection.send(request_body)
|
||||
# todo: add gzip compression
|
||||
|
||||
def getparser(self):
|
||||
# get parser and unmarshaller
|
||||
parser = JSONDummyParser()
|
||||
return parser, parser
|
||||
|
||||
|
||||
class JSONTransport(JSONTransportMixin, Transport):
|
||||
pass
|
||||
|
||||
class JSONSafeTransport(JSONTransportMixin, SafeTransport):
|
||||
pass
|
||||
|
||||
|
||||
class ServerProxy(object):
|
||||
"JSON RPC Simple Client Service Proxy"
|
||||
|
||||
def __init__(self, uri, transport=None, encoding=None, verbose=0):
|
||||
self.location = uri # server location (url)
|
||||
self.trace = verbose # show debug messages
|
||||
self.exceptions = True # raise errors? (JSONRPCError)
|
||||
self.timeout = None
|
||||
self.json_request = self.json_response = ''
|
||||
|
||||
type, uri = urllib.splittype(uri)
|
||||
if type not in ("http", "https"):
|
||||
raise IOError, "unsupported JSON-RPC protocol"
|
||||
self.__host, self.__handler = urllib.splithost(uri)
|
||||
|
||||
if transport is None:
|
||||
if type == "https":
|
||||
transport = JSONSafeTransport()
|
||||
else:
|
||||
transport = JSONTransport()
|
||||
self.__transport = transport
|
||||
self.__encoding = encoding
|
||||
self.__verbose = verbose
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"pseudo method that can be called"
|
||||
return lambda *args: self.call(attr, *args)
|
||||
|
||||
def call(self, method, *args):
|
||||
"JSON RPC communication (method invocation)"
|
||||
|
||||
# build data sent to the service
|
||||
request_id = random.randint(0, sys.maxint)
|
||||
data = {'id': request_id, 'method': method, 'params': args, }
|
||||
request = json.dumps(data)
|
||||
|
||||
# make HTTP request (retry if connection is lost)
|
||||
response = self.__transport.request(
|
||||
self.__host,
|
||||
self.__handler,
|
||||
request,
|
||||
verbose=self.__verbose
|
||||
)
|
||||
|
||||
# store plain request and response for further debugging
|
||||
self.json_request = request
|
||||
self.json_response = response
|
||||
|
||||
# parse json data coming from service
|
||||
# {'version': '1.1', 'id': id, 'result': result, 'error': None}
|
||||
response = json.loads(response)
|
||||
|
||||
if response['id'] != request_id:
|
||||
raise JSONRPCError(0, "JSON Request ID != Response ID")
|
||||
|
||||
self.error = response.get('error', {})
|
||||
if self.error and self.exceptions:
|
||||
raise JSONRPCError(self.error.get('code', 0), self.error.get('message', ''))
|
||||
|
||||
return response.get('result')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# basic tests:
|
||||
location = "http://www.web2py.com.ar/webservices/sample/call/jsonrpc"
|
||||
client = ServerProxy(location, verbose='--verbose' in sys.argv,)
|
||||
print client.add(1, 2)
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
SMSCODES = {
|
||||
'Aliant':'@chat.wirefree.ca',
|
||||
'Alltel':'@message.alltel.com',
|
||||
'Ameritech':'@paging.acswireless.com',
|
||||
'AT&T':'@txt.att.net',
|
||||
'AU by KDDI':'@ezweb.ne.jp',
|
||||
'BeeLine GSM':'@sms.beemail.ru',
|
||||
'Bell Mobility Canada':'@txt.bellmobility.ca',
|
||||
'Bellsouth':'@bellsouth.cl',
|
||||
'BellSouth Mobility':'@blsdcs.net',
|
||||
'Blue Sky Frog':'@blueskyfrog.com',
|
||||
'Boost':'@myboostmobile.com',
|
||||
'Cellular South':'@csouth1.com',
|
||||
'CellularOne':'@mobile.celloneusa.com',
|
||||
'CellularOne West':'@mycellone.com',
|
||||
'Cincinnati Bell':'@gocbw.com',
|
||||
'Claro':'@clarotorpedo.com.br',
|
||||
'Comviq':'@sms.comviq.se',
|
||||
'Dutchtone/Orange-NL':'@sms.orange.nl',
|
||||
'Edge Wireless':'@sms.edgewireless.com',
|
||||
'EinsteinPCS / Airadigm Communications':'@einsteinsms.com',
|
||||
'EPlus':'@smsmail.eplus.de',
|
||||
'Fido Canada':'@fido.ca',
|
||||
'Golden Telecom':'@sms.goldentele.com',
|
||||
'Idea Cellular':'@ideacellular.net',
|
||||
'Kyivstar':'@sms.kyivstar.net',
|
||||
'LMT':'@sms.lmt.lv',
|
||||
'Manitoba Telecom Systems':'@text.mtsmobility.com',
|
||||
'Meteor':'@sms.mymeteor.ie',
|
||||
'Metro PCS':'@mymetropcs.com',
|
||||
'Metrocall Pager':'@page.metrocall.com',
|
||||
'MobileOne':'@m1.com.sg',
|
||||
'Mobilfone':'@page.mobilfone.com',
|
||||
'Mobility Bermuda':'@ml.bm',
|
||||
'Netcom':'@sms.netcom.no',
|
||||
'Nextel':'@messaging.nextel.com',
|
||||
'NPI Wireless':'@npiwireless.com',
|
||||
'O2':'@o2.co.uk',
|
||||
'O2 M-mail':'@mmail.co.uk',
|
||||
'Optus':'@optusmobile.com.au',
|
||||
'Orange':'@orange.net',
|
||||
'Oskar':'@mujoskar.cz',
|
||||
'Pagenet':'@pagenet.net',
|
||||
'PCS Rogers':'@pcs.rogers.com',
|
||||
'Personal Communication':'@pcom.ru',
|
||||
'Plus GSM Poland':'@text.plusgsm.pl',
|
||||
'Powertel':'@ptel.net',
|
||||
'Primtel':'@sms.primtel.ru',
|
||||
'PSC Wireless':'@sms.pscel.com',
|
||||
'Qualcomm':'@pager.qualcomm.com',
|
||||
'Qwest':'@qwestmp.com',
|
||||
'Safaricom':'@safaricomsms.com',
|
||||
'Satelindo GSM':'@satelindogsm.com',
|
||||
'SCS-900':'@scs-900.ru',
|
||||
'Simple Freedom':'@text.simplefreedom.net',
|
||||
'Skytel - Alphanumeric':'@skytel.com',
|
||||
'Smart Telecom':'@mysmart.mymobile.ph',
|
||||
'Southern Linc':'@page.southernlinc.com',
|
||||
'Sprint PCS':'@messaging.sprintpcs.com',
|
||||
'Sprint PCS - Short Mail':'@sprintpcs.com',
|
||||
'SunCom':'@tms.suncom.com',
|
||||
'SureWest Communications':'@mobile.surewest.com',
|
||||
'SwissCom Mobile':'@bluewin.ch',
|
||||
'T-Mobile Germany':'@T-D1-SMS.de',
|
||||
'T-Mobile Netherlands':'@gin.nl',
|
||||
'T-Mobile UK':'@t-mobile.uk.net',
|
||||
'T-Mobile USA (tmail)':'@tmail.com',
|
||||
'T-Mobile USA (tmomail)':'@tmomail.net',
|
||||
'Tele2 Latvia':'@sms.tele2.lv',
|
||||
'Telefonica Movistar':'@movistar.net',
|
||||
'Telenor':'@mobilpost.no',
|
||||
'Telia Denmark':'@gsm1800.telia.dk',
|
||||
'Telus Mobility':'@msg.telus.com',
|
||||
'The Phone House':'@sms.phonehouse.de',
|
||||
'TIM':'@timnet.com',
|
||||
'UMC':'@sms.umc.com.ua',
|
||||
'Unicel':'@utext.com',
|
||||
'US Cellular':'@email.uscc.net',
|
||||
'Verizon Wireless (vtext)':'@vtext.com',
|
||||
'Verizon Wireless (airtouchpaging)':'@airtouchpaging.com',
|
||||
'Verizon Wireless (myairmail)':'@myairmail.com',
|
||||
'Vessotel':'@pager.irkutsk.ru',
|
||||
'Virgin Mobile Canada':'@vmobile.ca',
|
||||
'Virgin Mobile USA':'@vmobl.com',
|
||||
'Vodafone Italy':'@sms.vodafone.it',
|
||||
'Vodafone Japan (n)':'@n.vodafone.ne.jp',
|
||||
'Vodafone Japan (d)':'@d.vodafone.ne.jp',
|
||||
'Vodafone Japan (r)':'@r.vodafone.ne.jp',
|
||||
'Vodafone Japan (k)':'@k.vodafone.ne.jp',
|
||||
'Vodafone Japan (t)':'@t.vodafone.ne.jp',
|
||||
'Vodafone Japan (q)':'@q.vodafone.ne.jp',
|
||||
'Vodafone Japan (s)':'@s.vodafone.ne.jp',
|
||||
'Vodafone Japan (h)':'@h.vodafone.ne.jp',
|
||||
'Vodafone Japan (c)':'@c.vodafone.ne.jp',
|
||||
'Vodafone Spain':'@vodafone.es',
|
||||
'Vodafone UK':'@vodafone.net',
|
||||
'Weblink Wireless':'@airmessage.net',
|
||||
'WellCom':'@sms.welcome2well.com',
|
||||
'WyndTell':'@wyndtell.com',
|
||||
}
|
||||
|
||||
def sms_email(number,provider):
|
||||
"""
|
||||
>>> print sms_email('1 (312) 375-6536','T-Mobile USA (tmail)')
|
||||
print 13123756536@tmail.com
|
||||
"""
|
||||
import re
|
||||
if number[0]=='+1': number=number[1:]
|
||||
elif number[0]=='+': number=number[3:]
|
||||
elif number[:2]=='00': number=number[3:]
|
||||
number=re.sub('[^\d]','',number)
|
||||
return number+SMSCODES[provider]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user