Package Shipment and Address Verification with ``PyDicia`` |
Package Shipment and Address Verification with ``PyDicia`` |
========================================================== |
========================================================== |
|
|
|
New in 0.1a3 |
|
* Added ``FlatRateLargeBox`` and ``ExpressMailPremiumService`` tags, for use |
|
with DAZzle 8.0 and up. |
|
|
|
New in 0.1a2 |
|
* Dropped MS Windows requirement; you can now use PyDicia to generate XML for |
|
the Endicia Mac client, or to be sent via download or networked directory |
|
to a Windows client. |
|
|
|
* Added a ``Shipment.run()`` method to allow running multiple batches in |
|
sequence. |
|
|
|
* Fixed the ``Status.ToZip4`` attribute (it was incorrectly spelled |
|
``ToZIP4``, and thus didn't work). |
|
|
PyDicia is a Python interface to endicia.com's postal services client, DAZzle. |
PyDicia is a Python interface to endicia.com's postal services client, DAZzle. |
Using DAZzle's XML interface, PyDicia can be used to print shipping labels, |
Using DAZzle's XML interface, PyDicia can be used to print shipping labels, |
envelopes, postcards and more, with or without prepaid US postage indicia |
envelopes, postcards and more, with or without prepaid US postage indicia |
In addition to providing a layer of syntax sugar for the DAZzle XML interface, |
In addition to providing a layer of syntax sugar for the DAZzle XML interface, |
PyDicia provides a novel adaptive interface that lets you smoothly integrate |
PyDicia provides a novel adaptive interface that lets you smoothly integrate |
its functions with your application's core types (like invoice, customer, or |
its functions with your application's core types (like invoice, customer, or |
packing ticket objects) without subclassing. (This is particularly useful if |
"packing slip" objects) without subclassing. (This is particularly useful if |
you are extending a CRM or other database that was written by somebody else.) |
you are extending a CRM or other database that was written by somebody else.) |
|
|
|
This version of PyDicia is an alpha proof-of-concept release. It is actually |
|
usable -- I've already used it to print about a dozen international shipping |
|
labels to almost as many countries. However, the API is subject to change, |
|
the reference documentation is sketchy, and the developer's guide lacks detail |
|
about some of the more advanced features. This should improve in future |
|
releases, but I just want to get this milestone out to start with. Reading the |
|
DAZzle XML API specification is a good idea if you want to use this; make sure |
|
you get at least the 7.0.x version, as that's required. |
|
|
PyDicia uses the ElementTree, simplegeneric, and DecoratorTools packages, and |
PyDicia uses the ElementTree, simplegeneric, and DecoratorTools packages, and |
requires Python 2.4 or higher. |
requires Python 2.4 or higher (due to use of decorators and the ``Decimal`` |
|
type). Actually printing any labels requires that you have an Endicia |
|
"Premium" or "Mac" account. (Note: I have not used the Mac client, so I don't |
|
know how well it works there. See the section below on `Using PyDicia on |
|
Non-Windows Platforms`_ for more info.) |
|
|
|
IMPORTANT |
|
Please note that PyDicia does not attempt to implement all of the US Postal |
|
Service's business rules for what options may be used in what combinations. |
|
It doesn't even validate most of the DAZzle client's documented |
|
restrictions! So it's strictly a "Garbage In, Garbage Out" kind of deal. |
|
If you put garbage in, who knows what the heck will happen. You might end |
|
up spending lots of money *and* getting your packages returned to you -- |
|
and **I AM NOT RESPONSIBLE**, even if your problem is due to an error in |
|
PyDicia or its documentation! |
|
|
|
So, make sure you understand the shipping options you wish to use, and test |
|
your application thoroughly before using this code in production. You have |
|
been warned! |
|
|
|
Questions, discussion, and bug reports for this software should be directed to |
|
the PEAK mailing list; see http://www.eby-sarna.com/mailman/listinfo/PEAK/ |
|
for details. |
|
|
TODO: |
.. contents:: **Table of Contents** |
|
|
* ``~`` operator for DocInfo, make DocInfo public |
|
|
|
* global defaults |
----------------- |
|
Developer's Guide |
|
----------------- |
|
|
* Cmd-line and queue mode handlers |
|
|
|
* Response parsing and application |
Basic XML Generation |
|
==================== |
|
|
|
PyDicia simplifies the creation of XML for DAZzle by using objects to specify |
|
what data needs to go in the XML. These objects are mostly ``Option`` |
|
instances, or callables that create ``Option`` instances. However, the |
|
framework is extensible, so that you can use your own object types with the |
|
same API. Your object types can either generate ``Option`` instances, or |
|
directly manipulate the XML using ElementTree APIs for maximum control. |
|
|
|
In the simpler cases, however, you will just use lists or tuples of objects |
|
provided by (or created with) the PyDicia API to represent packages or labels. |
|
|
|
|
|
Batch Objects |
|
------------- |
|
|
|
XML documents are represented using ``Batch`` objects:: |
|
|
|
>>> from pydicia import * |
|
>>> b = Batch() |
|
|
|
The ``tostring()`` method of a batch returns its XML in string form, optionally |
|
in a given encoding (defaulting to ASCII if not specified):: |
|
|
|
>>> print b.tostring('latin1') |
|
<?xml version='1.0' encoding='latin1'?> |
|
<DAZzle /> |
|
|
|
To add a package to a batch, you use the ``add_package()`` method:: |
|
|
|
>>> b.add_package(ToName('Phillip Eby')) |
|
>>> print b.tostring() |
|
<DAZzle> |
|
<Package ID="1"> |
|
<ToName>Phillip Eby</ToName> |
|
</Package> |
|
</DAZzle> |
|
|
|
The ``add_package()`` method accepts zero or more objects that can manipulate |
|
PyDicia package objects. It also accepts tuples or lists of such objects, |
|
nested to arbitrary depth:: |
|
|
|
>>> b.add_package([Services.COD, (Stealth, ToName('Ty Sarna'))], FlatRateBox) |
|
|
|
>>> print b.tostring() |
|
<DAZzle> |
|
<Package ID="1"> |
|
<ToName>Phillip Eby</ToName> |
|
</Package> |
|
<Package ID="2"> |
|
<Services COD="ON" /> |
|
<Stealth>TRUE</Stealth> |
|
<ToName>Ty Sarna</ToName> |
|
<PackageType>FLATRATEBOX</PackageType> |
|
</Package> |
|
</DAZzle> |
|
|
|
And the ``packages`` attribute of a batch keeps track of the arguments that |
|
have been passed to ``add_package()``:: |
|
|
|
>>> b.packages |
|
[Package(ToName('Phillip Eby'),), |
|
Package([Services.COD('ON'), (Stealth('TRUE'), ToName('Ty Sarna'))], |
|
PackageType('FLATRATEBOX'))] |
|
|
|
Each package object in the list wraps a tuple of the arguments that were |
|
supplied for each invocation of ``add_package()``. This allows the system |
|
to send status updates (including delivery confirmation numbers, customs IDs, |
|
etc.) back to the application. |
|
|
|
But before we can process status updates, we need to have some application |
|
objects, as described in the next section. |
|
|
|
|
|
Using Your Application Objects as Package Sources |
|
------------------------------------------------- |
|
|
|
In addition to PyDicia-defined objects and sequences thereof, the |
|
``add_package()`` method also accepts any custom objects of your own design |
|
that have been registered with the ``pydicia.add_to_package()`` or |
|
``pydicia.iter_options()`` generic functions:: |
|
|
|
>>> class Customer: |
|
... def __init__(self, **kw): |
|
... self.__dict__ = kw |
|
|
|
>>> @iter_options.when_type(Customer) |
|
... def cust_options(ob): |
|
... yield ToName(ob.name) |
|
... yield ToAddress(ob.address) |
|
... yield ToCity(ob.city) |
|
... yield ToState(ob.state) |
|
... yield ToPostalCode(ob.zip) |
|
|
|
>>> b = Batch() |
|
>>> c = Customer( |
|
... name='PJE', address='123 Nowhere Dr', state='FL', city='Nowhere', |
|
... zip='12345-6789' |
|
... ) |
|
>>> b.add_package(c) |
|
>>> print b.tostring() |
|
<DAZzle> |
|
<Package ID="1"> |
|
<ToName>PJE</ToName> |
|
<ToAddress1>123 Nowhere Dr</ToAddress1> |
|
<ToCity>Nowhere</ToCity> |
|
<ToState>FL</ToState> |
|
<ToPostalCode>12345-6789</ToPostalCode> |
|
</Package> |
|
</DAZzle> |
|
|
|
This allows you to pass customer, package, product, invoice, or other |
|
application-specific objects into ``add_package()``. And the objects yielded |
|
by your ``iter_options`` implementation can also be application objects, e.g.:: |
|
|
|
>>> class Invoice: |
|
... def __init__(self, **kw): |
|
... self.__dict__ = kw |
|
|
|
>>> @iter_options.when_type(Invoice) |
|
... def invoice_options(ob): |
|
... yield ob.shippingtype |
|
... yield ob.products |
|
... yield ob.customer |
|
|
|
>>> b = Batch() |
|
>>> i = Invoice( |
|
... shippingtype=(Tomorrow, MailClass('MEDIAMAIL')), |
|
... products=[WeightOz(27),], customer=c |
|
... ) |
|
>>> b.add_package(i) |
|
>>> print b.tostring() |
|
<DAZzle> |
|
<Package ID="1"> |
|
<DateAdvance>1</DateAdvance> |
|
<MailClass>MEDIAMAIL</MailClass> |
|
<WeightOz>27</WeightOz> |
|
<ToName>PJE</ToName> |
|
<ToAddress1>123 Nowhere Dr</ToAddress1> |
|
<ToCity>Nowhere</ToCity> |
|
<ToState>FL</ToState> |
|
<ToPostalCode>12345-6789</ToPostalCode> |
|
</Package> |
|
</DAZzle> |
|
|
|
Note that there is no particular significance to my choice of lists vs. tuples |
|
in these examples; they're more to demonstrate that you can use arbitrary |
|
structures, as long as they contain objects that are supported by either |
|
``iter_options()`` or ``add_to_package()``. Normally, you will simply use |
|
collections of either PyDicia-provided symbols, or application objects for |
|
which you've defined an ``iter_options()`` method. |
|
|
|
You will also usually want to implement your PyDicia support in a module by |
|
itself, so you can use ``from pydicia import *`` without worrying about symbol |
|
collisions. |
|
|
|
|
|
Batch-wide Options |
|
------------------ |
|
|
|
When you create a batch, you can pass in any number of objects, to specify |
|
options that will be applied to every package. For example, this batch will |
|
have every package set to be mailed tomorrow as media mail:: |
|
|
|
>>> b = Batch( Tomorrow, MailClass('MEDIAMAIL') ) |
|
>>> b.add_package(ToName('PJE')) |
|
>>> print b.tostring() |
|
<DAZzle> |
|
<Package ID="1"> |
|
<ToName>PJE</ToName> |
|
<DateAdvance>1</DateAdvance> |
|
<MailClass>MEDIAMAIL</MailClass> |
|
</Package> |
|
</DAZzle> |
|
|
----------------- |
|
Developer's Guide |
|
----------------- |
|
|
|
Basic Use |
Multi-Batch Shipments |
========= |
===================== |
|
|
|
Certain DAZzle options can only be set once per XML file, such as the choice of |
|
layout file. If you are shipping multiple packages with different label |
|
layouts (such as domestic vs. international mail), you need to separate these |
|
packages into different batches, each of which will be in a separate XML file. |
|
The ``Shipment`` class handles this separation for you automatically. |
|
|
|
When you create a shipment, it initially has no batches:: |
|
|
|
>>> s = Shipment() |
|
>>> s.batches |
|
[] |
|
|
|
|
|
But as you add packages to it, it will create batches as needed:: |
|
|
|
>>> s.add_package(ToName('Phillip Eby'), DAZzle.Test) |
|
>>> len(s.batches) |
|
1 |
|
|
|
>>> print s.batches[0].tostring() |
|
<DAZzle Test="YES"> |
|
<Package ID="1"> |
|
<ToName>Phillip Eby</ToName> |
|
</Package> |
|
</DAZzle> |
|
|
|
As long as you're adding packages with the same or compatible options, the |
|
same batch will be reused:: |
|
|
|
>>> s.add_package(ToName('Ty Sarna'), DAZzle.Test) |
|
>>> len(s.batches) |
|
1 |
|
>>> print s.batches[0].tostring() |
|
<DAZzle Test="YES"> |
|
<Package ID="1"> |
|
<ToName>Phillip Eby</ToName> |
|
</Package> |
|
<Package ID="2"> |
|
<ToName>Ty Sarna</ToName> |
|
</Package> |
|
</DAZzle> |
|
|
|
But as soon as you add a package with any incompatible options, a new batch |
|
will be created and used:: |
|
|
|
>>> s.add_package(ToName('PJE'), ~DAZzle.Test) |
|
>>> len(s.batches) |
|
2 |
|
|
|
>>> print s.batches[1].tostring() |
|
<DAZzle Test="NO"> |
|
<Package ID="1"> |
|
<ToName>PJE</ToName> |
|
</Package> |
|
</DAZzle> |
|
|
|
And each time you add a package, it's added to the first compatible batch:: |
|
|
|
>>> s.add_package(ToName('Some Body'), ~DAZzle.Test) |
|
>>> len(s.batches) |
|
2 |
|
|
|
>>> print s.batches[1].tostring() |
|
<DAZzle Test="NO"> |
|
<Package ID="1"> |
|
<ToName>PJE</ToName> |
|
</Package> |
|
<Package ID="2"> |
|
<ToName>Some Body</ToName> |
|
</Package> |
|
</DAZzle> |
|
|
|
>>> s.add_package(ToName('No Body'), DAZzle.Test) |
|
>>> len(s.batches) |
|
2 |
|
|
|
>>> print s.batches[0].tostring() |
|
<DAZzle Test="YES"> |
|
<Package ID="1"> |
|
<ToName>Phillip Eby</ToName> |
|
</Package> |
|
<Package ID="2"> |
|
<ToName>Ty Sarna</ToName> |
|
</Package> |
|
<Package ID="3"> |
|
<ToName>No Body</ToName> |
|
</Package> |
|
</DAZzle> |
|
|
|
By the way, as with batches, you can create a shipment with options that will |
|
be applied to all packages:: |
|
|
|
>>> s = Shipment(Tomorrow, Services.COD) |
|
>>> s.add_package(ToName('Some Body'), DAZzle.Test) |
|
>>> s.add_package(ToName('No Body'), ~DAZzle.Test) |
|
>>> len(s.batches) |
|
2 |
|
>>> print s.batches[0].tostring() |
|
<DAZzle Test="YES"> |
|
<Package ID="1"> |
|
<ToName>Some Body</ToName> |
|
<DateAdvance>1</DateAdvance> |
|
<Services COD="ON" /> |
|
</Package> |
|
</DAZzle> |
|
|
|
>>> print s.batches[1].tostring() |
|
<DAZzle Test="NO"> |
|
<Package ID="1"> |
|
<ToName>No Body</ToName> |
|
<DateAdvance>1</DateAdvance> |
|
<Services COD="ON" /> |
|
</Package> |
|
</DAZzle> |
|
|
|
|
|
|
|
Receiving Status Updates |
|
======================== |
|
|
|
When DAZzle completes a batch, it creates an output file containing status |
|
information for each package in the batch. If you'd like to process this |
|
status information for the corresponding application objects you passed in |
|
to ``add_package()``, you can extend the ``report_status()`` generic function |
|
to do this:: |
|
|
|
>>> @report_status.when_type(Customer) |
|
... def customer_status(ob, status): |
|
... print ob |
|
... print status |
|
|
|
>>> b = Batch() |
|
>>> b.add_package(c) |
|
|
|
When the batch receives status information, it will invoke ``report_status()`` |
|
on each package's application items, with a status object for the corresponding |
|
package:: |
|
|
|
>>> b.report_statuses() |
|
<...Customer instance...> |
|
ToAddress : [u'123 Nowhere Dr'] |
|
ToCity : u'Nowhere' |
|
ToState : u'FL' |
|
ToPostalCode : u'12345-6789' |
|
ToAddress1 : u'123 Nowhere Dr' |
|
|
|
Note that you don't normally need to call ``report_statuses()`` directly; it's |
|
usually done for you as part of the process of running a batch or shipment. |
|
(See the section below on `Invoking DAZzle`_.) |
|
|
|
The `status` object passed to your method will be a ``Status`` instance with |
|
attributes similar to those above, containing USPS-normalized address data. |
|
In addition, several other fields are possible:: |
|
|
|
>>> from pydicia import ET |
|
>>> b.etree = ET.fromstring(''' |
|
... <DAZzle><Package ID="1"> |
|
... <ToZip4>1234</ToZip4> |
|
... <Status>Rejected (-3)</Status> |
|
... <PIC>123465874359</PIC> |
|
... <FinalPostage>4.60</FinalPostage> |
|
... <TransactionDateTime>20070704173221</TransactionDateTime> |
|
... <PostmarkDate>20070705</PostmarkDate> |
|
... </Package></DAZzle>''') |
|
|
|
>>> b.report_statuses() |
|
<...Customer instance...> |
|
Status : 'Rejected (-3)' |
|
ErrorCode : -3 |
|
ToAddress : [] |
|
ToZip4 : '1234' |
|
PIC : '123465874359' |
|
FinalPostage : Decimal("4.60") |
|
TransactionDateTime : datetime.datetime(2007, 7, 4, 17, 32, 21) |
|
PostmarkDate : datetime.date(2007, 7, 5) |
|
|
|
The ``Status`` object should support all output fields supported by DAZzle; see |
|
the DAZzle documentation for details. The non-string fields shown above are |
|
the only ones which are postprocessed to specialized Python objects; the rest |
|
are kept as strings or Unicode values. The ``ErrorCode`` field is computed by |
|
extracting the integer portion of any rejection code. It is ``None`` in the |
|
case of a successful live print, and ``0`` in the case of a successful test |
|
print. See the DAZzle XML interface documentation for a description of other |
|
error codes. |
|
|
|
Note that for a more compact presentation, attributes with ``None`` values are |
|
not included in the ``str()`` of a ``Status`` object, which is why the statuses |
|
displayed above show different sets of fields. The attributes, however, always |
|
exist; they simply have ``None`` as their value. |
|
|
|
|
Application Integration |
Invoking DAZzle |
======================= |
=============== |
|
|
DocInfo yielding, Status handling, Address updating, ... |
In the simplest case, invoking a batch or shipment object's ``.run()`` method |
|
will launch a local copy of DAZzle on a temporary file containing the batch's |
|
XML, wait for DAZzle to exit, then process status updates from the output file |
|
and return DAZzle's return code. (Or a list of return codes, in the case of a |
|
``Shipment``.) |
|
|
|
If you are using this approach, you may wish to include ``~DAZzle.Prompt`` |
|
(which keeps end-user prompts to a minimum) and ``DAZzle.AutoClose`` (so that |
|
DAZzle exits upon completion of the batch) in your batch options. |
|
|
|
If you do not have a local copy of DAZzle, but instead are using a network |
|
queue directory to send jobs remotely, you can instead use the batch object's |
|
``.write(queuedir)`` method to send the batch to the queue. You can also |
|
use this approach to send jobs to a local copy of DAZzle running in the |
|
background. |
|
|
|
If a copy of DAZzle is installed locally, you can get its XML queue directory |
|
from ``DAZzle.XMLDirectory``, and check whether it is monitoring for files |
|
using ``DAZzle.get_preference("MonitorXML")``. (These values will be ``None`` |
|
if DAZzle is not installed.) |
|
|
|
If DAZzle is installed locally, you can launch it with the |
|
``DAZzle.run(args=(), sync=True)`` function. The `args` are a list of command |
|
line arguments to pass, and `sync` is a flag indicating whether to wait for |
|
DAZzle to exit. If `sync` is a false value, ``run()`` returns a |
|
``subprocess.Popen`` instance. Otherwise, it returns the process's exit code |
|
(ala ``subprocess.call``). |
|
|
|
XXX async batch status retrieval |
|
|
|
XXX DAZzle.exe_path, DAZzle.get_preference(), DAZzle.LayoutDirectory |
|
|
|
XXX Launching for multi-batch, remote, queued, and other async processing |
|
|
|
|
|
Using PyDicia on Non-Windows Platforms |
|
====================================== |
|
|
|
When used on a non-Windows platform, PyDicia cannot detect any DAZzle |
|
configuration information, so you must manually set ``DAZzle.exe_path`` to |
|
the client program, if you wish to use any of the ``run()`` methods. |
|
(Likewise, you must manually set ``DAZzle.LayoutDirectory`` if you want layout |
|
paths to be automatically adjusted.) |
|
|
|
On the Mac, the ``exe_path`` should be set to a program that takes a single |
|
XML filename as an argument. The Mac ``endiciatool`` program probably will |
|
not work on its own, without a wrapper shell script of some kind; I'm open to |
|
suggestions as to how to improve this. (Note, by the way, that the Mac client |
|
doesn't support all of the options that the Windows client does, so remember |
|
that use of PyDicia is entirely at your own risk, whatever the platform!) |
|
|
|
On other platforms, the main usefulness of PyDicia would be in generating XML |
|
for users to download (e.g. from a web application) or submitting and |
|
processing jobs via a Samba-mounted queue directory. You don't need an |
|
``exe_path`` for this, but you will need to generate your own layout and output |
|
file paths using ``Option`` objects, to avoid them being mangled by PyDicia's |
|
platform-specific path munging. |
|
|
|
XXX explain how to do that, or make it work anyway |
|
|
|
|
Advanced Customization |
Advanced Customization |
====================== |
====================== |
|
|
Using DocInfo elements |
XXX Using Option elements, add_to_package() |
|
|
|
|
----------------- |
----------------- |
Basic Package Options |
Basic Package Options |
===================== |
===================== |
|
|
MailClass(text), NoPostage |
XXX MailClass(text), NoPostage |
|
DateAdvance(), Today, Tomorrow |
|
Value() |
|
Description() |
|
WeightOz() |
|
|
DateAdvance |
|
WeightOz |
|
Value |
|
Description |
|
|
|
Addresses |
Addresses |
========= |
========= |
|
|
ToName(text), ToTitle(text), ToCompany(text) |
:: |
ToAddress(*lines) |
>>> ToName("Phillip J. Eby") |
ToCity(text), ToState(text), ToPostalCode(text), ToZIP4(text), ToCountry(text) |
ToName('Phillip J. Eby') |
|
|
ReturnAddress(*lines) |
>>> ToTitle("President") |
|
ToTitle('President') |
|
|
|
>>> ToCompany("Dirt Simple, Inc.") |
|
ToCompany('Dirt Simple, Inc.') |
|
|
|
|
|
XXX ToAddress(\*lines) |
|
ToCity(text), ToState(text), ToPostalCode(text), ToZIP4(text), ToCountry(text) |
|
|
|
XXX ReturnAddress(\*lines) |
ToDeliveryPoint(text) |
ToDeliveryPoint(text) |
EndorsementLine(text) |
EndorsementLine(text) |
ToCarrierRoute(text) |
ToCarrierRoute(text) |
ToReturnCode(text) |
|
|
|
|
|
Service Options |
Package Details |
=============== |
=============== |
|
|
DomesticFlatRateEnvelope |
XXX PackageType() |
DomesticFlatRateBox |
FlatRateEnvelope |
|
FlatRateBox |
|
RectangularParcel |
|
NonRectangularParcel |
|
Postcard |
|
Flat |
|
Envelope |
|
Width(), Length(), Depth() |
|
NonMachinable |
|
BalloonRate |
|
|
ReplyPostage |
|
|
Service Options |
|
=============== |
|
|
|
XXX ReplyPostage |
Stealth |
Stealth |
Oversize |
|
SignatureWaiver |
SignatureWaiver |
NoWeekendDelivery |
NoWeekendDelivery |
NoHolidayDelivery |
NoHolidayDelivery |
ReturnToSender |
ReturnToSender |
|
|
RegisteredMail |
|
Insurance.USPS |
Insurance.USPS |
Insurance.Endicia |
Insurance.Endicia |
Insurance.UPIC |
Insurance.UPIC |
CertifiedMail |
Insurance.NONE |
RestrictedDelivery |
Services.RegisteredMail |
CertificateOfMailing |
Services.CertifiedMail |
ReturnReceipt |
Services.RestrictedDelivery |
DeliveryConfirmation |
Services.CertificateOfMailing |
SignatureConfirmation |
Services.ReturnReceipt |
COD |
Services.DeliveryConfirmation |
|
Services.SignatureConfirmation |
|
Services.COD |
|
Services.InsuredMail() |
|
|
|
|
Customs Forms |
Customs Forms |
============= |
============= |
|
|
Sample |
When processing international shipments, you will usually need to specify a |
Gift |
customs form, contents type, and items. Additionally, if you want to print |
Documents |
the customs forms already "signed", you can specify a signer and the |
Other |
certification option. |
Merchandise |
|
|
Contents Types |
Customs.GEM(ctype, *items) |
-------------- |
Customs.CN22(ctype, *items) |
|
Customs.CP72(ctype, *items) |
The ``ContentsType`` constructor defines the type of contents declared on the |
|
customs form. There are six predefined constants for the standard contents |
|
types:: |
|
|
|
>>> Customs.Sample |
|
ContentsType('SAMPLE') |
|
|
|
>>> Customs.Gift |
|
ContentsType('GIFT') |
|
|
|
>>> Customs.Documents |
|
ContentsType('DOCUMENTS') |
|
|
|
>>> Customs.Other |
|
ContentsType('OTHER') |
|
|
|
>>> Customs.Merchandise |
|
ContentsType('MERCHANDISE') |
|
|
|
>>> Customs.ReturnedGoods |
|
ContentsType('RETURNEDGOODS') |
|
|
|
|
|
Customs Form Types |
|
------------------ |
|
|
|
The ``CustomsFormType`` constructor defines the type of customs form to be |
|
used. There are four predefined constants for the allowed form types:: |
|
|
|
>>> Customs.GEM |
|
CustomsFormType('GEM') |
|
|
|
>>> Customs.CN22 |
|
CustomsFormType('CN22') |
|
|
|
>>> Customs.CP72 |
|
CustomsFormType('CP72') |
|
|
|
>>> Customs.NONE |
|
CustomsFormType('NONE') |
|
|
Customs(formtype, ctype, *items) |
|
|
|
Item(desc, weight, value, qty=1, origin='United States') |
Customs Items |
|
------------- |
|
|
|
Items to be declared on a customs form are created using ``Customs.Item``. |
|
The minimum required arguments are a description, a unit weight in ounces |
|
(which must be an integer or decimal), and a value in US dollars (also an |
|
integer or decimal):: |
|
|
|
>>> from decimal import Decimal |
|
>>> i = Customs.Item("Paperback book", 12, Decimal('29.95')) |
|
|
|
You may also optionally specify a quantity (which must be an integer) and a |
|
country of origin. The defaults for these are ``1`` and ``"United States"``, |
|
respectively:: |
|
|
|
>>> i |
|
Item('Paperback book', Decimal("12"), Decimal("29.95"), 1, 'United States') |
|
|
|
You always specify a unit weight and value; these are automatically multiplied |
|
by the quantity on the customs form, and for purposes of calculating total |
|
weight/value. |
|
|
|
Note that a package's total weight must be greater than or equal to the sum of |
|
its items' weight, and its value must exactly equal the sum of its items' |
|
values:: |
|
|
|
>>> b = Batch() |
|
>>> b.add_package(i) |
|
Traceback (most recent call last): |
|
... |
|
OptionConflict: Total package weight must be specified when Customs.Items |
|
are used |
|
|
|
>>> b.add_package(i, WeightOz(1)) |
|
Traceback (most recent call last): |
|
... |
|
OptionConflict: Total item weight is 12 oz, but |
|
total package weight is only 1 oz |
|
|
|
>>> b.add_package(i, WeightOz(12), Value(69)) |
|
Traceback (most recent call last): |
|
... |
|
OptionConflict: Can't set 'Value=29.95' when 'Value=69' already set |
|
|
|
And a form type and contents type must be specified if you include any items:: |
|
|
|
>>> b.add_package(i, WeightOz(12)) |
|
Traceback (most recent call last): |
|
... |
|
OptionConflict: Customs form + content type must be specified with items |
|
|
|
>>> b.add_package(i, WeightOz(12), Customs.Gift) |
|
Traceback (most recent call last): |
|
... |
|
OptionConflict: Customs form + content type must be specified with items |
|
|
|
>>> b.add_package(i, WeightOz(12), Customs.CN22) |
|
Traceback (most recent call last): |
|
... |
|
OptionConflict: Customs form + content type must be specified with items |
|
|
|
>>> b.add_package(i, WeightOz(12), Customs.Gift, Customs.CN22) |
|
>>> print b.tostring() |
|
<DAZzle> |
|
<Package ID="1"> |
|
<CustomsQuantity1>1</CustomsQuantity1> |
|
<CustomsCountry1>United States</CustomsCountry1> |
|
<CustomsDescription1>Paperback book</CustomsDescription1> |
|
<CustomsWeight1>12</CustomsWeight1> |
|
<CustomsValue1>29.95</CustomsValue1> |
|
<WeightOz>12</WeightOz> |
|
<ContentsType>GIFT</ContentsType> |
|
<CustomsFormType>CN22</CustomsFormType> |
|
<Value>29.95</Value> |
|
</Package> |
|
</DAZzle> |
|
|
|
The final customs form will include the multiplied-out weights and values based |
|
on the quantity of each item:: |
|
|
|
>>> b = Batch() |
|
>>> b.add_package( |
|
... Customs.Item('x',23,42,3), Customs.Item('y',1,7), |
|
... WeightOz(99), Customs.Gift, Customs.CN22 |
|
... ) |
|
>>> print b.tostring() |
|
<DAZzle> |
|
<Package ID="1"> |
|
<CustomsQuantity1>3</CustomsQuantity1> |
|
<CustomsCountry1>United States</CustomsCountry1> |
|
<CustomsDescription1>x</CustomsDescription1> |
|
<CustomsWeight1>69</CustomsWeight1> |
|
<CustomsValue1>126</CustomsValue1> |
|
<CustomsQuantity2>1</CustomsQuantity2> |
|
<CustomsCountry2>United States</CustomsCountry2> |
|
<CustomsDescription2>y</CustomsDescription2> |
|
<CustomsWeight2>1</CustomsWeight2> |
|
<CustomsValue2>7</CustomsValue2> |
|
<WeightOz>99</WeightOz> |
|
<ContentsType>GIFT</ContentsType> |
|
<CustomsFormType>CN22</CustomsFormType> |
|
<Value>133</Value> |
|
</Package> |
|
</DAZzle> |
|
|
|
|
|
Customs Signature |
|
----------------- |
|
|
|
You can specify the person who's certifying the customs form using these |
|
options:: |
|
|
|
>>> Customs.Signer("Phillip Eby") |
|
CustomsSigner('Phillip Eby') |
|
|
|
>>> Customs.Certify |
|
CustomsCertify('TRUE') |
|
|
|
|
|
|
Processing Options |
Processing Options |
================== |
================== |
|
|
Test |
XXX DAZzle.Test |
Layout(filename) |
DAZzle.Layout(filename) |
Print |
DAZzle.OutputFile(filename) |
Verify |
DAZzle.Print |
SkipUnverified |
DAZzle.Verify |
AutoClose |
DAZzle.SkipUnverified |
Prompt |
DAZzle.AutoClose |
AbortOnError |
DAZzle.Prompt |
AutoPrintCustomsForms |
DAZzle.AbortOnError |
|
DAZzle.AutoPrintCustomsForms |
|
DAZzle.XMLDirectory |
|
DAZzle.LayoutDirectory |
|
DAZzle.exe_path |
|
|
|
|
Miscellaneous |
Miscellaneous |
============= |
============= |
|
|
RubberStamp(n, text) |
XXX RubberStamp(n, text) |
ReferenceID(text) |
ReferenceID(text) |
CostCenter(int) |
CostCenter(int) |
|
|
Internals and Tests |
Internals and Tests |
------------------- |
------------------- |
|
|
DocInfo applications:: |
Misc imports for tests:: |
|
|
|
>>> from pydicia import add_to_package, ET, Option, Batch, Package |
|
|
>>> from pydicia import docinfo_to_etree, ET, _DocInfo, make_tree |
Packages:: |
|
|
>>> root = ET.Element('DAZzle') |
>>> b = Batch() |
>>> pkg = ET.SubElement(root, 'Package', ID='1') |
>>> p = Package(b) |
|
|
>>> print ET.tostring(root) |
>>> print b.tostring() |
<DAZzle><Package ID="1" /></DAZzle> |
<DAZzle> |
|
<Package ID="1" /> |
|
</DAZzle> |
|
|
>>> Box = _DocInfo('FlatRate', 'BOX') |
>>> Box = Option('FlatRate', 'BOX') |
>>> docinfo_to_etree(Box, root, False) |
>>> add_to_package(Box, p, False) |
|
|
>>> print ET.tostring(root) |
>>> print b.tostring() |
<DAZzle><Package ID="1"><FlatRate>BOX</FlatRate></Package></DAZzle> |
<DAZzle> |
|
<Package ID="1"> |
|
<FlatRate>BOX</FlatRate> |
|
</Package> |
|
</DAZzle> |
|
|
>>> Envelope = _DocInfo('FlatRate', 'TRUE') |
>>> Envelope = Option('FlatRate', 'TRUE') |
>>> docinfo_to_etree(Envelope, root, False) |
>>> add_to_package(Envelope, p, False) |
Traceback (most recent call last): |
Traceback (most recent call last): |
... |
... |
DocinfoConflict: Can't set 'FlatRate=TRUE' when 'FlatRate=BOX' already set |
OptionConflict: Can't set 'FlatRate=TRUE' when 'FlatRate=BOX' already set |
|
|
>>> print ET.tostring(root) |
|
<DAZzle><Package ID="1"><FlatRate>BOX</FlatRate></Package></DAZzle> |
|
|
|
>>> docinfo_to_etree(Box, root, False) |
|
>>> print ET.tostring(root) |
|
<DAZzle><Package ID="1"><FlatRate>BOX</FlatRate></Package></DAZzle> |
|
|
|
>>> docinfo_to_etree(Envelope, root, True) |
>>> print b.tostring() |
>>> print ET.tostring(root) |
<DAZzle> |
<DAZzle><Package ID="1"><FlatRate>BOX</FlatRate></Package></DAZzle> |
<Package ID="1"> |
|
<FlatRate>BOX</FlatRate> |
|
</Package> |
|
</DAZzle> |
|
|
|
>>> add_to_package(Box, p, False) |
|
>>> print b.tostring() |
|
<DAZzle> |
|
<Package ID="1"> |
|
<FlatRate>BOX</FlatRate> |
|
</Package> |
|
</DAZzle> |
|
|
|
>>> add_to_package(Envelope, p, True) |
|
>>> print b.tostring() |
|
<DAZzle> |
|
<Package ID="1"> |
|
<FlatRate>BOX</FlatRate> |
|
</Package> |
|
</DAZzle> |
|
|
|
>>> del p.element[-1]; p.element.text='' |
|
>>> print b.tostring() |
|
<DAZzle> |
|
<Package ID="1" /> |
|
</DAZzle> |
|
|
|
>>> verify_zip = Option('DAZzle', 'DAZ', 'Start') |
|
|
|
>>> add_to_package(verify_zip, p, False) |
|
>>> print b.tostring() |
|
<DAZzle Start="DAZ"> |
|
<Package ID="1" /> |
|
</DAZzle> |
|
|
>>> del pkg[-1] |
>>> add_to_package(Option('DAZzle', 'PRINTING', 'Start'), p, False) |
>>> print ET.tostring(root) |
Traceback (most recent call last): |
<DAZzle><Package ID="1" /></DAZzle> |
... |
|
OptionConflict: Can't set 'DAZzle.Start=PRINTING' when 'DAZzle.Start=DAZ' already set |
>>> verify_zip = _DocInfo('DAZzle', 'DAZ', 'Start') |
|
|
|
>>> docinfo_to_etree(verify_zip, root, False) |
>>> b = Batch() |
>>> print ET.tostring(root) |
>>> p = Package(b) |
<DAZzle Start="DAZ"><Package ID="1" /></DAZzle> |
>>> add_to_package([verify_zip, Envelope], p, False) |
|
>>> print b.tostring() |
|
<DAZzle Start="DAZ"> |
|
<Package ID="1"> |
|
<FlatRate>TRUE</FlatRate> |
|
</Package> |
|
</DAZzle> |
|
|
|
>>> p.should_queue(Services.COD) |
|
True |
|
>>> print b.tostring() |
|
<DAZzle Start="DAZ"> |
|
<Package ID="1"> |
|
<FlatRate>TRUE</FlatRate> |
|
</Package> |
|
</DAZzle> |
|
|
|
>>> p.finish() |
|
>>> print b.tostring() |
|
<DAZzle Start="DAZ"> |
|
<Package ID="1"> |
|
<FlatRate>TRUE</FlatRate> |
|
<Services COD="ON" /> |
|
</Package> |
|
</DAZzle> |
|
|
|
>>> p.should_queue(Services.COD) |
|
False |
|
|
|
|
|
Batch rollback:: |
|
|
|
>>> b = Batch() |
|
>>> print b.tostring() |
|
<DAZzle /> |
|
|
>>> docinfo_to_etree(_DocInfo('DAZzle', 'PRINTING', 'Start'), root, False) |
>>> b.add_package(FlatRateEnvelope, FlatRateBox) |
Traceback (most recent call last): |
Traceback (most recent call last): |
... |
... |
DocinfoConflict: Can't set 'DAZzle.Start=PRINTING' when 'DAZzle.Start=DAZ' already set |
OptionConflict: Can't set 'PackageType=FLATRATEBOX' when |
|
'PackageType=FLATRATEENVELOPE' already set |
|
|
>>> root = ET.Element('DAZzle') |
>>> print b.tostring() # rollback on error |
>>> pkg = ET.SubElement(root, 'Package', ID='1') |
<DAZzle /> |
>>> print ET.tostring(root) |
|
<DAZzle><Package ID="1" /></DAZzle> |
|
|
|
>>> docinfo_to_etree([verify_zip, Envelope], root, False) |
|
>>> print ET.tostring(root) |
|
<DAZzle Start="DAZ"><Package ID="1"><FlatRate>TRUE</FlatRate></Package></DAZzle> |
|
|
|
|
Misc shipment and postprocessing:: |
|
|
>>> root = make_tree([(Box,), (Envelope,)], verify_zip) |
>>> s = Shipment(verify_zip) |
>>> print ET.tostring(root) # doctest: +NORMALIZE_WHITESPACE |
>>> s.add_package(Box) |
<DAZzle Start="DAZ"><Package |
>>> s.add_package(Envelope) |
ID="1"><FlatRate>BOX</FlatRate></Package><Package |
>>> root, = s.batches |
ID="2"><FlatRate>TRUE</FlatRate></Package></DAZzle> |
>>> print root.tostring() |
|
<DAZzle Start="DAZ"> |
|
<Package ID="1"> |
|
<FlatRate>BOX</FlatRate> |
|
</Package> |
|
<Package ID="2"> |
|
<FlatRate>TRUE</FlatRate> |
|
</Package> |
|
</DAZzle> |
|
|
|
Option inversion:: |
|
|
|
>>> ~Envelope |
|
FlatRate('FALSE') |
|
>>> ~~Envelope |
|
FlatRate('TRUE') |
|
|
|
>>> ~Option('Services', 'ON', 'RegisteredMail') |
|
Services.RegisteredMail('OFF') |
|
>>> ~~Option('Services', 'ON', 'RegisteredMail') |
|
Services.RegisteredMail('ON') |
|
|
|
>>> ~Option('DAZzle', 'YES', 'Prompt') |
|
DAZzle.Prompt('NO') |
|
>>> ~~Option('DAZzle', 'YES', 'Prompt') |
|
DAZzle.Prompt('YES') |
|
|
|
|
The ``iter_docinfo()`` generic function yields "docinfo" objects for an |
The ``iter_options()`` generic function yields "option" objects for an |
application object. The default implementation is to raise an error:: |
application object. The default implementation is to raise an error:: |
|
|
>>> from pydicia import iter_docinfo |
>>> from pydicia import iter_options |
|
|
>>> iter_docinfo(27) |
>>> iter_options(27) |
Traceback (most recent call last): |
Traceback (most recent call last): |
... |
... |
NotImplementedError: ('No docinfo producer registered for', <type 'int'>) |
NotImplementedError: ('No option producer registered for', <type 'int'>) |
|
|
And for lists and tuples, the default is to yield their contents:: |
And for lists and tuples, the default is to yield their contents:: |
|
|
>>> list(iter_docinfo((1, 2, 3))) |
>>> list(iter_options((1, 2, 3))) |
[1, 2, 3] |
[1, 2, 3] |
|
|
>>> list(iter_docinfo(['a', 'b'])) |
>>> list(iter_options(['a', 'b'])) |
['a', 'b'] |
['a', 'b'] |
|
|
This routine is used internally by ``docinfo_to_etree()``. |
This routine is used internally by ``add_to_package()``. |
|
|
|
|