{ "info": { "author": "Gary Poster", "author_email": "gary@zope.com", "bugtrack_url": null, "classifiers": [], "description": "=========\nobjectlog\n=========\n\nThe objectlog package provides a customizable log for a single object. The\nsystem was designed to provide information for a visual log of important\nobject changes and to provide analyzable information for metrics.\n\n- It provides automatic recording for each log entry of a timestamp and the\n principals in the request when the log was made.\n\n- Given a schema of data to collect about the object, it automatically\n calculates and stores changesets from the last log entry, primarily to\n provide a quick and easy answer to the question \"what changed?\" and\n secondarily to reduce database size.\n\n- It accepts optional summary and detail values that allow the system or users\n to annotate the entries with human-readable messages.\n\n- It allows each log entry to be annotated with zero or more marker interfaces\n so that log entries may be classified with an interface.\n\nMoreover, the log entries can be set to occur at transition boundaries, and\nto only ocur if a change was made (according to the changeset) since the last\nlog entry.\n\nTo show this, we need to set up a dummy interaction. We do this below, then\ncreate an object with a log, then actually make a log.\n\n >>> import zope.security.management\n >>> import zope.security.interfaces\n >>> import zope.app.security.interfaces\n >>> from zope import interface, schema\n >>> from zope.app.testing import ztapi\n >>> class DummyPrincipal(object):\n ... interface.implements(zope.security.interfaces.IPrincipal)\n ... def __init__(self, id, title, description):\n ... self.id = unicode(id)\n ... self.title = title\n ... self.description = description\n ...\n >>> alice = DummyPrincipal('alice', 'Alice Aal', 'first principal')\n >>> betty = DummyPrincipal('betty', 'Betty Barnes', 'second principal')\n >>> cathy = DummyPrincipal('cathy', 'Cathy Camero', 'third principal')\n >>> class DummyParticipation(object):\n ... interface.implements(zope.security.interfaces.IParticipation)\n ... interaction = principal = None\n ... def __init__(self, principal):\n ... self.principal = principal\n ...\n >>> import zope.publisher.interfaces\n\n >>> import zc.objectlog\n >>> import zope.location\n >>> WORKING = u\"Where I'm working\"\n >>> COUCH = u\"On couch\"\n >>> BED = u\"On bed\"\n >>> KITCHEN = u\"In kitchen\"\n >>> class ICat(interface.Interface):\n ... name = schema.TextLine(title=u\"Name\", required=True)\n ... location = schema.Choice(\n ... (WORKING, COUCH, BED, KITCHEN),\n ... title=u\"Location\", required=False)\n ... weight = schema.Int(title=u\"Weight in Pounds\", required=True)\n ... getAge, = schema.accessors(\n ... schema.Int(title=u\"Age in Years\", readonly=True,\n ... required=False))\n ...\n >>> import persistent\n >>> class Cat(persistent.Persistent):\n ... interface.implements(ICat)\n ... def __init__(self, name, weight, age, location=None):\n ... self.name = name\n ... self.weight = weight\n ... self.location = location\n ... self._age = age\n ... self.log = zc.objectlog.Log(ICat)\n ... zope.location.locate(self.log, self, 'log')\n ... def getAge(self):\n ... return self._age\n ...\n\nNotice in the __init__ for cat that we located the log on the cat. This is\nan important step, as it enables the automatic changesets.\n\nNow we are set up to look at examples. With one exception, each example\nruns within a faux interaction so we can see how the principal_ids\nattribute works. First we'll see that len works, that the record_schema\nattribute is set properly, that the timestamp uses a pytz.utc timezone for\nthe timestamp, that log iteration works, and that summary, details, and data\nwere set properly.\n\n >>> import pytz, datetime\n >>> a_p = DummyParticipation(alice)\n >>> interface.directlyProvides(a_p, zope.publisher.interfaces.IRequest)\n >>> zope.security.management.newInteraction(a_p)\n >>> emily = Cat(u'Emily', 16, 5, WORKING)\n >>> len(emily.log)\n 0\n >>> emily.log.record_schema is ICat\n True\n >>> before = datetime.datetime.now(pytz.utc)\n >>> entry = emily.log(\n ... u'Starting to keep track of Emily',\n ... u'Looks like\\nshe might go upstairs soon')\n >>> entry is emily.log[0]\n True\n >>> after = datetime.datetime.now(pytz.utc)\n >>> len(emily.log)\n 1\n >>> before <= entry.timestamp <= after\n True\n >>> entry.timestamp.tzinfo is pytz.utc\n True\n >>> entry.principal_ids\n (u'alice',)\n >>> list(emily.log) == [entry]\n True\n >>> entry.record_schema is ICat\n True\n >>> entry.summary\n u'Starting to keep track of Emily'\n >>> entry.details\n u'Looks like\\nshe might go upstairs soon'\n\nThe record and the record_changes should have a full set of values from the\nobject. The record has a special security checker that allows users to\naccess any field defined on the schema, but not to access any others nor to\nwrite any values.\n\n >>> record = emily.log[0].record\n >>> record.name\n u'Emily'\n >>> record.location==WORKING\n True\n >>> record.weight\n 16\n >>> record.getAge()\n 5\n >>> ICat.providedBy(record)\n True\n >>> emily.log[0].record_changes == {\n ... 'name': u'Emily', 'weight': 16, 'location': u\"Where I'm working\",\n ... 'getAge': 5}\n True\n >>> from zope.security.checker import ProxyFactory\n >>> proxrecord = ProxyFactory(record)\n >>> ICat.providedBy(proxrecord)\n True\n >>> from zc.objectlog import interfaces\n >>> interfaces.IRecord.providedBy(proxrecord)\n True\n >>> from zope.security import canAccess, canWrite\n >>> canAccess(record, 'name')\n True\n >>> canAccess(record, 'weight')\n True\n >>> canAccess(record, 'location')\n True\n >>> canAccess(record, 'getAge')\n True\n >>> canAccess(record, 'shazbot') # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ForbiddenAttribute: ('shazbot', ...\n >>> canWrite(record, 'name')\n False\n >>> zope.security.management.endInteraction()\n\nInteractions with multiple principals are correctly recorded as well. Note\nthat non-request participations are not included in the records. We also\nlook a bit more at the record and the change set.\n\n >>> a_p = DummyParticipation(alice)\n >>> b_p = DummyParticipation(betty)\n >>> c_p = DummyParticipation(cathy)\n >>> interface.directlyProvides(a_p, zope.publisher.interfaces.IRequest)\n >>> interface.directlyProvides(b_p, zope.publisher.interfaces.IRequest)\n >>> zope.security.management.newInteraction(a_p, b_p, c_p)\n >>> emily.location = KITCHEN\n >>> entry = emily.log(u\"Sounds like she's eating\", u\"Dry food,\\nin fact.\")\n >>> len(emily.log)\n 2\n >>> emily.log[0].summary\n u'Starting to keep track of Emily'\n >>> emily.log[1].summary\n u\"Sounds like she's eating\"\n >>> after <= emily.log[1].timestamp <= datetime.datetime.now(pytz.utc)\n True\n >>> emily.log[1].principal_ids # cathy was not a request, so not included\n (u'alice', u'betty')\n >>> emily.log[1].details\n u'Dry food,\\nin fact.'\n >>> emily.log[1].record_changes\n {'location': u'In kitchen'}\n >>> record = emily.log[1].record\n >>> record.location\n u'In kitchen'\n >>> record.name\n u'Emily'\n >>> record.weight\n 16\n >>> zope.security.management.endInteraction()\n\nIt is possible to make a log without an interaction as well.\n\n >>> emily._age = 6\n >>> entry = emily.log(u'Happy Birthday') # no interaction\n >>> len(emily.log)\n 3\n >>> emily.log[2].principal_ids\n ()\n >>> emily.log[2].record_changes\n {'getAge': 6}\n >>> record = emily.log[2].record\n >>> record.location\n u'In kitchen'\n >>> record.name\n u'Emily'\n >>> record.weight\n 16\n >>> record.getAge()\n 6\n\nEntries may be marked with marker interfaces to categorize them. This approach\nmay be difficult with security proxies, so it may be changed. We'll do all\nthe rest of our examples within the same interaction.\n\n >>> c_p = DummyParticipation(cathy)\n >>> interface.directlyProvides(c_p, zope.publisher.interfaces.IRequest)\n >>> zope.security.management.newInteraction(c_p)\n >>> emily.location = None\n >>> emily.weight = 17\n >>> class IImportantLogEntry(interface.Interface):\n ... \"A marker interface for log entries\"\n >>> interface.directlyProvides(\n ... emily.log(u'Emily is in transit...and ate a bit too much'),\n ... IImportantLogEntry)\n >>> len(emily.log)\n 4\n >>> [e for e in emily.log if IImportantLogEntry.providedBy(e)] == [\n ... emily.log[3]]\n True\n >>> emily.log[3].principal_ids\n (u'cathy',)\n >>> emily.log[3].record_changes=={'weight': 17, 'location': None}\n True\n >>> record = emily.log[3].record\n >>> old_record = emily.log[2].record\n >>> record.name == old_record.name == u'Emily'\n True\n >>> record.weight\n 17\n >>> old_record.weight\n 16\n >>> record.location # None\n >>> old_record.location\n u'In kitchen'\n\nMaking a log will fail if the record it is trying to make does not conform\nto its schema.\n\n >>> emily.location = u'Outside'\n >>> emily.log(u'This should never happen')\n Traceback (most recent call last):\n ...\n ConstraintNotSatisfied: Outside\n >>> len(emily.log)\n 4\n >>> emily.location = BED\n\nIt will also fail if the arguments passed to it are not correct.\n\n >>> emily.log(\"This isn't unicode so will not succeed\")\n Traceback (most recent call last):\n ...\n WrongType: (\"This isn't unicode so will not succeed\", )\n >>> len(emily.log)\n 4\n >>> success = emily.log(u\"Yay, unicode\")\n\nThe following is commented out until we have more\n\n # >>> emily.log(u\"Data without an interface won't work\", None, 'boo hoo')\n Traceback (most recent call last):\n ...\n WrongContainedType: []\n\nZero or more additional arbitrary data objects may be included on the log entry\nas long as they implement an interface.\n\n >>> class IConsumableRecord(interface.Interface):\n ... dry_food = schema.Int(\n ... title=u\"Dry found consumed in ounces\", required=False)\n ... wet_food = schema.Int(\n ... title=u\"Wet food consumed in ounces\", required=False)\n ... water = schema.Int(\n ... title=u\"Water consumed in teaspoons\", required=False)\n ...\n\n # >>> class ConsumableRecord(object):\n ... interface.implements(IConsumableRecord)\n ... def __init__(self, dry_food=None, wet_food=None, water=None):\n ... self.dry_food = dry_food\n ... self.wet_food = wet_food\n ... self.water = water\n ...\n # >>> entry = emily.log(u'Collected eating records', None, ConsumableRecord(1))\n # >>> len(emily.log)\n 5\n # >>> len(emily.log[4].data)\n 1\n # >>> IConsumableRecord.providedBy(emily.log[4].data[0])\n True\n # >>> emily.log[4].data[0].dry_food\n 1\n\n__getitem__ and __iter__ work as normal for a Python sequence, including\nsupport for extended slices.\n\n >>> list(emily.log) == [emily.log[0], emily.log[1], emily.log[2],\n ... emily.log[3], emily.log[4]]\n True\n >>> emily.log[-1] is emily.log[4]\n True\n >>> emily.log[0] is emily.log[-5]\n True\n >>> emily.log[5]\n Traceback (most recent call last):\n ...\n IndexError: list index out of range\n >>> emily.log[-6]\n Traceback (most recent call last):\n ...\n IndexError: list index out of range\n >>> emily.log[4:2:-1] == [emily.log[4], emily.log[3]]\n True\n\nThe log's record_schema may be changed as long as there are no logs or the\ninterface extends (or is) the interface for the last log.\n\n >>> emily.log.record_schema = IConsumableRecord # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError: Once entries have been made, may only change schema to one...\n >>> class IExtendedCat(ICat):\n ... parent_object_intid = schema.Int(title=u\"Parent Object\")\n ...\n >>> emily.log.record_schema = IExtendedCat\n >>> emily.log.record_schema = ICat\n >>> emily.log.record_schema = IExtendedCat\n >>> class ExtendedCatAdapter(object):\n ... interface.implements(IExtendedCat)\n ... def __init__(self, cat): # getAge is left off\n ... self.name = cat.name\n ... self.weight = cat.weight\n ... self.location = cat.location\n ... self.parent_object_intid = 42\n ...\n >>> ztapi.provideAdapter((ICat,), IExtendedCat, ExtendedCatAdapter)\n >>> entry = emily.log(u'First time with extended interface')\n >>> emily.log.record_schema = ICat # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError: Once entries have been made, may only change schema to one...\n >>> emily.log[5].record_changes == {\n ... 'parent_object_intid': 42, 'getAge': None}\n True\n >>> record = emily.log[5].record\n >>> record.parent_object_intid\n 42\n >>> record.name\n u'Emily'\n >>> record.location\n u'On bed'\n >>> record.weight\n 17\n >>> record.getAge() # None\n >>> IExtendedCat.providedBy(record)\n True\n >>> old_record = emily.log[3].record\n >>> IExtendedCat.providedBy(old_record)\n False\n >>> ICat.providedBy(old_record)\n True\n >>> old_record.parent_object_intid # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n AttributeError: ...\n\nEntries support convenience next and previous attributes, which make them\nact like immutable doubly linked lists:\n\n >>> entry = emily.log[5]\n >>> entry.previous is emily.log[4]\n True\n >>> entry.next # None\n >>> entry.previous.previous.previous.previous.previous is emily.log[0]\n True\n >>> emily.log[0].previous # None\n >>> emily.log[0].next is emily.log[1]\n True\n\nObjectlogs also support deferring until the end of a transaction. To show\nthis, we will need a sample database, a transaction, and key reference\nadapters. We show the simplest example first.\n\n >>> from ZODB.tests import util\n >>> import transaction\n\n >>> db = util.DB()\n >>> connection = db.open()\n >>> root = connection.root()\n >>> root[\"emily\"] = emily\n >>> transaction.commit()\n >>> import zope.app.keyreference.persistent\n >>> import zope.app.keyreference.interfaces\n >>> import ZODB.interfaces\n >>> import persistent.interfaces\n >>> from zope import component\n >>> component.provideAdapter(\n ... zope.app.keyreference.persistent.KeyReferenceToPersistent,\n ... (persistent.interfaces.IPersistent,),\n ... zope.app.keyreference.interfaces.IKeyReference)\n >>> component.provideAdapter(\n ... zope.app.keyreference.persistent.connectionOfPersistent,\n ... (persistent.interfaces.IPersistent,),\n ... ZODB.interfaces.IConnection)\n\n >>> len(emily.log)\n 6\n >>> emily.log(u'This one is deferred', defer=True) # returns None: deferred!\n >>> len(emily.log)\n 6\n >>> transaction.commit()\n >>> len(emily.log)\n 7\n >>> emily.log[6].summary\n u'This one is deferred'\n >>> emily.log[6].record_changes\n {}\n\nWhile this is interesting, the point is to capture changes to the object,\nwhether or not they happened when the log was called. Here is a more pertinent\nexample, then.\n\n >>> len(emily.log)\n 7\n >>> emily.weight = 16\n >>> emily.log(u'Also deferred', defer=True) # returns None: deferred!\n >>> len(emily.log)\n 7\n >>> emily.location = COUCH\n >>> transaction.commit()\n >>> len(emily.log)\n 8\n >>> emily.log[7].summary\n u'Also deferred'\n >>> import pprint\n >>> pprint.pprint(emily.log[7].record_changes)\n {'location': u'On couch', 'weight': 16}\n\nMultiple deferred log entries can be deferred, if desired.\n\n >>> emily.log(u'One log', defer=True)\n >>> emily.log(u'Two log', defer=True)\n >>> len(emily.log)\n 8\n >>> transaction.commit()\n >>> len(emily.log)\n 10\n >>> emily.log[8].summary\n u'One log'\n >>> emily.log[9].summary\n u'Two log'\n\nAnother option is if_changed. It should not make a log unless there was a\nchange.\n\n >>> len(emily.log)\n 10\n >>> emily.log(u'If changed', if_changed=True) # returns None: no change!\n >>> len(emily.log)\n 10\n >>> emily.location = BED\n >>> entry = emily.log(u'If changed', if_changed=True)\n >>> len(emily.log)\n 11\n >>> emily.log[10] is entry\n True\n >>> entry.summary\n u'If changed'\n >>> pprint.pprint(entry.record_changes)\n {'location': u'On bed'}\n >>> transaction.commit()\n\nThe two options, if_changed and defer, can be used together. This makes for\na log entry that will only be made at a transition boundary if there have\nbeen no previous changes. Note that a log entry that occurs whether or not\nchanges were made (hereafter called a \"required\" log entry) that is also\ndeferred will always eliminate any deferred if_changed log entry, even if the\nrequired log entry was registered later in the transaction.\n\n >>> len(emily.log)\n 11\n >>> emily.log(u'Another', defer=True, if_changed=True) # returns None\n >>> transaction.commit()\n >>> len(emily.log)\n 11\n >>> emily.log(u'Yet another', defer=True, if_changed=True) # returns None\n >>> emily.location = COUCH\n >>> len(emily.log)\n 11\n >>> transaction.commit()\n >>> len(emily.log)\n 12\n >>> emily.log[11].summary\n u'Yet another'\n >>> emily.location = KITCHEN\n >>> entry = emily.log(u'non-deferred entry', if_changed=True)\n >>> len(emily.log)\n 13\n >>> entry.summary\n u'non-deferred entry'\n >>> emily.log(u'will not write', defer=True, if_changed=True)\n >>> transaction.commit()\n >>> len(emily.log)\n 13\n >>> emily.log(u'will not write', defer=True, if_changed=True)\n >>> emily.location = WORKING\n >>> emily.log(u'also will not write', defer=True, if_changed=True)\n >>> emily.log(u'required, deferred', defer=True)\n >>> len(emily.log)\n 13\n >>> transaction.commit()\n >>> len(emily.log)\n 14\n >>> emily.log[13].summary\n u'required, deferred'\n\nThis should all work in the presence of multiple objects, of course.\n\n >>> sam = Cat(u'Sam', 20, 4)\n >>> root['sam'] = sam\n >>> transaction.commit()\n >>> sam.weight = 19\n >>> sam.log(u'Auto log', defer=True, if_changed=True)\n >>> sam.log(u'Sam lost weight!', defer=True)\n >>> sam.log(u'Saw sam today', defer=True)\n >>> emily.log(u'Auto log', defer=True, if_changed=True)\n >>> emily.weight = 15\n >>> transaction.commit()\n >>> len(sam.log)\n 2\n >>> sam.log[0].summary\n u'Sam lost weight!'\n >>> sam.log[1].summary\n u'Saw sam today'\n >>> len(emily.log)\n 15\n >>> emily.log[14].summary\n u'Auto log'\n\n >>> # TEAR DOWN\n >>> zope.security.management.endInteraction()\n >>> ztapi.unprovideUtility(zope.app.security.interfaces.IAuthentication)\n\n\nChanges\n=======\n\n0.2 (2008-05-16)\n----------------\n\n* removed dependency on zc.security; loosened restrictions on principal_ids.\n\n0.1.1 (2008-04-02)\n------------------\n\n* Updated path in setup.py for setup to work on platforms other than linux.\n\n\n0.1 (2008-04-02)\n----------------\n\n* Initial release (removed ``dev`` status)", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "UNKNOWN", "keywords": "zope zope3 logging", "license": "ZPL 2.1", "maintainer": null, "maintainer_email": null, "name": "zc.objectlog", "package_url": "https://pypi.org/project/zc.objectlog/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/zc.objectlog/", "project_urls": { "Download": "UNKNOWN", "Homepage": "UNKNOWN" }, "release_url": "https://pypi.org/project/zc.objectlog/0.2.2/", "requires_dist": null, "requires_python": null, "summary": "a customizable log for a single object in Zope 3.", "version": "0.2.2" }, "last_serial": 802187, "releases": { "0.1": [ { "comment_text": "", "digests": { "md5": "fdae454eae6ec99e93e8d1db9ad6873e", "sha256": "4bece24749f56bd15c5252e23370afe0211c511196a6a3d2890e7e2815f7f62d" }, "downloads": -1, "filename": "zc.objectlog-0.1.tar.gz", "has_sig": false, "md5_digest": "fdae454eae6ec99e93e8d1db9ad6873e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 24252, "upload_time": "2008-04-02T18:24:13", "url": "https://files.pythonhosted.org/packages/f4/4f/d5a3441ca3954a21041cded9b9ca6b6836c657e2259c4753910467f64622/zc.objectlog-0.1.tar.gz" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "f916f8a10caa9509ee60fd86eb4de752", "sha256": "b211ba9cbf2ae7be6c2956de31d93a7a6e756c3cc130634c4b49ec58f5446f38" }, "downloads": -1, "filename": "zc.objectlog-0.1.1.tar.gz", "has_sig": false, "md5_digest": "f916f8a10caa9509ee60fd86eb4de752", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 24460, "upload_time": "2008-04-02T19:25:23", "url": "https://files.pythonhosted.org/packages/fd/75/5621e2388f6fc356d3bab3cbb6af5078866bab294de74303061448188e78/zc.objectlog-0.1.1.tar.gz" } ], "0.2": [ { "comment_text": "", "digests": { "md5": "df1e6fdad2196f94a4492fb7ea24732d", "sha256": "60c7959277ed38acb2e0461538b06fd52f1982011ff3c29f7daf55e48eb3a2a9" }, "downloads": -1, "filename": "zc.objectlog-0.2.tar.gz", "has_sig": false, "md5_digest": "df1e6fdad2196f94a4492fb7ea24732d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 25837, "upload_time": "2008-05-17T00:22:56", "url": "https://files.pythonhosted.org/packages/7e/12/67bfbf75aa4198496e944077f134818b408afc9d5d5f3497a10012ecb161/zc.objectlog-0.2.tar.gz" } ], "0.2.1": [ { "comment_text": "", "digests": { "md5": "4a2fec10eb45672c4f470ccc5cbde1a4", "sha256": "2bb19df4986cb6bfc61c8a68f78eb766061588c5c4468cd038d33dc4f2d17997" }, "downloads": -1, "filename": "zc.objectlog-0.2.1.tar.gz", "has_sig": false, "md5_digest": "4a2fec10eb45672c4f470ccc5cbde1a4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 25741, "upload_time": "2008-05-17T01:48:06", "url": "https://files.pythonhosted.org/packages/48/7a/ef1b931d62639bd92f8c20bdfc4810a4fc3723871028593d9b2c9ba6190f/zc.objectlog-0.2.1.tar.gz" } ], "0.2.2": [ { "comment_text": "", "digests": { "md5": "48976ebf496ff208d5e4bf8d0e89bca1", "sha256": "0f81c5dcc912dc403a4ec470cbb7cfb2b70e7ef49aaacb30d534adcd0239a808" }, "downloads": -1, "filename": "zc.objectlog-0.2.2.tar.gz", "has_sig": false, "md5_digest": "48976ebf496ff208d5e4bf8d0e89bca1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26193, "upload_time": "2011-09-26T21:53:35", "url": "https://files.pythonhosted.org/packages/16/7a/04ad9aa6dc0647251885bd6365835c9bb837cd619a13027ca61825dd86e2/zc.objectlog-0.2.2.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "48976ebf496ff208d5e4bf8d0e89bca1", "sha256": "0f81c5dcc912dc403a4ec470cbb7cfb2b70e7ef49aaacb30d534adcd0239a808" }, "downloads": -1, "filename": "zc.objectlog-0.2.2.tar.gz", "has_sig": false, "md5_digest": "48976ebf496ff208d5e4bf8d0e89bca1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26193, "upload_time": "2011-09-26T21:53:35", "url": "https://files.pythonhosted.org/packages/16/7a/04ad9aa6dc0647251885bd6365835c9bb837cd619a13027ca61825dd86e2/zc.objectlog-0.2.2.tar.gz" } ] }