from simplegeneric import generic |
from simplegeneric import generic |
from peak.util.decorators import struct |
from peak.util.decorators import struct |
|
|
try: |
try: |
import xml.etree.cElementTree as ET |
import xml.etree.cElementTree as ET |
except ImportError: |
except ImportError: |
import elementtree.ElementTree as ET |
import elementtree.ElementTree as ET |
|
|
__all__ = [ |
__all__ = [ |
'DocinfoConflict', |
'DocInfo', 'DocInfoConflict', 'Layout', 'OutputFile', 'Insurance', |
|
'DateAdvance', 'Today', 'Tomorrow', 'WeekendDelivery', 'HolidayDelivery', |
|
'NoPostage', 'Domestic', 'International', 'Shipment', 'Postcard', |
|
'Envelope', 'Flat', 'RectangularParcel', 'NonRectangularParcel', |
|
'FlatRateEnvelope', 'FlatRateBox', 'ToAddress', 'ReturnAddress', |
|
'RubberStamp', 'Print', 'Verify', |
|
# ...and many more symbols added dynamically! |
] |
] |
|
|
class DocinfoConflict(ValueError): |
class DocInfoConflict(ValueError): |
"""Attempt to set conflicting options""" |
"""Attempt to set conflicting options""" |
|
|
@generic |
@generic |
yield ob |
yield ob |
|
|
@generic |
@generic |
def docinfo_to_etree(ob, etree, isdefault): |
def add_to_package(ob, package, isdefault): |
"""Update `etree` to apply document info""" |
"""Update `etree` to apply document info""" |
for ob in docinfo_iterable(ob): |
for ob in iter_docinfo(ob): |
docinfo_to_etree(ob, etree, isdefault) |
add_to_package(ob, package, isdefault) |
|
|
@struct() |
|
def _DocInfo(tag, value, attr=None): |
|
"""Object representing DAZzle XML text or attributes""" |
|
return tag, value, attr |
|
|
|
|
class Package: |
|
"""The XML for a single package/label""" |
|
finished = False |
|
|
@docinfo_to_etree.when_type(_DocInfo) |
def __init__(self, batch): |
def _di_to_etree(ob, etree, isdefault): |
parent = batch.etree |
t, tag, value, attr = ob |
self.element = ET.SubElement(parent, 'Package', ID=str(len(parent)+1)) |
|
self.parent = parent |
|
self.queue = [] |
|
|
|
def __getitem__(self, (tag, attr)): |
if tag=='DAZzle': |
if tag=='DAZzle': |
el = etree |
el = self.parent |
else: |
else: |
el = etree[-1].find(tag) |
el = self.element.find(tag) |
if el is None: |
if el is not None: |
el = ET.SubElement(etree[-1], tag) |
if attr: |
|
return el.attrib.get(attr) |
|
return el.text |
|
|
|
def __setitem__(self, (tag, attr), value): |
|
if tag=='DAZzle': |
|
el = self.parent |
|
else: |
|
el = self.element.find(tag) |
|
if el is None: |
|
el = ET.SubElement(self.element, tag) |
if attr: |
if attr: |
old = el.attrib.get(attr) |
el.attrib[attr] = unicode(value) |
set = el.attrib.__setitem__ |
|
else: |
else: |
old = el.text |
el.text = unicode(value) |
set = lambda a, v: setattr(el, 'text', v) |
|
|
def should_queue(self, data): |
|
if self.finished: return False |
|
self.queue.append(data) |
|
return True |
|
|
|
def finish(self): |
|
self.finished = True |
|
for item in self.queue: add_to_package(item, self, False) |
|
|
|
class Batch: |
|
"""An XML document and its corresponding package objects""" |
|
|
|
def __init__(self, *rules): |
|
self.etree = ET.Element('DAZzle') |
|
self.packages = [] |
|
self.rules = rules |
|
|
|
def tostring(self, *args): |
|
return ET.tostring(self.etree, *args) |
|
|
|
def ship(self, *packageinfo): |
|
"""Add `package` to batch, with error recovery""" |
|
etree = self.etree |
|
before = etree.attrib.copy() |
|
self.packages.append(packageinfo) |
|
package = Package(self) |
|
try: |
|
add_to_package((packageinfo, self.rules), package, False) |
|
package.finish() |
|
except: |
|
del etree[-1], self.packages[-1] |
|
etree.attrib = before |
|
raise |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Shipment: |
|
"""A collection of batches of packages for shipping""" |
|
|
|
def __init__(self, *rules): |
|
self.batches = [] |
|
self.rules = rules |
|
|
|
def ship(self, *packageinfo): |
|
for batch in self.batches: |
|
try: |
|
return batch.ship(*packageinfo) |
|
except DocInfoConflict: |
|
pass |
|
|
|
batch = Batch(*self.rules) |
|
batch.ship(*packageinfo) |
|
|
|
# only add the batch if the above operations were successful... |
|
self.batches.append(batch) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if old is not None and old<>unicode(value): |
|
|
|
|
|
|
|
|
inverses = dict( |
|
TRUE='FALSE', FALSE='TRUE', YES='NO', NO='YES', ON='OFF', OFF='ON' |
|
) |
|
|
|
class DocInfoBase(object): |
|
__slots__ = () |
|
|
|
def __invert__(self): |
|
try: |
|
return DocInfo(self.tag, inverses[self.value], self.attr) |
|
except KeyError: |
|
raise ValueError("%r has no inverse" % (self,)) |
|
|
|
def clone(self, value): |
|
return DocInfo(self.tag, value, self.attr) |
|
|
|
def set(self, package, isdefault=False): |
|
old = package[self.tag, self.attr] |
|
if old is not None and old<>unicode(self.value): |
if isdefault: |
if isdefault: |
return |
return |
name = tag+(attr and '.'+attr or '') |
name = self.tag+(self.attr and '.'+self.attr or '') |
raise DocinfoConflict( |
raise DocInfoConflict( |
"Can't set '%s=%s' when '%s=%s' already set" % (name,value,name,old) |
"Can't set '%s=%s' when '%s=%s' already set" % ( |
) |
name, self.value, name, old |
set(attr, value) |
) |
|
) |
|
if self.value is not None: |
def add_packages(etree, packages, *defaults): |
package[self.tag, self.attr] = self.value |
for p in packages: |
|
ET.SubElement(etree, 'Package', ID=str(len(etree)+1)) |
|
docinfo_to_etree(p, etree, False) |
@struct(DocInfoBase) |
docinfo_to_etree(defaults, etree, True) |
def DocInfo(tag, value=None, attr=None): |
|
"""Object representing DAZzle XML text or attributes""" |
def make_tree(packages, *defaults): |
return tag, value, attr |
"""Create an Element subtree for `packages`, using `defaults`""" |
|
etree=ET.Element('DAZzle') |
add_to_package.when_type(DocInfo)(DocInfo.set) |
add_packages(etree, packages, *defaults) |
|
return etree |
|
|
|
|
|
|
def _make_symbols(d, nattr, names, factory=DocInfo, **kw): |
|
for name in names: |
|
kw[nattr] = name |
|
d[name] = factory(**kw) |
|
__all__.append(name) |
|
|
|
def _make_globals(nattr, names, *args, **kw): |
|
_make_symbols(globals(), nattr, names, *args, **kw) |
|
__all__.extend(names) |
|
|
|
_make_globals( |
|
'attr', """ |
|
Prompt AbortOnError Test SkipUnverified AutoClose AutoPrintCustomsForms |
|
""".split(), tag='DAZzle', value='YES' |
|
) |
|
_make_globals( |
|
'attr', """ |
|
RegisteredMail InsuredMail CertifiedMail RestrictedDelivery ReturnReceipt |
|
CertificateOfMailing DeliveryConfirmation SignatureConfirmation COD |
|
""".split(), tag='Services', value='ON' |
|
) |
|
_make_globals( |
|
'tag', """ |
|
ReplyPostage BalloonRate NonMachinable OversizeRate Stealth SignatureWaiver |
|
NoWeekendDelivery NoHolidayDelivery ReturnToSender CustomsCertify |
|
""".split(), value='TRUE' |
|
) |
|
|
|
WeekendDelivery = ~NoWeekendDelivery |
|
HolidayDelivery = ~NoHolidayDelivery |
|
NoPostage = DocInfo('MailClass', 'NONE') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_make_globals( |
|
'tag', """ |
|
ToName ToTitle ToCompany ToCity ToState ToPostalCode ToZIP4 ToCountry |
|
ToCarrierRoute ToReturnCode ToEmail ToPhone EndorsementLine ReferenceID |
|
ToDeliveryPoint CustomsSigner Description |
|
|
|
WeightOz Width Length Depth CostCenter Value |
|
""".split(), lambda tag: DocInfo(tag).clone |
|
) |
|
|
|
def Layout(filename): |
|
"""Return a docinfo specifying the desired layout""" |
|
return DocInfo('DAZzle', os.path.abspath(filename), 'Layout') |
|
|
|
def OutputFile(filename): |
|
"""Return a docinfo specifying the desired layout""" |
|
return DocInfo('DAZzle', os.path.abspath(filename), 'OutputFile') |
|
|
|
|
|
def Insurance(type): |
|
"""Return a docinfo for UPIC or ENDICIA insurance""" |
|
if type not in ('UPIC', 'ENDICIA'): |
|
raise ValueError("Insurance() must be 'UPIC' or 'ENDICIA'") |
|
return DocInfo('Services', type, 'InsuredMail') |
|
|
|
def ToAddress(*lines): |
|
assert len(lines)<=6 |
|
return [DocInfo('ToAddress'+str(n+1), v) for n, v in enumerate(lines)] |
|
|
|
def ReturnAddress(*lines): |
|
assert len(lines)<=6 |
|
return [DocInfo('ReturnAddress'+str(n+1), v) for n, v in enumerate(lines)] |
|
|
|
def RubberStamp(n, text): |
|
assert 1<=n<=50 |
|
return DocInfo('RubberStamp'+str(n), text) |
|
|
|
|
|
|
|
|
|
|
|
class Domestic: |
|
FirstClass = DocInfo('MailClass', 'FIRST') |
|
Priority = DocInfo('MailClass', 'PRIORITY') |
|
ParcelPost = DocInfo('MailClass', 'PARCELPOST') |
|
Media = DocInfo('MailClass', 'MEDIAMAIL') |
|
Library = DocInfo('MailClass', 'LIBRARY') |
|
BPM = DocInfo('MailClass', 'BOUNDPRINTEDMATTER') |
|
Express = DocInfo('MailClass', 'EXPRESS') |
|
PresortedFirstClass = DocInfo('MailClass', 'PRESORTEDFIRST') |
|
PresortedStandard = DocInfo('MailClass', 'PRESORTEDSTANDARD') |
|
|
|
class International: |
|
FirstClass = DocInfo('MailClass', 'INTLFIRST') |
|
Priority = DocInfo('MailClass', 'INTLPRIORITY') |
|
Express = DocInfo('MailClass', 'INTLEXPRESS') |
|
GXG = DocInfo('MailClass', 'INTLGXG') |
|
GXGNoDoc = DocInfo('MailClass', 'INTLGXGNODOC') |
|
|
|
Postcard = DocInfo('PackageType', 'POSTCARD') |
|
Envelope = DocInfo('PackageType', 'ENVELOPE') |
|
Flat = DocInfo('PackageType', 'FLAT') |
|
RectangularParcel = DocInfo('PackageType', 'RECTPARCEL') |
|
NonRectangularParcel = DocInfo('PackageType', 'NONRECTPARCEL') |
|
FlatRateEnvelope = DocInfo('PackageType', 'FLATRATEENVELOPE') |
|
FlatRateBox = DocInfo('PackageType', 'FLATRATEBOX') |
|
|
|
def DateAdvance(days): |
|
"""Return a docinfo for the number of days ahead of time we're mailing""" |
|
if not isinstance(days, int) or not (0<=days<=30): |
|
raise ValueError("DateAdvance() must be an integer from 0-30") |
|
return DocInfo('DateAdvance', str(days)) |
|
|
|
Today = DateAdvance(0) |
|
Tomorrow = DateAdvance(1) |
|
|
|
Print = DocInfo('DAZzle', 'PRINTING', 'Start') |
|
Verify = DocInfo('DAZzle', 'DAZ', 'Start') |
|
|
|
|
|
|
|
|
def additional_tests(): |
def additional_tests(): |
return doctest.DocFileSuite( |
return doctest.DocFileSuite( |
'README.txt', |
'README.txt', |
optionflags=doctest.ELLIPSIS|doctest.REPORT_ONLY_FIRST_FAILURE |
optionflags=doctest.ELLIPSIS|doctest.REPORT_ONLY_FIRST_FAILURE |
|
| doctest.NORMALIZE_WHITESPACE |
) |
) |
|
|
|
|
|
|
|
|
|
|
|
|