{ "info": { "author": "Kapil Thangavelu", "author_email": "kapil.foss@gmail.com", "bugtrack_url": null, "classifiers": [], "description": "-----------------\nore.contentmirror\n-----------------\n\nA facility for mirroring the content of a Plone site into a structured\nexternal datastore. Primarily, it focuses and supports out of the box,\ncontent deployment to a relational database. The current\nimplementation provides for synchronous content mirroring or full site\ncopies. In synchronous mode, it updates the external store, as changes\nare happening in Plone, and is integrated with the zope transaction\nmachinery.\n\nIt allows the access of content from your Plone site in a language\nand platform neutral manner.\n\nIt generically handles any archetypes content, with support for all\narchetype field types, as well as serializing containment information.\nReference support does not support content or stateful based\nreferences objects.\n\nFeatures\n--------\n\n - Out of the Box support for Default Plone Content Types.\n - Out of the Box support for all builtin Archetypes Fields (including files, and references ).\n - Supports Any 3rd Party / Custom Archetypes Content.\n - Supports Capturing Containment / Content hierarchy in the serialized database.\n - Completely Automated Mirroring, zero configuration required beyond installation.\n - Easy customization via the Zope Component Architecture\n - Opensource ( GPLv3 )\n - Elegant and Simple Design, less than 600 lines of code, 100% unit test coverage.\n - Support for Plone 2.5, 3.0, and 3.1\n - Commercial Support Available ( ObjectRealms )\n\nInstallation\n------------\n\n see install.txt\n\nBootstrapping\n------------\n\nTo demonstrate the system, let's create a custom archetype content\ntype and an instance of it. We'll add the marker interface IMirrored,\nto get the mirror package's default component registrations. In\npractice, we typically apply this interface via a zcml implements\ndirective to third party components::\n\n >>> import zope.interface\n >>> from datetime import datetime\n >>> from ore.contentmirror import interfaces\n >>> class MyPage( BaseContent ):\n ... portal_type = 'My Page'\n ... zope.interface.implements( interfaces.IMirrored )\n ... schema = Schema((\n ... StringField('title'),\n ... StringField('slug', required=True),\n ... IntegerField('days'),\n ... LinesField('people'),\n ... DateTimeField('discovered_date')\n ... ))\n\n >>> content = MyPage('front-page', title=\"The Cloud Apps\", slug=\"Miracle Cures for Rabbits\")\n >>> content.title = u\"FooBar\"\n >>> content.discovered_date = DateTime() # now\n >>> content.people = [\"venkat\", \"tyrell\", \"johan\", \"arjun\", \"smithfield\"]\n\nNote on Examples\n----------------\n\nFor testing purposes a mock implementation was constructed to avoid\nthe need for setting up a Plone instance for development. From the\nperspective of the api used by contentmirror the machinery is\nidentical but the differences in for some attributes as done in these\nexamples differs from the actual plone api for doing the same.\n\nSchema Tranformation\n--------------------\n\nIn order to serialize content to a relational database, we need to\ntranform our Archeypes schema to a relational database table. The package\nprovides sensible default FieldTransformers for all builtin Archetypes fields.\n\nFirst let's grab the sqlalchemy metadata structure to store our tables in::\n\n >>> from ore.contentmirror.schema import metadata\n\nNow let's perform the transformation::\n\n >>> from ore.contentmirror import transform\n >>> transformer = transform.SchemaTransformer( content, metadata)\n >>> table = transformer.transform()\n >>> for column in table.columns: print column, column.type.__class__.__name__\n mypage.content_id Integer\n mypage.slug Text\n mypage.days Integer\n mypage.people Text\n mypage.discovered_date DateTime\n\nThe default implementation of the ISchemaTransformer uses a common content\ntable, to model common fields like dublin core attributes which are common to\nall content.\n\nPeers\n-----\n\nIn order to utilize SQLAlchemy's ORM, we create a orm mapped class for\neach content class. We call such sql persisted classes a peer. Using a\npeer class allows us to serialize state to a relational database\nwithout writing any SQL by hand. We can have the system create a peer\nclass for us, by using the peer factory::\n\n >>> from ore.contentmirror import peer\n >>> factory = peer.PeerFactory( content, transformer )\n >>> peer_class = factory.make()\n >>> peer_class\n \n\n\nModel Loader\n------------\n\nThe model loader provides an abstraction for generating a relational\nschema and a peer in high level interface. It looks up and utilizes\nISchemaTransformer and IPeerFactory components to load a content class\ninto the mirroring system::\n\n >>> from ore.contentmirror.loader import loader\n >>> loader.load( MyPage )\n\nIf we attempt to load the same class twice a KeyError is raised::\n\n >>> loader.load( MyPage )\n Traceback (most recent call last):\n ...\n KeyError: \"Duplicate \"\n\n\nEvent Stream\n------------\n\nIn order to serialize content as changes are happening in the CMS, we\nintegrate into the application server's event stream and subscribe to\ncontent events. A typical problem in Plone at least, is that redundant\noperations and events are fairly common, as well outright spurious\nevents from facilities like portal_factory. To combat that we\naggregate events on transaction boundaries, and automatically collapse\nmultiple operations for the same object.\n\nTo process the event stream, first we need to setup the database\nconnection, and database table structure::\n\n >>> import os\n >>> import sqlalchemy as rdb\n >>> metadata.bind = rdb.create_engine(test_db_uri())\n >>> metadata.create_all(checkfirst=True)\n\n\nOperation Factories\n-------------------\n\nThe event subscribers delegate to an operation factory, which provides\nfor creating deferred operations objects analagous to the Command\npattern.\n\nFor the runtime mirroring system, the operation factory provides a\ncritical policy point for customizing the behavior of content\nmirroring. Providing a different operation factory, could be utilized\nfor deploying content to an xml database, or subversion content store,\nor portal audit logs and BI reports. The default operation factory\ntackles the relational mirroring problem domain. It provides\noperations that process various content lifecycle events::\n\n >>> from ore.contentmirror import operation\n >>> ops = operation.OperationFactory( content )\n\nWe can confirm that multiple operations automatically collapse to the\nminimal set::\n\n >>> ops.add()\n >>> list(operation.get_buffer())\n []\n\nIf we delete the content in the same transaction scope, then\neffectively for the purposes of mirroring, the content was never\ncreated, and the buffer automatically removes any pending operations\nfor the content::\n\n >>> ops.delete()\n >>> operation.get_buffer().get( id(content) )\n\nif we create an add operation, and an update operation in a single\ntransaction scope, it should collapse down to just the add operation::\n\n >>> ops.add()\n >>> ops.update()\n >>> operation.get_buffer().get( id(content) )\n \n\nif we have modified the object's uid in the same transaction we should\nhave still one operation for the object::\n\n >>> len(list(operation.get_buffer()))\n 1\n >>> from ore.contentmirror.tests.base import make_uuid\n >>> content.uid = make_uuid(content.id)\n >>> ops.update()\n >>> len(list(operation.get_buffer()))\n 1\n\n\nOperations in the transaction buffer are automatically processed at\ntransaction boundaries, if we commit the transaction all operations\nheld in the buffer are processed::\n\n >>> import transaction\n >>> transaction.get().commit()\n >>> list(operation.get_buffer())\n []\n\nAlternatively if the transaction is aborted, all operations are discarded::\n\n >>> content.title = u\"Shall Not Pass\"\n >>> ops.update()\n >>> transaction.get().abort()\n >>> list(operation.get_buffer())\n []\n\nLet's go ahead and process an update operation for test coverage::\n\n >>> ops.update()\n >>> transaction.get().commit()\n\nLet's also exercise the delete operation to reset the database state\nfor other tests::\n\n >>> ops.delete()\n >>> transaction.get().commit()\n\n\n\nFilters\n-------\n\nSometimes we have content that we don't to mirror to the external\ndatastore. For example in Plone, Archetypes content is often created\ninside the Portal Factory machinery, which creates objects in\ntemporary containers. This content is not persistent, and often has\npartial state, nevertheless object lifecycle events are sent out for\nit. The system uses a filter in its default configuration, to\nautomatically suppress processing of any content in portal factory.\n\nFilters are modeled as subscription adapters, meaning each filter\nmatching against the context and operation, is applied in turn.\n\nLet's create a simple filter that filters all content. We do this by\nsetting the filtered attribute of the operation, to True::\n\n >>> def content_filter( content, operation ):\n ... operation.filtered = True\n\nAnd let's register our filter with the component architecture::\n\n >>> from zope import component\n >>> component.provideSubscriptionAdapter(\n ... content_filter,\n ... (interfaces.IMirrored, interfaces.IOperation ),\n ... interfaces.IFilter )\n\nNow if we try create an operation for the content, it will automatically\nbe filtered::\n\n >>> ops.add()\n >>> list(operation.get_buffer())\n []\n\nFinally, let's remove the filter for other tests::\n\n >>> component.getSiteManager().unregisterSubscriptionAdapter(\n ... content_filter,\n ... (interfaces.IMirrored, interfaces.IOperation ),\n ... interfaces.IFilter )\n True\n\nSerializer\n----------\n\nOperations in turn delegate to a serializer. Serializers are\nresponsible for persisting the state of the object. They utilize the\ncontent's peer to effect this. Peers are looked up via a peer registry\nutility. Schema transformers are used to copy the content's fields\nstate to the peer.\n\nTo demonstrate the serializer, first we need to register the peer\nclass with the registry::\n\n >>> from ore.contentmirror import interfaces\n >>> registry = component.getUtility( interfaces.IPeerRegistry )\n >>> registry[ MyPage ] = peer_class\n\nNow we can utilize the serializer directly to serialize our content to the\ndatabase::\n\n >>> from ore.contentmirror import serializer\n >>> content_serializer = serializer.Serializer( content )\n >>> peer = content_serializer.add()\n >>> peer.slug\n 'Miracle Cures for Rabbits'\n\nWe can directly check the database to see the serialized content there::\n\n >>> import sqlalchemy as rdb\n >>> from ore.contentmirror.session import Session\n >>> session = Session()\n >>> session.flush()\n >>> list(rdb.select( [table.c.content_id, table.c.slug] ).execute())\n [(..., u'Miracle Cures for Rabbits')]\n\nSerializers are also responsible for updating database respresentations::\n\n >>> content.slug = \"Find a home in the clouds\"\n >>> peer = content_serializer.update()\n >>> peer.slug\n 'Find a home in the clouds'\n\nand deleting them::\n\n >>> session.flush()\n >>> content_serializer.delete()\n >>> list(rdb.select( [table.c.content_id, table.c.slug] ).execute())\n []\n\nDue to the possibility of being installed and working with existing\ncontent all the methods need to be reentrant. For example deleting\nnon existent content shouldn't cause an exception::\n\n >>> content_serializer.delete()\n\nor attempting to update content which does not exist, should in turn add it::\n\n >>> peer = content_serializer.update()\n >>> session.flush()\n >>> list(rdb.select( [table.c.content_id, table.c.slug] ).execute())\n [(..., u'Find a home in the clouds')]\n\n\nContainment\n-----------\n\nContent in a plone portal is contained within the portal, and has\nexplicit containment structure based on access (Acquisition).\nie. Content is contained within folders, and folders are content. The\ncontentmirror system captures this containment structure in the\ndatabase serialization using the adjancey list support in SQLAlchemy.\n\nTo demonstrate, let's create a folderish content type and initialize\nit with the mirroring system::\n\n >>> class Folder( BaseContent ):\n ... portal_type = 'Simple Folder'\n ... zope.interface.implements( interfaces.IMirrored )\n ... schema = Schema((\n ... StringField('name'),\n ... StringField('slug', required=True),\n ... ReferenceField('related', relationship='inkind'),\n ... DateTimeField('discovered_date')\n ... ))\n\n >>> loader.load( Folder )\n >>> metadata.create_all(checkfirst=True)\n >>> root = Folder('root', name=\"Root\")\n >>> subfolder = Folder('subfolder', name=\"SubOne\", container=root)\n >>> peer = interfaces.ISerializer( subfolder ).add()\n >>> peer.parent.name == \"Root\"\n True\n >>> transaction.abort()\n\nThe content mirror automatically serializes a content's container if\nits not already serialized. Containment serialization is a recursive\noperation. In the course of normal operations, this has a nominal\ncost, as the content's container would already have been serialized,\nfrom a previous add event.\n\nNonetheless, a common scenario when starting to use content mirror on\nan existing system is that content will be added to a container thats\nnot been serialized, in which case the serialization will recurse till\nit has captured the entire parent chain. When operating on existing\nsystems, its best to let the content mirroring process 'catch up', by\nrunning the bulk serialization tool as documented in the installation\nfile. Once this tools has been run, containment operation recursion is\nminimal.\n \nAdditionally, when adding content to a container, the container will\nhave have been the subject of an object modified event, when a content\nobject is added to it, leading to redundant serialization\noperations. The content mirror automatically detects and handles this.\n \nLet's try loading this chain of objects through the operations\nfactory, to demonstrate, with an additional update event for the\ncontainer modification event::\n\n >>> operation.OperationFactory( root ).update()\n >>> operation.OperationFactory( subfolder ).add()\n >>> transaction.commit()\n \nAnd let's load the subfolder peer from the database and verify its\ncontained in the \"root\" folder::\n\n >>> from ore.contentmirror import schema\n >>> schema.fromUID( subfolder.UID() ).parent.name\n u'Root'\n\nA caveat to using containment, is that filtering containers, will\ncause contained mirrored content to appear as orphans/root objects.\n\n\nContainment hiearchies can be queried either via the adjancency tree model using\njoin depth to control the number of levels fetched in a single query, or via\na path prefix query against the portal relative path on a content node.\n \n >>> schema.fromUID( subfolder.UID() ).path\n u'root/subfolder'\n\nDelete operations on a container, cascade down to all contained content.\nTest note, demonstrating this requires usage of a database with foreign key action\nsupport ( for cascade operators ), but the default test database is sqlite\nso we won't attempt verification.\n \n >>> operation.OperationFactory( root ).delete()\n >>> transaction.commit()\n\nWorkflow\n--------\n\nA content's workflow status is also captured in the database. the state are evident below\nas we construct some sample content to serialize the state::\n\n >>> my_space = Folder(\"my-space\")\n >>> my_space.workflow_state = \"archived\"\n >>> peer = interfaces.ISerializer( my_space ).add()\n >>> peer.status\n 'archived'\n\nReferences\n----------\n\nArchetypes references are a topic onto themselves. By default,\nReferences got stored in a relation table which containing the source\nand target content ids, and a relationship name. Let's create a\ncontent class with reference fields to demonstrate::\n\n >>> class MyAsset( BaseContent ):\n ... portal_type = \"My Asset\"\n ... zope.interface.implements( interfaces.IMirrored )\n ... schema = Schema((\n ... StringField('name'), \n ... StringField('slug', required=True), \n ... ReferenceField('related', relationship='inkind'), \n ... DateTimeField('discoveredDate')\n ... ))\n\nAnd setup the peers and database tables for our new content class::\n\n >>> loader.load( MyAsset )\n >>> metadata.create_all(checkfirst=True)\n >>> table = component.getUtility( interfaces.IPeerRegistry )[ MyAsset ].transformer.table\n >>> for column in table.columns: print column, column.type.__class__.__name__\n myasset.content_id Integer\n myasset.name Text\n myasset.slug Text\n myasset.discovereddate DateTime\n\nLet's create some related content::\n\n >>> xo_image = MyAsset('xo-image', name=\"Icon\")\n >>> logo = MyAsset('logo', name=\"Logo\")\n >>> xo_article = MyAsset('xo-article', name='Article', related=xo_image, discoveredDate=DateTime() )\n >>> home_page = MyAsset( 'home-page', related=[xo_article, logo] ) \n\nAnd serialze the content. Any objects referenced by a serialized\nobject are also serialized if they not have already been\nserialized. In this way reference processing like containment, is\nrecursive::\n\n >>> peer = interfaces.ISerializer( home_page ).add()\n \nRelated objects are accessible from the peer as the relations\ncollection attribute::\n\n >>> for ob in peer.relations: print ob.target.name, ob.relationship\n Article inkind\n Logo inkind\n\nIf we modify a content object, its references are not serialized\nagain::\n\n >>> session.flush()\n >>> home_page.title = \"Home\"\n >>> peer = interfaces.ISerializer( home_page ).update()\n >>> session.dirty\n IdentitySet([])\n \n >>> for ob in peer.relations: print ob.target.name, ob.relationship\n Article inkind\n Logo inkind\n\nWhen performing case manipulation, let's verify that we're serializing the values properly.\n\n >>> schema.fromUID( xo_article.UID() ).discovereddate\n datetime.datetime(...)\n\nFiles\n-----\n\nFile content is automatically stored in a separate files table, with\nforeign key pointers back to the origin content. The files table uses\nthe binary field in sqlalchemy for storing content.\n\nLet's demonstrate using the default file handling which stores files\ninto a database. First a class with a file field::\n\n >>> class ExampleContent( BaseContent ):\n ... portal_type = \"My File\"\n ... zope.interface.implements( interfaces.IMirrored )\n ... schema = Schema((\n ... StringField('Name'), \n ... FileField('file_content', required=True), \n ... ))\n >>> loader.load( ExampleContent )\n >>> metadata.create_all( checkfirst=True )\n \nWe can see that the sqlalchemy class mapper uses a relation property for the field::\n \n >>> from sqlalchemy import orm\n >>> peer_factory = component.getUtility( interfaces.IPeerRegistry )[ ExampleContent ]\n >>> mapper = orm.class_mapper( peer_factory )\n >>> mapper.get_property('file_content')\n \n \nLet's create some content with files and serialize it::\n\n >>> image = ExampleContent('moon-image', Name=\"Icon\", file_content=File(\"treatise.txt\", \"hello world\") )\n >>> peer = interfaces.ISerializer( image ).add()\n >>> peer\n \n >>> peer.name\n 'Icon'\n >>> session.flush() \n \nNow let's verify the presence of the file in the files table:\n \n >>> list(rdb.select( [schema.files.c.file_name, schema.files.c.content, schema.files.c.checksum],\n ... schema.files.c.content_id == peer.content_id).execute() )\n [(u'treatise.txt', 'hello world', u'5eb63bbbe01eeed093cb22bb8f5acdc3')]\n \nIf we modify it, what happens to the database during update::\n\n >>> image.file_content=File(\"treatise.txt\", \"hello world 2\") \n >>> peer = interfaces.ISerializer( image ).update()\n >>> peer.file_content\n \n\nWe'll end up with two dirty (modified) objects in the sqlalchemy session\ncorresponding to the content peer, and its file peer::\n \n >>> dirty = list(session.dirty)\n >>> dirty.sort()\n >>> dirty\n [, ]\n \nAnd if we flush the session, we can verify the updated contents of the database::\n \n >>> session.flush() \n >>> list(rdb.select( [schema.files.c.file_name, schema.files.c.content, schema.files.c.checksum, ],\n ... schema.files.c.content_id == peer.content_id).execute() )\n [(u'treatise.txt', 'hello world 2', u'5270941191198af2a01db3572f1b47e8')]\n \nIf we modify the object without modifying the file content, the file\ncontent is not written to the database, a md5 checksum comparison is\nmade between the object file contents and the file peer checksum,\nbefore modifying the file peer::\n\n >>> image.title = \"rabbit\"\n >>> peer = interfaces.ISerializer( image ).update()\n >>> session.dirty\n IdentitySet([])\n \n \nReserved Words\n--------------\n\nMost databases have a variety of sql words reserved in their dialect,\nthe schema transformation takes this into account when generating\ncolumn names, and prefixes any reserved words with 'at'::\n\n >>> class ExamplePage( BaseContent ):\n ... portal_type = 'My Page'\n ... zope.interface.implements( interfaces.IMirrored )\n ... schema = Schema(( \n ... StringField('begin'), \n ... StringField('end', required=True), \n ... IntegerField('commit'), \n ... LinesField('select'),\n ... DateTimeField('where')\n ... )) \n >>> \n >>> transformer = transform.SchemaTransformer( ExamplePage('a'), metadata)\n >>> table = transformer.transform()\n >>> for column in table.columns: print column, column.type.__class__.__name__\n examplepage.content_id Integer\n examplepage.at_begin Text\n examplepage.at_end Text\n examplepage.at_commit Integer\n examplepage.at_select Text\n examplepage.at_where DateTime\n \nCustom Types\n------------\n\nAny custom archetypes can easily be added in via a zcml declaration,\nas an example this is the configuration to setup ATDocuments::\n\n \n \n \n\nLimitations\n-----------\n\nUndo is not supported as it directly relies on ZODB level features without\nany application awareness. Content mirror relies on application level events\nfor proper functioning. Even in that case, Content mirror will become eventually\nconsistent with the ZODB state as application level events are triggered for\ncontent affected by the Undo, the one exception to this is new content added,\nthat is removed by the Undo functionality of the ZODB. Typically on a Plone\nsite with active content creation, the utility of Undo is limited in practice.\nThere are no plans to address this at this time. If its of significant concern,\nutilizing the serializer scripts in cron fashion instead of the synchronous\nreplication offered by the runtime might be more effective.\n\n\nDevelopment\n-----------\n\nThe project homepage is http://contentmirror.googlecode.com which\ncontains links to documentation, issue trackers, code repositories, and\nmailing lists.\n\nCommercial Support\n------------------\n\nThe contentmirror system is designed to be useable out of the box, but\nif you need commercial support, please contact us at kapil.foss@gmail.com\n\nCredits\n-------\nKapil Thangavelu\nAlan Runyan\nLaurence Rowe\nTim Knapp\nWichert Akkerman", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://contentmirror.googlecode.com", "keywords": "plone zope zope3", "license": "GPL", "maintainer": null, "maintainer_email": null, "name": "ore.contentmirror", "package_url": "https://pypi.org/project/ore.contentmirror/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/ore.contentmirror/", "project_urls": { "Download": "UNKNOWN", "Homepage": "http://contentmirror.googlecode.com" }, "release_url": "https://pypi.org/project/ore.contentmirror/0.7.1/", "requires_dist": null, "requires_python": null, "summary": "Deploy Content from Plone to a Relational Database", "version": "0.7.1" }, "last_serial": 795824, "releases": { "0.5": [], "0.5.1": [], "0.5.2b": [ { "comment_text": "", "digests": { "md5": "c55e09a5ac6b39e1dcfb2c96a8e2c94c", "sha256": "b02b211e81fa6b3e2e8a220bd2f6091b55c7cb5fa771cf487cda05bd8d914832" }, "downloads": -1, "filename": "ore.contentmirror-0.5.2b.tar.gz", "has_sig": false, "md5_digest": "c55e09a5ac6b39e1dcfb2c96a8e2c94c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 46764, "upload_time": "2009-04-03T06:33:57", "url": "https://files.pythonhosted.org/packages/3d/ad/f6b7267b68a4231b6a0cfc8d5ffcc03ba3b061e199d24af0b7bbd1afb2a8/ore.contentmirror-0.5.2b.tar.gz" } ], "0.5.2b2": [ { "comment_text": "", "digests": { "md5": "ece0b14f9aa4abb3ebe038cf702b28e9", "sha256": "574d6793d39f58f76e55a270f38ab008aca46128c68f48d1d3ba09c32866982c" }, "downloads": -1, "filename": "ore.contentmirror-0.5.2b2.tar.gz", "has_sig": false, "md5_digest": "ece0b14f9aa4abb3ebe038cf702b28e9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 47358, "upload_time": "2009-05-16T23:15:30", "url": "https://files.pythonhosted.org/packages/31/d6/a4793eaa3145ad99782c27c7c113ed8006ded9b2f11b676b9265c5639c92/ore.contentmirror-0.5.2b2.tar.gz" } ], "0.6.0": [ { "comment_text": "", "digests": { "md5": "95d2222fdd09dec4748b5ff9cecd4369", "sha256": "64091060f2093c09c0ca0bed3e3fc2564e2f1fe870ec501567cf0316e405d152" }, "downloads": -1, "filename": "ore.contentmirror-0.6.0.tar.gz", "has_sig": false, "md5_digest": "95d2222fdd09dec4748b5ff9cecd4369", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26748, "upload_time": "2010-09-30T22:03:02", "url": "https://files.pythonhosted.org/packages/33/14/82e09e361e702c58df5bf830d3dfa41bfe97571db94ef243ca769707fa5e/ore.contentmirror-0.6.0.tar.gz" } ], "0.6.0-rc1": [ { "comment_text": "", "digests": { "md5": "d8238229111a0254bc71093736dcce8f", "sha256": "34fd869f24fb3c1c205c132880111c181a61f88dce5296867aafe4ac006a47b8" }, "downloads": -1, "filename": "ore.contentmirror-0.6.0-rc1.tar.gz", "has_sig": false, "md5_digest": "d8238229111a0254bc71093736dcce8f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 33133, "upload_time": "2010-04-11T21:55:24", "url": "https://files.pythonhosted.org/packages/18/ae/be8b1322232a722754acdbde8b2e00ca9c83feb237a392b0e68006168f93/ore.contentmirror-0.6.0-rc1.tar.gz" } ], "0.6.0-rc2": [ { "comment_text": "", "digests": { "md5": "d0393a3159173c477fa9d9287332debf", "sha256": "b781ecdb288110031cbb7145aba58987e0bd99205f17825e7198e79f5c21dff3" }, "downloads": -1, "filename": "ore.contentmirror-0.6.0-rc2.tar.gz", "has_sig": false, "md5_digest": "d0393a3159173c477fa9d9287332debf", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 561986, "upload_time": "2010-05-11T02:25:09", "url": "https://files.pythonhosted.org/packages/be/d8/e3f8d2e5c590dbd5dba8286f2790a1835aa1f56d4c968b0bf312c0d2ccdb/ore.contentmirror-0.6.0-rc2.tar.gz" } ], "0.6.0-rc3": [ { "comment_text": "", "digests": { "md5": "e0660fb21df657938aba99d8e11aaebd", "sha256": "714f0d6d7a09ba04cee1764d468829385ddaa4a2c2077734c9254559e6816f63" }, "downloads": -1, "filename": "ore.contentmirror-0.6.0-rc3.tar.gz", "has_sig": false, "md5_digest": "e0660fb21df657938aba99d8e11aaebd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 563491, "upload_time": "2010-07-28T13:56:31", "url": "https://files.pythonhosted.org/packages/ec/74/9235a55d2e507d588a8173dba76ac4aad1271e464188efaa7cf91a6bdd47/ore.contentmirror-0.6.0-rc3.tar.gz" } ], "0.6.1": [ { "comment_text": "", "digests": { "md5": "ec54a189707d1efbe87ca8baa1554572", "sha256": "9e7b93619c6eadc0502f214b1541e8db48d5cb52db92f0d364c6f086333cb6db" }, "downloads": -1, "filename": "ore.contentmirror-0.6.1.tar.gz", "has_sig": false, "md5_digest": "ec54a189707d1efbe87ca8baa1554572", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 44046, "upload_time": "2010-10-01T15:51:36", "url": "https://files.pythonhosted.org/packages/b3/33/ff022731a28347a56cd1e386fdbcedcf7936ae0ae83e7bc3be4306d150b9/ore.contentmirror-0.6.1.tar.gz" } ], "0.7.1": [ { "comment_text": "", "digests": { "md5": "1b1a4b823e81089d1ed05b2d18bd5ca1", "sha256": "d72a71353aa18a9628e554ba47ecf14e09bc22f545d1bd8e44f2f3d974b9636f" }, "downloads": -1, "filename": "ore.contentmirror-0.7.1.tar.gz", "has_sig": false, "md5_digest": "1b1a4b823e81089d1ed05b2d18bd5ca1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 565429, "upload_time": "2010-10-01T15:59:43", "url": "https://files.pythonhosted.org/packages/43/7b/e3725e4fb5e5bd0f7ee4630a8245e745457986b9ca88e0dd77a92a4e8f69/ore.contentmirror-0.7.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "1b1a4b823e81089d1ed05b2d18bd5ca1", "sha256": "d72a71353aa18a9628e554ba47ecf14e09bc22f545d1bd8e44f2f3d974b9636f" }, "downloads": -1, "filename": "ore.contentmirror-0.7.1.tar.gz", "has_sig": false, "md5_digest": "1b1a4b823e81089d1ed05b2d18bd5ca1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 565429, "upload_time": "2010-10-01T15:59:43", "url": "https://files.pythonhosted.org/packages/43/7b/e3725e4fb5e5bd0f7ee4630a8245e745457986b9ca88e0dd77a92a4e8f69/ore.contentmirror-0.7.1.tar.gz" } ] }