{ "info": { "author": "Zope Corporation and Contributors", "author_email": "zope-dev@zope.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Zope :: 3", "Intended Audience :: Developers", "License :: OSI Approved :: Zope Public License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP" ], "description": "zc.catalog is an extension to the Zope 3 catalog, Zope 3's indexing\nand search facility. zc.catalog contains a number of extensions to the\nZope 3 catalog, such as some new indexes, improved globbing and\nstemming support, and an alternative catalog implementation.\n\n\n.. contents::\n\n=========\n CHANGES\n=========\n\n3.0 (2019-03-21)\n================\n\n- Drop support for Python 3.4 as it reached its end of life.\n\n- Add support for Python 3.7 and 3.8a2.\n\n\n2.0.1 (2017-06-15)\n==================\n\n- Add Python 3 compatibility for the ``zopyx.txng3.ext`` stemmer.\n See `#4 `_.\n\n\n2.0.0 (2017-05-09)\n==================\n\n- Add support for Python 3.4, 3.5, 3.6 and PyPy. Note that the\n ``zopyx.txng3.ext`` stemmer is not available on Python 3.\n\n- Remove test dependency on zope.app.zcmlfiles and zope.app.testing,\n among others.\n\n\n1.6 (2013-07-04)\n================\n\n- Using Python's ``doctest`` module instead of deprecated\n ``zope.testing.doctest``.\n\n- Move ``zope.intid`` to dependencies.\n\n\n1.5.1 (2012-01-20)\n==================\n\n- Fix the extent catalog's `searchResults` method to work when using a\n local uid source.\n\n- Replaced a testing dependency on ``zope.app.authentication`` with\n ``zope.password``.\n\n- Removed ``zope.app.server`` test dependency.\n\n\n1.5 (2010-10-19)\n================\n\n- The package's ``configure.zcml`` does not include the browser subpackage's\n ``configure.zcml`` anymore.\n\n This, together with ``browser`` and ``test_browser`` ``extras_require``,\n decouples the browser view registrations from the main code. As a result\n projects that do not need the ZMI views to be registered are not pulling in\n the zope.app.* dependencies anymore.\n\n To enable the ZMI views for your project, you will have to do two things:\n\n * list ``zc.catalog [browser]`` as a ``install_requires``.\n\n * have your project's ``configure.zcml`` include the ``zc.catalog.browser``\n subpackage.\n\n- Only include the browser tests whenever the dependencies for the browser\n tests are available.\n\n- Python2.7 test fix.\n\n\n1.4.5 (2010-10-05)\n==================\n\n- Remove implicit test dependency on zope.app.dublincore, that was not needed\n in the first place.\n\n\n1.4.4 (2010-07-06)\n==================\n\n* Fixed test-failure happening with more recent ``mechanize`` (>=2.0).\n\n\n1.4.3 (2010-03-09)\n==================\n\n* Try to import the stemmer from the zopyx.txng3.ext package first, which\n as of 3.3.2 contains stability and memory leak fixes.\n\n\n1.4.2 (2010-01-20)\n==================\n\n* Fix missing testing dependencies when using ZTK by adding zope.login.\n\n1.4.1 (2009-02-27)\n==================\n\n* Add FieldIndex-like sorting support for the ValueIndex.\n\n* Add sorting indexes support for the NormalizationWrapper.\n\n\n1.4.0 (2009-02-07)\n==================\n\n* Fixed a typo in ValueIndex addform and addMenuItem\n\n* Use ``zope.container`` instead of ``zope.app.container``.\n\n* Use ``zope.keyreference`` instead of ``zope.app.keyreference``.\n\n* Use ``zope.intid`` instead of ``zope.app.intid``.\n\n* Use ``zope.catalog`` instead of ``zope.app.catalog``.\n\n\n1.3.0 (2008-09-10)\n==================\n\n* Added hook point to allow extent catalog to be used with local UID sources.\n\n\n1.2.0 (2007-11-03)\n==================\n\n* Updated package meta-data.\n\n* zc.catalog now can use 64-bit BTrees (\"L\") as provided by ZODB 3.8.\n\n* Albertas Agejavas (alga@pov.lt) included the new CallableWrapper, for\n when the typical Zope 3 index-by-adapter story\n (zope.app.catalog.attribute) is unnecessary trouble, and you just want\n to use a callable. See callablewrapper.txt. This can also be used for\n other indexes based on the zope.index interfaces.\n\n* Extents now have a __len__. The current implementation defers to the\n standard BTree len implementation, and shares its performance\n characteristics: it needs to wake up all of the buckets, but if all of the\n buckets are awake it is a fairly quick operation.\n\n* A simple ISelfPoulatingExtent was added to the extentcatalog module for\n which populating is a no-op. This is directly useful for catalogs that\n are used as implementation details of a component, in which objects are\n indexed explicitly by your own calls rather than by the usual subscribers.\n It is also potentially slightly useful as a base for other self-populating\n extents.\n\n\n1.1.1 (2007-3-17)\n=================\n\n'all_of' would return all results when one of the values had no results.\nReported, with test and fix provided, by Nando Quintana.\n\n\n1.1 (2007-01-06)\n================\n\nFeatures removed\n----------------\n\nThe queueing of events in the extent catalog has been entirely removed.\nSubtransactions caused significant problems to the code introduced in 1.0.\nOther solutions also have significant problems, and the win of this kind\nof queueing is qustionable. Here is a run down of the approaches rejected\nfor getting the queueing to work:\n\n* _p_invalidate (used in 1.0). Not really designed for use within a\n transaction, and reverts to last savepoint, rather than the beginning of\n the transaction. Could monkeypatch savepoints to iterate over\n precommit transaction hooks but that just smells too bad.\n\n* _p_resolveConflict. Requires application software to exist in ZEO and\n even ZRS installations, which is counter to our software deployment goals.\n Also causes useless repeated writes of empty queue to database, but that's\n not the showstopper.\n\n* vague hand-wavy ideas for separate storages or transaction managers for the\n queue. Never panned out in discussion.\n\n\n1.0 (2007-01-05)\n================\n\nBugs fixed\n----------\n\n* adjusted extentcatalog tests to trigger (and discuss and test) the queueing\n behavior.\n\n* fixed problem with excessive conflict errors due to queueing code.\n\n* updated stemming to work with newest version of TextIndexNG's extensions.\n\n* omitted stemming test when TextIndexNG's extensions are unavailable, so\n tests pass without it. Since TextIndexNG's extensions are optional, this\n seems reasonable.\n\n* removed use of zapi in extentcatalog.\n\n\n0.2 (2006-11-22)\n================\n\nFeatures added\n--------------\n\n* First release on Cheeseshop.\n\n\n=============\n Value Index\n=============\n\nThe valueindex is an index similar to, but more flexible than a standard Zope\nfield index. The index allows searches for documents that contain any of a\nset of values; between a set of values; any (non-None) values; and any empty\nvalues.\n\nAdditionally, the index supports an interface that allows examination of the\nindexed values.\n\nIt is as policy-free as possible, and is intended to be the engine for indexes\nwith more policy, as well as being useful itself.\n\nOn creation, the index has no wordCount, no documentCount, and is, as\nexpected, fairly empty.\n\n >>> from zc.catalog.index import ValueIndex\n >>> index = ValueIndex()\n >>> index.documentCount()\n 0\n >>> index.wordCount()\n 0\n >>> index.maxValue() # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError:...\n >>> index.minValue() # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError:...\n >>> list(index.values())\n []\n >>> len(index.apply({'any_of': (5,)}))\n 0\n\nThe index supports indexing any value. All values within a given index must\nsort consistently across Python versions.\n\n >>> data = {1: 'a',\n ... 2: 'b',\n ... 3: 'a',\n ... 4: 'c',\n ... 5: 'd',\n ... 6: 'c',\n ... 7: 'c',\n ... 8: 'b',\n ... 9: 'c',\n ... }\n >>> for k, v in data.items():\n ... index.index_doc(k, v)\n ...\n\nAfter indexing, the statistics and values match the newly entered content.\n\n >>> list(index.values())\n ['a', 'b', 'c', 'd']\n >>> index.documentCount()\n 9\n >>> index.wordCount()\n 4\n >>> index.maxValue()\n 'd'\n >>> index.minValue()\n 'a'\n >>> list(index.ids())\n [1, 2, 3, 4, 5, 6, 7, 8, 9]\n\nThe index supports four types of query. The first is 'any_of'. It\ntakes an iterable of values, and returns an iterable of document ids that\ncontain any of the values. The results are not weighted.\n\n >>> list(index.apply({'any_of': ('b', 'c')}))\n [2, 4, 6, 7, 8, 9]\n >>> list(index.apply({'any_of': ('b',)}))\n [2, 8]\n >>> list(index.apply({'any_of': ('d',)}))\n [5]\n >>> bool(index.apply({'any_of': (42,)}))\n False\n\nAnother query is 'any', If the key is None, all indexed document ids with any\nvalues are returned. If the key is an extent, the intersection of the extent\nand all document ids with any values is returned.\n\n >>> list(index.apply({'any': None}))\n [1, 2, 3, 4, 5, 6, 7, 8, 9]\n\n >>> from zc.catalog.extentcatalog import FilterExtent\n >>> extent = FilterExtent(lambda extent, uid, obj: True)\n >>> for i in range(15):\n ... extent.add(i, i)\n ...\n >>> list(index.apply({'any': extent}))\n [1, 2, 3, 4, 5, 6, 7, 8, 9]\n >>> limited_extent = FilterExtent(lambda extent, uid, obj: True)\n >>> for i in range(5):\n ... limited_extent.add(i, i)\n ...\n >>> list(index.apply({'any': limited_extent}))\n [1, 2, 3, 4]\n\nThe 'between' argument takes from 1 to four values. The first is the\nminimum, and defaults to None, indicating no minimum; the second is the\nmaximum, and defaults to None, indicating no maximum; the next is a boolean for\nwhether the minimum value should be excluded, and defaults to False; and the\nlast is a boolean for whether the maximum value should be excluded, and also\ndefaults to False. The results are not weighted.\n\n >>> list(index.apply({'between': ('b', 'd')}))\n [2, 4, 5, 6, 7, 8, 9]\n >>> list(index.apply({'between': ('c', None)}))\n [4, 5, 6, 7, 9]\n >>> list(index.apply({'between': ('c',)}))\n [4, 5, 6, 7, 9]\n >>> list(index.apply({'between': ('b', 'd', True, True)}))\n [4, 6, 7, 9]\n\nUsing an invalid (non-comparable on Python 3) argument to between produces\nnothing:\n\n >>> list(index.apply({'between': (1, 5)}))\n []\n\nThe 'none' argument takes an extent and returns the ids in the extent\nthat are not indexed; it is intended to be used to return docids that have\nno (or empty) values.\n\n >>> list(index.apply({'none': extent}))\n [0, 10, 11, 12, 13, 14]\n\nTrying to use more than one of these at a time generates an error.\n\n >>> index.apply({'between': (5,), 'any_of': (3,)})\n ... # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError:...\n\nUsing none of them simply returns None.\n\n >>> index.apply({}) # returns None\n\nInvalid query names cause ValueErrors.\n\n >>> index.apply({'foo': ()})\n ... # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError:...\n\nWhen you unindex a document, the searches and statistics should be updated.\n\n >>> index.unindex_doc(5)\n >>> len(index.apply({'any_of': ('d',)}))\n 0\n >>> index.documentCount()\n 8\n >>> index.wordCount()\n 3\n >>> list(index.values())\n ['a', 'b', 'c']\n >>> list(index.ids())\n [1, 2, 3, 4, 6, 7, 8, 9]\n\nReindexing a document that has a changed value also is reflected in\nsubsequent searches and statistic checks.\n\n >>> list(index.apply({'any_of': ('b',)}))\n [2, 8]\n >>> data[8] = 'e'\n >>> index.index_doc(8, data[8])\n >>> index.documentCount()\n 8\n >>> index.wordCount()\n 4\n >>> list(index.apply({'any_of': ('e',)}))\n [8]\n >>> list(index.apply({'any_of': ('b',)}))\n [2]\n >>> data[2] = 'e'\n >>> index.index_doc(2, data[2])\n >>> index.documentCount()\n 8\n >>> index.wordCount()\n 3\n >>> list(index.apply({'any_of': ('e',)}))\n [2, 8]\n >>> list(index.apply({'any_of': ('b',)}))\n []\n\nReindexing a document for which the value is now None causes it to be removed\nfrom the statistics.\n\n >>> data[3] = None\n >>> index.index_doc(3, data[3])\n >>> index.documentCount()\n 7\n >>> index.wordCount()\n 3\n >>> list(index.ids())\n [1, 2, 4, 6, 7, 8, 9]\n\nThis affects both ways of determining the ids that are and are not in the index\n(that do and do not have values).\n\n >>> list(index.apply({'any': None}))\n [1, 2, 4, 6, 7, 8, 9]\n >>> list(index.apply({'any': extent}))\n [1, 2, 4, 6, 7, 8, 9]\n >>> list(index.apply({'none': extent}))\n [0, 3, 5, 10, 11, 12, 13, 14]\n\nThe values method can be used to examine the indexed values for a given\ndocument id. For a valueindex, the \"values\" for a given doc_id will always\nhave a length of 0 or 1.\n\n >>> index.values(doc_id=8)\n ('e',)\n\nAnd the containsValue method provides a way of determining membership in the\nvalues.\n\n >>> index.containsValue('a')\n True\n >>> index.containsValue('q')\n False\n\nSorting Value Indexes\n=====================\n\nValue indexes supports sorting, just like zope.index.field.FieldIndex.\n\n >>> index.clear()\n\n >>> index.index_doc(1, 9)\n >>> index.index_doc(2, 8)\n >>> index.index_doc(3, 7)\n >>> index.index_doc(4, 6)\n >>> index.index_doc(5, 5)\n >>> index.index_doc(6, 4)\n >>> index.index_doc(7, 3)\n >>> index.index_doc(8, 2)\n >>> index.index_doc(9, 1)\n\n >>> list(index.sort([4, 2, 9, 7, 3, 1, 5]))\n [9, 7, 5, 4, 3, 2, 1]\n\nWe can also specify the ``reverse`` argument to reverse results:\n\n >>> list(index.sort([4, 2, 9, 7, 3, 1, 5], reverse=True))\n [1, 2, 3, 4, 5, 7, 9]\n\nAnd as per IIndexSort, we can limit results by specifying the ``limit``\nargument:\n\n >>> list(index.sort([4, 2, 9, 7, 3, 1, 5], limit=3))\n [9, 7, 5]\n\nIf we pass an id that is not indexed by this index, it won't be included\nin the result.\n\n >>> list(index.sort([2, 10]))\n [2]\n\n\n=========\nSet Index\n=========\n\nThe setindex is an index similar to, but more general than a traditional\nkeyword index. The values indexed are expected to be iterables; the index\nallows searches for documents that contain any of a set of values; all of a set\nof values; or between a set of values.\n\nAdditionally, the index supports an interface that allows examination of the\nindexed values.\n\nIt is as policy-free as possible, and is intended to be the engine for indexes\nwith more policy, as well as being useful itself.\n\nOn creation, the index has no wordCount, no documentCount, and is, as\nexpected, fairly empty.\n\n >>> from zc.catalog.index import SetIndex\n >>> index = SetIndex()\n >>> index.documentCount()\n 0\n >>> index.wordCount()\n 0\n >>> index.maxValue() # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError:...\n >>> index.minValue() # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError:...\n >>> list(index.values())\n []\n >>> len(index.apply({'any_of': (5,)}))\n 0\n\nThe index supports indexing any value. All values within a given index must\nsort consistently across Python versions. In practice, in Python 3\nthis means that the values need to be homogeneous.\n\n >>> data = {1: ['a', '1'],\n ... 2: ['b', 'a', '3', '4', '7'],\n ... 3: ['1'],\n ... 4: ['1', '4', 'c'],\n ... 5: ['7'],\n ... 6: ['5', '6', '7'],\n ... 7: ['c'],\n ... 8: ['1', '6'],\n ... 9: ['a', 'c', '2', '3', '4', '6',],\n ... }\n >>> for k, v in data.items():\n ... index.index_doc(k, v)\n ...\n\nAfter indexing, the statistics and values match the newly entered content.\n\n >>> list(index.values())\n ['1', '2', '3', '4', '5', '6', '7', 'a', 'b', 'c']\n >>> index.documentCount()\n 9\n >>> index.wordCount()\n 10\n >>> index.maxValue()\n 'c'\n >>> index.minValue()\n '1'\n >>> list(index.ids())\n [1, 2, 3, 4, 5, 6, 7, 8, 9]\n\nThe index supports five types of query. The first is 'any_of'. It\ntakes an iterable of values, and returns an iterable of document ids that\ncontain any of the values. The results are weighted.\n\n >>> list(index.apply({'any_of': ('b', '1', '5')}))\n [1, 2, 3, 4, 6, 8]\n >>> list(index.apply({'any_of': ('b', '1', '5')}))\n [1, 2, 3, 4, 6, 8]\n >>> list(index.apply({'any_of': ('42',)}))\n []\n >>> index.apply({'any_of': ('a', '3', '7')}) # doctest: +ELLIPSIS\n BTrees...FBucket([(1, 1.0), (2, 3.0), (5, 1.0), (6, 1.0), (9, 2.0)])\n\nUsing an invalid (non-comparable on Python 3) argument is ignored:\n\n >>> list(index.apply({'any_of': (1,)}))\n []\n >>> list(index.apply({'any_of': (1, '1')}))\n [1, 3, 4, 8]\n\nAnother query is 'any'. If the key is None, all indexed document ids with any\nvalues are returned. If the key is an extent, the intersection of the extent\nand all document ids with any values is returned.\n\n >>> list(index.apply({'any': None}))\n [1, 2, 3, 4, 5, 6, 7, 8, 9]\n\n >>> from zc.catalog.extentcatalog import FilterExtent\n >>> extent = FilterExtent(lambda extent, uid, obj: True)\n >>> for i in range(15):\n ... extent.add(i, i)\n ...\n >>> list(index.apply({'any': extent}))\n [1, 2, 3, 4, 5, 6, 7, 8, 9]\n\n >>> limited_extent = FilterExtent(lambda extent, uid, obj: True)\n >>> for i in range(5):\n ... limited_extent.add(i, i)\n ...\n >>> list(index.apply({'any': limited_extent}))\n [1, 2, 3, 4]\n\nThe 'all_of' argument also takes an iterable of values, but returns an\niterable of document ids that contains all of the values. The results are not\nweighted.\n\n >>> list(index.apply({'all_of': ('a',)}))\n [1, 2, 9]\n >>> list(index.apply({'all_of': ('3', '4')}))\n [2, 9]\n >>> list(index.apply({'all_of': (3, '4')}))\n []\n >>> list(index.apply({'all_of': ('3', 4)}))\n []\n\nThese tests illustrate two related reported errors that have been fixed.\n\n >>> list(index.apply({'all_of': ('z', '3', '4')}))\n []\n >>> list(index.apply({'all_of': ('3', '4', 'z')}))\n []\n\nThe 'between' argument takes from 1 to four values. The first is the\nminimum, and defaults to None, indicating no minimum; the second is the\nmaximum, and defaults to None, indicating no maximum; the next is a boolean for\nwhether the minimum value should be excluded, and defaults to False; and the\nlast is a boolean for whether the maximum value should be excluded, and also\ndefaults to False. The results are weighted.\n\n >>> list(index.apply({'between': ('1', '7')}))\n [1, 2, 3, 4, 5, 6, 8, 9]\n >>> list(index.apply({'between': ('b', None)}))\n [2, 4, 7, 9]\n >>> list(index.apply({'between': ('b',)}))\n [2, 4, 7, 9]\n >>> list(index.apply({'between': ('1', '7', True, True)}))\n [2, 4, 6, 8, 9]\n >>> index.apply({'between': ('2', '6')}) # doctest: +ELLIPSIS\n BTrees...FBucket([(2, 2.0), (4, 1.0), (6, 2.0), (8, 1.0), (9, 4.0)])\n\nUsing invalid (non-comparable on Python 3) arguments produces no results:\n\n >>> list(index.apply({'between': (1, 7)}))\n []\n\n\nThe 'none' argument takes an extent and returns the ids in the extent\nthat are not indexed; it is intended to be used to return docids that have\nno (or empty) values.\n\n >>> list(index.apply({'none': extent}))\n [0, 10, 11, 12, 13, 14]\n\nTrying to use more than one of these at a time generates an error.\n\n >>> index.apply({'all_of': ('5',), 'any_of': ('3',)})\n ... # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError:...\n\nUsing none of them simply returns None.\n\n >>> index.apply({}) # returns None\n\nInvalid query names cause ValueErrors.\n\n >>> index.apply({'foo': ()})\n ... # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError:...\n\nWhen you unindex a document, the searches and statistics should be updated.\n\n >>> index.unindex_doc(6)\n >>> len(index.apply({'any_of': ('5',)}))\n 0\n >>> index.documentCount()\n 8\n >>> index.wordCount()\n 9\n >>> list(index.values())\n ['1', '2', '3', '4', '6', '7', 'a', 'b', 'c']\n >>> list(index.ids())\n [1, 2, 3, 4, 5, 7, 8, 9]\n\nReindexing a document that has new additional values also is reflected in\nsubsequent searches and statistic checks.\n\n >>> data[8].extend(['5', 'c'])\n >>> index.index_doc(8, data[8])\n >>> index.documentCount()\n 8\n >>> index.wordCount()\n 10\n >>> list(index.apply({'any_of': ('5',)}))\n [8]\n >>> list(index.apply({'any_of': ('c',)}))\n [4, 7, 8, 9]\n\nThe same is true for reindexing a document with both additions and removals.\n\n >>> 2 in set(index.apply({'any_of': ('7',)}))\n True\n >>> 2 in set(index.apply({'any_of': ('2',)}))\n False\n >>> data[2].pop()\n '7'\n >>> data[2].append('2')\n >>> index.index_doc(2, data[2])\n >>> 2 in set(index.apply({'any_of': ('7',)}))\n False\n >>> 2 in set(index.apply({'any_of': ('2',)}))\n True\n\nReindexing a document that no longer has any values causes it to be removed\nfrom the statistics.\n\n >>> del data[2][:]\n >>> index.index_doc(2, data[2])\n >>> index.documentCount()\n 7\n >>> index.wordCount()\n 9\n >>> list(index.ids())\n [1, 3, 4, 5, 7, 8, 9]\n\nThis affects both ways of determining the ids that are and are not in the index\n(that do and do not have values).\n\n >>> list(index.apply({'any': None}))\n [1, 3, 4, 5, 7, 8, 9]\n >>> list(index.apply({'none': extent}))\n [0, 2, 6, 10, 11, 12, 13, 14]\n\nThe values method can be used to examine the indexed values for a given\ndocument id.\n\n >>> set(index.values(doc_id=8)) == set(['1', '5', '6', 'c'])\n True\n\nAnd the containsValue method provides a way of determining membership in the\nvalues.\n\n >>> index.containsValue('5')\n True\n >>> index.containsValue(5)\n False\n >>> index.containsValue('20')\n False\n\n\n==================\n Normalized Index\n==================\n\nThe index module provides a normalizing wrapper, a DateTime normalizer, and\na set index and a value index normalized with the DateTime normalizer.\n\nThe normalizing wrapper implements a full complement of index interfaces--\nzope.index.interfaces.IInjection, zope.index.interfaces.IIndexSearch,\nzope.index.interfaces.IStatistics, and zc.catalog.interfaces.IIndexValues--\nand delegates all of the behavior to the wrapped index, normalizing values\nusing the normalizer before the index sees them.\n\nThe normalizing wrapper currently only supports queries offered by\nzc.catalog.interfaces.ISetIndex and zc.catalog.interfaces.IValueIndex.\n\nThe normalizer interface requires the following methods, as defined in the\ninterface:\n\n def value(value):\n \"\"\"normalize or check constraints for an input value; raise an error\n or return the value to be indexed.\"\"\"\n\n def any(value, index):\n \"\"\"normalize a query value for a \"any_of\" search; return a sequence of\n values.\"\"\"\n\n def all(value, index):\n \"\"\"Normalize a query value for an \"all_of\" search; return the value\n for query\"\"\"\n\n def minimum(value, index):\n \"\"\"normalize a query value for minimum of a range; return the value for\n query\"\"\"\n\n def maximum(value, index):\n \"\"\"normalize a query value for maximum of a range; return the value for\n query\"\"\"\n\nThe DateTime normalizer performs the following normalizations and validations.\nWhenever a timezone is needed, it tries to get a request from the current\ninteraction and adapt it to zope.interface.common.idatetime.ITZInfo; failing\nthat (no request or no adapter) it uses the system local timezone.\n\n- input values must be datetimes with a timezone. They are normalized to the\n resolution specified when the normalizer is created: a resolution of 0\n normalizes values to days; a resolution of 1 to hours; 2 to minutes; 3 to\n seconds; and 4 to microseconds.\n\n- 'any' values may be timezone-aware datetimes, timezone-naive datetimes,\n or dates. dates are converted to any value from the start to the end of the\n given date in the found timezone, as described above. timezone-naive\n datetimes get the found timezone.\n\n- 'all' values may be timezone-aware datetimes or timezone-naive datetimes.\n timezone-naive datetimes get the found timezone.\n\n- 'minimum' values may be timezone-aware datetimes, timezone-naive datetimes,\n or dates. dates are converted to the start of the given date in the found\n timezone, as described above. timezone-naive datetimes get the found\n timezone.\n\n- 'maximum' values may be timezone-aware datetimes, timezone-naive datetimes,\n or dates. dates are converted to the end of the given date in the found\n timezone, as described above. timezone-naive datetimes get the found\n timezone.\n\nLet's look at the DateTime normalizer first, and then an integration of it\nwith the normalizing wrapper and the value and set indexes.\n\nThe indexed values are parsed with 'value'.\n\n >>> from zc.catalog.index import DateTimeNormalizer\n >>> n = DateTimeNormalizer() # defaults to minutes\n >>> import datetime\n >>> import pytz\n >>> naive_datetime = datetime.datetime(2005, 7, 15, 11, 21, 32, 104)\n >>> date = naive_datetime.date()\n >>> aware_datetime = naive_datetime.replace(\n ... tzinfo=pytz.timezone('US/Eastern'))\n >>> n.value(naive_datetime)\n Traceback (most recent call last):\n ...\n ValueError: This index only indexes timezone-aware datetimes.\n >>> n.value(date)\n Traceback (most recent call last):\n ...\n ValueError: This index only indexes timezone-aware datetimes.\n >>> n.value(aware_datetime) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, tzinfo=)\n\nIf we specify a different resolution, the results are different.\n\n >>> another = DateTimeNormalizer(1) # hours\n >>> another.value(aware_datetime) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 0, tzinfo=)\n\nNote that changing the resolution of an indexed value may create surprising\nresults, because queries do not change their resolution. Therefore, if you\nindex something with a datetime with a finer resolution that the normalizer's,\nthen searching for that datetime will not find the doc_id.\n\nValues in an 'any_of' query are parsed with 'any'. 'any' should return a\nsequence of values. It requires an index, which we will mock up here.\n\n >>> class DummyIndex(object):\n ... def values(self, start, stop, exclude_start, exclude_stop):\n ... assert not exclude_start and exclude_stop\n ... six_hours = datetime.timedelta(hours=6)\n ... res = []\n ... dt = start\n ... while dt < stop:\n ... res.append(dt)\n ... dt += six_hours\n ... return res\n ...\n >>> index = DummyIndex()\n >>> tuple(n.any(naive_datetime, index)) # doctest: +ELLIPSIS\n (datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>),)\n >>> tuple(n.any(aware_datetime, index)) # doctest: +ELLIPSIS\n (datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>),)\n >>> tuple(n.any(date, index)) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS\n (datetime.datetime(2005, 7, 15, 0, 0, tzinfo=<...Local...>),\n datetime.datetime(2005, 7, 15, 6, 0, tzinfo=<...Local...>),\n datetime.datetime(2005, 7, 15, 12, 0, tzinfo=<...Local...>),\n datetime.datetime(2005, 7, 15, 18, 0, tzinfo=<...Local...>))\n\nValues in an 'all_of' query are parsed with 'all'.\n\n >>> n.all(naive_datetime, index) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>)\n >>> n.all(aware_datetime, index) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>)\n >>> n.all(date, index) # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError: ...\n\nMinimum values in a 'between' query as well as those in other methods are\nparsed with 'minimum'. They also take an optional exclude boolean, which\nindicates whether the minimum is to be excluded. For datetimes, it only\nmakes a difference if you pass in a date.\n\n >>> n.minimum(naive_datetime, index) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>)\n >>> n.minimum(naive_datetime, index, exclude=True) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>)\n\n >>> n.minimum(aware_datetime, index) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>)\n >>> n.minimum(aware_datetime, index, True) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>)\n\n >>> n.minimum(date, index) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 0, 0, tzinfo=<...Local...>)\n >>> n.minimum(date, index, True) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 23, 59, 59, 999999, tzinfo=<...Local...>)\n\nMaximum values in a 'between' query as well as those in other methods are\nparsed with 'maximum'. They also take an optional exclude boolean, which\nindicates whether the maximum is to be excluded. For datetimes, it only\nmakes a difference if you pass in a date.\n\n >>> n.maximum(naive_datetime, index) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>)\n >>> n.maximum(naive_datetime, index, exclude=True) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>)\n\n >>> n.maximum(aware_datetime, index) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>)\n >>> n.maximum(aware_datetime, index, True) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>)\n\n >>> n.maximum(date, index) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 23, 59, 59, 999999, tzinfo=<...Local...>)\n >>> n.maximum(date, index, True) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 0, 0, tzinfo=<...Local...>)\n\nNow let's examine these normalizers in the context of a real index.\n\n >>> from zc.catalog.index import DateTimeValueIndex, DateTimeSetIndex\n >>> setindex = DateTimeSetIndex() # minutes resolution\n >>> data = [] # generate some data\n >>> def date_gen(\n ... start=aware_datetime,\n ... count=12,\n ... period=datetime.timedelta(hours=10)):\n ... dt = start\n ... ix = 0\n ... while ix < count:\n ... yield dt\n ... dt += period\n ... ix += 1\n ...\n >>> gen = date_gen()\n >>> count = 0\n >>> while True:\n ... try:\n ... next_ = [next(gen) for i in range(6)]\n ... except StopIteration:\n ... break\n ... data.append((count, next_[0:1]))\n ... count += 1\n ... data.append((count, next_[1:3]))\n ... count += 1\n ... data.append((count, next_[3:6]))\n ... count += 1\n ...\n >>> print(data) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE\n [(0,\n [datetime.datetime(2005, 7, 15, 11, 21, 32, 104, ...<...Eastern...>)]),\n (1,\n [datetime.datetime(2005, 7, 15, 21, 21, 32, 104, ...<...Eastern...>),\n datetime.datetime(2005, 7, 16, 7, 21, 32, 104, ...<...Eastern...>)]),\n (2,\n [datetime.datetime(2005, 7, 16, 17, 21, 32, 104, ...<...Eastern...>),\n datetime.datetime(2005, 7, 17, 3, 21, 32, 104, ...<...Eastern...>),\n datetime.datetime(2005, 7, 17, 13, 21, 32, 104, ...<...Eastern...>)]),\n (3,\n [datetime.datetime(2005, 7, 17, 23, 21, 32, 104, ...<...Eastern...>)]),\n (4,\n [datetime.datetime(2005, 7, 18, 9, 21, 32, 104, ...<...Eastern...>),\n datetime.datetime(2005, 7, 18, 19, 21, 32, 104, ...<...Eastern...>)]),\n (5,\n [datetime.datetime(2005, 7, 19, 5, 21, 32, 104, ...<...Eastern...>),\n datetime.datetime(2005, 7, 19, 15, 21, 32, 104, ...<...Eastern...>),\n datetime.datetime(2005, 7, 20, 1, 21, 32, 104, ...<...Eastern...>)])]\n >>> data_dict = dict(data)\n >>> for doc_id, value in data:\n ... setindex.index_doc(doc_id, value)\n ...\n >>> list(setindex.ids())\n [0, 1, 2, 3, 4, 5]\n >>> set(setindex.values()) == set(\n ... setindex.normalizer.value(v) for v in date_gen())\n True\n\nFor the searches, we will actually use a request and interaction, with an\nadapter that returns the Eastern timezone. This makes the examples less\ndependent on the machine that they use.\n\n >>> import zope.security.management\n >>> import zope.publisher.browser\n >>> import zope.interface.common.idatetime\n >>> import zope.publisher.interfaces\n >>> request = zope.publisher.browser.TestRequest()\n >>> zope.security.management.newInteraction(request)\n >>> from zope import interface, component\n >>> @interface.implementer(zope.interface.common.idatetime.ITZInfo)\n ... @component.adapter(zope.publisher.interfaces.IRequest)\n ... def tzinfo(req):\n ... return pytz.timezone('US/Eastern')\n ...\n >>> component.provideAdapter(tzinfo)\n >>> n.all(naive_datetime, index).tzinfo is pytz.timezone('US/Eastern')\n True\n\n >>> set(setindex.apply({'any_of': (datetime.date(2005, 7, 17),\n ... datetime.date(2005, 7, 20),\n ... datetime.date(2005, 12, 31))})) == set(\n ... (2, 3, 5))\n True\n\nNote that this search is using the normalized values.\n\n >>> set(setindex.apply({'all_of': (\n ... datetime.datetime(\n ... 2005, 7, 16, 7, 21, tzinfo=pytz.timezone('US/Eastern')),\n ... datetime.datetime(\n ... 2005, 7, 15, 21, 21, tzinfo=pytz.timezone('US/Eastern')),)})\n ... ) == set((1,))\n True\n >>> list(setindex.apply({'any': None}))\n [0, 1, 2, 3, 4, 5]\n >>> set(setindex.apply({'between': (\n ... datetime.datetime(2005, 4, 1, 12), datetime.datetime(2006, 5, 1))})\n ... ) == set((0, 1, 2, 3, 4, 5))\n True\n >>> set(setindex.apply({'between': (\n ... datetime.datetime(2005, 4, 1, 12), datetime.datetime(2006, 5, 1),\n ... True, True)})\n ... ) == set((0, 1, 2, 3, 4, 5))\n True\n\n'between' searches should deal with dates well.\n\n >>> set(setindex.apply({'between': (\n ... datetime.date(2005, 7, 16), datetime.date(2005, 7, 17))})\n ... ) == set((1, 2, 3))\n True\n >>> len(setindex.apply({'between': (\n ... datetime.date(2005, 7, 16), datetime.date(2005, 7, 17))})\n ... ) == len(setindex.apply({'between': (\n ... datetime.date(2005, 7, 15), datetime.date(2005, 7, 18),\n ... True, True)})\n ... )\n True\n\nRemoving docs works as usual.\n\n >>> setindex.unindex_doc(1)\n >>> list(setindex.ids())\n [0, 2, 3, 4, 5]\n\nValue, Minvalue and Maxvalue can take timezone-less datetimes and dates.\n\n >>> setindex.minValue() # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 15, 11, 21, ...<...Eastern...>)\n >>> setindex.minValue(datetime.date(2005, 7, 17)) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 17, 3, 21, ...<...Eastern...>)\n\n >>> setindex.maxValue() # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 20, 1, 21, ...<...Eastern...>)\n >>> setindex.maxValue(datetime.date(2005, 7, 17)) # doctest: +ELLIPSIS\n datetime.datetime(2005, 7, 17, 23, 21, ...<...Eastern...>)\n\n >>> list(setindex.values(\n ... datetime.date(2005, 7, 17), datetime.date(2005, 7, 17)))\n ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE\n [datetime.datetime(2005, 7, 17, 3, 21, ...<...Eastern...>),\n datetime.datetime(2005, 7, 17, 13, 21, ...<...Eastern...>),\n datetime.datetime(2005, 7, 17, 23, 21, ...<...Eastern...>)]\n\n >>> zope.security.management.endInteraction() # TODO put in tests tearDown\n\nSorting\n=======\n\nThe normalization wrapper provides the zope.index.interfaces.IIndexSort\ninterface if its upstream index provides it. For example, the\nDateTimeValueIndex will provide IIndexSort, because ValueIndex provides\nsorting. It will also delegate the ``sort`` method to the value index.\n\n >>> from zc.catalog.index import DateTimeValueIndex\n >>> from zope.index.interfaces import IIndexSort\n\n >>> ix = DateTimeValueIndex()\n >>> IIndexSort.providedBy(ix.index)\n True\n >>> IIndexSort.providedBy(ix)\n True\n >>> ix.sort.__self__ is ix.index\n True\n\nBut it won't work for indexes that doesn't do sorting, for example\nDateTimeSetIndex.\n\n >>> ix = DateTimeSetIndex()\n >>> IIndexSort.providedBy(ix.index)\n False\n >>> IIndexSort.providedBy(ix)\n False\n >>> ix.sort\n Traceback (most recent call last):\n ...\n AttributeError: 'SetIndex' object has no attribute 'sort'\n\n\n================\n Extent Catalog\n================\n\nAn extent catalog is very similar to a normal catalog except that it\nonly indexes items addable to its extent. The extent is both a filter\nand a set that may be merged with other result sets. The filtering is\nan additional feature we will discuss below; we'll begin with a simple\n\"do nothing\" extent that only supports the second use case.\n\nWe create the state that the text needs here.\n\n >>> import zope.keyreference.persistent\n >>> import zope.component\n >>> import zope.intid\n >>> import zope.component\n >>> import zope.interface.interfaces\n >>> import zope.component.persistentregistry\n >>> from ZODB.MappingStorage import DB\n >>> import transaction\n\n >>> zope.component.provideAdapter(\n ... zope.keyreference.persistent.KeyReferenceToPersistent,\n ... adapts=(zope.interface.Interface,))\n >>> zope.component.provideAdapter(\n ... zope.keyreference.persistent.connectionOfPersistent,\n ... adapts=(zope.interface.Interface,))\n\n >>> site_manager = None\n >>> def getSiteManager(context=None):\n ... if context is None:\n ... if site_manager is None:\n ... return zope.component.getGlobalSiteManager()\n ... else:\n ... return site_manager\n ... else:\n ... try:\n ... return zope.interface.interfaces.IComponentLookup(context)\n ... except TypeError as error:\n ... raise zope.component.ComponentLookupError(*error.args)\n ...\n >>> def setSiteManager(sm):\n ... global site_manager\n ... site_manager = sm\n ... if sm is None:\n ... zope.component.getSiteManager.reset()\n ... else:\n ... zope.component.getSiteManager.sethook(getSiteManager)\n ...\n >>> def makeRoot():\n ... db = DB()\n ... conn = db.open()\n ... root = conn.root()\n ... site_manager = root['components'] = (\n ... zope.component.persistentregistry.PersistentComponents())\n ... site_manager.__bases__ = (zope.component.getGlobalSiteManager(),)\n ... site_manager.registerUtility(\n ... zope.intid.IntIds(family=btrees_family),\n ... provided=zope.intid.interfaces.IIntIds)\n ... setSiteManager(site_manager)\n ... transaction.commit()\n ... return root\n ...\n\n >>> @zope.component.adapter(zope.interface.Interface)\n ... @zope.interface.implementer(zope.interface.interfaces.IComponentLookup)\n ... def getComponentLookup(obj):\n ... return obj._p_jar.root()['components']\n ...\n >>> zope.component.provideAdapter(getComponentLookup)\n\nTo show the extent catalog at work, we need an intid utility, an\nindex, some items to index. We'll do this within a real ZODB and a\nreal intid utility.\n\n >>> import zc.catalog\n >>> import zc.catalog.interfaces\n >>> from zc.catalog import interfaces, extentcatalog\n >>> from zope import interface, component\n >>> from zope.interface import verify\n >>> import persistent\n >>> import BTrees.IFBTree\n\n >>> root = makeRoot()\n >>> intid = zope.component.getUtility(\n ... zope.intid.interfaces.IIntIds, context=root)\n >>> TreeSet = btrees_family.IF.TreeSet\n\n >>> from zope.container.interfaces import IContained\n >>> @interface.implementer(IContained)\n ... class DummyIndex(persistent.Persistent):\n ... __parent__ = __name__ = None\n ... def __init__(self):\n ... self.uids = TreeSet()\n ... def unindex_doc(self, uid):\n ... if uid in self.uids:\n ... self.uids.remove(uid)\n ... def index_doc(self, uid, obj):\n ... self.uids.insert(uid)\n ... def clear(self):\n ... self.uids.clear()\n ... def apply(self, query):\n ... return [uid for uid in self.uids if uid <= query]\n ...\n >>> class DummyContent(persistent.Persistent):\n ... def __init__(self, name, parent):\n ... self.id = name\n ... self.__parent__ = parent\n ...\n\n >>> extent = extentcatalog.Extent(family=btrees_family)\n >>> verify.verifyObject(interfaces.IExtent, extent)\n True\n >>> root['catalog'] = catalog = extentcatalog.Catalog(extent)\n >>> verify.verifyObject(interfaces.IExtentCatalog, catalog)\n True\n >>> index = DummyIndex()\n >>> catalog['index'] = index\n >>> transaction.commit()\n\nNow we have a catalog set up with an index and an extent. We can add\nsome data to the extent:\n\n >>> matches = []\n >>> for i in range(100):\n ... c = DummyContent(i, root)\n ... root[i] = c\n ... doc_id = intid.register(c)\n ... catalog.index_doc(doc_id, c)\n ... matches.append(doc_id)\n >>> matches.sort()\n >>> sorted(extent) == sorted(index.uids) == matches\n True\n\nWe can get the size of the extent.\n\n >>> len(extent)\n 100\n\nUnindexing an object that is in the catalog should simply remove it from the\ncatalog and index as usual.\n\n >>> matches[0] in catalog.extent\n True\n >>> matches[0] in catalog['index'].uids\n True\n >>> catalog.unindex_doc(matches[0])\n >>> matches[0] in catalog.extent\n False\n >>> matches[0] in catalog['index'].uids\n False\n >>> doc_id = matches.pop(0)\n >>> sorted(extent) == sorted(index.uids) == matches\n True\n\nClearing the catalog clears both the extent and the contained indexes.\n\n >>> catalog.clear()\n >>> list(catalog.extent) == list(catalog['index'].uids) == []\n True\n\nUpdating all indexes and an individual index both also update the extent.\n\n >>> catalog.updateIndexes()\n >>> matches.insert(0, doc_id)\n >>> sorted(extent) == sorted(index.uids) == matches\n True\n\n >>> index2 = DummyIndex()\n >>> catalog['index2'] = index2\n >>> index2.__parent__ == catalog\n True\n >>> index.uids.remove(matches[0]) # to confirm that only index 2 is touched\n >>> catalog.updateIndex(index2)\n >>> sorted(extent) == sorted(index2.uids) == matches\n True\n >>> matches[0] in index.uids\n False\n >>> matches[0] in index2.uids\n True\n >>> res = index.uids.insert(matches[0])\n\nBut so why have an extent in the first place? It allows indices to\noperate against a reliable collection of the full indexed data;\ntherefore, it allows the indices in zc.catalog to perform NOT\noperations.\n\nThe extent itself provides a number of merging features to allow its\nvalues to be merged with other BTrees.IFBTree data structures. These\ninclude intersection, union, difference, and reverse difference.\nGiven an extent named 'extent' and another IFBTree data structure\nnamed 'data', intersections can be spelled \"extent & data\" or \"data &\nextent\"; unions can be spelled \"extent | data\" or \"data | extent\";\ndifferences can be spelled \"extent - data\"; and reverse differences\ncan be spelled \"data - extent\". Unions and intersections are\nweighted.\n\n >>> extent = extentcatalog.Extent(family=btrees_family)\n >>> for i in range(1, 100, 2):\n ... extent.add(i, None)\n ...\n >>> alt_set = TreeSet()\n >>> _ = alt_set.update(range(0, 166, 33)) # return value is unimportant here\n >>> sorted(alt_set)\n [0, 33, 66, 99, 132, 165]\n >>> sorted(extent & alt_set)\n [33, 99]\n >>> sorted(alt_set & extent)\n [33, 99]\n >>> sorted(extent.intersection(alt_set))\n [33, 99]\n >>> original = set(extent)\n >>> union_matches = original.copy()\n >>> union_matches.update(alt_set)\n >>> union_matches = sorted(union_matches)\n >>> sorted(alt_set | extent) == union_matches\n True\n >>> sorted(extent | alt_set) == union_matches\n True\n >>> sorted(extent.union(alt_set)) == union_matches\n True\n >>> sorted(alt_set - extent)\n [0, 66, 132, 165]\n >>> sorted(extent.rdifference(alt_set))\n [0, 66, 132, 165]\n >>> original.remove(33)\n >>> original.remove(99)\n >>> set(extent - alt_set) == original\n True\n >>> set(extent.difference(alt_set)) == original\n True\n\nWe can pass our own instantiated UID utility to extentcatalog.Catalog.\n\n >>> extent = extentcatalog.Extent(family=btrees_family)\n >>> uidutil = zope.intid.IntIds()\n >>> cat = extentcatalog.Catalog(extent, uidutil)\n >>> cat[\"index\"] = DummyIndex()\n >>> cat.UIDSource is uidutil\n True\n\n >>> cat._getUIDSource() is uidutil\n True\n\nThe ResultSet instance returned by the catalog's `searchResults` method\nuses our UID utility.\n\n >>> obj = DummyContent(43, root)\n >>> uid = uidutil.register(obj)\n >>> cat.index_doc(uid, obj)\n >>> res = cat.searchResults(index=uid)\n >>> res.uidutil is uidutil\n True\n\n >>> list(res) == [obj]\n True\n\n`searchResults` may also return None.\n\n >>> cat.searchResults() is None\n True\n\nCalling `updateIndex` and `updateIndexes` when the catalog has its uid source\nset works as well.\n\n >>> cat.clear()\n >>> uid in cat.extent\n False\n\nAll objects in the uid utility are indexed.\n\n >>> cat.updateIndexes()\n >>> uid in cat.extent\n True\n\n >>> len(cat.extent)\n 1\n\n >>> obj2 = DummyContent(44, root)\n >>> uid2 = uidutil.register(obj2)\n >>> cat.updateIndexes()\n >>> len(cat.extent)\n 2\n\n >>> uid2 in cat.extent\n True\n\n >>> uidutil.unregister(obj2)\n\n >>> cat.clear()\n >>> uid in cat.extent\n False\n >>> cat.updateIndex(cat[\"index\"])\n >>> uid in cat.extent\n True\n\nWith a self-populating extent, calling `updateIndex` or `updateIndexes` means\nonly the objects whose ids are in the extent are updated/reindexed; if present,\nthe catalog will use its uid source to look up the objects by id.\n\n >>> extent = extentcatalog.NonPopulatingExtent(family=btrees_family)\n >>> cat = extentcatalog.Catalog(extent, uidutil)\n >>> cat[\"index\"] = DummyIndex()\n\n >>> extent.add(uid, obj)\n >>> uid in cat[\"index\"].uids\n False\n\n >>> cat.updateIndexes()\n >>> uid in cat[\"index\"].uids\n True\n\n >>> cat.clear()\n >>> uid in cat[\"index\"].uids\n False\n\n >>> uid in cat.extent\n False\n\n >>> cat.extent.add(uid, obj)\n >>> cat.updateIndex(cat[\"index\"])\n >>> uid in cat[\"index\"].uids\n True\n\nUnregister the objects of the previous tests from intid utility:\n\n >>> intid = zope.component.getUtility(\n ... zope.intid.interfaces.IIntIds, context=root)\n >>> for doc_id in matches:\n ... intid.unregister(intid.queryObject(doc_id))\n\n\nCatalog with a filter extent\n============================\n\nAs discussed at the beginning of this document, extents can not only help\nwith index operations, but also act as a filter, so that a given catalog\ncan answer questions about a subset of the objects contained in the intids.\n\nThe filter extent only stores objects that match a given filter.\n\n >>> def filter(extent, uid, ob):\n ... assert interfaces.IFilterExtent.providedBy(extent)\n ... # This is an extent of objects with odd-numbered uids without a\n ... # True ignore attribute\n ... return uid % 2 and not getattr(ob, 'ignore', False)\n ...\n >>> extent = extentcatalog.FilterExtent(filter, family=btrees_family)\n >>> verify.verifyObject(interfaces.IFilterExtent, extent)\n True\n >>> root['catalog1'] = catalog = extentcatalog.Catalog(extent)\n >>> verify.verifyObject(interfaces.IExtentCatalog, catalog)\n True\n >>> index = DummyIndex()\n >>> catalog['index'] = index\n >>> transaction.commit()\n\nNow we have a catalog set up with an index and an extent. If we create\nsome content and ask the catalog to index it, only the ones that match\nthe filter will be in the extent and in the index.\n\n >>> matches = []\n >>> fails = []\n >>> i = 0\n >>> while True:\n ... c = DummyContent(i, root)\n ... root[i] = c\n ... doc_id = intid.register(c)\n ... catalog.index_doc(doc_id, c)\n ... if filter(extent, doc_id, c):\n ... matches.append(doc_id)\n ... else:\n ... fails.append(doc_id)\n ... i += 1\n ... if i > 99 and len(matches) > 4:\n ... break\n ...\n >>> matches.sort()\n >>> sorted(extent) == sorted(index.uids) == matches\n True\n\nIf a content object is indexed that used to match the filter but no longer\ndoes, it should be removed from the extent and indexes.\n\n >>> matches[0] in catalog.extent\n True\n >>> obj = intid.getObject(matches[0])\n >>> obj.ignore = True\n >>> filter(extent, matches[0], obj)\n False\n >>> catalog.index_doc(matches[0], obj)\n >>> doc_id = matches.pop(0)\n >>> doc_id in catalog.extent\n False\n >>> sorted(extent) == sorted(index.uids) == matches\n True\n\nUnindexing an object that is not in the catalog should be a no-op.\n\n >>> fails[0] in catalog.extent\n False\n >>> catalog.unindex_doc(fails[0])\n >>> fails[0] in catalog.extent\n False\n >>> sorted(extent) == sorted(index.uids) == matches\n True\n\nUpdating all indexes and an individual index both also update the extent.\n\n >>> index2 = DummyIndex()\n >>> catalog['index2'] = index2\n >>> index2.__parent__ == catalog\n True\n >>> index.uids.remove(matches[0]) # to confirm that only index 2 is touched\n >>> catalog.updateIndex(index2)\n >>> sorted(extent) == sorted(index2.uids)\n True\n >>> matches[0] in index.uids\n False\n >>> matches[0] in index2.uids\n True\n >>> res = index.uids.insert(matches[0])\n\nIf you update a single index and an object is no longer a member of the extent,\nit is removed from all indexes.\n\n >>> matches[0] in catalog.extent\n True\n >>> matches[0] in index.uids\n True\n >>> matches[0] in index2.uids\n True\n >>> obj = intid.getObject(matches[0])\n >>> obj.ignore = True\n >>> catalog.updateIndex(index2)\n >>> matches[0] in catalog.extent\n False\n >>> matches[0] in index.uids\n False\n >>> matches[0] in index2.uids\n False\n >>> doc_id = matches.pop(0)\n >>> (matches == sorted(catalog.extent) == sorted(index.uids)\n ... == sorted(index2.uids))\n True\n\n\nSelf-populating extents\n=======================\n\nAn extent may know how to populate itself; this is especially useful if\nthe catalog can be initialized with fewer items than those available in\nthe IIntIds utility that are also within the nearest Zope 3 site (the\npolicy coded in the basic Zope 3 catalog).\n\nSuch an extent must implement the `ISelfPopulatingExtent` interface,\nwhich requires two attributes. Let's use the `FilterExtent` class as a\nbase for implementing such an extent, with a method that selects content item\n0 (created and registered above)::\n\n >>> class PopulatingExtent(\n ... extentcatalog.FilterExtent,\n ... extentcatalog.NonPopulatingExtent):\n ...\n ... def populate(self):\n ... if self.populated:\n ... return\n ... self.add(intid.getId(root[0]), root[0])\n ... super(PopulatingExtent, self).populate()\n\nCreating a catalog based on this extent ignores objects in the\ndatabase already::\n\n >>> def accept_any(extent, uid, ob):\n ... return True\n\n >>> extent = PopulatingExtent(accept_any, family=btrees_family)\n >>> catalog = extentcatalog.Catalog(extent)\n >>> index = DummyIndex()\n >>> catalog['index'] = index\n >>> root['catalog2'] = catalog\n >>> transaction.commit()\n\nAt this point, our extent remains unpopulated::\n\n >>> extent.populated\n False\n\nIterating over the extent does not cause it to be automatically\npopulated::\n\n >>> list(extent)\n []\n\nCausing our new index to be filled will cause the `populate()` method\nto be called, setting the `populate` flag as a side-effect::\n\n >>> catalog.updateIndex(index)\n >>> extent.populated\n True\n\n >>> list(extent) == [intid.getId(root[0])]\n True\n\nThe index has been updated with the documents identified by the\nextent::\n\n >>> list(index.uids) == [intid.getId(root[0])]\n True\n\nUpdating the same index repeatedly will continue to use the extent as\nthe source of documents to include::\n\n >>> catalog.updateIndex(index)\n\n >>> list(extent) == [intid.getId(root[0])]\n True\n >>> list(index.uids) == [intid.getId(root[0])]\n True\n\nThe `updateIndexes()` method has a similar behavior. If we add an\nadditional index to the catalog, we see that it indexes only those\nobjects from the extent::\n\n >>> index2 = DummyIndex()\n >>> catalog['index2'] = index2\n\n >>> catalog.updateIndexes()\n\n >>> list(extent) == [intid.getId(root[0])]\n True\n >>> list(index.uids) == [intid.getId(root[0])]\n True\n >>> list(index2.uids) == [intid.getId(root[0])]\n True\n\nWhen we have fresh catalog and extent (not yet populated), we see that\n`updateIndexes()` will cause the extent to be populated::\n\n >>> extent = PopulatingExtent(accept_any, family=btrees_family)\n >>> root['catalog3'] = catalog = extentcatalog.Catalog(extent)\n >>> index1 = DummyIndex()\n >>> index2 = DummyIndex()\n >>> catalog['index1'] = index1\n >>> catalog['index2'] = index2\n >>> transaction.commit()\n\n >>> extent.populated\n False\n\n >>> catalog.updateIndexes()\n\n >>> extent.populated\n True\n\n >>> list(extent) == [intid.getId(root[0])]\n True\n >>> list(index1.uids) == [intid.getId(root[0])]\n True\n >>> list(index2.uids) == [intid.getId(root[0])]\n True\n\nWe'll make sure everything can be safely committed.\n\n >>> transaction.commit()\n >>> setSiteManager(None)\n\n\n=======\nStemmer\n=======\n\nThe stemmer uses Andreas Jung's stemmer code, which is a Python wrapper of\nM. F. Porter's Snowball project (http://snowball.tartarus.org/index.php).\nIt is designed to be used as part of a pipeline in a zope/index/text/\nlexicon, after a splitter. This enables getting the relevance ranking\nof the zope/index/text code with the splitting functionality of TextIndexNG 3.x.\n\nIt requires that the TextIndexNG extensions--specifically txngstemmer--have\nbeen compiled and installed in your Python installation. Inclusion of the\ntextindexng package is not necessary.\n\nAs of this writing (Jan 3, 2007), installing the necessary extensions can be\ndone with the following steps:\n\n- `svn co https://svn.sourceforge.net/svnroot/textindexng/extension_modules/trunk ext_mod`\n- `cd ext_mod`\n- (using the python you use for Zope) `python setup.py install`\n\nAnother approach is to simply install TextIndexNG (see\nhttp://opensource.zopyx.com/software/textindexng3)\n\nThe stemmer must be instantiated with the language for which stemming is\ndesired. It defaults to 'english'. For what it is worth, other languages\nsupported as of this writing, using the strings that the stemmer expects,\ninclude the following: 'danish', 'dutch', 'english', 'finnish', 'french',\n'german', 'italian', 'norwegian', 'portuguese', 'russian', 'spanish', and\n'swedish'.\n\nFor instance, let's build an index with an english stemmer.\n\n >>> from zope.index.text import textindex, lexicon\n >>> import zc.catalog.stemmer\n >>> lex = lexicon.Lexicon(\n ... lexicon.Splitter(), lexicon.CaseNormalizer(),\n ... lexicon.StopWordRemover(), zc.catalog.stemmer.Stemmer('english'))\n >>> ix = textindex.TextIndex(lex)\n >>> data = [\n ... (0, 'consigned consistency consoles the constables'),\n ... (1, 'knaves kneeled and knocked knees, knowing no knights')]\n >>> for doc_id, text in data:\n ... ix.index_doc(doc_id, text)\n ...\n >>> list(ix.apply('consoling a constable'))\n [0]\n >>> list(ix.apply('knightly kneel'))\n [1]\n\nNote that query terms with globbing characters are not stemmed.\n\n >>> list(ix.apply('constables*'))\n []\n\n\n=======================\nSupport for legacy data\n=======================\n\nPrior to the introduction of btree \"families\" and the\n``BTrees.Interfaces.IBTreeFamily`` interface, the indexes defined by\nthe ``zc.catalog.index`` module used the instance attributes\n``btreemodule`` and ``IOBTree``, initialized in the constructor, and\nthe ``BTreeAPI`` property. These are replaced by the ``family``\nattribute in the current implementation.\n\nThis is a white-box test that verifies that the supported values in\nexisting data structures (loaded from pickles) can be used effectively\nwith the current implementation.\n\nThere are two supported sets of values; one for 32-bit btrees::\n\n >>> import BTrees.IOBTree\n\n >>> legacy32 = {\n ... \"btreemodule\": \"BTrees.IFBTree\",\n ... \"IOBTree\": BTrees.IOBTree.IOBTree,\n ... }\n\nand another for 64-bit btrees::\n\n >>> import BTrees.LOBTree\n\n >>> legacy64 = {\n ... \"btreemodule\": \"BTrees.LFBTree\",\n ... \"IOBTree\": BTrees.LOBTree.LOBTree,\n ... }\n\nIn each case, actual legacy structures will also include index\nstructures that match the right integer size::\n\n >>> import BTrees.OOBTree\n >>> import BTrees.Length\n\n >>> legacy32[\"values_to_documents\"] = BTrees.OOBTree.OOBTree()\n >>> legacy32[\"documents_to_values\"] = BTrees.IOBTree.IOBTree()\n >>> legacy32[\"documentCount\"] = BTrees.Length.Length(0)\n >>> legacy32[\"wordCount\"] = BTrees.Length.Length(0)\n\n >>> legacy64[\"values_to_documents\"] = BTrees.OOBTree.OOBTree()\n >>> legacy64[\"documents_to_values\"] = BTrees.LOBTree.LOBTree()\n >>> legacy64[\"documentCount\"] = BTrees.Length.Length(0)\n >>> legacy64[\"wordCount\"] = BTrees.Length.Length(0)\n\nWhat we want to do is verify that the ``family`` attribute is properly\ncomputed for instances loaded from legacy data, and ensure that the\nstructure is updated cleanly without providing cause for a read-only\ntransaction to become a write-transaction. We'll need to create\ninstances that conform to the old data structures, pickle them, and\nshow that unpickling them produces instances that use the correct\nfamilies.\n\nLet's create new instances, and force the internal data to match the\nold structures::\n\n >>> import pickle\n >>> import zc.catalog.index\n\n >>> vi32 = zc.catalog.index.ValueIndex()\n >>> vi32.__dict__ = legacy32.copy()\n >>> legacy32_pickle = pickle.dumps(vi32)\n\n >>> vi64 = zc.catalog.index.ValueIndex()\n >>> vi64.__dict__ = legacy64.copy()\n >>> legacy64_pickle = pickle.dumps(vi64)\n\nNow, let's unpickle these structures and verify the structures. We'll\nstart with the 32-bit variety::\n\n >>> vi32 = pickle.loads(legacy32_pickle)\n\n >>> vi32.__dict__[\"btreemodule\"]\n 'BTrees.IFBTree'\n >>> vi32.__dict__[\"IOBTree\"]\n \n\n >>> \"family\" in vi32.__dict__\n False\n\n >>> vi32._p_changed\n False\n\nThe ``family`` property returns the ``BTrees.family32`` singleton::\n\n >>> vi32.family is BTrees.family32\n True\n\nOnce accessed, the legacy values have been cleaned out from the\ninstance dictionary::\n\n >>> \"btreemodule\" in vi32.__dict__\n False\n >>> \"IOBTree\" in vi32.__dict__\n False\n >>> \"BTreeAPI\" in vi32.__dict__\n False\n\nAccessing these attributes as attributes provides the proper values\nanyway::\n\n >>> vi32.btreemodule\n 'BTrees.IFBTree'\n >>> vi32.IOBTree\n \n >>> vi32.BTreeAPI\n \n\nEven though the instance dictionary has been cleaned up, the change\nflag hasn't been set. This is handled this way to avoid turning a\nread-only transaction into a write-transaction::\n\n >>> vi32._p_changed\n False\n\nThe 64-bit variation provides equivalent behavior::\n\n >>> vi64 = pickle.loads(legacy64_pickle)\n\n >>> vi64.__dict__[\"btreemodule\"]\n 'BTrees.LFBTree'\n >>> vi64.__dict__[\"IOBTree\"]\n \n\n >>> \"family\" in vi64.__dict__\n False\n\n >>> vi64._p_changed\n False\n\n >>> vi64.family is BTrees.family64\n True\n\n >>> \"btreemodule\" in vi64.__dict__\n False\n >>> \"IOBTree\" in vi64.__dict__\n False\n >>> \"BTreeAPI\" in vi64.__dict__\n False\n\n >>> vi64.btreemodule\n 'BTrees.LFBTree'\n >>> vi64.IOBTree\n \n >>> vi64.BTreeAPI\n \n\n >>> vi64._p_changed\n False\n\nNow, if we have a legacy structure and explicitly set the ``family``\nattribute, the old data structures will be cleared and replaced with\nthe new structure. If the object is associated with a data manager,\nthe changed flag will be set as well::\n\n >>> class DataManager(object):\n ... def register(self, ob):\n ... pass\n\n >>> vi64 = pickle.loads(legacy64_pickle)\n >>> vi64._p_jar = DataManager()\n >>> vi64.family = BTrees.family64\n\n >>> vi64._p_changed\n True\n\n >>> \"btreemodule\" in vi64.__dict__\n False\n >>> \"IOBTree\" in vi64.__dict__\n False\n >>> \"BTreeAPI\" in vi64.__dict__\n False\n\n >>> \"family\" in vi64.__dict__\n True\n >>> vi64.family is BTrees.family64\n True\n\n >>> vi64.btreemodule\n 'BTrees.LFBTree'\n >>> vi64.IOBTree\n \n >>> vi64.BTreeAPI\n \n\n\n=======\nGlobber\n=======\n\nThe globber takes a query and makes any term that isn't already a glob into\nsomething that ends in a star. It was originally envisioned as a *very* low-\nrent stemming hack. The author now questions its value, and hopes that the new\nstemming pipeline option can be used instead. Nonetheless, here is an example\nof it at work.\n\n >>> from zope.index.text import textindex\n >>> index = textindex.TextIndex()\n >>> lex = index.lexicon\n >>> from zc.catalog import globber\n >>> globber.glob('foo bar and baz or (b?ng not boo)', lex)\n '(((foo* and bar*) and baz*) or (b?ng and not boo*))'\n\n\n================\nCallable Wrapper\n================\n\nIf we want to index some value that is easily derivable from a\ndocument, we have to define an interface with this value as an\nattribute, and create an adapter that calculates this value and\nimplements this interface. All this is too much hassle if the want to\nstore a single easily derivable value. CallableWrapper solves this\nproblem, by converting the document to the indexed value with a\ncallable converter.\n\nHere's a contrived example. Suppose we have cars that know their\nmileage expressed in miles per gallon, but we want to index their\neconomy in litres per 100 km.\n\n >>> class Car(object):\n ... def __init__(self, mpg):\n ... self.mpg = mpg\n\n >>> def mpg2lp100(car):\n ... return 100.0/(1.609344/3.7854118 * car.mpg)\n\nLet's create an index that would index cars' l/100 km rating.\n\n >>> from zc.catalog import index, catalogindex\n >>> idx = catalogindex.CallableWrapper(index.ValueIndex(), mpg2lp100)\n\nLet's add a couple of cars to the index!\n\n >>> hummer = Car(10.0)\n >>> beamer = Car(22.0)\n >>> civic = Car(45.0)\n\n >>> idx.index_doc(1, hummer)\n >>> idx.index_doc(2, beamer)\n >>> idx.index_doc(3, civic)\n\nThe indexed values should be the converted l/100 km ratings:\n\n >>> list(idx.values()) # doctest: +ELLIPSIS\n [5.22699076283393..., 10.691572014887601, 23.521458432752723]\n\nWe can query for cars that consume fuel in some range:\n\n >>> list(idx.apply({'between': (5.0, 7.0)}))\n [3]\n\n\n============================\n zc.catalog Browser Support\n============================\n\nThe zc.catalog.browser package adds simple TTW addition/inspection for SetIndex\nand ValueIndex.\n\nFirst, we need a browser so we can test the web UI.\n\n >>> from zope.testbrowser.wsgi import Browser\n >>> browser = Browser()\n >>> browser.handleErrors = False\n >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')\n >>> browser.addHeader('Accept-Language', 'en-US')\n >>> browser.open('http://localhost/')\n\nNow we need to add the catalog that these indexes are going to reside within.\n\n >>> browser.open('http://localhost/++etc++site/default/@@+/')\n >>> browser.getControl('Catalog').click()\n >>> browser.getControl(name='id').value = 'catalog'\n >>> browser.getControl('Add').click()\n\n\nSetIndex\n========\n\nAdd the SetIndex to the catalog.\n\n >>> browser.open(browser.getLink('Add').url + '/')\n >>> browser.getControl('Set Index').click()\n >>> browser.getControl(name='id').value = 'set_index'\n >>> browser.getControl('Add').click()\n\nThe add form needs values for what interface to adapt candidate objects to, and\nwhat field name to use, and whether-or-not that field is a callable. (We'll use\na simple interfaces for demonstration purposes, it's not really significant.)\n\n >>> browser.getControl('Interface', index=0).displayValue = [\n ... 'zope.size.interfaces.ISized']\n >>> browser.getControl('Field Name').value = 'sizeForDisplay'\n >>> browser.getControl('Field Callable').click()\n >>> browser.getControl(name='add_input_name').value = 'set_index'\n >>> browser.getControl('Add').click()\n\nNow we can look at the index and see how is is configured.\n\n >>> browser.getLink('set_index').click()\n >>> print(browser.contents)\n <...\n ...Interface...zope.size.interfaces.ISized...\n ...Field Name...sizeForDisplay...\n ...Field Callable...True...\n\nWe need to go back to the catalog so we can add a different index.\n\n >>> browser.open('/++etc++site/default/catalog/@@contents.html')\n\n\nValueIndex\n==========\n\nAdd the ValueIndex to the catalog.\n\n >>> browser.open(browser.getLink('Add').url + '/')\n >>> browser.getControl('Value Index').click()\n >>> browser.getControl(name='id').value = 'value_index'\n >>> browser.getControl('Add').click()\n\nThe add form needs values for what interface to adapt candidate objects to, and\nwhat field name to use, and whether-or-not that field is a callable. (We'll use\na simple interfaces for demonstration purposes, it's not really significant.)\n\n >>> browser.getControl('Interface', index=0).displayValue = [\n ... 'zope.size.interfaces.ISized']\n >>> browser.getControl('Field Name').value = 'sizeForDisplay'\n >>> browser.getControl('Field Callable').click()\n >>> browser.getControl(name='add_input_name').value = 'value_index'\n >>> browser.getControl('Add').click()\n\nNow we can look at the index and see how is is configured.\n\n >>> browser.getLink('value_index').click()\n >>> print(browser.contents)\n <...\n ...Interface...zope.size.interfaces.ISized...\n ...Field Name...sizeForDisplay...\n ...Field Callable...True...\n\n\n", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://github.com/zopefoundation/zc.catalog", "keywords": "zope3 i18n date time duration catalog index", "license": "ZPL 2.1", "maintainer": "", "maintainer_email": "", "name": "zc.catalog", "package_url": "https://pypi.org/project/zc.catalog/", "platform": "", "project_url": "https://pypi.org/project/zc.catalog/", "project_urls": { "Homepage": "http://github.com/zopefoundation/zc.catalog" }, "release_url": "https://pypi.org/project/zc.catalog/3.0/", "requires_dist": [ "BTrees (>=4.4.1)", "persistent", "pytz", "setuptools", "zope.catalog (>=4.2.1)", "zope.component (>=4.3.0)", "zope.container (>=4.1.0)", "zope.i18nmessageid (>=4.1.0)", "zope.index (>=4.3.0)", "zope.interface (>=4.4.0)", "zope.intid (>=4.2.0)", "zope.publisher (>=3.12)", "zope.schema (>=4.4.2)", "zope.security (>=4.1.0)", "zope.app.form; extra == 'browser'", "zope.browsermenu; extra == 'browser'", "zopyx.txng3.ext (>=2.0.0); extra == 'stemmer'", "ZODB; extra == 'test'", "beautifulsoup4; extra == 'test'", "zope.annotation; extra == 'test'", "zope.app.appsetup; extra == 'test'", "zope.app.basicskin; extra == 'test'", "zope.app.catalog; extra == 'test'", "zope.app.catalog; extra == 'test'", "zope.app.component; extra == 'test'", "zope.app.container; extra == 'test'", "zope.app.form; extra == 'test'", "zope.app.publisher (>=4.0); extra == 'test'", "zope.app.rotterdam; extra == 'test'", "zope.app.schema (>=4.0); extra == 'test'", "zope.app.wsgi; extra == 'test'", "zope.browsermenu; extra == 'test'", "zope.browserpage; extra == 'test'", "zope.browserresource; extra == 'test'", "zope.dottedname; extra == 'test'", "zope.keyreference; extra == 'test'", "zope.login; extra == 'test'", "zope.password; extra == 'test'", "zope.principalannotation; extra == 'test'", "zope.principalregistry; extra == 'test'", "zope.securitypolicy; extra == 'test'", "zope.testbrowser (>=5.2); extra == 'test'", "zope.testing; extra == 'test'", "zope.testrunner; extra == 'test'", "zopyx.txng3.ext (>=2.0.0); extra == 'test'" ], "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "summary": "Extensions to the Zope 3 Catalog", "version": "3.0" }, "last_serial": 4969549, "releases": { "0.2": [ { "comment_text": "", "digests": { "md5": "769b6ed369d75f5f9e79bdbc48a44262", "sha256": "3a6c8a9a6904d44828c85ec2918cbe8c4eb15f143ba45513c28a1ee4bd3a85d6" }, "downloads": -1, "filename": "zc.catalog-0.2-py2.4.egg", "has_sig": false, "md5_digest": "769b6ed369d75f5f9e79bdbc48a44262", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 48448, "upload_time": "2006-09-22T16:25:19", "url": "https://files.pythonhosted.org/packages/9a/21/f4e5659227b71bda30a0a37b8b4a956790d1e64ad9ed8e36adaa5fd45e88/zc.catalog-0.2-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "a78affe26638fffd84713be893cbab6b", "sha256": "6261afb9fbb6dd60f95aeb489660e3ef30117d81199831862ea6b68609d3a706" }, "downloads": -1, "filename": "zc.catalog-0.2.tar.gz", "has_sig": false, "md5_digest": "a78affe26638fffd84713be893cbab6b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 21251, "upload_time": "2006-09-22T16:25:12", "url": "https://files.pythonhosted.org/packages/e9/c8/fd211c3031542d88efd910ed61bdf446e04fb54b027a00f1a22c388b5448/zc.catalog-0.2.tar.gz" } ], "1.0": [ { "comment_text": "", "digests": { "md5": "5ebf979d735178dbf852238c2753c878", "sha256": "1ed291f02ce7da5d9369fb55de513a4c886327522a23f16f888af4744ac1bf3b" }, "downloads": -1, "filename": "zc.catalog-1.0-py2.4.egg", "has_sig": false, "md5_digest": "5ebf979d735178dbf852238c2753c878", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 53360, "upload_time": "2007-01-05T05:42:10", "url": "https://files.pythonhosted.org/packages/f4/fa/8e95135484c9260c5ed0962ab3b9b27c9163fe034152fe831fd0ca73b455/zc.catalog-1.0-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "dadbb279520b3784c439984904888c93", "sha256": "30b807b8458965a274e44d52570bcdd670b7cb115d19173a9c254500019f1e68" }, "downloads": -1, "filename": "zc.catalog-1.0.tar.gz", "has_sig": false, "md5_digest": "dadbb279520b3784c439984904888c93", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26789, "upload_time": "2007-01-05T05:42:09", "url": "https://files.pythonhosted.org/packages/e3/b7/d091d00fb01e73938256262048abc5a826a0011aea97201c50491c72a216/zc.catalog-1.0.tar.gz" } ], "1.1": [ { "comment_text": "", "digests": { "md5": "958ea296315949942b551d983f400e43", "sha256": "91985d1dbf2691625df51603aabec8926efea20ed4d961f0263647ce1af341c3" }, "downloads": -1, "filename": "zc.catalog-1.1-py2.4.egg", "has_sig": false, "md5_digest": "958ea296315949942b551d983f400e43", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 51204, "upload_time": "2007-01-06T02:34:15", "url": "https://files.pythonhosted.org/packages/68/f7/476fbc7f58609e1d3f9a08c861a03478872550599ae218b0ed851dd52790/zc.catalog-1.1-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "d0df1f3a50a08dc6278e35bdfb007bf3", "sha256": "cdecb314eeef366077626edf1e0b517f2320f9d55e9fbf048ff6c830f63e754c" }, "downloads": -1, "filename": "zc.catalog-1.1.tar.gz", "has_sig": false, "md5_digest": "d0df1f3a50a08dc6278e35bdfb007bf3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 25982, "upload_time": "2007-01-06T02:34:13", "url": "https://files.pythonhosted.org/packages/99/58/6d998a2418047e8a5545194cac36221f8fcafef83bab1fa55eeeb501ecf2/zc.catalog-1.1.tar.gz" } ], "1.1.1": [ { "comment_text": "", "digests": { "md5": "f36d32c269d8d5c95dd32bd5a7717890", "sha256": "cdb1c2431ac03020f28857b13cd69e68ed1332298f947ecc593d5c77155bfeb5" }, "downloads": -1, "filename": "zc.catalog-1.1.1-py2.4.egg", "has_sig": false, "md5_digest": "f36d32c269d8d5c95dd32bd5a7717890", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 51407, "upload_time": "2007-03-18T01:56:44", "url": "https://files.pythonhosted.org/packages/5c/51/99257e31346fe811bd063edcc08d496410fdd1d270938e5ad4d453d3f183/zc.catalog-1.1.1-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "d68630ac83147dfa926a97efdbf498a4", "sha256": "63e15a640a91ac1ab144314096fb34f4e021be5096b505891d245b4ea6d40024" }, "downloads": -1, "filename": "zc.catalog-1.1.1.tar.gz", "has_sig": false, "md5_digest": "d68630ac83147dfa926a97efdbf498a4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26191, "upload_time": "2007-03-18T01:56:45", "url": "https://files.pythonhosted.org/packages/57/b6/1f115a244fed2fd4b353b5f255e5d01e1d257ff2b68cbb05fe386e09941e/zc.catalog-1.1.1.tar.gz" } ], "1.2.0": [ { "comment_text": "", "digests": { "md5": "82e823ccaf8e3d34551b32d2a76e9995", "sha256": "82b1ba18205e06832aa87362b47dabcb1cd3a9d266c817918b195e416c028a4f" }, "downloads": -1, "filename": "zc.catalog-1.2.0.tar.gz", "has_sig": false, "md5_digest": "82e823ccaf8e3d34551b32d2a76e9995", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 55951, "upload_time": "2007-11-03T18:32:55", "url": "https://files.pythonhosted.org/packages/c3/ac/f27ce5a96a31de05c8afc614a81c5634d1dc2376f2532fbef5a5e98e9241/zc.catalog-1.2.0.tar.gz" } ], "1.2b": [ { "comment_text": "", "digests": { "md5": "a1cbae8ad5e6c158ac78a3701141ab5a", "sha256": "1cff40e867243ca80c974eb4b6c035a3e2f53e394932c16ad8e8378a1bd321ed" }, "downloads": -1, "filename": "zc.catalog-1.2b-py2.4.egg", "has_sig": false, "md5_digest": "a1cbae8ad5e6c158ac78a3701141ab5a", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 60535, "upload_time": "2007-07-03T16:16:48", "url": "https://files.pythonhosted.org/packages/d3/a6/b8e880765ea11db076fcb592bd6bfebdd4589058232aeecfeb77cd1fda2a/zc.catalog-1.2b-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "21a961bed84144ffa9cf098acd3f2960", "sha256": "dab6cfa543f207936e2cf5ae703034a1ddca912de6d2c7ef8baf235ef7c3ba33" }, "downloads": -1, "filename": "zc.catalog-1.2b.tar.gz", "has_sig": false, "md5_digest": "21a961bed84144ffa9cf098acd3f2960", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32260, "upload_time": "2007-07-03T16:16:53", "url": "https://files.pythonhosted.org/packages/ae/f2/46cac02f88f0a3bf6cb1bd7a39ac6ec9b98551b94d5b883d47b6b668989b/zc.catalog-1.2b.tar.gz" } ], "1.2dev-r74688": [ { "comment_text": "", "digests": { "md5": "53b4546717e7b4dae93d38c9eed288b2", "sha256": "a8d4e504f76d0b19bbe887fd158a6684b87c18d16e6e0fbd61b5497dce4a4f09" }, "downloads": -1, "filename": "zc.catalog-1.2dev_r74688-py2.4.egg", "has_sig": false, "md5_digest": "53b4546717e7b4dae93d38c9eed288b2", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 56993, "upload_time": "2007-04-23T22:01:17", "url": "https://files.pythonhosted.org/packages/33/03/0e54b27eec77c837b2826a0ead8c39d0d47e1808972378f9f4da5b77abf5/zc.catalog-1.2dev_r74688-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "a814e780d01988d54d89956bc62c45f8", "sha256": "35422c803a77342e7e7b729953f22c5e3824cb1e7e66fa7582caa20b1f8cd468" }, "downloads": -1, "filename": "zc.catalog-1.2dev-r74688.tar.gz", "has_sig": false, "md5_digest": "a814e780d01988d54d89956bc62c45f8", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 28790, "upload_time": "2007-04-23T22:01:18", "url": "https://files.pythonhosted.org/packages/13/bd/8d2cde98bb852388390a2c13fa3e51fba8a906f394b9f9b460d46819b777/zc.catalog-1.2dev-r74688.tar.gz" } ], "1.3.0": [ { "comment_text": "", "digests": { "md5": "9d7628da0187f9e286a8443fd0c3ddf9", "sha256": "745afb526fedc5af64c7e9b38fa9be8a9db4cf1c61ee37730efcc65b66c24661" }, "downloads": -1, "filename": "zc.catalog-1.3.0.tar.gz", "has_sig": false, "md5_digest": "9d7628da0187f9e286a8443fd0c3ddf9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 57420, "upload_time": "2008-09-10T19:13:24", "url": "https://files.pythonhosted.org/packages/93/a0/3a2aa7c9985f446f76d91bc33b202da9e0812a91f0d9229e9f0e0d8e44e3/zc.catalog-1.3.0.tar.gz" } ], "1.3.1": [], "1.4.0": [ { "comment_text": "", "digests": { "md5": "3864257745d20ff4489aa4d0d3b9a074", "sha256": "2113a44f45722e4550cffacc79ca6c887ee3d4425a5d886429e36e842aa5fe7b" }, "downloads": -1, "filename": "zc.catalog-1.4.0.tar.gz", "has_sig": false, "md5_digest": "3864257745d20ff4489aa4d0d3b9a074", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 56859, "upload_time": "2009-02-07T15:49:36", "url": "https://files.pythonhosted.org/packages/ea/a3/f4093197ddf7ee34f326998cfb97033e794bbc4d46581ccdb0f3dee2758e/zc.catalog-1.4.0.tar.gz" } ], "1.4.1": [ { "comment_text": "", "digests": { "md5": "d9c75877b520922e87a6ea3a3269a13c", "sha256": "6717734b1b9ec862a5738017c19a85520c04ffd94173fea0ccd4a73a648669a1" }, "downloads": -1, "filename": "zc.catalog-1.4.1.tar.gz", "has_sig": false, "md5_digest": "d9c75877b520922e87a6ea3a3269a13c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 60148, "upload_time": "2009-02-27T15:46:25", "url": "https://files.pythonhosted.org/packages/60/86/7511b1c03de327e16c16694d33ddbea21b9cc4cb910f7610d86d5425f9a3/zc.catalog-1.4.1.tar.gz" } ], "1.4.2": [ { "comment_text": "", "digests": { "md5": "6804d78368855a93ab5b680396f518df", "sha256": "26d3e13aa664fc83b9e20a7fcdda3cc80b06aa8736417249374c6ac3efd25666" }, "downloads": -1, "filename": "zc.catalog-1.4.2.tar.gz", "has_sig": false, "md5_digest": "6804d78368855a93ab5b680396f518df", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 59226, "upload_time": "2010-01-20T15:18:20", "url": "https://files.pythonhosted.org/packages/1c/33/68d2d839398d96a7ed0db0f1c25b3ac8c8c518374f2facbaecfa039ebda2/zc.catalog-1.4.2.tar.gz" } ], "1.4.3": [ { "comment_text": "", "digests": { "md5": "e8b55bfc89621a73143af9de9580aa91", "sha256": "6a0a8d9843beac8fabc8c24c304fd834032bc2c985c0ca1f2df3aff2d7ef571c" }, "downloads": -1, "filename": "zc.catalog-1.4.3.tar.gz", "has_sig": false, "md5_digest": "e8b55bfc89621a73143af9de9580aa91", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 60752, "upload_time": "2010-03-09T17:52:18", "url": "https://files.pythonhosted.org/packages/ab/db/26f786bf133a9171a00580792639b4db725bc6e90f9b0263d8a16ad27726/zc.catalog-1.4.3.tar.gz" } ], "1.4.4": [ { "comment_text": "", "digests": { "md5": "7c079e1ecc8175c92e1e6ee160ffd46e", "sha256": "b67a461a05adcaaa002354e8f4b7729d407773c90a7840fbde39a97120a3593c" }, "downloads": -1, "filename": "zc.catalog-1.4.4.tar.gz", "has_sig": false, "md5_digest": "7c079e1ecc8175c92e1e6ee160ffd46e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 61128, "upload_time": "2010-07-06T15:37:39", "url": "https://files.pythonhosted.org/packages/b0/81/0e52a43bdcfba565b77403209d02aaaabb9a6825335c536e5f7d58b03044/zc.catalog-1.4.4.tar.gz" } ], "1.4.5": [ { "comment_text": "", "digests": { "md5": "1f73c2eff789d679a5b363c0d699acdf", "sha256": "9650a26554aa871cb7be3e4c589015485dbfaeee26903419d42a391f0454a39b" }, "downloads": -1, "filename": "zc.catalog-1.4.5.tar.gz", "has_sig": false, "md5_digest": "1f73c2eff789d679a5b363c0d699acdf", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 62811, "upload_time": "2010-10-05T13:31:50", "url": "https://files.pythonhosted.org/packages/9f/b0/0b36b34d738f29425812bf5d7ba48d5931f00f3cdd149f34a846afa7565b/zc.catalog-1.4.5.tar.gz" } ], "1.5": [ { "comment_text": "", "digests": { "md5": "1dacdac2b3f827e64f2c6e47a19e583a", "sha256": "be02a6cecdc7758fb706f2924ecf81e937d4d7d27d5b41badd51722849c05b60" }, "downloads": -1, "filename": "zc.catalog-1.5.tar.gz", "has_sig": false, "md5_digest": "1dacdac2b3f827e64f2c6e47a19e583a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 63815, "upload_time": "2010-10-19T09:46:42", "url": "https://files.pythonhosted.org/packages/d8/a0/051bc4862b6c880924534014aca8c23ef8cb098debd289f4295668d27f33/zc.catalog-1.5.tar.gz" } ], "1.5.1": [ { "comment_text": "", "digests": { "md5": "f115b785aa19723e261639e1e23db0e2", "sha256": "acb74b856386d858035b25d765ff0ce602c715b5a777e50b1e77a9f408f35d34" }, "downloads": -1, "filename": "zc.catalog-1.5.1.tar.gz", "has_sig": false, "md5_digest": "f115b785aa19723e261639e1e23db0e2", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 63287, "upload_time": "2012-01-20T20:20:51", "url": "https://files.pythonhosted.org/packages/6d/ac/7338488ff447474c1ca90b9e2eb963f3a69a14ef14a23743d2565c9e1805/zc.catalog-1.5.1.tar.gz" } ], "1.6": [ { "comment_text": "", "digests": { "md5": "e4fac44d79ab29575d74fbdb5f011b8c", "sha256": "2186f2522b2f68ee515d659cceb980c68d7bfe4b9faa5c7a8bbfabf885c99b01" }, "downloads": -1, "filename": "zc.catalog-1.6.tar.gz", "has_sig": false, "md5_digest": "e4fac44d79ab29575d74fbdb5f011b8c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 65497, "upload_time": "2013-07-04T17:06:19", "url": "https://files.pythonhosted.org/packages/ca/75/ae29732cefda3a6f9d42c0b04aa47fafb4b36b516eb82795ef791a25fe4d/zc.catalog-1.6.tar.gz" } ], "2.0.0": [ { "comment_text": "", "digests": { "md5": "aefa2c470a53f4aaf236f01c490caf8b", "sha256": "1af66b75e1ade5cba21a3b4b77744d15eb66382fa73cd6dd0a58044b1b1b7ead" }, "downloads": -1, "filename": "zc.catalog-2.0.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "aefa2c470a53f4aaf236f01c490caf8b", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 73789, "upload_time": "2017-05-09T16:13:45", "url": "https://files.pythonhosted.org/packages/91/a7/3a05acfc31edc534fa6ab80b79d2e3afb595dd4424a74594781172f2dcca/zc.catalog-2.0.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "460d5866169e11b0cbcfd38c2bc6981e", "sha256": "33ab148817fa1754640da9ad272a8063fa0e476491de17e70a99a4b0323bcbc5" }, "downloads": -1, "filename": "zc.catalog-2.0.0.tar.gz", "has_sig": false, "md5_digest": "460d5866169e11b0cbcfd38c2bc6981e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 65842, "upload_time": "2017-05-09T16:13:48", "url": "https://files.pythonhosted.org/packages/86/3f/5027436abe8170cb3e45f3ad6fcf5d3496437905d01e87883994d0f36a03/zc.catalog-2.0.0.tar.gz" } ], "2.0.1": [ { "comment_text": "", "digests": { "md5": "54ba32371efb0a00c6c89fc37430bb15", "sha256": "03ba0cedf19d5644c2eeb2ac8bac7a08631dfc1fd9ab3e66417516d88f90503c" }, "downloads": -1, "filename": "zc.catalog-2.0.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "54ba32371efb0a00c6c89fc37430bb15", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 73881, "upload_time": "2017-06-15T11:02:39", "url": "https://files.pythonhosted.org/packages/12/da/f1a72cae78ae6d50ea92aacde7564b7dac8c1455304648f2146ee5a9e28f/zc.catalog-2.0.1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "0466a2bfb15c9e782b2091c1c0d7c359", "sha256": "80818e564b87f9b90ea076153c94020bc86dc7552ac9e945bf0556f015544bf8" }, "downloads": -1, "filename": "zc.catalog-2.0.1.tar.gz", "has_sig": false, "md5_digest": "0466a2bfb15c9e782b2091c1c0d7c359", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 65929, "upload_time": "2017-06-15T11:02:41", "url": "https://files.pythonhosted.org/packages/fc/45/e7531ab9c4074fed0992598e90225e9077eb953207bed4d7b529bf4773ab/zc.catalog-2.0.1.tar.gz" } ], "3.0": [ { "comment_text": "", "digests": { "md5": "8dce77a734c3f02df60cd4981b55bbf2", "sha256": "4d63916af2eff52c75502daa113feeb3878e0826e7eb0625a33b82da1a34357c" }, "downloads": -1, "filename": "zc.catalog-3.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "8dce77a734c3f02df60cd4981b55bbf2", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "size": 73984, "upload_time": "2019-03-21T19:08:49", "url": "https://files.pythonhosted.org/packages/69/5a/86e233a3a8e397b2844da74cafc46fc510fa6c8ae87ee87bfdc7c21173c3/zc.catalog-3.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "6db798c34ed19925288d24f79ece5328", "sha256": "7c0125657dc0d1341b61af04e98e17ddc720e9025d907cb98c4db7581753b13f" }, "downloads": -1, "filename": "zc.catalog-3.0.tar.gz", "has_sig": false, "md5_digest": "6db798c34ed19925288d24f79ece5328", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "size": 71132, "upload_time": "2019-03-21T19:08:51", "url": "https://files.pythonhosted.org/packages/75/6c/b69f19f72114f8777e4821ef955aaeddc12db0f6dbac521dbc2dcd0eeca2/zc.catalog-3.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "8dce77a734c3f02df60cd4981b55bbf2", "sha256": "4d63916af2eff52c75502daa113feeb3878e0826e7eb0625a33b82da1a34357c" }, "downloads": -1, "filename": "zc.catalog-3.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "8dce77a734c3f02df60cd4981b55bbf2", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "size": 73984, "upload_time": "2019-03-21T19:08:49", "url": "https://files.pythonhosted.org/packages/69/5a/86e233a3a8e397b2844da74cafc46fc510fa6c8ae87ee87bfdc7c21173c3/zc.catalog-3.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "6db798c34ed19925288d24f79ece5328", "sha256": "7c0125657dc0d1341b61af04e98e17ddc720e9025d907cb98c4db7581753b13f" }, "downloads": -1, "filename": "zc.catalog-3.0.tar.gz", "has_sig": false, "md5_digest": "6db798c34ed19925288d24f79ece5328", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", "size": 71132, "upload_time": "2019-03-21T19:08:51", "url": "https://files.pythonhosted.org/packages/75/6c/b69f19f72114f8777e4821ef955aaeddc12db0f6dbac521dbc2dcd0eeca2/zc.catalog-3.0.tar.gz" } ] }