Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

history truncated from 0412c9f

  • Loading branch information...
commit b7111e43325c4bd57fe721d437dedffc4b2e796b 0 parents
authored May 23, 2011

Showing 34 changed files with 4,907 additions and 0 deletions. Show diff stats Hide diff stats

  1. 202  APACHE-LICENSE-2.0.txt
  2. 13  CHANGELOG
  3. 80  doc/Makefile
  4. 1  doc/changelog.rst
  5. 34  doc/conf.py
  6. 3  doc/doc_djangosettings.py
  7. 130  doc/existdb.rst
  8. 29  doc/index.rst
  9. 37  eulexistdb/__init__.py
  10. 725  eulexistdb/db.py
  11. 70  eulexistdb/exceptions.py
  12. 16  eulexistdb/management/__init__.py
  13. 16  eulexistdb/management/commands/__init__.py
  14. 135  eulexistdb/management/commands/existdb.py
  15. 105  eulexistdb/manager.py
  16. 93  eulexistdb/models.py
  17. 1,252  eulexistdb/query.py
  18. 16  eulexistdb/templatetags/__init__.py
  19. 126  eulexistdb/templatetags/existdb.py
  20. 240  eulexistdb/testutil.py
  21. 6  pip-dev-req.txt
  22. 36  setup.py
  23. 30  test/localsettings.py.dist
  24. 42  test/test_all.py
  25. 20  test/test_existdb/__init__.py
  26. 4  test/test_existdb/exist_fixtures/goodbye-english.xml
  27. 4  test/test_existdb/exist_fixtures/goodbye-french.xml
  28. 1  test/test_existdb/exist_fixtures/hello.xml
  29. 439  test/test_existdb/test_db.py
  30. 73  test/test_existdb/test_models.py
  31. 798  test/test_existdb/test_query.py
  32. 56  test/test_existdb/test_templatetags.py
  33. 43  test/testcore.py
  34. 32  test/testsettings.py
202  APACHE-LICENSE-2.0.txt
... ...
@@ -0,0 +1,202 @@
  1
+
  2
+                                 Apache License
  3
+                           Version 2.0, January 2004
  4
+                        http://www.apache.org/licenses/
  5
+
  6
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  7
+
  8
+   1. Definitions.
  9
+
  10
+      "License" shall mean the terms and conditions for use, reproduction,
  11
+      and distribution as defined by Sections 1 through 9 of this document.
  12
+
  13
+      "Licensor" shall mean the copyright owner or entity authorized by
  14
+      the copyright owner that is granting the License.
  15
+
  16
+      "Legal Entity" shall mean the union of the acting entity and all
  17
+      other entities that control, are controlled by, or are under common
  18
+      control with that entity. For the purposes of this definition,
  19
+      "control" means (i) the power, direct or indirect, to cause the
  20
+      direction or management of such entity, whether by contract or
  21
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
  22
+      outstanding shares, or (iii) beneficial ownership of such entity.
  23
+
  24
+      "You" (or "Your") shall mean an individual or Legal Entity
  25
+      exercising permissions granted by this License.
  26
+
  27
+      "Source" form shall mean the preferred form for making modifications,
  28
+      including but not limited to software source code, documentation
  29
+      source, and configuration files.
  30
+
  31
+      "Object" form shall mean any form resulting from mechanical
  32
+      transformation or translation of a Source form, including but
  33
+      not limited to compiled object code, generated documentation,
  34
+      and conversions to other media types.
  35
+
  36
+      "Work" shall mean the work of authorship, whether in Source or
  37
+      Object form, made available under the License, as indicated by a
  38
+      copyright notice that is included in or attached to the work
  39
+      (an example is provided in the Appendix below).
  40
+
  41
+      "Derivative Works" shall mean any work, whether in Source or Object
  42
+      form, that is based on (or derived from) the Work and for which the
  43
+      editorial revisions, annotations, elaborations, or other modifications
  44
+      represent, as a whole, an original work of authorship. For the purposes
  45
+      of this License, Derivative Works shall not include works that remain
  46
+      separable from, or merely link (or bind by name) to the interfaces of,
  47
+      the Work and Derivative Works thereof.
  48
+
  49
+      "Contribution" shall mean any work of authorship, including
  50
+      the original version of the Work and any modifications or additions
  51
+      to that Work or Derivative Works thereof, that is intentionally
  52
+      submitted to Licensor for inclusion in the Work by the copyright owner
  53
+      or by an individual or Legal Entity authorized to submit on behalf of
  54
+      the copyright owner. For the purposes of this definition, "submitted"
  55
+      means any form of electronic, verbal, or written communication sent
  56
+      to the Licensor or its representatives, including but not limited to
  57
+      communication on electronic mailing lists, source code control systems,
  58
+      and issue tracking systems that are managed by, or on behalf of, the
  59
+      Licensor for the purpose of discussing and improving the Work, but
  60
+      excluding communication that is conspicuously marked or otherwise
  61
+      designated in writing by the copyright owner as "Not a Contribution."
  62
+
  63
+      "Contributor" shall mean Licensor and any individual or Legal Entity
  64
+      on behalf of whom a Contribution has been received by Licensor and
  65
+      subsequently incorporated within the Work.
  66
+
  67
+   2. Grant of Copyright License. Subject to the terms and conditions of
  68
+      this License, each Contributor hereby grants to You a perpetual,
  69
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
  70
+      copyright license to reproduce, prepare Derivative Works of,
  71
+      publicly display, publicly perform, sublicense, and distribute the
  72
+      Work and such Derivative Works in Source or Object form.
  73
+
  74
+   3. Grant of Patent License. Subject to the terms and conditions of
  75
+      this License, each Contributor hereby grants to You a perpetual,
  76
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
  77
+      (except as stated in this section) patent license to make, have made,
  78
+      use, offer to sell, sell, import, and otherwise transfer the Work,
  79
+      where such license applies only to those patent claims licensable
  80
+      by such Contributor that are necessarily infringed by their
  81
+      Contribution(s) alone or by combination of their Contribution(s)
  82
+      with the Work to which such Contribution(s) was submitted. If You
  83
+      institute patent litigation against any entity (including a
  84
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
  85
+      or a Contribution incorporated within the Work constitutes direct
  86
+      or contributory patent infringement, then any patent licenses
  87
+      granted to You under this License for that Work shall terminate
  88
+      as of the date such litigation is filed.
  89
+
  90
+   4. Redistribution. You may reproduce and distribute copies of the
  91
+      Work or Derivative Works thereof in any medium, with or without
  92
+      modifications, and in Source or Object form, provided that You
  93
+      meet the following conditions:
  94
+
  95
+      (a) You must give any other recipients of the Work or
  96
+          Derivative Works a copy of this License; and
  97
+
  98
+      (b) You must cause any modified files to carry prominent notices
  99
+          stating that You changed the files; and
  100
+
  101
+      (c) You must retain, in the Source form of any Derivative Works
  102
+          that You distribute, all copyright, patent, trademark, and
  103
+          attribution notices from the Source form of the Work,
  104
+          excluding those notices that do not pertain to any part of
  105
+          the Derivative Works; and
  106
+
  107
+      (d) If the Work includes a "NOTICE" text file as part of its
  108
+          distribution, then any Derivative Works that You distribute must
  109
+          include a readable copy of the attribution notices contained
  110
+          within such NOTICE file, excluding those notices that do not
  111
+          pertain to any part of the Derivative Works, in at least one
  112
+          of the following places: within a NOTICE text file distributed
  113
+          as part of the Derivative Works; within the Source form or
  114
+          documentation, if provided along with the Derivative Works; or,
  115
+          within a display generated by the Derivative Works, if and
  116
+          wherever such third-party notices normally appear. The contents
  117
+          of the NOTICE file are for informational purposes only and
  118
+          do not modify the License. You may add Your own attribution
  119
+          notices within Derivative Works that You distribute, alongside
  120
+          or as an addendum to the NOTICE text from the Work, provided
  121
+          that such additional attribution notices cannot be construed
  122
+          as modifying the License.
  123
+
  124
+      You may add Your own copyright statement to Your modifications and
  125
+      may provide additional or different license terms and conditions
  126
+      for use, reproduction, or distribution of Your modifications, or
  127
+      for any such Derivative Works as a whole, provided Your use,
  128
+      reproduction, and distribution of the Work otherwise complies with
  129
+      the conditions stated in this License.
  130
+
  131
+   5. Submission of Contributions. Unless You explicitly state otherwise,
  132
+      any Contribution intentionally submitted for inclusion in the Work
  133
+      by You to the Licensor shall be under the terms and conditions of
  134
+      this License, without any additional terms or conditions.
  135
+      Notwithstanding the above, nothing herein shall supersede or modify
  136
+      the terms of any separate license agreement you may have executed
  137
+      with Licensor regarding such Contributions.
  138
+
  139
+   6. Trademarks. This License does not grant permission to use the trade
  140
+      names, trademarks, service marks, or product names of the Licensor,
  141
+      except as required for reasonable and customary use in describing the
  142
+      origin of the Work and reproducing the content of the NOTICE file.
  143
+
  144
+   7. Disclaimer of Warranty. Unless required by applicable law or
  145
+      agreed to in writing, Licensor provides the Work (and each
  146
+      Contributor provides its Contributions) on an "AS IS" BASIS,
  147
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  148
+      implied, including, without limitation, any warranties or conditions
  149
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
  150
+      PARTICULAR PURPOSE. You are solely responsible for determining the
  151
+      appropriateness of using or redistributing the Work and assume any
  152
+      risks associated with Your exercise of permissions under this License.
  153
+
  154
+   8. Limitation of Liability. In no event and under no legal theory,
  155
+      whether in tort (including negligence), contract, or otherwise,
  156
+      unless required by applicable law (such as deliberate and grossly
  157
+      negligent acts) or agreed to in writing, shall any Contributor be
  158
+      liable to You for damages, including any direct, indirect, special,
  159
+      incidental, or consequential damages of any character arising as a
  160
+      result of this License or out of the use or inability to use the
  161
+      Work (including but not limited to damages for loss of goodwill,
  162
+      work stoppage, computer failure or malfunction, or any and all
  163
+      other commercial damages or losses), even if such Contributor
  164
+      has been advised of the possibility of such damages.
  165
+
  166
+   9. Accepting Warranty or Additional Liability. While redistributing
  167
+      the Work or Derivative Works thereof, You may choose to offer,
  168
+      and charge a fee for, acceptance of support, warranty, indemnity,
  169
+      or other liability obligations and/or rights consistent with this
  170
+      License. However, in accepting such obligations, You may act only
  171
+      on Your own behalf and on Your sole responsibility, not on behalf
  172
+      of any other Contributor, and only if You agree to indemnify,
  173
+      defend, and hold each Contributor harmless for any liability
  174
+      incurred by, or claims asserted against, such Contributor by reason
  175
+      of your accepting any such warranty or additional liability.
  176
+
  177
+   END OF TERMS AND CONDITIONS
  178
+
  179
+   APPENDIX: How to apply the Apache License to your work.
  180
+
  181
+      To apply the Apache License to your work, attach the following
  182
+      boilerplate notice, with the fields enclosed by brackets "[]"
  183
+      replaced with your own identifying information. (Don't include
  184
+      the brackets!)  The text should be enclosed in the appropriate
  185
+      comment syntax for the file format. We also recommend that a
  186
+      file or class name and description of purpose be included on the
  187
+      same "printed page" as the copyright notice for easier
  188
+      identification within third-party archives.
  189
+
  190
+   Copyright [yyyy] [name of copyright owner]
  191
+
  192
+   Licensed under the Apache License, Version 2.0 (the "License");
  193
+   you may not use this file except in compliance with the License.
  194
+   You may obtain a copy of the License at
  195
+
  196
+       http://www.apache.org/licenses/LICENSE-2.0
  197
+
  198
+   Unless required by applicable law or agreed to in writing, software
  199
+   distributed under the License is distributed on an "AS IS" BASIS,
  200
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  201
+   See the License for the specific language governing permissions and
  202
+   limitations under the License.
13  CHANGELOG
... ...
@@ -0,0 +1,13 @@
  1
+Change & Version Information
  2
+============================
  3
+
  4
+The following is a summary of changes and improvements to
  5
+:mod:`eulexistdb`.  New features in each version should be listed, with
  6
+any necessary information about installation or upgrade notes.
  7
+
  8
+
  9
+Initial Release
  10
+---------------
  11
+
  12
+* Split out existdb-specific components from :mod:`eulcore`; now
  13
+  depends on :mod:`eulxml`.
80  doc/Makefile
... ...
@@ -0,0 +1,80 @@
  1
+# Makefile for EULcore Sphinx-based documentation
  2
+#
  3
+
  4
+export PYTHONPATH=.:..
  5
+# sphinx needs django settings while loading eulcore.django.* to introspect
  6
+# those modules for autodocs
  7
+export DJANGO_SETTINGS_MODULE=doc_djangosettings
  8
+
  9
+SPHINXOPTS    =
  10
+SPHINXBUILD   = sphinx-build
  11
+PAPER         =
  12
+
  13
+PAPEROPT_a4     = -D latex_paper_size=a4
  14
+PAPEROPT_letter = -D latex_paper_size=letter
  15
+ALLSPHINXOPTS   = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
  16
+
  17
+.PHONY: help all clean html web pickle htmlhelp latex changes linkcheck
  18
+
  19
+help:
  20
+	@echo "Please use \`make <target>' where <target> is one of"
  21
+	@echo "  html      to make standalone HTML files"
  22
+	@echo "  pickle    to make pickle files"
  23
+	@echo "  json      to make JSON files"
  24
+	@echo "  htmlhelp  to make HTML files and a HTML help project"
  25
+	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
  26
+	@echo "  changes   to make an overview over all changed/added/deprecated items"
  27
+	@echo "  linkcheck to check all external links for integrity"
  28
+
  29
+all: html
  30
+
  31
+clean:
  32
+	-rm -rf build *.pyc
  33
+
  34
+html:
  35
+	mkdir -p build/html build/doctrees
  36
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html
  37
+	@echo
  38
+	@echo "Build finished. The HTML pages are in build/html."
  39
+
  40
+pickle:
  41
+	mkdir -p build/pickle build/doctrees
  42
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle
  43
+	@echo
  44
+	@echo "Build finished; now you can process the pickle files."
  45
+
  46
+web: pickle
  47
+
  48
+json:
  49
+	mkdir -p build/json build/doctrees
  50
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json
  51
+	@echo
  52
+	@echo "Build finished; now you can process the JSON files."
  53
+
  54
+htmlhelp:
  55
+	mkdir -p build/htmlhelp build/doctrees
  56
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp
  57
+	@echo
  58
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
  59
+	      ".hhp project file in build/htmlhelp."
  60
+
  61
+latex:
  62
+	mkdir -p build/latex build/doctrees
  63
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex
  64
+	@echo
  65
+	@echo "Build finished; the LaTeX files are in build/latex."
  66
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
  67
+	      "run these through (pdf)latex."
  68
+
  69
+changes:
  70
+	mkdir -p build/changes build/doctrees
  71
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes
  72
+	@echo
  73
+	@echo "The overview file is in build/changes."
  74
+
  75
+linkcheck:
  76
+	mkdir -p build/linkcheck build/doctrees
  77
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck
  78
+	@echo
  79
+	@echo "Link check complete; look for any errors in the above output " \
  80
+	      "or in build/linkcheck/output.txt."
1  doc/changelog.rst
Source Rendered
... ...
@@ -0,0 +1 @@
  1
+.. include:: ../CHANGELOG
34  doc/conf.py
... ...
@@ -0,0 +1,34 @@
  1
+# eulcore documentation build configuration file
  2
+
  3
+import eulexistdb
  4
+
  5
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
  6
+
  7
+#templates_path = ['templates']
  8
+exclude_trees = ['build']
  9
+source_suffix = '.rst'
  10
+master_doc = 'index'
  11
+
  12
+project = 'EULexistdb'
  13
+copyright = '2011, Emory University Libraries'
  14
+version = '%d.%d' % eulexistdb.__version_info__[:2]
  15
+release = eulexistdb.__version__
  16
+modindex_common_prefix = ['eulexistdb.']
  17
+
  18
+pygments_style = 'sphinx'
  19
+
  20
+html_style = 'default.css'
  21
+#html_static_path = ['static']
  22
+htmlhelp_basename = 'eulcoredoc'
  23
+
  24
+latex_documents = [
  25
+  ('index', 'eulcore.tex', 'EULexistdb Documentation',
  26
+   'Emory University Libraries', 'manual'),
  27
+]
  28
+
  29
+# configuration for intersphinx: refer to the Python standard library, eulxml, django
  30
+intersphinx_mapping = {
  31
+    'http://docs.python.org/': None,
  32
+    'http://waterhouse.library.emory.edu:8080/hudson/job/eulxml/javadoc/': None,
  33
+    'http://docs.djangoproject.com/en/1.3/': 'http://docs.djangoproject.com/en/dev/_objects/',
  34
+}
3  doc/doc_djangosettings.py
... ...
@@ -0,0 +1,3 @@
  1
+# This dummy django settings file is used by sphinx while loading
  2
+# eulcore.django.* to examine it for autodoc generation.
  3
+FEDORA_ROOT = 'dummy root'  # NOTE: there are documentation errors without this
130  doc/existdb.rst
Source Rendered
... ...
@@ -0,0 +1,130 @@
  1
+:mod:`eulcore.existdb` -- Store and retrieve data in an eXist database
  2
+======================================================================
  3
+
  4
+.. automodule:: eulexistdb
  5
+
  6
+.. FIXME: automodules here rely on undoc-members to include undocumented
  7
+     members in the output documentation. We should move away from this,
  8
+     preferring instead to add docstrings and/or reST docs right here) for
  9
+     members that need documentation.
  10
+
  11
+See :mod:`eulcore.django.existdb` for existdb and django integration.
  12
+
  13
+Direct database access
  14
+----------------------
  15
+
  16
+.. automodule:: eulexistdb.db
  17
+
  18
+   .. autoclass:: ExistDB(server_url[, resultType[, encoding[, verbose]]])
  19
+      :members:
  20
+
  21
+      .. automethod:: getDocument(name)
  22
+
  23
+      .. automethod:: createCollection(collection_name[, overwrite])
  24
+
  25
+      .. automethod:: removeCollection(collection_name)
  26
+
  27
+      .. automethod:: hasCollection(collection_name)
  28
+
  29
+      .. automethod:: load(xml, path[, overwrite])
  30
+
  31
+      .. automethod:: query(xquery[, start[, how_many]])
  32
+
  33
+      .. automethod:: executeQuery(xquery)
  34
+
  35
+      .. automethod:: querySummary(result_id)
  36
+
  37
+      .. automethod:: getHits(result_id)
  38
+
  39
+      .. automethod:: retrieve(result_id, position)
  40
+
  41
+      .. automethod:: releaseQueryResult(result_id)
  42
+
  43
+   .. autoclass:: QueryResult
  44
+      :members:
  45
+
  46
+   .. autoexception:: ExistDBException
  47
+
  48
+
  49
+Object-based searching
  50
+----------------------
  51
+
  52
+.. automodule:: eulexistdb.query
  53
+
  54
+   .. autoclass:: QuerySet
  55
+      :members:
  56
+
  57
+
  58
+Django tie-ins for :mod:`eulexistdb`
  59
+------------------------------------
  60
+
  61
+
  62
+.. automodule:: eulexistdb.manager
  63
+   :members:
  64
+
  65
+.. automodule:: eulexistdb.models
  66
+
  67
+   .. autoclass:: XmlModel
  68
+
  69
+      Two use cases are particularly common. First, a developer may wish to
  70
+      use an ``XmlModel`` just like an :class:`~eulxml.xmlmap.XmlObject`,
  71
+      but with the added semantics of being eXist-backed::
  72
+      
  73
+        class StoredWidget(XmlModel):
  74
+            name = StringField("name")
  75
+            quantity = IntegerField("quantity")
  76
+            top_customers = StringListField("(order[@status='active']/customer)[position()<5]/name")
  77
+            objects = Manager("//widget")
  78
+
  79
+      Second, if an :class:`~eulxml.xml.XmlObject` is defined elsewhere, an
  80
+      application developer might simply expose
  81
+      :class:`~eulexistdb.db.ExistDB` backed objects::
  82
+
  83
+        class StoredThingie(XmlModel, Thingie):
  84
+            objects = Manager("/thingie")
  85
+
  86
+      Of course, some applications ask for mixing these two cases, extending
  87
+      an existing :class:`~eulxml.xml.XmlObject` while adding
  88
+      application-specific fields::
  89
+
  90
+        class CustomThingie(XmlModel, Thingie):
  91
+            best_foobar = StringField("qux/fnord[@application='myapp']/name")
  92
+            custom_detail = IntegerField("detail/@level")
  93
+            objects = Manager("/thingie")
  94
+
  95
+      In addition to the fields inherited from
  96
+      :class:`~eulxml.xmlmap.XmlObject`, ``XmlModel`` objects have one
  97
+      extra field:
  98
+
  99
+      .. attribute:: _managers
  100
+
  101
+         A dictionary mapping manager names to
  102
+         :class:`~eulcexistdb.manager.Manager` objects. This
  103
+         dictionary includes all of the managers defined on the model
  104
+         itself, though it does not currently include managers inherited
  105
+         from the model's parents.
  106
+
  107
+Custom Template Tags
  108
+^^^^^^^^^^^^^^^^^^^^
  109
+
  110
+.. automodule:: eulexistdb.templatetags.existdb
  111
+    :members:
  112
+
  113
+:mod:`~eulexistdb` Management commands
  114
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  115
+
  116
+The following management command will be available when you include
  117
+:mod:`eulexistdb` in your django ``INSTALLED_APPS`` and rely on the
  118
+existdb settings described above.
  119
+
  120
+For more details on these commands, use ``manage.py <command> help``
  121
+
  122
+ * **existdb** - update, remove, and show information about the index
  123
+   configuration for a collection index; reindex the configured
  124
+   collection based on that index configuration
  125
+
  126
+:mod:`~eulexistdb.testutil` Unit Test utilities
  127
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  128
+
  129
+.. automodule:: eulexistdb.testutil
  130
+    :members:
29  doc/index.rst
Source Rendered
... ...
@@ -0,0 +1,29 @@
  1
+EULcore |release|
  2
+=================
  3
+
  4
+EULexistdb is one of several Python_ components from `Emory University
  5
+Libraries`_. The library contains both released and unreleased beta
  6
+components. Except where noted otherwise, components documented here
  7
+are released and ready for production use.
  8
+
  9
+:mod:`eulexistdb` attempts to provide a pythonic interface to the
  10
+`eXist-db XML database <http://exist.sourceforge.net/>`_.
  11
+
  12
+.. _Python: http://www.python.org/
  13
+.. _Emory University Libraries: http://web.library.emory.edu/
  14
+
  15
+Contents
  16
+--------
  17
+
  18
+.. toctree::
  19
+   :maxdepth: 3
  20
+   
  21
+   changelog
  22
+   existdb
  23
+
  24
+Indices and tables
  25
+------------------
  26
+
  27
+* :ref:`genindex`
  28
+* :ref:`modindex`
  29
+* :ref:`search`
37  eulexistdb/__init__.py
... ...
@@ -0,0 +1,37 @@
  1
+# file eulexistdb/__init__.py
  2
+# 
  3
+#   Copyright 2010,2011 Emory University Libraries
  4
+#
  5
+#   Licensed under the Apache License, Version 2.0 (the "License");
  6
+#   you may not use this file except in compliance with the License.
  7
+#   You may obtain a copy of the License at
  8
+#
  9
+#       http://www.apache.org/licenses/LICENSE-2.0
  10
+#
  11
+#   Unless required by applicable law or agreed to in writing, software
  12
+#   distributed under the License is distributed on an "AS IS" BASIS,
  13
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14
+#   See the License for the specific language governing permissions and
  15
+#   limitations under the License.
  16
+
  17
+"""Interact with `eXist-db`_ XML databases.
  18
+
  19
+This package provides classes to ease interaction with eXist XML databases.
  20
+It contains the following modules:
  21
+
  22
+ * :mod:`eulexistdb.db` -- Connect to the database and query
  23
+ * :mod:`eulexistdb.query` -- Query :class:`~eulxml.xmlmap.XmlObject`
  24
+   models from eXist with semantics like a Django_ QuerySet
  25
+
  26
+.. _eXist-db: http://exist.sourceforge.net/
  27
+.. _Django: http://www.djangoproject.com/
  28
+
  29
+"""
  30
+
  31
+__version_info__ = (0, 1, 'dev')
  32
+
  33
+# Dot-connect all but the last. Last is dash-connected if not None.
  34
+__version__ = '.'.join([ str(i) for i in __version_info__[:-1] ])
  35
+if __version_info__[-1] is not None:
  36
+    __version__ += ('-%s' % (__version_info__[-1],))
  37
+
725  eulexistdb/db.py
... ...
@@ -0,0 +1,725 @@
  1
+# file eulexistdb/db.py
  2
+#
  3
+#   Copyright 2010,2011 Emory University Libraries
  4
+#
  5
+#   Licensed under the Apache License, Version 2.0 (the "License");
  6
+#   you may not use this file except in compliance with the License.
  7
+#   You may obtain a copy of the License at
  8
+#
  9
+#       http://www.apache.org/licenses/LICENSE-2.0
  10
+#
  11
+#   Unless required by applicable law or agreed to in writing, software
  12
+#   distributed under the License is distributed on an "AS IS" BASIS,
  13
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14
+#   See the License for the specific language governing permissions and
  15
+#   limitations under the License.
  16
+
  17
+"""Connect to an eXist XML database and query it.
  18
+
  19
+This module provides :class:`ExistDB` and related classes for connecting to
  20
+an eXist-db_ database and executing XQuery_ queries against it.
  21
+
  22
+.. _XQuery: http://www.w3.org/TR/xquery/
  23
+.. _eXist-db: http://exist.sourceforge.net/
  24
+
  25
+When used with Django, :class:`~eulexistdb.db.ExistDB` can pull
  26
+configuration settings directly from Django settings.  If you create
  27
+an instance of :class:`~eulexistdb.db.ExistDB` without specifying a
  28
+server url, it will attempt to configure an eXist database based on
  29
+Django settings, using the configuration names documented below.
  30
+
  31
+
  32
+
  33
+Projects that use this module should include the following settings in their
  34
+``settings.py``::
  35
+
  36
+  #Exist DB Settings
  37
+  EXISTDB_SERVER_USER = 'user'
  38
+  EXISTDB_SERVER_PASSWORD = 'password'
  39
+  EXISTDB_SERVER_URL = "http://megaserver.example.com:8042/exist"
  40
+  EXISTDB_ROOT_COLLECTION = "/sample_collection"
  41
+
  42
+.. note:
  43
+
  44
+  User and password settings are optional.
  45
+
  46
+To configure a timeout for most eXist connections, specify the desired
  47
+time in seconds as ``EXISTDB_TIMEOUT``; if none is specified, the
  48
+global default socket timeout will be used.
  49
+
  50
+.. note::
  51
+
  52
+  Any configured ``EXISTDB_TIMEOUT`` will be ignored by the
  53
+  **existdb** management command, since reindexing a large collection
  54
+  could take significantly longer than a normal timeout would allow
  55
+  for.
  56
+
  57
+If you are using an eXist index configuration file, you can add another setting
  58
+to specify your configuration file::
  59
+
  60
+  EXISTDB_INDEX_CONFIGFILE = "/path/to/my/exist_index.xconf"
  61
+
  62
+This will allow you to use the ``existdb`` management command to
  63
+manage your index configuration file in eXist.
  64
+
  65
+If you wish to specify options for fulltext queries, you can set a dictionary
  66
+of options like this::
  67
+
  68
+    EXISTDB_FULLTEXT_OPTIONS = {'default-operator': 'and'}
  69
+
  70
+.. note::
  71
+
  72
+  Full-text query options are only available in very recent versions of eXist.
  73
+
  74
+
  75
+If you are writing unit tests against code that uses
  76
+:mod:`eulexistdb`, you may want to take advantage of
  77
+:class:`eulexistdb.testutil.TestCase` for loading fixture data to a
  78
+test eXist-db collection, and
  79
+:class:`eulexistdb.testutil.ExistDBTestSuiteRunner`, which has logic
  80
+to set up and switch configurations between a development and test
  81
+collections in eXist.
  82
+
  83
+----
  84
+
  85
+"""
  86
+
  87
+from functools import wraps
  88
+import httplib
  89
+import logging
  90
+import socket
  91
+from urllib import unquote_plus, splittype
  92
+import urlparse
  93
+import warnings
  94
+import xmlrpclib
  95
+
  96
+from eulxml import xmlmap
  97
+from eulexistdb.exceptions import ExistDBException, ExistDBTimeout
  98
+
  99
+__all__ = ['ExistDB', 'QueryResult', 'ExistDBException', 'EXISTDB_NAMESPACE']
  100
+
  101
+logger = logging.getLogger(__name__)
  102
+
  103
+EXISTDB_NAMESPACE = 'http://exist.sourceforge.net/NS/exist'
  104
+
  105
+def _wrap_xmlrpc_fault(f):
  106
+    @wraps(f)
  107
+    def wrapper(*args, **kwargs):
  108
+        try:
  109
+            return f(*args, **kwargs)
  110
+        except socket.timeout as e:
  111
+            raise ExistDBTimeout(e)
  112
+        except (socket.error, xmlrpclib.Fault, \
  113
+            xmlrpclib.ProtocolError, xmlrpclib.ResponseError) as e:
  114
+                raise ExistDBException(e)
  115
+        # FIXME: could we catch IOerror (connection reset) and try again ?
  116
+        # occasionally getting this error (so far exclusively in unit tests)
  117
+        # error: [Errno 104] Connection reset by peer
  118
+    return wrapper
  119
+
  120
+
  121
+class ExistDB:
  122
+    """Connect to an eXist database, and manipulate and query it.
  123
+
  124
+    Construction doesn't initiate server communication, only store
  125
+    information about where the server is, to be used in later
  126
+    communications.
  127
+
  128
+    :param server_url: The XML-RPC endpoint of the server, typically
  129
+                       ``/xmlrpc`` within the server root.
  130
+    :param resultType: The class to use for returning :meth:`query` results;
  131
+                       defaults to :class:`QueryResult`
  132
+    :param encoding:   The encoding used to communicate with the server;
  133
+                       defaults to "UTF-8"
  134
+    :param verbose:    When True, print XML-RPC debugging messages to stdout
  135
+    :param timeout: Specify a timeout for xmlrpc connection
  136
+      requests.If not specified, the global default socket timeout
  137
+      value will be used.
  138
+
  139
+    """
  140
+
  141
+    def __init__(self, server_url=None, resultType=None, encoding='UTF-8', verbose=False,
  142
+                 **kwargs):
  143
+        # FIXME: Will encoding ever be anything but UTF-8? Does this really
  144
+        #   need to be part of our public interface?
  145
+
  146
+        self.resultType = resultType or QueryResult
  147
+        datetime_opt = {'use_datetime': True}
  148
+
  149
+        # distinguish between timeout not set and no timeout, to allow
  150
+        # easily setting a timeout of None and have it override any
  151
+        # configured EXISTDB_TIMEOUT
  152
+        timeout = None
  153
+        if 'timeout' in kwargs:
  154
+            timeout = kwargs['timeout']
  155
+
  156
+        # if server url or timeout are not set, attempt to get from django settings
  157
+        if server_url is None or 'timeout' not in kwargs:
  158
+            try:
  159
+                from django.conf import settings
  160
+                if server_url is None:
  161
+                    server_url = self._serverurl_from_djangoconf()
  162
+                    
  163
+                if 'timeout' not in kwargs:
  164
+                    timeout = getattr(settings, 'EXISTDB_TIMEOUT', None)
  165
+            except ImportError:
  166
+                pass
  167
+
  168
+            
  169
+        # if server url is still not set, we have a problem
  170
+        if server_url is None:
  171
+            raise Exception('Cannot initialize an eXist-db connection without specifying ' +
  172
+                            'eXist server url directly or in Django settings as EXISTDB_SERVER_URL')
  173
+
  174
+
  175
+
  176
+        # determine if we need http or https transport
  177
+        # (duplicates some logic in xmlrpclib)
  178
+        type, uri = splittype(server_url)
  179
+        if type not in ("http", "https"):
  180
+            raise IOError, "unsupported XML-RPC protocol"
  181
+        if type == 'https':
  182
+            transport = TimeoutSafeTransport(timeout=timeout, **datetime_opt)
  183
+        else:
  184
+            transport = TimeoutTransport(timeout=timeout, **datetime_opt)
  185
+
  186
+        self.server = xmlrpclib.ServerProxy(
  187
+                uri="%s/xmlrpc" % server_url.rstrip('/'),
  188
+                transport=transport,
  189
+                encoding=encoding,
  190
+                verbose=verbose,
  191
+                allow_none=True,
  192
+                **datetime_opt
  193
+            )
  194
+
  195
+    def _serverurl_from_djangoconf(self):
  196
+        # determine what exist url to use based on django settings, if available
  197
+        try:
  198
+            from django.conf import settings
  199
+
  200
+            # don't worry about errors on this one - if it isn't set, this should fail
  201
+            exist_url = settings.EXISTDB_SERVER_URL
  202
+
  203
+            # former syntax had credentials in the server url; warn about the change
  204
+            if '@' in exist_url:
  205
+                warnings.warn('EXISTDB_SERVER_URL should not include eXist user or ' +
  206
+                              'password information.  You should update your django ' +
  207
+                              'settings to use EXISTDB_SERVER_USER and EXISTDB_SERVER_PASSWORD.')
  208
+
  209
+            # look for username & password
  210
+            username = getattr(settings, 'EXISTDB_SERVER_USER', None)
  211
+            password = getattr(settings, 'EXISTDB_SERVER_PASSWORD', None)
  212
+
  213
+            # if username or password are configured, add them to the url
  214
+            if username or password:
  215
+                # split the url into its component parts
  216
+                urlparts = urlparse.urlsplit(exist_url)
  217
+                # could have both username and password or just a username
  218
+                if username and password:
  219
+                    prefix = '%s:%s' % (username, password)
  220
+                else:
  221
+                    prefix = username
  222
+                # prefix the network location with credentials
  223
+                netloc = '%s@%s' % (prefix, urlparts.netloc)
  224
+                # un-split the url with all the previous parts and modified location
  225
+                exist_url = urlparse.urlunsplit((urlparts.scheme, netloc, urlparts.path,
  226
+                                                urlparts.query, urlparts.fragment))
  227
+
  228
+            return exist_url
  229
+        except ImportError:
  230
+            pass
  231
+
  232
+
  233
+    def getDocument(self, name, **kwargs):
  234
+        """Retrieve a document from the database.
  235
+
  236
+        :param name: database document path to retrieve
  237
+        :rtype: string contents of the document
  238
+
  239
+        """
  240
+        logger.debug('getDocumentAsString %s options=%s' % (name, kwargs))
  241
+        return self.server.getDocumentAsString(name, kwargs)
  242
+
  243
+    def getDoc(self, name, **kwargs):
  244
+        "Alias for :meth:`getDocument`."
  245
+        return self.getDocument(name, **kwargs)
  246
+
  247
+
  248
+    def createCollection(self, collection_name, overwrite=False):
  249
+        """Create a new collection in the database.
  250
+
  251
+        :param collection_name: string name of collection
  252
+        :param overwrite: overwrite existing document?
  253
+        :rtype: boolean indicating success
  254
+
  255
+        """
  256
+        if not overwrite and self.hasCollection(collection_name):
  257
+            raise ExistDBException(collection_name + " exists")
  258
+
  259
+        logger.debug('createCollection %s' % collection_name)
  260
+        return self.server.createCollection(collection_name)
  261
+
  262
+    @_wrap_xmlrpc_fault
  263
+    def removeCollection(self, collection_name):
  264
+        """Remove the named collection from the database.
  265
+
  266
+        :param collection_name: string name of collection
  267
+        :rtype: boolean indicating success
  268
+
  269
+        """
  270
+        if (not self.hasCollection(collection_name)):
  271
+            raise ExistDBException(collection_name + " does not exist")
  272
+
  273
+        logger.debug('removeCollection %s' % collection_name)
  274
+        return self.server.removeCollection(collection_name)
  275
+
  276
+    def hasCollection(self, collection_name):
  277
+        """Check if a collection exists.
  278
+
  279
+        :param collection_name: string name of collection
  280
+        :rtype: boolean
  281
+
  282
+        """
  283
+        try:
  284
+            logger.debug('describeCollection %s' % collection_name)
  285
+            self.server.describeCollection(collection_name)
  286
+            return True
  287
+        except xmlrpclib.Fault, e:
  288
+            s = "collection " + collection_name + " not found"
  289
+            if (e.faultCode == 0 and s in e.faultString):
  290
+                return False
  291
+            else:
  292
+                raise ExistDBException(e)
  293
+
  294
+    def reindexCollection(self, collection_name):
  295
+        """Reindex a collection.
  296
+        Reindex will fail if the eXist user does not have the correct permissions
  297
+        within eXist (must be a member of the DBA group).
  298
+
  299
+        :param collection_name: string name of collection
  300
+        :rtype: boolean success
  301
+
  302
+        """
  303
+        if (not self.hasCollection(collection_name)):
  304
+            raise ExistDBException(collection_name + " does not exist")
  305
+
  306
+        # xquery reindex function requires that collection name begin with /db/
  307
+        if collection_name[0:3] != '/db':
  308
+            collection_name = '/db/' + collection_name.strip('/')
  309
+
  310
+        result = self.query("xmldb:reindex('%s')" % collection_name)
  311
+        return result.values[0] == 'true'
  312
+
  313
+    @_wrap_xmlrpc_fault
  314
+    def hasDocument(self, document_path):
  315
+        """Check if a document is present in eXist.
  316
+
  317
+        :param document_path: string full path to document in eXist
  318
+        :rtype: boolean
  319
+
  320
+        """
  321
+        if self.describeDocument(document_path) == {}:
  322
+            return False
  323
+        else:
  324
+            return True
  325
+
  326
+    @_wrap_xmlrpc_fault
  327
+    def describeDocument(self, document_path):
  328
+        """Return information about a document in eXist.
  329
+        Includes name, owner, group, created date, permissions, mime-type,
  330
+        type, content-length.
  331
+        Returns an empty dictionary if document is not found.
  332
+
  333
+        :param document_path: string full path to document in eXist
  334
+        :rtype: dictionary
  335
+
  336
+        """
  337
+        logger.debug('describeResource %s' % document_path)
  338
+        return self.server.describeResource(document_path)
  339
+
  340
+    @_wrap_xmlrpc_fault
  341
+    def getCollectionDescription(self, collection_name):
  342
+        """Retrieve information about a collection.
  343
+
  344
+        :param collection_name: string name of collection
  345
+        :rtype: boolean
  346
+
  347
+        """
  348
+        logger.debug('getCollectionDesc %s' % collection_name)
  349
+        return self.server.getCollectionDesc(collection_name)
  350
+
  351
+    @_wrap_xmlrpc_fault
  352
+    def load(self, xml, path, overwrite=False):
  353
+        """Insert or overwrite a document in the database.
  354
+        
  355
+        :param xml: string or file object with the document contents
  356
+        :param path: destination location in the database
  357
+        :param overwrite: True to allow overwriting an existing document
  358
+        :rtype: boolean indicating success
  359
+
  360
+        """
  361
+        if hasattr(xml, 'read'):
  362
+            xml = xml.read()
  363
+
  364
+        logger.debug('parse %s overwrite=%s' % (path, overwrite))
  365
+        return self.server.parse(xml, path, int(overwrite))
  366
+
  367
+    @_wrap_xmlrpc_fault
  368
+    def removeDocument(self, name):
  369
+        """Remove a document from the database.
  370
+
  371
+        :param name: full eXist path to the database document to be removed
  372
+        :rtype: boolean indicating success
  373
+
  374
+        """
  375
+        logger.debug('remove %s' % name)
  376
+        return self.server.remove(name)
  377
+
  378
+    @_wrap_xmlrpc_fault
  379
+    def moveDocument(self, from_collection, to_collection, document):
  380
+        """Move a document in eXist from one collection to another.
  381
+
  382
+        :param from_collection: collection where the document currently exists
  383
+        :param to_collection: collection where the document should be moved
  384
+        :param document: name of the document in eXist
  385
+        :rtype: boolean
  386
+        """
  387
+        self.query("xmldb:move('%s', '%s', '%s')" % \
  388
+                            (from_collection, to_collection, document))
  389
+        # query result does not return any meaningful content,
  390
+        # but any failure (missing collection, document, etc) should result in
  391
+        # an exception, so return true if the query completed successfully
  392
+        return True
  393
+
  394
+    @_wrap_xmlrpc_fault
  395
+    def query(self, xquery, start=1, how_many=10, **kwargs):
  396
+        """Execute an XQuery query, returning the results directly.
  397
+
  398
+        :param xquery: a string XQuery query
  399
+        :param start: first index to return (1-based)
  400
+        :param how_many: maximum number of items to return
  401
+        :rtype: the resultType specified at the creation of this ExistDB;
  402
+                defaults to :class:`QueryResult`.
  403
+
  404
+        """
  405
+        logger.debug('query how_many=%d start=%d args=%s\n%s' % (how_many, start, kwargs, xquery))
  406
+        xml_s = self.server.query(xquery, how_many, start, kwargs)
  407
+
  408
+        # xmlrpclib tries to guess whether the result is a string or
  409
+        # unicode, returning whichever it deems most appropriate.
  410
+        # Unfortunately, :meth:`~eulxml.xmlmap.load_xmlobject_from_string`
  411
+        # requires a byte string. This means that if xmlrpclib gave us a
  412
+        # unicode, we need to encode it:
  413
+        if isinstance(xml_s, unicode):
  414
+            xml_s = xml_s.encode("UTF-8")
  415
+
  416
+        return xmlmap.load_xmlobject_from_string(xml_s, self.resultType)
  417
+
  418
+    @_wrap_xmlrpc_fault
  419
+    def executeQuery(self, xquery):
  420
+        """Execute an XQuery query, returning a server-provided result
  421
+        handle.
  422
+
  423
+        :param xquery: a string XQuery query 
  424
+        :rtype: an integer handle identifying the query result for future calls
  425
+
  426
+        """
  427
+        # NOTE: eXist's xmlrpc interface requires a dictionary parameter.
  428
+        #   This parameter is not documented in the eXist docs at
  429
+        #   http://demo.exist-db.org/exist/devguide_xmlrpc.xml
  430
+        #   so it's not clear what we can pass there.
  431
+        logger.debug('executeQuery\n%s' % xquery)
  432
+        result_id = self.server.executeQuery(xquery, {})
  433
+        logger.debug('result id is %s' % result_id)
  434
+        return result_id
  435
+
  436
+    @_wrap_xmlrpc_fault
  437
+    def querySummary(self, result_id):
  438
+        """Retrieve results summary from a past query.
  439
+
  440
+        :param result_id: an integer handle returned by :meth:`executeQuery`
  441
+        :rtype: a dict describing the results
  442
+
  443
+        The returned dict has four fields:
  444
+
  445
+         * *queryTime*: processing time in milliseconds
  446
+
  447
+         * *hits*: number of hits in the result set
  448
+
  449
+         * *documents*: a list of lists. Each identifies a document and
  450
+           takes the form [`doc_id`, `doc_name`, `hits`], where:
  451
+
  452
+             * *doc_id*: an internal integer identifier for the document
  453
+             * *doc_name*: the name of the document as a string
  454
+             * *hits*: the number of hits within that document
  455
+
  456
+         * *doctype*: a list of lists. Each contains a doctype public
  457
+                      identifier and the number of hits found for this
  458
+                      doctype.
  459
+
  460
+        """
  461
+        # FIXME: This just exposes the existdb xmlrpc querySummary function.
  462
+        #   Frankly, this return is just plain ugly. We should come up with
  463
+        #   something more meaningful.
  464
+        summary = self.server.querySummary(result_id)
  465
+        logger.debug('querySummary result id %d : ' % result_id + \
  466
+                     '%(hits)s hits, query took %(queryTime)s ms' % summary)
  467
+        return summary
  468
+
  469
+    @_wrap_xmlrpc_fault
  470
+    def getHits(self, result_id):
  471
+        """Get the number of hits in a query result.
  472
+
  473
+        :param result_id: an integer handle returned by :meth:`executeQuery`
  474
+        :rtype: integer representing the number of hits
  475
+
  476
+        """
  477
+
  478
+        hits = self.server.getHits(result_id)
  479
+        logger.debug('getHits result id %d : %s' % (result_id, hits))
  480
+        return hits
  481
+
  482
+    @_wrap_xmlrpc_fault
  483
+    def retrieve(self, result_id, position, highlight=False, **options):
  484
+        """Retrieve a single result fragment.
  485
+
  486
+        :param result_id: an integer handle returned by :meth:`executeQuery`
  487
+        :param position: the result index to return
  488
+        :param highlight: enable search term highlighting in result; optional,
  489
+            defaults to False
  490
+        :rtype: the query result item as a string
  491
+
  492
+        """        
  493
+        if highlight:
  494
+            # eXist highlight modes: attributes, elements, or both
  495
+            # using elements because it seems most reasonable default
  496
+            options['highlight-matches'] = 'elements'
  497
+            # pretty-printing with eXist matches can introduce unwanted whitespace
  498
+            if 'indent' not in options:
  499
+                options['indent'] = 'no'
  500
+        logger.debug('retrieve result id %d position=%d options=%s' % (result_id, position, options))
  501
+        return self.server.retrieve(result_id, position, options)
  502
+
  503
+    @_wrap_xmlrpc_fault
  504
+    def releaseQueryResult(self, result_id):
  505
+        """Release a result set handle in the server.
  506
+
  507
+        :param result_id: an integer handle returned by :meth:`executeQuery`
  508
+
  509
+        """
  510
+        logger.debug('releaseQueryResult result id %d' % result_id)
  511
+        self.server.releaseQueryResult(result_id)
  512
+
  513
+    @_wrap_xmlrpc_fault
  514
+    def setPermissions(self, resource, permissions):
  515
+        """Set permissions on a resource in eXist.
  516
+
  517
+        :param resource: full path to a collection or document in eXist
  518
+        :param permissions: int or string permissions statement
  519
+        """
  520
+        # TODO: support setting owner, group ?
  521
+        logger.debug('setPermissions %s %s' % (resource, permissions))
  522
+        self.server.setPermissions(resource, permissions)
  523
+
  524
+    @_wrap_xmlrpc_fault
  525
+    def getPermissions(self, resource):
  526
+        """Retrieve permissions for a resource in eXist.
  527
+
  528
+        :param resource: full path to a collection or document in eXist
  529
+        :rtype: ExistPermissions
  530
+        """
  531
+        return ExistPermissions(self.server.getPermissions(resource))
  532
+
  533
+
  534
+    def loadCollectionIndex(self, collection_name, index, overwrite=True):
  535
+        """Load an index configuration for the specified collection.
  536
+        Creates the eXist system config collection if it is not already there,
  537
+        and loads the specified index config file, as per eXist collection and
  538
+        index naming conventions.
  539
+
  540
+        :param collection_name: name of the collection to be indexed
  541
+        :param index: string or file object with the document contents (as used by :meth:`load`)
  542
+        :param overwrite: set to False to disallow overwriting current index (overwrite allowed by default)        
  543
+        :rtype: boolean indicating success
  544
+        
  545
+        """
  546
+        index_collection = self._configCollectionName(collection_name)
  547
+        # FIXME: what error handling should be done at this level?
  548
+        
  549
+        # create config collection if it does not exist
  550
+        if not self.hasCollection(index_collection):
  551
+            self.createCollection(index_collection)
  552
+
  553
+        # load index content as the collection index configuration file
  554
+        return self.load(index, self._collectionIndexPath(collection_name), overwrite)
  555
+
  556
+    def removeCollectionIndex(self, collection_name):
  557
+        """Remove index configuration for the specified collection.
  558
+        If index collection has no documents or subcollections after the index
  559
+        file is removed, the configuration collection will also be removed.
  560
+        
  561
+        :param collection: name of the collection with an index to be removed
  562
+        :rtype: boolean indicating success
  563
+
  564
+        """
  565
+        # collection indexes information must be stored under system/config/db/collection_name
  566
+        index_collection = self._configCollectionName(collection_name)
  567
+        
  568
+        # remove collection.xconf in the configuration collection
  569
+        self.removeDocument(self._collectionIndexPath(collection_name))
  570
+        
  571
+        desc = self.getCollectionDescription(index_collection)
  572
+        # no documents and no sub-collections - safe to remove index collection
  573
+        if desc['collections'] == [] and desc['documents'] == []:
  574
+            self.removeCollection(index_collection)
  575
+            
  576
+        return True
  577
+
  578
+    def hasCollectionIndex(self, collection_name):
  579
+        """Check if the specified collection has an index configuration in eXist.
  580
+
  581
+        Note: according to eXist documentation, index config file does not *have*
  582
+        to be named *collection.xconf* for reasons of backward compatibility.
  583
+        This function assumes that the recommended naming conventions are followed.
  584
+        
  585
+        :param collection: name of the collection with an index to be removed
  586
+        :rtype: boolean indicating collection index is present
  587
+        
  588
+        """
  589
+        return self.hasCollection(self._configCollectionName(collection_name)) \
  590
+            and self.hasDocument(self._collectionIndexPath(collection_name))
  591
+
  592
+
  593
+    def _configCollectionName(self, collection_name):
  594
+        """Generate eXist db path to the configuration collection for a specified collection
  595
+        according to eXist collection naming conventions.
  596
+        """
  597
+        # collection indexes information must be stored under system/config/db/collection_name
  598
+        return "/db/system/config/db/" + collection_name.strip('/')
  599
+
  600
+    def _collectionIndexPath(self, collection_name):
  601
+        """Generate full eXist db path to the index configuration file for a specified
  602
+        collection according to eXist collection naming conventions.
  603
+        """
  604
+        # collection indexes information must be stored under system/config/db/collection_name
  605
+        return self._configCollectionName(collection_name) + "/collection.xconf"
  606
+
  607
+class ExistPermissions:
  608
+    "Permissions for an eXist resource - owner, group, and active permissions."
  609
+    def __init__(self, data):
  610
+        self.owner = data['owner']
  611
+        self.group = data['group']
  612
+        self.permissions = data['permissions']
  613
+
  614
+    def __str__(self):
  615
+        return "owner: %s; group: %s; permissions: %s" % (self.owner, self.group, self.permissions)
  616
+
  617
+    def __repr__(self):
  618
+        return '<%s %s>' % (self.__class__.__name__, str(self))
  619
+
  620
+
  621
+class QueryResult(xmlmap.XmlObject):
  622
+    """The results of an eXist XQuery query"""
  623
+    
  624
+    start = xmlmap.IntegerField("@start")