Compare commits
37 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
742924596d | ||
|
|
ba2b6db75d | ||
|
|
d2725c2fb3 | ||
|
|
b0ac9bc7eb | ||
|
|
c91d98a609 | ||
|
|
5bf93ef57c | ||
|
|
594c58065c | ||
|
|
9c764ea240 | ||
|
|
1d238f3575 | ||
|
|
9dd8e11a79 | ||
|
|
8168c06192 | ||
|
|
3a21fa23bf | ||
|
|
2f63fed30a | ||
|
|
4e93f3b93a | ||
|
|
8bf44f3ded | ||
|
|
c4072c7ac0 | ||
|
|
ade17c5f90 | ||
|
|
8007343b3a | ||
|
|
405d78fb40 | ||
|
|
9624cb8fe4 | ||
|
|
c07438837e | ||
|
|
ff0eb3cf80 | ||
|
|
0471180ee9 | ||
|
|
6e43e03b95 | ||
|
|
5eae6aa7ec | ||
|
|
09ba2a7c3c | ||
|
|
717496d58a | ||
|
|
f32bd6946c | ||
|
|
d1df2aa368 | ||
|
|
36ab26dfaf | ||
|
|
6be40fd4e3 | ||
|
|
25096dc70d | ||
|
|
7106f42461 | ||
|
|
07e3b4afb8 | ||
|
|
3f337e8608 | ||
|
|
ad9777adb6 | ||
|
|
affd0acf4c |
113 changed files with 12798 additions and 1571 deletions
|
|
@ -1 +1,9 @@
|
|||
include *.txt
|
||||
|
||||
recursive-include rattail/templates *.mako
|
||||
|
||||
include docs/Makefile
|
||||
include docs/make.bat
|
||||
include docs/_static/.dummy
|
||||
recursive-include docs *.rst
|
||||
prune docs/_build
|
||||
|
|
|
|||
153
docs/Makefile
Normal file
153
docs/Makefile
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/rattail.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/rattail.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/rattail"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/rattail"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
0
docs/_static/.dummy
vendored
Normal file
0
docs/_static/.dummy
vendored
Normal file
21
docs/barcodes.rst
Normal file
21
docs/barcodes.rst
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
``rattail.barcodes``
|
||||
====================
|
||||
|
||||
.. automodule:: rattail.barcodes
|
||||
|
||||
|
||||
Check Digit Calculation
|
||||
-----------------------
|
||||
|
||||
.. autofunction:: upc_check_digit
|
||||
|
||||
.. autofunction:: price_check_digit
|
||||
|
||||
.. autofunction:: luhn_check_digit
|
||||
|
||||
|
||||
Barcode Conversion
|
||||
------------------
|
||||
|
||||
.. autofunction:: upce_to_upca
|
||||
243
docs/conf.py
Normal file
243
docs/conf.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# rattail documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sat Dec 01 11:14:03 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
import rattail
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'rattail'
|
||||
copyright = u'2012, Lance Edgar'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.3'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = rattail.__version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'rattaildoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'rattail.tex', u'rattail Documentation',
|
||||
u'Lance Edgar', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'rattail', u'rattail Documentation',
|
||||
[u'Lance Edgar'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'rattail', u'rattail Documentation',
|
||||
u'Lance Edgar', 'rattail', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
37
docs/configuration.rst
Normal file
37
docs/configuration.rst
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
|
||||
``rattaill.configuration``
|
||||
==========================
|
||||
|
||||
.. automodule:: rattail.configuration
|
||||
|
||||
|
||||
Storage API
|
||||
-----------
|
||||
|
||||
.. autoclass:: Storage
|
||||
:members:
|
||||
|
||||
.. autoclass:: UserConfigFileStorage
|
||||
|
||||
.. automethod:: __init__
|
||||
|
||||
|
||||
Configuration Parser
|
||||
--------------------
|
||||
|
||||
.. autoclass:: RattailConfigParser
|
||||
:members:
|
||||
|
||||
|
||||
Default File Locations
|
||||
----------------------
|
||||
|
||||
.. autofunction:: default_system_paths
|
||||
|
||||
.. autofunction:: default_user_paths
|
||||
|
||||
|
||||
Logging
|
||||
-------
|
||||
|
||||
.. autofunction:: basic_logging
|
||||
22
docs/core.rst
Normal file
22
docs/core.rst
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
``rattail``
|
||||
===========
|
||||
|
||||
.. automodule:: rattail
|
||||
|
||||
|
||||
Initialization
|
||||
--------------
|
||||
|
||||
.. autofunction:: init
|
||||
|
||||
.. autofunction:: init_modules
|
||||
|
||||
|
||||
Core Classes
|
||||
------------
|
||||
|
||||
.. autoclass:: GPC
|
||||
|
||||
.. automethod:: __init__
|
||||
|
||||
519
docs/db_model.rst
Normal file
519
docs/db_model.rst
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
|
||||
``rattail.db.model``
|
||||
====================
|
||||
|
||||
.. automodule:: rattail.db.model
|
||||
|
||||
|
||||
Model Classes
|
||||
-------------
|
||||
|
||||
.. autoclass:: Batch
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: id
|
||||
|
||||
.. autoattribute:: source
|
||||
|
||||
.. autoattribute:: destination
|
||||
|
||||
.. autoattribute:: action_type
|
||||
|
||||
.. autoattribute:: description
|
||||
|
||||
.. autoattribute:: purge
|
||||
|
||||
.. autoattribute:: executed
|
||||
|
||||
.. autoattribute:: columns
|
||||
|
||||
.. autoattribute:: rowclass
|
||||
|
||||
.. autoattribute:: rowcount
|
||||
|
||||
.. autoattribute:: rows
|
||||
|
||||
Methods:
|
||||
|
||||
.. automethod:: create_table
|
||||
|
||||
.. automethod:: drop_table
|
||||
|
||||
.. automethod:: add_column
|
||||
|
||||
.. automethod:: add_row
|
||||
|
||||
|
||||
.. autoclass:: BatchColumn
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: batch
|
||||
|
||||
.. autoattribute:: ordinal
|
||||
|
||||
.. autoattribute:: name
|
||||
|
||||
.. autoattribute:: sil_name
|
||||
|
||||
.. autoattribute:: data_type
|
||||
|
||||
.. autoattribute:: display_name
|
||||
|
||||
.. autoattribute:: description
|
||||
|
||||
.. autoattribute:: visible
|
||||
|
||||
|
||||
.. autoclass:: BatchRow
|
||||
|
||||
|
||||
.. autoclass:: Brand
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: name
|
||||
|
||||
|
||||
.. autoclass:: Category
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: number
|
||||
|
||||
.. autoattribute:: name
|
||||
|
||||
.. autoattribute:: department
|
||||
|
||||
|
||||
.. autoclass:: Change
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: class_name
|
||||
|
||||
.. autoattribute:: deleted
|
||||
|
||||
|
||||
.. autoclass:: Customer
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: id
|
||||
|
||||
.. autoattribute:: name
|
||||
|
||||
.. autoattribute:: email_preference
|
||||
|
||||
.. autoattribute:: phones
|
||||
|
||||
.. autoattribute:: phone
|
||||
|
||||
.. autoattribute:: emails
|
||||
|
||||
.. autoattribute:: email
|
||||
|
||||
.. autoattribute:: people
|
||||
|
||||
.. autoattribute:: _people
|
||||
|
||||
.. autoattribute:: person
|
||||
|
||||
.. autoattribute:: _person
|
||||
|
||||
.. autoattribute:: groups
|
||||
|
||||
.. autoattribute:: _groups
|
||||
|
||||
Methods:
|
||||
|
||||
.. automethod:: add_email_address
|
||||
|
||||
.. automethod:: add_phone_number
|
||||
|
||||
|
||||
.. autoclass:: CustomerEmailAddress
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: type
|
||||
|
||||
.. autoattribute:: address
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
.. autoclass:: CustomerGroup
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: id
|
||||
|
||||
.. autoattribute:: name
|
||||
|
||||
|
||||
.. autoclass:: CustomerGroupAssignment
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: customer
|
||||
|
||||
.. autoattribute:: group
|
||||
|
||||
|
||||
.. autoclass:: CustomerPerson
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: customer
|
||||
|
||||
.. autoattribute:: person
|
||||
|
||||
.. autoattribute:: ordinal
|
||||
|
||||
|
||||
.. autoclass:: CustomerPhoneNumber
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: type
|
||||
|
||||
.. autoattribute:: number
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
.. autoclass:: Department
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: number
|
||||
|
||||
.. autoattribute:: name
|
||||
|
||||
.. autoattribute:: subdepartments
|
||||
|
||||
|
||||
.. autoclass:: Employee
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: id
|
||||
|
||||
.. autoattribute:: person
|
||||
|
||||
.. autoattribute:: first_name
|
||||
|
||||
.. autoattribute:: last_name
|
||||
|
||||
.. autoattribute:: phones
|
||||
|
||||
.. autoattribute:: phone
|
||||
|
||||
.. autoattribute:: emails
|
||||
|
||||
.. autoattribute:: email
|
||||
|
||||
Methods:
|
||||
|
||||
.. automethod:: add_email_address
|
||||
|
||||
.. automethod:: add_phone_number
|
||||
|
||||
|
||||
.. autoclass:: EmployeeEmailAddress
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: type
|
||||
|
||||
.. autoattribute:: address
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
.. autoclass:: EmployeePhoneNumber
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: type
|
||||
|
||||
.. autoattribute:: number
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
.. autoclass:: LabelProfile
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: code
|
||||
|
||||
.. autoattribute:: description
|
||||
|
||||
.. autoattribute:: printer_spec
|
||||
|
||||
.. autoattribute:: formatter_spec
|
||||
|
||||
.. autoattribute:: format
|
||||
|
||||
.. autoattribute:: visible
|
||||
|
||||
.. autoattribute:: ordinal
|
||||
|
||||
|
||||
.. autoclass:: Person
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: first_name
|
||||
|
||||
.. autoattribute:: last_name
|
||||
|
||||
.. autoattribute:: display_name
|
||||
|
||||
.. autoattribute:: phones
|
||||
|
||||
.. autoattribute:: phone
|
||||
|
||||
.. autoattribute:: emails
|
||||
|
||||
.. autoattribute:: email
|
||||
|
||||
Methods:
|
||||
|
||||
.. automethod:: add_email_address
|
||||
|
||||
.. automethod:: add_phone_number
|
||||
|
||||
|
||||
.. autoclass:: PersonEmailAddress
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: type
|
||||
|
||||
.. autoattribute:: address
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
.. autoclass:: PersonPhoneNumber
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: type
|
||||
|
||||
.. autoattribute:: number
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
.. autoclass:: Product
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: upc
|
||||
|
||||
.. autoattribute:: department
|
||||
|
||||
.. autoattribute:: subdepartment
|
||||
|
||||
.. autoattribute:: category
|
||||
|
||||
.. autoattribute:: brand
|
||||
|
||||
.. autoattribute:: description
|
||||
|
||||
.. autoattribute:: description2
|
||||
|
||||
.. autoattribute:: size
|
||||
|
||||
.. autoattribute:: unit_of_measure
|
||||
|
||||
.. autoattribute:: prices
|
||||
|
||||
.. autoattribute:: regular_price
|
||||
|
||||
.. autoattribute:: current_price
|
||||
|
||||
.. autoattribute:: costs
|
||||
|
||||
.. autoattribute:: cost
|
||||
|
||||
.. autoattribute:: vendor
|
||||
|
||||
|
||||
.. autoclass:: ProductCost
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: product
|
||||
|
||||
.. autoattribute:: vendor
|
||||
|
||||
.. autoattribute:: code
|
||||
|
||||
.. autoattribute:: case_size
|
||||
|
||||
.. autoattribute:: case_cost
|
||||
|
||||
.. autoattribute:: pack_size
|
||||
|
||||
.. autoattribute:: pack_cost
|
||||
|
||||
.. autoattribute:: unit_cost
|
||||
|
||||
.. autoattribute:: effective
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
.. autoclass:: ProductPrice
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: product
|
||||
|
||||
.. autoattribute:: type
|
||||
|
||||
.. autoattribute:: level
|
||||
|
||||
.. autoattribute:: price
|
||||
|
||||
.. autoattribute:: multiple
|
||||
|
||||
.. autoattribute:: pack_price
|
||||
|
||||
.. autoattribute:: pack_multiple
|
||||
|
||||
.. autoattribute:: starts
|
||||
|
||||
.. autoattribute:: ends
|
||||
|
||||
|
||||
.. autoclass:: Store
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: id
|
||||
|
||||
.. autoattribute:: name
|
||||
|
||||
.. autoattribute:: phones
|
||||
|
||||
.. autoattribute:: phone
|
||||
|
||||
.. autoattribute:: emails
|
||||
|
||||
.. autoattribute:: email
|
||||
|
||||
Methods:
|
||||
|
||||
.. automethod:: add_email_address
|
||||
|
||||
.. automethod:: add_phone_number
|
||||
|
||||
|
||||
.. autoclass:: StoreEmailAddress
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: type
|
||||
|
||||
.. autoattribute:: address
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
.. autoclass:: StorePhoneNumber
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: type
|
||||
|
||||
.. autoattribute:: number
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
.. autoclass:: Subdepartment
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: number
|
||||
|
||||
.. autoattribute:: name
|
||||
|
||||
.. autoattribute:: department
|
||||
|
||||
|
||||
.. autoclass:: Vendor
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: id
|
||||
|
||||
.. autoattribute:: name
|
||||
|
||||
.. autoattribute:: special_discount
|
||||
|
||||
.. autoattribute:: phones
|
||||
|
||||
.. autoattribute:: phone
|
||||
|
||||
.. autoattribute:: emails
|
||||
|
||||
.. autoattribute:: email
|
||||
|
||||
.. autoattribute:: contacts
|
||||
|
||||
.. autoattribute:: contact
|
||||
|
||||
Methods:
|
||||
|
||||
.. automethod:: add_email_address
|
||||
|
||||
.. automethod:: add_phone_number
|
||||
|
||||
.. automethod:: add_contact
|
||||
|
||||
|
||||
.. autoclass:: VendorContact
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: vendor
|
||||
|
||||
.. autoattribute:: person
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
.. autoclass:: VendorEmailAddress
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: type
|
||||
|
||||
.. autoattribute:: address
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
.. autoclass:: VendorPhoneNumber
|
||||
|
||||
Attributes:
|
||||
|
||||
.. autoattribute:: type
|
||||
|
||||
.. autoattribute:: number
|
||||
|
||||
.. autoattribute:: preference
|
||||
|
||||
|
||||
Utilities
|
||||
---------
|
||||
|
||||
.. autofunction:: uuid_column
|
||||
7
docs/db_types.rst
Normal file
7
docs/db_types.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
``rattail.db.types``
|
||||
====================
|
||||
|
||||
.. automodule:: rattail.db.types
|
||||
|
||||
.. autoclass:: GPCType
|
||||
7
docs/db_util.rst
Normal file
7
docs/db_util.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
``rattail.db.util``
|
||||
===================
|
||||
|
||||
.. automodule:: rattail.db.util
|
||||
|
||||
.. autofunction:: install_core_schema
|
||||
21
docs/exceptions.rst
Normal file
21
docs/exceptions.rst
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
``rattail.exceptions``
|
||||
======================
|
||||
|
||||
.. automodule:: rattail.exceptions
|
||||
|
||||
.. autoclass:: RattailError
|
||||
|
||||
.. autoclass:: InitializationError
|
||||
|
||||
.. autoclass:: ModuleHasNoInit
|
||||
|
||||
.. autoclass:: ConfigurationError
|
||||
|
||||
.. autoclass:: NoDefaultDatabase
|
||||
|
||||
.. autoclass:: SILError
|
||||
|
||||
.. autoclass:: SILColumnNotFound
|
||||
|
||||
.. autoclass:: LabelPrintingError
|
||||
15
docs/files.rst
Normal file
15
docs/files.rst
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
``rattail.files``
|
||||
=================
|
||||
|
||||
.. automodule:: rattail.files
|
||||
|
||||
.. autofunction:: count_lines
|
||||
|
||||
.. autofunction:: creation_time
|
||||
|
||||
.. autofunction:: locking_copy
|
||||
|
||||
.. autofunction:: resource_path
|
||||
|
||||
.. autofunction:: temp_path
|
||||
68
docs/index.rst
Normal file
68
docs/index.rst
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
|
||||
``rattail`` Package
|
||||
===================
|
||||
|
||||
The ``rattail`` package is the "core" of the Rattail software framework within
|
||||
the context of Python. This package serves a few distinct purposes, which are
|
||||
outlined below.
|
||||
|
||||
|
||||
Namespace
|
||||
---------
|
||||
|
||||
First and foremost, ``rattail`` is a namespace package. This means that
|
||||
everything provided by Rattail will fall under the ``rattail`` namespace. For
|
||||
example, the pieces which integrate with `LOC Store Management Suite
|
||||
<http://www.locsoftware.com/>`_ exist in the subpackage ``rattail.sw.locsms``
|
||||
(which is distributed separately).
|
||||
|
||||
In practice, most folks may not *need* to know that ``rattail`` is a namespace
|
||||
package; however it is pointed out here for the sake of completeness.
|
||||
|
||||
|
||||
Library API
|
||||
-----------
|
||||
|
||||
Secondly, and perhaps more usefully, ``rattail`` provides a library of various
|
||||
utilities. These are "benign" in that they do not rely on the framework
|
||||
aspects of Rattail at all, and may be used simply by importing the needed
|
||||
function (etc.) and invoking it.
|
||||
|
||||
The following outlines the portions of ``rattail`` which are meant to be
|
||||
generally available, and which incur no framework overhead:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
core
|
||||
barcodes
|
||||
configuration
|
||||
exceptions
|
||||
files
|
||||
modules
|
||||
pricing
|
||||
sil
|
||||
util
|
||||
|
||||
|
||||
Database API
|
||||
------------
|
||||
|
||||
Of course the data is really what a retail shop is interested in. Rattail
|
||||
provides a generic database schema. In addition to the data classes, there are
|
||||
a handful of tools used to create and interact with the data structures:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
db_model
|
||||
db_types
|
||||
db_util
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
190
docs/make.bat
Normal file
190
docs/make.bat
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=_build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\rattail.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\rattail.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
||||
9
docs/modules.rst
Normal file
9
docs/modules.rst
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
``rattail.modules``
|
||||
===================
|
||||
|
||||
.. automodule:: rattail.modules
|
||||
|
||||
.. autofunction:: graft
|
||||
|
||||
.. autofunction:: prune
|
||||
7
docs/pricing.rst
Normal file
7
docs/pricing.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
``rattail.pricing``
|
||||
===================
|
||||
|
||||
.. automodule:: rattail.pricing
|
||||
|
||||
.. autofunction:: gross_margin
|
||||
43
docs/sil.rst
Normal file
43
docs/sil.rst
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
|
||||
``rattail.sil``
|
||||
===============
|
||||
|
||||
.. automodule:: rattail.sil
|
||||
|
||||
|
||||
Column API
|
||||
----------
|
||||
|
||||
.. autoclass:: Column
|
||||
|
||||
.. autofunction:: get_column
|
||||
|
||||
|
||||
Batch API
|
||||
---------
|
||||
|
||||
.. autoclass:: Writer
|
||||
|
||||
.. automethod:: __init__
|
||||
|
||||
.. automethod:: get_fileobj
|
||||
|
||||
.. automethod:: close
|
||||
|
||||
.. automethod:: write
|
||||
|
||||
.. automethod:: val
|
||||
|
||||
.. automethod:: write_row
|
||||
|
||||
.. automethod:: write_rows
|
||||
|
||||
.. automethod:: write_batch_header_raw
|
||||
|
||||
.. automethod:: write_batch_header
|
||||
|
||||
.. automethod:: write_create_header
|
||||
|
||||
.. automethod:: write_maintenance_header
|
||||
|
||||
.. autofunction:: consume_batch_id
|
||||
9
docs/util.rst
Normal file
9
docs/util.rst
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
``rattail.util``
|
||||
================
|
||||
|
||||
.. automodule:: rattail.util
|
||||
|
||||
.. autoclass:: Object
|
||||
|
||||
.. autofunction:: get_uuid
|
||||
|
|
@ -29,4 +29,7 @@
|
|||
__import__('pkg_resources').declare_namespace(__name__)
|
||||
|
||||
from rattail._version import __version__
|
||||
from rattail.util import Object, get_uuid
|
||||
from rattail.barcodes import GPC
|
||||
from rattail.initialization import *
|
||||
from rattail.enum import *
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = '0.3a25'
|
||||
__version__ = '0.4a1'
|
||||
|
|
|
|||
|
|
@ -24,17 +24,104 @@
|
|||
|
||||
"""
|
||||
``rattail.barcodes`` -- Barcode Utilities
|
||||
|
||||
This module contains utility functions for handling barcode conversions, check
|
||||
digit calculations, etc.
|
||||
|
||||
See also the :class:`rattail.GPC` class, which may be used to represent most
|
||||
barcode data.
|
||||
"""
|
||||
|
||||
# import re
|
||||
|
||||
|
||||
class GPC(object):
|
||||
"""
|
||||
Class to abstract the representation of `Global Product Classification
|
||||
<http://www.gs1.org/gdsn/gpc>`_ data. Examples of this would be UPC or EAN
|
||||
barcodes.
|
||||
|
||||
The initial motivation for this class was to provide better SIL support.
|
||||
To that end, the instances are assumed to always be comprised of only
|
||||
numeric digits, and must include a check digit. If you do not know the
|
||||
check digit, provide a ``calc_check_digit`` value to the constructor.
|
||||
"""
|
||||
|
||||
def __init__(self, value, calc_check_digit=False):
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
:param value: The barcode data. This must be either a value of numeric
|
||||
type, or a string containing only digits.
|
||||
:type value: integer or string
|
||||
|
||||
:param calc_check_digit: If this is ``False``, then the ``value`` parameter
|
||||
is assumed to include the check digit.
|
||||
|
||||
If ``value`` does not include a check digit and one needs to be
|
||||
calculated, then this paramter should be a keyword signifying the
|
||||
algorithm to be used.
|
||||
|
||||
Currently the only check digit algorithm keyword supported is
|
||||
``'upc'``. As that is likely to always be the default, setting this
|
||||
parameter to ``True`` will also be interpreted as ``'upc'``.
|
||||
:type calc_check_digit: boolean or string
|
||||
"""
|
||||
|
||||
value = str(value)
|
||||
if calc_check_digit is True or calc_check_digit == 'upc':
|
||||
value += str(upc_check_digit(value))
|
||||
self.value = int(value)
|
||||
|
||||
def __eq__(self, other):
|
||||
return int(self) == int(other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return int(self) != int(other)
|
||||
|
||||
def __gt__(self, other):
|
||||
return int(self) > int(other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return int(self) >= int(other)
|
||||
|
||||
def __lt__(self, other):
|
||||
return int(self) < int(other)
|
||||
|
||||
def __le__(self, other):
|
||||
return int(self) <= int(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.value)
|
||||
|
||||
def __int__(self):
|
||||
return int(self.value)
|
||||
|
||||
def __long__(self):
|
||||
return long(self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return "GPC('%014d')" % self.value
|
||||
|
||||
def __str__(self):
|
||||
return '%014d' % self.value
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%014d' % self.value
|
||||
|
||||
|
||||
def upc_check_digit(data):
|
||||
"""
|
||||
Calculates the check digit for ``data``, according to the standard
|
||||
`UPC algorithm <http://en.wikipedia.org/wiki/Check_digit#UPC>`_. The check
|
||||
digit will be returned in integer form.
|
||||
Calculates a check digit according to the standard `UPC algorithm
|
||||
<http://en.wikipedia.org/wiki/Universal_Product_Code#Check_digits>`_.
|
||||
|
||||
:param data: The barcode data, without check digit.
|
||||
:type data: string
|
||||
|
||||
:returns: The calculated check digit.
|
||||
:rtype: integer
|
||||
"""
|
||||
|
||||
sum_ = 0
|
||||
for i in range(len(data) - 1, -1, -2):
|
||||
sum_ += int(data[i]) * 3
|
||||
|
|
@ -48,10 +135,16 @@ def upc_check_digit(data):
|
|||
|
||||
def luhn_check_digit(data):
|
||||
"""
|
||||
Calculate the check digit for ``data`` according to the
|
||||
`Luhn algorithm <http://en.wikipedia.org/wiki/Luhn_algorithm>`_. The check
|
||||
digit will be returned in integer form.
|
||||
Calculates a check digit according to the `Luhn algorithm
|
||||
<http://en.wikipedia.org/wiki/Luhn_algorithm>`_.
|
||||
|
||||
:param data: The barcode data, without check digit.
|
||||
:type data: string
|
||||
|
||||
:returns: The calculated check digit.
|
||||
:rtype: integer
|
||||
"""
|
||||
|
||||
reverse_data = ''.join([x for x in reversed(data)])
|
||||
sum_ = 0
|
||||
for i in range(len(reverse_data)):
|
||||
|
|
@ -68,17 +161,18 @@ def luhn_check_digit(data):
|
|||
|
||||
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.
|
||||
Calculates a check digit according to the `Price Check Digit algorithm
|
||||
<http://barcodes.gs1us.org/GS1%20US%20BarCodes%20and%20eCom%20-%20The%20Global%20Language%20of%20Business.htm#PCD>`_
|
||||
|
||||
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)
|
||||
<http://www.gs1us.org/GS1%20US%20BarCodes%20and%20eCom%20-%20The%20Global%20Language%20of%20Business.htm#PCD>`_
|
||||
for more information.
|
||||
:param data: The price data, without check digit. The length of the data
|
||||
string must be exactly 4 characters.
|
||||
:type data: string
|
||||
|
||||
:returns: The calculated check digit.
|
||||
:rtype: integer
|
||||
"""
|
||||
|
||||
if not isinstance(data, basestring) and len(data) == 4:
|
||||
if not isinstance(data, basestring) or 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}
|
||||
|
|
@ -90,42 +184,57 @@ def price_check_digit(data):
|
|||
return int(str(3 * sum_)[-1])
|
||||
|
||||
|
||||
def upce_to_upca(upce, include_check_digit=False):
|
||||
def upce_to_upca(data, 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``.
|
||||
Converts a `UPC-E
|
||||
<http://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E>`_
|
||||
(zero-compressed) barcode into its expanded UPC-A equivalent.
|
||||
|
||||
:param data: The UPC-E barcode data. The length of the data string must be
|
||||
either 6 or 8 characters.
|
||||
:type data: string
|
||||
|
||||
:param include_check_digit: Whether or not to include the check digit in
|
||||
the return value.
|
||||
:type include_check_digit: boolean
|
||||
|
||||
:returns: The expanded UPC-A barcode data. The length of the data string
|
||||
will be 11 or 12 characters, depending on the ``include_check_digit``
|
||||
parameter.
|
||||
:rtype: string
|
||||
"""
|
||||
if len(upce) == 8:
|
||||
upce = upce[1:7]
|
||||
assert len(upce) == 6
|
||||
assert upce.isdigit()
|
||||
|
||||
if len(data) == 8:
|
||||
data = data[1:7]
|
||||
assert len(data) == 6
|
||||
assert data.isdigit()
|
||||
upce = data
|
||||
|
||||
last_digit = int(upce[-1])
|
||||
|
||||
if last_digit == 0:
|
||||
upca = "0%02u00000%03u" % (int(upce[0:2]), int(upce[2:5]))
|
||||
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]))
|
||||
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]))
|
||||
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]))
|
||||
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]))
|
||||
upca = '0%04u00000%01u' % (int(upce[0:4]), int(upce[4]))
|
||||
elif last_digit == 5:
|
||||
upca = "0%05u00005" % int(upce[0:5])
|
||||
upca = '0%05u00005' % int(upce[0:5])
|
||||
elif last_digit == 6:
|
||||
upca = "0%05u00006" % int(upce[0:5])
|
||||
upca = '0%05u00006' % int(upce[0:5])
|
||||
elif last_digit == 7:
|
||||
upca = "0%05u00007" % int(upce[0:5])
|
||||
upca = '0%05u00007' % int(upce[0:5])
|
||||
elif last_digit == 8:
|
||||
upca = "0%05u00008" % int(upce[0:5])
|
||||
upca = '0%05u00008' % int(upce[0:5])
|
||||
elif last_digit == 9:
|
||||
upca = "0%05u00009" % int(upce[0:5])
|
||||
upca = '0%05u00009' % int(upce[0:5])
|
||||
|
||||
if include_check_digit:
|
||||
upca += str(calculate_check_digit(upca))
|
||||
upca += str(upc_check_digit(upca))
|
||||
|
||||
return upca
|
||||
|
||||
|
|
|
|||
|
|
@ -26,24 +26,4 @@
|
|||
``rattail.batches`` -- Batch System
|
||||
"""
|
||||
|
||||
from edbob.util import entry_point_map
|
||||
|
||||
from rattail.batches.exceptions import *
|
||||
from rattail.batches.providers import *
|
||||
|
||||
|
||||
def get_provider(name):
|
||||
providers = get_providers()
|
||||
provider = providers.get(name)
|
||||
if not provider:
|
||||
raise BatchProviderNotFound(name)
|
||||
return provider()
|
||||
|
||||
|
||||
def get_providers():
|
||||
return entry_point_map('rattail.batches.providers')
|
||||
|
||||
|
||||
def iter_providers():
|
||||
providers = get_providers()
|
||||
return sorted(providers.itervalues(), key=lambda x: x.description)
|
||||
|
|
|
|||
|
|
@ -27,17 +27,18 @@
|
|||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import edbob
|
||||
import pkg_resources
|
||||
|
||||
import rattail
|
||||
from rattail import sil
|
||||
from rattail.exceptions import BatchProviderNotFound
|
||||
from rattail.db.model import Batch
|
||||
|
||||
|
||||
__all__ = ['BatchProvider']
|
||||
__all__ = ['BatchProvider', 'iter_providers', 'get_provider']
|
||||
|
||||
|
||||
class BatchProvider(edbob.Object):
|
||||
class BatchProvider(rattail.Object):
|
||||
|
||||
name = None
|
||||
description = None
|
||||
|
|
@ -49,7 +50,7 @@ class BatchProvider(edbob.Object):
|
|||
session = None
|
||||
|
||||
def add_columns(self, batch):
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
def add_rows_begin(self, batch, data):
|
||||
pass
|
||||
|
|
@ -63,7 +64,7 @@ class BatchProvider(edbob.Object):
|
|||
prog = None
|
||||
if progress:
|
||||
prog = progress("Adding rows to batch \"%s\"" % batch.description,
|
||||
data.count())
|
||||
len(data))
|
||||
cancel = False
|
||||
for i, datum in enumerate(data, 1):
|
||||
self.add_row(batch, datum)
|
||||
|
|
@ -86,17 +87,20 @@ class BatchProvider(edbob.Object):
|
|||
def execute(self, batch, progress=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def make_batch(self, session, data, progress=None):
|
||||
def make_batch(self, session, data, progress=None, **kwargs):
|
||||
self.session = session
|
||||
|
||||
batch = rattail.Batch()
|
||||
batch.provider = self.name
|
||||
batch.source = self.source
|
||||
batch.id = sil.consume_batch_id(batch.source)
|
||||
batch.destination = self.destination
|
||||
batch.description = self.description
|
||||
batch.action_type = self.action_type
|
||||
self.set_purge_date(batch)
|
||||
kwargs.setdefault('provider', self.name)
|
||||
kwargs.setdefault('source', self.source)
|
||||
kwargs.setdefault('destination', self.destination)
|
||||
kwargs.setdefault('description', self.description)
|
||||
kwargs.setdefault('action_type', self.action_type)
|
||||
|
||||
batch = Batch(**kwargs)
|
||||
if batch.id is None: # pragma: no cover
|
||||
batch.id = sil.consume_batch_id(batch.source)
|
||||
if batch.purge is None:
|
||||
self.set_purge_date(batch)
|
||||
session.add(batch)
|
||||
session.flush()
|
||||
|
||||
|
|
@ -108,9 +112,29 @@ class BatchProvider(edbob.Object):
|
|||
return batch
|
||||
|
||||
def set_purge_date(self, batch):
|
||||
today = edbob.utc_time(naive=True).date()
|
||||
today = datetime.date.today()
|
||||
purge_offset = datetime.timedelta(days=self.purge_date_offset)
|
||||
batch.purge = today + purge_offset
|
||||
|
||||
def set_params(self, session, **params):
|
||||
pass
|
||||
|
||||
|
||||
def get_providers():
|
||||
providers = {}
|
||||
for entrypoint in pkg_resources.iter_entry_points('rattail.batches.providers'):
|
||||
providers[entrypoint.name] = entrypoint.load()
|
||||
return providers
|
||||
|
||||
|
||||
def iter_providers():
|
||||
providers = get_providers()
|
||||
return sorted(providers.itervalues(), key=lambda x: x.description)
|
||||
|
||||
|
||||
def get_provider(name):
|
||||
providers = get_providers()
|
||||
provider = providers.get(name)
|
||||
if not provider:
|
||||
raise BatchProviderNotFound(name)
|
||||
return provider()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,35 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``dtail.batches.products.labels`` -- Print Labels Batch
|
||||
``rattail.batches.providers.labels`` -- Print Labels Batch
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import object_session
|
||||
|
||||
import rattail
|
||||
from rattail.batches.providers import BatchProvider
|
||||
from rattail.db.model import Product, LabelProfile
|
||||
|
||||
|
||||
class PrintLabels(BatchProvider):
|
||||
|
|
@ -29,8 +51,8 @@ class PrintLabels(BatchProvider):
|
|||
def add_rows_begin(self, batch, data):
|
||||
session = object_session(batch)
|
||||
if not self.default_profile:
|
||||
q = session.query(rattail.LabelProfile)
|
||||
q = q.order_by(rattail.LabelProfile.ordinal)
|
||||
q = session.query(LabelProfile)
|
||||
q = q.order_by(LabelProfile.ordinal)
|
||||
self.default_profile = q.first()
|
||||
assert self.default_profile
|
||||
else:
|
||||
|
|
@ -56,18 +78,18 @@ class PrintLabels(BatchProvider):
|
|||
profiles = {}
|
||||
|
||||
cancel = False
|
||||
for i, row in enumerate(batch.iter_rows(), 1):
|
||||
for i, row in enumerate(batch.rows, 1):
|
||||
|
||||
profile = profiles.get(row.F95)
|
||||
if not profile:
|
||||
q = session.query(rattail.LabelProfile)
|
||||
q = q.filter(rattail.LabelProfile.code == row.F95)
|
||||
q = session.query(LabelProfile)
|
||||
q = q.filter(LabelProfile.code == row.F95)
|
||||
profile = q.one()
|
||||
profile.labels = []
|
||||
profiles[row.F95] = profile
|
||||
|
||||
q = session.query(rattail.Product)
|
||||
q = q.filter(rattail.Product.upc == row.F01)
|
||||
q = session.query(Product)
|
||||
q = q.filter(Product.upc == row.F01)
|
||||
product = q.one()
|
||||
|
||||
profile.labels.append((product, row.F94))
|
||||
|
|
@ -90,8 +112,8 @@ class PrintLabels(BatchProvider):
|
|||
def set_params(self, session, **params):
|
||||
profile = params.get('profile')
|
||||
if profile:
|
||||
q = session.query(rattail.LabelProfile)
|
||||
q = q.filter(rattail.LabelProfile.code == profile)
|
||||
q = session.query(LabelProfile)
|
||||
q = q.filter(LabelProfile.code == profile)
|
||||
self.default_profile = q.one()
|
||||
|
||||
quantity = params.get('quantity')
|
||||
|
|
|
|||
|
|
@ -27,23 +27,38 @@
|
|||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import edbob
|
||||
from edbob import commands
|
||||
import argparse
|
||||
import pkg_resources
|
||||
import logging
|
||||
|
||||
import rattail
|
||||
from rattail.configuration import basic_logging
|
||||
|
||||
|
||||
class Command(commands.Command):
|
||||
class ArgumentParser(argparse.ArgumentParser):
|
||||
"""
|
||||
The primary command for Rattail.
|
||||
Customized version of ``argparse.ArgumentParser``, which overrides some of
|
||||
the argument parsing logic. This is necessary for the primary command
|
||||
(:class:`Command`); but is not used with :class:`Subcommand` derivatives.
|
||||
"""
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
args, argv = self.parse_known_args(args, namespace)
|
||||
args.argv = argv
|
||||
return args
|
||||
|
||||
|
||||
class Command(rattail.Object):
|
||||
"""
|
||||
The primary console command for Rattail.
|
||||
"""
|
||||
|
||||
name = 'rattail'
|
||||
version = rattail.__version__
|
||||
description = "Retail Software Framework"
|
||||
description = "Pythonic Retail Software Framework"
|
||||
|
||||
long_description = """
|
||||
Rattail is a retail software framework.
|
||||
Rattail is a Pythonic retail software framework.
|
||||
|
||||
Copyright (c) 2010-2012 Lance Edgar <lance@edbob.org>
|
||||
|
||||
|
|
@ -52,8 +67,179 @@ and you are welcome to redistribute it under certain conditions.
|
|||
See the file COPYING.txt for more information.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
subcommands = {}
|
||||
for entrypoint in pkg_resources.iter_entry_points('%s.commands' % self.name):
|
||||
subcommands[entrypoint.name] = entrypoint.load()
|
||||
kwargs.setdefault('subcommands', subcommands)
|
||||
kwargs.setdefault('output_stream', None)
|
||||
super(Command, self).__init__(**kwargs)
|
||||
|
||||
class DatabaseSyncCommand(commands.Subcommand):
|
||||
if self.output_stream is None: # pragma: no cover
|
||||
self.output_stream = sys.stdout
|
||||
|
||||
def __repr__(self):
|
||||
return '<Command: %s>' % self.name
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.name)
|
||||
|
||||
def iter_subcommands(self):
|
||||
"""
|
||||
Generator which yields associated :class:`Subcommand` classes, ordered
|
||||
by :attr:`Subcommand.name`.
|
||||
"""
|
||||
|
||||
for name in sorted(self.subcommands):
|
||||
yield self.subcommands[name]
|
||||
|
||||
def print_help(self):
|
||||
"""
|
||||
Prints help text for the primary command, including a list of available
|
||||
subcommands.
|
||||
"""
|
||||
|
||||
stream = self.output_stream
|
||||
|
||||
stream.write("""%(description)s
|
||||
|
||||
Usage: %(name)s [options] <command> [command-options]
|
||||
|
||||
Options:
|
||||
-c PATH, --config=PATH
|
||||
Config path (may be specified more than once)
|
||||
-n, --no-init Don't load config before executing command
|
||||
-d, --debug Increase logging level to DEBUG
|
||||
-v, --verbose Increase logging level to INFO
|
||||
-V, --version Display program version and exit
|
||||
|
||||
Commands:
|
||||
""" % self)
|
||||
|
||||
for cmd in self.iter_subcommands():
|
||||
stream.write(' %-16s %s\n' % (cmd.name, cmd.description))
|
||||
|
||||
stream.write("""
|
||||
Try '%(name)s help <command>' for more help.
|
||||
""" % self)
|
||||
|
||||
def run(self, *args):
|
||||
"""
|
||||
Parses ``args`` and executes the appropriate subcommand action
|
||||
accordingly (or displays help text).
|
||||
"""
|
||||
|
||||
parser = ArgumentParser(
|
||||
prog=self.name,
|
||||
description=self.description,
|
||||
add_help=False,
|
||||
)
|
||||
|
||||
parser.add_argument('-c', '--config', action='append', dest='config_paths',
|
||||
metavar='PATH')
|
||||
parser.add_argument('-d', '--debug', action='store_true', dest='debug')
|
||||
parser.add_argument('-n', '--no-init', action='store_true', default=False)
|
||||
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose')
|
||||
parser.add_argument('-V', '--version', action='version',
|
||||
version="%%(prog)s %s" % self.version)
|
||||
parser.add_argument('command', nargs='*')
|
||||
|
||||
# Parse args and determind subcommand.
|
||||
args = parser.parse_args(list(args))
|
||||
if not args or not args.command:
|
||||
self.print_help()
|
||||
return
|
||||
|
||||
# Show (sub)command help if so instructed, or unknown subcommand.
|
||||
cmd = args.command.pop(0)
|
||||
if cmd == 'help':
|
||||
if len(args.command) != 1:
|
||||
self.print_help()
|
||||
return
|
||||
cmd = args.command[0]
|
||||
if cmd not in self.subcommands:
|
||||
self.print_help()
|
||||
return
|
||||
cmd = self.subcommands[cmd](parent=self, output_stream=self.output_stream)
|
||||
cmd.print_help()
|
||||
return
|
||||
elif cmd not in self.subcommands:
|
||||
self.print_help()
|
||||
return
|
||||
|
||||
# Initialize everything...
|
||||
if not args.no_init:
|
||||
|
||||
# Basic logging should be established before init()ing.
|
||||
basic_logging()
|
||||
log = logging.getLogger()
|
||||
if args.verbose: # pragma: no cover
|
||||
log.setLevel(logging.INFO)
|
||||
if args.debug: # pragma: no cover
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
rattail.init(*(args.config_paths or []))
|
||||
|
||||
# Command line logging flags should override config.
|
||||
if args.verbose: # pragma: no cover
|
||||
log.setLevel(logging.INFO)
|
||||
if args.debug: # pragma: no cover
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
# And finally, do something of real value...
|
||||
cmd = self.subcommands[cmd](parent=self, output_stream=self.output_stream)
|
||||
cmd._run(*(args.command + args.argv))
|
||||
|
||||
|
||||
class Subcommand(rattail.Object):
|
||||
"""
|
||||
Base class for application "subcommands." You'll want to derive from this
|
||||
class as needed to implement most of your application's command logic.
|
||||
"""
|
||||
|
||||
name = None
|
||||
description = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs.setdefault('parent', None)
|
||||
kwargs.setdefault('output_stream', None)
|
||||
super(Subcommand, self).__init__(**kwargs)
|
||||
self.parser = argparse.ArgumentParser(
|
||||
prog='%s %s' % (self.parent, self.name),
|
||||
description=self.description,
|
||||
)
|
||||
self.add_parser_args(self.parser)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Subcommand: %s>' % self.name
|
||||
|
||||
def add_parser_args(self, parser):
|
||||
"""
|
||||
If your subcommand accepts optional (or positional) arguments, you
|
||||
should override this and add the possible arguments directly to
|
||||
``parser`` (which is a ``argparse.ArgumentParser`` instance) via its
|
||||
``add_argument()`` method.
|
||||
"""
|
||||
|
||||
def print_help(self):
|
||||
self.parser.print_help(self.output_stream)
|
||||
|
||||
def _run(self, *args):
|
||||
args = self.parser.parse_args(list(args))
|
||||
return self.run(args)
|
||||
|
||||
def run(self, args):
|
||||
"""
|
||||
Runs the subcommand. You must override this method within your
|
||||
subclass. ``args`` will be a ``argparse.Namespace`` object containing
|
||||
all parsed arguments found on the original command line executed by the
|
||||
user.
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DatabaseSyncCommand(Subcommand):
|
||||
"""
|
||||
Interacts with the database synchronization service; called as ``rattail
|
||||
dbsync``.
|
||||
|
|
@ -70,7 +256,7 @@ class DatabaseSyncCommand(commands.Subcommand):
|
|||
stop = subparsers.add_parser('stop', help="Stop service")
|
||||
stop.set_defaults(subcommand='stop')
|
||||
|
||||
def run(self, args):
|
||||
def run(self, args): # pragma: win32 no cover
|
||||
from rattail.db.sync import linux as dbsync
|
||||
|
||||
if args.subcommand == 'start':
|
||||
|
|
@ -80,25 +266,104 @@ class DatabaseSyncCommand(commands.Subcommand):
|
|||
dbsync.stop_daemon()
|
||||
|
||||
|
||||
class FileMonitorCommand(commands.FileMonitorCommand):
|
||||
class FileMonitorCommand(Subcommand):
|
||||
"""
|
||||
Interacts with the file monitor service; called as ``rattail filemon``.
|
||||
This command expects a subcommand; one of the following:
|
||||
|
||||
See :class:`edbob.commands.FileMonitorCommand` for more information.
|
||||
* ``rattail filemon start``
|
||||
* ``rattail filemon stop``
|
||||
|
||||
On Windows platforms, the following additional subcommands are available:
|
||||
|
||||
* ``rattail filemon install``
|
||||
* ``rattail filemon uninstall``
|
||||
|
||||
.. note::
|
||||
The Windows Vista family of operating systems requires you to launch
|
||||
``cmd.exe`` as an Administrator in order to have sufficient rights to
|
||||
run the above commands.
|
||||
"""
|
||||
|
||||
appname = 'rattail'
|
||||
name = 'filemon'
|
||||
description = "Manage the file monitor service"
|
||||
|
||||
def get_win32_module(self):
|
||||
from rattail import filemon
|
||||
return filemon
|
||||
def add_parser_args(self, parser):
|
||||
subparsers = parser.add_subparsers(title='subcommands')
|
||||
|
||||
def get_win32_service(self):
|
||||
from rattail.filemon import RattailFileMonitor
|
||||
return RattailFileMonitor
|
||||
start = subparsers.add_parser('start', help="Start service")
|
||||
start.set_defaults(subcommand='start')
|
||||
stop = subparsers.add_parser('stop', help="Stop service")
|
||||
stop.set_defaults(subcommand='stop')
|
||||
|
||||
if sys.platform == 'win32':
|
||||
install = subparsers.add_parser('install', help="Install service")
|
||||
install.set_defaults(subcommand='install')
|
||||
install.add_argument('-a', '--auto-start', action='store_true',
|
||||
help="Configure service to start automatically")
|
||||
remove = subparsers.add_parser('remove', help="Uninstall (remove) service")
|
||||
remove.set_defaults(subcommand='remove')
|
||||
uninstall = subparsers.add_parser('uninstall', help="Uninstall (remove) service")
|
||||
uninstall.set_defaults(subcommand='remove')
|
||||
|
||||
else: # pragma: win32 no cover
|
||||
parser.add_argument('-D', '--dont-daemonize',
|
||||
action='store_false', dest='daemonize',
|
||||
help="Don't daemonize when starting")
|
||||
|
||||
def run(self, args):
|
||||
if sys.platform == 'linux2': # pragma: win32 no cover
|
||||
from rattail.filemon import linux as filemon
|
||||
|
||||
if args.subcommand == 'start':
|
||||
filemon.start_daemon(daemonize=args.daemonize)
|
||||
|
||||
elif args.subcommand == 'stop':
|
||||
filemon.stop_daemon()
|
||||
|
||||
elif sys.platform == 'win32': # pragma: no cover
|
||||
from rattail import win32
|
||||
from rattail.filemon import win32 as filemon
|
||||
|
||||
# Execute typical service command.
|
||||
options = []
|
||||
if args.subcommand == 'install' and args.auto_start:
|
||||
options = ['--startup', 'auto']
|
||||
win32.execute_service_command(filemon, args.subcommand, *options)
|
||||
|
||||
# If installing auto-start service on Windows 7, we should update
|
||||
# its startup type to be "Automatic (Delayed Start)".
|
||||
if args.subcommand == 'install' and args.auto_start:
|
||||
if platform.release() == '7':
|
||||
win32.delayed_auto_start_service('RattailFileMonitor')
|
||||
|
||||
|
||||
class LoadHostDataCommand(commands.Subcommand):
|
||||
class InitDatabaseCommand(Subcommand):
|
||||
"""
|
||||
Initializes a database.
|
||||
"""
|
||||
|
||||
name = 'initdb'
|
||||
description = "Initialize a Rattail database"
|
||||
|
||||
def run(self, args):
|
||||
from rattail import db
|
||||
from rattail.db.util import core_schema_installed, install_core_schema
|
||||
|
||||
rattail.init_modules(['rattail.db'])
|
||||
|
||||
if core_schema_installed():
|
||||
self.output_stream.write("Database has already been initialized:\n")
|
||||
self.output_stream.write(" %s\n" % str(db.engine.url))
|
||||
return
|
||||
|
||||
install_core_schema()
|
||||
|
||||
self.output_stream.write("Initialized database:\n")
|
||||
self.output_stream.write(" %s\n" % str(db.engine.url))
|
||||
|
||||
|
||||
class LoadHostDataCommand(Subcommand):
|
||||
"""
|
||||
Loads data from the Rattail host database, if one is configured.
|
||||
"""
|
||||
|
|
@ -107,28 +372,71 @@ class LoadHostDataCommand(commands.Subcommand):
|
|||
description = "Load data from host database"
|
||||
|
||||
def run(self, args):
|
||||
from edbob.console import Progress
|
||||
from rattail import db
|
||||
from rattail.db import load
|
||||
from rattail.console import Progress
|
||||
|
||||
edbob.init_modules(['edbob.db'])
|
||||
rattail.init_modules(['rattail.db'])
|
||||
host_engine = db.engines.get('host')
|
||||
|
||||
if 'host' not in edbob.engines:
|
||||
print "Host engine URL not configured."
|
||||
if not host_engine:
|
||||
self.output_stream.write("Host engine URL not configured.\n")
|
||||
return
|
||||
|
||||
proc = load.LoadProcessor()
|
||||
proc.load_all_data(edbob.engines['host'], Progress)
|
||||
proc.load_all_data(host_engine, Progress)
|
||||
|
||||
|
||||
def main(*args):
|
||||
class PurgeBatchesCommand(Subcommand):
|
||||
"""
|
||||
.. highlight:: sh
|
||||
|
||||
Purges stale batches from the database; called as::
|
||||
|
||||
rattail purge-batches
|
||||
"""
|
||||
|
||||
name = 'purge-batches'
|
||||
description = "Purge stale batches from the database"
|
||||
|
||||
def run(self, args):
|
||||
from rattail import db
|
||||
from rattail.db.batches.util import purge_batches
|
||||
|
||||
rattail.init_modules(['rattail.db'])
|
||||
session = db.Session()
|
||||
|
||||
self.output_stream.write("Purging batches from database:\n")
|
||||
self.output_stream.write(" %s\n" % str(db.engine.url))
|
||||
|
||||
purged = purge_batches(session)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
self.output_stream.write("\nPurged %d batches\n" % purged)
|
||||
|
||||
|
||||
class UUIDCommand(Subcommand):
|
||||
"""
|
||||
Command for generating an UUID; called as ``rattail uuid``.
|
||||
"""
|
||||
|
||||
name = 'uuid'
|
||||
description = "Generate an universally-unique identifier"
|
||||
|
||||
def run(self, args):
|
||||
self.output_stream.write(rattail.get_uuid())
|
||||
|
||||
|
||||
def main(*args, **kwargs):
|
||||
"""
|
||||
The primary entry point for the Rattail command system.
|
||||
"""
|
||||
|
||||
if args:
|
||||
args = list(args)
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
args = sys.argv[1:]
|
||||
|
||||
cmd = Command()
|
||||
cmd = Command(**kwargs)
|
||||
cmd.run(*args)
|
||||
|
|
|
|||
638
rattail/configuration.py
Normal file
638
rattail/configuration.py
Normal file
|
|
@ -0,0 +1,638 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.configuration`` -- Configuration Framework
|
||||
|
||||
This module provides a configuration storage interface, as well as some utility
|
||||
functions for locating default configuration file paths.
|
||||
"""
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import re
|
||||
import ConfigParser
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
from rattail.files import temp_path
|
||||
from rattail.exceptions import MissingConfiguration
|
||||
|
||||
|
||||
__all__ = ['RattailConfigParser', 'default_system_paths', 'default_user_paths']
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RattailConfigParser(ConfigParser.SafeConfigParser):
|
||||
"""
|
||||
Subclass of ``ConfigParser.SafeConfigParser`` which adds some customization
|
||||
specific to Rattail, mostly for the sake of convenience.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
ConfigParser.SafeConfigParser.__init__(self, *args, **kwargs)
|
||||
self.paths_attempted = []
|
||||
self.paths_loaded = []
|
||||
|
||||
def configure_logging(self):
|
||||
"""
|
||||
Saves the current (possibly cascaded) configuration to a temporary
|
||||
file, and passes that to ``logging.config.fileConfig()``.
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
if self.getboolean('rattail', 'basic_logging', default=False):
|
||||
basic_logging()
|
||||
if self.getboolean('rattail', 'configure_logging', default=False): # pragma: no cover
|
||||
path = temp_path('.conf')
|
||||
self.save(path)
|
||||
try:
|
||||
logging.config.fileConfig(path, disable_existing_loggers=False)
|
||||
except ConfigParser.NoSectionError:
|
||||
pass
|
||||
os.remove(path)
|
||||
if not self.getboolean('rattail', 'testing', default=False): # pragma: no cover
|
||||
log.debug("RattailConfigParser.configure_logging: configured logging")
|
||||
|
||||
def get(self, section, option, raw=None, vars=None, default=None):
|
||||
"""
|
||||
Customized version of ``ConfigParser.SafeConfigParser.get()``. This
|
||||
one adds the ``default`` keyword parameter and will return it instead
|
||||
of raising an error when ``option`` doesn't exist.
|
||||
|
||||
:param section: Section in which the option is expected to exist.
|
||||
:type section: string
|
||||
|
||||
:param option: Option whose value should be returned.
|
||||
:type option: string
|
||||
|
||||
:param raw: Whether or not to expand ``'%'`` interpolations in the
|
||||
resulting value.
|
||||
:type raw: boolean
|
||||
|
||||
:param vars: Optional dictionary in which to look for the value. If
|
||||
not provided, the configuration held in memory by the parser will be
|
||||
consulted instead.
|
||||
:type vars: dictionary
|
||||
|
||||
:param default: Default value to be returned, if the option cannot be
|
||||
found.
|
||||
:type default: string
|
||||
|
||||
:returns: The value held in memory (if found), or the value specified
|
||||
by the ``default`` parameter.
|
||||
:rtype: string, or ``None``
|
||||
"""
|
||||
|
||||
if self.has_option(section, option):
|
||||
return ConfigParser.SafeConfigParser.get(self, section, option, raw, vars)
|
||||
return default
|
||||
|
||||
def getboolean(self, section, option, default=None):
|
||||
"""
|
||||
Overriddes ``ConfigParser.SafeConfigParser.getboolean()``, to allow for
|
||||
a default.
|
||||
|
||||
:param section: Section in which the option is expected to exist.
|
||||
:type section: string
|
||||
|
||||
:param option: Option whose value should be returned.
|
||||
:type option: string
|
||||
|
||||
:param default: Default value to be returned, if the option cannot be
|
||||
found.
|
||||
:type default: boolean
|
||||
|
||||
:returns: Boolean value, or ``None`` if none could be found and no
|
||||
default is provided.
|
||||
:rtype: boolean, or ``None``
|
||||
"""
|
||||
|
||||
try:
|
||||
return ConfigParser.SafeConfigParser.getboolean(self, section, option)
|
||||
except AttributeError:
|
||||
return default
|
||||
|
||||
def get_user_dir(self, create=False, last_segment='rattail'):
|
||||
"""
|
||||
Returns the path to the "preferred" user-level configuration folder.
|
||||
The folder may be created if so desired.
|
||||
|
||||
:param create: Whether or not the folder should be created, if it
|
||||
doesn't already exist.
|
||||
:type create: boolean
|
||||
|
||||
:param last_segment: The final path segment for the folder. This
|
||||
parameter exists primarily for the sake of tests.
|
||||
:type last_segment: string
|
||||
|
||||
:returns: Path to the configuration folder.
|
||||
:rtype: string
|
||||
"""
|
||||
|
||||
if sys.platform == 'win32':
|
||||
from win32com.shell import shell, shellcon
|
||||
path = os.path.join(
|
||||
shell.SHGetSpecialFolderPath(0, shellcon.CSIDL_APPDATA),
|
||||
last_segment)
|
||||
else: # pragma: win32 no cover
|
||||
path = os.path.expanduser('~/.%s' % last_segment)
|
||||
|
||||
if create and not os.path.exists(path):
|
||||
os.mkdir(path)
|
||||
return path
|
||||
|
||||
def get_user_file(self, filename='rattail.conf', create=False):
|
||||
"""
|
||||
Returns the full path to a user-level config file location. The path
|
||||
is obtained by joining the result of :meth:`get_user_dir()` with
|
||||
``filename``.
|
||||
|
||||
:param filename: Base name for the file, including extension.
|
||||
:type filename: string
|
||||
|
||||
:param create: Whether or not the configuration folder should be
|
||||
created. This parameter is passed directly to
|
||||
:meth:`get_user_dir()`.
|
||||
:type create: boolean
|
||||
|
||||
:returns: Full path to the file.
|
||||
:rtype: string
|
||||
"""
|
||||
|
||||
return os.path.join(self.get_user_dir(create=create), filename)
|
||||
|
||||
def options(self, section):
|
||||
"""
|
||||
Overriddes ``ConfigParser.SafeConfigParser.options()``. This
|
||||
implementation returns an empty list instead of raising an exception,
|
||||
in the event the section requested doesn't exist.
|
||||
|
||||
:param section: Section whose options are to be returned.
|
||||
:type section: string
|
||||
|
||||
:returns: List of option names found within the section.
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
if self.has_section(section):
|
||||
return ConfigParser.SafeConfigParser.options(self, section)
|
||||
return []
|
||||
|
||||
def read(self, paths, recurse=True):
|
||||
r"""
|
||||
.. highlight:: ini
|
||||
|
||||
Overriddes ``ConfigParser.SafeConfigParser.read()`` by adding the
|
||||
following logic:
|
||||
|
||||
Prior to actually reading the contents of the specified file(s) into
|
||||
the current config instance, a recursive algorithm will inspect the
|
||||
config found in the file(s) to see if additional config file(s) are to
|
||||
be included. All config files, whether specified directly or
|
||||
indirectly via configuration, are finally read into the current config
|
||||
instance in the proper order so that cascading works as expected.
|
||||
|
||||
If you pass ``recurse=False`` to this method then none of the magical
|
||||
inclusion logic will happen at all.
|
||||
|
||||
Note that when a config file indicates that another file(s) is to be
|
||||
included, the referenced file will be read into this config instance
|
||||
*before* the original (primary) file is read into it. A convenient
|
||||
setup then could be to maintain a "site-wide" config file, shared on
|
||||
the network, including something like this::
|
||||
|
||||
# //file-server/rattail/site.conf
|
||||
#
|
||||
# This file contains settings relevant to all machines on the
|
||||
# network. Mail and logging configuration at least would be good
|
||||
# candidates for inclusion here.
|
||||
|
||||
[rattail.mail]
|
||||
smtp.server = mail.example.com
|
||||
# smtp.username = user
|
||||
# smtp.password = pass
|
||||
sender.default = noreply@example.com
|
||||
recipients.default = ['tech-support@example.com']
|
||||
|
||||
[loggers]
|
||||
keys = root, rattail
|
||||
|
||||
# ...etc. The bulk of logging configuration would go here.
|
||||
|
||||
Then a config file local to a particular machine could look something
|
||||
like this::
|
||||
|
||||
# C:\ProgramData\rattail\rattail.conf
|
||||
#
|
||||
# This file contains settings specific to the local machine.
|
||||
|
||||
[rattail]
|
||||
include_config = [r'\\file-server\rattail\site.conf']
|
||||
|
||||
# Add any local app config here, e.g. connection details for a
|
||||
# database, etc.
|
||||
|
||||
# All logging config is inherited from the site-wide file, except
|
||||
# we'll override the output file location so that it remains local.
|
||||
# And maybe we need the level bumped up while we troubleshoot
|
||||
# something.
|
||||
|
||||
[handler_file]
|
||||
args = (r'C:\ProgramData\rattail\log\rattail.log', 'a')
|
||||
|
||||
[logger_rattail]
|
||||
level = DEBUG
|
||||
|
||||
There is no right way to do this of course; the need should drive the
|
||||
method. Since recursion is used, there is also no real limit to how
|
||||
you go about it. A config file specific to a particular app on a
|
||||
particular machine can further include a config file specific to the
|
||||
local user on that machine, which in turn can include a file specific
|
||||
to the local machine generally, which could then include one or more
|
||||
site-wide files, etc. Or the "most specific" (initially read; primary)
|
||||
config file could indicate which other files to include for every level
|
||||
of that, in which case recursion would be less necessary (though still
|
||||
technically used).
|
||||
|
||||
:param paths: One or more configuration file paths to be read.
|
||||
:type paths: sequence or string
|
||||
|
||||
:param recurse: Flag indicating whether recursion should be used when
|
||||
reading configuration file(s).
|
||||
:type recurse: boolean
|
||||
|
||||
:returns: List of file paths which were successfully read into the
|
||||
configuration instance. Note that this list may include paths which
|
||||
were not passed to the method, in the case of recursion.
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
if isinstance(paths, basestring):
|
||||
paths = [paths]
|
||||
for path in paths:
|
||||
self.read_path(path, recurse=recurse)
|
||||
return self.paths_loaded
|
||||
|
||||
def read_path(self, path, recurse=True):
|
||||
r"""
|
||||
.. highlight:: ini
|
||||
|
||||
Reads a "single" config file into the instance. If ``recurse`` is
|
||||
``True``, *and* the config file references other "parent" config
|
||||
file(s), then the parent(s) are read also in recursive fashion.
|
||||
|
||||
Parent config file paths may be specified in this way::
|
||||
|
||||
[rattail]
|
||||
include_config = [
|
||||
r'\\file-server\rattail\site.conf',
|
||||
r'C:\ProgramData\rattail\special-stuff.conf',
|
||||
]
|
||||
|
||||
See :meth:`read()` for more information.
|
||||
|
||||
:param path: Path to the configuration file to be read.
|
||||
:type path: string
|
||||
|
||||
:param recurse: Flag indicating whether recursion should be used.
|
||||
:type recurse: boolean
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
path = os.path.abspath(path)
|
||||
if path in self.paths_attempted:
|
||||
return
|
||||
|
||||
self.paths_attempted.append(path)
|
||||
# if not self.getboolean('rattail', 'testing', default=False): # pragma: no cover
|
||||
# log.debug("RattailConfigParser.read_path: reading config file: %s" % path)
|
||||
if not os.path.exists(path):
|
||||
if not self.getboolean('rattail', 'testing', default=False): # pragma: no cover
|
||||
log.info("RattailConfigParser.read_path: file doesn't exist: %s" % path)
|
||||
return
|
||||
|
||||
here = os.path.abspath(os.path.dirname(path))
|
||||
config = ConfigParser.SafeConfigParser({'here':here})
|
||||
if not config.read(path):
|
||||
if not self.getboolean('rattail', 'testing', default=False): # pragma: no cover
|
||||
log.warning("RattailConfigParser.read_path: read failed for file: %s" % path)
|
||||
return
|
||||
|
||||
include = None
|
||||
if recurse:
|
||||
if (config.has_section('rattail') and
|
||||
config.has_option('rattail', 'include_config')):
|
||||
include = config.get('rattail', 'include_config')
|
||||
if include:
|
||||
for p in eval(include):
|
||||
self.read_path(p)
|
||||
|
||||
ConfigParser.SafeConfigParser.read(self, path)
|
||||
if include:
|
||||
self.remove_option('rattail', 'include_config')
|
||||
|
||||
self.paths_loaded.append(path)
|
||||
if not self.getboolean('rattail', 'testing', default=False): # pragma: no cover
|
||||
log.info("RattailConfigParser.read_path: successfully read file: %s" % path)
|
||||
|
||||
def read_service(self, service, paths):
|
||||
"""
|
||||
"Special" version of :meth:`read()` which will first inspect the
|
||||
file(s) for a service-specific directive, the presence of which
|
||||
indicates the *true* config file to be used for the service.
|
||||
|
||||
This method is pretty much a hack to get around certain limitations of
|
||||
Windows service implementations; it is not used otherwise.
|
||||
"""
|
||||
|
||||
if isinstance(paths, basestring):
|
||||
paths = [paths]
|
||||
|
||||
final_paths = list(paths)
|
||||
for path in paths:
|
||||
here = os.path.abspath(os.path.dirname(path))
|
||||
config = ConfigParser.SafeConfigParser({'here':here})
|
||||
if config.read(path):
|
||||
if (config.has_section('rattail.service_config')
|
||||
and config.has_option('rattail.service_config', service)):
|
||||
final_paths = config.get('rattail.service_config', service)
|
||||
|
||||
if isinstance(final_paths, basestring):
|
||||
final_paths = eval(final_paths)
|
||||
self.read(final_paths, recurse=True)
|
||||
|
||||
def require(self, section, option, raw=None, vars=None):
|
||||
"""
|
||||
Convenience method which is equivalent to :meth:`get()`, except that it
|
||||
will raise an error if the option value is not found.
|
||||
|
||||
:param section: Section in which the option is expected to exist.
|
||||
:type section: string
|
||||
|
||||
:param option: Option whose value should be returned.
|
||||
:type option: string
|
||||
|
||||
:param raw: Whether or not to expand ``'%'`` interpolations in the
|
||||
resulting value.
|
||||
:type raw: boolean
|
||||
|
||||
:param vars: Optional dictionary in which to look for the value. If
|
||||
not provided, the configuration held in memory by the parser will be
|
||||
consulted instead.
|
||||
:type vars: dictionary
|
||||
|
||||
:raises: :class:`rattail.exceptions.MissingConfiguration`, if the
|
||||
option value does not exist within the current configuration.
|
||||
|
||||
:returns: The value held in memory, if it is found.
|
||||
:rtype: string, or ``None``
|
||||
"""
|
||||
|
||||
value = self.get(section, option, raw, vars)
|
||||
if value:
|
||||
return value
|
||||
raise MissingConfiguration(section, option)
|
||||
|
||||
def save(self, path):
|
||||
"""
|
||||
Saves the current config contents to a file.
|
||||
|
||||
:param path: Path to the output file.
|
||||
:type path: string
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
f = open(path, 'w')
|
||||
self.write(f)
|
||||
f.close()
|
||||
|
||||
def set(self, section, option, value):
|
||||
"""
|
||||
Customized version of ``ConfigParser.SafeConfigParser.set()``. This
|
||||
one automatically creates the section if it doesn't already exist,
|
||||
instead of raising an error.
|
||||
|
||||
:param section: Section in which to put the option value.
|
||||
:type section: string
|
||||
|
||||
:param option: Option whose value should be set.
|
||||
:type option: string
|
||||
|
||||
:param value: Value for the option.
|
||||
:type value: string
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
if not self.has_section(section):
|
||||
self.add_section(section)
|
||||
ConfigParser.SafeConfigParser.set(self, section, option, value)
|
||||
|
||||
|
||||
class Storage(object):
|
||||
"""
|
||||
Base class for all configuration storage engines.
|
||||
|
||||
This class is designed to be a simple wrapper around a
|
||||
``ConfigParser.RawConfigParser`` (or derived,
|
||||
e.g. :class:`RattailConfigParser`) instance, hence the use of sections,
|
||||
options and values. However other implementations may exist, provided they
|
||||
adhere to the interface specified by this class.
|
||||
"""
|
||||
|
||||
def get(self, section, option, default=None):
|
||||
"""
|
||||
Retrieves a configuration value from storage.
|
||||
|
||||
:param section: The "section" from which the value should be retrieved.
|
||||
:type section: string
|
||||
|
||||
:param option: The "option" whose value should be retrieved.
|
||||
:type option: string
|
||||
|
||||
:param default: Optional default value to be returned, should the
|
||||
requested value not be found in storage.
|
||||
:type default: string
|
||||
|
||||
:returns: The configuration value as found in storage, or the value
|
||||
specified by the ``default`` parameter.
|
||||
:rtype: string, or ``None``
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def put(self, section, option, value):
|
||||
"""
|
||||
Writes a configuration value to storage.
|
||||
|
||||
:param section: The "section" under which the value should be written.
|
||||
:type section: string
|
||||
|
||||
:param option: The "option" whose value should be written.
|
||||
:type option: string
|
||||
|
||||
:param value: The value to be written.
|
||||
:type value: string
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class UserConfigFileStorage(Storage):
|
||||
"""
|
||||
:class:`Storage` implementation which (by default) uses a configuration
|
||||
file local to the current user's "home" folder.
|
||||
"""
|
||||
|
||||
def __init__(self, config_path=None, **kwargs):
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
:param config_path: Optional path to the configuration file to be used.
|
||||
If this is not specified, a default will be provided which exists in
|
||||
the user's home folder. :meth:`RattailConfigParser.get_user_file()`
|
||||
will be invoked to determine the default path.
|
||||
:type config_path: string
|
||||
|
||||
:param \*\*kwargs: Additional keyword arguments are currently used only
|
||||
for testing purposes.
|
||||
"""
|
||||
|
||||
self.config_parser = RattailConfigParser()
|
||||
if kwargs.get('testing'):
|
||||
self.config_parser.set('rattail', 'testing', 'True')
|
||||
self.config_path = config_path
|
||||
if not self.config_path:
|
||||
self.config_path = self.config_parser.get_user_file('rattail.conf')
|
||||
self.config_parser.read(self.config_path)
|
||||
|
||||
def get(self, section, option, default=None):
|
||||
return self.config_parser.get(section, option, default=default)
|
||||
|
||||
def put(self, section, option, value):
|
||||
self.config_parser.set(section, option, value)
|
||||
self.config_parser.save(self.config_path)
|
||||
|
||||
|
||||
def basic_logging():
|
||||
"""
|
||||
Does some basic configuration on the root logger.
|
||||
|
||||
:returns: ``None``
|
||||
|
||||
.. note::
|
||||
This only enables console output at this point; it is assumed that if
|
||||
you intend to "truly" configure logging that you will be using a proper
|
||||
config file and calling :func:`rattail.init()`.
|
||||
"""
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter(
|
||||
'%(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s'))
|
||||
root = logging.getLogger()
|
||||
root.addHandler(handler)
|
||||
|
||||
|
||||
def default_system_paths():
|
||||
r"""
|
||||
Returns a list of default system-level config file paths.
|
||||
|
||||
:returns: List of strings, each of which is a potential config file path.
|
||||
:rtype: list
|
||||
|
||||
The following paths will be returned, according to ``sys.platform``.
|
||||
|
||||
``win32``:
|
||||
* ``<COMMON_APPDATA>\rattail.conf``
|
||||
* ``<COMMON_APPDATA>\rattail\rattail.conf``
|
||||
|
||||
Any other platform:
|
||||
* ``/etc/rattail.conf``
|
||||
* ``/etc/rattail/rattail.conf``
|
||||
* ``/usr/local/etc/rattail.conf``
|
||||
* ``/usr/local/etc/rattail/rattail.conf``
|
||||
"""
|
||||
|
||||
if sys.platform == 'win32':
|
||||
from win32com.shell import shell, shellcon
|
||||
paths = [
|
||||
os.path.join(shell.SHGetSpecialFolderPath(
|
||||
0, shellcon.CSIDL_COMMON_APPDATA), 'rattail.conf'),
|
||||
os.path.join(shell.SHGetSpecialFolderPath(
|
||||
0, shellcon.CSIDL_COMMON_APPDATA), 'rattail', 'rattail.conf'),
|
||||
]
|
||||
|
||||
else: # pragma: win32 no cover
|
||||
paths = [
|
||||
'/etc/rattail.conf',
|
||||
'/etc/rattail/rattail.conf',
|
||||
'/usr/local/etc/rattail.conf',
|
||||
'/usr/local/etc/rattail/rattail.conf',
|
||||
]
|
||||
|
||||
return paths
|
||||
|
||||
|
||||
def default_user_paths():
|
||||
r"""
|
||||
Returns a list of default user-level config file paths.
|
||||
|
||||
:returns: List of strings, each of which is a potential config file path.
|
||||
:rtype: list
|
||||
|
||||
The following paths will be returned, according to ``sys.platform``:
|
||||
|
||||
``win32``:
|
||||
* ``<APPDATA>\rattail.conf``
|
||||
* ``<APPDATA>\rattail\rattail.conf``
|
||||
|
||||
Any other platform:
|
||||
* ``~/.rattail.conf``
|
||||
* ``~/.rattail/rattail.conf``
|
||||
"""
|
||||
|
||||
if sys.platform == 'win32':
|
||||
from win32com.shell import shell, shellcon
|
||||
paths = [
|
||||
os.path.join(shell.SHGetSpecialFolderPath(
|
||||
0, shellcon.CSIDL_APPDATA), 'rattail.conf'),
|
||||
os.path.join(shell.SHGetSpecialFolderPath(
|
||||
0, shellcon.CSIDL_APPDATA), 'rattail', 'rattail.conf'),
|
||||
]
|
||||
|
||||
else: # pragma: win32 no cover
|
||||
paths = [
|
||||
os.path.expanduser('~/.rattail.conf'),
|
||||
os.path.expanduser('~/.rattail/rattail.conf'),
|
||||
]
|
||||
|
||||
return paths
|
||||
|
|
@ -23,29 +23,26 @@
|
|||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.batches.exceptions`` -- Batch Exceptions
|
||||
``rattail.console`` -- Console-Specific Stuff
|
||||
"""
|
||||
|
||||
|
||||
class BatchError(Exception):
|
||||
|
||||
pass
|
||||
import sys
|
||||
import progressbar
|
||||
|
||||
|
||||
class BatchProviderNotFound(BatchError):
|
||||
class Progress(object):
|
||||
"""
|
||||
Provides a console-based progress bar.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
def __init__(self, message, maximum):
|
||||
sys.stderr.write("\n%s...(%u total)\n" % (message, maximum))
|
||||
widgets = [progressbar.Percentage(), ' ', progressbar.Bar(), ' ', progressbar.ETA()]
|
||||
self.progress = progressbar.ProgressBar(maxval=maximum, widgets=widgets).start()
|
||||
|
||||
def __str__(self):
|
||||
return "Batch provider not found: %s" % self.name
|
||||
def update(self, value):
|
||||
self.progress.update(value)
|
||||
return True
|
||||
|
||||
|
||||
class BatchDestinationNotSupported(BatchError):
|
||||
|
||||
def __init__(self, batch):
|
||||
self.batch = batch
|
||||
|
||||
def __str__(self):
|
||||
return "Destination '%s' not supported for batch: %s" % (
|
||||
self.batch.destination, self.batch.description)
|
||||
def destroy(self):
|
||||
sys.stderr.write("\n")
|
||||
135
rattail/daemon.py
Normal file
135
rattail/daemon.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# This code was stolen from:
|
||||
# http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
|
||||
|
||||
import sys, os, time, atexit
|
||||
from signal import SIGTERM
|
||||
|
||||
class Daemon:
|
||||
"""
|
||||
A generic daemon class.
|
||||
|
||||
Usage: subclass the Daemon class and override the run() method
|
||||
"""
|
||||
def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
||||
self.stdin = stdin
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
self.pidfile = pidfile
|
||||
|
||||
def daemonize(self):
|
||||
"""
|
||||
do the UNIX double-fork magic, see Stevens' "Advanced
|
||||
Programming in the UNIX Environment" for details (ISBN 0201563177)
|
||||
http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
|
||||
"""
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
# exit first parent
|
||||
sys.exit(0)
|
||||
except OSError, e:
|
||||
sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
|
||||
sys.exit(1)
|
||||
|
||||
# decouple from parent environment
|
||||
os.chdir("/")
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
|
||||
# do second fork
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
# exit from second parent
|
||||
sys.exit(0)
|
||||
except OSError, e:
|
||||
sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
|
||||
sys.exit(1)
|
||||
|
||||
# redirect standard file descriptors
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
si = file(self.stdin, 'r')
|
||||
so = file(self.stdout, 'a+')
|
||||
se = file(self.stderr, 'a+', 0)
|
||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||
|
||||
# write pidfile
|
||||
atexit.register(self.delpid)
|
||||
pid = str(os.getpid())
|
||||
file(self.pidfile,'w+').write("%s\n" % pid)
|
||||
|
||||
def delpid(self):
|
||||
os.remove(self.pidfile)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start the daemon
|
||||
"""
|
||||
# Check for a pidfile to see if the daemon already runs
|
||||
try:
|
||||
pf = file(self.pidfile,'r')
|
||||
pid = int(pf.read().strip())
|
||||
pf.close()
|
||||
except IOError:
|
||||
pid = None
|
||||
|
||||
if pid:
|
||||
message = "pidfile %s already exist. Daemon already running?\n"
|
||||
sys.stderr.write(message % self.pidfile)
|
||||
sys.exit(1)
|
||||
|
||||
# Start the daemon
|
||||
self.daemonize()
|
||||
self.run()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop the daemon
|
||||
"""
|
||||
# Get the pid from the pidfile
|
||||
try:
|
||||
pf = file(self.pidfile,'r')
|
||||
pid = int(pf.read().strip())
|
||||
pf.close()
|
||||
except IOError:
|
||||
pid = None
|
||||
|
||||
if not pid:
|
||||
message = "pidfile %s does not exist. Daemon not running?\n"
|
||||
sys.stderr.write(message % self.pidfile)
|
||||
return # not an error in a restart
|
||||
|
||||
# Try killing the daemon process
|
||||
try:
|
||||
while 1:
|
||||
os.kill(pid, SIGTERM)
|
||||
time.sleep(0.1)
|
||||
except OSError, err:
|
||||
err = str(err)
|
||||
if err.find("No such process") > 0:
|
||||
if os.path.exists(self.pidfile):
|
||||
os.remove(self.pidfile)
|
||||
else:
|
||||
print str(err)
|
||||
sys.exit(1)
|
||||
|
||||
def restart(self):
|
||||
"""
|
||||
Restart the daemon
|
||||
"""
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
You should override this method when you subclass Daemon. It will be called after the process has been
|
||||
daemonized by start() or restart().
|
||||
"""
|
||||
|
|
@ -26,122 +26,20 @@
|
|||
``rattail.db`` -- Database Stuff
|
||||
"""
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from sqlalchemy import types
|
||||
from sqlalchemy.event import listen
|
||||
from sqlalchemy.orm import object_mapper, RelationshipProperty
|
||||
|
||||
import edbob
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
import rattail
|
||||
from rattail.gpc import GPC
|
||||
from rattail.db import changes
|
||||
from rattail.exceptions import NoDefaultDatabase
|
||||
from rattail.modules import graft
|
||||
|
||||
|
||||
ignore_role_changes = None
|
||||
__all__ = ['engines', 'engine', 'Session']
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GPCType(types.TypeDecorator):
|
||||
"""
|
||||
SQLAlchemy type engine for GPC data.
|
||||
"""
|
||||
|
||||
impl = types.BigInteger
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is None:
|
||||
return None
|
||||
return int(value)
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is None:
|
||||
return None
|
||||
return GPC(value)
|
||||
|
||||
|
||||
def before_flush(session, flush_context, instances):
|
||||
"""
|
||||
Listens for session flush events. This function is responsible for adding
|
||||
stub records to the 'changes' table, which will in turn be used by the
|
||||
database synchronizer.
|
||||
"""
|
||||
|
||||
def record_change(instance, deleted=False):
|
||||
|
||||
# No need to record changes for Change. :)
|
||||
if isinstance(instance, rattail.Change):
|
||||
return
|
||||
|
||||
# No need to record changes for batch data.
|
||||
if isinstance(instance, (rattail.Batch,
|
||||
rattail.BatchColumn,
|
||||
rattail.BatchRow)):
|
||||
return
|
||||
|
||||
# Ignore instances which don't use UUID.
|
||||
if not hasattr(instance, 'uuid'):
|
||||
return
|
||||
|
||||
# Ignore Role instances, if so configured.
|
||||
if ignore_role_changes:
|
||||
if isinstance(instance, (edbob.Role, edbob.UserRole)):
|
||||
return
|
||||
|
||||
# Provide an UUID value, if necessary.
|
||||
if not instance.uuid:
|
||||
|
||||
# If the 'uuid' column is actually a foreign key to another
|
||||
# table...well, then we can't just generate a new value for it.
|
||||
# Instead we must traverse the relationship and fetch the existing
|
||||
# foreign key value...
|
||||
|
||||
mapper = object_mapper(instance)
|
||||
uuid = mapper.columns['uuid']
|
||||
if uuid.foreign_keys:
|
||||
|
||||
for prop in mapper.iterate_properties:
|
||||
if (isinstance(prop, RelationshipProperty)
|
||||
and len(prop.remote_side) == 1
|
||||
and prop.remote_side[0].key == 'uuid'):
|
||||
|
||||
instance.uuid = getattr(instance, prop.key).uuid
|
||||
break
|
||||
assert instance.uuid
|
||||
|
||||
# ...but if there is no foreign key, just generate a new UUID.
|
||||
else:
|
||||
instance.uuid = edbob.get_uuid()
|
||||
|
||||
# Record the change.
|
||||
change = session.query(rattail.Change).get(
|
||||
(instance.__class__.__name__, instance.uuid))
|
||||
if not change:
|
||||
change = rattail.Change(
|
||||
class_name=instance.__class__.__name__,
|
||||
uuid=instance.uuid)
|
||||
session.add(change)
|
||||
change.deleted = deleted
|
||||
log.debug("before_flush: recorded change: %s" % repr(change))
|
||||
|
||||
for instance in session.deleted:
|
||||
log.debug("before_flush: deleted instance: %s" % repr(instance))
|
||||
record_change(instance, deleted=True)
|
||||
|
||||
for instance in session.new:
|
||||
log.debug("before_flush: new instance: %s" % repr(instance))
|
||||
record_change(instance)
|
||||
|
||||
for instance in session.dirty:
|
||||
if session.is_modified(instance, passive=True):
|
||||
log.debug("before_flush: dirty instance: %s" % repr(instance))
|
||||
record_change(instance)
|
||||
|
||||
|
||||
def record_changes(session):
|
||||
listen(session, 'before_flush', before_flush)
|
||||
engines = {}
|
||||
engine = None
|
||||
Session = sessionmaker()
|
||||
|
||||
|
||||
def init(config):
|
||||
|
|
@ -149,18 +47,38 @@ def init(config):
|
|||
Initialize the Rattail database framework.
|
||||
"""
|
||||
|
||||
from rattail.db.extension import model
|
||||
edbob.graft(rattail, model)
|
||||
global engine
|
||||
|
||||
global ignore_role_changes
|
||||
ignore_role_changes = config.getboolean(
|
||||
keys = config.require('rattail.db', 'engines')
|
||||
keys = keys.split(',')
|
||||
keys = [x.strip() for x in keys]
|
||||
|
||||
# Determine the default database.
|
||||
if 'default' in keys:
|
||||
default = 'default'
|
||||
elif len(keys) == 1:
|
||||
default = keys[0]
|
||||
else:
|
||||
raise NoDefaultDatabase()
|
||||
|
||||
# Instantiate all database engines.
|
||||
cfg = dict(config.items('rattail.db'))
|
||||
for key in keys:
|
||||
engines[key] = engine_from_config(cfg, '%s.' % key)
|
||||
|
||||
# Use the default database.
|
||||
engine = engines[default]
|
||||
Session.configure(bind=engine)
|
||||
|
||||
# Bring all model classes into the root namespace.
|
||||
from rattail.db import model
|
||||
graft(rattail, model)
|
||||
|
||||
# from edbob.db.extensions import extend_framework
|
||||
# extend_framework()
|
||||
|
||||
# Maybe record data changes.
|
||||
changes.ignore_role_changes = config.getboolean(
|
||||
'rattail.db', 'changes.ignore_roles', default=True)
|
||||
|
||||
if config.getboolean('rattail.db', 'changes.record'):
|
||||
record_changes(edbob.Session)
|
||||
|
||||
elif config.getboolean('rattail.db', 'record_changes'):
|
||||
warnings.warn("Config setting 'record_changes' in section [rattail.db] "
|
||||
"is deprecated; please use 'changes.record' instead.",
|
||||
DeprecationWarning)
|
||||
record_changes(edbob.Session)
|
||||
changes.record_changes(Session)
|
||||
|
|
|
|||
160
rattail/db/auth.py
Normal file
160
rattail/db/auth.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.db.auth`` -- Authentication & Authorization
|
||||
"""
|
||||
|
||||
import sys
|
||||
import bcrypt
|
||||
|
||||
from sqlalchemy.orm import object_session
|
||||
|
||||
from rattail.db.model import Role, User
|
||||
|
||||
|
||||
class BcryptAuthenticator(object):
|
||||
"""
|
||||
Authentication with py-bcrypt (Blowfish).
|
||||
"""
|
||||
|
||||
def populate_user(self, user, password):
|
||||
user.salt = bcrypt.gensalt()
|
||||
user.password = bcrypt.hashpw(password, user.salt)
|
||||
|
||||
def authenticate_user(self, user, password):
|
||||
return bcrypt.hashpw(password, user.salt) == user.password
|
||||
|
||||
|
||||
def authenticate_user(session, username, password):
|
||||
"""
|
||||
Attempts to authenticate with ``username`` and ``password``. If
|
||||
successful, returns the :class:`rattail.db.model.User` instance; otherwise
|
||||
returns ``None``.
|
||||
"""
|
||||
|
||||
q = session.query(User)
|
||||
q = q.filter(User.username == username)
|
||||
user = q.first()
|
||||
if user:
|
||||
auth = BcryptAuthenticator()
|
||||
if auth.authenticate_user(user, password):
|
||||
return user
|
||||
|
||||
|
||||
def administrator_role(session):
|
||||
"""
|
||||
Returns the "Administrator" :class:`rattail.db.model.Role` instance,
|
||||
attached to the given ``session``.
|
||||
"""
|
||||
|
||||
uuid = 'd937fa8a965611dfa0dd001143047286'
|
||||
admin = session.query(Role).get(uuid)
|
||||
if admin:
|
||||
return admin
|
||||
admin = Role(uuid=uuid, name='Administrator')
|
||||
session.add(admin)
|
||||
return admin
|
||||
|
||||
|
||||
def guest_role(session):
|
||||
"""
|
||||
Returns the "Guest" :class:`rattail.db.model.Role` instance, attached to
|
||||
the given ``session``.
|
||||
"""
|
||||
|
||||
uuid = 'f8a27c98965a11dfaff7001143047286'
|
||||
guest = session.query(Role).get(uuid)
|
||||
if guest:
|
||||
return guest
|
||||
guest = Role(uuid=uuid, name='Guest')
|
||||
session.add(guest)
|
||||
return guest
|
||||
|
||||
|
||||
def grant_permission(session, role, permission):
|
||||
"""
|
||||
Grants ``permission`` to ``role``.
|
||||
"""
|
||||
|
||||
if permission not in role.permissions:
|
||||
role.permissions.append(permission)
|
||||
|
||||
|
||||
def has_permission(session, obj, perm, include_guest=True):
|
||||
"""
|
||||
Checks the given ``obj`` (which may be either a
|
||||
:class:`rattail.db.model.User`` or :class:`rattail.db.model.Role`
|
||||
instance), and returns a boolean indicating whether or not the object is
|
||||
allowed the given permission. ``perm`` should be a fully-qualified
|
||||
permission name, e.g. ``'users.create'``.
|
||||
"""
|
||||
|
||||
if isinstance(obj, User):
|
||||
roles = list(obj.roles)
|
||||
elif isinstance(obj, Role):
|
||||
roles = [obj]
|
||||
elif obj is None:
|
||||
roles = []
|
||||
else:
|
||||
raise TypeError("You must pass either a User or Role for 'obj'; got: %s" % repr(obj))
|
||||
if include_guest:
|
||||
roles.append(guest_role(session))
|
||||
admin = administrator_role(session)
|
||||
for role in roles:
|
||||
if role is admin:
|
||||
return True
|
||||
for permission in role.permissions:
|
||||
if permission == perm:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def init_database(session, output_stream=None):
|
||||
"""
|
||||
Initialize the auth system within a Rattail database.
|
||||
|
||||
Currently this only creates an :class:`rattail.db.model.User` instance with
|
||||
username ``'admin'`` (and password the same), and assigns the user to the
|
||||
built-in administrative role (see :func:`administrator_role()`).
|
||||
"""
|
||||
|
||||
if output_stream is None: # pragma: no cover
|
||||
output_stream = sys.stdout
|
||||
|
||||
admin = User(username='admin')
|
||||
set_user_password(admin, 'admin')
|
||||
admin.roles.append(administrator_role(session))
|
||||
session.add(admin)
|
||||
session.flush()
|
||||
output_stream.write("Created 'admin' user with password 'admin'\n")
|
||||
|
||||
|
||||
def set_user_password(user, password):
|
||||
"""
|
||||
Sets the password for the given :class:`rattail.db.model.User` instance.
|
||||
"""
|
||||
|
||||
auth = BcryptAuthenticator()
|
||||
auth.populate_user(user, password)
|
||||
|
|
@ -23,19 +23,10 @@
|
|||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.sil.exceptions`` -- SIL Exceptions
|
||||
``rattail.db.batches`` -- Batch API
|
||||
"""
|
||||
|
||||
|
||||
class SILError(Exception):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class SILColumnNotFound(SILError):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
return "SIL column not found: %s" % self.name
|
||||
from rattail.db.batches.types import *
|
||||
from rattail.db.batches.data import *
|
||||
from rattail.db.batches.makers import *
|
||||
from rattail.db.batches.executors import *
|
||||
71
rattail/db/batches/data.py
Normal file
71
rattail/db/batches/data.py
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.db.batches.data`` -- Batch Data Providers
|
||||
"""
|
||||
|
||||
|
||||
class BatchDataProvider(object):
|
||||
|
||||
def __len__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __iter__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class QueryDataProvider(BatchDataProvider):
|
||||
|
||||
def __init__(self, query):
|
||||
self.query = query
|
||||
|
||||
def __len__(self):
|
||||
return self.query.count()
|
||||
|
||||
|
||||
class ProductDataProxy(object):
|
||||
|
||||
def __init__(self, product):
|
||||
self.product = product
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name == 'F01':
|
||||
return self.product.upc
|
||||
if name == 'F02':
|
||||
return self.product.description
|
||||
if name == 'F22':
|
||||
return self.product.size
|
||||
if name == 'F155':
|
||||
if self.product.brand:
|
||||
return self.product.brand.name
|
||||
return ''
|
||||
raise AttributeError("Product has no attribute '%s'" % name)
|
||||
|
||||
|
||||
class ProductQueryDataProvider(QueryDataProvider):
|
||||
|
||||
def __iter__(self):
|
||||
for product in self.query:
|
||||
yield ProductDataProxy(product)
|
||||
93
rattail/db/batches/executors.py
Normal file
93
rattail/db/batches/executors.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.db.batches.executors`` -- Batch Executors
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import object_session
|
||||
|
||||
from rattail.db.model import LabelProfile, Product
|
||||
from rattail.exceptions import BatchTypeNotSupported
|
||||
|
||||
|
||||
__all__ = ['BatchExecutor']
|
||||
|
||||
|
||||
class BatchExecutor(object):
|
||||
|
||||
batch_type = None
|
||||
|
||||
def execute(self, batch, progress=None):
|
||||
if batch.type != self.batch_type:
|
||||
raise BatchTypeNotSupported(self, batch.type)
|
||||
session = object_session(batch)
|
||||
return self.execute_batch(session, batch, progress)
|
||||
|
||||
def execute_batch(self, session, batch, progress=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class LabelsBatchExecutor(BatchExecutor):
|
||||
|
||||
batch_type = 'labels'
|
||||
|
||||
def execute_batch(self, session, batch, progress=None):
|
||||
prog = None
|
||||
if progress:
|
||||
prog = progress("Loading product data", batch.rowcount)
|
||||
|
||||
profiles = {}
|
||||
|
||||
cancel = False
|
||||
for i, row in enumerate(batch.rows, 1):
|
||||
|
||||
profile = profiles.get(row.F95)
|
||||
if not profile:
|
||||
q = session.query(LabelProfile)
|
||||
q = q.filter(LabelProfile.code == row.F95)
|
||||
profile = q.one()
|
||||
profile.labels = []
|
||||
profiles[row.F95] = profile
|
||||
|
||||
q = session.query(Product)
|
||||
q = q.filter(Product.upc == row.F01)
|
||||
product = q.one()
|
||||
|
||||
profile.labels.append((product, row.F94))
|
||||
if prog and not prog.update(i):
|
||||
cancel = True
|
||||
break
|
||||
|
||||
if not cancel:
|
||||
for profile in profiles.itervalues():
|
||||
printer = profile.get_printer()
|
||||
assert printer
|
||||
if not printer.print_labels(profile.labels):
|
||||
cancel = True
|
||||
break
|
||||
|
||||
if prog:
|
||||
prog.destroy()
|
||||
return not cancel
|
||||
120
rattail/db/batches/makers.py
Normal file
120
rattail/db/batches/makers.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.db.batches.makers`` -- Batch Makers
|
||||
"""
|
||||
|
||||
from rattail.db.batches.types import get_batch_type
|
||||
from rattail.db.batches.data import BatchDataProvider
|
||||
from rattail.db.model import Batch, LabelProfile
|
||||
|
||||
|
||||
class BatchMaker(object):
|
||||
|
||||
progress_message = "Making batch(es)"
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
self.batches = {}
|
||||
|
||||
def get_batch(self, name):
|
||||
if name not in self.batches:
|
||||
self.batches[name] = self.make_batch(name)
|
||||
return self.batches[name]
|
||||
|
||||
def make_batch(self, name):
|
||||
if hasattr(self, 'make_batch_%s' % name):
|
||||
batch = getattr(self, 'make_batch_%s' % name)()
|
||||
else:
|
||||
batch_type = get_batch_type(name)
|
||||
batch = Batch()
|
||||
batch_type.initialize(batch)
|
||||
self.session.add(batch)
|
||||
self.session.flush()
|
||||
batch.create_table()
|
||||
return batch
|
||||
|
||||
def make_batches_begin(self, data):
|
||||
pass
|
||||
|
||||
def make_batches(self, data, progress=None):
|
||||
if not isinstance(data, BatchDataProvider):
|
||||
raise TypeError("Sorry, you must pass a BatchDataProvider instance")
|
||||
|
||||
result = self.make_batches_begin(data)
|
||||
if result is not None and not result:
|
||||
return False
|
||||
|
||||
prog = None
|
||||
if progress and len(data):
|
||||
prog = progress(self.progress_message, len(data))
|
||||
|
||||
cancel = False
|
||||
for i, data_row in enumerate(data, 1):
|
||||
self.process_data_row(data_row)
|
||||
if prog and not prog.update(i):
|
||||
cancel = True
|
||||
break
|
||||
self.session.flush()
|
||||
|
||||
if prog:
|
||||
prog.destroy()
|
||||
|
||||
if not cancel:
|
||||
result = self.make_batches_end()
|
||||
if result is not None:
|
||||
cancel = not result
|
||||
|
||||
return not cancel
|
||||
|
||||
def make_batches_end(self):
|
||||
pass
|
||||
|
||||
def process_data_row(self, data_row):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class LabelsBatchMaker(BatchMaker):
|
||||
|
||||
default_profile = None
|
||||
default_quantity = 1
|
||||
|
||||
def make_batches_begin(self, data):
|
||||
if not self.default_profile:
|
||||
q = self.session.query(LabelProfile)
|
||||
q = q.order_by(LabelProfile.ordinal)
|
||||
self.default_profile = q.first()
|
||||
assert self.default_profile
|
||||
|
||||
def process_data_row(self, data_row):
|
||||
batch = self.get_batch('labels')
|
||||
row = batch.rowclass()
|
||||
row.F01 = data_row.F01
|
||||
row.F155 = data_row.F155
|
||||
row.F02 = data_row.F02
|
||||
row.F22 = data_row.F22
|
||||
row.F95 = self.default_profile.code
|
||||
row.F94 = self.default_quantity
|
||||
batch.add_row(row)
|
||||
89
rattail/db/batches/types.py
Normal file
89
rattail/db/batches/types.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.db.batches.types`` -- Batch Types
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import pkg_resources
|
||||
|
||||
from rattail.time import local_time
|
||||
from rattail.exceptions import BatchTypeNotFound
|
||||
|
||||
|
||||
batch_types = None
|
||||
|
||||
|
||||
def get_batch_type(name):
|
||||
global batch_types
|
||||
if batch_types is None:
|
||||
batch_types = {}
|
||||
for entrypoint in pkg_resources.iter_entry_points('rattail.batches.types'):
|
||||
batch_types[entrypoint.name] = entrypoint.load()
|
||||
if name in batch_types:
|
||||
return batch_types[name]()
|
||||
raise BatchTypeNotFound(name)
|
||||
|
||||
|
||||
class BatchType(object):
|
||||
|
||||
name = None
|
||||
description = None
|
||||
source = None
|
||||
destination = None
|
||||
action_type = None
|
||||
|
||||
purge_date_offset = None
|
||||
|
||||
def initialize(self, batch):
|
||||
batch.description = self.description
|
||||
batch.source = self.source
|
||||
batch.destination = self.destination
|
||||
batch.action_type = self.action_type
|
||||
self.set_purge_date(batch)
|
||||
self.add_columns(batch)
|
||||
|
||||
def set_purge_date(self, batch):
|
||||
if self.purge_date_offset is not None:
|
||||
today = local_time().date()
|
||||
purge_offset = datetime.timedelta(days=self.purge_date_offset)
|
||||
batch.purge = today + purge_offset
|
||||
|
||||
def add_columns(self, batch):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class LabelsBatchType(BatchType):
|
||||
|
||||
name = 'labels'
|
||||
description = "Print Labels"
|
||||
|
||||
def add_columns(self, batch):
|
||||
batch.add_column('F01')
|
||||
batch.add_column('F155')
|
||||
batch.add_column('F02')
|
||||
batch.add_column('F22', display_name="Size")
|
||||
batch.add_column('F95', display_name="Label")
|
||||
batch.add_column('F94', display_name="Quantity")
|
||||
91
rattail/db/batches/util.py
Normal file
91
rattail/db/batches/util.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.db.batches.util`` -- Batch Utilities
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from sqlalchemy import MetaData
|
||||
|
||||
import rattail
|
||||
from rattail.db.model import Batch
|
||||
from rattail.time import local_time
|
||||
|
||||
|
||||
def purge_batches(session, effective_date=None):
|
||||
"""
|
||||
Purge batches from the database which are considered old.
|
||||
|
||||
:param session: Active database session.
|
||||
:type session: ``sqlalchemy.orm.Session`` instance
|
||||
|
||||
:param effective_date: Date against which comparisons should be made when
|
||||
determining if a batch is "old" (based on its
|
||||
:attr:`rattail.db.model.Batch.purge` attribute). If no value is
|
||||
provided, then :func:`rattail.time.local_time()` is called to determine
|
||||
the current date.
|
||||
:type effective_date: ``datetime.date`` instance, or ``None``
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
if effective_date is None:
|
||||
rattail.init_modules(['rattail.time'])
|
||||
effective_date = local_time().date()
|
||||
|
||||
purged = 0
|
||||
|
||||
q = session.query(Batch)
|
||||
q = q.filter(Batch.purge != None)
|
||||
q = q.filter(Batch.purge < effective_date)
|
||||
for batch in q:
|
||||
batch.drop_table()
|
||||
session.delete(batch)
|
||||
session.flush()
|
||||
purged += 1
|
||||
|
||||
# This should theoretically not be necessary, if/when the batch processing
|
||||
# is cleaning up after itself properly. For now though, it seems that
|
||||
# orphaned data tables are sometimes being left behind.
|
||||
|
||||
batch_pattern = re.compile(r'^batch\.[0-9a-f]{32}$')
|
||||
|
||||
current_batches = []
|
||||
for batch in session.query(Batch):
|
||||
current_batches.append('batch.%s' % batch.uuid)
|
||||
|
||||
def orphaned_batches(name, metadata):
|
||||
if batch_pattern.match(name):
|
||||
if name not in current_batches:
|
||||
return True
|
||||
return False
|
||||
|
||||
metadata = MetaData(session.bind)
|
||||
metadata.reflect(only=orphaned_batches)
|
||||
for table in reversed(metadata.sorted_tables):
|
||||
table.drop()
|
||||
|
||||
return purged
|
||||
|
|
@ -26,21 +26,18 @@
|
|||
``rattail.db.cache`` -- Cache Helpers
|
||||
"""
|
||||
|
||||
import edbob
|
||||
from edbob.util import requires_impl
|
||||
|
||||
import rattail
|
||||
from rattail.util import Object
|
||||
from rattail.db import model
|
||||
|
||||
|
||||
class DataCacher(edbob.Object):
|
||||
class DataCacher(Object):
|
||||
|
||||
def __init__(self, session=None, **kwargs):
|
||||
super(DataCacher, self).__init__(session=session, **kwargs)
|
||||
|
||||
@property
|
||||
@requires_impl(is_property=True)
|
||||
def class_(self):
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
@ -76,7 +73,7 @@ class DataCacher(edbob.Object):
|
|||
|
||||
class DepartmentCacher(DataCacher):
|
||||
|
||||
class_ = rattail.Department
|
||||
class_ = model.Department
|
||||
|
||||
def cache_instance(self, dept):
|
||||
self.instances[dept.number] = dept
|
||||
|
|
@ -84,7 +81,7 @@ class DepartmentCacher(DataCacher):
|
|||
|
||||
class SubdepartmentCacher(DataCacher):
|
||||
|
||||
class_ = rattail.Subdepartment
|
||||
class_ = model.Subdepartment
|
||||
|
||||
def cache_instance(self, subdept):
|
||||
self.instances[subdept.number] = subdept
|
||||
|
|
@ -92,7 +89,7 @@ class SubdepartmentCacher(DataCacher):
|
|||
|
||||
class BrandCacher(DataCacher):
|
||||
|
||||
class_ = rattail.Brand
|
||||
class_ = model.Brand
|
||||
|
||||
def cache_instance(self, brand):
|
||||
self.instances[brand.name] = brand
|
||||
|
|
@ -100,7 +97,7 @@ class BrandCacher(DataCacher):
|
|||
|
||||
class VendorCacher(DataCacher):
|
||||
|
||||
class_ = rattail.Vendor
|
||||
class_ = model.Vendor
|
||||
|
||||
def cache_instance(self, vend):
|
||||
self.instances[vend.id] = vend
|
||||
|
|
@ -108,7 +105,7 @@ class VendorCacher(DataCacher):
|
|||
|
||||
class ProductCacher(DataCacher):
|
||||
|
||||
class_ = rattail.Product
|
||||
class_ = model.Product
|
||||
|
||||
def cache_instance(self, prod):
|
||||
self.instances[prod.upc] = prod
|
||||
|
|
@ -116,7 +113,7 @@ class ProductCacher(DataCacher):
|
|||
|
||||
class CustomerGroupCacher(DataCacher):
|
||||
|
||||
class_ = rattail.CustomerGroup
|
||||
class_ = model.CustomerGroup
|
||||
|
||||
def cache_instance(self, group):
|
||||
self.instances[group.id] = group
|
||||
|
|
@ -124,7 +121,7 @@ class CustomerGroupCacher(DataCacher):
|
|||
|
||||
class CustomerCacher(DataCacher):
|
||||
|
||||
class_ = rattail.Customer
|
||||
class_ = model.Customer
|
||||
|
||||
def cache_instance(self, customer):
|
||||
self.instances[customer.id] = customer
|
||||
|
|
|
|||
124
rattail/db/changes.py
Normal file
124
rattail/db/changes.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.db.changes`` -- Recording Changes
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from sqlalchemy.event import listen
|
||||
from sqlalchemy.orm import object_mapper, RelationshipProperty
|
||||
|
||||
import rattail
|
||||
from rattail.db.model import (
|
||||
Change, Batch, BatchColumn, BatchRow, Role, UserRole)
|
||||
|
||||
|
||||
__all__ = ['record_changes']
|
||||
|
||||
|
||||
ignore_role_changes = None
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def before_flush(session, flush_context, instances):
|
||||
"""
|
||||
Listens for session flush events. This function is responsible for adding
|
||||
stub records to the 'changes' table, which will in turn be used by the
|
||||
database synchronizer.
|
||||
"""
|
||||
|
||||
def record_change(instance, deleted=False):
|
||||
|
||||
# No need to record changes for Change. :)
|
||||
if isinstance(instance, Change):
|
||||
return
|
||||
|
||||
# No need to record changes for batch data.
|
||||
if isinstance(instance, (Batch, BatchColumn, BatchRow)):
|
||||
return
|
||||
|
||||
# Ignore instances which don't use UUID.
|
||||
if not hasattr(instance, 'uuid'):
|
||||
return
|
||||
|
||||
# Ignore Role instances, if so configured.
|
||||
if ignore_role_changes:
|
||||
if isinstance(instance, (Role, UserRole)):
|
||||
return
|
||||
|
||||
# Provide an UUID value, if necessary.
|
||||
if not instance.uuid:
|
||||
|
||||
# If the 'uuid' column is actually a foreign key to another
|
||||
# table...well, then we can't just generate a new value for it.
|
||||
# Instead we must traverse the relationship and fetch the existing
|
||||
# foreign key value...
|
||||
|
||||
mapper = object_mapper(instance)
|
||||
uuid = mapper.columns['uuid']
|
||||
if uuid.foreign_keys:
|
||||
|
||||
for prop in mapper.iterate_properties:
|
||||
if (isinstance(prop, RelationshipProperty)
|
||||
and len(prop.remote_side) == 1
|
||||
and prop.remote_side[0].key == 'uuid'):
|
||||
|
||||
instance.uuid = getattr(instance, prop.key).uuid
|
||||
break
|
||||
assert instance.uuid
|
||||
|
||||
# ...but if there is no foreign key, just generate a new UUID.
|
||||
else:
|
||||
instance.uuid = rattail.get_uuid()
|
||||
|
||||
# Record the change.
|
||||
change = session.query(Change).get(
|
||||
(instance.__class__.__name__, instance.uuid))
|
||||
if not change:
|
||||
change = Change(
|
||||
class_name=instance.__class__.__name__,
|
||||
uuid=instance.uuid)
|
||||
session.add(change)
|
||||
change.deleted = deleted
|
||||
log.debug("before_flush: recorded change: %s" % repr(change))
|
||||
|
||||
for instance in session.deleted:
|
||||
log.debug("before_flush: deleted instance: %s" % repr(instance))
|
||||
record_change(instance, deleted=True)
|
||||
|
||||
for instance in session.new:
|
||||
log.debug("before_flush: new instance: %s" % repr(instance))
|
||||
record_change(instance)
|
||||
|
||||
for instance in session.dirty:
|
||||
if session.is_modified(instance, passive=True):
|
||||
log.debug("before_flush: dirty instance: %s" % repr(instance))
|
||||
record_change(instance)
|
||||
|
||||
|
||||
def record_changes(session):
|
||||
listen(session, 'before_flush', before_flush)
|
||||
|
|
@ -1,955 +0,0 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.db.extension.model`` -- Schema Definition
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from sqlalchemy import Column, ForeignKey
|
||||
from sqlalchemy import String, Integer, DateTime, Date, Boolean, Numeric, Text
|
||||
from sqlalchemy import types
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm import relationship, object_session
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
|
||||
import edbob
|
||||
from edbob.db.model import Base, uuid_column
|
||||
from edbob.db.extensions.contact import Person, EmailAddress, PhoneNumber
|
||||
from edbob.exceptions import LoadSpecError
|
||||
from edbob.sqlalchemy import getset_factory
|
||||
|
||||
from rattail import sil
|
||||
from rattail import batches
|
||||
from rattail.db import GPCType
|
||||
|
||||
|
||||
__all__ = ['Change', 'Store', 'StoreEmailAddress', 'StorePhoneNumber',
|
||||
'Department', 'Subdepartment', 'Brand', 'Category', 'Vendor',
|
||||
'VendorContact', 'VendorPhoneNumber', 'VendorEmailAddress',
|
||||
'Product', 'ProductCost', 'ProductPrice', 'Customer',
|
||||
'CustomerEmailAddress', 'CustomerPhoneNumber', 'CustomerGroup',
|
||||
'CustomerGroupAssignment', 'CustomerPerson', 'Employee',
|
||||
'EmployeeEmailAddress', 'EmployeePhoneNumber',
|
||||
'BatchColumn', 'Batch', 'BatchRow', 'LabelProfile']
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Change(Base):
|
||||
"""
|
||||
Represents a changed (or deleted) record, which is pending synchronization
|
||||
to another database.
|
||||
"""
|
||||
|
||||
__tablename__ = 'changes'
|
||||
|
||||
class_name = Column(String(25), primary_key=True)
|
||||
uuid = Column(String(32), primary_key=True)
|
||||
deleted = Column(Boolean)
|
||||
|
||||
def __repr__(self):
|
||||
status = 'deleted' if self.deleted else 'new/changed'
|
||||
return "<Change: %s, %s, %s>" % (self.class_name, self.uuid, status)
|
||||
|
||||
|
||||
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, nullable=False)
|
||||
name = Column(String(20))
|
||||
display_name = Column(String(50))
|
||||
sil_name = Column(String(10))
|
||||
data_type = Column(String(15))
|
||||
description = Column(String(50))
|
||||
visible = Column(Boolean, default=True)
|
||||
|
||||
def __init__(self, sil_name=None, **kwargs):
|
||||
if sil_name:
|
||||
kwargs['sil_name'] = sil_name
|
||||
sil_column = sil.get_column(sil_name)
|
||||
kwargs.setdefault('name', sil_name)
|
||||
kwargs.setdefault('data_type', sil_column.data_type)
|
||||
kwargs.setdefault('description', sil_column.description)
|
||||
kwargs.setdefault('display_name', sil_column.display_name)
|
||||
super(BatchColumn, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return "<BatchColumn: %s>" % self.name
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.display_name or '')
|
||||
|
||||
|
||||
class BatchRow(edbob.Object):
|
||||
"""
|
||||
Superclass of batch row objects.
|
||||
"""
|
||||
|
||||
def __unicode__(self):
|
||||
return u"Row %d" % self.ordinal
|
||||
|
||||
|
||||
class Batch(Base):
|
||||
"""
|
||||
Represents a SIL-compliant batch of data.
|
||||
"""
|
||||
|
||||
__tablename__ = 'batches'
|
||||
|
||||
uuid = uuid_column()
|
||||
provider = Column(String(50))
|
||||
id = Column(String(8))
|
||||
source = Column(String(6))
|
||||
destination = Column(String(6))
|
||||
action_type = Column(String(6))
|
||||
description = Column(String(50))
|
||||
rowcount = Column(Integer, default=0)
|
||||
executed = Column(DateTime)
|
||||
purge = Column(Date)
|
||||
|
||||
columns = relationship(BatchColumn, backref='batch',
|
||||
collection_class=ordering_list('ordinal'),
|
||||
order_by=BatchColumn.ordinal,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
_rowclasses = {}
|
||||
|
||||
def __repr__(self):
|
||||
return "<Batch: %s>" % self.description
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.description or '')
|
||||
|
||||
@property
|
||||
def rowclass(self):
|
||||
"""
|
||||
Returns the mapped class for the underlying row (data) table.
|
||||
"""
|
||||
|
||||
if not self.uuid:
|
||||
object_session(self).flush()
|
||||
assert self.uuid
|
||||
|
||||
if self.uuid not in self._rowclasses:
|
||||
|
||||
kwargs = {
|
||||
'__tablename__': 'batch.%s' % self.uuid,
|
||||
'uuid': uuid_column(),
|
||||
'ordinal': Column(Integer, nullable=False),
|
||||
}
|
||||
|
||||
for column in self.columns:
|
||||
data_type = sil.get_sqlalchemy_type(column.data_type)
|
||||
kwargs[column.name] = Column(data_type)
|
||||
rowclass = type('BatchRow_%s' % str(self.uuid), (Base, BatchRow), kwargs)
|
||||
|
||||
batch_uuid = self.uuid
|
||||
def batch(self):
|
||||
return object_session(self).query(Batch).get(batch_uuid)
|
||||
rowclass.batch = property(batch)
|
||||
|
||||
self._rowclasses[self.uuid] = rowclass
|
||||
|
||||
return self._rowclasses[self.uuid]
|
||||
|
||||
def add_column(self, sil_name=None, **kwargs):
|
||||
column = BatchColumn(sil_name, **kwargs)
|
||||
self.columns.append(column)
|
||||
|
||||
def add_row(self, row, **kwargs):
|
||||
"""
|
||||
Adds a row to the batch data table.
|
||||
"""
|
||||
|
||||
session = object_session(self)
|
||||
# FIXME: This probably needs to use a func.max() query.
|
||||
row.ordinal = self.rowcount + 1
|
||||
session.add(row)
|
||||
self.rowcount += 1
|
||||
session.flush()
|
||||
|
||||
def create_table(self):
|
||||
"""
|
||||
Creates the batch's data table within the database.
|
||||
"""
|
||||
|
||||
session = object_session(self)
|
||||
self.rowclass.__table__.create(session.bind)
|
||||
|
||||
def drop_table(self):
|
||||
"""
|
||||
Drops the batch's data table from the database.
|
||||
"""
|
||||
|
||||
log.debug("Batch.drop_table: Dropping table for batch: %s, %s (%s)"
|
||||
% (self.id, self.description, self.uuid))
|
||||
session = object_session(self)
|
||||
self.rowclass.__table__.drop(session.bind)
|
||||
|
||||
def execute(self, progress=None):
|
||||
provider = self.get_provider()
|
||||
assert provider
|
||||
if not provider.execute(self, progress):
|
||||
return False
|
||||
self.executed = edbob.utc_time(naive=True)
|
||||
object_session(self).flush()
|
||||
return True
|
||||
|
||||
def get_provider(self):
|
||||
assert self.provider
|
||||
return batches.get_provider(self.provider)
|
||||
|
||||
def iter_rows(self):
|
||||
session = object_session(self)
|
||||
q = session.query(self.rowclass)
|
||||
q = q.order_by(self.rowclass.ordinal)
|
||||
return q
|
||||
|
||||
|
||||
class StoreEmailAddress(EmailAddress):
|
||||
"""
|
||||
Represents an email address associated with a :class:`Store`.
|
||||
"""
|
||||
|
||||
__mapper_args__ = {'polymorphic_identity': 'Store'}
|
||||
|
||||
|
||||
class StorePhoneNumber(PhoneNumber):
|
||||
"""
|
||||
Represents a phone (or fax) number associated with a :class:`Store`.
|
||||
"""
|
||||
|
||||
__mapper_args__ = {'polymorphic_identity': 'Store'}
|
||||
|
||||
|
||||
class Store(Base):
|
||||
"""
|
||||
Represents a store (physical or otherwise) within the organization.
|
||||
"""
|
||||
|
||||
__tablename__ = 'stores'
|
||||
|
||||
uuid = uuid_column()
|
||||
id = Column(String(10))
|
||||
name = Column(String(100))
|
||||
|
||||
def __repr__(self):
|
||||
return "<Store: %s, %s>" % (self.id, self.name)
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.name or '')
|
||||
|
||||
def add_email_address(self, address, type='Info'):
|
||||
email = StoreEmailAddress(address=address, type=type)
|
||||
self.emails.append(email)
|
||||
|
||||
def add_phone_number(self, number, type='Voice'):
|
||||
phone = StorePhoneNumber(number=number, type=type)
|
||||
self.phones.append(phone)
|
||||
|
||||
Store.emails = relationship(
|
||||
StoreEmailAddress,
|
||||
backref='store',
|
||||
primaryjoin=StoreEmailAddress.parent_uuid == Store.uuid,
|
||||
foreign_keys=[StoreEmailAddress.parent_uuid],
|
||||
collection_class=ordering_list('preference', count_from=1),
|
||||
order_by=StoreEmailAddress.preference,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
Store.email = relationship(
|
||||
StoreEmailAddress,
|
||||
primaryjoin=and_(
|
||||
StoreEmailAddress.parent_uuid == Store.uuid,
|
||||
StoreEmailAddress.preference == 1),
|
||||
foreign_keys=[StoreEmailAddress.parent_uuid],
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
Store.phones = relationship(
|
||||
StorePhoneNumber,
|
||||
backref='store',
|
||||
primaryjoin=StorePhoneNumber.parent_uuid == Store.uuid,
|
||||
foreign_keys=[StorePhoneNumber.parent_uuid],
|
||||
collection_class=ordering_list('preference', count_from=1),
|
||||
order_by=StorePhoneNumber.preference,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
Store.phone = relationship(
|
||||
StorePhoneNumber,
|
||||
primaryjoin=and_(
|
||||
StorePhoneNumber.parent_uuid == Store.uuid,
|
||||
StorePhoneNumber.preference == 1),
|
||||
foreign_keys=[StorePhoneNumber.parent_uuid],
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
|
||||
class Brand(Base):
|
||||
"""
|
||||
Represents a brand or similar product line.
|
||||
"""
|
||||
|
||||
__tablename__ = 'brands'
|
||||
|
||||
uuid = uuid_column()
|
||||
name = Column(String(100))
|
||||
|
||||
def __repr__(self):
|
||||
return "<Brand: %s>" % self.name
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.name or '')
|
||||
|
||||
|
||||
class Department(Base):
|
||||
"""
|
||||
Represents an organizational department.
|
||||
"""
|
||||
|
||||
__tablename__ = 'departments'
|
||||
|
||||
uuid = uuid_column()
|
||||
number = Column(Integer)
|
||||
name = Column(String(30))
|
||||
|
||||
def __repr__(self):
|
||||
return "<Department: %s>" % self.name
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.name or '')
|
||||
|
||||
|
||||
class Subdepartment(Base):
|
||||
"""
|
||||
Represents an organizational subdepartment.
|
||||
"""
|
||||
|
||||
__tablename__ = 'subdepartments'
|
||||
|
||||
uuid = uuid_column()
|
||||
number = Column(Integer)
|
||||
name = Column(String(30))
|
||||
department_uuid = Column(String(32), ForeignKey('departments.uuid'))
|
||||
|
||||
department = relationship(Department)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Subdepartment: %s>" % self.name
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.name or '')
|
||||
|
||||
|
||||
class Category(Base):
|
||||
"""
|
||||
Represents an organizational category for products.
|
||||
"""
|
||||
|
||||
__tablename__ = 'categories'
|
||||
|
||||
uuid = uuid_column()
|
||||
number = Column(Integer)
|
||||
name = Column(String(50))
|
||||
department_uuid = Column(String(32), ForeignKey('departments.uuid'))
|
||||
|
||||
department = relationship(Department)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Category: %s, %s>" % (self.number, self.name)
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.name or '')
|
||||
|
||||
|
||||
class VendorEmailAddress(EmailAddress):
|
||||
"""
|
||||
Represents an email address associated with a :class:`Vendor`.
|
||||
"""
|
||||
|
||||
__mapper_args__ = {'polymorphic_identity': 'Vendor'}
|
||||
|
||||
|
||||
class VendorPhoneNumber(PhoneNumber):
|
||||
"""
|
||||
Represents a phone (or fax) number associated with a :class:`Vendor`.
|
||||
"""
|
||||
|
||||
__mapper_args__ = {'polymorphic_identity': 'Vendor'}
|
||||
|
||||
|
||||
class VendorContact(Base):
|
||||
"""
|
||||
Represents a point of contact (e.g. salesperson) for a vendor, from the
|
||||
retailer's perspective.
|
||||
"""
|
||||
|
||||
__tablename__ = 'vendor_contacts'
|
||||
|
||||
uuid = uuid_column()
|
||||
vendor_uuid = Column(String(32), ForeignKey('vendors.uuid'), nullable=False)
|
||||
person_uuid = Column(String(32), ForeignKey('people.uuid'), nullable=False)
|
||||
preference = Column(Integer, nullable=False)
|
||||
|
||||
person = relationship(Person)
|
||||
|
||||
def __repr__(self):
|
||||
return "<VendorContact: %s, %s>" % (self.vendor, self.person)
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.person)
|
||||
|
||||
|
||||
class Vendor(Base):
|
||||
"""
|
||||
Represents a vendor from which products are purchased by the store.
|
||||
"""
|
||||
|
||||
__tablename__ = 'vendors'
|
||||
|
||||
uuid = uuid_column()
|
||||
id = Column(String(15))
|
||||
name = Column(String(40))
|
||||
special_discount = Column(Numeric(5,3))
|
||||
|
||||
contacts = relationship(VendorContact, backref='vendor',
|
||||
collection_class=ordering_list('preference', count_from=1),
|
||||
order_by=VendorContact.preference,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
def __repr__(self):
|
||||
return "<Vendor: %s>" % self.name
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.name or '')
|
||||
|
||||
def add_contact(self, person):
|
||||
contact = VendorContact(person=person)
|
||||
self.contacts.append(contact)
|
||||
|
||||
def add_email_address(self, address, type='Info'):
|
||||
email = VendorEmailAddress(address=address, type=type)
|
||||
self.emails.append(email)
|
||||
|
||||
def add_phone_number(self, number, type='Voice'):
|
||||
phone = VendorPhoneNumber(number=number, type=type)
|
||||
self.phones.append(phone)
|
||||
|
||||
Vendor.emails = relationship(
|
||||
VendorEmailAddress,
|
||||
backref='vendor',
|
||||
primaryjoin=VendorEmailAddress.parent_uuid == Vendor.uuid,
|
||||
foreign_keys=[VendorEmailAddress.parent_uuid],
|
||||
collection_class=ordering_list('preference', count_from=1),
|
||||
order_by=VendorEmailAddress.preference,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
Vendor.email = relationship(
|
||||
VendorEmailAddress,
|
||||
primaryjoin=and_(
|
||||
VendorEmailAddress.parent_uuid == Vendor.uuid,
|
||||
VendorEmailAddress.preference == 1),
|
||||
foreign_keys=[VendorEmailAddress.parent_uuid],
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
Vendor.phones = relationship(
|
||||
VendorPhoneNumber,
|
||||
backref='vendor',
|
||||
primaryjoin=VendorPhoneNumber.parent_uuid == Vendor.uuid,
|
||||
foreign_keys=[VendorPhoneNumber.parent_uuid],
|
||||
collection_class=ordering_list('preference', count_from=1),
|
||||
order_by=VendorPhoneNumber.preference,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
Vendor.phone = relationship(
|
||||
VendorPhoneNumber,
|
||||
primaryjoin=and_(
|
||||
VendorPhoneNumber.parent_uuid == Vendor.uuid,
|
||||
VendorPhoneNumber.preference == 1),
|
||||
foreign_keys=[VendorPhoneNumber.parent_uuid],
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
Vendor.contact = relationship(
|
||||
VendorContact,
|
||||
primaryjoin=and_(
|
||||
VendorContact.vendor_uuid == Vendor.uuid,
|
||||
VendorContact.preference == 1),
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
|
||||
class ProductCost(Base):
|
||||
"""
|
||||
Represents a source from which a product may be obtained via purchase.
|
||||
"""
|
||||
|
||||
__tablename__ = 'product_costs'
|
||||
|
||||
uuid = uuid_column()
|
||||
product_uuid = Column(String(32), ForeignKey('products.uuid'), nullable=False)
|
||||
vendor_uuid = Column(String(32), ForeignKey('vendors.uuid'), nullable=False)
|
||||
preference = Column(Integer, nullable=False)
|
||||
code = Column(String(15))
|
||||
|
||||
case_size = Column(Integer)
|
||||
case_cost = Column(Numeric(9,5))
|
||||
pack_size = Column(Integer)
|
||||
pack_cost = Column(Numeric(9,5))
|
||||
unit_cost = Column(Numeric(9,5))
|
||||
effective = Column(DateTime)
|
||||
|
||||
vendor = relationship(Vendor)
|
||||
|
||||
def __repr__(self):
|
||||
return "<ProductCost: %s : %s>" % (self.product, self.vendor)
|
||||
|
||||
|
||||
class ProductPrice(Base):
|
||||
"""
|
||||
Represents a price for a product.
|
||||
"""
|
||||
|
||||
__tablename__ = 'product_prices'
|
||||
|
||||
uuid = uuid_column()
|
||||
product_uuid = Column(String(32), ForeignKey('products.uuid'), nullable=False)
|
||||
type = Column(Integer)
|
||||
level = Column(Integer)
|
||||
starts = Column(DateTime)
|
||||
ends = Column(DateTime)
|
||||
price = Column(Numeric(8,3))
|
||||
multiple = Column(Integer)
|
||||
pack_price = Column(Numeric(8,3))
|
||||
pack_multiple = Column(Integer)
|
||||
|
||||
def __repr__(self):
|
||||
return "<ProductPrice: %s : %s>" % (self.product, self.price)
|
||||
|
||||
|
||||
class Product(Base):
|
||||
"""
|
||||
Represents a product for sale and/or purchase.
|
||||
"""
|
||||
|
||||
__tablename__ = 'products'
|
||||
|
||||
uuid = uuid_column()
|
||||
upc = Column(GPCType, index=True)
|
||||
department_uuid = Column(String(32), ForeignKey('departments.uuid'))
|
||||
subdepartment_uuid = Column(String(32), ForeignKey('subdepartments.uuid'))
|
||||
category_uuid = Column(String(32), ForeignKey('categories.uuid'))
|
||||
brand_uuid = Column(String(32), ForeignKey('brands.uuid'))
|
||||
description = Column(String(60))
|
||||
description2 = Column(String(60))
|
||||
size = Column(String(30))
|
||||
unit_of_measure = Column(String(4))
|
||||
|
||||
regular_price_uuid = Column(
|
||||
String(32),
|
||||
ForeignKey('product_prices.uuid',
|
||||
use_alter=True,
|
||||
name='products_regular_price_uuid_fkey'))
|
||||
|
||||
current_price_uuid = Column(
|
||||
String(32),
|
||||
ForeignKey('product_prices.uuid',
|
||||
use_alter=True,
|
||||
name='products_current_price_uuid_fkey'))
|
||||
|
||||
department = relationship(Department)
|
||||
subdepartment = relationship(Subdepartment)
|
||||
category = relationship(Category)
|
||||
brand = relationship(Brand)
|
||||
|
||||
costs = relationship(ProductCost, backref='product',
|
||||
collection_class=ordering_list('preference', count_from=1),
|
||||
order_by=ProductCost.preference,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
def __repr__(self):
|
||||
return "<Product: %s>" % self.description
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.description or '')
|
||||
|
||||
Product.prices = relationship(
|
||||
ProductPrice, backref='product',
|
||||
primaryjoin=ProductPrice.product_uuid == Product.uuid,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
Product.regular_price = relationship(
|
||||
ProductPrice,
|
||||
primaryjoin=Product.regular_price_uuid == ProductPrice.uuid,
|
||||
lazy='joined',
|
||||
post_update=True)
|
||||
|
||||
Product.current_price = relationship(
|
||||
ProductPrice,
|
||||
primaryjoin=Product.current_price_uuid == ProductPrice.uuid,
|
||||
lazy='joined',
|
||||
post_update=True)
|
||||
|
||||
Product.cost = relationship(
|
||||
ProductCost,
|
||||
primaryjoin=and_(
|
||||
ProductCost.product_uuid == Product.uuid,
|
||||
ProductCost.preference == 1,
|
||||
),
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
Product.vendor = relationship(
|
||||
Vendor,
|
||||
secondary=ProductCost.__table__,
|
||||
primaryjoin=and_(
|
||||
ProductCost.product_uuid == Product.uuid,
|
||||
ProductCost.preference == 1,
|
||||
),
|
||||
secondaryjoin=Vendor.uuid == ProductCost.vendor_uuid,
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
|
||||
class EmployeeEmailAddress(EmailAddress):
|
||||
"""
|
||||
Represents an email address associated with a :class:`Employee`.
|
||||
"""
|
||||
|
||||
__mapper_args__ = {'polymorphic_identity': 'Employee'}
|
||||
|
||||
|
||||
class EmployeePhoneNumber(PhoneNumber):
|
||||
"""
|
||||
Represents a phone (or fax) number associated with a :class:`Employee`.
|
||||
"""
|
||||
|
||||
__mapper_args__ = {'polymorphic_identity': 'Employee'}
|
||||
|
||||
|
||||
class Employee(Base):
|
||||
"""
|
||||
Represents an employee within the organization.
|
||||
"""
|
||||
|
||||
__tablename__ = 'employees'
|
||||
|
||||
uuid = uuid_column()
|
||||
id = Column(Integer)
|
||||
person_uuid = Column(String(32), ForeignKey('people.uuid'), nullable=False)
|
||||
status = Column(Integer)
|
||||
display_name = Column(String(100))
|
||||
|
||||
person = relationship(Person)
|
||||
|
||||
first_name = association_proxy('person', 'first_name')
|
||||
last_name = association_proxy('person', 'last_name')
|
||||
|
||||
def __repr__(self):
|
||||
return "<Employee: %s>" % self.person
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.display_name or self.person)
|
||||
|
||||
def add_email_address(self, address, type='Home'):
|
||||
email = EmployeeEmailAddress(address=address, type=type)
|
||||
self.emails.append(email)
|
||||
|
||||
def add_phone_number(self, number, type='Home'):
|
||||
phone = EmployeePhoneNumber(number=number, type=type)
|
||||
self.phones.append(phone)
|
||||
|
||||
Employee.emails = relationship(
|
||||
EmployeeEmailAddress,
|
||||
backref='employee',
|
||||
primaryjoin=EmployeeEmailAddress.parent_uuid == Employee.uuid,
|
||||
foreign_keys=[EmployeeEmailAddress.parent_uuid],
|
||||
collection_class=ordering_list('preference', count_from=1),
|
||||
order_by=EmployeeEmailAddress.preference,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
Employee.email = relationship(
|
||||
EmployeeEmailAddress,
|
||||
primaryjoin=and_(
|
||||
EmployeeEmailAddress.parent_uuid == Employee.uuid,
|
||||
EmployeeEmailAddress.preference == 1),
|
||||
foreign_keys=[EmployeeEmailAddress.parent_uuid],
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
Employee.phones = relationship(
|
||||
EmployeePhoneNumber,
|
||||
backref='employee',
|
||||
primaryjoin=EmployeePhoneNumber.parent_uuid == Employee.uuid,
|
||||
foreign_keys=[EmployeePhoneNumber.parent_uuid],
|
||||
collection_class=ordering_list('preference', count_from=1),
|
||||
order_by=EmployeePhoneNumber.preference,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
Employee.phone = relationship(
|
||||
EmployeePhoneNumber,
|
||||
primaryjoin=and_(
|
||||
EmployeePhoneNumber.parent_uuid == Employee.uuid,
|
||||
EmployeePhoneNumber.preference == 1),
|
||||
foreign_keys=[EmployeePhoneNumber.parent_uuid],
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
|
||||
class CustomerEmailAddress(EmailAddress):
|
||||
"""
|
||||
Represents an email address associated with a :class:`Customer`.
|
||||
"""
|
||||
|
||||
__mapper_args__ = {'polymorphic_identity': 'Customer'}
|
||||
|
||||
|
||||
class CustomerPhoneNumber(PhoneNumber):
|
||||
"""
|
||||
Represents a phone (or fax) number associated with a :class:`Customer`.
|
||||
"""
|
||||
|
||||
__mapper_args__ = {'polymorphic_identity': 'Customer'}
|
||||
|
||||
|
||||
class CustomerPerson(Base):
|
||||
"""
|
||||
Represents the association between a :class:`Person` and a customer account
|
||||
(:class:`Customer`).
|
||||
"""
|
||||
|
||||
__tablename__ = 'customers_people'
|
||||
|
||||
uuid = uuid_column()
|
||||
customer_uuid = Column(String(32), ForeignKey('customers.uuid'))
|
||||
person_uuid = Column(String(32), ForeignKey('people.uuid'))
|
||||
ordinal = Column(Integer, nullable=False)
|
||||
|
||||
person = relationship(Person)
|
||||
|
||||
|
||||
class Customer(Base):
|
||||
"""
|
||||
Represents a customer account. Customer accounts may consist of more than
|
||||
one :class:`Person`, in some cases.
|
||||
"""
|
||||
|
||||
__tablename__ = 'customers'
|
||||
|
||||
uuid = uuid_column()
|
||||
id = Column(String(20))
|
||||
name = Column(String(255))
|
||||
email_preference = Column(Integer)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Customer: %s, %s>" % (self.id, self.name or self.person)
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.name or self.person)
|
||||
|
||||
def add_email_address(self, address, type='Home'):
|
||||
email = CustomerEmailAddress(address=address, type=type)
|
||||
self.emails.append(email)
|
||||
|
||||
def add_phone_number(self, number, type='Home'):
|
||||
phone = CustomerPhoneNumber(number=number, type=type)
|
||||
self.phones.append(phone)
|
||||
|
||||
Customer.emails = relationship(
|
||||
CustomerEmailAddress,
|
||||
backref='customer',
|
||||
primaryjoin=CustomerEmailAddress.parent_uuid == Customer.uuid,
|
||||
foreign_keys=[CustomerEmailAddress.parent_uuid],
|
||||
collection_class=ordering_list('preference', count_from=1),
|
||||
order_by=CustomerEmailAddress.preference,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
Customer.email = relationship(
|
||||
CustomerEmailAddress,
|
||||
primaryjoin=and_(
|
||||
CustomerEmailAddress.parent_uuid == Customer.uuid,
|
||||
CustomerEmailAddress.preference == 1),
|
||||
foreign_keys=[CustomerEmailAddress.parent_uuid],
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
Customer.phones = relationship(
|
||||
CustomerPhoneNumber,
|
||||
backref='customer',
|
||||
primaryjoin=CustomerPhoneNumber.parent_uuid == Customer.uuid,
|
||||
foreign_keys=[CustomerPhoneNumber.parent_uuid],
|
||||
collection_class=ordering_list('preference', count_from=1),
|
||||
order_by=CustomerPhoneNumber.preference,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
Customer.phone = relationship(
|
||||
CustomerPhoneNumber,
|
||||
primaryjoin=and_(
|
||||
CustomerPhoneNumber.parent_uuid == Customer.uuid,
|
||||
CustomerPhoneNumber.preference == 1),
|
||||
foreign_keys=[CustomerPhoneNumber.parent_uuid],
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
Customer._people = relationship(
|
||||
CustomerPerson, backref='customer',
|
||||
primaryjoin=CustomerPerson.customer_uuid == Customer.uuid,
|
||||
collection_class=ordering_list('ordinal', count_from=1),
|
||||
order_by=CustomerPerson.ordinal)
|
||||
|
||||
Customer.people = association_proxy(
|
||||
'_people', 'person',
|
||||
getset_factory=getset_factory,
|
||||
creator=lambda x: CustomerPerson(person=x))
|
||||
|
||||
Customer._person = relationship(
|
||||
CustomerPerson,
|
||||
primaryjoin=and_(
|
||||
CustomerPerson.customer_uuid == Customer.uuid,
|
||||
CustomerPerson.ordinal == 1),
|
||||
uselist=False,
|
||||
viewonly=True)
|
||||
|
||||
Customer.person = association_proxy(
|
||||
'_person', 'person',
|
||||
getset_factory=getset_factory)
|
||||
|
||||
|
||||
class CustomerGroup(Base):
|
||||
"""
|
||||
Represents an arbitrary group to which :class:`Customer` instances may (or
|
||||
may not) belong.
|
||||
"""
|
||||
|
||||
__tablename__ = 'customer_groups'
|
||||
|
||||
uuid = uuid_column()
|
||||
id = Column(String(20))
|
||||
name = Column(String(255))
|
||||
|
||||
def __repr__(self):
|
||||
return "<CustomerGroup: %s, %s>" % (self.id, self.name)
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.name or '')
|
||||
|
||||
|
||||
class CustomerGroupAssignment(Base):
|
||||
"""
|
||||
Represents the assignment of a :class:`Customer` to a
|
||||
:class:`CustomerGroup`.
|
||||
"""
|
||||
|
||||
__tablename__ = 'customers_groups'
|
||||
|
||||
uuid = uuid_column()
|
||||
customer_uuid = Column(String(32), ForeignKey('customers.uuid'))
|
||||
group_uuid = Column(String(32), ForeignKey('customer_groups.uuid'))
|
||||
ordinal = Column(Integer, nullable=False)
|
||||
|
||||
group = relationship(CustomerGroup)
|
||||
|
||||
Customer._groups = relationship(
|
||||
CustomerGroupAssignment, backref='customer',
|
||||
collection_class=ordering_list('ordinal', count_from=1),
|
||||
order_by=CustomerGroupAssignment.ordinal)
|
||||
|
||||
Customer.groups = association_proxy(
|
||||
'_groups', 'group',
|
||||
getset_factory=getset_factory,
|
||||
creator=lambda x: CustomerGroupAssignment(group=x))
|
||||
|
||||
|
||||
class LabelProfile(Base):
|
||||
"""
|
||||
Represents a "profile" (collection of settings) for product label printing.
|
||||
"""
|
||||
|
||||
__tablename__ = 'label_profiles'
|
||||
|
||||
uuid = uuid_column()
|
||||
ordinal = Column(Integer)
|
||||
code = Column(String(3))
|
||||
description = Column(String(50))
|
||||
printer_spec = Column(String(255))
|
||||
formatter_spec = Column(String(255))
|
||||
format = Column(Text)
|
||||
visible = Column(Boolean)
|
||||
|
||||
_printer = None
|
||||
_formatter = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<LabelProfile: %s>" % self.code
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.description or '')
|
||||
|
||||
def get_formatter(self):
|
||||
if not self._formatter and self.formatter_spec:
|
||||
try:
|
||||
formatter = edbob.load_spec(self.formatter_spec)
|
||||
except LoadSpecError:
|
||||
pass
|
||||
else:
|
||||
self._formatter = formatter()
|
||||
self._formatter.format = self.format
|
||||
return self._formatter
|
||||
|
||||
def get_printer(self):
|
||||
if not self._printer and self.printer_spec:
|
||||
try:
|
||||
printer = edbob.load_spec(self.printer_spec)
|
||||
except LoadSpecError:
|
||||
pass
|
||||
else:
|
||||
self._printer = printer()
|
||||
for name in printer.required_settings:
|
||||
setattr(printer, name, self.get_printer_setting(name))
|
||||
self._printer.formatter = self.get_formatter()
|
||||
return self._printer
|
||||
|
||||
def get_printer_setting(self, name):
|
||||
session = object_session(self)
|
||||
if not self.uuid:
|
||||
session.flush()
|
||||
name = 'labels.%s.printer.%s' % (self.uuid, name)
|
||||
return edbob.get_setting(name, session)
|
||||
|
||||
def save_printer_setting(self, name, value):
|
||||
session = object_session(self)
|
||||
if not self.uuid:
|
||||
session.flush()
|
||||
name = 'labels.%s.printer.%s' % (self.uuid, name)
|
||||
edbob.save_setting(name, value, session)
|
||||
136
rattail/db/extensions.py
Normal file
136
rattail/db/extensions.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.db.extensions`` -- Database Extensions
|
||||
"""
|
||||
|
||||
import logging
|
||||
import pkg_resources
|
||||
|
||||
import rattail
|
||||
from rattail import db
|
||||
from rattail.db.util import get_core_metadata
|
||||
from rattail.modules import graft, import_module_path
|
||||
from rattail.exceptions import NoDefaultDatabase, SchemaExtensionNotFound
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
extensions = None
|
||||
|
||||
|
||||
class Extension(object):
|
||||
"""
|
||||
Base class for schema/ORM extensions.
|
||||
"""
|
||||
|
||||
# You can set this to any dotted module path you like. If unset a default
|
||||
# will be assumed, of the form ``<path.to.extension>.model`` (see
|
||||
# :meth:`get_models_module()` for more info).
|
||||
model_module = ''
|
||||
|
||||
def extend_namespace(self):
|
||||
"""
|
||||
Extends the ``rattail`` namespace with model classes provided by the
|
||||
extension.
|
||||
"""
|
||||
|
||||
graft(rattail, self.get_model_module())
|
||||
|
||||
def get_model_module(self):
|
||||
"""
|
||||
Imports and returns a reference to the Python module providing schema
|
||||
definition for the extension.
|
||||
|
||||
:attr:`model_module` is first consulted to determine the dotted module
|
||||
path. If nothing is found there, a default path is constructed by
|
||||
appending ``'.model'`` to the extension module's own dotted path.
|
||||
"""
|
||||
|
||||
if self.model_module:
|
||||
module = self.model_module
|
||||
else:
|
||||
module = str(self.__class__.__module__) + '.model'
|
||||
return import_module_path(module)
|
||||
|
||||
def populate_metadata(self, metadata):
|
||||
"""
|
||||
Populates ``metadata`` with tables provided by the extension.
|
||||
"""
|
||||
|
||||
model = self.get_model_module()
|
||||
for name in model.__all__:
|
||||
obj = getattr(model, name)
|
||||
if isinstance(obj, type) and issubclass(obj, model.Base):
|
||||
if obj.__tablename__ not in metadata.tables:
|
||||
obj.__table__.tometadata(metadata)
|
||||
|
||||
|
||||
def get_extensions():
|
||||
"""
|
||||
Returns the map of available :class:`Extension` classes, as determined by
|
||||
``setuptools`` entry points..
|
||||
"""
|
||||
|
||||
global extensions
|
||||
|
||||
if extensions is None:
|
||||
extensions = {}
|
||||
for entrypoint in pkg_resources.iter_entry_points('rattail.db.extensions'):
|
||||
extensions[entrypoint.name] = entrypoint.load()
|
||||
return extensions
|
||||
|
||||
|
||||
def get_extension(name):
|
||||
"""
|
||||
Returns a :class:`Extension` instance, according to ``name``. An error is
|
||||
raised if the extension cannot be found.
|
||||
"""
|
||||
|
||||
extensions = get_extensions()
|
||||
if name in extensions:
|
||||
return extensions[name]()
|
||||
raise SchemaExtensionNotFound(name)
|
||||
|
||||
|
||||
def install_extension_schema(extension, engine=None):
|
||||
"""
|
||||
Installs an extension's schema to the database. ``extension`` must be a
|
||||
valid :class:`Extension` instance.
|
||||
|
||||
If ``engine`` is not provided, :attr:`rattail.db.engine` is assumed.
|
||||
"""
|
||||
|
||||
if engine is None:
|
||||
engine = db.engine
|
||||
if not engine:
|
||||
raise NoDefaultDatabase
|
||||
|
||||
if not isinstance(extension, Extension):
|
||||
extension = get_extension(extension)
|
||||
|
||||
meta = get_core_metadata()
|
||||
extension.populate_metadata(meta)
|
||||
meta.create_all(engine)
|
||||
|
|
@ -28,20 +28,19 @@
|
|||
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
import edbob
|
||||
import edbob.db
|
||||
|
||||
import rattail
|
||||
from rattail.db import Session
|
||||
from rattail.db import model
|
||||
|
||||
|
||||
class LoadProcessor(edbob.Object):
|
||||
class LoadProcessor(object):
|
||||
|
||||
def load_all_data(self, host_engine, progress=None):
|
||||
|
||||
edbob.init_modules(['edbob.db', 'rattail.db'])
|
||||
rattail.init_modules(['rattail.db'])
|
||||
|
||||
self.host_session = edbob.Session(bind=host_engine)
|
||||
self.session = edbob.Session()
|
||||
self.host_session = Session(bind=host_engine)
|
||||
self.session = Session()
|
||||
|
||||
cancel = False
|
||||
for cls in self.relevant_classes():
|
||||
|
|
@ -86,48 +85,48 @@ class LoadProcessor(edbob.Object):
|
|||
return not cancel
|
||||
|
||||
def relevant_classes(self):
|
||||
yield edbob.Person
|
||||
yield edbob.User
|
||||
yield rattail.Store
|
||||
yield rattail.Department
|
||||
yield rattail.Subdepartment
|
||||
yield rattail.Category
|
||||
yield rattail.Brand
|
||||
yield rattail.Vendor
|
||||
yield rattail.Product
|
||||
yield rattail.CustomerGroup
|
||||
yield rattail.Customer
|
||||
yield rattail.Employee
|
||||
yield model.Person
|
||||
# yield model.User
|
||||
yield model.Store
|
||||
yield model.Department
|
||||
yield model.Subdepartment
|
||||
yield model.Category
|
||||
yield model.Brand
|
||||
yield model.Vendor
|
||||
yield model.Product
|
||||
yield model.CustomerGroup
|
||||
yield model.Customer
|
||||
yield model.Employee
|
||||
|
||||
classes = edbob.config.get('rattail.db', 'load.extra_classes')
|
||||
if classes:
|
||||
for cls in classes.split():
|
||||
yield getattr(edbob, cls)
|
||||
# classes = rattail.config.get('rattail.db', 'load.extra_classes')
|
||||
# if classes:
|
||||
# for cls in classes.split():
|
||||
# yield getattr(model, cls)
|
||||
|
||||
def query_Customer(self, q):
|
||||
q = q.options(joinedload(rattail.Customer.phones))
|
||||
q = q.options(joinedload(rattail.Customer.emails))
|
||||
q = q.options(joinedload(rattail.Customer._people))
|
||||
q = q.options(joinedload(rattail.Customer._groups))
|
||||
q = q.options(joinedload(model.Customer.phones))
|
||||
q = q.options(joinedload(model.Customer.emails))
|
||||
q = q.options(joinedload(model.Customer._people))
|
||||
q = q.options(joinedload(model.Customer._groups))
|
||||
return q
|
||||
|
||||
def query_CustomerPerson(self, q):
|
||||
q = q.options(joinedload(rattail.CustomerPerson.person))
|
||||
return q
|
||||
# def query_CustomerPerson(self, q):
|
||||
# q = q.options(joinedload(model.CustomerPerson.person))
|
||||
# return q
|
||||
|
||||
def query_Employee(self, q):
|
||||
q = q.options(joinedload(rattail.Employee.phones))
|
||||
q = q.options(joinedload(rattail.Employee.emails))
|
||||
q = q.options(joinedload(model.Employee.phones))
|
||||
q = q.options(joinedload(model.Employee.emails))
|
||||
return q
|
||||
|
||||
def query_Person(self, q):
|
||||
q = q.options(joinedload(edbob.Person.phones))
|
||||
q = q.options(joinedload(edbob.Person.emails))
|
||||
q = q.options(joinedload(model.Person.phones))
|
||||
q = q.options(joinedload(model.Person.emails))
|
||||
return q
|
||||
|
||||
def query_Product(self, q):
|
||||
q = q.options(joinedload(rattail.Product.costs))
|
||||
q = q.options(joinedload(rattail.Product.prices))
|
||||
q = q.options(joinedload(model.Product.costs))
|
||||
q = q.options(joinedload(model.Product.prices))
|
||||
return q
|
||||
|
||||
def merge_Product(self, host_product):
|
||||
|
|
@ -145,12 +144,12 @@ class LoadProcessor(edbob.Object):
|
|||
product.current_price = self.session.merge(host_product.current_price)
|
||||
|
||||
def query_Store(self, q):
|
||||
q = q.options(joinedload(rattail.Store.phones))
|
||||
q = q.options(joinedload(rattail.Store.emails))
|
||||
q = q.options(joinedload(model.Store.phones))
|
||||
q = q.options(joinedload(model.Store.emails))
|
||||
return q
|
||||
|
||||
def query_Vendor(self, q):
|
||||
q = q.options(joinedload(rattail.Vendor.contacts))
|
||||
q = q.options(joinedload(rattail.Vendor.phones))
|
||||
q = q.options(joinedload(rattail.Vendor.emails))
|
||||
q = q.options(joinedload(model.Vendor.contacts))
|
||||
q = q.options(joinedload(model.Vendor.phones))
|
||||
q = q.options(joinedload(model.Vendor.emails))
|
||||
return q
|
||||
|
|
|
|||
2126
rattail/db/model.py
Normal file
2126
rattail/db/model.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -36,32 +36,32 @@ if sys.platform == 'win32':
|
|||
import sqlalchemy.exc
|
||||
from sqlalchemy.orm import class_mapper
|
||||
|
||||
import edbob
|
||||
|
||||
import rattail
|
||||
from rattail import db
|
||||
from rattail.modules import load_spec
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_sync_engines():
|
||||
edbob.init_modules(['edbob.db'])
|
||||
rattail.init_modules(['rattail.db'])
|
||||
|
||||
keys = edbob.config.get('rattail.db', 'syncs')
|
||||
keys = rattail.config.get('rattail.db', 'syncs')
|
||||
if not keys:
|
||||
return None
|
||||
|
||||
engines = {}
|
||||
for key in keys.split(','):
|
||||
key = key.strip()
|
||||
engines[key] = edbob.engines[key]
|
||||
engines[key] = db.engines[key]
|
||||
log.info("get_sync_engines: Found engine keys: %s" % ','.join(engines.keys()))
|
||||
return engines
|
||||
|
||||
|
||||
def dependency_sort(x, y):
|
||||
map_x = class_mapper(getattr(edbob, x))
|
||||
map_y = class_mapper(getattr(edbob, y))
|
||||
map_x = class_mapper(getattr(rattail, x))
|
||||
map_y = class_mapper(getattr(rattail, y))
|
||||
|
||||
dep_x = []
|
||||
table_y = map_y.tables[0].name
|
||||
|
|
@ -98,7 +98,7 @@ class Synchronizer(object):
|
|||
|
||||
while True:
|
||||
try:
|
||||
local_session = edbob.Session()
|
||||
local_session = db.Session()
|
||||
local_changes = local_session.query(rattail.Change)
|
||||
|
||||
if local_changes.count():
|
||||
|
|
@ -118,13 +118,13 @@ class Synchronizer(object):
|
|||
remote_sessions = []
|
||||
for remote_engine in engines.itervalues():
|
||||
remote_sessions.append(
|
||||
edbob.Session(bind=remote_engine))
|
||||
db.Session(bind=remote_engine))
|
||||
|
||||
for class_name in class_names:
|
||||
|
||||
for change in local_changes.filter_by(class_name=class_name):
|
||||
log.debug("synchronize_changes: processing change: %s" % change)
|
||||
cls = getattr(edbob, change.class_name)
|
||||
cls = getattr(rattail, change.class_name)
|
||||
|
||||
if change.deleted:
|
||||
for remote_session in remote_sessions:
|
||||
|
|
@ -278,9 +278,9 @@ def synchronize_changes(engines):
|
|||
sync.synchronizer_class = myapp.sync:MySynchronizer
|
||||
"""
|
||||
|
||||
cls = edbob.config.get('rattail.db', 'sync.synchronizer_class')
|
||||
cls = rattail.config.get('rattail.db', 'sync.synchronizer_class')
|
||||
if cls:
|
||||
cls = edbob.load_spec(cls)
|
||||
cls = load_spec(cls)
|
||||
else:
|
||||
cls = Synchronizer
|
||||
sync = cls()
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@
|
|||
``rattail.db.sync.linux`` -- Database Synchronization for Linux
|
||||
"""
|
||||
|
||||
from edbob.daemon import Daemon
|
||||
|
||||
from rattail.daemon import Daemon
|
||||
from rattail.db.sync import get_sync_engines, synchronize_changes
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -30,9 +30,8 @@ import sys
|
|||
import logging
|
||||
import threading
|
||||
|
||||
import edbob
|
||||
from edbob.win32 import Service
|
||||
|
||||
import rattail
|
||||
from rattail.win32 import Service
|
||||
from rattail.db.sync import get_sync_engines, synchronize_changes
|
||||
|
||||
|
||||
|
|
@ -50,8 +49,6 @@ class DatabaseSynchronizerService(Service):
|
|||
"and synchronizes them to the configured remote "
|
||||
"database(s).")
|
||||
|
||||
appname = 'rattail'
|
||||
|
||||
def Initialize(self):
|
||||
"""
|
||||
Service initialization.
|
||||
|
|
@ -60,7 +57,7 @@ class DatabaseSynchronizerService(Service):
|
|||
if not Service.Initialize(self):
|
||||
return False
|
||||
|
||||
edbob.init_modules(['rattail.db'])
|
||||
rattail.init_modules(['rattail.db'])
|
||||
|
||||
engines = get_sync_engines()
|
||||
if not engines:
|
||||
|
|
|
|||
|
|
@ -23,34 +23,60 @@
|
|||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.sil.sqlalchemy`` -- SQLAlchemy Utilities
|
||||
"""
|
||||
``rattail.db.types`` -- Custom Data Types
|
||||
|
||||
from __future__ import absolute_import
|
||||
This module contains various data type engines for use with SQLAlchemy.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from sqlalchemy import types
|
||||
from sqlalchemy import String, Numeric, Boolean
|
||||
from sqlalchemy.types import TypeDecorator, BigInteger
|
||||
|
||||
from rattail.db import GPCType
|
||||
from rattail.barcodes import GPC
|
||||
from rattail.exceptions import SILInvalidDataType
|
||||
|
||||
|
||||
__all__ = ['get_sqlalchemy_type']
|
||||
|
||||
__all__ = ['GPCType']
|
||||
|
||||
sil_type_pattern = re.compile(r'^(CHAR|NUMBER)\((\d+(?:\,\d+)?)\)$')
|
||||
|
||||
|
||||
def get_sqlalchemy_type(sil_type):
|
||||
class GPCType(TypeDecorator):
|
||||
"""
|
||||
Type engine for GPC data.
|
||||
|
||||
This class is responsible for converting values to and from
|
||||
:class:`rattail.GPC` instances. The underlying data type used to store
|
||||
values in the database is ``sqlalchemy.types.BigInteger``.
|
||||
|
||||
The primary use for this class is to store the :attr:`rattail.Product.upc`
|
||||
field. It also is used for batches when a ``F01`` column is involved.
|
||||
"""
|
||||
|
||||
impl = BigInteger
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is None:
|
||||
return None
|
||||
return int(value)
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is None:
|
||||
return None
|
||||
return GPC(value)
|
||||
|
||||
|
||||
def get_sil_column_type(sil_type):
|
||||
"""
|
||||
Returns a SQLAlchemy data type according to a SIL data type.
|
||||
"""
|
||||
|
||||
if sil_type == 'GPC(14)':
|
||||
return GPCType
|
||||
return GPCType()
|
||||
|
||||
if sil_type == 'FLAG(1)':
|
||||
return types.Boolean
|
||||
return Boolean()
|
||||
|
||||
m = sil_type_pattern.match(sil_type)
|
||||
if m:
|
||||
|
|
@ -64,8 +90,8 @@ def get_sqlalchemy_type(sil_type):
|
|||
scale = int(scale)
|
||||
if data_type == 'CHAR':
|
||||
assert not scale, "FIXME"
|
||||
return types.String(precision)
|
||||
return String(precision)
|
||||
if data_type == 'NUMBER':
|
||||
return types.Numeric(precision, scale)
|
||||
return Numeric(precision, scale)
|
||||
|
||||
assert False, "FIXME"
|
||||
raise SILInvalidDataType(sil_type)
|
||||
154
rattail/db/util.py
Normal file
154
rattail/db/util.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.db.util`` -- Database Utilities
|
||||
"""
|
||||
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy.exc import OperationalError, ProgrammingError
|
||||
|
||||
import rattail
|
||||
from rattail import db
|
||||
from rattail.db.model import Product, Setting
|
||||
from rattail.exceptions import NoDefaultDatabase
|
||||
|
||||
|
||||
def get_core_metadata():
|
||||
"""
|
||||
Returns the core Rattail schema metadata.
|
||||
|
||||
:returns: SQLAlchemy ``MetaData`` containing the core Rattail schema.
|
||||
:rtype: ``sqlalchemy.MetaData`` instance
|
||||
"""
|
||||
|
||||
from rattail.db import model
|
||||
|
||||
meta = MetaData()
|
||||
for name in model.__all__:
|
||||
obj = getattr(model, name)
|
||||
if (isinstance(obj, type)
|
||||
and len(obj.__bases__) == 1
|
||||
and obj.__bases__[0] is model.Base):
|
||||
obj.__table__.tometadata(meta)
|
||||
return meta
|
||||
|
||||
|
||||
def install_core_schema(engine=None):
|
||||
"""
|
||||
Installs the core Rattail schema to a database.
|
||||
|
||||
:param engine: A SQLAlchemy engine. If none is provided,
|
||||
:attr:`rattail.db.engine` is assumed.
|
||||
:type engine: ``sqlalchemy.engine.Engine`` instance, or ``None``
|
||||
|
||||
:raises: :class:`rattail.exceptions.NoDefaultDatabase`, if no engine is
|
||||
provided, nor is one found in configuration. Other errors are also
|
||||
possible; see below.
|
||||
|
||||
:returns: ``None``
|
||||
|
||||
Prior to installing the schema, a simple connection is attempted to the
|
||||
database engine. This is intentionally done with no exception handling, so
|
||||
that any connection issues may be raised directly to the caller.
|
||||
|
||||
.. note::
|
||||
For all engines *except* SQLite, the underlying database must already
|
||||
exist. This function is only responsible for adding the schema to an
|
||||
existing database.
|
||||
"""
|
||||
|
||||
if engine is None:
|
||||
engine = db.engine
|
||||
if engine is None:
|
||||
raise NoDefaultDatabase()
|
||||
|
||||
# Attempt connection in order to force an error, if applicable.
|
||||
conn = engine.connect()
|
||||
conn.close()
|
||||
|
||||
# Create tables for core schema.
|
||||
meta = get_core_metadata()
|
||||
meta.create_all(engine)
|
||||
|
||||
|
||||
def core_schema_installed(engine=None):
|
||||
"""
|
||||
Checks whether the core schema has been installed to a database.
|
||||
|
||||
:param engine: A SQLAlchemy engine. If none is provided,
|
||||
:attr:`rattail.db.engine` is assumed.
|
||||
:type engine: ``sqlalchemy.engine.Engine`` instance, or ``None``
|
||||
|
||||
:returns: ``True`` if the core schema appears to be installed; otherwise
|
||||
``False``.
|
||||
:rtype: boolean
|
||||
"""
|
||||
|
||||
if engine is None:
|
||||
engine = db.engine
|
||||
if engine is None:
|
||||
raise NoDefaultDatabase()
|
||||
|
||||
# Check database existence and/or connectivity.
|
||||
try:
|
||||
conn = engine.connect()
|
||||
except OperationalError:
|
||||
return False
|
||||
else:
|
||||
conn.close()
|
||||
|
||||
# Issue dummy query to verify core table existence.
|
||||
session = db.Session(bind=engine)
|
||||
try:
|
||||
session.query(Product).count()
|
||||
except (OperationalError, ProgrammingError):
|
||||
return False
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_setting(session, name):
|
||||
"""
|
||||
Returns a setting from the database.
|
||||
"""
|
||||
|
||||
setting = session.query(Setting).get(name)
|
||||
if setting:
|
||||
return setting.value
|
||||
return None
|
||||
|
||||
|
||||
def save_setting(session, name, value):
|
||||
"""
|
||||
Saves a setting to the database.
|
||||
"""
|
||||
|
||||
setting = session.query(Setting).get(name)
|
||||
if not setting:
|
||||
setting = Setting(name=name)
|
||||
session.add(setting)
|
||||
setting.value = value
|
||||
|
|
@ -70,6 +70,19 @@ UNIT_OF_MEASURE = {
|
|||
}
|
||||
|
||||
|
||||
EMAIL_PREFERENCE_NONE = 0
|
||||
EMAIL_PREFERENCE_TEXT = 1
|
||||
EMAIL_PREFERENCE_HTML = 2
|
||||
EMAIL_PREFERENCE_MOBILE = 3
|
||||
|
||||
EMAIL_PREFERENCE = {
|
||||
EMAIL_PREFERENCE_NONE : "No Emails",
|
||||
EMAIL_PREFERENCE_TEXT : "Text",
|
||||
EMAIL_PREFERENCE_HTML : "HTML",
|
||||
EMAIL_PREFERENCE_MOBILE : "Mobile",
|
||||
}
|
||||
|
||||
|
||||
EMPLOYEE_STATUS_CURRENT = 1
|
||||
EMPLOYEE_STATUS_FORMER = 2
|
||||
|
||||
|
|
|
|||
113
rattail/errors.py
Normal file
113
rattail/errors.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.errors`` -- Error Alert Emails
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
import socket
|
||||
import logging
|
||||
from traceback import format_exception
|
||||
from cStringIO import StringIO
|
||||
|
||||
import rattail
|
||||
from rattail.mail import sendmail_with_config
|
||||
from rattail.files import resource_path
|
||||
from rattail.time import local_time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def init(config):
|
||||
"""
|
||||
Creates a system-wide exception hook which logs exceptions and emails them
|
||||
to configured recipient(s).
|
||||
"""
|
||||
|
||||
def excepthook(type, value, traceback):
|
||||
email_exception(type, value, traceback)
|
||||
sys.__excepthook__(type, value, traceback)
|
||||
|
||||
sys.excepthook = excepthook
|
||||
|
||||
|
||||
def email_exception(type=None, value=None, traceback=None):
|
||||
"""
|
||||
Sends an email containing a traceback to the configured recipient(s).
|
||||
"""
|
||||
|
||||
if not (type and value and traceback):
|
||||
type, value, traceback = sys.exc_info()
|
||||
|
||||
hostname = socket.gethostname()
|
||||
traceback = ''.join(format_exception(type, value, traceback))
|
||||
traceback = traceback.strip()
|
||||
data = {
|
||||
'host_name': hostname,
|
||||
'host_ip': socket.gethostbyname(hostname),
|
||||
'host_time': local_time(),
|
||||
'traceback': traceback,
|
||||
}
|
||||
|
||||
body, ctype = render_exception(data)
|
||||
ctype = rattail.config.get('rattail.errors', 'content_type', default=ctype)
|
||||
sendmail_with_config('errors', body, content_type=ctype)
|
||||
|
||||
|
||||
def render_exception(data):
|
||||
"""
|
||||
Renders the exception data using a Mako template if one is configured;
|
||||
otherwise as a simple string.
|
||||
"""
|
||||
|
||||
template = rattail.config.get('rattail.errors', 'template')
|
||||
if template:
|
||||
template = resource_path(template)
|
||||
if os.path.exists(template):
|
||||
|
||||
# Assume Mako template; render and return.
|
||||
from mako.template import Template
|
||||
template = Template(filename=template)
|
||||
return template.render(**data), 'text/html'
|
||||
|
||||
# If not a Mako template, return regular text with substitutions.
|
||||
body = StringIO()
|
||||
data['host_time'] = data['host_time'].strftime('%Y-%m-%d %H:%M:%S %Z%z')
|
||||
|
||||
body.write("""\
|
||||
An unhandled exception occurred.
|
||||
|
||||
Machine Name: %(host_name)s (%(host_ip)s)
|
||||
|
||||
Machine Time: %(host_time)s
|
||||
|
||||
%(traceback)s
|
||||
""" % data)
|
||||
|
||||
b = body.getvalue()
|
||||
body.close()
|
||||
|
||||
return b, 'text/plain'
|
||||
|
|
@ -27,6 +27,231 @@
|
|||
"""
|
||||
|
||||
|
||||
class LabelPrintingError(Exception):
|
||||
class RattailError(Exception):
|
||||
"""
|
||||
Base class for all Rattail exceptions.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class ConfigurationError(RattailError):
|
||||
"""
|
||||
Base class for configuration errors.
|
||||
"""
|
||||
|
||||
|
||||
class MissingConfiguration(ConfigurationError):
|
||||
"""
|
||||
Raised when the current configuration is missing a required value.
|
||||
"""
|
||||
|
||||
def __init__(self, section, option):
|
||||
self.section = section
|
||||
self.option = option
|
||||
|
||||
def __str__(self):
|
||||
return ("Configuration is missing a required option. Please define "
|
||||
"'%s' within the [%s] section of your configuration file." % (
|
||||
self.option, self.section))
|
||||
|
||||
|
||||
class InitializationError(RattailError):
|
||||
"""
|
||||
Base class for initialization errors.
|
||||
"""
|
||||
|
||||
|
||||
class ModuleHasNoInit(InitializationError):
|
||||
"""
|
||||
Raised when ``init()`` is attempted for a given module, but the module has
|
||||
no ``init()`` function.
|
||||
"""
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
def __str__(self):
|
||||
return "Module has no init() function: %s" % self.module.__name__
|
||||
|
||||
|
||||
class MailError(RattailError):
|
||||
"""
|
||||
Base class for all mail-related errors.
|
||||
"""
|
||||
|
||||
|
||||
class SenderNotFound(MailError):
|
||||
"""
|
||||
Raised when no sender could be found in config.
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def __str__(self):
|
||||
return "No sender configured (set 'sender.%s' or 'sender.default' in [rattail.mail])" % self.key
|
||||
|
||||
|
||||
class RecipientsNotFound(MailError):
|
||||
"""
|
||||
Raised when no recipients could be found in config.
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def __str__(self):
|
||||
return "No recipients configured (set 'recipients.%s' or 'recipients.default' in [rattail.mail])" % self.key
|
||||
|
||||
|
||||
class LoadSpecError(RattailError):
|
||||
"""
|
||||
Base class for all errors relating to dynamically loading Python objects
|
||||
from "spec" strings.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidSpecString(LoadSpecError):
|
||||
"""
|
||||
Raised when a "spec" string is not valid.
|
||||
"""
|
||||
|
||||
def __init__(self, spec):
|
||||
self.spec = spec
|
||||
|
||||
def __str__(self):
|
||||
return "Python spec string is not valid: %s" % self.spec
|
||||
|
||||
|
||||
class ModuleMissingAttribute(InvalidSpecString):
|
||||
"""
|
||||
Raised during :func:`rattail.modules.load_spec()` when the module
|
||||
referenced by the spec is imported okay, but the attribute referenced by
|
||||
the spec cannot be found.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
msg = super(ModuleMissingAttribute, self).__str__()
|
||||
module, attribute = self.spec.split(':')
|
||||
return "%s (module '%s' was loaded but attribute '%s' not found)" % (
|
||||
msg, module, attribute)
|
||||
|
||||
|
||||
class NoDefaultDatabase(ConfigurationError):
|
||||
"""
|
||||
Raised when configuration does not specify a default database. This may be
|
||||
the case if no database configuration is present, or, if multiple databases
|
||||
are confiured, if none can be identified as the default engine.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return ("A default database engine could not be determined from the "
|
||||
"configuration file(s).")
|
||||
|
||||
|
||||
class SchemaExtensionNotFound(ConfigurationError):
|
||||
"""
|
||||
Raised when a schema extension is required to exist, but cannot be found.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
return "The schema extension '%s' could not be located." % self.name
|
||||
|
||||
|
||||
class SILError(RattailError):
|
||||
"""
|
||||
Base class for all SIL errors.
|
||||
"""
|
||||
|
||||
|
||||
class SILColumnNotFound(SILError):
|
||||
"""
|
||||
Raised when a SIL column is requested, but does not exist in the global
|
||||
dictionary of supported columns.
|
||||
"""
|
||||
|
||||
def __init__(self, column_name):
|
||||
self.column_name = column_name
|
||||
|
||||
def __str__(self):
|
||||
return "SIL column not found: %s" % self.column_name
|
||||
|
||||
|
||||
class SILInvalidDataType(SILError):
|
||||
"""
|
||||
Raised when a SIL data type string is not supported.
|
||||
"""
|
||||
|
||||
def __init__(self, data_type):
|
||||
self.data_type = data_type
|
||||
|
||||
def __str__(self):
|
||||
return "Invalid SIL data type: %s" % self.data_type
|
||||
|
||||
|
||||
class BatchError(RattailError):
|
||||
"""
|
||||
Base class for all batch-related errors.
|
||||
"""
|
||||
|
||||
|
||||
class BatchTypeNotFound(BatchError):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
return "Batch type not found: %s" % self.name
|
||||
|
||||
|
||||
class BatchTypeNotSupported(BatchError):
|
||||
"""
|
||||
Raised when a :class:`rattail.db.batches.BatchExecutor` instance is asked
|
||||
to execute a batch, but the batch is of an unsupported type.
|
||||
"""
|
||||
|
||||
def __init__(self, executor, batch_type):
|
||||
self.executor = executor
|
||||
self.batch_type = batch_type
|
||||
|
||||
def __str__(self):
|
||||
return "Batch type '%s' is not supported by executor: %s" % (
|
||||
self.batch_type, repr(self.executor))
|
||||
|
||||
|
||||
class BatchNotSaved(BatchError):
|
||||
"""
|
||||
Raised when an operation is requested on a :class:`rattail.Batch` instance,
|
||||
but the instance has not yet been committed to the database.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return ("Batch is pending; you must flush it to the database before "
|
||||
"the operation you requested may proceed.")
|
||||
|
||||
|
||||
class BatchProviderNotFound(BatchError):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
return "Batch provider not found: %s" % self.name
|
||||
|
||||
|
||||
class BatchDestinationNotSupported(BatchError):
|
||||
|
||||
def __init__(self, batch):
|
||||
self.batch = batch
|
||||
|
||||
def __str__(self):
|
||||
return "Destination '%s' not supported for batch: %s" % (
|
||||
self.batch.destination, self.batch.description)
|
||||
|
||||
|
||||
class LabelPrintingError(RattailError):
|
||||
"""
|
||||
Base class for all label printing errors.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.filemon`` -- Windows File Monitor
|
||||
"""
|
||||
|
||||
from edbob.filemon.win32 import FileMonitorService
|
||||
|
||||
|
||||
class RattailFileMonitor(FileMonitorService):
|
||||
|
||||
_svc_name_ = "RattailFileMonitor"
|
||||
_svc_display_name_ = "Rattail : File Monitoring Service"
|
||||
|
||||
appname = 'rattail'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import win32serviceutil
|
||||
win32serviceutil.HandleCommandLine(RattailFileMonitor)
|
||||
112
rattail/filemon/__init__.py
Normal file
112
rattail/filemon/__init__.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.filemon`` -- File Monitoring Service
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import logging
|
||||
|
||||
import rattail
|
||||
from rattail.modules import load_spec
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MonitorProfile(object):
|
||||
"""
|
||||
This is a simple profile class, used to represent configuration of the file
|
||||
monitor service.
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
self.dirs = rattail.config.require('rattail.filemon', '%s.dirs' % key)
|
||||
self.dirs = eval(self.dirs)
|
||||
|
||||
actions = rattail.config.require('rattail.filemon', '%s.actions' % key)
|
||||
actions = eval(actions)
|
||||
|
||||
self.actions = []
|
||||
for action in actions:
|
||||
if isinstance(action, tuple):
|
||||
spec = action[0]
|
||||
args = list(action[1:])
|
||||
else:
|
||||
spec = action
|
||||
args = []
|
||||
func = load_spec(spec)
|
||||
self.actions.append((spec, func, args))
|
||||
|
||||
self.locks = rattail.config.getboolean(
|
||||
'rattail.filemon', '%s.locks' % key, default=False)
|
||||
|
||||
|
||||
def get_monitor_profiles():
|
||||
"""
|
||||
Convenience function to load monitor profiles from config.
|
||||
"""
|
||||
|
||||
monitored = {}
|
||||
|
||||
# Read monitor profile(s) from config.
|
||||
keys = rattail.config.require('rattail.filemon', 'monitored')
|
||||
keys = keys.split(',')
|
||||
for key in keys:
|
||||
key = key.strip()
|
||||
profile = MonitorProfile(key)
|
||||
monitored[key] = profile
|
||||
for path in profile.dirs[:]:
|
||||
|
||||
# Ensure the monitored path exists.
|
||||
if not os.path.exists(path):
|
||||
log.warning("get_monitor_profiles: Profile '%s' has nonexistent "
|
||||
"path, which will be pruned: %s" % (key, path))
|
||||
profile.dirs.remove(path)
|
||||
|
||||
# Ensure the monitored path is a folder.
|
||||
elif not os.path.isdir(path):
|
||||
log.warning("get_monitor_profiles: Profile '%s' has non-folder "
|
||||
"path, which will be pruned: %s" % (key, path))
|
||||
profile.dirs.remove(path)
|
||||
|
||||
for key in monitored.keys():
|
||||
profile = monitored[key]
|
||||
|
||||
# Prune any profiles with no valid folders to monitor.
|
||||
if not profile.dirs:
|
||||
log.warning("get_monitor_profiles: Profile '%s' has no folders to "
|
||||
"monitor, and will be pruned." % key)
|
||||
del monitored[key]
|
||||
|
||||
# Prune any profiles with no valid actions to perform.
|
||||
elif not profile.actions:
|
||||
log.warning("get_monitor_profiles: Profile '%s' has no actions to "
|
||||
"perform, and will be pruned." % key)
|
||||
del monitored[key]
|
||||
|
||||
return monitored
|
||||
142
rattail/filemon/linux.py
Normal file
142
rattail/filemon/linux.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.filemon.linux`` -- File Monitor for Linux
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import os.path
|
||||
import signal
|
||||
import logging
|
||||
import pyinotify
|
||||
|
||||
import rattail
|
||||
from rattail.filemon import get_monitor_profiles
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventHandler(pyinotify.ProcessEvent):
|
||||
"""
|
||||
Event processor for file monitor daemon.
|
||||
"""
|
||||
|
||||
def my_init(self, actions=[], locks=False, **kwargs):
|
||||
self.actions = actions
|
||||
self.locks = locks
|
||||
|
||||
def process_IN_ACCESS(self, event):
|
||||
log.debug("EventHandler: IN_ACCESS: %s" % event.pathname)
|
||||
|
||||
def process_IN_ATTRIB(self, event):
|
||||
log.debug("EventHandler: IN_ATTRIB: %s" % event.pathname)
|
||||
|
||||
def process_IN_CLOSE_WRITE(self, event):
|
||||
log.debug("EventHandler: IN_CLOSE_WRITE: %s" % event.pathname)
|
||||
if not self.locks:
|
||||
self.perform_actions(event.pathname)
|
||||
|
||||
def process_IN_CREATE(self, event):
|
||||
log.debug("EventHandler: IN_CREATE: %s" % event.pathname)
|
||||
|
||||
def process_IN_DELETE(self, event):
|
||||
log.debug("EventHandler: IN_DELETE: %s" % event.pathname)
|
||||
if self.locks and event.pathname.endswith('.lock'):
|
||||
self.perform_actions(event.pathname[:-5])
|
||||
|
||||
def process_IN_MODIFY(self, event):
|
||||
log.debug("EventHandler: IN_MODIFY: %s" % event.pathname)
|
||||
|
||||
def process_IN_MOVED_TO(self, event):
|
||||
log.debug("EventHandler: IN_MOVED_TO: %s" % event.pathname)
|
||||
if not self.locks:
|
||||
self.perform_actions(event.pathname)
|
||||
|
||||
def perform_actions(self, path):
|
||||
for spec, func, args in self.actions:
|
||||
func(path, *args)
|
||||
|
||||
|
||||
def get_pid_path():
|
||||
"""
|
||||
Returns the path to the PID file for the file monitor daemon.
|
||||
"""
|
||||
|
||||
pid_path = rattail.config.get('rattail.filemon', 'pid_path')
|
||||
if not pid_path:
|
||||
pid_path = '/tmp/rattail_filemon.pid'
|
||||
return pid_path
|
||||
|
||||
|
||||
def start_daemon(daemonize=True):
|
||||
"""
|
||||
Starts the file monitor daemon.
|
||||
"""
|
||||
|
||||
pid_path = get_pid_path()
|
||||
if os.path.exists(pid_path):
|
||||
print "File monitor is already running"
|
||||
return
|
||||
|
||||
wm = pyinotify.WatchManager()
|
||||
notifier = pyinotify.Notifier(wm)
|
||||
|
||||
monitored = get_monitor_profiles()
|
||||
|
||||
mask = (pyinotify.IN_ACCESS | pyinotify.IN_ATTRIB
|
||||
| pyinotify.IN_CLOSE_WRITE | pyinotify.IN_CREATE
|
||||
| pyinotify.IN_DELETE | pyinotify.IN_MODIFY
|
||||
| pyinotify.IN_MOVED_TO)
|
||||
for profile in monitored.itervalues():
|
||||
for path in profile.dirs:
|
||||
wm.add_watch(path, mask, proc_fun=EventHandler(
|
||||
actions=profile.actions, locks=profile.locks))
|
||||
|
||||
if not daemonize:
|
||||
sys.stderr.write("Starting file monitor. (Press Ctrl+C to quit.)\n")
|
||||
notifier.loop(daemonize=daemonize, pid_file=pid_path)
|
||||
|
||||
|
||||
def stop_daemon():
|
||||
"""
|
||||
Stops the file monitor daemon.
|
||||
"""
|
||||
|
||||
pid_path = get_pid_path()
|
||||
if not os.path.exists(pid_path):
|
||||
print "File monitor is not running"
|
||||
return
|
||||
|
||||
f = open(pid_path)
|
||||
pid = f.read().strip()
|
||||
f.close()
|
||||
if not pid.isdigit():
|
||||
log.warning("stop_daemon: Found bogus PID (%s) in file: %s" % (pid, pid_path))
|
||||
return
|
||||
|
||||
os.kill(int(pid), signal.SIGKILL)
|
||||
os.remove(pid_path)
|
||||
177
rattail/filemon/win32.py
Normal file
177
rattail/filemon/win32.py
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.filemon.win32`` -- File Monitoring Service for Windows
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
import Queue
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from rattail.errors import email_exception
|
||||
from rattail.win32 import Service, file_is_free
|
||||
from rattail.filemon import get_monitor_profiles
|
||||
|
||||
if sys.platform == 'win32':
|
||||
import win32api
|
||||
import win32con
|
||||
import win32event
|
||||
import win32file
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
import winnt
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RattailFileMonitor(Service):
|
||||
"""
|
||||
Implements Rattail's file monitor Windows service.
|
||||
"""
|
||||
|
||||
_svc_name_ = "RattailFileMonitor"
|
||||
_svc_display_name_ = "Rattail : File Monitoring Service"
|
||||
_svc_description_ = ("Monitors one or more folders for incoming files, "
|
||||
"and performs configured actions as new files arrive.")
|
||||
|
||||
def Initialize(self):
|
||||
"""
|
||||
Service initialization.
|
||||
"""
|
||||
|
||||
if not Service.Initialize(self):
|
||||
return False
|
||||
|
||||
# Read monitor profile(s) from config.
|
||||
self.monitored = get_monitor_profiles()
|
||||
|
||||
# Make sure we have something to do.
|
||||
if not self.monitored:
|
||||
return False
|
||||
|
||||
# Create monitor and action threads for each profile.
|
||||
for key, profile in self.monitored.iteritems():
|
||||
|
||||
# Create a file queue for the profile.
|
||||
queue = Queue.Queue()
|
||||
|
||||
# Create a monitor thread for each folder in profile.
|
||||
for i, path in enumerate(profile.dirs, 1):
|
||||
name = 'monitor-%s-%u' % (key, i)
|
||||
log.debug("Initialize: Starting '%s' thread for folder: %s" %
|
||||
(name, path))
|
||||
thread = threading.Thread(
|
||||
target=monitor_files,
|
||||
name=name,
|
||||
args=(queue, path, profile))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
# Create an action thread for the profile.
|
||||
name = 'actions-%s' % key
|
||||
log.debug("Initialize: Starting '%s' thread" % name)
|
||||
thread = threading.Thread(
|
||||
target=perform_actions,
|
||||
name=name,
|
||||
args=(queue, profile))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def monitor_files(queue, path, profile):
|
||||
"""
|
||||
Callable target for file monitor threads.
|
||||
"""
|
||||
|
||||
hDir = win32file.CreateFile(
|
||||
path,
|
||||
winnt.FILE_LIST_DIRECTORY,
|
||||
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE,
|
||||
None,
|
||||
win32con.OPEN_EXISTING,
|
||||
win32con.FILE_FLAG_BACKUP_SEMANTICS,
|
||||
None)
|
||||
|
||||
if hDir == win32file.INVALID_HANDLE_VALUE:
|
||||
log.warning("monitor_files: Can't open directory with CreateFile(): %s" % path)
|
||||
return
|
||||
|
||||
while True:
|
||||
results = win32file.ReadDirectoryChangesW(
|
||||
hDir,
|
||||
1024,
|
||||
False,
|
||||
win32con.FILE_NOTIFY_CHANGE_FILE_NAME)
|
||||
|
||||
log.debug("monitor_files: ReadDirectoryChangesW() results: %s" % results)
|
||||
for action, fname in results:
|
||||
fpath = os.path.join(path, fname)
|
||||
if action in (winnt.FILE_ACTION_ADDED,
|
||||
winnt.FILE_ACTION_RENAMED_NEW_NAME):
|
||||
log.debug("monitor_files: Queueing '%s' file: %s" %
|
||||
(profile.key, fpath))
|
||||
queue.put(fpath)
|
||||
|
||||
|
||||
def perform_actions(queue, profile):
|
||||
"""
|
||||
Callable target for action threads.
|
||||
"""
|
||||
|
||||
while True:
|
||||
|
||||
try:
|
||||
path = queue.get_nowait()
|
||||
except Queue.Empty:
|
||||
pass
|
||||
else:
|
||||
|
||||
while not file_is_free(path):
|
||||
win32api.Sleep(0)
|
||||
|
||||
for spec, func, args in profile.actions:
|
||||
|
||||
log.info("perform_actions: Calling function '%s' on file: %s" %
|
||||
(spec, path))
|
||||
|
||||
try:
|
||||
func(path, *args)
|
||||
|
||||
except:
|
||||
log.exception("perform_actions: An exception occurred "
|
||||
"while processing file: %s" % path)
|
||||
email_exception()
|
||||
|
||||
# This file probably shouldn't be processed any further.
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
win32serviceutil.HandleCommandLine(RattailFileMonitor)
|
||||
172
rattail/files.py
Normal file
172
rattail/files.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.files`` -- File Utilities
|
||||
|
||||
This module contains various utility functions for use with the filesystem.
|
||||
"""
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import tempfile
|
||||
import lockfile
|
||||
from datetime import datetime
|
||||
|
||||
import pkg_resources
|
||||
|
||||
|
||||
__all__ = ['count_lines', 'creation_time', 'locking_copy', 'resource_path',
|
||||
'temp_path']
|
||||
|
||||
|
||||
class DosFile(file):
|
||||
"""
|
||||
Subclass of ``file`` which uses DOS line endings when writing the file.
|
||||
"""
|
||||
|
||||
def write(self, string):
|
||||
super(DosFile, self).write(string.replace(os.linesep, '\r\n'))
|
||||
|
||||
|
||||
def count_lines(path):
|
||||
"""
|
||||
Counts the number of lines in a text file. Some attempt is made to ensure
|
||||
cross-platform compatibility.
|
||||
|
||||
:param path: Path to the file.
|
||||
:type path: string
|
||||
|
||||
:returns: Number of lines in the file.
|
||||
:rtype: integer
|
||||
"""
|
||||
|
||||
f = open(path, 'rb')
|
||||
lines = f.read().count('\n') + 1
|
||||
f.close()
|
||||
return lines
|
||||
|
||||
|
||||
def creation_time(path):
|
||||
"""
|
||||
Returns the "naive" (i.e. not timezone-aware) creation timestamp for a
|
||||
file.
|
||||
|
||||
:param path: Path to the file.
|
||||
:type path: string
|
||||
|
||||
:returns: The creation timestamp for the file.
|
||||
:rtype: ``datetime.datetime`` instance
|
||||
"""
|
||||
|
||||
time = os.path.getctime(path)
|
||||
return datetime.fromtimestamp(time)
|
||||
|
||||
|
||||
def locking_copy(src, dst):
|
||||
"""
|
||||
Implements a "locking" version of ``shutil.copy()``.
|
||||
|
||||
This function exists to provide a more atomic method for copying a file
|
||||
into a folder which is being watched by a file monitor. The assumption is
|
||||
that the monitor is configured to expect "locks" and therefore only process
|
||||
files once they have had their locks removed.
|
||||
|
||||
:param src: Path to the source file.
|
||||
:type src: string
|
||||
|
||||
:param dst: Path to the destination file (or directory).
|
||||
:type dst: string
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
if os.path.isdir(dst):
|
||||
fn = os.path.basename(src)
|
||||
dst = os.path.join(dst, fn)
|
||||
|
||||
with lockfile.FileLock(dst):
|
||||
shutil.copy(src, dst)
|
||||
|
||||
|
||||
def overwriting_move(src, dst):
|
||||
"""
|
||||
Convenience function which is equivalent to ``shutil.move()``, except it
|
||||
will cause the destination file to be overwritten if it exists.
|
||||
"""
|
||||
|
||||
if os.path.isdir(dst):
|
||||
dst = os.path.join(dst, os.path.basename(src))
|
||||
if os.path.exists(dst):
|
||||
os.remove(dst)
|
||||
shutil.move(src, dst)
|
||||
|
||||
|
||||
def resource_path(path):
|
||||
"""
|
||||
Obtain a resource file path, extracting the resource and/or coercing the
|
||||
path as necessary.
|
||||
|
||||
:param path: May be either a package resource specifier, or a regular file
|
||||
path.
|
||||
:type path: string
|
||||
|
||||
:returns: Absolute file path to the resource.
|
||||
:rtype: string
|
||||
|
||||
If ``path`` is a package resource specifier, and the package containing it
|
||||
is a zipped egg, then the resource will be extracted and the resultant
|
||||
filename will be returned.
|
||||
"""
|
||||
|
||||
if not os.path.isabs(path) and ':' in path:
|
||||
return pkg_resources.resource_filename(*path.split(':'))
|
||||
return path
|
||||
|
||||
|
||||
def temp_path(suffix='.tmp', prefix='rattail.'):
|
||||
"""
|
||||
Returns a (presumably "safe" and unique) temporary file path. This path
|
||||
will be located in the user's temp folder.
|
||||
|
||||
.. note::
|
||||
Really, the meaning of the parameters is the same as for
|
||||
``tempfile.mkstemp()``, which is called under the hood.
|
||||
|
||||
:param suffix: Suffix for the filename. This must include the "dot" if you
|
||||
wish it to come through as a proper extension.
|
||||
:type suffix: string
|
||||
|
||||
:param prefix: Prefix for the filename.
|
||||
:type prefix: string
|
||||
|
||||
:returns: Path to the temporary file.
|
||||
:rtype: string
|
||||
"""
|
||||
|
||||
fd, path = tempfile.mkstemp(suffix=suffix, prefix=prefix)
|
||||
os.close(fd)
|
||||
os.remove(path)
|
||||
return path
|
||||
|
|
@ -26,76 +26,15 @@
|
|||
``rattail.gpc`` -- Global Product Code
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from rattail import barcodes
|
||||
|
||||
|
||||
class GPC(object):
|
||||
"""
|
||||
Class to abstract the details of Global Product Code data. Examples of
|
||||
this would be UPC or EAN barcodes.
|
||||
class GPC(barcodes.GPC):
|
||||
|
||||
The initial motivation for this class was to provide better SIL support.
|
||||
To that end, the instances are assumed to always be comprised of only
|
||||
numeric digits, and must include a check digit. If you do not know the
|
||||
check digit, provide a ``calc_check_digit`` value to the constructor.
|
||||
"""
|
||||
|
||||
def __init__(self, value, calc_check_digit=False):
|
||||
"""
|
||||
Constructor. ``value`` must be either an integer or a long value, or a
|
||||
string containing only digits.
|
||||
|
||||
If ``calc_check_digit`` is ``False``, then ``value`` is assumed to
|
||||
include the check digit. If the value does not include a check digit
|
||||
and needs one to be calculated, then ``calc_check_digit`` should be a
|
||||
keyword signifying the algorithm to be used.
|
||||
|
||||
Currently the only check digit algorithm keyword supported is
|
||||
``'upc'``. As that is likely to always be the default, a
|
||||
``calc_check_digit`` value of ``True`` will be perceived as equivalent
|
||||
to ``'upc'``.
|
||||
"""
|
||||
|
||||
value = str(value)
|
||||
if calc_check_digit is True or calc_check_digit == 'upc':
|
||||
value += str(barcodes.upc_check_digit(value))
|
||||
self.value = int(value)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
return int(self) == int(other)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
try:
|
||||
return int(self) != int(other)
|
||||
except (TypeError, ValueError):
|
||||
return True
|
||||
|
||||
def __cmp__(self, other):
|
||||
if int(self) < int(other):
|
||||
return -1
|
||||
if int(self) > int(other):
|
||||
return 1
|
||||
if int(self) == int(other):
|
||||
return 0
|
||||
assert False
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.value)
|
||||
|
||||
def __int__(self):
|
||||
return int(self.value)
|
||||
|
||||
def __long__(self):
|
||||
return long(self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return "GPC('%014d')" % self.value
|
||||
|
||||
def __str__(self):
|
||||
return str(unicode(self))
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%014d' % self.value
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn("Using 'rattail.gpc.GPC' is deprecated; "
|
||||
"please use 'rattail.GPC' instead.",
|
||||
DeprecationWarning)
|
||||
super(GPC, self).__init__(*args, **kwargs)
|
||||
|
|
|
|||
136
rattail/initialization.py
Normal file
136
rattail/initialization.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.initialization`` -- Framework Initialization
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import rattail
|
||||
from rattail.exceptions import ModuleHasNoInit
|
||||
from rattail.configuration import (
|
||||
RattailConfigParser, default_system_paths, default_user_paths)
|
||||
|
||||
|
||||
__all__ = ['init', 'init_modules']
|
||||
|
||||
inited = []
|
||||
|
||||
|
||||
def init(*args, **kwargs):
|
||||
"""
|
||||
Initialize the running application.
|
||||
|
||||
This function is primarily responsible for reading the configuration
|
||||
file(s) which determine the particulars of how the app should behave. In
|
||||
addition to this (depending on what the config files dictate), various
|
||||
modules may be asked to initialize as well.
|
||||
|
||||
:param \*args: The meaning of ``args`` is as follows:
|
||||
|
||||
If ``args`` is empty, the ``RATTAIL_CONFIG`` environment variable is
|
||||
first consulted. If it is nonempty, then its value is split according
|
||||
to ``os.pathsep`` and the resulting sequence is passed to
|
||||
:meth:`rattail.configuration.RattailConfigParser.read()`.
|
||||
|
||||
If both ``args`` and ``RATTAIL_CONFIG`` are empty, the "standard"
|
||||
locations are assumed, and the results of calling both
|
||||
:func:`rattail.configuration.default_system_paths()` and
|
||||
:func:`rattail.configuration.default_user_paths()` are passed on to
|
||||
:meth:`rattail.configuration.RattailConfigParser.read()`.
|
||||
|
||||
If ``args`` consists only of a single value of ``None``, then no
|
||||
configuration files will be read.
|
||||
|
||||
Any other values in ``args`` will be passed directly to
|
||||
:meth:`rattail.configuration.RattailConfigParser.read()`` and so will be
|
||||
interpreted there. Basically they are assumed to be either strings, or
|
||||
sequences of strings, which represent paths to various config files,
|
||||
each being read in the order in which it appears within ``args``.
|
||||
(Configuration is thereby cascaded such that the file read last will
|
||||
override those before it.)
|
||||
:type args: zero or more strings
|
||||
|
||||
:param \*\*kwargs: Keyword arguments are currently used only for testing.
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
config = RattailConfigParser()
|
||||
|
||||
if args:
|
||||
if len(args) == 1 and args[0] is None:
|
||||
config_paths = []
|
||||
else:
|
||||
config_paths = list(args)
|
||||
elif os.environ.get('RATTAIL_CONFIG'):
|
||||
config_paths = os.environ['RATTAIL_CONFIG'].split(os.pathsep)
|
||||
else: # pragma: no cover
|
||||
config_paths = default_system_paths() + default_user_paths()
|
||||
|
||||
service = kwargs.get('service')
|
||||
if service:
|
||||
config.read_service(service, config_paths)
|
||||
else:
|
||||
for paths in config_paths:
|
||||
config.read(paths)
|
||||
if not kwargs.get('testing'):
|
||||
config.configure_logging()
|
||||
|
||||
rattail.config = config
|
||||
inited.append('rattail')
|
||||
|
||||
|
||||
def init_modules(modules, config=None):
|
||||
"""
|
||||
Initialize the given modules, if they haven't yet been. Any modules which
|
||||
have previously been initialized will be silently skipped.
|
||||
|
||||
:param modules: Sequence of strings, each of which must be a dotted module
|
||||
name which points to a Python module containing an ``init()`` function.
|
||||
:type modules: sequence
|
||||
|
||||
:param config: Configuration object which will be passed to the modules'
|
||||
``init()`` function. If none is specified, then :attr:`rattail.config`
|
||||
will be used.
|
||||
:type config: ``ConfigParser`` instance, or ``None``
|
||||
|
||||
:raises: :class:`rattail.exceptions.ModuleHasNoInit`, when a module is
|
||||
encountered which contains no ``init()`` function.
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
if config is None:
|
||||
config = rattail.config
|
||||
|
||||
for name in modules:
|
||||
name = name.strip()
|
||||
if name and name not in inited:
|
||||
module = __import__(name, globals(), locals(), fromlist=['init'])
|
||||
if not hasattr(module, 'init'):
|
||||
raise ModuleHasNoInit(module)
|
||||
getattr(module, 'init')(config)
|
||||
inited.append(name)
|
||||
|
|
@ -32,13 +32,13 @@ import socket
|
|||
import shutil
|
||||
from cStringIO import StringIO
|
||||
|
||||
import edbob
|
||||
from edbob.util import OrderedDict, requires_impl
|
||||
|
||||
from rattail.exceptions import LabelPrintingError
|
||||
from rattail.util import Object, OrderedDict
|
||||
from rattail.files import temp_path
|
||||
from rattail.time import local_time
|
||||
|
||||
|
||||
class LabelPrinter(edbob.Object):
|
||||
class LabelPrinter(Object):
|
||||
"""
|
||||
Base class for all label printers.
|
||||
|
||||
|
|
@ -52,13 +52,12 @@ class LabelPrinter(edbob.Object):
|
|||
formatter = None
|
||||
required_settings = None
|
||||
|
||||
@requires_impl()
|
||||
def print_labels(self, labels, *args, **kwargs):
|
||||
"""
|
||||
Prints labels found in ``labels``.
|
||||
"""
|
||||
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CommandPrinter(LabelPrinter):
|
||||
|
|
@ -115,7 +114,7 @@ class CommandFilePrinter(CommandPrinter):
|
|||
if not output_dir:
|
||||
raise LabelPrintingError("Printer does not have an output folder defined")
|
||||
|
||||
labels_path = edbob.temp_path(prefix='rattail.', suffix='.labels')
|
||||
labels_path = temp_path('.labels')
|
||||
labels_file = open(labels_path, 'w')
|
||||
|
||||
header = self.batch_header_commands()
|
||||
|
|
@ -136,7 +135,7 @@ class CommandFilePrinter(CommandPrinter):
|
|||
|
||||
labels_file.close()
|
||||
fn = '%s_%s.labels' % (socket.gethostname(),
|
||||
edbob.local_time().strftime('%Y-%m-%d_%H-%M-%S'))
|
||||
local_time().strftime('%Y-%m-%d_%H-%M-%S'))
|
||||
final_path = os.path.join(output_dir, fn)
|
||||
shutil.move(labels_path, final_path)
|
||||
return final_path
|
||||
|
|
@ -202,7 +201,7 @@ class CommandNetworkPrinter(CommandPrinter):
|
|||
data.close()
|
||||
|
||||
|
||||
class LabelFormatter(edbob.Object):
|
||||
class LabelFormatter(Object):
|
||||
"""
|
||||
Base class for all label formatters.
|
||||
"""
|
||||
|
|
@ -219,13 +218,12 @@ class LabelFormatter(edbob.Object):
|
|||
|
||||
raise NotImplementedError
|
||||
|
||||
@requires_impl()
|
||||
def format_labels(self, labels, progress=None, *args, **kwargs):
|
||||
"""
|
||||
Formats ``labels`` and returns the result.
|
||||
"""
|
||||
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CommandFormatter(LabelFormatter):
|
||||
|
|
@ -276,9 +274,8 @@ class CommandFormatter(LabelFormatter):
|
|||
|
||||
return None
|
||||
|
||||
@requires_impl()
|
||||
def label_body_commands(self):
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
def label_footer_commands(self):
|
||||
"""
|
||||
|
|
@ -300,14 +297,13 @@ class TwoUpCommandFormatter(CommandFormatter):
|
|||
"""
|
||||
|
||||
@property
|
||||
@requires_impl(is_property=True)
|
||||
def half_offset(self):
|
||||
"""
|
||||
The X-coordinate value by which the second label should be offset, when
|
||||
two labels are printed side-by-side.
|
||||
"""
|
||||
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
def format_labels(self, labels, progress=None):
|
||||
prog = None
|
||||
|
|
|
|||
|
|
@ -23,13 +23,5 @@
|
|||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.extension`` -- Database Extension
|
||||
``rattail.lib`` -- Third-Party Libraries
|
||||
"""
|
||||
|
||||
from edbob.db.extensions import Extension
|
||||
|
||||
|
||||
class RattailExtension(Extension):
|
||||
|
||||
name = 'rattail'
|
||||
required_extensions = ['contact', 'auth']
|
||||
76
rattail/lib/pretty.py
Normal file
76
rattail/lib/pretty.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
pretty
|
||||
|
||||
Formats dates, numbers, etc. in a pretty, human readable format.
|
||||
"""
|
||||
__author__ = "S Anand (sanand@s-anand.net)"
|
||||
__copyright__ = "Copyright 2010, S Anand"
|
||||
__license__ = "WTFPL"
|
||||
|
||||
|
||||
# Note that some modifications exist between this file and the original as
|
||||
# downloaded from http://pypi.python.org/pypi/py-pretty
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from rattail.time import utc_time
|
||||
|
||||
|
||||
def _df(seconds, denominator=1, text='', past=True):
|
||||
if past: return str((seconds + denominator/2)/ denominator) + text + ' ago'
|
||||
else: return 'in ' + str((seconds + denominator/2)/ denominator) + text
|
||||
|
||||
|
||||
def date(time=False, asdays=False, short=False):
|
||||
'''Returns a pretty formatted date.
|
||||
Inputs:
|
||||
time is a datetime object or an int timestamp
|
||||
asdays is True if you only want to measure days, not seconds
|
||||
short is True if you want "1d ago", "2d ago", etc. False if you want
|
||||
'''
|
||||
|
||||
now = utc_time()
|
||||
if type(time) is int: time = datetime.fromtimestamp(time)
|
||||
elif not time: time = now
|
||||
|
||||
if time > now: past, diff = False, time - now
|
||||
else: past, diff = True, now - time
|
||||
seconds = diff.seconds
|
||||
days = diff.days
|
||||
|
||||
if short:
|
||||
if days == 0 and not asdays:
|
||||
if seconds < 10: return 'now'
|
||||
elif seconds < 60: return _df(seconds, 1, 's', past)
|
||||
elif seconds < 3600: return _df(seconds, 60, 'm', past)
|
||||
else: return _df(seconds, 3600, 'h', past)
|
||||
else:
|
||||
if days == 0: return 'today'
|
||||
elif days == 1: return past and 'yest' or 'tom'
|
||||
elif days < 7: return _df(days, 1, 'd', past)
|
||||
elif days < 31: return _df(days, 7, 'w', past)
|
||||
elif days < 365: return _df(days, 30, 'mo', past)
|
||||
else: return _df(days, 365, 'y', past)
|
||||
else:
|
||||
if days == 0 and not asdays:
|
||||
if seconds < 10: return 'now'
|
||||
elif seconds < 60: return _df(seconds, 1, ' seconds', past)
|
||||
elif seconds < 120: return past and 'a minute ago' or 'in a minute'
|
||||
elif seconds < 3600: return _df(seconds, 60, ' minutes', past)
|
||||
elif seconds < 7200: return past and 'an hour ago' or'in an hour'
|
||||
else: return _df(seconds, 3600, ' hours', past)
|
||||
else:
|
||||
if days == 0: return 'today'
|
||||
elif days == 1: return past and 'yesterday' or'tomorrow'
|
||||
elif days == 2: return past and 'day before' or 'day after'
|
||||
elif days < 7: return _df(days, 1, ' days', past)
|
||||
elif days < 14: return past and 'last week' or 'next week'
|
||||
elif days < 31: return _df(days, 7, ' weeks', past)
|
||||
elif days < 61: return past and 'last month' or 'next month'
|
||||
elif days < 365: return _df(days, 30, ' months', past)
|
||||
elif days < 730: return past and 'last year' or 'next year'
|
||||
else: return _df(days, 365, ' years', past)
|
||||
139
rattail/mail.py
Normal file
139
rattail/mail.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.mail`` -- Email Framework
|
||||
"""
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
from email.message import Message
|
||||
|
||||
import rattail
|
||||
from rattail.exceptions import SenderNotFound, RecipientsNotFound
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sendmail(sender, recipients, subject, body, content_type='text/plain'):
|
||||
"""
|
||||
Sends an email message from ``sender`` to each address in ``recipients``
|
||||
(which should be a sequence), with subject ``subject`` and body ``body``.
|
||||
|
||||
If sending an HTML message instead of plain text, be sure to set
|
||||
``content_type`` to ``'text/html'``.
|
||||
"""
|
||||
message = Message()
|
||||
message.set_type(content_type)
|
||||
message['From'] = sender
|
||||
for recipient in recipients:
|
||||
message['To'] = recipient
|
||||
message['Subject'] = subject
|
||||
message.set_payload(body)
|
||||
|
||||
server = rattail.config.get('rattail.mail', 'smtp.server',
|
||||
default='localhost')
|
||||
username = rattail.config.get('rattail.mail', 'smtp.username')
|
||||
password = rattail.config.get('rattail.mail', 'smtp.password')
|
||||
|
||||
log.debug("sendmail: connecting to server: %s" % server)
|
||||
session = smtplib.SMTP(server)
|
||||
if username and password:
|
||||
res = session.login(username, password)
|
||||
log.debug("sendmail: login result is: %s" % str(res))
|
||||
res = session.sendmail(message['From'], message.get_all('To'), message.as_string())
|
||||
log.debug("sendmail: sendmail result is: %s" % res)
|
||||
session.quit()
|
||||
|
||||
|
||||
def get_sender(key):
|
||||
sender = rattail.config.get('rattail.mail', 'sender.'+key)
|
||||
if sender:
|
||||
return sender
|
||||
sender = rattail.config.get('rattail.mail', 'sender.default')
|
||||
if sender:
|
||||
return sender
|
||||
raise SenderNotFound(key)
|
||||
|
||||
|
||||
def get_recipients(key):
|
||||
recips = rattail.config.get('rattail.mail', 'recipients.'+key)
|
||||
if recips:
|
||||
return eval(recips)
|
||||
recips = rattail.config.get('rattail.mail', 'recipients.default')
|
||||
if recips:
|
||||
return eval(recips)
|
||||
raise RecipientsNotFound(key)
|
||||
|
||||
|
||||
def get_subject(key):
|
||||
subject = rattail.config.get('rattail.mail', 'subject.'+key)
|
||||
if subject:
|
||||
return subject
|
||||
subject = rattail.config.get('rattail.mail', 'subject.default')
|
||||
if subject:
|
||||
return subject
|
||||
return "[Rattail]"
|
||||
|
||||
|
||||
def sendmail_with_config(key, body, subject=None, **kwargs):
|
||||
"""
|
||||
.. highlight:: ini
|
||||
|
||||
Sends mail using sender/recipient/subject values found in config, according
|
||||
to ``key``. Probably the easiest way to explain would be to show an example::
|
||||
|
||||
[rattail.mail]
|
||||
smtp.server = localhost
|
||||
sender.default = Lance Edgar <lance@edbob.org>
|
||||
subject.default = A Nice Shrubbery, Not Too Expensive
|
||||
|
||||
recipients.nightly_reports = ['Lance Edgar <lance@edbob.org>']
|
||||
|
||||
subject.tragic_errors = The World Is Nearing The End!!
|
||||
recipients.tragic_errors = ['Lance Edgar <lance@edbob.org>']
|
||||
|
||||
Anything not configured explicitly will fall back to defaults where
|
||||
possible. Note however that a sender and recipients (default or otherwise)
|
||||
*must* be found or else an exception will be raised.
|
||||
|
||||
The above does not include a default recipient list, but it would work the same
|
||||
as the subject and sender as far as the key goes. To send these mails then::
|
||||
|
||||
from rattail.mail import sendmail_with_config
|
||||
|
||||
# just a report
|
||||
sendmail_with_config('nightly_reports', open('report.txt').read())
|
||||
|
||||
# you might want to sit down for this one...
|
||||
sendmail_with_config('tragic_errors', open('OMGWTFBBQ.txt').read())
|
||||
|
||||
If you do not provide ``subject`` to this function, and there is no
|
||||
``subject.default`` setting found in config, a default of "[Rattail]" will
|
||||
be used.
|
||||
"""
|
||||
if subject is None:
|
||||
subject = get_subject(key)
|
||||
return sendmail(get_sender(key), get_recipients(key), subject, body, **kwargs)
|
||||
159
rattail/modules.py
Normal file
159
rattail/modules.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.modules`` -- Python Module Tools
|
||||
|
||||
This module contains various utilities for working with Python modules.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from rattail.exceptions import InvalidSpecString, ModuleMissingAttribute
|
||||
|
||||
|
||||
def graft(module, source, names=None):
|
||||
"""
|
||||
Grafts attributes from a source namespace onto a target namespace.
|
||||
|
||||
:param module: Module onto which the attributes should be grafted.
|
||||
:type module: module
|
||||
|
||||
:param source: Source which contains the attributes to be grafted.
|
||||
:type source: module or dictionary
|
||||
|
||||
:param names: If provided, must be either a string attribute name, or a
|
||||
sequence of string attribute names. If it not provided, then
|
||||
"everything" from ``source`` will be grafted. (See note below.)
|
||||
:type names: varies
|
||||
|
||||
:returns: ``None``
|
||||
|
||||
.. note::
|
||||
If "everything" is to be grafted (i.e. ``names`` is ``None``), and
|
||||
``source`` is a module, then ``source.__all__`` will be consulted if
|
||||
available. If the module does not contain an ``__all__`` attribute,
|
||||
then ``dir(source)`` will be used instead. Or if ``source`` is a
|
||||
dictionary, its keys will be used to determine what should be grafted.
|
||||
"""
|
||||
|
||||
if names is None:
|
||||
if isinstance(source, dict):
|
||||
names = source.keys()
|
||||
elif hasattr(source, '__all__'):
|
||||
names = source.__all__
|
||||
else:
|
||||
names = [x for x in dir(source) if not x.startswith('_')]
|
||||
elif isinstance(names, basestring):
|
||||
names = [names]
|
||||
|
||||
for name in names:
|
||||
if hasattr(source, name):
|
||||
setattr(module, name, getattr(source, name))
|
||||
else:
|
||||
setattr(module, name, source.get(name))
|
||||
if not hasattr(module, '__all__'):
|
||||
module.__all__ = []
|
||||
module.__all__.append(name)
|
||||
|
||||
|
||||
def import_module_path(module_path):
|
||||
"""
|
||||
Imports and returns an arbitrary Python module, given its "dotted" path
|
||||
(i.e. not its file path).
|
||||
"""
|
||||
|
||||
if module_path in sys.modules:
|
||||
return sys.modules[module_path]
|
||||
module = __import__(module_path)
|
||||
return last_module(module, module_path)
|
||||
|
||||
|
||||
def last_module(module, module_path):
|
||||
"""
|
||||
Returns the "last" module represented by ``module_path``, by walking
|
||||
``module`` until the desired module is found.
|
||||
|
||||
For example, passing a reference to the ``rattail`` module and a module
|
||||
path of ``"rattail.sw.ishida.slpv"``, this function will ultimately return
|
||||
a reference to the actual ``rattail.sw.ishida.slpv`` module.
|
||||
|
||||
This function is primarily used by :func:`import_module_path()`, since
|
||||
Python's ``__import__()`` function will typically return the top-most
|
||||
("first") module in the dotted path.
|
||||
"""
|
||||
|
||||
parts = module_path.split('.')
|
||||
parts.pop(0)
|
||||
child = getattr(module, parts[0])
|
||||
if len(parts) == 1:
|
||||
return child
|
||||
return last_module(child, '.'.join(parts))
|
||||
|
||||
|
||||
def load_spec(spec):
|
||||
"""
|
||||
.. highlight:: none
|
||||
|
||||
Returns an object as found in a module namespace. ``spec`` should be of
|
||||
the same form which setuptools uses for its entry points, e.g.::
|
||||
|
||||
rattail.ce:collect_batch
|
||||
|
||||
The above would return a reference to the ``collect_batch`` function found
|
||||
in the ``rattail.ce`` module namespace. The module is loaded (imported) if
|
||||
necessary.
|
||||
"""
|
||||
|
||||
if spec.count(':') != 1:
|
||||
raise InvalidSpecString(spec)
|
||||
|
||||
module_path, attr = spec.split(':')
|
||||
module = import_module_path(module_path)
|
||||
if not hasattr(module, attr):
|
||||
raise ModuleMissingAttribute(spec)
|
||||
return getattr(module, attr)
|
||||
|
||||
|
||||
def prune(module, names):
|
||||
"""
|
||||
Remove one or more attributes from a module. This is the complement of
|
||||
:func:`graft()`, and is meant to undo its changes. (Mostly this exists for
|
||||
the sake of tests.)
|
||||
|
||||
:param module: Module from which attributes should be removed.
|
||||
:type module: module
|
||||
|
||||
:param names: One or more attribute names. A single name may be passed as
|
||||
a string; otherwise a sequence is expected.
|
||||
:type names: sequence or string
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
if isinstance(names, basestring):
|
||||
names = [names]
|
||||
for name in names:
|
||||
module.__all__.remove(name)
|
||||
del module.__dict__[name]
|
||||
|
|
@ -24,6 +24,8 @@
|
|||
|
||||
"""
|
||||
``rattail.pricing`` -- Pricing Utilities
|
||||
|
||||
This module contains various functions for price calculations.
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -32,18 +34,38 @@ __all__ = ['gross_margin']
|
|||
|
||||
def gross_margin(price, cost):
|
||||
"""
|
||||
Calculate and return a gross margin percentage based on ``price`` and
|
||||
``cost``.
|
||||
Calculate and return a gross margin percentage.
|
||||
|
||||
If ``price`` is empty (or zero), returns ``None``.
|
||||
:param price: The retail price.
|
||||
:type price: float or ``decimal.Decimal`` instance
|
||||
|
||||
If ``cost`` is empty (or zero), returns ``100``.
|
||||
:param cost: The wholesale cost.
|
||||
:type cost: float or ``decimal.Decimal`` instance
|
||||
|
||||
:returns: The gross margin percentage, or ``None``.
|
||||
:rtype: varies
|
||||
|
||||
If ``price`` is empty or zero, this function will return ``None``.
|
||||
|
||||
If ``cost`` is empty or zero: If ``price`` is a ``float`` instance, this
|
||||
function will return ``100.0``; otherwise it will return ``100``.
|
||||
|
||||
If both ``price`` and ``cost`` are non-empty, a true calculation will
|
||||
be performed. The result will be of the same type as the inputs.
|
||||
|
||||
.. note::
|
||||
If the type of inputs do not match (e.g. one is a ``float`` instance and
|
||||
the other a ``decimal.Decimal`` instance), the underlying error which
|
||||
results from attempting to do the math will not be handled and will
|
||||
raise to the caller.
|
||||
"""
|
||||
|
||||
if not price:
|
||||
return None
|
||||
|
||||
if not cost:
|
||||
if isinstance(price, float):
|
||||
return 100.0
|
||||
return 100
|
||||
|
||||
return 100 * (price - cost) / price
|
||||
|
|
|
|||
|
|
@ -23,14 +23,22 @@
|
|||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.sil`` -- Standard Interchange Language
|
||||
``rattail.sil`` -- SIL Support
|
||||
|
||||
This module provides support for writing, and otherwise interacting with,
|
||||
Standard Interchange Language (SIL) elements and files. Its purpose is
|
||||
primarily to support writing files which target various external systems for
|
||||
the sake of integration. However, the notion of SIL columns is also used in
|
||||
Rattail's native batch framework.
|
||||
|
||||
The *eventual* goal is to add support for reading SIL files also, to extend the
|
||||
integration concept so that external systems may create batches within Rattail.
|
||||
|
||||
Please see the `Standard Interchange Language Specifications
|
||||
<http://productcatalog.gs1us.org/Store/tabid/86/CategoryID/21/List/1/catpageindex/2/Level/a/ProductID/46/Default.aspx>`_
|
||||
for more information.
|
||||
for more information about SIL itself.
|
||||
"""
|
||||
|
||||
from rattail.sil.columns import *
|
||||
from rattail.sil.batches import *
|
||||
from rattail.sil.sqlalchemy import *
|
||||
from rattail.sil.writer import *
|
||||
|
|
|
|||
|
|
@ -23,34 +23,46 @@
|
|||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.sil.batches`` -- Batch Stuff
|
||||
``rattail.sil.batches`` -- SIL Batch Utilities
|
||||
"""
|
||||
|
||||
import edbob
|
||||
from rattail.configuration import UserConfigFileStorage
|
||||
|
||||
|
||||
__all__ = ['consume_batch_id']
|
||||
|
||||
|
||||
def consume_batch_id(source='RATAIL'):
|
||||
def consume_batch_id(source='RATAIL', storage=None):
|
||||
"""
|
||||
Returns the next available batch identifier for ``source``, incrementing
|
||||
the number to preserve uniqueness.
|
||||
Returns the next available SIL batch identifier for ``source``,
|
||||
incrementing the number to preserve uniqueness.
|
||||
|
||||
:param source: The SIL-compliant source name for the batch. If none is
|
||||
explicitly provided, ``'RATAIL'`` is assumed.
|
||||
:type source: string
|
||||
|
||||
:param storage: The engine which implements storage of the batch
|
||||
identifier. This engine will be consulted to determine the next
|
||||
available identifier, and the incremented identifier value will be
|
||||
written to it as well.
|
||||
|
||||
If no storage engine is specified, an instance of
|
||||
:class:`rattail.configuration.UserConfigFileStorage` will be created to
|
||||
handle the request.
|
||||
:type storage: :class:`rattail.configuration.Storage` instance, or
|
||||
``None``
|
||||
|
||||
:returns: A numeric batch identifier, zero-padded to 8 digits.
|
||||
:rtype: string
|
||||
"""
|
||||
|
||||
option = 'next_batch_id.%s' % source
|
||||
if not storage: # pragma: no cover
|
||||
storage = UserConfigFileStorage()
|
||||
|
||||
config = edbob.AppConfigParser('rattail')
|
||||
config_path = config.get_user_file('rattail.conf', create=True)
|
||||
config.read(config_path)
|
||||
|
||||
batch_id = config.get('rattail.sil', option, default='')
|
||||
option = 'next_batch_id.%s' % source.lower()
|
||||
batch_id = storage.get('rattail.sil', option, default='')
|
||||
if not batch_id.isdigit():
|
||||
batch_id = '1'
|
||||
batch_id = int(batch_id)
|
||||
|
||||
config.set('rattail.sil', option, str(batch_id + 1))
|
||||
config_file = open(config_path, 'w')
|
||||
config.write(config_file)
|
||||
config_file.close()
|
||||
return '%08u' % batch_id
|
||||
storage.put('rattail.sil', option, str(batch_id + 1))
|
||||
return '%08d' % batch_id
|
||||
|
|
|
|||
|
|
@ -26,35 +26,70 @@
|
|||
``rattail.sil.columns`` -- SIL Columns
|
||||
"""
|
||||
|
||||
import edbob
|
||||
from edbob.util import entry_point_map
|
||||
import pkg_resources
|
||||
|
||||
from rattail.sil.exceptions import SILColumnNotFound
|
||||
from rattail.exceptions import SILColumnNotFound
|
||||
|
||||
|
||||
__all__ = ['get_column']
|
||||
__all__ = ['Column', 'get_column']
|
||||
|
||||
|
||||
supported_columns = None
|
||||
|
||||
|
||||
class SILColumn(edbob.Object):
|
||||
class Column(object):
|
||||
"""
|
||||
Represents a column for use with SIL.
|
||||
Represents a column for use with SIL. The following attributes exist for
|
||||
``Column`` instances, most of which must be provided to the constructor:
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
The SIL-compliant name for the column as a string, e.g. ``'F01'``.
|
||||
|
||||
.. attribute:: data_type
|
||||
|
||||
The SIL-compliant data type for the column as a string. Per the SIL
|
||||
specifications, there are six primary data types available. The
|
||||
following examples highlight the possibilities which exist for this
|
||||
attribute:
|
||||
|
||||
* ``'GPC(14)'``
|
||||
* ``'CHAR(30)'``
|
||||
* ``'NUMBER(9,5)'``
|
||||
* ``'DATE(7)'``
|
||||
* ``'TIME(4)'``
|
||||
* ``'FLAG(1)'``
|
||||
|
||||
.. note::
|
||||
Only the ``'CHAR'`` and ``'NUMBER'`` data types allow arbitrary size
|
||||
parameters, and only the ``'NUMBER'`` type allows an arbitrary
|
||||
precision parameter. All other columns must be specified exactly as
|
||||
listed above. Again, see the official SIL language documentation for
|
||||
more info.
|
||||
|
||||
.. attribute:: description
|
||||
|
||||
The human-readable description for the column, e.g. ``"Department
|
||||
Number"``.
|
||||
|
||||
.. attribute:: display_name
|
||||
|
||||
The name used when displaying the column within the user interface. If
|
||||
this attribute is not explicitly provided, then :attr:`description` will
|
||||
be used instead.
|
||||
"""
|
||||
|
||||
def __init__(self, name, data_type, description, display_name=None, **kwargs):
|
||||
edbob.Object.__init__(self, **kwargs)
|
||||
def __init__(self, name, data_type, description, display_name=None):
|
||||
self.name = name
|
||||
self.data_type = data_type
|
||||
self.description = description
|
||||
self.display_name = display_name or description
|
||||
|
||||
def __repr__(self):
|
||||
return "<SILColumn: %s>" % self.name
|
||||
return "<Column: %s>" % self.name
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.name)
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
def provide_columns():
|
||||
|
|
@ -62,7 +97,7 @@ def provide_columns():
|
|||
Provides all SIL columns natively supported by Rattail.
|
||||
"""
|
||||
|
||||
SC = SILColumn
|
||||
SC = Column
|
||||
|
||||
standard = [ # These columns are part of the SIL standard.
|
||||
|
||||
|
|
@ -135,15 +170,25 @@ def provide_columns():
|
|||
|
||||
def get_column(name):
|
||||
"""
|
||||
Returns the :class:`SILColumn` instance named ``name``.
|
||||
Consults the global dictionary of supported columns, looking for a column
|
||||
named ``name``. If found, it is returned; otherwise an error is raised.
|
||||
|
||||
:param name: The SIL-compliant name of the column, e.g. ``'F01'``.
|
||||
:type name: string
|
||||
|
||||
:raises: :class:`rattail.exceptions.SILColumnNotFound`, if the column
|
||||
cannot be found.
|
||||
|
||||
:returns: :class:`Column` instance
|
||||
"""
|
||||
|
||||
global supported_columns
|
||||
|
||||
if supported_columns is None:
|
||||
supported_columns = {}
|
||||
providers = entry_point_map('rattail.sil.column_providers')
|
||||
for provider in providers.itervalues():
|
||||
for provider in pkg_resources.iter_entry_points(
|
||||
'rattail.sil.column_providers'):
|
||||
provider = provider.load()
|
||||
supported_columns.update(provider())
|
||||
|
||||
column = supported_columns.get(name)
|
||||
|
|
|
|||
|
|
@ -29,51 +29,100 @@
|
|||
import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import edbob
|
||||
|
||||
import rattail
|
||||
from rattail.gpc import GPC
|
||||
|
||||
|
||||
__all__ = ['Writer']
|
||||
|
||||
|
||||
class Writer(edbob.Object):
|
||||
class Writer(object):
|
||||
"""
|
||||
This class contains logic for writing SIL files.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
:param path: Path to the SIL file which is to be written.
|
||||
:type path: string
|
||||
|
||||
The new instance will immediately open a file object for writing, using
|
||||
the path provided.
|
||||
"""
|
||||
|
||||
def __init__(self, path, **kwargs):
|
||||
edbob.Object.__init__(self, **kwargs)
|
||||
self.sil_path = path
|
||||
self.fileobj = self.get_fileobj()
|
||||
|
||||
def get_fileobj(self):
|
||||
"""
|
||||
Should return a file-like object to which textual commands and data
|
||||
will be written. This method is called by the constructor.
|
||||
|
||||
This may be overriden within a subclass if needed. The default
|
||||
implementation simply opens the SIL file path (passed to the
|
||||
constructor) with write access.
|
||||
|
||||
:returns: File-like object.
|
||||
:rtype: object
|
||||
"""
|
||||
|
||||
return open(self.sil_path, 'w')
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the underlying file object. You should call this when you have
|
||||
finished writing the file.
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
self.fileobj.close()
|
||||
|
||||
def write(self, string):
|
||||
"""
|
||||
Writes an arbitrary string to the underlying file object.
|
||||
|
||||
:param string: String to be written.
|
||||
:type string: string
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
self.fileobj.write(string)
|
||||
|
||||
def val(self, value):
|
||||
"""
|
||||
Returns a string version of ``value``, suitable for inclusion within a
|
||||
data row of a SIL batch. The conversion is done as follows:
|
||||
Converts a value to a string, suitable for inclusion within a data row
|
||||
of a SIL batch.
|
||||
|
||||
:param value: Arbitrary value to be converted.
|
||||
:type value: any
|
||||
|
||||
:returns: String version of ``value``, possibly quoted and escaped to
|
||||
be SIL-compliant.
|
||||
:rtype: string
|
||||
|
||||
The conversion is done as follows:
|
||||
|
||||
If ``value`` is ``None``, an empty string is returned.
|
||||
|
||||
If it is an ``int`` or ``decimal.Decimal`` instance, it is converted
|
||||
directly to a string (i.e. not quoted).
|
||||
If ``value`` is an ``int`` or ``decimal.Decimal`` instance, it is
|
||||
converted directly to a string (i.e. not quoted).
|
||||
|
||||
If it is a ``datetime.date`` instance, it will be formatted as
|
||||
``'%Y%j'``.
|
||||
If ``value`` is a ``datetime.date`` instance, it will be formatted as a
|
||||
7-digit Julian date (``'%Y%j'``).
|
||||
|
||||
If it is a ``datetime.time`` instance, it will be formatted as
|
||||
``'%H%M'``.
|
||||
If ``value`` is a ``datetime.time`` instance, it will be formatted as a
|
||||
4-digit numeric-only value (``'%H%M'``).
|
||||
|
||||
Otherwise, it is converted to a string if necessary, and quoted with
|
||||
apostrophes escaped.
|
||||
Any other value will first be converted to a string (if it isn't
|
||||
already), and quoted with apostrophes escaped.
|
||||
"""
|
||||
|
||||
if value is None:
|
||||
return ''
|
||||
if isinstance(value, GPC):
|
||||
if isinstance(value, rattail.GPC):
|
||||
return str(value)
|
||||
if isinstance(value, int):
|
||||
return str(value)
|
||||
|
|
@ -89,19 +138,62 @@ class Writer(edbob.Object):
|
|||
value = str(value)
|
||||
return "'%s'" % value.replace("'", "''")
|
||||
|
||||
def write(self, string):
|
||||
self.fileobj.write(string)
|
||||
def write_row(self, row, quote=True, last=False):
|
||||
"""
|
||||
Writes a SIL data row string to the underlying file object.
|
||||
|
||||
:param row: Sequence of values to be written.
|
||||
:type row: sequence
|
||||
|
||||
:param quote: Whether or not the values within ``row`` should be ran
|
||||
through the :meth:`val()` method (vs. writing as-is). Note that
|
||||
this parameter exists primarily for the header writing methods. It
|
||||
should typically be left alone for writing actual data rows.
|
||||
:type quote: boolean
|
||||
|
||||
:param last: Whether or not the row should be considered the last,
|
||||
within the current batch. This controls whether the "terminating"
|
||||
character within the text line being written will be a comma or a
|
||||
semi-colon.
|
||||
:type last: boolean
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
terminator = ';' if last else ','
|
||||
if quote:
|
||||
row = [self.val(x) for x in row]
|
||||
self.fileobj.write('(' + ','.join(row) + ')' + terminator + '\n')
|
||||
|
||||
def write_rows(self, rows):
|
||||
"""
|
||||
Writes a series of data row strings to the underlying file object.
|
||||
|
||||
:param rows: Sequence of sequences (!) containing the data to be
|
||||
written.
|
||||
:type rows: sequence
|
||||
|
||||
:returns: ``None``
|
||||
|
||||
This function simply provides a wrapper for :meth:`write_row()`. Its
|
||||
primary function is to handle the mundane task of setting the ``last``
|
||||
flag when calling ``write_row()``.
|
||||
"""
|
||||
|
||||
last = len(rows) - 1
|
||||
for i, row in enumerate(rows):
|
||||
self.write_row(row, last=i == last)
|
||||
|
||||
def write_batch_header_raw(self, **kwargs):
|
||||
"""
|
||||
Writes a SIL batch header string. All keyword arguments correspond to
|
||||
the SIL specification for the Batch Header Dictionary.
|
||||
Writes a SIL batch header string to the underlying file object.
|
||||
|
||||
**Batch Header Dictionary:**
|
||||
All keyword arguments correspond to the SIL specification for the Batch
|
||||
Header Dictionary:
|
||||
|
||||
==== ==== ==== ===========
|
||||
==== ====== ==== ===========
|
||||
Name Type Size Description
|
||||
==== ==== ==== ===========
|
||||
==== ====== ==== ===========
|
||||
H01 CHAR 2 Batch Type
|
||||
H02 CHAR 8 Batch Identifier
|
||||
H03 CHAR 6 Source Identifier
|
||||
|
|
@ -125,8 +217,11 @@ class Writer(edbob.Object):
|
|||
H21 CHAR 50 Primary Key
|
||||
H22 CHAR 512 System Specific Command
|
||||
H23 CHAR 8 Dictionary Revision
|
||||
==== ====== ==== ===========
|
||||
|
||||
Consult the SIL Specification for more information.
|
||||
Consult the SIL Specifications for more information.
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
kw = kwargs
|
||||
|
|
@ -167,28 +262,35 @@ class Writer(edbob.Object):
|
|||
self.val(kw.get('H23')),
|
||||
]
|
||||
|
||||
self.fileobj.write('INSERT INTO HEADER_DCT VALUES\n')
|
||||
self.write('INSERT INTO HEADER_DCT VALUES\n')
|
||||
self.write_row(row, quote=False, last=True)
|
||||
self.fileobj.write('\n')
|
||||
self.write('\n')
|
||||
|
||||
def write_batch_header(self, **kwargs):
|
||||
"""
|
||||
Convenience method to take some of the gruntwork out of writing batch
|
||||
Convenience method to take some of the grunt work out of writing batch
|
||||
headers.
|
||||
|
||||
If you do not override ``H03`` (Source Identifier), then Rattail will
|
||||
provide a default value for it, as well as ``H20`` (Software Revision)
|
||||
- that is, unless you've supplied it yourself.
|
||||
All SIL batch header keyword arguments are accepted, as with
|
||||
:meth:`write_batch_header_raw()`. However, this method provides some
|
||||
default values:
|
||||
|
||||
If you do not provide values for ``H07`` or ``H08``, the current date
|
||||
and time will be assumed.
|
||||
If you do not override ``H03`` (Source Identifier), then a default
|
||||
value of ``'RATAIL'`` will be used. If you do not override ``H03``
|
||||
*or* ``H20`` (Software Revision), then ``rattail.__version__`` will be
|
||||
used for the latter.
|
||||
|
||||
If you do not provide values for ``H09`` or ``H10``, it is assumed that
|
||||
you wish the batch to be immediately executable. Default values will
|
||||
be provided accordingly.
|
||||
If you do not override ``H07`` or ``H08`` (Origin Date and Time), the
|
||||
current date and time will be assumed.
|
||||
|
||||
If you do not provide a value for ``H11`` (Purge Date), a default of 90
|
||||
days from the current date will be assumed.
|
||||
If you do not override ``H09`` or ``H10`` (Execution Date and Time), it
|
||||
is assumed that you wish the batch to be immediately executable.
|
||||
Default values will be provided accordingly.
|
||||
|
||||
If you do not override ``H11`` (Purge Date), a default of 90 days from
|
||||
the current date will be assumed.
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
kw = kwargs
|
||||
|
|
@ -203,7 +305,7 @@ class Writer(edbob.Object):
|
|||
|
||||
# Provide default (current local time) values H07 and H08 (Origin Date /
|
||||
# Time) if none was specified.
|
||||
now = edbob.local_time()
|
||||
now = datetime.datetime.now()
|
||||
if 'H07' not in kw:
|
||||
kw['H07'] = now.date()
|
||||
if 'H08' not in kw:
|
||||
|
|
@ -224,16 +326,16 @@ class Writer(edbob.Object):
|
|||
|
||||
def write_create_header(self, **kwargs):
|
||||
"""
|
||||
Convenience method to take some of the gruntwork out of writing batch
|
||||
headers.
|
||||
Convenience method to write a batch creation header.
|
||||
|
||||
The following default values are provided by this method:
|
||||
All SIL batch header keyword arguments are accepted, as with
|
||||
:meth:`write_batch_header()`. However, this method provides some
|
||||
additional default values:
|
||||
|
||||
* ``H01`` = ``'HC'``
|
||||
* ``H12`` = ``'LOAD'``
|
||||
|
||||
This method also calls :meth:`write_batch_header()`; see its
|
||||
documentation for the other default values provided.
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
kw = kwargs
|
||||
|
|
@ -243,51 +345,17 @@ class Writer(edbob.Object):
|
|||
|
||||
def write_maintenance_header(self, **kwargs):
|
||||
"""
|
||||
Convenience method to take some of the gruntwork out of writing batch
|
||||
headers.
|
||||
Convenience method to write a batch maintenance header.
|
||||
|
||||
The following default values are provided by this method:
|
||||
All SIL batch header keyword arguments are accepted, as with
|
||||
:meth:`write_batch_header()`. However, this method provides some
|
||||
additional default values:
|
||||
|
||||
* ``H01`` = ``'HM'``
|
||||
|
||||
This method also calls :meth:`write_batch_header()`; see its
|
||||
documentation for the other default values provided.
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
kw = kwargs
|
||||
kw.setdefault('H01', 'HM')
|
||||
self.write_batch_header(**kw)
|
||||
|
||||
def write_row(self, row, quote=True, last=False):
|
||||
"""
|
||||
Writes a SIL row string.
|
||||
|
||||
``row`` should be a sequence of values.
|
||||
|
||||
If ``quote`` is ``True``, each value in ``row`` will be ran through the
|
||||
:func:`val()` function before being written. If it is ``False``, the
|
||||
values are written as-is.
|
||||
|
||||
If ``last`` is ``True``, then ``';'`` will be used as the statement
|
||||
terminator; otherwise ``','`` is used.
|
||||
"""
|
||||
|
||||
terminator = ';' if last else ','
|
||||
if quote:
|
||||
row = [self.val(x) for x in row]
|
||||
self.fileobj.write('(' + ','.join(row) + ')' + terminator + '\n')
|
||||
|
||||
def write_rows(self, rows):
|
||||
"""
|
||||
Writes a set of SIL row strings.
|
||||
|
||||
``rows`` should be a sequence of sequences, each of which should be
|
||||
suitable for use with :meth:`write_row()`.
|
||||
|
||||
(This funcion primarily exists to handle the mundane task of setting
|
||||
the ``last`` flag when calling :meth:`write_row()`.)
|
||||
"""
|
||||
|
||||
last = len(rows) - 1
|
||||
for i, row in enumerate(rows):
|
||||
self.write_row(row, last=i == last)
|
||||
|
|
|
|||
9
rattail/templates/errors/redmine.mako
Normal file
9
rattail/templates/errors/redmine.mako
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
An unhandled exception occurred.
|
||||
|
||||
*Machine Name:* ${host_name} (${host_ip})
|
||||
|
||||
*Machine Time:* ${host_time.strftime('%Y-%m-%d %H:%M:%S %Z%z')}
|
||||
|
||||
<pre>
|
||||
${traceback}
|
||||
</pre>
|
||||
21
rattail/tests/__init__.py
Normal file
21
rattail/tests/__init__.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from rattail.configuration import RattailConfigParser
|
||||
|
||||
|
||||
class CancelProgress(object):
|
||||
|
||||
def __init__(self, message, maximum):
|
||||
pass
|
||||
|
||||
def update(self, value):
|
||||
return False
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_config():
|
||||
config = RattailConfigParser()
|
||||
config.set('rattail', 'testing', 'True')
|
||||
return config
|
||||
1
rattail/tests/batches/__init__.py
Normal file
1
rattail/tests/batches/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
#!/usr/bin/env python
|
||||
1
rattail/tests/batches/providers/__init__.py
Normal file
1
rattail/tests/batches/providers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
#!/usr/bin/env python
|
||||
132
rattail/tests/batches/providers/test_init.py
Normal file
132
rattail/tests/batches/providers/test_init.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail.tests import CancelProgress
|
||||
|
||||
import datetime
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from rattail.batches import providers
|
||||
from rattail.exceptions import BatchProviderNotFound
|
||||
from rattail.db.model import Batch
|
||||
from rattail.db.util import install_core_schema
|
||||
|
||||
|
||||
class BogusProvider(providers.BatchProvider):
|
||||
|
||||
name = 'bogus'
|
||||
source = 'BOGUS'
|
||||
|
||||
def add_columns(self, batch):
|
||||
pass
|
||||
|
||||
def add_row(self, batch, datum):
|
||||
pass
|
||||
|
||||
|
||||
class CancelBegin(BogusProvider):
|
||||
|
||||
def add_rows_begin(self, batch, data):
|
||||
return False
|
||||
|
||||
|
||||
class CancelEnd(BogusProvider):
|
||||
|
||||
def add_rows_end(self, batch, progress=None):
|
||||
return False
|
||||
|
||||
|
||||
# class BatchProviderTest(unittest.TestCase):
|
||||
|
||||
# def test_add_columns(self):
|
||||
# provider = providers.BatchProvider()
|
||||
# batch = Batch()
|
||||
# self.assertEqual(len(batch.columns), 0)
|
||||
# self.assertRaises(NotImplementedError, provider.add_columns, batch)
|
||||
# self.assertEqual(len(batch.columns), 0)
|
||||
|
||||
# def test_add_rows_begin(self):
|
||||
# provider = CancelBegin()
|
||||
# self.assertEqual(provider.add_rows_begin(Batch(), []), False)
|
||||
# self.assertEqual(provider.add_rows(Batch(), []), False)
|
||||
|
||||
# def test_add_rows(self):
|
||||
# provider = BogusProvider()
|
||||
# self.assertEqual(provider.add_rows(Batch(), ['stuff'], CancelProgress), False)
|
||||
|
||||
# def test_add_rows_end(self):
|
||||
# provider = CancelEnd()
|
||||
# self.assertEqual(provider.add_rows_end(Batch()), False)
|
||||
# self.assertEqual(provider.add_rows(Batch(), []), False)
|
||||
# provider = BogusProvider()
|
||||
# self.assertEqual(provider.add_rows_end(Batch()), None)
|
||||
|
||||
# def test_make_batch(self):
|
||||
# engine = create_engine('sqlite://')
|
||||
# install_core_schema(engine)
|
||||
# Session = sessionmaker(bind=engine)
|
||||
# provider = BogusProvider()
|
||||
# session = Session()
|
||||
# batch = provider.make_batch(session, [], id='00000420')
|
||||
# self.assertEqual(batch.id, '00000420')
|
||||
# self.assertEqual(batch.provider, 'bogus')
|
||||
# self.assertEqual(batch.source, 'BOGUS')
|
||||
# session.rollback()
|
||||
# session.close()
|
||||
|
||||
# def test_make_batch_cancel_end(self):
|
||||
# engine = create_engine('sqlite://')
|
||||
# install_core_schema(engine)
|
||||
# Session = sessionmaker(bind=engine)
|
||||
# provider = CancelEnd()
|
||||
# session = Session()
|
||||
# batch = provider.make_batch(session, [], id='00000420')
|
||||
# self.assertEqual(batch, None)
|
||||
# session.rollback()
|
||||
# session.close()
|
||||
|
||||
# def test_execute(self):
|
||||
# provider = providers.BatchProvider()
|
||||
# self.assertRaises(NotImplementedError, provider.execute, Batch())
|
||||
|
||||
# def test_set_params(self):
|
||||
# provider = providers.BatchProvider()
|
||||
# Session = sessionmaker()
|
||||
# provider.set_params(Session())
|
||||
|
||||
# def test_set_purge_date(self):
|
||||
# provider = providers.BatchProvider()
|
||||
# batch = Batch()
|
||||
# provider.set_purge_date(batch)
|
||||
# self.assertEqual(batch.purge,
|
||||
# datetime.date.today() + datetime.timedelta(days=90))
|
||||
|
||||
|
||||
class GetProvidersTest(unittest.TestCase):
|
||||
|
||||
def test_get_providers(self):
|
||||
providers_ = providers.get_providers()
|
||||
self.assertIsInstance(providers_, dict)
|
||||
self.assertGreater(len(providers_), 0)
|
||||
|
||||
|
||||
class IterProvidersTest(unittest.TestCase):
|
||||
|
||||
def test_iter_providers(self):
|
||||
providers_ = providers.iter_providers()
|
||||
self.assertIsInstance(providers_, list)
|
||||
self.assertGreater(len(providers_), 0)
|
||||
|
||||
|
||||
class GetProviderTest(unittest.TestCase):
|
||||
|
||||
def test_good(self):
|
||||
provider = providers.get_provider('print_labels')
|
||||
self.assertIsInstance(provider, providers.BatchProvider)
|
||||
self.assertEqual(provider.name, 'print_labels')
|
||||
|
||||
def test_bad(self):
|
||||
self.assertRaises(BatchProviderNotFound, providers.get_provider, 'bogus')
|
||||
80
rattail/tests/batches/providers/test_labels.py
Normal file
80
rattail/tests/batches/providers/test_labels.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail.tests.db import ignores_warnings
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from rattail.batches.providers import labels
|
||||
from rattail.db.util import install_core_schema
|
||||
from rattail.db.model import Batch, LabelProfile, Brand, Product
|
||||
|
||||
|
||||
# class PrintLabelsTest(unittest.TestCase):
|
||||
|
||||
# def setUp(self):
|
||||
# engine = create_engine('sqlite://')
|
||||
# install_core_schema(engine)
|
||||
# self.Session = sessionmaker(bind=engine)
|
||||
|
||||
# session = self.Session()
|
||||
|
||||
# brand = Brand()
|
||||
# brand.name = 'Acme'
|
||||
# session.add(brand)
|
||||
|
||||
# product = Product()
|
||||
# product.brand = brand
|
||||
# product.description = 'Test'
|
||||
# session.add(product)
|
||||
|
||||
# profile = LabelProfile()
|
||||
# profile.code = 'XXX'
|
||||
# profile.printer_spec = 'rattail.labels:LabelPrinter'
|
||||
# session.add(profile)
|
||||
|
||||
# session.commit()
|
||||
# session.close()
|
||||
|
||||
# def test_make_batch(self):
|
||||
# session = self.Session()
|
||||
# provider = labels.PrintLabels()
|
||||
# batch = provider.make_batch(session, [], id='00000420')
|
||||
# self.assertEqual(len(batch.columns), 6)
|
||||
# batch.drop_table()
|
||||
# session.rollback()
|
||||
# session.close()
|
||||
|
||||
# def test_make_batch_with_profile(self):
|
||||
# session = self.Session()
|
||||
# provider = labels.PrintLabels()
|
||||
# provider.default_profile = session.query(LabelProfile).one()
|
||||
# batch = provider.make_batch(session, [], id='00000420')
|
||||
# self.assertEqual(len(batch.columns), 6)
|
||||
# batch.drop_table()
|
||||
# session.rollback()
|
||||
# session.close()
|
||||
|
||||
# def test_set_params(self):
|
||||
# session = self.Session()
|
||||
# profile = session.query(LabelProfile).one()
|
||||
# provider = labels.PrintLabels()
|
||||
# self.assertEqual(provider.default_profile, None)
|
||||
# self.assertEqual(provider.default_quantity, 1)
|
||||
# provider.set_params(session, profile='XXX', quantity='2')
|
||||
# self.assertIs(provider.default_profile, profile)
|
||||
# self.assertEqual(provider.default_quantity, 2)
|
||||
# session.close()
|
||||
|
||||
# @ignores_warnings
|
||||
# def test_add_rows(self):
|
||||
# session = self.Session()
|
||||
# provider = labels.PrintLabels()
|
||||
# batch = provider.make_batch(session, session.query(Product), id='00000420')
|
||||
# self.assertEqual(len(batch.columns), 6)
|
||||
# self.assertEqual(batch.rowcount, 1)
|
||||
# batch.drop_table()
|
||||
# session.rollback()
|
||||
# session.close()
|
||||
8
rattail/tests/bogus1.py
Normal file
8
rattail/tests/bogus1.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
__all__ = ['bogus_thing1']
|
||||
|
||||
bogus_thing1 = object()
|
||||
|
||||
def init(config):
|
||||
pass
|
||||
3
rattail/tests/bogus2.py
Normal file
3
rattail/tests/bogus2.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
bogus_thing2 = object()
|
||||
15
rattail/tests/config/base.conf
Normal file
15
rattail/tests/config/base.conf
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
############################################################
|
||||
#
|
||||
# base.conf
|
||||
#
|
||||
# This specifies only that we're running in "test mode."
|
||||
#
|
||||
############################################################
|
||||
|
||||
[rattail]
|
||||
testing = True
|
||||
some_option = some_value
|
||||
|
||||
[rattail.service_config]
|
||||
RattailFileMonitor = [r'%(here)s\service.conf']
|
||||
17
rattail/tests/config/db.conf
Normal file
17
rattail/tests/config/db.conf
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
############################################################
|
||||
#
|
||||
# db.conf
|
||||
#
|
||||
# This configuration is used for those tests needing only
|
||||
# a single database. The SQLite in-memory engine will
|
||||
# suffice in these cases.
|
||||
#
|
||||
############################################################
|
||||
|
||||
[rattail]
|
||||
testing = True
|
||||
|
||||
[rattail.db]
|
||||
engines = default
|
||||
default.url = sqlite://
|
||||
13
rattail/tests/config/local.conf
Normal file
13
rattail/tests/config/local.conf
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
############################################################
|
||||
#
|
||||
# local.conf
|
||||
#
|
||||
# This is used for testing configuration inheritance.
|
||||
#
|
||||
############################################################
|
||||
|
||||
[rattail]
|
||||
testing = True
|
||||
include_config = [r'%(here)s\base.conf']
|
||||
another_option = another_value
|
||||
12
rattail/tests/config/service.conf
Normal file
12
rattail/tests/config/service.conf
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
############################################################
|
||||
#
|
||||
# service.conf
|
||||
#
|
||||
# This is used for testing special service configuration.
|
||||
#
|
||||
############################################################
|
||||
|
||||
[rattail]
|
||||
testing = True
|
||||
hells = yeah
|
||||
40
rattail/tests/db/__init__.py
Normal file
40
rattail/tests/db/__init__.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from rattail.db.util import install_core_schema
|
||||
from rattail.db.model import Base
|
||||
|
||||
|
||||
class DataTestCase(unittest.TestCase):
|
||||
|
||||
default_url = 'postgresql://rattail:rattail@localhost/rattail.test'
|
||||
|
||||
def setUp(self):
|
||||
data_url = os.environ.get('RATTAIL_TEST_ENGINE', self.default_url)
|
||||
self.engine = create_engine(data_url)
|
||||
install_core_schema(self.engine)
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
|
||||
def tearDown(self):
|
||||
Base.metadata.drop_all(self.engine)
|
||||
|
||||
|
||||
def ignores_warnings(func):
|
||||
def wrapped(*args, **kwargs):
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings(
|
||||
'ignore',
|
||||
r"^Dialect sqlite\+pysqlite does \*not\* support Decimal "
|
||||
"objects natively, and SQLAlchemy must convert from floating "
|
||||
"point - rounding errors and other issues may occur\. Please "
|
||||
"consider storing Decimal numbers as strings or integers on "
|
||||
"this platform for lossless storage\.$")
|
||||
return func(*args, **kwargs)
|
||||
wrapped.__name__ = func.__name__
|
||||
return wrapped
|
||||
1
rattail/tests/db/batches/__init__.py
Normal file
1
rattail/tests/db/batches/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
#!/usr/bin/env python
|
||||
116
rattail/tests/db/batches/test_data.py
Normal file
116
rattail/tests/db/batches/test_data.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail.tests.db import DataTestCase
|
||||
|
||||
from rattail.db.batches import data
|
||||
from rattail.db.model import Brand, Product
|
||||
from rattail.barcodes import GPC
|
||||
|
||||
|
||||
class BatchDataProviderTest(unittest.TestCase):
|
||||
|
||||
def test_not_implemented(self):
|
||||
provider = data.BatchDataProvider()
|
||||
self.assertRaises(NotImplementedError, len, provider)
|
||||
self.assertRaises(NotImplementedError, iter, provider)
|
||||
|
||||
|
||||
class QueryDataProviderTest(DataTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(QueryDataProviderTest, self).setUp()
|
||||
session = self.Session()
|
||||
|
||||
product = Product()
|
||||
product.upc = '074305001321'
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
product.size = '32oz'
|
||||
session.add(product)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
def test_len(self):
|
||||
session = self.Session()
|
||||
query = session.query(Product)
|
||||
provider = data.QueryDataProvider(query)
|
||||
self.assertEqual(len(provider), 1)
|
||||
query = query.filter_by(upc='074305001161')
|
||||
provider = data.QueryDataProvider(query)
|
||||
self.assertEqual(len(provider), 0)
|
||||
session.close()
|
||||
|
||||
|
||||
class ProductQueryDataProviderTest(DataTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ProductQueryDataProviderTest, self).setUp()
|
||||
session = self.Session()
|
||||
|
||||
brand = Brand()
|
||||
brand.name = "Bragg's"
|
||||
session.add(brand)
|
||||
|
||||
product = Product()
|
||||
product.brand = brand
|
||||
product.upc = '074305001321'
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
product.size = '32oz'
|
||||
session.add(product)
|
||||
|
||||
product = Product()
|
||||
product.brand = brand
|
||||
product.upc = '074305001161'
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
product.size = '16oz'
|
||||
session.add(product)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
def test_iter(self):
|
||||
session = self.Session()
|
||||
query = session.query(Product)
|
||||
provider = data.ProductQueryDataProvider(query)
|
||||
self.assertEqual(len(provider), 2)
|
||||
iterator = iter(provider)
|
||||
proxy = iterator.next()
|
||||
proxy = iterator.next()
|
||||
self.assertRaises(StopIteration, iterator.next)
|
||||
session.close()
|
||||
|
||||
|
||||
class ProductDataProxyTest(unittest.TestCase):
|
||||
|
||||
def test_without_brand(self):
|
||||
product = Product()
|
||||
product.upc = GPC('074305001161')
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
product.size = '16oz'
|
||||
|
||||
proxy = data.ProductDataProxy(product)
|
||||
self.assertIsInstance(proxy.F01, GPC)
|
||||
self.assertEqual(proxy.F01, '074305001161')
|
||||
self.assertEqual(proxy.F02, 'Apple Cider Vinegar')
|
||||
self.assertEqual(proxy.F22, '16oz')
|
||||
self.assertEqual(proxy.F155, '')
|
||||
self.assertRaises(AttributeError, getattr, proxy, 'F420')
|
||||
|
||||
def test_with_brand(self):
|
||||
product = Product()
|
||||
product.brand = Brand()
|
||||
product.brand.name = "Bragg's"
|
||||
product.upc = GPC('074305001321')
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
product.size = '32oz'
|
||||
|
||||
proxy = data.ProductDataProxy(product)
|
||||
self.assertIsInstance(proxy.F01, GPC)
|
||||
self.assertEqual(proxy.F01, '074305001321')
|
||||
self.assertEqual(proxy.F02, 'Apple Cider Vinegar')
|
||||
self.assertEqual(proxy.F22, '32oz')
|
||||
self.assertEqual(proxy.F155, "Bragg's")
|
||||
self.assertRaises(AttributeError, getattr, proxy, 'F420')
|
||||
|
||||
152
rattail/tests/db/batches/test_executors.py
Normal file
152
rattail/tests/db/batches/test_executors.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail.tests.db import DataTestCase
|
||||
from rattail.tests import CancelProgress
|
||||
|
||||
from rattail.db.batches import executors
|
||||
from rattail.db.model import Batch, Product, LabelProfile
|
||||
from rattail.exceptions import BatchTypeNotSupported
|
||||
from rattail.labels import LabelPrinter
|
||||
|
||||
|
||||
class BatchExecutorTest(DataTestCase):
|
||||
|
||||
def test_not_implemented(self):
|
||||
executor = executors.BatchExecutor()
|
||||
self.assertIsNone(executor.batch_type)
|
||||
self.assertRaises(NotImplementedError, executor.execute_batch, None, None)
|
||||
|
||||
session = self.Session()
|
||||
batch = Batch()
|
||||
session.add(batch)
|
||||
self.assertRaises(NotImplementedError, executor.execute, batch)
|
||||
session.rollback()
|
||||
session.close()
|
||||
|
||||
def test_batch_type_not_supported(self):
|
||||
executor = executors.BatchExecutor()
|
||||
session = self.Session()
|
||||
batch = Batch()
|
||||
batch.type = 'bogus'
|
||||
session.add(batch)
|
||||
self.assertRaises(BatchTypeNotSupported, executor.execute, batch)
|
||||
session.rollback()
|
||||
session.close()
|
||||
|
||||
|
||||
class GoodPrinter(LabelPrinter):
|
||||
|
||||
def print_labels(self, labels, *args, **kwargs):
|
||||
return True
|
||||
|
||||
|
||||
class BadPrinter(LabelPrinter):
|
||||
|
||||
def print_labels(self, labels, *args, **kwargs):
|
||||
return False
|
||||
|
||||
|
||||
class LabelsBatchExecutorTest(DataTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(LabelsBatchExecutorTest, self).setUp()
|
||||
session = self.Session()
|
||||
|
||||
product = Product()
|
||||
product.upc = '074305001321'
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
product.size = '32oz'
|
||||
session.add(product)
|
||||
|
||||
profile = LabelProfile()
|
||||
profile.code = 'REG'
|
||||
profile.printer_spec = 'rattail.tests.db.batches.test_executors:GoodPrinter'
|
||||
profile.formatter_spec = 'rattail.labels:LabelFormatter'
|
||||
session.add(profile)
|
||||
|
||||
profile = LabelProfile()
|
||||
profile.code = 'SAL'
|
||||
profile.printer_spec = 'rattail.tests.db.batches.test_executors:BadPrinter'
|
||||
profile.formatter_spec = 'rattail.labels:LabelFormatter'
|
||||
session.add(profile)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
def create_batch(self):
|
||||
batch = Batch()
|
||||
batch.type = 'labels'
|
||||
batch.add_column('F01')
|
||||
batch.add_column('F155')
|
||||
batch.add_column('F02')
|
||||
batch.add_column('F22', display_name="Size")
|
||||
batch.add_column('F95', display_name="Label")
|
||||
batch.add_column('F94', display_name="Quantity")
|
||||
return batch
|
||||
|
||||
def test_execute_cancel_progress(self):
|
||||
session = self.Session()
|
||||
|
||||
batch = self.create_batch()
|
||||
session.add(batch)
|
||||
session.flush()
|
||||
batch.create_table()
|
||||
|
||||
row = batch.rowclass()
|
||||
row.F01 = '074305001321'
|
||||
row.F94 = 1
|
||||
row.F95 = 'REG'
|
||||
batch.add_row(row)
|
||||
|
||||
session.flush()
|
||||
executor = executors.LabelsBatchExecutor()
|
||||
self.assertFalse(executor.execute(batch, CancelProgress))
|
||||
|
||||
session.rollback()
|
||||
session.close()
|
||||
|
||||
def test_execute_good_printer(self):
|
||||
session = self.Session()
|
||||
|
||||
batch = self.create_batch()
|
||||
session.add(batch)
|
||||
session.flush()
|
||||
batch.create_table()
|
||||
|
||||
row = batch.rowclass()
|
||||
row.F01 = '074305001321'
|
||||
row.F94 = 1
|
||||
row.F95 = 'REG'
|
||||
batch.add_row(row)
|
||||
|
||||
session.flush()
|
||||
executor = executors.LabelsBatchExecutor()
|
||||
self.assertTrue(executor.execute(batch))
|
||||
session.commit()
|
||||
|
||||
batch.drop_table()
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
def test_execute_bad_printer(self):
|
||||
session = self.Session()
|
||||
|
||||
batch = self.create_batch()
|
||||
session.add(batch)
|
||||
session.flush()
|
||||
batch.create_table()
|
||||
|
||||
row = batch.rowclass()
|
||||
row.F01 = '074305001321'
|
||||
row.F94 = 1
|
||||
row.F95 = 'SAL'
|
||||
batch.add_row(row)
|
||||
|
||||
session.flush()
|
||||
executor = executors.LabelsBatchExecutor()
|
||||
self.assertFalse(executor.execute(batch))
|
||||
|
||||
session.rollback()
|
||||
session.close()
|
||||
187
rattail/tests/db/batches/test_makers.py
Normal file
187
rattail/tests/db/batches/test_makers.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from rattail.tests.db import DataTestCase, ignores_warnings
|
||||
from rattail.tests import CancelProgress
|
||||
|
||||
from rattail.db.batches import makers
|
||||
from rattail.db.batches import BatchType, ProductQueryDataProvider
|
||||
from rattail.db.model import Batch, LabelProfile, Brand, Product
|
||||
|
||||
|
||||
class FakeType(BatchType):
|
||||
|
||||
def add_columns(self, batch):
|
||||
batch.add_column('F01')
|
||||
batch.add_column('F02')
|
||||
|
||||
|
||||
class FakeMaker(makers.BatchMaker):
|
||||
|
||||
def make_batch_fake(self):
|
||||
fake = FakeType()
|
||||
batch = Batch()
|
||||
fake.initialize(batch)
|
||||
return batch
|
||||
|
||||
def process_data_row(self, data_row):
|
||||
batch = self.get_batch('fake')
|
||||
row = batch.rowclass()
|
||||
row.F01 = data_row.F01
|
||||
row.F02 = data_row.F02
|
||||
batch.add_row(row)
|
||||
|
||||
|
||||
class FakeMakerCancelBegin(FakeMaker):
|
||||
|
||||
def make_batches_begin(self, data):
|
||||
return False
|
||||
|
||||
|
||||
class FakeMakerCancelEnd(FakeMaker):
|
||||
|
||||
def make_batches_end(self):
|
||||
return False
|
||||
|
||||
|
||||
class BatchMakerTest(DataTestCase):
|
||||
|
||||
def test_bad_data(self):
|
||||
session = self.Session()
|
||||
maker = makers.BatchMaker(session)
|
||||
self.assertRaises(TypeError, maker.make_batches, [])
|
||||
self.assertRaises(TypeError, maker.make_batches, session.query(Product))
|
||||
session.close()
|
||||
|
||||
def test_implemented(self):
|
||||
session = self.Session()
|
||||
|
||||
product = Product()
|
||||
product.upc = '074305001321'
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
session.add(product)
|
||||
session.flush()
|
||||
|
||||
maker = makers.BatchMaker(session)
|
||||
data = ProductQueryDataProvider(session.query(Product))
|
||||
self.assertRaises(NotImplementedError, maker.make_batches, data)
|
||||
|
||||
session.rollback()
|
||||
session.close()
|
||||
|
||||
@ignores_warnings
|
||||
def test_custom_maker(self):
|
||||
session = self.Session()
|
||||
|
||||
product = Product()
|
||||
product.upc = '074305001321'
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
session.add(product)
|
||||
session.flush()
|
||||
|
||||
self.assertEqual(session.query(Batch).count(), 0)
|
||||
maker = FakeMaker(session)
|
||||
data = ProductQueryDataProvider(session.query(Product))
|
||||
self.assertTrue(maker.make_batches(data), True)
|
||||
session.commit()
|
||||
|
||||
self.assertEqual(session.query(Batch).count(), 1)
|
||||
batch = session.query(Batch).one()
|
||||
self.assertTrue(batch.rowclass.__table__.exists(session.connection()))
|
||||
batch.drop_table()
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
def test_cancel_begin(self):
|
||||
session = self.Session()
|
||||
|
||||
product = Product()
|
||||
product.upc = '074305001321'
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
session.add(product)
|
||||
session.flush()
|
||||
|
||||
self.assertEqual(session.query(Batch).count(), 0)
|
||||
maker = FakeMakerCancelBegin(session)
|
||||
data = ProductQueryDataProvider(session.query(Product))
|
||||
self.assertEqual(maker.make_batches(data), False)
|
||||
session.rollback()
|
||||
|
||||
self.assertEqual(session.query(Batch).count(), 0)
|
||||
session.close()
|
||||
|
||||
@ignores_warnings
|
||||
def test_cancel_end(self):
|
||||
session = self.Session()
|
||||
|
||||
product = Product()
|
||||
product.upc = '074305001321'
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
session.add(product)
|
||||
session.flush()
|
||||
|
||||
self.assertEqual(session.query(Batch).count(), 0)
|
||||
maker = FakeMakerCancelEnd(session)
|
||||
data = ProductQueryDataProvider(session.query(Product))
|
||||
self.assertEqual(maker.make_batches(data), False)
|
||||
session.rollback()
|
||||
|
||||
self.assertEqual(session.query(Batch).count(), 0)
|
||||
session.close()
|
||||
|
||||
def test_cancel_progress(self):
|
||||
session = self.Session()
|
||||
|
||||
product = Product()
|
||||
product.upc = '074305001321'
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
session.add(product)
|
||||
session.flush()
|
||||
|
||||
self.assertEqual(session.query(Batch).count(), 0)
|
||||
maker = FakeMaker(session)
|
||||
data = ProductQueryDataProvider(session.query(Product))
|
||||
self.assertEqual(maker.make_batches(data, CancelProgress), False)
|
||||
session.rollback()
|
||||
|
||||
self.assertEqual(session.query(Batch).count(), 0)
|
||||
session.close()
|
||||
|
||||
|
||||
class LabelsBatchMakerTest(DataTestCase):
|
||||
|
||||
@ignores_warnings
|
||||
def test_maker(self):
|
||||
session = self.Session()
|
||||
|
||||
profile = LabelProfile()
|
||||
profile.code = 'XXX'
|
||||
session.add(profile)
|
||||
|
||||
brand = Brand()
|
||||
brand.name = "Bragg's"
|
||||
session.add(brand)
|
||||
|
||||
product = Product()
|
||||
product.upc = '074305001321'
|
||||
product.brand = brand
|
||||
product.description = 'Apple Cider Vinegar'
|
||||
product.size = '32oz'
|
||||
session.add(product)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = self.Session()
|
||||
self.assertEqual(session.query(Batch).count(), 0)
|
||||
maker = makers.LabelsBatchMaker(session)
|
||||
data = ProductQueryDataProvider(session.query(Product))
|
||||
self.assertEqual(maker.make_batches(data), True)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = self.Session()
|
||||
self.assertEqual(session.query(Batch).count(), 1)
|
||||
batch = session.query(Batch).one()
|
||||
self.assertEqual(batch.rowcount, 1)
|
||||
self.assertEqual(batch.rows.count(), 1)
|
||||
session.close()
|
||||
94
rattail/tests/db/batches/test_types.py
Normal file
94
rattail/tests/db/batches/test_types.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
import rattail
|
||||
from rattail.db.batches import types
|
||||
from rattail.db.model import Batch
|
||||
from rattail.time import timezones
|
||||
from rattail.exceptions import BatchTypeNotFound
|
||||
|
||||
|
||||
class FakeBatchType(types.BatchType):
|
||||
|
||||
def add_columns(self, batch):
|
||||
pass
|
||||
|
||||
|
||||
class GetBatchTypeTest(unittest.TestCase):
|
||||
|
||||
def test_invalid(self):
|
||||
self.assertRaises(BatchTypeNotFound, types.get_batch_type, 'bogus')
|
||||
|
||||
def test_labels(self):
|
||||
type = types.get_batch_type('labels')
|
||||
self.assertIsInstance(type, types.LabelsBatchType)
|
||||
|
||||
|
||||
class BatchTypeTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
timezones['local'] = pytz.timezone('UTC')
|
||||
|
||||
def tearDown(self):
|
||||
del timezones['local']
|
||||
|
||||
def test_not_implemented(self):
|
||||
type = types.BatchType()
|
||||
self.assertRaises(NotImplementedError, type.add_columns, Batch())
|
||||
self.assertRaises(NotImplementedError, type.initialize, Batch())
|
||||
|
||||
def test_purge_date(self):
|
||||
today = datetime.datetime.utcnow().date()
|
||||
type = types.BatchType()
|
||||
batch = Batch()
|
||||
self.assertIsNone(batch.purge)
|
||||
type.set_purge_date(batch)
|
||||
self.assertIsNone(batch.purge)
|
||||
type.purge_date_offset = 90
|
||||
type.set_purge_date(batch)
|
||||
self.assertEqual(batch.purge, today + datetime.timedelta(days=90))
|
||||
type.purge_date_offset = 420
|
||||
type.set_purge_date(batch)
|
||||
self.assertEqual(batch.purge, today + datetime.timedelta(days=420))
|
||||
|
||||
def test_initialize(self):
|
||||
type = FakeBatchType()
|
||||
batch = Batch()
|
||||
|
||||
type.initialize(batch)
|
||||
self.assertIsNone(batch.description)
|
||||
self.assertIsNone(batch.source)
|
||||
self.assertIsNone(batch.destination)
|
||||
self.assertIsNone(batch.action_type)
|
||||
|
||||
type.description = 'Testing'
|
||||
type.source = 'RATAIL'
|
||||
type.destination = 'LOCAL'
|
||||
type.action_type = 'ADD'
|
||||
type.initialize(batch)
|
||||
self.assertEqual(batch.description, 'Testing')
|
||||
self.assertEqual(batch.source, 'RATAIL')
|
||||
self.assertEqual(batch.destination, 'LOCAL')
|
||||
self.assertEqual(batch.action_type, 'ADD')
|
||||
|
||||
|
||||
class LabelsBatchTypeTest(unittest.TestCase):
|
||||
|
||||
def test_initialize(self):
|
||||
type = types.LabelsBatchType()
|
||||
batch = Batch()
|
||||
self.assertIsNone(batch.description)
|
||||
self.assertEqual(len(batch.columns), 0)
|
||||
type.initialize(batch)
|
||||
self.assertEqual(batch.description, 'Print Labels')
|
||||
self.assertEqual(len(batch.columns), 6)
|
||||
self.assertEqual(batch.columns[0].name, 'F01')
|
||||
self.assertEqual(batch.columns[1].name, 'F155')
|
||||
self.assertEqual(batch.columns[2].name, 'F02')
|
||||
self.assertEqual(batch.columns[3].name, 'F22')
|
||||
self.assertEqual(batch.columns[4].name, 'F95')
|
||||
self.assertEqual(batch.columns[5].name, 'F94')
|
||||
222
rattail/tests/db/test_auth.py
Normal file
222
rattail/tests/db/test_auth.py
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail.tests.db import DataTestCase
|
||||
|
||||
import bcrypt
|
||||
from cStringIO import StringIO
|
||||
|
||||
from rattail.db import auth
|
||||
from rattail.db.model import User, Role
|
||||
|
||||
|
||||
class BcryptAuthenticatorTest(unittest.TestCase):
|
||||
|
||||
def test_populate_user(self):
|
||||
user = User()
|
||||
self.assertIsNone(user.salt)
|
||||
self.assertIsNone(user.password)
|
||||
|
||||
authenticator = auth.BcryptAuthenticator()
|
||||
authenticator.populate_user(user, 'seekrit')
|
||||
self.assertIsNotNone(user.salt)
|
||||
self.assertIsNotNone(user.password)
|
||||
|
||||
old_salt = user.salt
|
||||
old_password = user.password
|
||||
authenticator.populate_user(user, 'seekrit')
|
||||
self.assertNotEqual(user.salt, old_salt)
|
||||
self.assertNotEqual(user.password, old_password)
|
||||
|
||||
def test_authenticate_user(self):
|
||||
user = User()
|
||||
authenticator = auth.BcryptAuthenticator()
|
||||
authenticator.populate_user(user, 'seekrit')
|
||||
self.assertTrue(authenticator.authenticate_user(user, 'seekrit'))
|
||||
self.assertFalse(authenticator.authenticate_user(user, 'bogus'))
|
||||
|
||||
|
||||
class AuthenticateUserTest(DataTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(AuthenticateUserTest, self).setUp()
|
||||
authenticator = auth.BcryptAuthenticator()
|
||||
user = User()
|
||||
user.username = 'fred'
|
||||
authenticator.populate_user(user, 'seekrit')
|
||||
session = self.Session()
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
def test_authenticate_user(self):
|
||||
session = self.Session()
|
||||
user = session.query(User).one()
|
||||
self.assertIs(auth.authenticate_user(session, 'fred', 'seekrit'), user)
|
||||
self.assertIsNone(auth.authenticate_user(session, 'fred', 'bogus'))
|
||||
self.assertIsNone(auth.authenticate_user(session, 'wilma', 'bogus'))
|
||||
session.close()
|
||||
|
||||
|
||||
class AdministratorRoleTest(DataTestCase):
|
||||
|
||||
def test_administrator_role(self):
|
||||
session = self.Session()
|
||||
admin1 = auth.administrator_role(session)
|
||||
session.commit()
|
||||
admin2 = auth.administrator_role(session)
|
||||
self.assertIs(admin1, admin2)
|
||||
session.close()
|
||||
|
||||
|
||||
class GuestRoleTest(DataTestCase):
|
||||
|
||||
def test_guest_role(self):
|
||||
session = self.Session()
|
||||
guest1 = auth.guest_role(session)
|
||||
session.commit()
|
||||
guest2 = auth.guest_role(session)
|
||||
self.assertIs(guest1, guest2)
|
||||
session.close()
|
||||
|
||||
|
||||
class GrantPermissionTest(DataTestCase):
|
||||
|
||||
def test_grant_permission(self):
|
||||
session = self.Session()
|
||||
guest = auth.guest_role(session)
|
||||
session.commit()
|
||||
session.refresh(guest)
|
||||
self.assertEqual(len(guest.permissions), 0)
|
||||
auth.grant_permission(session, guest, 'products.delete')
|
||||
session.commit()
|
||||
session.refresh(guest)
|
||||
self.assertEqual(len(guest.permissions), 1)
|
||||
self.assertEqual(guest.permissions[0], 'products.delete')
|
||||
session.close()
|
||||
|
||||
|
||||
class HasPermissionTest(DataTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(HasPermissionTest, self).setUp()
|
||||
session = self.Session()
|
||||
|
||||
user = User()
|
||||
user.username = 'fred'
|
||||
session.add(user)
|
||||
|
||||
role = Role()
|
||||
role.name = 'Testing'
|
||||
session.add(role)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
def test_invalid_object(self):
|
||||
session = self.Session()
|
||||
self.assertRaises(TypeError, auth.has_permission, session, object(), 'products.delete')
|
||||
session.close()
|
||||
|
||||
def test_user_with_guest(self):
|
||||
session = self.Session()
|
||||
user = session.query(User).one()
|
||||
self.assertFalse(auth.has_permission(
|
||||
session, user, 'products.delete', include_guest=True))
|
||||
guest = auth.guest_role(session)
|
||||
auth.grant_permission(session, guest, 'products.delete')
|
||||
session.commit()
|
||||
self.assertTrue(auth.has_permission(
|
||||
session, user, 'products.delete', include_guest=True))
|
||||
session.close()
|
||||
|
||||
def test_user_without_guest(self):
|
||||
session = self.Session()
|
||||
user = session.query(User).one()
|
||||
self.assertFalse(auth.has_permission(
|
||||
session, user, 'products.delete', include_guest=False))
|
||||
guest = auth.guest_role(session)
|
||||
auth.grant_permission(session, guest, 'products.delete')
|
||||
session.commit()
|
||||
self.assertFalse(auth.has_permission(
|
||||
session, user, 'products.delete', include_guest=False))
|
||||
session.close()
|
||||
|
||||
def test_role_with_guest(self):
|
||||
session = self.Session()
|
||||
role = session.query(Role).one()
|
||||
self.assertFalse(auth.has_permission(
|
||||
session, role, 'products.delete', include_guest=True))
|
||||
guest = auth.guest_role(session)
|
||||
auth.grant_permission(session, guest, 'products.delete')
|
||||
session.commit()
|
||||
self.assertTrue(auth.has_permission(
|
||||
session, role, 'products.delete', include_guest=True))
|
||||
session.close()
|
||||
|
||||
def test_role_without_guest(self):
|
||||
session = self.Session()
|
||||
role = session.query(Role).one()
|
||||
self.assertFalse(auth.has_permission(
|
||||
session, role, 'products.delete', include_guest=False))
|
||||
guest = auth.guest_role(session)
|
||||
auth.grant_permission(session, guest, 'products.delete')
|
||||
session.commit()
|
||||
self.assertFalse(auth.has_permission(
|
||||
session, role, 'products.delete', include_guest=False))
|
||||
session.close()
|
||||
|
||||
def test_none_with_guest(self):
|
||||
session = self.Session()
|
||||
self.assertFalse(auth.has_permission(
|
||||
session, None, 'products.delete', include_guest=True))
|
||||
guest = auth.guest_role(session)
|
||||
auth.grant_permission(session, guest, 'products.delete')
|
||||
session.commit()
|
||||
self.assertTrue(auth.has_permission(
|
||||
session, None, 'products.delete', include_guest=True))
|
||||
session.close()
|
||||
|
||||
def test_none_without_guest(self):
|
||||
session = self.Session()
|
||||
self.assertFalse(auth.has_permission(
|
||||
session, None, 'products.delete', include_guest=False))
|
||||
guest = auth.guest_role(session)
|
||||
auth.grant_permission(session, guest, 'products.delete')
|
||||
session.commit()
|
||||
self.assertFalse(auth.has_permission(
|
||||
session, None, 'products.delete', include_guest=False))
|
||||
session.close()
|
||||
|
||||
def test_admin(self):
|
||||
session = self.Session()
|
||||
admin = auth.administrator_role(session)
|
||||
session.commit()
|
||||
session.refresh(admin)
|
||||
self.assertEqual(len(admin.permissions), 0)
|
||||
self.assertTrue(auth.has_permission(
|
||||
session, admin, 'products.delete', include_guest=False))
|
||||
session.close()
|
||||
|
||||
|
||||
class InitDatabaseTest(DataTestCase):
|
||||
|
||||
def test_init_database(self):
|
||||
session = self.Session()
|
||||
self.assertEqual(session.query(Role).count(), 0)
|
||||
self.assertEqual(session.query(User).count(), 0)
|
||||
stream = StringIO()
|
||||
auth.init_database(session, output_stream=stream)
|
||||
self.assertEqual(stream.getvalue(), "Created 'admin' user with password 'admin'\n")
|
||||
stream.close()
|
||||
session.commit()
|
||||
self.assertEqual(session.query(Role).count(), 1)
|
||||
self.assertEqual(session.query(User).count(), 1)
|
||||
admin = auth.administrator_role(session)
|
||||
self.assertIs(session.query(Role).one(), admin)
|
||||
user = session.query(User).one()
|
||||
self.assertEqual(user.username, 'admin')
|
||||
authenticator = auth.BcryptAuthenticator()
|
||||
self.assertTrue(authenticator.authenticate_user(user, 'admin'))
|
||||
session.close()
|
||||
42
rattail/tests/db/test_changes.py
Normal file
42
rattail/tests/db/test_changes.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
# import edbob
|
||||
# import edbob.db
|
||||
|
||||
import rattail
|
||||
from rattail.files import resource_path
|
||||
from rattail.db.util import install_core_schema
|
||||
|
||||
|
||||
# class RecordChangesTest(unittest.TestCase):
|
||||
|
||||
# def setUp(self):
|
||||
# # engine = create_engine('sqlite://')
|
||||
# # Base.metadata.create_all(engine)
|
||||
# # Session.configure(bind=engine)
|
||||
|
||||
# edbob.init('rattail', resource_path('rattail.tests:config/db.conf'))
|
||||
# install_core_schema()
|
||||
# # edbob.init_modules(['edbob.db'])
|
||||
# edbob.init_modules(['rattail.db'])
|
||||
|
||||
# def tearDown(self):
|
||||
# del edbob.inited[:]
|
||||
# edbob.db.engines.clear()
|
||||
# edbob.db.engine = None
|
||||
|
||||
# def test_no_recording(self):
|
||||
# session = edbob.Session()
|
||||
# product = rattail.Product()
|
||||
# session.add(product)
|
||||
# session.commit()
|
||||
# session.close()
|
||||
|
||||
# session = edbob.Session()
|
||||
# products = session.query(rattail.Product)
|
||||
# self.assertEqual(products.count(), 1)
|
||||
# changes = session.query(rattail.Change)
|
||||
# self.assertEqual(changes.count(), 0)
|
||||
# session.close()
|
||||
69
rattail/tests/db/test_init.py
Normal file
69
rattail/tests/db/test_init.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail.tests import get_config
|
||||
|
||||
from rattail import db
|
||||
from rattail.exceptions import MissingConfiguration, NoDefaultDatabase
|
||||
|
||||
|
||||
class InitTest(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
db.engines.clear()
|
||||
db.engine = None
|
||||
db.Session.configure(bind=None)
|
||||
|
||||
def test_init_first_default(self):
|
||||
self.assertEqual(db.engines, {})
|
||||
self.assertEqual(db.engine, None)
|
||||
session = db.Session()
|
||||
self.assertEqual(session.bind, None)
|
||||
|
||||
config = get_config()
|
||||
config.set('rattail.db', 'default.url', 'sqlite://')
|
||||
config.set('rattail.db', 'engines', 'default')
|
||||
db.init(config)
|
||||
|
||||
self.assertEqual(len(db.engines), 1)
|
||||
self.assertEqual(str(db.engine.url), 'sqlite://')
|
||||
session = db.Session()
|
||||
self.assertIs(session.bind, db.engine)
|
||||
|
||||
def test_init_second_default(self):
|
||||
self.assertEqual(db.engines, {})
|
||||
self.assertEqual(db.engine, None)
|
||||
session = db.Session()
|
||||
self.assertEqual(session.bind, None)
|
||||
|
||||
config = get_config()
|
||||
config.set('rattail.db', 'another.url', 'sqlite://')
|
||||
config.set('rattail.db', 'engines', 'another')
|
||||
db.init(config)
|
||||
|
||||
self.assertEqual(len(db.engines), 1)
|
||||
self.assertEqual(str(db.engine.url), 'sqlite://')
|
||||
session = db.Session()
|
||||
self.assertIs(session.bind, db.engine)
|
||||
|
||||
def test_init_no_engines(self):
|
||||
self.assertEqual(db.engines, {})
|
||||
self.assertEqual(db.engine, None)
|
||||
session = db.Session()
|
||||
self.assertEqual(session.bind, None)
|
||||
|
||||
config = get_config()
|
||||
self.assertRaises(MissingConfiguration, db.init, config)
|
||||
|
||||
def test_init_no_default(self):
|
||||
self.assertEqual(db.engines, {})
|
||||
self.assertEqual(db.engine, None)
|
||||
session = db.Session()
|
||||
self.assertEqual(session.bind, None)
|
||||
|
||||
config = get_config()
|
||||
config.set('rattail.db', 'engines', 'one, two')
|
||||
config.set('rattail.db', 'one.url', 'sqlite://')
|
||||
config.set('rattail.db', 'two.url', 'sqlite://')
|
||||
self.assertRaises(NoDefaultDatabase, db.init, config)
|
||||
126
rattail/tests/db/test_load.py
Normal file
126
rattail/tests/db/test_load.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail.tests import CancelProgress, get_config
|
||||
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
import rattail
|
||||
from rattail import db
|
||||
from rattail.db import load
|
||||
from rattail.initialization import inited
|
||||
from rattail.files import temp_path
|
||||
from rattail.db.util import install_core_schema
|
||||
from rattail.db.model import Department, Product, ProductPrice
|
||||
|
||||
|
||||
class LoadProcessorTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
rattail.config = get_config()
|
||||
inited.append('rattail.db')
|
||||
|
||||
self.host_path = temp_path('.sqlite')
|
||||
self.store_path = temp_path('.sqlite')
|
||||
|
||||
db.engines = {
|
||||
'host': create_engine('sqlite:///%s' % self.host_path.replace('\\', '/')),
|
||||
'store': create_engine('sqlite:///%s' % self.store_path.replace('\\', '/')),
|
||||
}
|
||||
db.engine = db.engines['store']
|
||||
db.Session.configure(bind=db.engine)
|
||||
|
||||
install_core_schema(db.engines['host'])
|
||||
install_core_schema(db.engines['store'])
|
||||
|
||||
def tearDown(self):
|
||||
db.Session.configure(bind=None)
|
||||
db.engine = None
|
||||
db.engines.clear()
|
||||
|
||||
inited.remove('rattail.db')
|
||||
del rattail.config
|
||||
|
||||
os.remove(self.host_path)
|
||||
os.remove(self.store_path)
|
||||
|
||||
def test_load_cancel(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings(
|
||||
'ignore',
|
||||
r"^Dialect sqlite\+pysqlite does \*not\* support Decimal "
|
||||
"objects natively, and SQLAlchemy must convert from floating "
|
||||
"point - rounding errors and other issues may occur\. Please "
|
||||
"consider storing Decimal numbers as strings or integers on "
|
||||
"this platform for lossless storage\.$")
|
||||
|
||||
host_session = db.Session(bind=db.engines['host'])
|
||||
self.assertEqual(host_session.query(Product).count(), 0)
|
||||
for x in range(100):
|
||||
host_session.add(Product())
|
||||
host_session.commit()
|
||||
host_session.close()
|
||||
|
||||
host_session = db.Session(bind=db.engines['host'])
|
||||
self.assertEqual(host_session.query(Product).count(), 100)
|
||||
host_session.close()
|
||||
|
||||
store_session = db.Session()
|
||||
self.assertEqual(store_session.query(Product).count(), 0)
|
||||
store_session.close()
|
||||
|
||||
proc = load.LoadProcessor()
|
||||
self.assertEqual(proc.load_all_data(db.engines['host'], CancelProgress), False)
|
||||
|
||||
store_session = db.Session()
|
||||
self.assertEqual(store_session.query(Product).count(), 0)
|
||||
store_session.close()
|
||||
|
||||
def test_load(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings(
|
||||
'ignore',
|
||||
r"^Dialect sqlite\+pysqlite does \*not\* support Decimal "
|
||||
"objects natively, and SQLAlchemy must convert from floating "
|
||||
"point - rounding errors and other issues may occur\. Please "
|
||||
"consider storing Decimal numbers as strings or integers on "
|
||||
"this platform for lossless storage\.$")
|
||||
|
||||
host_session = db.Session(bind=db.engines['host'])
|
||||
self.assertEqual(host_session.query(Product).count(), 0)
|
||||
department = Department()
|
||||
product = Product()
|
||||
product.department = department
|
||||
price = ProductPrice()
|
||||
product.prices.append(price)
|
||||
product.regular_price = price
|
||||
product.current_price = price
|
||||
host_session.add(product)
|
||||
host_session.commit()
|
||||
host_session.close()
|
||||
|
||||
host_session = db.Session(bind=db.engines['host'])
|
||||
self.assertEqual(host_session.query(Department).count(), 1)
|
||||
self.assertEqual(host_session.query(Product).count(), 1)
|
||||
self.assertEqual(host_session.query(ProductPrice).count(), 1)
|
||||
host_session.close()
|
||||
|
||||
store_session = db.Session()
|
||||
self.assertEqual(store_session.query(Department).count(), 0)
|
||||
self.assertEqual(store_session.query(Product).count(), 0)
|
||||
self.assertEqual(store_session.query(ProductPrice).count(), 0)
|
||||
store_session.close()
|
||||
|
||||
proc = load.LoadProcessor()
|
||||
self.assertEqual(proc.load_all_data(db.engines['host']), True)
|
||||
|
||||
store_session = db.Session()
|
||||
self.assertEqual(store_session.query(Department).count(), 1)
|
||||
self.assertEqual(store_session.query(Product).count(), 1)
|
||||
self.assertEqual(store_session.query(ProductPrice).count(), 1)
|
||||
store_session.close()
|
||||
|
||||
711
rattail/tests/db/test_model.py
Normal file
711
rattail/tests/db/test_model.py
Normal file
|
|
@ -0,0 +1,711 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import Column, String
|
||||
|
||||
from rattail.db import model
|
||||
from rattail.util import Object
|
||||
from rattail.tests.db import DataTestCase
|
||||
from rattail.exceptions import BatchNotSaved
|
||||
|
||||
|
||||
class UUIDColumnTest(unittest.TestCase):
|
||||
|
||||
def test_uuid_column(self):
|
||||
col = model.uuid_column()
|
||||
self.assertIsInstance(col, Column)
|
||||
self.assertEqual(col.primary_key, True)
|
||||
self.assertIsInstance(col.type, String)
|
||||
self.assertEqual(col.default.is_callable, True)
|
||||
|
||||
|
||||
class GetSetFactoryTest(unittest.TestCase):
|
||||
|
||||
def test_getset_factory(self):
|
||||
proxy = Object()
|
||||
proxy.value_attr = 'second'
|
||||
getter, setter = model.getset_factory(None, proxy)
|
||||
self.assertEqual(getter(None), None)
|
||||
obj = Object()
|
||||
obj.second = 'something'
|
||||
self.assertEqual(getter(obj), 'something')
|
||||
setter(obj, 'another')
|
||||
self.assertEqual(getter(obj), 'another')
|
||||
|
||||
|
||||
class GetPersonDisplayNameTest(unittest.TestCase):
|
||||
|
||||
def test_first_and_last(self):
|
||||
context = Object()
|
||||
context.current_parameters = {
|
||||
'first_name': 'Fred',
|
||||
'last_name': 'Flintstone',
|
||||
}
|
||||
self.assertEqual(model.get_person_display_name(context), 'Fred Flintstone')
|
||||
|
||||
def test_first_only(self):
|
||||
context = Object()
|
||||
context.current_parameters = {
|
||||
'first_name': 'Fred',
|
||||
'last_name': '',
|
||||
}
|
||||
self.assertEqual(model.get_person_display_name(context), 'Fred')
|
||||
|
||||
def test_last_only(self):
|
||||
context = Object()
|
||||
context.current_parameters = {
|
||||
'first_name': '',
|
||||
'last_name': 'Flintstone',
|
||||
}
|
||||
self.assertEqual(model.get_person_display_name(context), 'Flintstone')
|
||||
|
||||
def test_neither(self):
|
||||
context = Object()
|
||||
context.current_parameters = {
|
||||
'first_name': '',
|
||||
'last_name': '',
|
||||
}
|
||||
self.assertEqual(model.get_person_display_name(context), None)
|
||||
|
||||
|
||||
class ChangeTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
change = model.Change(
|
||||
class_name='Product',
|
||||
uuid='97fc2e21403f11e298b460eb69b23879',
|
||||
deleted=False)
|
||||
self.assertEqual(repr(change), '<Change: Product, 97fc2e21403f11e298b460eb69b23879, new/changed>')
|
||||
change = model.Change(
|
||||
class_name='Product',
|
||||
uuid='97fc2e21403f11e298b460eb69b23879',
|
||||
deleted=True)
|
||||
self.assertEqual(repr(change), '<Change: Product, 97fc2e21403f11e298b460eb69b23879, deleted>')
|
||||
|
||||
|
||||
class BatchColumnTest(unittest.TestCase):
|
||||
|
||||
def test_construct(self):
|
||||
column = model.BatchColumn()
|
||||
self.assertEqual(column.sil_name, None)
|
||||
|
||||
column = model.BatchColumn('F01')
|
||||
self.assertEqual(column.sil_name, 'F01')
|
||||
self.assertEqual(column.name, 'F01')
|
||||
self.assertEqual(column.data_type, 'GPC(14)')
|
||||
|
||||
column = model.BatchColumn('F01', data_type='bogus')
|
||||
self.assertEqual(column.sil_name, 'F01')
|
||||
self.assertEqual(column.data_type, 'bogus')
|
||||
|
||||
def test_repr(self):
|
||||
column = model.BatchColumn('F01')
|
||||
self.assertEqual(repr(column), '<BatchColumn: F01>')
|
||||
|
||||
def test_unicode(self):
|
||||
column = model.BatchColumn()
|
||||
self.assertEqual(unicode(column), u'')
|
||||
column = model.BatchColumn(display_name='Whatever')
|
||||
self.assertEqual(unicode(column), u'Whatever')
|
||||
|
||||
|
||||
class BatchRowTest(unittest.TestCase):
|
||||
|
||||
def test_unicode(self):
|
||||
row = model.BatchRow(ordinal=420)
|
||||
self.assertEqual(unicode(row), u'Row 420')
|
||||
|
||||
|
||||
class BatchTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
batch = model.Batch()
|
||||
batch.description = 'Test Batch'
|
||||
self.assertEqual(repr(batch), '<Batch: Test Batch>')
|
||||
|
||||
def test_unicode(self):
|
||||
batch = model.Batch()
|
||||
self.assertEqual(unicode(batch), u'')
|
||||
batch.description = 'Test Batch'
|
||||
self.assertEqual(unicode(batch), u'Test Batch')
|
||||
|
||||
def test_add_column(self):
|
||||
batch = model.Batch()
|
||||
self.assertEqual(len(batch.columns), 0)
|
||||
batch.add_column('F01')
|
||||
self.assertEqual(len(batch.columns), 1)
|
||||
column = batch.columns[0]
|
||||
self.assertEqual(column.sil_name, 'F01')
|
||||
self.assertEqual(column.name, 'F01')
|
||||
self.assertEqual(column.data_type, 'GPC(14)')
|
||||
|
||||
|
||||
class BatchDataTest(DataTestCase):
|
||||
|
||||
def test_batch(self):
|
||||
session = self.Session()
|
||||
self.assertEqual(session.query(model.Batch).count(), 0)
|
||||
batch = model.Batch()
|
||||
session.add(batch)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = self.Session()
|
||||
self.assertEqual(session.query(model.Batch).count(), 1)
|
||||
session.close()
|
||||
|
||||
def test_column(self):
|
||||
session = self.Session()
|
||||
self.assertEqual(session.query(model.Batch).count(), 0)
|
||||
self.assertEqual(session.query(model.BatchColumn).count(), 0)
|
||||
batch = model.Batch()
|
||||
batch.add_column('F01')
|
||||
batch.add_column('F02')
|
||||
session.add(batch)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = self.Session()
|
||||
self.assertEqual(session.query(model.Batch).count(), 1)
|
||||
self.assertEqual(session.query(model.BatchColumn).count(), 2)
|
||||
session.close()
|
||||
|
||||
def test_rowclass(self):
|
||||
before = len(model.Base.metadata.tables)
|
||||
|
||||
session = self.Session()
|
||||
batch = model.Batch()
|
||||
batch.add_column('F01')
|
||||
batch.add_column('F02')
|
||||
session.add(batch)
|
||||
|
||||
self.assertRaises(BatchNotSaved, getattr, batch, 'rowclass')
|
||||
session.commit()
|
||||
|
||||
self.assertEqual(len(model.Base.metadata.tables), before)
|
||||
cls = batch.rowclass
|
||||
self.assertEqual(len(model.Base.metadata.tables), before + 1)
|
||||
session.close()
|
||||
|
||||
def test_create_table(self):
|
||||
session = self.Session()
|
||||
batch = model.Batch()
|
||||
batch.add_column('F01')
|
||||
batch.add_column('F02')
|
||||
session.add(batch)
|
||||
session.commit()
|
||||
batch.create_table()
|
||||
session.close()
|
||||
|
||||
def test_drop_table(self):
|
||||
session = self.Session()
|
||||
batch = model.Batch()
|
||||
batch.add_column('F01')
|
||||
batch.add_column('F02')
|
||||
session.add(batch)
|
||||
session.commit()
|
||||
batch.create_table()
|
||||
batch.drop_table()
|
||||
session.close()
|
||||
|
||||
def test_row_batch(self):
|
||||
session = self.Session()
|
||||
batch = model.Batch()
|
||||
batch.add_column('F01')
|
||||
batch.add_column('F02')
|
||||
batch.add_column('F22')
|
||||
session.add(batch)
|
||||
session.commit()
|
||||
batch.create_table()
|
||||
|
||||
row = batch.rowclass()
|
||||
row.F01 = '074305001321'
|
||||
row.F02 = 'Apple Cider Vinegar'
|
||||
row.F22 = '32oz'
|
||||
batch.add_row(row)
|
||||
|
||||
session.commit()
|
||||
self.assertIs(row.batch, batch)
|
||||
session.close()
|
||||
|
||||
def test_add_row(self):
|
||||
session = self.Session()
|
||||
batch = model.Batch()
|
||||
batch.add_column('F01')
|
||||
batch.add_column('F02')
|
||||
batch.add_column('F22')
|
||||
session.add(batch)
|
||||
session.commit()
|
||||
batch.create_table()
|
||||
|
||||
self.assertEqual(session.query(batch.rowclass).count(), 0)
|
||||
self.assertEqual(batch.rowcount, 0)
|
||||
|
||||
row = batch.rowclass()
|
||||
row.F01 = '074305001321'
|
||||
row.F02 = 'Apple Cider Vinegar'
|
||||
row.F22 = '32oz'
|
||||
batch.add_row(row)
|
||||
|
||||
row = batch.rowclass()
|
||||
row.F01 = '074305001161'
|
||||
row.F02 = 'Apple Cider Vinegar'
|
||||
row.F22 = '16oz'
|
||||
batch.add_row(row)
|
||||
|
||||
session.commit()
|
||||
self.assertEqual(session.query(batch.rowclass).count(), 2)
|
||||
self.assertEqual(batch.rowcount, 2)
|
||||
session.close()
|
||||
|
||||
def test_rows(self):
|
||||
session = self.Session()
|
||||
batch = model.Batch()
|
||||
batch.add_column('F01')
|
||||
batch.add_column('F02')
|
||||
batch.add_column('F22')
|
||||
session.add(batch)
|
||||
session.commit()
|
||||
batch.create_table()
|
||||
|
||||
self.assertEqual(batch.rows.count(), 0)
|
||||
|
||||
row = batch.rowclass()
|
||||
row.F01 = '074305001321'
|
||||
row.F02 = 'Apple Cider Vinegar'
|
||||
row.F22 = '32oz'
|
||||
batch.add_row(row)
|
||||
|
||||
row = batch.rowclass()
|
||||
row.F01 = '074305001161'
|
||||
row.F02 = 'Apple Cider Vinegar'
|
||||
row.F22 = '16oz'
|
||||
batch.add_row(row)
|
||||
|
||||
session.commit()
|
||||
self.assertEqual(batch.rows.count(), 2)
|
||||
session.close()
|
||||
|
||||
|
||||
class PhoneNumberTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
phone = model.PhoneNumber()
|
||||
phone.number = '800-555-1234'
|
||||
self.assertEqual(repr(phone), '<PhoneNumber: 800-555-1234>')
|
||||
|
||||
def test_unicode(self):
|
||||
phone = model.PhoneNumber()
|
||||
phone.number = '800-555-1234'
|
||||
self.assertEqual(unicode(phone), u'800-555-1234')
|
||||
|
||||
|
||||
class EmailAddressTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
email = model.EmailAddress()
|
||||
email.address = 'fred@mailinator.com'
|
||||
self.assertEqual(repr(email), '<EmailAddress: fred@mailinator.com>')
|
||||
|
||||
def test_unicode(self):
|
||||
email = model.EmailAddress()
|
||||
email.address = 'fred@mailinator.com'
|
||||
self.assertEqual(unicode(email), u'fred@mailinator.com')
|
||||
|
||||
|
||||
class PersonTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
person = model.Person()
|
||||
person.display_name = 'Fred Flintstone'
|
||||
self.assertEqual(repr(person), '<Person: Fred Flintstone>')
|
||||
|
||||
def test_unicode(self):
|
||||
person = model.Person()
|
||||
self.assertEqual(unicode(person), u'')
|
||||
person.display_name = 'Fred Flintstone'
|
||||
self.assertEqual(unicode(person), u'Fred Flintstone')
|
||||
|
||||
def test_add_email_address(self):
|
||||
person = model.Person()
|
||||
self.assertEqual(len(person.emails), 0)
|
||||
person.add_email_address('fred@mailinator.com', type='Work')
|
||||
self.assertEqual(len(person.emails), 1)
|
||||
email = person.emails[0]
|
||||
self.assertEqual(email.address, 'fred@mailinator.com')
|
||||
self.assertEqual(email.type, 'Work')
|
||||
|
||||
def test_add_phone_number(self):
|
||||
person = model.Person()
|
||||
self.assertEqual(len(person.phones), 0)
|
||||
person.add_phone_number('800-555-1234', type='Work')
|
||||
self.assertEqual(len(person.phones), 1)
|
||||
phone = person.phones[0]
|
||||
self.assertEqual(phone.number, '800-555-1234')
|
||||
self.assertEqual(phone.type, 'Work')
|
||||
|
||||
|
||||
class StoreTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
store = model.Store()
|
||||
store.id = '1'
|
||||
store.name = 'Acme Brick'
|
||||
self.assertEqual(repr(store), '<Store: 1, Acme Brick>')
|
||||
|
||||
def test_unicode(self):
|
||||
store = model.Store()
|
||||
self.assertEqual(unicode(store), u'')
|
||||
store.name = 'Acme Brick'
|
||||
self.assertEqual(unicode(store), u'Acme Brick')
|
||||
|
||||
def test_add_email_address(self):
|
||||
store = model.Store()
|
||||
self.assertEqual(len(store.emails), 0)
|
||||
store.add_email_address('info@mailinator.com', type='Info')
|
||||
self.assertEqual(len(store.emails), 1)
|
||||
email = store.emails[0]
|
||||
self.assertEqual(email.address, 'info@mailinator.com')
|
||||
self.assertEqual(email.type, 'Info')
|
||||
|
||||
def test_add_phone_number(self):
|
||||
store = model.Store()
|
||||
self.assertEqual(len(store.phones), 0)
|
||||
store.add_phone_number('800-555-1234', type='Help')
|
||||
self.assertEqual(len(store.phones), 1)
|
||||
phone = store.phones[0]
|
||||
self.assertEqual(phone.number, '800-555-1234')
|
||||
self.assertEqual(phone.type, 'Help')
|
||||
|
||||
|
||||
class BrandTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
brand = model.Brand()
|
||||
brand.name = 'Acme'
|
||||
self.assertEqual(repr(brand), '<Brand: Acme>')
|
||||
|
||||
def test_unicode(self):
|
||||
brand = model.Brand()
|
||||
self.assertEqual(unicode(brand), u'')
|
||||
brand.name = 'Acme'
|
||||
self.assertEqual(unicode(brand), u'Acme')
|
||||
|
||||
|
||||
class DepartmentTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
department = model.Department()
|
||||
department.name = 'Grocery'
|
||||
self.assertEqual(repr(department), '<Department: Grocery>')
|
||||
|
||||
def test_unicode(self):
|
||||
department = model.Department()
|
||||
self.assertEqual(unicode(department), u'')
|
||||
department.name = 'Grocery'
|
||||
self.assertEqual(unicode(department), u'Grocery')
|
||||
|
||||
|
||||
class SubdepartmentTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
subdepartment = model.Subdepartment()
|
||||
subdepartment.name = 'Deli'
|
||||
self.assertEqual(repr(subdepartment), '<Subdepartment: Deli>')
|
||||
|
||||
def test_unicode(self):
|
||||
subdepartment = model.Subdepartment()
|
||||
self.assertEqual(unicode(subdepartment), u'')
|
||||
subdepartment.name = 'Deli'
|
||||
self.assertEqual(unicode(subdepartment), u'Deli')
|
||||
|
||||
|
||||
class CategoryTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
category = model.Category()
|
||||
category.number = 420
|
||||
category.name = 'Munchies'
|
||||
self.assertEqual(repr(category), '<Category: 420, Munchies>')
|
||||
|
||||
def test_unicode(self):
|
||||
category = model.Category()
|
||||
self.assertEqual(unicode(category), u'')
|
||||
category.name = 'Munchies'
|
||||
self.assertEqual(unicode(category), u'Munchies')
|
||||
|
||||
|
||||
class VendorContactTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
vendor = model.Vendor()
|
||||
vendor.name = 'Acme Distribution'
|
||||
person = model.Person()
|
||||
person.display_name = 'Fred Flintstone'
|
||||
contact = model.VendorContact()
|
||||
contact.vendor = vendor
|
||||
contact.person = person
|
||||
self.assertEqual(repr(contact), '<VendorContact: Acme Distribution, Fred Flintstone>')
|
||||
|
||||
def test_unicode(self):
|
||||
person = model.Person()
|
||||
person.display_name = 'Fred Flintstone'
|
||||
contact = model.VendorContact()
|
||||
contact.person = person
|
||||
self.assertEqual(unicode(contact), u'Fred Flintstone')
|
||||
|
||||
|
||||
class VendorTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
vendor = model.Vendor()
|
||||
vendor.name = 'Acme Distribution'
|
||||
self.assertEqual(repr(vendor), '<Vendor: Acme Distribution>')
|
||||
|
||||
def test_unicode(self):
|
||||
vendor = model.Vendor()
|
||||
self.assertEqual(unicode(vendor), u'')
|
||||
vendor.name = 'Acme Distribution'
|
||||
self.assertEqual(unicode(vendor), u'Acme Distribution')
|
||||
|
||||
def test_add_email_address(self):
|
||||
vendor = model.Vendor()
|
||||
self.assertEqual(len(vendor.emails), 0)
|
||||
vendor.add_email_address('sales@mailinator.com', type='Sales')
|
||||
self.assertEqual(len(vendor.emails), 1)
|
||||
email = vendor.emails[0]
|
||||
self.assertEqual(email.address, 'sales@mailinator.com')
|
||||
self.assertEqual(email.type, 'Sales')
|
||||
|
||||
def test_add_phone_number(self):
|
||||
vendor = model.Vendor()
|
||||
self.assertEqual(len(vendor.phones), 0)
|
||||
vendor.add_phone_number('800-555-1234', type='Sales')
|
||||
self.assertEqual(len(vendor.phones), 1)
|
||||
phone = vendor.phones[0]
|
||||
self.assertEqual(phone.number, '800-555-1234')
|
||||
self.assertEqual(phone.type, 'Sales')
|
||||
|
||||
def test_add_contact(self):
|
||||
vendor = model.Vendor()
|
||||
self.assertEqual(len(vendor.contacts), 0)
|
||||
person = model.Person()
|
||||
vendor.add_contact(person)
|
||||
self.assertEqual(len(vendor.contacts), 1)
|
||||
self.assertIs(vendor.contacts[0].person, person)
|
||||
|
||||
|
||||
class ProductCostTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
product = model.Product()
|
||||
product.description = 'Test Item'
|
||||
vendor = model.Vendor()
|
||||
vendor.name = 'Acme Distribution'
|
||||
cost = model.ProductCost()
|
||||
cost.product = product
|
||||
cost.vendor = vendor
|
||||
self.assertEqual(repr(cost), '<ProductCost: Test Item : Acme Distribution>')
|
||||
|
||||
|
||||
class ProductPriceTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
product = model.Product()
|
||||
product.description = 'Test Item'
|
||||
price = model.ProductPrice()
|
||||
price.product = product
|
||||
price.price = Decimal('4.20')
|
||||
self.assertEqual(repr(price), '<ProductPrice: Test Item : 4.20>')
|
||||
|
||||
|
||||
class ProductTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
product = model.Product()
|
||||
product.description = 'Test Item'
|
||||
self.assertEqual(repr(product), '<Product: Test Item>')
|
||||
|
||||
def test_unicode(self):
|
||||
product = model.Product()
|
||||
self.assertEqual(unicode(product), u'')
|
||||
product.description = 'Test Item'
|
||||
self.assertEqual(unicode(product), u'Test Item')
|
||||
|
||||
|
||||
class EmployeeTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
person = model.Person()
|
||||
person.display_name = 'Fred Flintstone'
|
||||
employee = model.Employee()
|
||||
employee.person = person
|
||||
self.assertEqual(repr(employee), '<Employee: Fred Flintstone>')
|
||||
|
||||
def test_unicode(self):
|
||||
person = model.Person()
|
||||
person.display_name = 'Fred Flintstone'
|
||||
employee = model.Employee()
|
||||
employee.person = person
|
||||
self.assertEqual(unicode(employee), u'Fred Flintstone')
|
||||
employee.display_name = 'Fred F.'
|
||||
self.assertEqual(unicode(employee), u'Fred F.')
|
||||
|
||||
def test_add_email_address(self):
|
||||
employee = model.Employee()
|
||||
self.assertEqual(len(employee.emails), 0)
|
||||
employee.add_email_address('fred@mailinator.com', type='Home')
|
||||
self.assertEqual(len(employee.emails), 1)
|
||||
email = employee.emails[0]
|
||||
self.assertEqual(email.address, 'fred@mailinator.com')
|
||||
self.assertEqual(email.type, 'Home')
|
||||
|
||||
def test_add_phone_number(self):
|
||||
employee = model.Employee()
|
||||
self.assertEqual(len(employee.phones), 0)
|
||||
employee.add_phone_number('800-555-1234', type='Home')
|
||||
self.assertEqual(len(employee.phones), 1)
|
||||
phone = employee.phones[0]
|
||||
self.assertEqual(phone.number, '800-555-1234')
|
||||
self.assertEqual(phone.type, 'Home')
|
||||
|
||||
|
||||
class CustomerTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
person = model.Person()
|
||||
person.display_name = 'Fred Flintstone'
|
||||
customer = model.Customer()
|
||||
customer.id = '420'
|
||||
customer.person = person
|
||||
self.assertEqual(repr(customer), '<Customer: 420, Fred Flintstone>')
|
||||
customer.name = 'Fred & Wilma Flintstone'
|
||||
self.assertEqual(repr(customer), '<Customer: 420, Fred & Wilma Flintstone>')
|
||||
|
||||
def test_unicode(self):
|
||||
person = model.Person()
|
||||
person.display_name = 'Fred Flintstone'
|
||||
customer = model.Customer()
|
||||
customer.id = '420'
|
||||
customer.person = person
|
||||
self.assertEqual(unicode(customer), u'Fred Flintstone')
|
||||
customer.name = 'Fred & Wilma Flintstone'
|
||||
self.assertEqual(unicode(customer), u'Fred & Wilma Flintstone')
|
||||
|
||||
def test_add_email_address(self):
|
||||
customer = model.Customer()
|
||||
self.assertEqual(len(customer.emails), 0)
|
||||
customer.add_email_address('fred@mailinator.com', type='Home')
|
||||
self.assertEqual(len(customer.emails), 1)
|
||||
email = customer.emails[0]
|
||||
self.assertEqual(email.address, 'fred@mailinator.com')
|
||||
self.assertEqual(email.type, 'Home')
|
||||
|
||||
def test_add_phone_number(self):
|
||||
customer = model.Customer()
|
||||
self.assertEqual(len(customer.phones), 0)
|
||||
customer.add_phone_number('800-555-1234', type='Home')
|
||||
self.assertEqual(len(customer.phones), 1)
|
||||
phone = customer.phones[0]
|
||||
self.assertEqual(phone.number, '800-555-1234')
|
||||
self.assertEqual(phone.type, 'Home')
|
||||
|
||||
|
||||
class CustomerGroupTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
group = model.CustomerGroup()
|
||||
group.id = '420'
|
||||
group.name = 'Important Customers'
|
||||
self.assertEqual(repr(group), '<CustomerGroup: 420, Important Customers>')
|
||||
|
||||
def test_unicode(self):
|
||||
group = model.CustomerGroup()
|
||||
self.assertEqual(unicode(group), u'')
|
||||
group.name = 'Important Customers'
|
||||
self.assertEqual(unicode(group), u'Important Customers')
|
||||
|
||||
|
||||
class LabelProfileTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
profile = model.LabelProfile()
|
||||
profile.description = 'Regular Label'
|
||||
self.assertEqual(repr(profile), '<LabelProfile: Regular Label>')
|
||||
|
||||
def test_unicode(self):
|
||||
profile = model.LabelProfile()
|
||||
self.assertEqual(unicode(profile), u'')
|
||||
profile.description = 'Regular Label'
|
||||
self.assertEqual(unicode(profile), u'Regular Label')
|
||||
|
||||
|
||||
class RoleTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
role = model.Role()
|
||||
role.name = 'Manager'
|
||||
self.assertEqual(repr(role), '<Role: Manager>')
|
||||
|
||||
def test_unicode(self):
|
||||
role = model.Role()
|
||||
self.assertEqual(unicode(role), u'')
|
||||
role.name = 'Manager'
|
||||
self.assertEqual(unicode(role), u'Manager')
|
||||
|
||||
|
||||
class UserTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
user = model.User()
|
||||
user.username = 'fred'
|
||||
self.assertEqual(repr(user), '<User: fred>')
|
||||
|
||||
def test_unicode(self):
|
||||
user = model.User()
|
||||
self.assertEqual(unicode(user), u'')
|
||||
user.username = 'fred'
|
||||
self.assertEqual(unicode(user), u'fred')
|
||||
|
||||
def test_display_name(self):
|
||||
user = model.User()
|
||||
user.username = 'fred'
|
||||
self.assertEqual(user.display_name, 'fred')
|
||||
person = model.Person()
|
||||
user.person = person
|
||||
self.assertEqual(user.display_name, 'fred')
|
||||
person.display_name = 'Fred Flintstone'
|
||||
self.assertEqual(user.display_name, 'Fred Flintstone')
|
||||
|
||||
|
||||
class UserRoleTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
fred = model.User()
|
||||
fred.username = 'fred'
|
||||
manager = model.Role()
|
||||
manager.name = 'Manager'
|
||||
user_role = model.UserRole()
|
||||
user_role.user = fred
|
||||
user_role.role = manager
|
||||
self.assertEqual(repr(user_role), '<UserRole: fred : Manager>')
|
||||
|
||||
|
||||
class PermissionTest(unittest.TestCase):
|
||||
|
||||
def test_repr(self):
|
||||
manager = model.Role()
|
||||
manager.name = 'Manager'
|
||||
permission = model.Permission()
|
||||
permission.role = manager
|
||||
permission.permission = 'products.delete'
|
||||
self.assertEqual(repr(permission), '<Permission: Manager, products.delete>')
|
||||
|
||||
def test_unicode(self):
|
||||
permission = model.Permission()
|
||||
self.assertEqual(unicode(permission), u'')
|
||||
permission.permission = 'products.delete'
|
||||
self.assertEqual(unicode(permission), u'products.delete')
|
||||
167
rattail/tests/db/test_types.py
Normal file
167
rattail/tests/db/test_types.py
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy import Column, Integer, String, Numeric, Boolean
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from rattail.db import types
|
||||
from rattail.barcodes import GPC
|
||||
from rattail.exceptions import SILInvalidDataType
|
||||
|
||||
|
||||
Base = declarative_base()
|
||||
Session = sessionmaker()
|
||||
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = 'products'
|
||||
id = Column(Integer, primary_key=True)
|
||||
upc = Column(types.GPCType)
|
||||
|
||||
|
||||
class GPCTypeTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
engine = create_engine('sqlite://')
|
||||
Base.metadata.create_all(engine)
|
||||
Session.configure(bind=engine)
|
||||
|
||||
def test_store_integer(self):
|
||||
session = Session()
|
||||
product = Product()
|
||||
product.upc = 74305001321
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = Session()
|
||||
product = session.query(Product).one()
|
||||
self.assertIsInstance(product.upc, GPC)
|
||||
self.assertEqual(product.upc, GPC('00074305001321'))
|
||||
session.close()
|
||||
|
||||
def test_store_string(self):
|
||||
session = Session()
|
||||
product = Product()
|
||||
product.upc = '074305001321'
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = Session()
|
||||
product = session.query(Product).one()
|
||||
self.assertIsInstance(product.upc, GPC)
|
||||
self.assertEqual(product.upc, GPC('00074305001321'))
|
||||
session.close()
|
||||
|
||||
def test_store_gpc(self):
|
||||
session = Session()
|
||||
product = Product()
|
||||
product.upc = GPC('00074305001321')
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = Session()
|
||||
product = session.query(Product).one()
|
||||
self.assertIsInstance(product.upc, GPC)
|
||||
self.assertEqual(product.upc, GPC('00074305001321'))
|
||||
session.close()
|
||||
|
||||
def test_store_none(self):
|
||||
session = Session()
|
||||
product = Product()
|
||||
product.upc = None
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = Session()
|
||||
product = session.query(Product).one()
|
||||
self.assertEqual(product.upc, None)
|
||||
session.close()
|
||||
|
||||
def test_query_integer(self):
|
||||
session = Session()
|
||||
product = Product()
|
||||
product.upc = GPC('00074305001321')
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = Session()
|
||||
query = session.query(Product).filter_by(upc=74305001321)
|
||||
self.assertEqual(query.count(), 1)
|
||||
session.close()
|
||||
|
||||
def test_query_string(self):
|
||||
session = Session()
|
||||
product = Product()
|
||||
product.upc = GPC('00074305001321')
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = Session()
|
||||
query = session.query(Product).filter_by(upc='074305001321')
|
||||
self.assertEqual(query.count(), 1)
|
||||
session.close()
|
||||
|
||||
def test_query_gpc(self):
|
||||
session = Session()
|
||||
product = Product()
|
||||
product.upc = GPC('00074305001321')
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = Session()
|
||||
query = session.query(Product).filter_by(upc=GPC('00074305001321'))
|
||||
self.assertEqual(query.count(), 1)
|
||||
session.close()
|
||||
|
||||
def test_query_none(self):
|
||||
session = Session()
|
||||
product = Product()
|
||||
product.upc = None
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = Session()
|
||||
query = session.query(Product).filter_by(upc=None)
|
||||
self.assertEqual(query.count(), 1)
|
||||
session.close()
|
||||
|
||||
|
||||
class GetSILColumnTypeTest(unittest.TestCase):
|
||||
|
||||
def test_gpc(self):
|
||||
self.assertIsInstance(types.get_sil_column_type('GPC(14)'), types.GPCType)
|
||||
|
||||
def test_boolean(self):
|
||||
self.assertIsInstance(types.get_sil_column_type('FLAG(1)'), Boolean)
|
||||
|
||||
def test_string(self):
|
||||
type_ = types.get_sil_column_type('CHAR(30)')
|
||||
self.assertIsInstance(type_, String)
|
||||
self.assertEqual(type_.length, 30)
|
||||
type_ = types.get_sil_column_type('CHAR(255)')
|
||||
self.assertIsInstance(type_, String)
|
||||
self.assertEqual(type_.length, 255)
|
||||
|
||||
def test_numeric(self):
|
||||
type_ = types.get_sil_column_type('NUMBER(9,5)')
|
||||
self.assertIsInstance(type_, Numeric)
|
||||
self.assertEqual(type_.precision, 9)
|
||||
self.assertEqual(type_.scale, 5)
|
||||
type_ = types.get_sil_column_type('NUMBER(4,0)')
|
||||
self.assertIsInstance(type_, Numeric)
|
||||
self.assertEqual(type_.precision, 4)
|
||||
self.assertEqual(type_.scale, 0)
|
||||
|
||||
def test_invalid(self):
|
||||
self.assertRaises(SILInvalidDataType, types.get_sil_column_type, 'BOGUS(20)')
|
||||
61
rattail/tests/db/test_util.py
Normal file
61
rattail/tests/db/test_util.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine, MetaData
|
||||
|
||||
from rattail import db
|
||||
from rattail.db import util
|
||||
from rattail.db.model import Product
|
||||
from rattail.util import Object
|
||||
from rattail.exceptions import NoDefaultDatabase
|
||||
|
||||
|
||||
class InstallCoreSchemaTest(unittest.TestCase):
|
||||
|
||||
def test_install_core_schema(self):
|
||||
engine = create_engine('sqlite://')
|
||||
util.install_core_schema(engine)
|
||||
|
||||
session = db.Session(bind=engine)
|
||||
self.assertEqual(session.query(Product).count(), 0)
|
||||
product = Product()
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
session = db.Session(bind=engine)
|
||||
self.assertEqual(session.query(Product).count(), 1)
|
||||
session.close()
|
||||
|
||||
def test_install_no_engine(self):
|
||||
self.assertRaises(NoDefaultDatabase, util.install_core_schema)
|
||||
|
||||
|
||||
class CoreSchemaInstalledTest(unittest.TestCase):
|
||||
|
||||
def test_no_database(self):
|
||||
self.assertRaises(NoDefaultDatabase, util.core_schema_installed)
|
||||
|
||||
def test_empty_database(self):
|
||||
db.engine = create_engine('sqlite://')
|
||||
self.assertEqual(util.core_schema_installed(), False)
|
||||
db.engine = None
|
||||
|
||||
def test_invalid_database(self):
|
||||
db.engine = create_engine('mysql://this_had_better_not_exist')
|
||||
self.assertEqual(util.core_schema_installed(), False)
|
||||
db.engine = None
|
||||
|
||||
def test_good_database(self):
|
||||
engine = create_engine('sqlite://')
|
||||
util.install_core_schema(engine)
|
||||
self.assertEqual(util.core_schema_installed(engine), True)
|
||||
|
||||
|
||||
class GetCoreMetadataTest(unittest.TestCase):
|
||||
|
||||
def test_get_core_metadata(self):
|
||||
meta = util.get_core_metadata()
|
||||
self.assertIsInstance(meta, MetaData)
|
||||
self.assertGreater(len(meta.tables), 1)
|
||||
1
rattail/tests/sil/__init__.py
Normal file
1
rattail/tests/sil/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
#!/usr/bin/env python
|
||||
31
rattail/tests/sil/test_batches.py
Normal file
31
rattail/tests/sil/test_batches.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
import os
|
||||
|
||||
from rattail.sil import batches
|
||||
from rattail.configuration import UserConfigFileStorage
|
||||
from rattail.files import temp_path
|
||||
|
||||
|
||||
class ConsumeBatchIDTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.config_path = temp_path('.conf')
|
||||
self.storage = UserConfigFileStorage(self.config_path)
|
||||
|
||||
def tearDown(self):
|
||||
os.remove(self.config_path)
|
||||
|
||||
def test_typical(self):
|
||||
self.assertEqual(self.storage.get('rattail.sil', 'next_batch_id.test'), None)
|
||||
self.assertEqual(batches.consume_batch_id('test', self.storage), '00000001')
|
||||
self.assertEqual(self.storage.get('rattail.sil', 'next_batch_id.test'), '2')
|
||||
self.assertEqual(batches.consume_batch_id('test', self.storage), '00000002')
|
||||
self.assertEqual(self.storage.get('rattail.sil', 'next_batch_id.test'), '3')
|
||||
|
||||
def test_invalid(self):
|
||||
self.storage.put('rattail.sil', 'next_batch_id.test', 'invalid')
|
||||
self.assertEqual(batches.consume_batch_id('test', self.storage), '00000001')
|
||||
self.assertEqual(self.storage.get('rattail.sil', 'next_batch_id.test'), '2')
|
||||
87
rattail/tests/sil/test_columns.py
Normal file
87
rattail/tests/sil/test_columns.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail.sil import columns
|
||||
from rattail import exceptions
|
||||
|
||||
|
||||
class ColumnTest(unittest.TestCase):
|
||||
|
||||
def test_constructor(self):
|
||||
col = columns.Column('F01', 'GPC(14)',
|
||||
"Primary Item U.P.C. Number (Key)", "UPC")
|
||||
self.assertEqual(col.name, 'F01')
|
||||
self.assertEqual(col.data_type, 'GPC(14)')
|
||||
self.assertEqual(col.description, "Primary Item U.P.C. Number (Key)")
|
||||
self.assertEqual(col.display_name, "UPC")
|
||||
|
||||
def test_constructor_without_display_name(self):
|
||||
col = columns.Column('F01', 'GPC(14)',
|
||||
"Primary Item U.P.C. Number (Key)")
|
||||
self.assertEqual(col.name, 'F01')
|
||||
self.assertEqual(col.data_type, 'GPC(14)')
|
||||
self.assertEqual(col.description, "Primary Item U.P.C. Number (Key)")
|
||||
self.assertEqual(col.display_name, "Primary Item U.P.C. Number (Key)")
|
||||
|
||||
def test_repr(self):
|
||||
col = columns.Column('F01', 'GPC(14)', "UPC")
|
||||
self.assertEqual(repr(col), '<Column: F01>')
|
||||
|
||||
def test_str(self):
|
||||
col = columns.Column('F01', 'GPC(14)', "UPC")
|
||||
self.assertEqual(str(col), 'F01')
|
||||
|
||||
|
||||
class FunctionsTest(unittest.TestCase):
|
||||
|
||||
def test_provide_columns(self):
|
||||
cols = columns.provide_columns()
|
||||
self.assertTrue(cols)
|
||||
|
||||
# Do a sanity check on some of the columns.
|
||||
|
||||
self.assertIn('F01', cols)
|
||||
col = cols['F01']
|
||||
self.assertEqual(col.data_type, 'GPC(14)')
|
||||
self.assertEqual(col.description, "Primary Item U.P.C. Number (Key)")
|
||||
self.assertEqual(col.display_name, "UPC")
|
||||
|
||||
self.assertIn('F30', cols)
|
||||
col = cols['F30']
|
||||
self.assertEqual(col.data_type, 'NUMBER(8,3)')
|
||||
self.assertEqual(col.description, "Retail Sell Price")
|
||||
self.assertEqual(col.display_name, "Retail Sell Price")
|
||||
|
||||
self.assertIn('F19', cols)
|
||||
col = cols['F19']
|
||||
self.assertEqual(col.data_type, 'NUMBER(4,0)')
|
||||
self.assertEqual(col.description, "Case Pack Size")
|
||||
self.assertEqual(col.display_name, "Case Pack Size")
|
||||
|
||||
self.assertIn('F03', cols)
|
||||
col = cols['F03']
|
||||
self.assertEqual(col.data_type, 'NUMBER(4,0)')
|
||||
self.assertEqual(col.description, "Department Number")
|
||||
self.assertEqual(col.display_name, "Department Number")
|
||||
|
||||
self.assertIn('F27', cols)
|
||||
col = cols['F27']
|
||||
self.assertEqual(col.data_type, 'CHAR(9)')
|
||||
self.assertEqual(col.description, "Vendor Number")
|
||||
self.assertEqual(col.display_name, "Vendor Number")
|
||||
|
||||
self.assertIn('R38', cols)
|
||||
col = cols['R38']
|
||||
self.assertEqual(col.data_type, 'NUMBER(9,5)')
|
||||
self.assertEqual(col.description, "Unit Receiving Base Cost")
|
||||
self.assertEqual(col.display_name, "Unit Receiving Base Cost")
|
||||
|
||||
def test_get_column(self):
|
||||
col = columns.get_column('F01')
|
||||
self.assertEqual(col.data_type, 'GPC(14)')
|
||||
self.assertEqual(col.description, "Primary Item U.P.C. Number (Key)")
|
||||
self.assertEqual(col.display_name, "UPC")
|
||||
|
||||
def test_get_column_invalid(self):
|
||||
self.assertRaises(exceptions.SILColumnNotFound, columns.get_column, 'FXXX')
|
||||
217
rattail/tests/sil/test_writer.py
Normal file
217
rattail/tests/sil/test_writer.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
import os
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import rattail
|
||||
from rattail.sil.writer import Writer
|
||||
from rattail.files import temp_path
|
||||
|
||||
|
||||
class WriterTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.sil_path = temp_path('.sil')
|
||||
|
||||
def tearDown(self):
|
||||
os.remove(self.sil_path)
|
||||
|
||||
def test_construct(self):
|
||||
writer = Writer(self.sil_path)
|
||||
self.assertEqual(writer.sil_path, self.sil_path)
|
||||
writer.close()
|
||||
|
||||
def test_write(self):
|
||||
writer = Writer(self.sil_path)
|
||||
writer.write('simple test')
|
||||
writer.close()
|
||||
|
||||
sil_file = open(self.sil_path, 'r')
|
||||
self.assertEqual(sil_file.read(), 'simple test')
|
||||
sil_file.close()
|
||||
|
||||
def test_val_none(self):
|
||||
writer = Writer(self.sil_path)
|
||||
self.assertEqual(writer.val(None), '')
|
||||
writer.close()
|
||||
|
||||
def test_val_gpc(self):
|
||||
writer = Writer(self.sil_path)
|
||||
self.assertEqual(writer.val(rattail.GPC('074305001321')), '00074305001321')
|
||||
writer.close()
|
||||
|
||||
def test_val_int(self):
|
||||
writer = Writer(self.sil_path)
|
||||
self.assertEqual(writer.val(420), '420')
|
||||
writer.close()
|
||||
|
||||
def test_val_float(self):
|
||||
writer = Writer(self.sil_path)
|
||||
self.assertEqual(writer.val(420.0), '420.0')
|
||||
writer.close()
|
||||
|
||||
def test_val_decimal(self):
|
||||
writer = Writer(self.sil_path)
|
||||
self.assertEqual(writer.val(Decimal('420.0000')), '420.0000')
|
||||
writer.close()
|
||||
|
||||
def test_val_date(self):
|
||||
writer = Writer(self.sil_path)
|
||||
self.assertEqual(writer.val(datetime.date(2012, 12, 5)), '2012340')
|
||||
writer.close()
|
||||
|
||||
def test_val_time(self):
|
||||
writer = Writer(self.sil_path)
|
||||
self.assertEqual(writer.val(datetime.time(16, 20)), '1620')
|
||||
writer.close()
|
||||
|
||||
def test_val_str(self):
|
||||
writer = Writer(self.sil_path)
|
||||
self.assertEqual(writer.val('test'), "'test'")
|
||||
writer.close()
|
||||
|
||||
def test_val_unicode(self):
|
||||
writer = Writer(self.sil_path)
|
||||
self.assertEqual(writer.val(u'test'), "'test'")
|
||||
writer.close()
|
||||
|
||||
def test_val_object(self):
|
||||
writer = Writer(self.sil_path)
|
||||
self.assertRegexpMatches(writer.val(object()), r"^'<object object at 0x.*>'$")
|
||||
writer.close()
|
||||
|
||||
def test_write_batch_header_raw(self):
|
||||
writer = Writer(self.sil_path)
|
||||
writer.write_batch_header_raw()
|
||||
writer.close()
|
||||
|
||||
sil_file = open(self.sil_path, 'r')
|
||||
self.assertEqual(sil_file.read(), 'INSERT INTO HEADER_DCT VALUES\n(,,,,,,,,,,,,,,,,,,,,,,);\n\n')
|
||||
sil_file.close()
|
||||
|
||||
def parse_header_fields(self):
|
||||
sil_file = open(self.sil_path, 'r')
|
||||
sil_file.readline()
|
||||
fields = sil_file.readline()
|
||||
fields = fields.lstrip('(')
|
||||
fields = fields.rstrip(');\n')
|
||||
fields = fields.split(',')
|
||||
sil_file.close()
|
||||
return fields
|
||||
|
||||
def test_write_batch_header(self):
|
||||
writer = Writer(self.sil_path)
|
||||
writer.write_batch_header(
|
||||
H01='HM',
|
||||
H02='00000420',
|
||||
H03='SOMEGUY',
|
||||
H04='ANOTHER',
|
||||
H05='audit.txt',
|
||||
H06='response.txt',
|
||||
H07=datetime.date(2012, 12, 5),
|
||||
H08=datetime.time(16, 20),
|
||||
H09=datetime.date(2012, 12, 6),
|
||||
H10=datetime.time(4, 20),
|
||||
H11=datetime.date(2012, 12, 7),
|
||||
H12='ADDRPL',
|
||||
H13="Batch Test",
|
||||
H14='something',
|
||||
H15='something else',
|
||||
H16='another something',
|
||||
H17=2,
|
||||
H18=420,
|
||||
H19='1/1.00',
|
||||
H20='4.20',
|
||||
H21='F03+F01',
|
||||
H22='ABC1000=RESET SALES;DEF960=SALES_RESET;GHI3000=ZEROSALES;',
|
||||
H23='6.0',
|
||||
)
|
||||
writer.close()
|
||||
|
||||
sil_file = open(self.sil_path, 'r')
|
||||
self.assertEqual(sil_file.read(), """\
|
||||
INSERT INTO HEADER_DCT VALUES
|
||||
('HM','00000420','SOMEGUY','ANOTHER','audit.txt','response.txt',2012340,1620,2012341,0420,2012342,'ADDRPL','Batch Test','something','something else','another something',2,420,'1/1.00','4.20','F03+F01','ABC1000=RESET SALES;DEF960=SALES_RESET;GHI3000=ZEROSALES;','6.0');
|
||||
|
||||
""")
|
||||
sil_file.close()
|
||||
|
||||
def test_write_batch_header_defaults(self):
|
||||
writer = Writer(self.sil_path)
|
||||
writer.write_batch_header()
|
||||
writer.close()
|
||||
|
||||
fields = self.parse_header_fields()
|
||||
self.assertEqual(fields[0], '')
|
||||
self.assertEqual(fields[1], '')
|
||||
self.assertEqual(fields[2], "'RATAIL'")
|
||||
self.assertEqual(fields[3], '')
|
||||
self.assertEqual(fields[4], '')
|
||||
self.assertEqual(fields[5], '')
|
||||
self.assertRegexpMatches(fields[6], r'^\d{7}$')
|
||||
self.assertRegexpMatches(fields[7], r'^\d{4}$')
|
||||
self.assertEqual(fields[8], '0000000')
|
||||
self.assertEqual(fields[9], '0000')
|
||||
self.assertRegexpMatches(fields[10], r'^\d{7}$')
|
||||
self.assertEqual(fields[11], '')
|
||||
self.assertEqual(fields[12], '')
|
||||
self.assertEqual(fields[13], '')
|
||||
self.assertEqual(fields[14], '')
|
||||
self.assertEqual(fields[15], '')
|
||||
self.assertEqual(fields[16], '')
|
||||
self.assertEqual(fields[17], '')
|
||||
self.assertEqual(fields[18], '')
|
||||
self.assertEqual(fields[19], "'%s'" % rattail.__version__[:4])
|
||||
self.assertEqual(fields[20], '')
|
||||
self.assertEqual(fields[21], '')
|
||||
self.assertEqual(fields[22], '')
|
||||
|
||||
def test_write_create_header(self):
|
||||
writer = Writer(self.sil_path)
|
||||
writer.write_create_header()
|
||||
writer.close()
|
||||
|
||||
fields = self.parse_header_fields()
|
||||
self.assertEqual(fields[0], "'HC'")
|
||||
self.assertEqual(fields[11], "'LOAD'")
|
||||
|
||||
def test_write_maintenance_header(self):
|
||||
writer = Writer(self.sil_path)
|
||||
writer.write_maintenance_header()
|
||||
writer.close()
|
||||
|
||||
fields = self.parse_header_fields()
|
||||
self.assertEqual(fields[0], "'HM'")
|
||||
|
||||
def test_write_row(self):
|
||||
writer = Writer(self.sil_path)
|
||||
writer.write_row([419, 'test'])
|
||||
writer.write_row([420, 'another'], last=True)
|
||||
writer.close()
|
||||
|
||||
sil_file = open(self.sil_path, 'r')
|
||||
self.assertEqual(sil_file.read(), """\
|
||||
(419,'test'),
|
||||
(420,'another');
|
||||
""")
|
||||
sil_file.close()
|
||||
|
||||
def test_write_rows(self):
|
||||
writer = Writer(self.sil_path)
|
||||
writer.write_rows([
|
||||
(419, 'test'),
|
||||
(420, 'another'),
|
||||
])
|
||||
writer.close()
|
||||
|
||||
sil_file = open(self.sil_path, 'r')
|
||||
self.assertEqual(sil_file.read(), """\
|
||||
(419,'test'),
|
||||
(420,'another');
|
||||
""")
|
||||
sil_file.close()
|
||||
|
||||
|
||||
324
rattail/tests/test_barcodes.py
Normal file
324
rattail/tests/test_barcodes.py
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail import GPC
|
||||
from rattail import barcodes
|
||||
|
||||
|
||||
class GPCTest(unittest.TestCase):
|
||||
|
||||
def test_constructor_with_None(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertEqual(gpc, '074305001321')
|
||||
|
||||
def test_constructor_with_upc(self):
|
||||
gpc = GPC('07430500132', calc_check_digit='upc')
|
||||
self.assertEqual(gpc, '074305001321')
|
||||
|
||||
def test_constructor_with_true(self):
|
||||
gpc = GPC('07430500132', calc_check_digit=True)
|
||||
self.assertEqual(gpc, '074305001321')
|
||||
|
||||
def test_eq_gpc(self):
|
||||
gpc1 = GPC('074305001321')
|
||||
gpc2 = GPC('074305001321')
|
||||
self.assertEqual(gpc1, gpc2)
|
||||
|
||||
def test_eq_int(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertEqual(gpc, 74305001321)
|
||||
|
||||
def test_eq_string(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertEqual(gpc, '074305001321')
|
||||
|
||||
def test_eq_string_invalid(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertRaises(ValueError, gpc.__eq__, 'invalid')
|
||||
|
||||
def test_eq_object_invalid(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertRaises(TypeError, gpc.__eq__, object())
|
||||
|
||||
def test_ne_gpc(self):
|
||||
gpc1 = GPC('074305001321')
|
||||
gpc2 = GPC('074305001161')
|
||||
self.assertNotEqual(gpc1, gpc2)
|
||||
|
||||
def test_ne_int(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertNotEqual(gpc, 74305001161)
|
||||
|
||||
def test_ne_string(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertNotEqual(gpc, '074305001161')
|
||||
|
||||
def test_ne_string_invalid(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertRaises(ValueError, gpc.__ne__, 'invalid')
|
||||
|
||||
def test_ne_object_invalid(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertRaises(TypeError, gpc.__ne__, object())
|
||||
|
||||
def test_gt_gpc(self):
|
||||
gpc1 = GPC('074305001321')
|
||||
gpc2 = GPC('074305001161')
|
||||
self.assertTrue(gpc1 > gpc2)
|
||||
|
||||
def test_gt_int(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertTrue(gpc > 74305001161)
|
||||
|
||||
def test_gt_string(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertTrue(gpc > '074305001161')
|
||||
|
||||
def test_gt_string_invalid(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertRaises(ValueError, gpc.__gt__, 'invalid')
|
||||
|
||||
def test_gt_object_invalid(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertRaises(TypeError, gpc.__gt__, object())
|
||||
|
||||
def test_ge_gpc(self):
|
||||
gpc1 = GPC('074305001321')
|
||||
gpc2 = GPC('074305001161')
|
||||
self.assertTrue(gpc1 >= gpc2)
|
||||
|
||||
def test_ge_int(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertTrue(gpc >= 74305001161)
|
||||
|
||||
def test_ge_string(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertTrue(gpc >= '074305001161')
|
||||
|
||||
def test_ge_string_invalid(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertRaises(ValueError, gpc.__ge__, 'invalid')
|
||||
|
||||
def test_ge_object_invalid(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertRaises(TypeError, gpc.__ge__, object())
|
||||
|
||||
def test_lt_gpc(self):
|
||||
gpc1 = GPC('074305001161')
|
||||
gpc2 = GPC('074305001321')
|
||||
self.assertTrue(gpc1 < gpc2)
|
||||
|
||||
def test_lt_int(self):
|
||||
gpc = GPC('074305001161')
|
||||
self.assertTrue(gpc < 74305001321)
|
||||
|
||||
def test_lt_string(self):
|
||||
gpc = GPC('074305001161')
|
||||
self.assertTrue(gpc < '074305001321')
|
||||
|
||||
def test_lt_string_invalid(self):
|
||||
gpc = GPC('074305001161')
|
||||
self.assertRaises(ValueError, gpc.__lt__, 'invalid')
|
||||
|
||||
def test_lt_object_invalid(self):
|
||||
gpc = GPC('074305001161')
|
||||
self.assertRaises(TypeError, gpc.__lt__, object())
|
||||
|
||||
def test_le_gpc(self):
|
||||
gpc1 = GPC('074305001161')
|
||||
gpc2 = GPC('074305001321')
|
||||
self.assertTrue(gpc1 <= gpc2)
|
||||
|
||||
def test_le_int(self):
|
||||
gpc = GPC('074305001161')
|
||||
self.assertTrue(gpc <= 74305001321)
|
||||
|
||||
def test_le_string(self):
|
||||
gpc = GPC('074305001161')
|
||||
self.assertTrue(gpc <= '074305001321')
|
||||
|
||||
def test_le_string_invalid(self):
|
||||
gpc = GPC('074305001161')
|
||||
self.assertRaises(ValueError, gpc.__le__, 'invalid')
|
||||
|
||||
def test_le_object_invalid(self):
|
||||
gpc = GPC('074305001161')
|
||||
self.assertRaises(TypeError, gpc.__le__, object())
|
||||
|
||||
def test_hash(self):
|
||||
gpc1 = GPC('074305001321')
|
||||
gpc2 = GPC('074305001161')
|
||||
gpc3 = GPC('074305001321')
|
||||
self.assertNotEqual(hash(gpc1), hash(gpc2))
|
||||
self.assertEqual(hash(gpc1), hash(gpc3))
|
||||
|
||||
def test_int(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertEqual(int(gpc), 74305001321)
|
||||
|
||||
def test_long(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertEqual(long(gpc), 74305001321L)
|
||||
|
||||
def test_repr(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertEqual(repr(gpc), "GPC('00074305001321')")
|
||||
|
||||
def test_str(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertEqual(str(gpc), '00074305001321')
|
||||
|
||||
def test_unicode(self):
|
||||
gpc = GPC('074305001321')
|
||||
self.assertEqual(unicode(gpc), u'00074305001321')
|
||||
|
||||
|
||||
class UPCCheckDigitTest(unittest.TestCase):
|
||||
|
||||
def test_calculations(self):
|
||||
data = [
|
||||
('07430500132', 1),
|
||||
('03600029145', 2),
|
||||
('03600024145', 7),
|
||||
('01010101010', 5),
|
||||
('07430503112', 0),
|
||||
]
|
||||
for in_, out in data:
|
||||
self.assertEqual(barcodes.upc_check_digit(in_), out)
|
||||
|
||||
|
||||
class PriceCheckDigitTest(unittest.TestCase):
|
||||
|
||||
def test_calculations(self):
|
||||
data = [
|
||||
('0512', 3),
|
||||
]
|
||||
for in_, out in data:
|
||||
self.assertEqual(barcodes.price_check_digit(in_), out)
|
||||
|
||||
def test_short_string(self):
|
||||
self.assertRaises(ValueError, barcodes.price_check_digit, '123')
|
||||
|
||||
def test_long_string(self):
|
||||
self.assertRaises(ValueError, barcodes.price_check_digit, '12345')
|
||||
|
||||
def test_not_string(self):
|
||||
self.assertRaises(ValueError, barcodes.price_check_digit, 1234)
|
||||
|
||||
|
||||
class LuhnCheckDigitTest(unittest.TestCase):
|
||||
|
||||
def test_calculations(self):
|
||||
data = [
|
||||
('7992739871', 3),
|
||||
('4992739871', 6),
|
||||
('123456781234567', 0),
|
||||
]
|
||||
for in_, out in data:
|
||||
self.assertEqual(barcodes.luhn_check_digit(in_), out)
|
||||
|
||||
|
||||
class UPCEToUPCATest(unittest.TestCase):
|
||||
|
||||
def test_without_input_check_digit(self):
|
||||
data = [
|
||||
('123450', '01200000345'),
|
||||
('123451', '01210000345'),
|
||||
('123452', '01220000345'),
|
||||
('123453', '01230000045'),
|
||||
('123454', '01234000005'),
|
||||
('123455', '01234500005'),
|
||||
('123456', '01234500006'),
|
||||
('123457', '01234500007'),
|
||||
('123458', '01234500008'),
|
||||
('123459', '01234500009'),
|
||||
('654321', '06510000432'),
|
||||
('425261', '04210000526'),
|
||||
('234903', '02340000090'),
|
||||
('234567', '02345600007'),
|
||||
('234514', '02345000001'),
|
||||
('639712', '06320000971'),
|
||||
('867933', '08670000093'),
|
||||
('913579', '09135700009'),
|
||||
('421184', '04211000008'),
|
||||
]
|
||||
for in_, out in data:
|
||||
self.assertEqual(barcodes.upce_to_upca(in_), out)
|
||||
|
||||
def test_with_input_check_digit(self):
|
||||
data = [
|
||||
('01234505', '01200000345'),
|
||||
('01234514', '01210000345'),
|
||||
('01234523', '01220000345'),
|
||||
('01234531', '01230000045'),
|
||||
('01234543', '01234000005'),
|
||||
('01234558', '01234500005'),
|
||||
('01234565', '01234500006'),
|
||||
('01234572', '01234500007'),
|
||||
('01234589', '01234500008'),
|
||||
('01234596', '01234500009'),
|
||||
('06543217', '06510000432'),
|
||||
('04252614', '04210000526'),
|
||||
('02349036', '02340000090'),
|
||||
('02345673', '02345600007'),
|
||||
('02345147', '02345000001'),
|
||||
('06397126', '06320000971'),
|
||||
('08679339', '08670000093'),
|
||||
('09135796', '09135700009'),
|
||||
('04211842', '04211000008'),
|
||||
]
|
||||
for in_, out in data:
|
||||
self.assertEqual(barcodes.upce_to_upca(in_), out)
|
||||
|
||||
def test_with_output_check_digit(self):
|
||||
data = [
|
||||
('123450', '012000003455'),
|
||||
('123451', '012100003454'),
|
||||
('123452', '012200003453'),
|
||||
('123453', '012300000451'),
|
||||
('123454', '012340000053'),
|
||||
('123455', '012345000058'),
|
||||
('123456', '012345000065'),
|
||||
('123457', '012345000072'),
|
||||
('123458', '012345000089'),
|
||||
('123459', '012345000096'),
|
||||
('654321', '065100004327'),
|
||||
('425261', '042100005264'),
|
||||
('234903', '023400000906'),
|
||||
('234567', '023456000073'),
|
||||
('234514', '023450000017'),
|
||||
('639712', '063200009716'),
|
||||
('867933', '086700000939'),
|
||||
('913579', '091357000096'),
|
||||
('421184', '042110000082'),
|
||||
]
|
||||
for in_, out in data:
|
||||
self.assertEqual(
|
||||
barcodes.upce_to_upca(in_, include_check_digit=True), out)
|
||||
|
||||
def test_with_input_and_output_check_digits(self):
|
||||
data = [
|
||||
('01234505', '012000003455'),
|
||||
('01234514', '012100003454'),
|
||||
('01234523', '012200003453'),
|
||||
('01234531', '012300000451'),
|
||||
('01234543', '012340000053'),
|
||||
('01234558', '012345000058'),
|
||||
('01234565', '012345000065'),
|
||||
('01234572', '012345000072'),
|
||||
('01234589', '012345000089'),
|
||||
('01234596', '012345000096'),
|
||||
('06543217', '065100004327'),
|
||||
('04252614', '042100005264'),
|
||||
('02349036', '023400000906'),
|
||||
('02345673', '023456000073'),
|
||||
('02345147', '023450000017'),
|
||||
('06397126', '063200009716'),
|
||||
('08679339', '086700000939'),
|
||||
('09135796', '091357000096'),
|
||||
('04211842', '042110000082'),
|
||||
]
|
||||
for in_, out in data:
|
||||
self.assertEqual(
|
||||
barcodes.upce_to_upca(in_, include_check_digit=True), out)
|
||||
224
rattail/tests/test_commands.py
Normal file
224
rattail/tests/test_commands.py
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail.tests import get_config
|
||||
|
||||
import os
|
||||
import argparse
|
||||
from cStringIO import StringIO
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
import rattail
|
||||
from rattail import db
|
||||
from rattail import commands
|
||||
from rattail import initialization
|
||||
from rattail.util import Object
|
||||
from rattail.files import resource_path, temp_path
|
||||
from rattail.db.util import install_core_schema
|
||||
|
||||
|
||||
class ArgumentParserTest(unittest.TestCase):
|
||||
|
||||
def test_parse_args(self):
|
||||
parser = commands.ArgumentParser()
|
||||
args = parser.parse_args(['--config', 'test.conf', 'something'])
|
||||
self.assertListEqual(args.argv, ['--config', 'test.conf', 'something'])
|
||||
|
||||
|
||||
class SampleCommand(commands.Subcommand):
|
||||
|
||||
name = 'sample'
|
||||
description = 'Just for testing...'
|
||||
|
||||
def print_help(self):
|
||||
self.output_stream.write('bogus help!!!')
|
||||
|
||||
def run(self, args):
|
||||
self.output_stream.write('bogus test!!!')
|
||||
|
||||
|
||||
class CommandTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.output_stream = StringIO()
|
||||
self.command = commands.Command(
|
||||
subcommands={'sample':SampleCommand},
|
||||
output_stream=self.output_stream)
|
||||
|
||||
def tearDown(self):
|
||||
self.output_stream.close()
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual(repr(self.command), '<Command: rattail>')
|
||||
|
||||
def test_unicode(self):
|
||||
self.assertEqual(unicode(self.command), u'rattail')
|
||||
|
||||
def test_iter_subcommands(self):
|
||||
subcommands = list(self.command.iter_subcommands())
|
||||
self.assertEqual(len(subcommands), 1)
|
||||
self.assertEqual(subcommands[0].name, 'sample')
|
||||
|
||||
def check_help_text(self):
|
||||
text = self.output_stream.getvalue()
|
||||
self.assertRegexpMatches(text, r'^Pythonic Retail Software Framework')
|
||||
self.assertRegexpMatches(text, r'Usage: rattail \[options\] <command> \[command-options\]')
|
||||
self.assertRegexpMatches(text, r'sample *Just for testing\.\.\.')
|
||||
self.assertRegexpMatches(text, r'Try \'rattail help <command>\' for more help.')
|
||||
|
||||
def test_print_help(self):
|
||||
self.assertEqual(self.output_stream.getvalue(), '')
|
||||
self.command.print_help()
|
||||
self.check_help_text()
|
||||
|
||||
def test_run_empty(self):
|
||||
self.assertEqual(self.output_stream.getvalue(), '')
|
||||
self.command.run()
|
||||
self.check_help_text()
|
||||
|
||||
def test_run_help(self):
|
||||
self.assertEqual(self.output_stream.getvalue(), '')
|
||||
self.command.run('help')
|
||||
self.check_help_text()
|
||||
|
||||
def test_run_bogus(self):
|
||||
self.assertEqual(self.output_stream.getvalue(), '')
|
||||
self.command.run('bogus')
|
||||
self.check_help_text()
|
||||
|
||||
def test_run_help_bogus(self):
|
||||
self.assertEqual(self.output_stream.getvalue(), '')
|
||||
self.command.run('help', 'bogus')
|
||||
self.check_help_text()
|
||||
|
||||
def test_run_help_sample(self):
|
||||
self.assertEqual(self.output_stream.getvalue(), '')
|
||||
self.command.run('help', 'sample')
|
||||
self.assertEqual(self.output_stream.getvalue(), 'bogus help!!!')
|
||||
|
||||
def test_run_sample_no_init(self):
|
||||
self.assertEqual(self.output_stream.getvalue(), '')
|
||||
self.command.run('sample', '--no-init')
|
||||
self.assertEqual(self.output_stream.getvalue(), 'bogus test!!!')
|
||||
|
||||
def test_run_sample_with_init(self):
|
||||
self.assertEqual(initialization.inited, [])
|
||||
self.assertEqual(self.output_stream.getvalue(), '')
|
||||
config_path = resource_path('rattail.tests:config/base.conf')
|
||||
self.command.run('--config', config_path, 'sample')
|
||||
self.assertEqual(self.output_stream.getvalue(), 'bogus test!!!')
|
||||
self.assertEqual(initialization.inited, ['rattail'])
|
||||
initialization.inited.remove('rattail')
|
||||
del rattail.config
|
||||
|
||||
|
||||
class SubcommandTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.output_stream = StringIO()
|
||||
self.subcommand = commands.Subcommand(
|
||||
name='bogus',
|
||||
output_stream=self.output_stream)
|
||||
|
||||
def tearDown(self):
|
||||
self.output_stream.close()
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual(repr(self.subcommand), '<Subcommand: bogus>')
|
||||
|
||||
def test_print_help(self):
|
||||
self.assertEqual(self.output_stream.getvalue(), '')
|
||||
self.subcommand.print_help()
|
||||
self.assertRegexpMatches(self.output_stream.getvalue(), 'bogus')
|
||||
|
||||
def test_run(self):
|
||||
self.assertRaises(NotImplementedError, self.subcommand.run, [])
|
||||
|
||||
|
||||
class MainTest(unittest.TestCase):
|
||||
|
||||
def test_main(self):
|
||||
stream = StringIO()
|
||||
commands.main('help', output_stream=stream)
|
||||
text = stream.getvalue()
|
||||
stream.close()
|
||||
self.assertRegexpMatches(text, r'^Pythonic Retail Software Framework')
|
||||
self.assertRegexpMatches(text, r'Usage: rattail \[options\] <command> \[command-options\]')
|
||||
self.assertRegexpMatches(text, r'Try \'rattail help <command>\' for more help.')
|
||||
|
||||
|
||||
class DatabaseSyncCommandTest(unittest.TestCase):
|
||||
|
||||
def test_basics(self):
|
||||
dbsync = commands.DatabaseSyncCommand()
|
||||
self.assertEqual(dbsync.name, 'dbsync')
|
||||
parser = argparse.ArgumentParser()
|
||||
dbsync.add_parser_args(parser)
|
||||
|
||||
|
||||
class FileMonitorCommandTest(unittest.TestCase):
|
||||
|
||||
def test_basics(self):
|
||||
filemon = commands.FileMonitorCommand()
|
||||
self.assertEqual(filemon.name, 'filemon')
|
||||
parser = argparse.ArgumentParser()
|
||||
filemon.add_parser_args(parser)
|
||||
|
||||
|
||||
class LoadHostDataCommandTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
rattail.config = get_config()
|
||||
initialization.inited.append('rattail.db')
|
||||
self.stream = StringIO()
|
||||
|
||||
def tearDown(self):
|
||||
self.stream.close()
|
||||
initialization.inited.remove('rattail.db')
|
||||
del rattail.config
|
||||
|
||||
def test_basics(self):
|
||||
load = commands.LoadHostDataCommand()
|
||||
self.assertEqual(load.name, 'load-host-data')
|
||||
|
||||
def test_run_no_engine(self):
|
||||
load = commands.LoadHostDataCommand(output_stream=self.stream)
|
||||
self.assertEqual(self.stream.getvalue(), '')
|
||||
load.run([])
|
||||
self.assertEqual(self.stream.getvalue(), 'Host engine URL not configured.\n')
|
||||
|
||||
def test_run_with_engine(self):
|
||||
host_path = temp_path('.sqlite')
|
||||
store_path = temp_path('.sqlite')
|
||||
|
||||
db.engines = {
|
||||
'host': create_engine('sqlite:///%s' % host_path.replace('\\', '/')),
|
||||
'store': create_engine('sqlite:///%s' % store_path.replace('\\', '/')),
|
||||
}
|
||||
install_core_schema(db.engines['host'])
|
||||
install_core_schema(db.engines['store'])
|
||||
db.engine = db.engines['store']
|
||||
db.Session.configure(bind=db.engine)
|
||||
|
||||
load = commands.LoadHostDataCommand(output_stream=self.stream)
|
||||
load.run([])
|
||||
|
||||
db.Session.configure(bind=None)
|
||||
db.engine = None
|
||||
db.engines.clear()
|
||||
|
||||
os.remove(host_path)
|
||||
os.remove(store_path)
|
||||
|
||||
|
||||
class UUIDCommandTest(unittest.TestCase):
|
||||
|
||||
def test_run(self):
|
||||
stream = StringIO()
|
||||
self.assertEqual(stream.getvalue(), '')
|
||||
uuid = commands.UUIDCommand(output_stream=stream)
|
||||
uuid.run([])
|
||||
self.assertRegexpMatches(stream.getvalue(), r'^[0-9a-f]{32}$')
|
||||
stream.close()
|
||||
322
rattail/tests/test_configuration.py
Normal file
322
rattail/tests/test_configuration.py
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail.tests import get_config
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from rattail import configuration
|
||||
from rattail.files import resource_path, temp_path
|
||||
from rattail.exceptions import MissingConfiguration
|
||||
|
||||
|
||||
class RattailConfigParserTest(unittest.TestCase):
|
||||
|
||||
def test_get(self):
|
||||
config = get_config()
|
||||
self.assertTrue(config.read(resource_path('rattail.tests:config/db.conf')))
|
||||
self.assertEqual(config.get('rattail.db', 'default.url'), 'sqlite://')
|
||||
|
||||
def test_get_default(self):
|
||||
config = get_config()
|
||||
self.assertEqual(config.get('test.section', 'test.option'), None)
|
||||
self.assertEqual(config.get('test.section', 'test.option', default='test.value'), 'test.value')
|
||||
|
||||
def test_get_boolean(self):
|
||||
config = get_config()
|
||||
config.set('something', 'else', '1')
|
||||
self.assertEqual(config.getboolean('something', 'else'), True)
|
||||
config.set('something', 'else', '0')
|
||||
self.assertEqual(config.getboolean('something', 'else'), False)
|
||||
|
||||
def test_get_boolean_default(self):
|
||||
config = get_config()
|
||||
self.assertEqual(config.getboolean('something', 'else'), None)
|
||||
self.assertEqual(config.getboolean('something', 'else', default=True), True)
|
||||
|
||||
def test_set(self):
|
||||
config = get_config()
|
||||
self.assertFalse(config.has_section('test.section'))
|
||||
self.assertEqual(config.get('test.section', 'test.option'), None)
|
||||
config.set('test.section', 'test.option', 'test.value')
|
||||
self.assertEqual(config.get('test.section', 'test.option'), 'test.value')
|
||||
|
||||
def test_get_user_dir(self):
|
||||
config = get_config()
|
||||
path = config.get_user_dir()
|
||||
self.assertRegexpMatches(path, r'^.*rattail$')
|
||||
|
||||
def test_get_user_dir_with_create(self):
|
||||
config = get_config()
|
||||
path = config.get_user_dir(last_segment='rattail_test_bogus')
|
||||
self.assertFalse(os.path.exists(path))
|
||||
path = config.get_user_dir(last_segment='rattail_test_bogus', create=True)
|
||||
self.assertTrue(os.path.exists(path))
|
||||
self.assertTrue(os.path.isdir(path))
|
||||
os.rmdir(path)
|
||||
|
||||
def test_get_user_file(self):
|
||||
config = get_config()
|
||||
path = config.get_user_file('rattail_test_bogus.conf')
|
||||
self.assertRegexpMatches(path, r'^.*rattail_test_bogus\.conf$')
|
||||
self.assertGreater(len(path), len('rattail_test_bogus.conf'))
|
||||
|
||||
def test_options(self):
|
||||
config = get_config()
|
||||
self.assertEqual(config.options('bogus'), [])
|
||||
config.set('bogus', 'some', 'value')
|
||||
self.assertEqual(config.options('bogus'), ['some'])
|
||||
|
||||
def test_read_path_only_once(self):
|
||||
config = get_config()
|
||||
self.assertEqual(len(config.paths_attempted), 0)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
path = resource_path('rattail.tests:config/base.conf')
|
||||
config.read_path(path)
|
||||
self.assertEqual(len(config.paths_attempted), 1)
|
||||
self.assertEqual(config.paths_attempted[0], path)
|
||||
self.assertEqual(len(config.paths_loaded), 1)
|
||||
self.assertEqual(config.paths_loaded[0], path)
|
||||
config.read_path(path)
|
||||
self.assertEqual(len(config.paths_attempted), 1)
|
||||
self.assertEqual(config.paths_attempted[0], path)
|
||||
self.assertEqual(len(config.paths_loaded), 1)
|
||||
self.assertEqual(config.paths_loaded[0], path)
|
||||
|
||||
def test_read_path_nonexistent(self):
|
||||
config = get_config()
|
||||
self.assertEqual(len(config.paths_attempted), 0)
|
||||
path = temp_path('.conf')
|
||||
config.read_path(path)
|
||||
self.assertEqual(len(config.paths_attempted), 1)
|
||||
self.assertEqual(config.paths_attempted[0], path)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
|
||||
def test_read_path_dir(self):
|
||||
config = get_config()
|
||||
self.assertEqual(len(config.paths_attempted), 0)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
path = temp_path('.conf-dir')
|
||||
os.mkdir(path)
|
||||
config.read_path(path)
|
||||
self.assertEqual(len(config.paths_attempted), 1)
|
||||
self.assertEqual(config.paths_attempted[0], path)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
os.rmdir(path)
|
||||
|
||||
def test_read_path_recursing(self):
|
||||
config = get_config()
|
||||
self.assertEqual(len(config.paths_attempted), 0)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
local_path = resource_path('rattail.tests:config/local.conf')
|
||||
base_path = resource_path('rattail.tests:config/base.conf')
|
||||
config.read_path(local_path, recurse=True)
|
||||
self.assertEqual(len(config.paths_attempted), 2)
|
||||
self.assertEqual(config.paths_attempted[0], local_path)
|
||||
self.assertEqual(config.paths_attempted[1], base_path)
|
||||
self.assertEqual(len(config.paths_loaded), 2)
|
||||
self.assertEqual(config.paths_loaded[0], base_path)
|
||||
self.assertEqual(config.paths_loaded[1], local_path)
|
||||
self.assertEqual(config.get('rattail', 'some_option'), 'some_value')
|
||||
|
||||
def test_read_path_not_recursing(self):
|
||||
config = get_config()
|
||||
self.assertEqual(len(config.paths_attempted), 0)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
local_path = resource_path('rattail.tests:config/local.conf')
|
||||
config.read_path(local_path, recurse=False)
|
||||
self.assertEqual(len(config.paths_attempted), 1)
|
||||
self.assertEqual(config.paths_attempted[0], local_path)
|
||||
self.assertEqual(len(config.paths_loaded), 1)
|
||||
self.assertEqual(config.paths_loaded[0], local_path)
|
||||
self.assertIsNone(config.get('rattail', 'some_option'))
|
||||
|
||||
def test_read_only_once(self):
|
||||
config = get_config()
|
||||
self.assertEqual(len(config.paths_attempted), 0)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
path = resource_path('rattail.tests:config/base.conf')
|
||||
config.read(path)
|
||||
self.assertEqual(len(config.paths_attempted), 1)
|
||||
self.assertEqual(config.paths_attempted[0], path)
|
||||
self.assertEqual(len(config.paths_loaded), 1)
|
||||
self.assertEqual(config.paths_loaded[0], path)
|
||||
config.read(path)
|
||||
self.assertEqual(len(config.paths_attempted), 1)
|
||||
self.assertEqual(config.paths_attempted[0], path)
|
||||
self.assertEqual(len(config.paths_loaded), 1)
|
||||
self.assertEqual(config.paths_loaded[0], path)
|
||||
|
||||
def test_read_nonexistent(self):
|
||||
config = get_config()
|
||||
self.assertEqual(len(config.paths_attempted), 0)
|
||||
path = temp_path('.conf')
|
||||
config.read(path)
|
||||
self.assertEqual(len(config.paths_attempted), 1)
|
||||
self.assertEqual(config.paths_attempted[0], path)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
|
||||
def test_read_dir(self):
|
||||
config = get_config()
|
||||
self.assertEqual(len(config.paths_attempted), 0)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
path = temp_path('.conf-dir')
|
||||
os.mkdir(path)
|
||||
config.read(path)
|
||||
self.assertEqual(len(config.paths_attempted), 1)
|
||||
self.assertEqual(config.paths_attempted[0], path)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
os.rmdir(path)
|
||||
|
||||
def test_read_recursing(self):
|
||||
config = get_config()
|
||||
self.assertEqual(len(config.paths_attempted), 0)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
local_path = resource_path('rattail.tests:config/local.conf')
|
||||
base_path = resource_path('rattail.tests:config/base.conf')
|
||||
config.read(local_path, recurse=True)
|
||||
self.assertEqual(len(config.paths_attempted), 2)
|
||||
self.assertEqual(config.paths_attempted[0], local_path)
|
||||
self.assertEqual(config.paths_attempted[1], base_path)
|
||||
self.assertEqual(len(config.paths_loaded), 2)
|
||||
self.assertEqual(config.paths_loaded[0], base_path)
|
||||
self.assertEqual(config.paths_loaded[1], local_path)
|
||||
self.assertEqual(config.get('rattail', 'some_option'), 'some_value')
|
||||
|
||||
def test_read_not_recursing(self):
|
||||
config = get_config()
|
||||
self.assertEqual(len(config.paths_attempted), 0)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
local_path = resource_path('rattail.tests:config/local.conf')
|
||||
config.read(local_path, recurse=False)
|
||||
self.assertEqual(len(config.paths_attempted), 1)
|
||||
self.assertEqual(config.paths_attempted[0], local_path)
|
||||
self.assertEqual(len(config.paths_loaded), 1)
|
||||
self.assertEqual(config.paths_loaded[0], local_path)
|
||||
self.assertIsNone(config.get('rattail', 'some_option'))
|
||||
|
||||
def test_read_service_with_override(self):
|
||||
config = get_config()
|
||||
self.assertEqual(len(config.paths_attempted), 0)
|
||||
self.assertEqual(len(config.paths_loaded), 0)
|
||||
base_path = resource_path('rattail.tests:config/base.conf')
|
||||
service_path = resource_path('rattail.tests:config/service.conf')
|
||||
config.read_service('RattailFileMonitor', base_path)
|
||||
self.assertEqual(len(config.paths_attempted), 1)
|
||||
self.assertEqual(config.paths_attempted[0], service_path)
|
||||
self.assertEqual(len(config.paths_loaded), 1)
|
||||
self.assertEqual(config.paths_loaded[0], service_path)
|
||||
self.assertEqual(config.get('rattail', 'hells'), 'yeah')
|
||||
|
||||
def test_require(self):
|
||||
config = get_config()
|
||||
self.assertRaises(MissingConfiguration, config.require, 'section', 'option')
|
||||
config.set('section', 'option', 'value')
|
||||
self.assertEqual(config.require('section', 'option'), 'value')
|
||||
|
||||
def test_save(self):
|
||||
path = temp_path('.conf')
|
||||
config = get_config()
|
||||
config.set('something', 'else', 'bogus')
|
||||
config.save(path)
|
||||
config = get_config()
|
||||
self.assertEqual(config.get('something', 'else'), None)
|
||||
config.read(path)
|
||||
self.assertEqual(config.get('something', 'else'), 'bogus')
|
||||
os.remove(path)
|
||||
|
||||
def test_configure_logging_basic(self):
|
||||
root = logging.getLogger()
|
||||
before = len(root.handlers)
|
||||
config = get_config()
|
||||
config.set('rattail', 'basic_logging', '1')
|
||||
config.configure_logging()
|
||||
self.assertEqual(len(root.handlers), before + 1)
|
||||
handler = root.handlers[-1]
|
||||
self.assertEqual(
|
||||
handler.formatter._fmt,
|
||||
'%(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s')
|
||||
root.handlers.remove(handler)
|
||||
self.assertEqual(len(root.handlers), before)
|
||||
|
||||
|
||||
class StorageTest(unittest.TestCase):
|
||||
|
||||
def test_get(self):
|
||||
storage = configuration.Storage()
|
||||
self.assertRaises(NotImplementedError, storage.get, 'section', 'option')
|
||||
|
||||
def test_put(self):
|
||||
storage = configuration.Storage()
|
||||
self.assertRaises(NotImplementedError, storage.put, 'section', 'option', 'value')
|
||||
|
||||
|
||||
class UserConfigFileStorageTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.config_path = temp_path('.conf')
|
||||
config_file = open(self.config_path, 'w')
|
||||
config_file.write('[test.section]\n')
|
||||
config_file.write('test.option = test.value\n')
|
||||
config_file.close()
|
||||
|
||||
def tearDown(self):
|
||||
os.remove(self.config_path)
|
||||
|
||||
def get_storage(self, config_path=None):
|
||||
return configuration.UserConfigFileStorage(config_path, testing=True)
|
||||
|
||||
def test_construct(self):
|
||||
storage = self.get_storage()
|
||||
self.assertRegexpMatches(storage.config_path, r'\brattail\.conf$')
|
||||
|
||||
storage = self.get_storage(self.config_path)
|
||||
self.assertEqual(storage.config_path, self.config_path)
|
||||
|
||||
def test_get(self):
|
||||
storage = self.get_storage(self.config_path)
|
||||
self.assertEqual(storage.get('test.section', 'test.option'), 'test.value')
|
||||
self.assertEqual(storage.get('test.section', 'test.bogus'), None)
|
||||
self.assertEqual(storage.get('test.section', 'test.bogus', 'default.value'), 'default.value')
|
||||
|
||||
def test_put(self):
|
||||
storage = self.get_storage(self.config_path)
|
||||
storage.put('new_section', 'new_option', 'new_value')
|
||||
|
||||
storage = self.get_storage(self.config_path)
|
||||
self.assertEqual(storage.get('new_section', 'new_option'), 'new_value')
|
||||
|
||||
|
||||
class FunctionsTest(unittest.TestCase):
|
||||
|
||||
def test_basic_logging(self):
|
||||
root = logging.getLogger()
|
||||
before = len(root.handlers)
|
||||
configuration.basic_logging()
|
||||
self.assertEqual(len(root.handlers), before + 1)
|
||||
handler = root.handlers[-1]
|
||||
self.assertEqual(
|
||||
handler.formatter._fmt,
|
||||
'%(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s')
|
||||
root.handlers.remove(handler)
|
||||
self.assertEqual(len(root.handlers), before)
|
||||
|
||||
def test_default_system_paths(self):
|
||||
if sys.platform == 'win32':
|
||||
expected = 2
|
||||
else:
|
||||
expected = 4
|
||||
paths = configuration.default_system_paths()
|
||||
self.assertEqual(len(paths), expected)
|
||||
self.assertTrue('rattail' in paths[0])
|
||||
self.assertTrue('rattail' in paths[1])
|
||||
|
||||
def test_default_user_paths(self):
|
||||
paths = configuration.default_user_paths()
|
||||
self.assertEqual(len(paths), 2)
|
||||
self.assertTrue('rattail' in paths[0])
|
||||
self.assertTrue('rattail' in paths[1])
|
||||
76
rattail/tests/test_exceptions.py
Normal file
76
rattail/tests/test_exceptions.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import unittest
|
||||
|
||||
from rattail import exceptions
|
||||
from rattail.db.model import Batch
|
||||
|
||||
|
||||
class MissingConfigTest(unittest.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
error = exceptions.MissingConfiguration('test.section', 'test.option')
|
||||
self.assertEqual(
|
||||
str(error),
|
||||
"Configuration is missing a required option. Please define "
|
||||
"'test.option' within the [test.section] section of your "
|
||||
"configuration file.")
|
||||
|
||||
|
||||
class ModuleHasNoInitTest(unittest.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
error = exceptions.ModuleHasNoInit(unittest)
|
||||
self.assertEqual(str(error), "Module has no init() function: unittest")
|
||||
|
||||
|
||||
class NoDefaultDatabaseTest(unittest.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
error = exceptions.NoDefaultDatabase()
|
||||
self.assertEqual(str(error), "A default database engine could not be determined from the configuration file(s).")
|
||||
|
||||
|
||||
class SILColumnNotFoundTest(unittest.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
error = exceptions.SILColumnNotFound('FXXX')
|
||||
self.assertEqual(str(error), "SIL column not found: FXXX")
|
||||
|
||||
|
||||
class SILInvalidDataType(unittest.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
error = exceptions.SILInvalidDataType('BOGUS(20)')
|
||||
self.assertEqual(str(error), "Invalid SIL data type: BOGUS(20)")
|
||||
|
||||
|
||||
class BatchTypeNotFound(unittest.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
error = exceptions.BatchTypeNotFound('bogus')
|
||||
self.assertEqual(str(error), "Batch type not found: bogus")
|
||||
|
||||
|
||||
class BatchNotSavedTest(unittest.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
error = exceptions.BatchNotSaved()
|
||||
self.assertEqual(str(error), "Batch is pending; you must flush it to the database before the operation you requested may proceed.")
|
||||
|
||||
|
||||
class BatchProviderNotFoundTest(unittest.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
error = exceptions.BatchProviderNotFound('bogus')
|
||||
self.assertEqual(str(error), "Batch provider not found: bogus")
|
||||
|
||||
|
||||
class BatchDestinationNotSupported(unittest.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
batch = Batch()
|
||||
batch.destination = 'BOGUS'
|
||||
batch.description = 'Test Batch'
|
||||
error = exceptions.BatchDestinationNotSupported(batch)
|
||||
self.assertEqual(str(error), "Destination 'BOGUS' not supported for batch: Test Batch")
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue