diff --git a/CHANGES.txt b/CHANGES.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ed0d4b77291a38bdc0dc040a1c5958636e16e5cb
--- /dev/null
+++ b/CHANGES.txt
@@ -0,0 +1,6 @@
+
+0.3a1
+-----
+
+- Refactored to rely on `edbob `_. (Most of Rattail's
+ "guts" now live there instead.)
diff --git a/COPYING.txt b/COPYING.txt
new file mode 100644
index 0000000000000000000000000000000000000000..dba13ed2ddf783ee8118c6a581dbf75305f816a3
--- /dev/null
+++ b/COPYING.txt
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000000000000000000000000000000000000..ab30e9aceec1669b047b51ca747345ce2f555354
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include *.txt
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3b83b2d27bbb88673436cf471319f4af15d600a5
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,27 @@
+
+Rattail
+=======
+
+Rattail is a retail software framework based on `edbob `_,
+and released under the GNU Affero General Public License.
+
+This is the core ``rattail`` package.
+
+Please see Rattail's `home page `_ for more
+information.
+
+
+Installation
+------------
+
+Install the software with::
+
+ $ pip install rattail
+
+
+Usage
+-----
+
+Built-in help can be seen with::
+
+ $ rattail help
diff --git a/rattail/__init__.py b/rattail/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..da0247814b1f2b3d3fcb2d20eccf3fe52ecdd3a5
--- /dev/null
+++ b/rattail/__init__.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Rattail. If not, see .
+#
+################################################################################
+
+"""
+``rattail`` -- Namespace Root
+"""
+
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
+
+
+from edbob.db.extensions import Extension
+
+import rattail.model
+from rattail._version import __version__
+from rattail.enum import *
+from rattail.model import *
+
+
+__all__ = rattail.model.__all__
+
+
+class RattailExtension(Extension):
+
+ name = 'rattail'
diff --git a/rattail/_version.py b/rattail/_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ca1d174f116beb2cd5086f455cf7a31cac432cf
--- /dev/null
+++ b/rattail/_version.py
@@ -0,0 +1 @@
+__version__ = '0.3a1'
diff --git a/rattail/barcodes.py b/rattail/barcodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf1e7174b9c7c957fedae8ad169a8def988a3edb
--- /dev/null
+++ b/rattail/barcodes.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Rattail. If not, see .
+#
+################################################################################
+
+"""
+``rattail.barcodes`` -- Barcode Utilities
+"""
+
+# import re
+
+
+def upc_check_digit(data):
+ """
+ Calculates the check digit for ``data``, according to the standard
+ `UPC algorithm `_. The check
+ digit will be returned in integer form.
+ """
+ sum_ = 0
+ for i in range(len(data) - 1, -1, -2):
+ sum_ += int(data[i]) * 3
+ for i in range(len(data) - 2, -1, -2):
+ sum_ += int(data[i])
+ remainder = sum_ % 10
+ if remainder == 0:
+ return 0
+ return 10 - remainder
+
+
+def luhn_check_digit(data):
+ """
+ Calculate the check digit for ``data`` according to the
+ `Luhn algorithm `_. The check
+ digit will be returned in integer form.
+ """
+ reverse_data = ''.join([x for x in reversed(data)])
+ sum_ = 0
+ for i in range(len(reverse_data)):
+ digit = int(reverse_data[i])
+ if i % 2 == 0:
+ digit *= 2
+ digit = sum([int(x) for x in str(digit)])
+ sum_ += digit
+ remainder = sum_ % 10
+ if remainder == 0:
+ return 0
+ return 10 - remainder
+
+
+def price_check_digit(data):
+ """
+ Returns the "price check digit" for ``data``, which must be a
+ four-character string of digits. The check digit is returned in integer
+ form.
+
+ This typically would be used to validate a random weight UPC. See GS1's
+ `Price Check Digit (North American POS Product Sold by Weight/Measure)
+ `_
+ for more information.
+ """
+
+ if not isinstance(data, basestring) and len(data) == 4:
+ raise ValueError("'data' must be 4-character string; got: %s" % str(data))
+
+ map1 = {0:0, 1:2, 2:4, 3:6, 4:8, 5:9, 6:1, 7:3, 8:5, 9:7}
+ map2 = {0:0, 1:3, 2:6, 3:9, 4:2, 5:5, 6:8, 7:1, 8:4, 9:7}
+ map3 = {0:0, 1:5, 2:9, 3:4, 4:8, 5:3, 6:7, 7:2, 8:6, 9:1}
+
+ data = [int(x) for x in data]
+ sum_ = map1[data[0]] + map1[data[1]] + map2[data[2]] + map3[data[3]]
+ return int(str(3 * sum_)[-1])
+
+
+def upce_to_upca(upce, include_check_digit=False):
+ """
+ Expands ``upce`` (which is assumed to be a valid UPC-E barcode) into its
+ full UPC-A equivalent. The return value will have either 11 or 12 digits,
+ depending on ``include_check_digit``.
+ """
+ if len(upce) == 8:
+ upce = upce[1:7]
+ assert len(upce) == 6
+ assert upce.isdigit()
+
+ last_digit = int(upce[-1])
+
+ if last_digit == 0:
+ upca = "0%02u00000%03u" % (int(upce[0:2]), int(upce[2:5]))
+ elif last_digit == 1:
+ upca = "0%02u10000%03u" % (int(upce[0:2]), int(upce[2:5]))
+ elif last_digit == 2:
+ upca = "0%02u20000%03u" % (int(upce[0:2]), int(upce[2:5]))
+ elif last_digit == 3:
+ upca = "0%03u00000%02u" % (int(upce[0:3]), int(upce[3:5]))
+ elif last_digit == 4:
+ upca = "0%04u00000%01u" % (int(upce[0:4]), int(upce[4]))
+ elif last_digit == 5:
+ upca = "0%05u00005" % int(upce[0:5])
+ elif last_digit == 6:
+ upca = "0%05u00006" % int(upce[0:5])
+ elif last_digit == 7:
+ upca = "0%05u00007" % int(upce[0:5])
+ elif last_digit == 8:
+ upca = "0%05u00008" % int(upce[0:5])
+ elif last_digit == 9:
+ upca = "0%05u00009" % int(upce[0:5])
+
+ if include_check_digit:
+ upca += str(calculate_check_digit(upca))
+
+ return upca
+
+
+# TODO: Finish and verify this function, eventually... (I wound up not needing
+# it for the moment.)
+
+# def upca_to_upce(upca):
+# """
+# Accepts a UPC-A barcode and returns its UPC-E (compressed) equivalent.
+
+# .. note::
+# Not all UPC-A barcodes support compression; in fact relatively few do.
+# This function will return ``None`` if it is not able to compress the
+# UPC-A it receives as input.
+# """
+# assert len(upca) == 11
+
+# m = re.match(r'^0(\d{2})00000(\d{3})$', upca)
+# if m:
+# return '%s%s0' % (m.group(1), m.group(2))
+# m = re.match(r'^0(\d{2})10000(\d{3})$', upca)
+# if m:
+# return '%s%s1' % (m.group(1), m.group(2))
+# m = re.match(r'^0(\d{2})20000(\d{3})$', upca)
+# if m:
+# return '%s%s2' % (m.group(1), m.group(2))
+# m = re.match(r'^0(\d{3})00000(\d{2})$', upca)
+# if m:
+# return '%s%s3' % (m.group(1), m.group(2))
+# m = re.match(r'^0(\d{4})00000(\d)$', upca)
+# if m:
+# return '%s%s4' % (m.group(1), m.group(2))
+# m = re.match(r'^0(\d{5})00005$', upca)
+# if m:
+# return '%s5' % m.group(1)
+# m = re.match(r'^0(\d{5})00006$', upca)
+# if m:
+# return '%s6' % m.group(1)
+# m = re.match(r'^0(\d{5})00007$', upca)
+# if m:
+# return '%s7' % m.group(1)
+# m = re.match(r'^0(\d{5})00008$', upca)
+# if m:
+# return '%s8' % m.group(1)
+# m = re.match(r'^0(\d{5})00009$', upca)
+# if m:
+# return '%s9' % m.group(1)
+# return None
diff --git a/rattail/batches.py b/rattail/batches.py
new file mode 100644
index 0000000000000000000000000000000000000000..fac796ed4733a87bbfb3f7aa5ec29fb93d1d5172
--- /dev/null
+++ b/rattail/batches.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Rattail. If not, see .
+#
+################################################################################
+
+"""
+``rattail.batches`` -- Batch Interface
+"""
+
+# import logging
+
+from sqlalchemy import and_
+from sqlalchemy.orm import object_session
+
+import edbob
+from edbob.db import needs_session
+from edbob.util import requires_impl
+
+import rattail
+# from rattail.db import needs_session
+# from rattail.util import get_entry_points, requires_impl
+
+
+# log = logging.getLogger(__name__)
+# # _registered_sources = None
+
+
+# # def get_registered_sources():
+# # """
+# # Returns the entry point map for registered batch sources.
+# # """
+
+# # global _registered_sources
+# # if _registered_sources is None:
+# # _registered_sources = get_entry_points('rattail.batch_sources')
+# # return _registered_sources
+
+
+# # @needs_session
+# # def get_sources(session):
+# # """
+# # Returns a dictionary of registered :class:`rattail.BatchSource` classes,
+# # keyed by :attr:`BatchSource.name`.
+# # """
+
+# # sources = {}
+# # for src in session.query(rattail.BatchSource):
+# # sources[src.name] = src
+# # return sources
+
+
+class BatchTerminal(edbob.Object):
+ """
+ Defines the interface for data batch terminals. Subclass this when
+ implementing new data awareness and/or integration with external systems.
+ """
+
+ @property
+ @requires_impl()
+ def name(self):
+ pass
+
+ @property
+ @requires_impl()
+ def display(self):
+ pass
+
+ @property
+ @requires_impl()
+ def fieldmap_internal(self):
+ pass
+
+ @property
+ @requires_impl()
+ def fieldmap_user(self):
+ pass
+
+ # def get_elements(self, elements, fieldmap, *required):
+ # """
+ # Returns the proper element list according to current context.
+ # """
+
+ # if elements is None:
+ # elements = sorted(fieldmap)
+ # self.require_elements(elements, *required)
+ # return elements
+
+ # def require_elements(self, using, *required):
+ # """
+ # Officially require one or more elements when processing a batch.
+ # """
+
+ # for elements in required:
+ # elements = elements.split(',')
+ # for element in elements:
+ # if element not in using:
+ # raise sil.ElementRequiredError(element, using)
+
+
+class RattailBatchTerminal(BatchTerminal):
+ """
+ Defines the core batch terminal for Rattail.
+ """
+
+ name = 'rattail'
+ description = "Rattail (local)"
+
+ # source = True
+ # target = True
+
+ # fieldmap_internal = {
+ # 'F01' : 'upc',
+ # 'F02' : 'description',
+ # 'F4001' : 'description2',
+ # }
+
+ # fieldmap_user = {
+ # 'F01' : "UPC",
+ # 'F02' : "Description",
+ # 'F4001' : "Description 2",
+ # }
+
+ source_columns = {
+ 'ITEM_DCT': [
+ 'F01',
+ 'F02',
+ ],
+ }
+
+ target_columns = {
+ 'ITEM_DCT': [
+ 'F01',
+ 'F02',
+ ],
+ }
+
+ # def import_main_item(self, session, elements=None, progress_factory=None):
+ # """
+ # Create a main item (ITEM_DCT) batch from current Rattail data.
+ # """
+
+ # elements = self.get_elements(elements, self.fieldname_map_main_item, 'F01')
+ # batch = make_batch(self.name, elements, session,
+ # description="Main item data from Rattail")
+
+ # products = session.query(rattail.Product)
+ # prog = None
+ # if progress_factory:
+ # prog = progress_factory("Creating batch", products.count())
+ # for i, prod in enumerate(products, 1):
+ # fields = {}
+ # for name in elements:
+ # fields[name] = row[self.fieldname_map_main_item[name]]
+ # batch.append(**fields)
+ # if prog:
+ # prog.update(i)
+ # if prog:
+ # prog.destroy()
+
+ # return batch
+
+ def execute_batch(self, batch):
+ """
+ Executes ``batch``, which should be a :class:`rattail.Batch` instance.
+ """
+
+ assert batch.action_type == rattail.BATCH_ADD
+ assert batch.dictionary.name == 'ITEM_DCT'
+
+ session = object_session(batch)
+ for row in batch.provide_rows():
+ prod = rattail.Product()
+ prod.upc = row.F01
+ prod.description = row.F02
+ session.add(prod)
+ session.flush()
+
+
+# def make_batch(source, elements, session, batch_id=None, **kwargs):
+# """
+# Create and return a new SIL-based :class:`rattail.Batch` instance.
+# """
+
+# if not batch_id:
+# batch_id = next_batch_id(source, consume=True)
+
+# kwargs['source'] = source
+# kwargs['batch_id'] = batch_id
+# kwargs['target'] = target
+# kwargs['elements'] = elements
+# kwargs.setdefault('sil_type', 'HM')
+# kwargs.setdefault('action_type', rattail.BATCH_ADD_REPLACE)
+# kwargs.setdefault('dictionary', rattail.BATCH_MAIN_ITEM)
+# batch = rattail.Batch(**kwargs)
+
+# session.add(batch)
+# session.flush()
+# batch.table.create()
+# log.info("Created batch table: %s" % batch.table.name)
+# return batch
+
+
+@needs_session
+def next_batch_id(session, source, consume=False):
+ """
+ Returns the next available batch ID (as an integer) for the given
+ ``source`` SIL ID.
+
+ If ``consume`` is ``True``, the "running" ID will be incremented so that
+ the next caller will receive a different ID.
+ """
+
+ batch_id = edbob.get_setting('batch.next_id.%s' % source,
+ session=session)
+ if batch_id is None or not batch_id.isdigit():
+ batch_id = 1
+ else:
+ batch_id = int(batch_id)
+
+ while True:
+ q = session.query(rattail.Batch)
+ q = q.join((rattail.BatchTerminal,
+ rattail.BatchTerminal.uuid == rattail.Batch.source_uuid))
+ q = q.filter(and_(
+ rattail.BatchTerminal.sil_id == source,
+ rattail.Batch.batch_id == '%08u' % batch_id,
+ ))
+ if not q.count():
+ break
+ batch_id += 1
+
+ if consume:
+ edbob.save_setting('batch.next_id.%s' % source, str(batch_id + 1),
+ session=session)
+ return batch_id
diff --git a/rattail/commands.py b/rattail/commands.py
new file mode 100644
index 0000000000000000000000000000000000000000..4444d34c7d708f865430fce3b79e723b069ab03f
--- /dev/null
+++ b/rattail/commands.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Rattail. If not, see .
+#
+################################################################################
+
+"""
+``rattail.commands`` -- Commands
+"""
+
+import sys
+
+from edbob import commands
+
+import rattail
+
+
+class Command(commands.Command):
+ """
+ The primary command for Rattail.
+ """
+
+ name = 'rattail'
+ version = rattail.__version__
+ description = "Retail Software Framework"
+ long_description = """
+Rattail is a retail software framework.
+
+Copyright (c) 2010-2012 Lance Edgar
+
+This program comes with ABSOLUTELY NO WARRANTY. This is free software,
+and you are welcome to redistribute it under certain conditions.
+See the file COPYING.txt for more information.
+"""
+
+
+
+class InitCommand(commands.Subcommand):
+ """
+ Initializes the database; called as ``{{package}} initialize``. This is
+ meant to be leveraged as part of setting up the application. The database
+ used by this command will be determined by config, for example::
+
+ .. highlight:: ini
+
+ [edbob.db]
+ sqlalchemy.url = postgresql://user:pass@localhost/{{package}}
+ """
+
+ name = 'initialize'
+ description = "Initialize the database"
+
+ def run(self, args):
+ from edbob.db import engine
+ from edbob.db.util import install_core_schema
+ from edbob.db.exceptions import CoreSchemaAlreadyInstalled
+ from edbob.db.extensions import activate_extension
+
+ # Install core schema to database.
+ try:
+ install_core_schema(engine)
+ except CoreSchemaAlreadyInstalled, err:
+ print '%s:' % err
+ print ' %s' % engine.url
+ return
+
+ # Activate any extensions you like here...
+ # activate_extension('shrubbery')
+
+ # Okay, on to bootstrapping...
+
+ from edbob.db import Session
+ from edbob.db.classes import Role, User
+ from edbob.db.auth import administrator_role
+
+ session = Session()
+
+ # Create 'admin' user with full rights.
+ admin = User(username='admin', password='admin')
+ admin.roles.append(administrator_role(session))
+ session.add(admin)
+
+ # Do any other bootstrapping you like here...
+
+ session.commit()
+ session.close()
+
+ print "Initialized database:"
+ print ' %s' % engine.url
+
+
+def main(*args):
+ """
+ The primary entry point for the Rattail command system.
+ """
+
+ if args:
+ args = list(args)
+ else:
+ args = sys.argv[1:]
+
+ cmd = Command()
+ cmd.run(*args)
diff --git a/rattail/db.py b/rattail/db.py
new file mode 100644
index 0000000000000000000000000000000000000000..53275c8153311eddb5d50cba95a3edbf389e91ac
--- /dev/null
+++ b/rattail/db.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Rattail. If not, see .
+#
+################################################################################
+
+"""
+``rattail.db`` -- Database Stuff
+"""
+
+from edbob.db.extensions import activate_extension
+
+import rattail
+
+
+def init_database(session):
+ """
+ Initialize an ``edbob`` database for use with Rattail.
+ """
+
+ activate_extension('rattail', session.bind)
+
+ columns = [
+ ('F01', 'UPC', 'GPC(14)'),
+ ('F02', 'Description', 'CHAR(20)'),
+ ]
+
+ for name, disp, dtype in columns:
+ session.add(rattail.SilColumn(
+ sil_name=name, display=disp, data_type=dtype))
+ session.flush()
+
+ dictionaries = [
+ # ('CLASS_GROUP', 'Scale Class / Group', []),
+ # ('DEPT_DCT', 'Department', []),
+ # ('FCOST_DCT', 'Future Cost', []),
+ # ('FSPRICE_DCT', 'Future Sale Price', []),
+ ('ITEM_DCT', 'Product', [
+ ('F01', True),
+ 'F02',
+ ]),
+ # ('NUTRITION', 'Scale Nutrition', []),
+ # ('PRICE_DCT', 'Price', []),
+ # ('SCALE_TEXT', 'Scale Text', []),
+ # ('VENDOR_DCT', 'Vendor', []),
+ ]
+
+ for name, desc, cols in dictionaries:
+ bd = rattail.BatchDictionary(name=name, description=desc)
+ for col in cols:
+ key = False
+ if not isinstance(col, basestring):
+ col, key = col
+ q = session.query(rattail.SilColumn)
+ q = q.filter(rattail.SilColumn.sil_name == col)
+ col = q.one()
+ bd.columns.append(
+ rattail.BatchDictionaryColumn(column=col, key=key))
+ session.add(bd)
+ session.flush()
diff --git a/rattail/enum.py b/rattail/enum.py
new file mode 100644
index 0000000000000000000000000000000000000000..25b0fa3bf28a903b902fc5b67cde9c02ae79214d
--- /dev/null
+++ b/rattail/enum.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Rattail. If not, see .
+#
+################################################################################
+
+"""
+``rattail.enum`` -- Enumerations
+"""
+
+
+BATCH_ADD = 'ADD'
+BATCH_ADD_REPLACE = 'ADDRPL'
+BATCH_CHANGE = 'CHANGE'
+BATCH_LOAD = 'LOAD'
+BATCH_REMOVE = 'REMOVE'
+
+BATCH_ACTION_TYPE = {
+ BATCH_ADD : "Add",
+ BATCH_ADD_REPLACE : "Add/Replace",
+ BATCH_CHANGE : "Change",
+ BATCH_LOAD : "Load",
+ BATCH_REMOVE : "Remove",
+ }
+
+
+# BATCH_MAIN_ITEM = 'ITEM_DCT'
+
+# BATCH_DICTIONARY = {
+# BATCH_MAIN_ITEM : "Main Item",
+# }
+
+
+# EMPLOYEE_STATUS_CURRENT = 1
+# EMPLOYEE_STATUS_FORMER = 2
+
+# EMPLOYEE_STATUS = {
+# EMPLOYEE_STATUS_CURRENT : "current",
+# EMPLOYEE_STATUS_FORMER : "former",
+# }
+
+
+# VENDOR_CATALOG_NOT_PARSED = 1
+# VENDOR_CATALOG_PARSED = 2
+# VENDOR_CATALOG_COGNIZED = 3
+# VENDOR_CATALOG_PROCESSED = 4
+
+# VENDOR_CATALOG_STATUS = {
+# VENDOR_CATALOG_NOT_PARSED : "not parsed",
+# VENDOR_CATALOG_PARSED : "parsed",
+# VENDOR_CATALOG_COGNIZED : "cognized",
+# VENDOR_CATALOG_PROCESSED : "processed",
+# }
diff --git a/rattail/model.py b/rattail/model.py
new file mode 100644
index 0000000000000000000000000000000000000000..e31bec8f0f7ed2add982ff4e47b758da60a7cf45
--- /dev/null
+++ b/rattail/model.py
@@ -0,0 +1,536 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Rattail. If not, see .
+#
+################################################################################
+
+"""
+``rattail.db.extension.model`` -- Schema Definition
+"""
+
+import re
+
+from sqlalchemy import (Column, String, Integer, Date, DateTime,
+ Boolean, Text, ForeignKey, BigInteger)
+from sqlalchemy.orm import relationship, object_session
+
+import edbob
+from edbob.db.model import Base, uuid_column
+
+
+__all__ = ['SilColumn', 'BatchDictionaryColumn', 'BatchDictionary',
+ 'BatchTerminalSourceColumn', 'BatchTerminalTargetColumn',
+ 'BatchTerminal', 'BatchColumn', 'Batch', 'Brand', 'Product']
+
+sil_type_pattern = re.compile(r'^(CHAR)\((\d+)\)$')
+
+
+class SilColumn(Base):
+ """
+ Represents a SIL-compatible column available to the batch system.
+ """
+
+ __tablename__ = 'sil_columns'
+
+ uuid = uuid_column()
+ sil_name = Column(String(10))
+ display = Column(String(20))
+ data_type = Column(String(15))
+
+ def __repr__(self):
+ return "" % self.sil_name
+
+ def __str__(self):
+ return str(self.sil_name or '')
+
+
+class BatchDictionaryColumn(Base):
+ """
+ Represents a column within a :class:`BatchDictionary`.
+ """
+
+ __tablename__ = 'batch_dictionary_columns'
+
+ uuid = uuid_column()
+ dictionary_uuid = Column(String(32), ForeignKey('batch_dictionaries.uuid'))
+ column_uuid = Column(String(32), ForeignKey('sil_columns.uuid'))
+ key = Column(Boolean)
+
+ column = relationship(SilColumn)
+
+ def __repr__(self):
+ return "" % self.column
+
+ def __str__(self):
+ return str(self.column or '')
+
+
+class BatchDictionary(Base):
+ """
+ Represents a SIL-based dictionary supported by one or more
+ :class:`BatchTerminal` classes.
+ """
+
+ __tablename__ = 'batch_dictionaries'
+
+ uuid = uuid_column()
+ name = Column(String(20))
+ description = Column(String(255))
+
+ columns = relationship(
+ BatchDictionaryColumn,
+ backref='dictionary')
+
+ def __repr__(self):
+ return "" % self.name
+
+ def __str__(self):
+ return str(self.description or '')
+
+
+class BatchTerminalSourceColumn(Base):
+ """
+ Represents a "source column" supported by a :class:`BatchTerminal`.
+ """
+
+ __tablename__ = 'batch_terminal_source_columns'
+
+ uuid = uuid_column()
+ terminal_uuid = Column(String(32), ForeignKey('batch_terminals.uuid'))
+ dictionary_uuid = Column(String(32), ForeignKey('batch_dictionaries.uuid'))
+ column_uuid = Column(String(32), ForeignKey('sil_columns.uuid'))
+ ordinal = Column(Integer)
+
+ dictionary = relationship(BatchDictionary)
+ column = relationship(SilColumn)
+
+ def __repr__(self):
+ return "" % (
+ self.terminal, self.dictionary, self.name)
+
+ def __str__(self):
+ return str(self.name or '')
+
+
+class BatchTerminalTargetColumn(Base):
+ """
+ Represents a "target column" supported by a :class:`BatchTerminal`.
+ """
+
+ __tablename__ = 'batch_terminal_target_columns'
+
+ uuid = uuid_column()
+ terminal_uuid = Column(String(32), ForeignKey('batch_terminals.uuid'))
+ dictionary_uuid = Column(String(32), ForeignKey('batch_dictionaries.uuid'))
+ column_uuid = Column(String(32), ForeignKey('sil_columns.uuid'))
+ ordinal = Column(Integer)
+
+ dictionary = relationship(BatchDictionary)
+ column = relationship(SilColumn)
+
+ def __repr__(self):
+ return "" % (
+ self.terminal, self.dictionary, self.name)
+
+ def __str__(self):
+ return str(self.name or '')
+
+
+class BatchTerminal(Base):
+ """
+ Represents a terminal, or "junction" for batch data.
+ """
+
+ __tablename__ = 'batch_terminals'
+
+ uuid = uuid_column()
+ sil_id = Column(String(20), unique=True)
+ description = Column(String(50))
+ class_spec = Column(String(255))
+ functional = Column(Boolean, default=False)
+ source = Column(Boolean)
+ target = Column(Boolean)
+ source_kwargs = Column(Text)
+ target_kwargs = Column(Text)
+
+ source_columns = relationship(
+ BatchTerminalSourceColumn,
+ backref='terminal')
+ target_columns = relationship(
+ BatchTerminalTargetColumn,
+ backref='terminal')
+
+ _terminal = 'not_got_yet'
+
+ def __repr__(self):
+ return "" % self.sil_id
+
+ def __str__(self):
+ return str(self.description or '')
+
+ def get_terminal(self):
+ """
+ Returns the :class:`rattail.batches.BatchTerminal` instance which is
+ associated with the database record via its ``python_spec`` field.
+ """
+
+ if self._terminal == 'not_got_yet':
+ self._terminal = None
+ if self.class_spec:
+ term = edbob.load_spec(self.class_spec)
+ if term:
+ self._terminal = term()
+ return self._terminal
+
+
+class BatchColumn(Base):
+ """
+ Represents a :class:`SilColumn` associated with a :class:`Batch`.
+ """
+
+ __tablename__ = 'batch_columns'
+
+ uuid = uuid_column()
+ batch_uuid = Column(String(32), ForeignKey('batches.uuid'))
+ ordinal = Column(Integer)
+ column_uuid = Column(String(32), ForeignKey('sil_columns.uuid'))
+ source_uuid = Column(String(32), ForeignKey('batch_terminals.uuid'))
+ targeted = Column(Boolean)
+
+ column = relationship(SilColumn)
+
+ source = relationship(
+ BatchTerminal,
+ primaryjoin=BatchTerminal.uuid == source_uuid,
+ order_by=[BatchTerminal.description],
+ )
+
+ def __repr__(self):
+ return "" % (self.batch, self.column)
+
+
+# def get_sil_column(name):
+# """
+# Returns a ``sqlalchemy.Column`` instance according to Rattail's notion of
+# what each SIL field ought to look like.
+# """
+
+# type_map = {
+
+# # The first list of columns is a subset of Level 1 SIL.
+
+# 'F01':
+# Integer, # upc
+# 'F02':
+# String(60), # short (receipt) description
+# 'F478':
+# Integer, # scale text type
+
+# # The remaining columns are custom to Rattail.
+
+# 'F4001':
+# String(60), # short description, line 2
+# }
+
+# return Column(name, type_map[name])
+
+
+def get_sil_type(data_type):
+ """
+ Returns a SQLAlchemy-based data type according to the SIL-compliant type
+ specifier found in ``data_type``.
+ """
+
+ if data_type == 'GPC(14)':
+ return BigInteger
+
+ m = sil_type_pattern.match(data_type)
+ if m:
+ data_type, precision = m.groups()
+ if data_type == 'CHAR':
+ return String(int(precision))
+
+ assert False, "FIXME"
+
+
+class Batch(Base):
+ """
+ Represents a batch of data, presumably in need of processing.
+ """
+
+ __tablename__ = 'batches'
+
+ _rowclass = None
+ _row_classes = {}
+
+ uuid = uuid_column()
+ source_uuid = Column(String(32), ForeignKey('batch_terminals.uuid'))
+ source_description = Column(String(50))
+ source_batch_id = Column(String(8))
+ batch_id = Column(String(8))
+ dictionary_uuid = Column(String(32), ForeignKey('batch_dictionaries.uuid'))
+ name = Column(String(30))
+ target_uuid = Column(String(32), ForeignKey('batch_terminals.uuid'))
+ action_type = Column(String(6))
+ elements = Column(String(255))
+ description = Column(String(50))
+ rowcount = Column(Integer, default=0)
+ effective = Column(DateTime)
+ deleted = Column(Boolean, default=False)
+ sil_type = Column(String(2))
+ sil_source_id = Column(String(20))
+ sil_target_id = Column(String(20))
+ sil_audit_file = Column(String(12))
+ sil_response_file = Column(String(12))
+ sil_origin_time = Column(DateTime)
+ sil_purge_date = Column(Date)
+ sil_user1 = Column(String(30))
+ sil_user2 = Column(String(30))
+ sil_user3 = Column(String(30))
+ sil_warning_level = Column(Integer)
+ sil_max_errors = Column(Integer)
+ sil_level = Column(String(7))
+ sil_software_revision = Column(String(4))
+ sil_primary_key = Column(String(50))
+ sil_sys_command = Column(String(512))
+ sil_dict_revision = Column(String(8))
+
+ source = relationship(
+ BatchTerminal,
+ primaryjoin=BatchTerminal.uuid == source_uuid,
+ order_by=[BatchTerminal.description],
+ )
+
+ dictionary = relationship(
+ BatchDictionary,
+ order_by=[BatchDictionary.name],
+ )
+
+ target = relationship(
+ BatchTerminal,
+ primaryjoin=BatchTerminal.uuid == target_uuid,
+ order_by=[BatchTerminal.description],
+ )
+
+ columns = relationship(
+ BatchColumn,
+ backref='batch',
+ )
+
+ # _table = None
+ # # _source_junction = 'not set'
+ # # _target_junction = 'not set'
+
+ # invalid_name_chars = re.compile(r'[^A-Za-z0-9]')
+
+ def __repr__(self):
+ return "" % (self.name or '(no name)')
+
+ def __str__(self):
+ return str(self.name or '')
+
+ # @property
+ # def source_junction(self):
+ # """
+ # Returns the :class:`rattail.BatchJunction` instance associated with
+ # this batch's :attr:`Batch.source` attribute.
+ # """
+
+ # if self._source_junction == 'not set':
+ # from rattail.sil import get_available_junctions
+ # self._source_junction = None
+ # junctions = get_available_junctions()
+ # if self.source in junctions:
+ # self._source_junction = junctions[self.source]
+ # return self._source_junction
+
+ # @property
+ # def table(self):
+ # """
+ # Returns the ``sqlalchemy.Table`` instance for the underlying batch
+ # data.
+ # """
+
+ # # from sqlalchemy import MetaData, Table, Column, String
+ # from sqlalchemy import Table, Column, String
+ # from rattail import metadata
+ # from rattail.sqlalchemy import get_sil_column
+
+ # if self._table is None:
+ # # assert self.uuid
+ # assert self.name
+ # name = 'batch.%s.%s' % (self.source, self.batch_id)
+ # if name in metadata.tables:
+ # self._table = metadata.tables[name]
+ # else:
+ # # session = object_session(self)
+ # # metadata = MetaData(session.bind)
+ # columns = [Column('uuid', String(32), primary_key=True, default=get_uuid)]
+ # # columns.extend([get_sil_column(x) for x in self.elements.split(',')])
+ # columns.extend([get_sil_column(x) for x in self.elements.split(',')])
+ # self._table = Table(name, metadata, *columns)
+ # return self._table
+
+ # @property
+ # def rowclass(self):
+ # """
+ # Returns a unique subclass of :class:`rattail.BatchRow`, specific to the
+ # batch.
+ # """
+
+ # if self._rowclass is None:
+ # name = self.invalid_name_chars.sub('_', self.name)
+ # self._rowclass = type('BatchRow_%s' % str(name), (BatchRow,), {})
+ # mapper(self._rowclass, self.table)
+ # # session = object_session(self)
+ # # engine = session.bind
+ # # session.configure(binds={self._rowclass:engine})
+ # return self._rowclass
+
+ @property
+ def rowclass(self):
+ """
+ Returns the SQLAlchemy-mapped class for the underlying data table.
+ """
+
+ assert self.uuid
+ if self.uuid not in self._row_classes:
+ kwargs = {
+ '__tablename__': 'batch.%s' % self.name,
+ 'uuid': uuid_column(),
+ }
+ for col in self.columns:
+ kwargs[col.column.sil_name] = Column(get_sil_type(col.column.data_type))
+ self._row_classes[self.uuid] = type('BatchRow', (Base,), kwargs)
+ return self._row_classes[self.uuid]
+
+ def create_table(self):
+ """
+ Creates the batch's data table within the database.
+ """
+
+ self.rowclass.__table__.create()
+
+ # @property
+ # def target_junction(self):
+ # """
+ # Returns the :class:`rattail.BatchJunction` instance associated with
+ # this batch's :attr:`Batch.target` attribute.
+ # """
+
+ # if self._target_junction == 'not set':
+ # from rattail.sil import get_available_junctions
+ # self._target_junction = None
+ # junctions = get_available_junctions()
+ # if self.target in junctions:
+ # self._target_junction = junctions[self.target]
+ # return self._target_junction
+
+ # def append(self, **row):
+ # """
+ # Appends a row of data to the batch. Note that this is done
+ # immediately, and not within the context of any transaction.
+ # """
+
+ # # self.connection.execute(self.table.insert().values(**row))
+ # # self.rowcount += 1
+ # session = object_session(self)
+ # session.add(self.rowclass(**row))
+ # self.rowcount += 1
+ # session.flush()
+
+ def add_rows(self, source, dictionary, **kwargs):
+ session = object_session(self)
+ source = source.get_terminal()
+ for row in source.provide_rows(session, self.rowclass,
+ dictionary, **kwargs):
+ session.add(row)
+ session.flush()
+
+ def execute(self):
+ """
+ Invokes the batch execution logic. This will instantiate the
+ :class:`rattail.batches.BatchTerminal` instance identified by the
+ batch's :attr:`target` attribute and ask it to process the batch
+ according to its action type.
+
+ .. note::
+ No check is performed to verify the current time is appropriate as
+ far as the batch's effective date is concerned. It is assumed that
+ other logic has already taken care of that and that yes, in fact it
+ *is* time for the batch to be executed.
+ """
+
+ target = self.target.get_terminal()
+ target.execute_batch(self)
+
+ def provide_rows(self):
+ """
+ Generator which yields :class:`BatchRow` instances belonging to the
+ batch.
+ """
+
+ session = object_session(self)
+ for row in session.query(self.rowclass):
+ yield row
+
+
+# class BatchRow(edbob.Object):
+# """
+# Superclass of batch row objects.
+# """
+
+# def __repr__(self):
+# return "" % self.key_value
+
+
+class Brand(Base):
+ """
+ Represents a brand or similar product line.
+ """
+
+ __tablename__ = 'brands'
+
+ uuid = uuid_column()
+ name = Column(String(100))
+
+
+class Product(Base):
+ """
+ Represents a product for sale and/or purchase.
+ """
+
+ __tablename__ = 'products'
+
+ uuid = uuid_column()
+ upc = Column(BigInteger)
+ brand_uuid = Column(String(32), ForeignKey('brands.uuid'))
+ description = Column(String(60))
+ description2 = Column(String(60))
+ size = Column(String(30))
+
+ brand = relationship(Brand)
+
+ def __repr__(self):
+ return "" % self.description
+
+ def __str__(self):
+ return str(self.description or '')
diff --git a/rattail/sil.py b/rattail/sil.py
new file mode 100644
index 0000000000000000000000000000000000000000..60bd7647916074f2f68281db83a34e609ad23aa4
--- /dev/null
+++ b/rattail/sil.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Rattail. If not, see .
+#
+################################################################################
+
+"""
+``rattail.sil`` -- SIL Interface
+"""
+
+# from pkg_resources import iter_entry_points
+
+# import rattail
+# from rattail.batch import make_batch, RattailBatchTerminal
+from rattail.batches import RattailBatchTerminal
+
+
+# _junctions = None
+
+
+# class SILError(Exception):
+# """
+# Base class for SIL errors.
+# """
+
+# pass
+
+
+# class ElementRequiredError(SILError):
+# """
+# Raised when a batch import or export is attempted, but the element list
+# supplied is missing a required element.
+# """
+
+# def __init__(self, required, using):
+# self.required = required
+# self.using = using
+
+# def __str__(self):
+# return "The element list supplied is missing required element '%s': %s" % (
+# self.required, self.using)
+
+
+def default_display(field):
+ """
+ Returns the default UI display value for a SIL field, according to the
+ Rattail field map.
+ """
+
+ return RattailBatchTerminal.fieldmap_user[field]
+
+
+# def get_available_junctions():
+# """
+# Returns a dictionary of available :class:`rattail.BatchJunction` classes,
+# keyed by entry point name.
+# """
+
+# global _junctions
+# if _junctions is None:
+# _junctions = {}
+# for entry_point in iter_entry_points('rattail.batch_junctions'):
+# _junctions[entry_point.name] = entry_point.load()
+# return _junctions
+
+
+# def get_junction_display(name):
+# """
+# Returns the ``display`` value for a registered
+# :class:`rattail.BatchJunction` class, given its ``name``.
+# """
+
+# juncs = get_available_junctions()
+# if name in juncs:
+# return juncs[name].display
+# return None
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..018c3b4389a0b7d7f12b8291475f220e9b7fb6fe
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[egg_info]
+tag_build = .dev
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..3bc1551c6951d6fe3621513b91f2bdc0b57c21d5
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Rattail. If not, see .
+#
+################################################################################
+
+
+import os.path
+from setuptools import setup, find_packages
+
+
+here = os.path.abspath(os.path.dirname(__file__))
+execfile(os.path.join(here, 'rattail', '_version.py'))
+README = open(os.path.join(here, 'README.txt')).read()
+CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
+
+
+requires = [
+ #
+ # Version numbers within comments below have specific meanings.
+ # Basically the 'low' value is a "soft low," and 'high' a "soft high."
+ # In other words:
+ #
+ # If either a 'low' or 'high' value exists, the primary point to be
+ # made about the value is that it represents the most current (stable)
+ # version available for the package (assuming typical public access
+ # methods) whenever this project was started and/or documented.
+ # Therefore:
+ #
+ # If a 'low' version is present, you should know that attempts to use
+ # versions of the package significantly older than the 'low' version
+ # may not yield happy results. (A "hard" high limit may or may not be
+ # indicated by a true version requirement.)
+ #
+ # Similarly, if a 'high' version is present, and especially if this
+ # project has laid dormant for a while, you may need to refactor a bit
+ # when attempting to support a more recent version of the package. (A
+ # "hard" low limit should be indicated by a true version requirement
+ # when a 'high' version is present.)
+ #
+ # In any case, developers and other users are encouraged to play
+ # outside the lines with regard to these soft limits. If bugs are
+ # encountered then they should be filed as such.
+ #
+ # package # low high
+
+ 'edbob', # 0.1a1.dev
+ ]
+
+
+setup(
+ name = "rattail",
+ version = __version__,
+ author = "Lance Edgar",
+ author_email = "lance@edbob.org",
+ url = "http://rattail.edbob.org/",
+ license = "GNU Affero GPL v3",
+ description = "Retail Software Framework",
+ long_description = README + '\n\n' + CHANGES,
+
+ classifiers = [
+ 'Development Status :: 3 - Alpha',
+ 'Environment :: Console',
+ 'Environment :: Web Environment',
+ 'Environment :: Win32 (MS Windows)',
+ 'Environment :: X11 Applications',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: GNU Affero General Public License v3',
+ 'Natural Language :: English',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Topic :: Office/Business',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ ],
+
+ install_requires = requires,
+
+ namespace_packages = ['rattail'],
+ packages = find_packages(),
+ include_package_data = True,
+ zip_safe = False,
+
+ entry_points = """
+
+[console_scripts]
+rattail = rattail.commands:main
+
+[gui_scripts]
+rattailw = rattail.commands:main
+
+[edbob.db.extensions]
+rattail = rattail:RattailExtension
+
+""",
+ )