[Subversion] / PyDicia / pydicia.py  

View of /PyDicia/pydicia.py

Parent Directory | Revision Log
Revision: 2320 - (download) (as text)
Wed Jul 4 16:25:46 2007 UTC (16 years, 9 months ago) by pje
File size: 9459 byte(s)
Document ``Batch`` and ``Shipment`` objects.  Prettify XML for easy
reading.  Add ``MailClass()``.  Demo how to adapt application objects
for shipping.
import os
from simplegeneric import generic
from peak.util.decorators import struct
try:
    import xml.etree.cElementTree as ET
except ImportError:
    try:
        import cElementTree as ET
    except ImportError:
        import elementtree.ElementTree as ET

__all__ = [
    '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', 'Batch', 'iter_docinfo',
    # ...and many more symbols added dynamically!
]

class DocInfoConflict(ValueError):
    """Attempt to set conflicting options"""

@generic
def iter_docinfo(ob):
    """Yield object(s) providing shipping document info for `ob`"""
    raise NotImplementedError("No docinfo producer registered for", type(ob))

@iter_docinfo.when_type(list)
@iter_docinfo.when_type(tuple)
def docinfo_iterable(ob):
    for ob in ob:
        yield ob

@generic
def add_to_package(ob, package, isdefault):
    """Update `etree` to apply document info"""
    for ob in iter_docinfo(ob):
        add_to_package(ob, package, isdefault)

class Package:
    """The XML for a single package/label"""
    finished = False

    def __init__(self, batch):
        parent = batch.etree
        self.element = nested_element(parent, 'Package', ID=str(len(parent)+1))
        self.parent = parent
        self.queue = []

    def __getitem__(self, (tag, attr)):
        if tag=='DAZzle':
            el = self.parent
        else:
            el = self.element.find(tag)
        if el is not None:
            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 = nested_element(self.element, tag, 2)
        if attr:
            el.attrib[attr] = unicode(value)
        else:
            el.text = unicode(value)

    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 add_package(self, *packageinfo):
        """Add `package` to batch, with error recovery"""
        etree = self.etree
        before = etree.attrib.copy(), etree.text
        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]
            if etree: etree[-1].tail = etree.text[:-4]
            etree.attrib, etree.text = before
            raise

def nested_element(parent, tag, indent=1, **kw):
    """Like ET.SubElement, but with pretty-printing indentation"""
    element = ET.SubElement(parent, tag, **kw)
    parent.text='\n'+'    '*indent
    element.tail = parent.text[:-4]
    if len(parent)>1:
        parent[-2].tail = parent.text
    return element







class Shipment:
    """A collection of batches of packages for shipping"""

    def __init__(self, *rules):
        self.batches = []
        self.rules = rules

    def add_package(self, *packageinfo):
        for batch in self.batches:
            try:
                return batch.add_package(*packageinfo)
            except DocInfoConflict:
                pass                

        batch = Batch(*self.rules)
        batch.add_package(*packageinfo)

        # only add the batch if the above operations were successful...
        self.batches.append(batch)






















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:
                return
            name = self.tag+(self.attr and '.'+self.attr or '')
            raise DocInfoConflict(
                "Can't set '%s=%s' when '%s=%s' already set" % (
                    name, self.value, name, old
                )
            )
        if self.value is not None:
            package[self.tag, self.attr] = self.value


@struct(DocInfoBase)
def DocInfo(tag, value=None, attr=None):
    """Object representing DAZzle XML text or attributes"""
    return tag, value, attr

add_to_package.when_type(DocInfo)(DocInfo.set)




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











_make_globals(
    'tag', """
    ToName ToTitle ToCompany ToCity ToState ToPostalCode ToZIP4 ToCountry
    ToCarrierRoute ToReturnCode ToEmail ToPhone EndorsementLine ReferenceID
    ToDeliveryPoint CustomsSigner Description MailClass

    WeightOz Width Length Depth CostCenter Value
    """.split(), lambda tag: DocInfo(tag).clone
)

NoPostage = MailClass('NONE')

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():
    import doctest
    return doctest.DocFileSuite(
        'README.txt',
        optionflags = doctest.ELLIPSIS |doctest.REPORT_ONLY_FIRST_FAILURE
            | doctest.NORMALIZE_WHITESPACE
    )



































cvs-admin@eby-sarna.com

Powered by ViewCVS 1.0-dev

ViewCVS and CVS Help