Revision: 5
Author:
Joshua.R...@gmail.com
Date: Sat Apr 6 14:46:09 2013
Log: Getting closer to an alpha release of the core module
http://code.google.com/p/pyxmldb/source/detail?r=5
Added:
/trunk/doc/conf.py
/trunk/doc/filtering.rst
/trunk/doc/index.rst
/trunk/doc/manager.rst
/trunk/doc/modules.rst
/trunk/doc/searching.rst
/trunk/doc/tutorial.rst
/trunk/examples
/trunk/examples/attributesearching
/trunk/examples/attributesearching/attributesearch.py
/trunk/examples/filtering02
/trunk/examples/filtering02/filtering.py
/trunk/examples/tutorial00
/trunk/examples/tutorial00/options.cfg
/trunk/examples/tutorial00/tutorial00.py
/trunk/examples/tutorial01
/trunk/examples/tutorial01/Data
/trunk/examples/tutorial01/options.cfg
/trunk/examples/tutorial01/tutorial01.py
/trunk/examples/tutorial02
/trunk/examples/tutorial02/options.cfg
/trunk/examples/tutorial02/tutorial02.py
Modified:
/trunk/setup.py
/trunk/xmldb/__init__.py
/trunk/xmldb/manager.py
/trunk/xmldb/options.py
=======================================
--- /dev/null
+++ /trunk/doc/conf.py Sat Apr 6 14:46:09 2013
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+#
+# xmldb documentation build configuration file, created by
+# sphinx-quickstart on Fri Mar 22 20:05:58 2013.
+#
+# 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
+
+# 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.coverage']
+
+# 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'xmldb'
+copyright = u'2013, Josh English'
+
+# 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.5.0'
+# The full version, including alpha/beta/rc tags.
+release = '0.5.0'
+
+# 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 = 'haiku'
+
+# 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 = 'xmldbdoc'
+
+
+# -- 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', 'xmldb.tex', u'xmldb Documentation',
+ u'Josh English', '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', 'xmldb', u'xmldb Documentation',
+ [u'Josh English'], 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', 'xmldb', u'xmldb Documentation',
+ u'Josh English', 'xmldb', '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'
=======================================
--- /dev/null
+++ /trunk/doc/filtering.rst Sat Apr 6 14:46:09 2013
@@ -0,0 +1,52 @@
+Filtering Databases
+====================
+
+Because of the flexibility of XML-Data, xmldb requires a minilanguage to
+perform simple searches. The DBManager offers a :meth:`DBManager.find`
method
+that takes one or more filter functions, applies them to every node in the
+database, and returns a generator of matching results.
+
+Each filter function should accept an Element object and return a boolean
+value.
+
+
+.. literalinclude:: /../examples/attributesearching/attributesearch.py
+
+
+Running this file works, but it is limited.
+
+.. code-block:: python
+
+ >>>
+ List all contacts
+ 1 Joe False
+ 2 Mary True
+
+ Finding Friends with is_friend
+ 1 Mary
+ >>>
+
+It can match attributes, but the attributes must match perfectly. A more
+generic, yet still not robust tool is the :meth:`make_filter_func`.
+
+.. literalinclude:: /../examples/filtering02/filtering.py
+
+Running this file matches both contact records.
+
+.. code-block:: python
+
+ >>>
+ List all contacts
+ 1 Joe False
+ 2 Mary True
+ 3 Jake True
+
+ Finding Friends with is_friend
+ 1 Mary
+ 2 Jake
+ >>>
+
+This method works, with limitations. The preferred method of filtering the
+database is to create your own filter functions. One of the examples will
+demontstrate how to use the argparse module to define commands for
interacting
+with the database.
=======================================
--- /dev/null
+++ /trunk/doc/index.rst Sat Apr 6 14:46:09 2013
@@ -0,0 +1,26 @@
+.. xmldb documentation master file, created by
+ sphinx-quickstart on Fri Mar 22 20:05:58 2013.
+ You can adapt this file completely to your liking, but it should at
least
+ contain the root `toctree` directive.
+
+Welcome to xmldb's documentation!
+=================================
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+ tutorial
+ filtering
+ modules
+
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
=======================================
--- /dev/null
+++ /trunk/doc/manager.rst Sat Apr 6 14:46:09 2013
@@ -0,0 +1,24 @@
+DBManager
+=========
+
+.. py:currentmodule:: xmldb
+
+The DBManager provides an interface to an XML file and also provides some
simple
+filtering tools. See some tutorial I haven't yet written about using
argparse
+to create proper filters, or the CMD tutorial.
+
+One database file contains an XML Tree where every child is the same form,
that is, they all have the same tag name. Every node in a database file
will be checked against the same `xmlcheck` object.
+
+.. autoclass:: DBManager
+
+
+.. note::
+ When creating a :class:`DBManager` with a given checker,
+ the following changes will be made to the checker:
+
+#. the checker will be given two `Datetimecheck` attributes
named 'mod_time' and 'cr_time'
+#. The checker will be given two `IntCheck` attributes named 'id'
and 'revision'
+
+If the checker already has any of these, it will remove them and append
its own attribute checkers.
+
+
=======================================
--- /dev/null
+++ /trunk/doc/modules.rst Sat Apr 6 14:46:09 2013
@@ -0,0 +1,10 @@
+Modules
+========
+
+There are two main modules that are useful to import. Other modules are
utility
+modules that the user should not need to import directly into their
programs
+
+.. toctree::
+ :maxdepth: 2
+
+ manager
=======================================
--- /dev/null
+++ /trunk/doc/searching.rst Sat Apr 6 14:46:09 2013
@@ -0,0 +1,15 @@
+Searching Databases
+====================
+
+Because of the flexibility of XML-Data, xmldb requires a minilanguage to
+perform simple searches. The DBManager offers a :meth:`DBManager.find`
method
+that takes one or more filter functions, applies them to every node in the
+database, and returns a generator of matching results.
+
+Each filter function should accept an Element object and return a boolean
+value.
+
+
+.. literalinclude:: /../examples/attributesearching/attributesearch.py
+
+
=======================================
--- /dev/null
+++ /trunk/doc/tutorial.rst Sat Apr 6 14:46:09 2013
@@ -0,0 +1,28 @@
+Tutorial
+========
+
+This file demonstrates how to create a simple database and add a node.
+
+The pattern here is will be repeated throughout all the tutorials::
+
+#. Define the checker using `xcheck`
+#. Create a manager with `DBManager`
+
+.. literalinclude:: /../examples/tutorial00/tutorial00.py
+
+
+The DBManager.save_node method will overwrite existing nodes if the ID
matches.
+
+With a few changes, you can add an xmlcheck.Wrap object to the database and
+return wrapped objects from the `get_nodes` method.
+
+.. literalinclude:: /../examples/tutorial01/tutorial01.py
+
+The DBManager.save_node method will overwrite existing nodes if the ID
matches.
+This behavior can be changed by settings
+`XMLDBOptions.prevent_update_on_save` to `True`
+
+.. literalinclude:: /../examples/tutorial02/tutoral02.py
+
+
+
=======================================
--- /dev/null
+++ /trunk/examples/attributesearching/attributesearch.py Sat Apr 6
14:46:09 2013
@@ -0,0 +1,82 @@
+""""attributesearch.py
+
+This tutorial demonstrates one simple way of searching the database.
+
+"""
+
+import xmldb
+import xcheck
+
+contact_checker = xcheck.load_checker("""<xcheck name="contact">
+<attributes>
+ <int name="id" required="yes" />
+ <bool name="friend" required="yes" />
+</attributes>
+<children>
+ <text name="name" min_length="1" />
+ <email name="email">
+ <attributes>
+ <selection name="type" values="home, work" />
+ </attributes>
+ </email>
+</children>
+</xcheck>
+""")
+
+class Contact(xcheck.Wrap):
+ def __init__(self, node=None):
+ xcheck.Wrap.__init__(self, contact_checker, node)
+
+ @property
+ def name(self):
+ return self._get_elem_value('name')
+
+ @name.setter
+ def name(self, value):
+ return self._set_elem_value('name', value)
+
+ @property
+ def friend(self):
+ return self._get_att('friend')
+
+my_options = xmldb.DBOptions()
+my_options.prevent_update_on_save = True
+
+Contacts = xmldb.DBManager(contact_checker, my_options)
+Contacts.register_wrapper(Contact)
+
+joe = """<contact id="1" friend="no">
+<name>Joe</name>
+<email type="home">
j...@example.com</email>
+</contact>"""
+
+mary = """<contact id="2" friend="yes">
+<name>Mary</name>
+<email type="work">
ma...@example.org</email>
+</contact>"""
+
+jake = """<contact id="3" friend="True">
+<name>Jake</name>
+<email type="work">
ja...@slavewage.com</email>
+</contact>"""
+
+try:
+ Contacts.save_node(joe)
+ Contacts.save_node(mary)
+ Contacts.save_node(jake)
+except xmldb.DBError:
+ pass
+
+print "List all contacts"
+for idx, contact in enumerate(Contacts.get_nodes(True),1):
+ print idx,
contact.name, contact.friend
+
+is_friend = Contacts.make_attribute_filter(friend='yes')
+print
+
+print "Finding Friends with is_friend"
+for idx, contact in enumerate(Contacts.find(is_friend), 1):
+ print idx, contact.findtext('name')
+
+# close() saves the database
+Contacts.close()
=======================================
--- /dev/null
+++ /trunk/examples/filtering02/filtering.py Sat Apr 6 14:46:09 2013
@@ -0,0 +1,82 @@
+""""attributesearch.py
+
+This tutorial demonstrates one simple way of searching the database.
+
+"""
+
+import xmldb
+import xcheck
+
+contact_checker = xcheck.load_checker("""<xcheck name="contact">
+<attributes>
+ <int name="id" required="yes" />
+ <bool name="friend" required="yes" />
+</attributes>
+<children>
+ <text name="name" min_length="1" />
+ <email name="email">
+ <attributes>
+ <selection name="type" values="home, work" />
+ </attributes>
+ </email>
+</children>
+</xcheck>
+""")
+
+class Contact(xcheck.Wrap):
+ def __init__(self, node=None):
+ xcheck.Wrap.__init__(self, contact_checker, node)
+
+ @property
+ def name(self):
+ return self._get_elem_value('name')
+
+ @name.setter
+ def name(self, value):
+ return self._set_elem_value('name', value)
+
+ @property
+ def friend(self):
+ return self._get_att('friend')
+
+my_options = xmldb.DBOptions()
+my_options.prevent_update_on_save = True
+
+Contacts = xmldb.DBManager(contact_checker, my_options)
+Contacts.register_wrapper(Contact)
+
+joe = """<contact id="1" friend="no">
+<name>Joe</name>
+<email type="home">
j...@example.com</email>
+</contact>"""
+
+mary = """<contact id="2" friend="yes">
+<name>Mary</name>
+<email type="work">
ma...@example.org</email>
+</contact>"""
+
+jake = """<contact id="3" friend="True">
+<name>Jake</name>
+<email type="work">
ja...@slavewage.com</email>
+</contact>"""
+
+try:
+ Contacts.save_node(joe)
+ Contacts.save_node(mary)
+ Contacts.save_node(jake)
+except xmldb.DBError:
+ pass
+
+print "List all contacts"
+for idx, contact in enumerate(Contacts.get_nodes(True),1):
+ print idx,
contact.name, contact.friend
+
+is_friend = Contacts.make_filter_func('friend eq yes')
+print
+
+print "Finding Friends with is_friend"
+for idx, contact in enumerate(Contacts.find(is_friend), 1):
+ print idx, contact.findtext('name')
+
+# close() saves the database
+Contacts.close()
=======================================
--- /dev/null
+++ /trunk/examples/tutorial00/options.cfg Sat Apr 6 14:46:09 2013
@@ -0,0 +1,9 @@
+[filepaths]
+use_folder = True
+dir = Data
+contact = contact.xml
+
+[xmldb]
+update_time = 20
+save_db_on_node_change = True
+
=======================================
--- /dev/null
+++ /trunk/examples/tutorial00/tutorial00.py Sat Apr 6 14:46:09 2013
@@ -0,0 +1,39 @@
+""""tutorial00.py
+
+This tutorial demonstrates setting up a simple xmldb manager using
+an xcheck-definition.
+"""
+
+import xmldb
+import xcheck
+
+contact_checker = xcheck.load_checker("""<xcheck name="contact">
+<attributes>
+ <int name="id" required="yes" />
+</attributes>
+<children>
+ <text name="name" min_length="1" />
+ <email name="email">
+ <attributes>
+ <selection name="type" values="home, work" />
+ </attributes>
+ </email>
+</children>
+</xcheck>
+""")
+
+Contacts = xmldb.DBManager(contact_checker, xmldb.DBOptions)
+
+joe = """<contact id="1">
+<name>Joe</name>
+<email type="home">
j...@example.com</email>
+</contact>"""
+
+# save_node() overwrites the previous file
+Contacts.save_node(joe)
+
+for idx, contact in enumerate(Contacts.get_nodes(),1):
+ print idx, contact, contact.get('id')
+
+# close() saves the database
+Contacts.close()
=======================================
--- /dev/null
+++ /trunk/examples/tutorial01/options.cfg Sat Apr 6 14:46:09 2013
@@ -0,0 +1,10 @@
+[filepaths]
+use_folder = True
+dir = Data
+contact = contact.xml
+
+[xmldb]
+update_time = 20
+save_db_on_node_change = True
+prevent_update_on_save = False
+
=======================================
--- /dev/null
+++ /trunk/examples/tutorial01/tutorial01.py Sat Apr 6 14:46:09 2013
@@ -0,0 +1,52 @@
+""""tutorial01.py
+
+This tutorial demonstrates setting up a simple xmldb manager using
+an xcheck-definition and returning objects based on xcheck.Wrap
+"""
+
+import xmldb
+import xcheck
+
+contact_checker = xcheck.load_checker("""<xcheck name="contact">
+<attributes>
+ <int name="id" required="yes" />
+</attributes>
+<children>
+ <text name="name" min_length="1" />
+ <email name="email">
+ <attributes>
+ <selection name="type" values="home, work" />
+ </attributes>
+ </email>
+</children>
+</xcheck>
+""")
+
+class Contact(xcheck.Wrap):
+ def __init__(self, node=None):
+ xcheck.Wrap.__init__(self, contact_checker, node)
+
+ @property
+ def name(self):
+ return self._get_elem_value('name')
+
+ @name.setter
+ def name(self, value):
+ return self._set_elem_value('name', value)
+
+Contacts = xmldb.DBManager(contact_checker, xmldb.DBOptions)
+Contacts.register_wrapper(Contact)
+
+joe = """<contact id="1">
+<name>Joe</name>
+<email type="home">
j...@example.com</email>
+</contact>"""
+
+# save_node() overwrites the previous file
+Contacts.save_node(joe)
+
+for idx, contact in enumerate(Contacts.get_nodes(True),1):
+ print idx, contact,
contact.name
+
+# close() saves the database
+Contacts.close()
=======================================
--- /dev/null
+++ /trunk/examples/tutorial02/options.cfg Sat Apr 6 14:46:09 2013
@@ -0,0 +1,10 @@
+[filepaths]
+use_folder = True
+dir = Data
+contact = contact.xml
+
+[xmldb]
+update_time = 20
+save_db_on_node_change = True
+prevent_update_on_save = True
+
=======================================
--- /dev/null
+++ /trunk/examples/tutorial02/tutorial02.py Sat Apr 6 14:46:09 2013
@@ -0,0 +1,60 @@
+""""tutorial02.py
+
+This tutorial demonstrates preventing the save_node method from overwriting
+nodes
+"""
+
+import xmldb
+import xcheck
+
+contact_checker = xcheck.load_checker("""<xcheck name="contact">
+<attributes>
+ <int name="id" required="yes" />
+</attributes>
+<children>
+ <text name="name" min_length="1" />
+ <email name="email">
+ <attributes>
+ <selection name="type" values="home, work" />
+ </attributes>
+ </email>
+</children>
+</xcheck>
+""")
+
+class Contact(xcheck.Wrap):
+ def __init__(self, node=None):
+ xcheck.Wrap.__init__(self, contact_checker, node)
+
+ @property
+ def name(self):
+ return self._get_elem_value('name')
+
+ @name.setter
+ def name(self, value):
+ return self._set_elem_value('name', value)
+
+my_options = xmldb.DBOptions()
+my_options.prevent_update_on_save = True
+
+Contacts = xmldb.DBManager(contact_checker, my_options)
+Contacts.register_wrapper(Contact)
+
+joe = """<contact id="1">
+<name>Joe</name>
+<email type="home">
j...@example.com</email>
+</contact>"""
+
+# save_node() can't the previous file
+
+try:
+ Contacts.save_node(joe)
+ Contacts.save_node(joe)
+except xmldb.DBError:
+ print "failed to overwrite node"
+
+for idx, contact in enumerate(Contacts.get_nodes(True),1):
+ print idx, contact,
contact.name
+
+# close() saves the database
+Contacts.close()
=======================================
--- /trunk/setup.py Fri Mar 22 10:27:44 2013
+++ /trunk/setup.py Sat Apr 6 14:46:09 2013
@@ -0,0 +1,22 @@
+from distutils.core import setup
+
+setup(name="XMLDB",
+ version="0.5.0",
+ author="Joshua R. English",
+ author_email="
Joshua.R...@gmail.com",
+ url="
https://code.google.com/p/pyxmldb/",
+ packages=['xmldb'],
+ description='xml-data database tools',
+ long_description='''
+ ''',
+ classifiers= [
+ 'Programming Language :: Python',
+ 'Operating System :: OS Independent',
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'Natural Language :: English',
+ 'Topic :: Database',
+ 'Topic :: Text Processing :: Markup :: XML'
+ ],
+ requires = ['xmlcheck (>=0.6.5)',],
+ )
=======================================
--- /trunk/xmldb/__init__.py Wed Mar 27 22:23:17 2013
+++ /trunk/xmldb/__init__.py Sat Apr 6 14:46:09 2013
@@ -17,15 +17,19 @@
import xcheck as XC
-from errors import DBError
import utils
from options import DBOptions
-from manager import DBManager
+from manager import DBManager, DBError
+from manager import UnregisterableWrapperError, UnregisteredWrapperError
-__all__=['DBOptions', 'DBManager', 'DBError', 'XC', 'ET', 'utils']
+__all__=['DBOptions', 'DBManager', 'DBError',
+ 'UnregisterableWrapperError',
+ 'UnregisteredWrapperError',
+
+ 'XC', 'ET', 'utils']
if __name__=='__main__':
=======================================
--- /trunk/xmldb/manager.py Wed Mar 27 22:23:17 2013
+++ /trunk/xmldb/manager.py Sat Apr 6 14:46:09 2013
@@ -6,24 +6,41 @@
import logging
from elementtree import ElementTree as ET
+import difflib
-import xcheck as XC
+
+import operator
+operation_whitelist = [ 'lt', 'le', 'gt', 'ge', 'ne']
+operation_aliases = {'before': 'le', 'after':'ge'}
+
+
+import xcheck
from utils import get_elem, indent
from filelock import FileLock
from options import DBOptions
from errors import DBError
-_modDateCheck = XC.DatetimeCheck('mod_time', required=False,
allow_none=True)
-_createDateCheck = XC.DatetimeCheck('cr_time', required=False,
allow_none=True)
-_revisionCheck = XC.IntCheck('revision', required=False, allow_none=True)
+_modDateCheck = xcheck.DatetimeCheck('mod_time', required=False,
allow_none=True)
+_createDateCheck = xcheck.DatetimeCheck('cr_time', required=False,
allow_none=True)
+_revisionCheck = xcheck.IntCheck('revision', required=False,
allow_none=True)
+_idCheck = xcheck.IntCheck('id', required=True, min=1)
from inspect import isclass
+def compare(conversion, op, a, b):
+ a = conversion(a)
+ b = conversion(b)
+ return op(a,b)
+
+class DBError(Exception): pass
+class UnregisterableWrapperError(DBError): pass
+class UnregisteredWrapperError(DBError): pass
+
class DBManager(object):
"""DBManager(checker, options[, encoding, extrakeys]
- :param checker: xcheck.XCheck class defining the data structure
+ :param checker: xcheckheck.xcheckheck class defining the data structure
:param options: DBOptions class or instance
:param encoding: encoding for the XML (default "us-ascii")
:param extrakeys: list of strings for extra xml attributes for the
root node
@@ -41,6 +58,7 @@
self._nodename=
checker.name
self._checker = checker
+ self._wrap_class = None
if not self._checker.has_attribute('mod_time'):
self._checker.addattribute(_modDateCheck)
@@ -51,9 +69,16 @@
if not self._checker.has_attribute('revision'):
self._checker.addattribute(_revisionCheck)
+ if not self._checker.has_attribute('id'):
+ self._checker.addattribute(_idCheck)
+
+ if not isinstance(self._checker.get('id'), xcheck.IntCheck):
+ self._checker.attributes.pop('id')
+ self._checker.addattribute(_idCheck)
+
if isclass(options) and issubclass(options, DBOptions):
self._options = options()
- elif isinstance(options, xmldbOptions):
+ elif isinstance(options, DBOptions):
self._options = options
self._options.register_database(
checker.name)
@@ -96,7 +121,7 @@
root.set(key, str(self.extra_attributes.get(key, None)))
tree.write(fp, encoding = self._encoding ,xml_declaration=True)
- def initialize_database(self, force=False):
+ def _initialize_database(self, force=False):
"If the file doesn't exist, create a default"
fp = self.get_default_db()
if not os.path.exists(fp) or force:
@@ -113,7 +138,7 @@
while 1:
if not self._alive:break
fp = self.get_default_db()
- self.initialize_database()
+ self._initialize_database()
stat = os.stat(fp)
if self._lastmodtime is None:
self.load_database()
@@ -125,7 +150,12 @@
time.sleep(self._options.update_time)
def load_database(self):
- self.initialize_database()
+ """load_database()
+ Reloads the database from the file into memory. This method should
+ be called only if the auto_load time is very long, or the user
feels
+ it should be forced.
+ """
+ self._initialize_database()
fp = self.get_default_db()
self._tlock.acquire()
with FileLock(fp) as lock:
@@ -146,6 +176,10 @@
self.on_load()
def save_database(self):
+ """save_database()
+ Manually saves the database. This method does not need to be called
+ unless the database's option manager's auto_save flag is set to
False.
+ """
fp = self.get_default_db()
self._tlock.acquire()
with FileLock(fp) as lock:
@@ -153,21 +187,46 @@
for key in self.extra_attributes:
self._tree.getroot().set(key,
str(self.extra_attributes[key]))
indent(self._tree.getroot())
- self._tree.write(fp, encoding = self._encoding,
xml_declaration=True)
+ self._tree.write(fp, encoding = self._encoding,
+ xml_declaration=True)
self._tlock.release()
self.on_save()
def get_default_db(self):
+ """get_default_db()
+ Shortcut of self._options.get_file_path(
self.name)
+ """
return self._options.get_file_path(
self.name)
def set_default_db(self, path):
- """set_default_db
- This sets the new database path in the options
+ """set_default_db()
+ This sets the new database path in the options. Using this method
+ could result in a brand-new, empty database being created.
"""
return self._options.setFilePath(self._
checker.name, path)
- def get_nodes(self):
- return self._tree.findall(self._nodename)
+ def register_wrapper(self, wrapperClass):
+ """register_wrapper(wrapperClass)
+ Assigns the object class to use as an interface to the XML nodes in
+ the database.
+ """
+ if not isclass(wrapperClass):
+ raise UnregisterableWrapperError(
+ 'Must register a class, not an instance')
+ if not issubclass(wrapperClass, xcheck.Wrap):
+ raise UnregisterableWrapperError("Cannot use as wrapper class")
+
+ self._wrap_class = wrapperClass
+
+ def get_nodes(self, as_object=False):
+ if as_object:
+ if self._wrap_class:
+ return (self._wrap_class(node)
+ for node in self._tree.findall(self._nodename))
+ else:
+ raise UnregisteredWrapperError("no registered wrapper
class")
+ else:
+ return self._tree.findall(self._nodename)
def get_node_by_ID(self, ID):
"""GetNode(ID)
@@ -176,8 +235,15 @@
return self._tree.find("%s[@id='%s']" % (self._nodename, ID))
def save_node(self, node):
+ """save_node(node)
+ Saves a node to the database, checking in for conformity with the
+ checker, first.
+ This method can overwrite nodes with the same ID attribute.
+ Will save the database if the options manager's auto_save flag is
True.
+
+ save_node will automatically assign a number to the ID attribute
+ """
node = get_elem(node)
- self._checker(node)
ID = node.get('id', None)
if ID in ['0', None]:
@@ -187,9 +253,12 @@
new_id = max(known_ids) + 1
## self._logger.debug('new_node being assigned %s', idx)
node.set('id', str(idx))
+ self._checker(node)
old_node = self.get_node_by_ID(ID)
if old_node is not None:
+ if self._options.prevent_update_on_save:
+ raise DBError('Cannot overwrite node')
## self._logger.debug('deleting node %s', old_node.get('id'))
self._tree.getroot().remove(old_node)
@@ -239,55 +308,171 @@
ok = True
for key in kwargs:
if not ok: break
- ok = node.get(key) == kwargs[key]
+ ok = node.get(key) == str(kwargs[key])
return ok
return is_
def on_init(self):
+ """on_init
+ This method is called when the database must be created.
+ """
pass
def on_load(self):
+ """on_load
+ This method is called when the database is loaded from memory
+ """
pass
def on_save(self):
+ """on_save
+ This method is called when the database is saved, either from
explicit
+ or automatic saves.
+ """
pass
def on_skip_load(self):
+ """on_skip_load
+ This method is called when the automatic load is not needed
+ """
pass
-if __name__=='__main__':
- import xcheck
+ def make_search_func(self, arg):
+ """make_filter_func(arg)
+
+ Returns a function that creates a score for a node.
+
+ The argument is a semi-colon dilimited string of comparions in the
+ form of `field` `operator` `parameter`.
+
+ `field` is an XML tag or attribute in a single word
+ `operator` is a single word
+ `parameter` is the value to search for
+
+ Valide operators are:
+
+ * 'lt' -- less than
+ * 'le' or 'before' -- less than or equal to
+ * 'gt' -- greater than
+ * 'ge' or 'after' -- greater than or equal to
+ * 'ne' -- Not Equal
+ * 'is' or 'eq'
+
+ """
+ args = map(str.strip, arg.split(';'))
+
+ def s(node):
+ node = get_elem(node)
+ scores = []
+ for arg in args:
+ field, operation, parameters = arg.split(' ', 2)
+ search_path = self._checker.xpath_to(field)
+ this_checker = self._checker.get(field)
+
+ # local_scores will store match scores from each element
tested
+ # the best score for this argument will be attached to the
score
+ local_scores = []
+ for n in node.findall(search_path):
+ if self._checker.is_att(field):
+ to_match = n.get(field)
+ else:
+ to_match = n.text
+ # determine which comparison to use
+ if operation in operation_aliases:
+ operation = operation_aliases[operation]
+
+ if operation in operation_whitelist:
+ res = compare(this_checker.normalize,
+ getattr(operator, operation),
+ to_match, parameters)
+ local_scores.append(100 if res else 0)
+
+ elif operation in ['is', 'eq']:
+ if isinstance(this_checker, (xcheck.BoolCheck,)):
+ cfunc = this_checker.normalize
+ else:
+ cfunc = str.lower
+ res = compare(cfunc, operator.eq,
+ to_match, parameters)
+ local_scores.append(100 if res else 0)
+
+ elif operation == 'startswith':
+ a = to_match.lower()
+ b = parameters.lower()
+ local_scores.append(100 if a.startswith(b) else 0)
+ else:
+ seq = difflib.SequenceMatcher(None,
+ parameters.lower(), to_match.lower())
+ local_scores.append(seq.ratio()*100)
+
+ scores.append(max(local_scores))
+
+ scores = map(float, scores)
+ score = sum(scores)/len(scores)
+
+ return int(score)
+ return s
+
+ def make_filter_func(self, args):
+ def f(node):
+ s = self.make_search_func(args)
+ return s(node) == 100
+ return f
+
+import unittest
+_simple_check="""<xcheck name="c">
+<children>
+ <text name="name" min_length="1" />
+</children>
+</xcheck>
+"""
+
+_bad_id="<xcheck name='c'><attributes><text
name='id'/></attributes></xcheck>"
- contact_checker = xcheck.load_checker("""<xcheck name="contact">
- <attributes>
- <int name="id" required="yes" />
- </attributes>
- <children>
- <text name="name" min_length="1" />
- <email name="email">
- <attributes>
- <selection name="type" values="home, work" />
- </attributes>
- </email>
- </children>
- </xcheck>
- """)
+class ManagerTC(unittest.TestCase):
+ def setUp(self):
+ self._opts = DBOptions()
+ self._opts.data_dir = 'testcase'
- Contacts = DBManager(contact_checker, DBOptions)
+## def tearDown(self):
+## os.remove(self._opts.get_file_path('c'))
- Contacts._logger.setLevel(logging.DEBUG)
+ def test_checker_changes(self):
+ simple_check = xcheck.load_checker(_simple_check)
+ man = DBManager(simple_check, self._opts)
+ ch = man._checker
+ self.assertTrue(ch.has_attribute('mod_time'),
+ "DBManager did not add 'mod_time' attribute to checker")
+ self.assertIsInstance(ch.get('mod_time'), xcheck.DatetimeCheck,
+ "DBManager did not force 'mod_time' attribute to
DatetimeCheck")
+ self.assertTrue(ch.has_attribute('cr_time'),
+ "DBManager did not add 'cr_time' attribute to checker")
+ self.assertTrue(ch.has_attribute('revision'),
+ "DBManager did not add 'revision' attribute to checker")
+ self.assertTrue(ch.has_attribute('id'),
+ "DBManager did not add 'id' attribute to checker")
- joe = """<contact id="1">
- <name>Joe</name>
- <email type="home">
j...@example.com</email>
- </contact>"""
- Contacts.save_node(joe)
+ def test_bad_id(self):
+ bad_id = xcheck.load_checker(_bad_id)
+ man = DBManager(bad_id, self._opts)
+ ch = man._checker
+ self.assertIsInstance(ch.get('id'), xcheck.IntCheck,
+ "DBManager did not override a bad ID attribute")
+ def test_make_search(self):
+ man = DBManager(xcheck.load_checker(_simple_check), self._opts)
+ f = man.make_search_func("name is frank")
+ self.assertEqual(f('<c><name>frank</name></c>'), 100)
+ self.assertEqual(f('<c><name>jill</name></c>'), 0)
- for idx, contact in enumerate(Contacts.get_nodes(),1):
- print idx, contact, contact.get('id')
+ def test_make_filter(self):
+ man = DBManager(xcheck.load_checker(_simple_check), self._opts)
+ f = man.make_filter_func("name is frank")
+ self.assertTrue(f('<c><name>frank</name></c>'))
+ self.assertFalse(f('<c><name>jill</name></c>'))
- Contacts.close()
+if __name__=='__main__':
+ unittest.main(verbosity=1)
=======================================
--- /trunk/xmldb/options.py Wed Mar 27 22:23:17 2013
+++ /trunk/xmldb/options.py Sat Apr 6 14:46:09 2013
@@ -32,6 +32,7 @@
[xmldb]
update_time = 20
save_db_on_node_change = True
+prevent_update_on_save = False
"""
from cStringIO import StringIO
@@ -60,7 +61,7 @@
SafeConfigParser.__init__(self)
self.readfp(StringIO(DEFAULTS))
self._load()
- self.fileopts = {}
+## self.fileopts = {}
## if not self.has_section('Filenames'):
## self.add_section('Filenames')
@@ -80,14 +81,16 @@
try:
int(seconds)
except:
- raise ValueError, "Cannot set %s as time in seconds for Update
Time" % seconds
+ raise ValueError(
+ "Cannot set %s as time in seconds for Update Time" %
seconds)
self.set('xmldb', 'update_time', str(seconds))
update_time = property(_get_update_time, _set_update_time)
def _get_save_db(self):
- """Boolean. If True, the database file will be re-written
- every time a node is saved. If False, the Save method must be
called.
+ """If True, the database file will be re-written
+ every time a node is saved. If False, the DBManager.Save method
must
+ be called.
The default value is True."""
return self.getboolean('xmldb', 'save_db_on_node_change')
@@ -97,9 +100,21 @@
auto_save = property(_get_save_db, _set_save_db)
+ def _get_prevent_update(self):
+ """If True, the DBManager.save_node method will fail if the ID
attribute
+ already exists in the database file.
+
+ The default value is False.
+ """
+ return self.getboolean('xmldb', 'prevent_update_on_save')
+
+ def _set_prevent_update(self, item):
+ return
self.set('xmldb','prevent_update_on_save',str(get_bool(item)))
+
+ prevent_update_on_save = property(_get_prevent_update,
_set_prevent_update)
def _get_use_dir(self):
- """Boolean. If True, the DataDir value is appended to the
+ """If True, the DataDir value is appended to the
appropriate file path. If False, the filepath is interpreted as
an absolute path.
@@ -122,7 +137,7 @@
data_dir = property(get_dir, set_dir)
def get_file_path(self, optname):
- """Returns the file path to a specific option. Parses the filepath
+ """Returns the file path to a specific database. Parses the
filepath
as an absolute path if UseDir is False, otherwise, appends file
name to the value returned by DataDir.
"""
@@ -135,7 +150,7 @@
return os.path.abspath(self.get('filepaths', optname))
def set_file_path(self, optname, val):
- """Changes the filepath name for an option. Using this method
should
+ """Changes the filepath name for a database. Using this method
should
allow users to change databases, but a cleaner approach would be to
use the directory and change the directory.
"""
@@ -156,12 +171,18 @@
`register_database` is called automatically when Managers create
their
instance.
+
+ `register_database` also creates a section in the config file if
the
+ section does not already exist.
"""
folder, fp = os.path.split(fp)
name, ext = os.path.splitext(fp)
if not self.has_option('filepaths', name):
self.set_file_path(name, name + '.xml')
+ if not self.has_section(name):
+ self.add_section(name)
+
if __name__=='__main__':
#informal tests