{ "info": { "author": "Derek Anderson", "author_email": "public@kered.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3" ], "description": "\nHerman\n======\nAccording to Charles, the Peewee ORM is currently going through a major 3.0 rewrite and isn't accepting many (any?) new features for the 2.x line. Because of this, I made Herman as a light fork of Peewee (2.8.1) implementing some syntactic sugar and minor tweaks I've liked from other ORMs.\n\nThis is mostly for my company's internal use, but anyone else is free to use it. I'm maintaining API compatibility as a drop in replacement for peewee 2.x.\n\n.. image:: https://api.travis-ci.org/keredson/peewee.png?branch=master\n :target: https://travis-ci.org/keredson/peewee\n\n\nHow To Install\n==============\n\n.. code-block:: bash\n\n sudo pip uninstall peewee\n sudo pip install herman\n\nOr to upgrade:\n\n.. code-block:: bash\n\n sudo pip install herman --upgrade\n\n*There is no `herman` package.* Herman represents itself as peewee (with the same peewee `__version__`) as a drop-in replacement. To check if you're running Peewee or Herman, check for `peewee.__herman_version__`.\n\nDifferences (Herman vs. Peewee)\n===============================\n\n\nSQL Generation\n--------------\n\nThe SQL generated by an ORM isn't just an artifact to be consumed by the database. Developers need to be able to read it as well. The more readable it is and the closer it is to what one would naturally write, the less time it takes to manually evaluate / debug it.\n\nFor example, this query:\n\n.. code-block:: python\n\n Article.select()\n .join(Person, join_type=pw.JOIN.LEFT_OUTER, on=(Article.author==Person.id))\n .where(Article.author==derek)\n\nIn Peewee generates this:\n\n.. code-block:: sql\n\n SELECT \"t1\".\"id\", \"t1\".\"title\", \"t1\".\"author_id\", \"t1\".\"editor_id\" \n FROM \"article\" AS t1 \n LEFT OUTER JOIN \"person\" AS t2 ON (\"t1\".\"author_id\" = \"t2\".\"id\") \n WHERE (\"t1\".\"author_id\" = ?\n\nWhereas Herman generates:\n\n.. code-block:: sql\n\n SELECT a1.id, a1.title, a1.author_id, a1.editor_id \n FROM article AS a1 \n LEFT OUTER JOIN person AS p1 ON (a1.author_id = p1.id) \n WHERE (a1.author_id = ?) \n\nYou'll notice that in Herman:\n\n* Table and column identifiers in Herman aren't unnecessarily quoted.\n* Table alias names are derived from their table names, not just \"t1..tN\".\n\n\nThe first() Method Uses a Limit\n-------------------------------\n\n.. code-block:: python\n\n Article.select().first()\n\nIn Peewee generates this:\n\n.. code-block:: sql\n\n SELECT \"t1\".\"id\", \"t1\".\"title\", \"t1\".\"author_id\", \"t1\".\"editor_id\" FROM \"article\" AS t1\n\nWhere in Herman this is generated:\n\n.. code-block:: sql\n\n SELECT a1.id, a1.title, a1.author_id, a1.editor_id FROM article AS a1 LIMIT 1\n\nNotice the additional \"LIMIT 1\" in Herman. Peewee 2.8.1 rather loads entire result set and returns this first object. (It's my understanding future versions of Peewee will also add a \"LIMIT 1\".)\n\n\nNamed Table Aliases\n-------------------\n\nIn Peewee you can create table aliases as a variable outside your query. For example:\n\n.. code-block:: python\n\n author_table = Person.alias()\n Article.select()\n .join(author_table)\n .where(author_table.name=='Derek')\n\nThis gets you a very respectable:\n\n.. code-block:: sql\n\n SELECT \"t1\".\"id\", \"t1\".\"title\", \"t1\".\"author_id\", \"t1\".\"editor_id\" \n FROM \"article\" AS t1\n INNER JOIN \"person\" AS t2 ON (\"t1\".\"author_id\" = \"t2\".\"id\") \n WHERE (\"t2\".\"name\" = ?)\n\nBut in some queries it's very useful to specify what the aliases are. In Herman you can do this:\n\n.. code-block:: python\n\n author_table = Person.alias('author')\n Article.select()\n .join(author_table)\n .where(author_table.name=='Derek')\n\nWhich generates this:\n\n.. code-block:: sql\n\n SELECT a1.id, a1.title, a1.author_id, a1.editor_id \n FROM article AS a1 \n INNER JOIN person AS author ON (a1.author_id = author.id) \n WHERE (author.\"name\" = ?)\n\nThese two statements are equivalent in Herman:\n\n.. code-block:: python\n\n author_table = Person.alias('author')\n author_table = Person.as_('author')\n\nAnd because aliases are named, you're no longer required to use the external variable:\n\n.. code-block:: python\n\n Article.select()\n .join(Person.as_('author'))\n .where(Person.as_('author').name=='Derek')\n\nWhether in-lining the aliases makes it easier or harder to read is entirely dependent on the query and the code block it's in, but it's good to have the option. Both are supported in Herman.\n\n\nAlias References are Inferred When Unambiguous\n----------------------------------------------\n\nIf a query is otherwise invalid and there is only one possible interpretation of the query author's intent, automatically tie the un-aliased column to the appropriate alias. For example:\n\n.. code-block:: python\n\n Person.alias('xyz').select().where(Person.name == 'Derek')\n\nGenerates invalid SQL in Peewee (which is run on the database, which throws an exception):\n\n.. code-block:: sql\n\n SELECT \"t1\".\"id\", \"t1\".\"name\", \"t1\".\"parent_id\"\n FROM \"person\" AS t1 WHERE (\"t2\".\"name\" = ?)\n peewee.OperationalError: no such column: t2.name\n\n\nBut in Herman it's valid:\n\n.. code-block:: sql\n\n SELECT xyz.id, xyz.\"name\", xyz.parent_id \n FROM person AS xyz WHERE (xyz.\"name\" = ?)\n\nThis would NOT work if more than one Person alias were included in the query.\n\n\nHerman Raises Exceptions When Invalid Columns Are Referenced\n------------------------------------------------------------\n\nPeewee will generate and run on the database SQL it knows is invalid. For example:\n\n.. code-block:: python\n\n Person.select().where(Article.title == 'xyz').first()\n\nIn Peewee will throw a database error:\n\n.. code-block:: sql\n\n SELECT \"t1\".\"id\", \"t1\".\"name\", \"t1\".\"parent_id\" \n FROM \"person\" AS t1 WHERE (\"t2\".\"title\" = ?)\n peewee.OperationalError: no such column: t2.title\n\nThis isn't good for two reasons. First, I don't like relying on the database to catch easily detectable errors for us. Second, the error is opaque and specific to the internal implementation details of peewee (the \"t2\").\n\nHerman on the other hand will not generate the SQL at all, instead raising:\n\n.. code-block:: python\n\n peewee.ProgrammingError: is not a part of this query\n\n\nThe get() Method Confirms Uniqueness\n------------------------------------\n\nThe get() method in Peewee adds a \"LIMIT 1\" and returns the first object. I feel this is incorrect behavior. The difference between first() and get() is get() should assert that only one matching record exists. (This is something Django got right IMO.) If my query criteria hasn't correctly isolated a unique row the ORM should throw an exception.\n\nThis is why Herman added a \"LIMIT 2\" to the SQL genned from get(), and does a check on the number of object returned. For example:\n\n.. code-block:: python\n\n derek = Person.create(name='Derek')\n callie = Person.create(name='Callie')\n Person.select().get()\n\nWill throw:\n\n.. code-block:: sql\n\n peewee.DataError: Too many instances matching query exist:\n SQL: SELECT p1.id, p1.\"name\", p1.parent_id FROM person AS p1 LIMIT 2\n\nRather than returning a random Person object selected by the database.\n\n\nThe Shortcut ALL\n----------------\n\nIn Herman, this:\n\n.. code-block:: python\n\n Person.ALL\n\nIs the same as this:\n\n.. code-block:: python\n\n Person.select()\n\nIt's just a nomenclature I preferred from Django. I made it uppercase to prevent conflicts with any columns named \"all\", and to highlight that it's effectively a constant.\n\n\nA New (Additional) Join Syntax\n------------------------------\n\nIf I have a set of models:\n\n.. code-block:: sql\n\n class Person(pw.Model):\n name = pw.CharField()\n\n class Article(pw.Model):\n title = pw.CharField()\n author = pw.ForeignKeyField(db_column='author_id', rel_model=Person, to_field='id')\n editor = pw.ForeignKeyField(db_column='editor_id', rel_model=Person, to_field='id', related_name='edited_articles', null=True)\n\n class Reply(pw.Model):\n text = pw.CharField()\n article = pw.ForeignKeyField(db_column='article_id', rel_model=Article, to_field='id')\n\n\nAnd I want to do something fancy like get all replies with their articles and authors and editors, in Peewee I have to do something like this:\n\n\n.. code-block:: python\n\n author_table = Person.alias()\n editor_table = Person.alias()\n replies = Reply.select(Reply, author_table, editor_table)\n .join(Article) \n .join(author_table, join_type=pw.JOIN.LEFT_OUTER, on=(author_table==Article.author)) \n .switch(Article) \n .join(editor_table, join_type=pw.JOIN.LEFT_OUTER, on=(editor_table==Article.editor)) \n .where(author_table.name==\"Derek\")\n\nWhich is all sorts of complicated. For instance:\n\n* I have to mentally keep track of what the join context is and manipulate it with the switch statement.\n* Because Article has two FKs to the same table, I have to manually specify the on conditionals.\n* Because an editor can be null, the default INNER JOIN will implicitly filter out replies to articles without editors, which is rarely what the developer wants when asking for a list of replies, so I have to use \"join_type=pw.JOIN.LEFT_OUTER\" a lot.\n\nHerman offers a simpler syntax:\n\n.. code-block:: python\n\n Reply.ALL\n .plus(Reply.article, Article.author.as_('author'))\n .plus(Reply.article, Article.editor)\n .where(Person.as_('author').name==\"Derek\")\n\nWhich generates the same SQL:\n\n.. code-block:: sql\n\n SELECT r1.id, r1.\"text\", r1.article_id, a1.id, a1.title, a1.author_id, a1.editor_id,\n author.id, author.\"name\", author.parent_id, p1.id, p1.\"name\", p1.parent_id \n FROM reply AS r1 \n LEFT OUTER JOIN article AS a1 ON (r1.article_id = a1.id) \n LEFT OUTER JOIN person AS author ON (a1.author_id = author.id) \n LEFT OUTER JOIN person AS p1 ON (a1.editor_id = p1.id) \n WHERE (author.\"name\" = ?)\n\nThe plus() method takes a variable number of ForeignKeyField objects which represent a path away from the primary query object (Reply in this case). For example:\n\n.. code-block:: python\n\n Reply.ALL.plus(Reply.article)\n\nGets all the replies with their associated articles.\n\n.. code-block:: python\n\n Reply.ALL.plus(Reply.article, Article.editor)\n\nGets all the replies with their associated articles and all the articles editors. Note that \n\"Reply.article\" is a foreign key from Reply to Article, and Article.editor is a foreign key from Article to Person. The list of foreign keys must create a logical path where the \"to\" type of one FK matches the \"from\" type of the next FK.\n\nFollowing the same path twice is harmless. For instance:\n\n.. code-block:: python\n\n Reply.ALL.plus(Reply.article).plus(Reply.article)\n\nIs no different than calling plus() once. This is why we can specify multiple paths that have some overlap, for example:\n\n.. code-block:: python\n\n Reply.ALL\n .plus(Reply.article, Article.author)\n .plus(Reply.article, Article.editor)\n\nWhich will join on the article table only once.\n\nYou can also alias your joined tables with:\n\n.. code-block:: python\n\n Article.author.as_('author')\n\nWhich allows you to reference it later in your conditional:\n\n.. code-block:: python\n\n .where(Person.as_('author').name==\"Derek\")\n\nHerman's plus() also supports following foreign keys from one-to-many. Like:\n\n.. code-block:: python\n\n Article.ALL.plus(Reply.article)\n\nWhich internally does a prefetch to populate the article with all of its replies. There will be O(k) SQL statements executed, where `k` is the number of two-many relationships. All of these queries will be grouped into one transaction to guarantee correctness.\n\n*IMPORTANT:*\n\n Remember that foreign keys represent edges in your object graph, and a call to `plus(*edges)` tells Herman to include that path from the object graph in your query.\n\nThis semantics for plus() have been co-opted from the `DKO `_ project, which I authored for my former employer. DKO's version of this syntax has been in broad production use since 2010 by hundreds of developers, accessing some of the largest (billions of rows) conventional relational databases that exist.\n\nCalling len() Does a Database Count\n-----------------------------------\n\nIf you call:\n\n.. code-block:: python\n\n len(Article.select())\n\nPeewee this will load a list of all objects, permanently cache said list, and then call `len()` on that cache.\n\nIn Herman this will call `count()` on the database and return the resulting integer. It does not build the list of objects in python nor cache anything. However, for backwards compatability, if something else has already populated the cached results of the query, it will call `len()` on that.\n\n\nA New DeferredRelation Syntax\n------------------------------\n\nThe semantics behind Peewee's `circular foreign key dependencies `_ get kind of unwieldy when you have more than a few models (and they're spread over multiple files). This is because the DeferredRelation object has to be defined, used, then the other model defined in another file, then set_model has to be called on the original, and then you're left with the object reference dangling around that has no purpose. IE the example in the docs:\n\n.. code-block:: python\n\n # Create a reference object to stand in for our as-yet-undefined Tweet model.\n DeferredTweet = DeferredRelation()\n\n class User(Model):\n username = CharField()\n # Tweet has not been defined yet so use the deferred reference.\n favorite_tweet = ForeignKeyField(DeferredTweet, null=True)\n \n class Tweet(Model):\n message = TextField()\n user = ForeignKeyField(User, related_name='tweets')\n \n # Now that Tweet is defined, we can initialize the reference.\n DeferredTweet.set_model(Tweet)\n\nOurs happens all in the model definition with an optional parameter given to DeferredRelation. Like:\n\n.. code-block:: python\n\n class User(Model):\n username = CharField()\n # Tweet has not been defined yet so use the deferred reference.\n favorite_tweet = ForeignKeyField(DeferredRelation('Tweet'), null=True)\n \n class Tweet(Model):\n message = TextField()\n user = ForeignKeyField(User, related_name='tweets')\n\n\nThis removes the need for the extra variable in the global namespace and the coordination of it over multiple files. And since the parameter is optional, it is fully backwards-compatible with the old syntax.\n\nOur patch for this has been incorporated upstream, so this is forwards-compatible too, following Peewee's next release.\n\n\nPassing an Empty List/Set/Tuple into IN Doesn't Gen Invalid SQL\n---------------------------------------------------------------\n\nIf you try to do a IN operation on an empty list:\n\n.. code-block:: python\n\n User.select().where(User.id << [])\n\nPeewee will generate the following SQL:\n\n.. code-block:: sql\n\n SELECT \"t1\".\"id\", \"t1\".\"username\" FROM \"user\" AS t1 WHERE (\"t1\".\"id\" IN ())\n\nWhich the database will reject as invalid, throwing an exception. We instead generate a \"false\" statement:\n\n.. code-block:: sql\n\n SELECT u1.id, u1.username FROM \"user\" AS u1 WHERE (0 = 1)\n\nSo you don't have to manually test for empty lists every time you use a SQL IN.\n\n\n\npeewee\n======\n\nPeewee is a simple and small ORM. It has few (but expressive) concepts, making it easy to learn and intuitive to use.\n\n* A small, expressive ORM\n* Written in python with support for versions 2.6+ and 3.2+.\n* built-in support for sqlite, mysql and postgresql\n* tons of extensions available in the `playhouse `_\n\n * `Postgresql HStore, JSON, arrays and more `_\n * `SQLite full-text search, user-defined functions, virtual tables and more `_\n * `Schema migrations `_ and `model code generator `_\n * `Connection pool `_\n * `Encryption `_\n * `and much, much more... `_\n\nNew to peewee? Here is a list of documents you might find most helpful when getting\nstarted:\n\n* `Quickstart guide `_ -- this guide covers all the essentials. It will take you between 5 and 10 minutes to go through it.\n* `Guide to the various query operators `_ describes how to construct queries and combine expressions.\n* `Field types table `_ lists the various field types peewee supports and the parameters they accept.\n\nFor flask helpers, check out the `flask_utils extension module `_. You can also use peewee with the popular extension `flask-admin `_ to provide a Django-like admin interface for managing peewee models.\n\nExamples\n--------\n\nDefining models is similar to Django or SQLAlchemy:\n\n.. code-block:: python\n\n from peewee import *\n from playhouse.sqlite_ext import SqliteExtDatabase\n import datetime\n\n db = SqliteExtDatabase('my_database.db')\n\n class BaseModel(Model):\n class Meta:\n database = db\n\n class User(BaseModel):\n username = CharField(unique=True)\n\n class Tweet(BaseModel):\n user = ForeignKeyField(User, related_name='tweets')\n message = TextField()\n created_date = DateTimeField(default=datetime.datetime.now)\n is_published = BooleanField(default=True)\n\nConnect to the database and create tables:\n\n.. code-block:: python\n\n db.connect()\n db.create_tables([User, Tweet])\n\nCreate a few rows:\n\n.. code-block:: python\n\n charlie = User.create(username='charlie')\n huey = User(username='huey')\n huey.save()\n\n # No need to set `is_published` or `created_date` since they\n # will just use the default values we specified.\n Tweet.create(user=charlie, message='My first tweet')\n\nQueries are expressive and composable:\n\n.. code-block:: python\n\n # A simple query selecting a user.\n User.get(User.username == 'charles')\n\n # Get tweets created by one of several users. The \"<<\" operator\n # corresponds to the SQL \"IN\" operator.\n usernames = ['charlie', 'huey', 'mickey']\n users = User.select().where(User.username << usernames)\n tweets = Tweet.select().where(Tweet.user << users)\n\n # We could accomplish the same using a JOIN:\n tweets = (Tweet\n .select()\n .join(User)\n .where(User.username << usernames))\n\n # How many tweets were published today?\n tweets_today = (Tweet\n .select()\n .where(\n (Tweet.created_date >= datetime.date.today()) &\n (Tweet.is_published == True))\n .count())\n\n # Paginate the user table and show me page 3 (users 41-60).\n User.select().order_by(User.username).paginate(3, 20)\n\n # Order users by the number of tweets they've created:\n tweet_ct = fn.Count(Tweet.id)\n users = (User\n .select(User, tweet_ct.alias('ct'))\n .join(Tweet, JOIN.LEFT_OUTER)\n .group_by(User)\n .order_by(tweet_ct.desc()))\n\n # Do an atomic update\n Counter.update(count=Counter.count + 1).where(\n Counter.url == request.url)\n\nCheck out the `example app `_ for a working Twitter-clone website written with Flask.\n\nLearning more\n-------------\n\nCheck the `documentation `_ for more examples.\n\nSpecific question? Come hang out in the #peewee channel on irc.freenode.net, or post to the mailing list, http://groups.google.com/group/peewee-orm . If you would like to report a bug, `create a new issue `_ on GitHub.\n\nStill want more info?\n---------------------\n\n.. image:: http://media.charlesleifer.com/blog/photos/wat.jpg\n\nI've written a number of blog posts about building applications and web-services with peewee (and usually Flask). If you'd like to see some real-life applications that use peewee, the following resources may be useful:\n\n* `Building a note-taking app with Flask and Peewee `_ as well as `Part 2 `_ and `Part 3 `_.\n* `Analytics web service built with Flask and Peewee `_.\n* `Personalized news digest (with a boolean query parser!) `_.\n* `Using peewee to explore CSV files `_.\n* `Structuring Flask apps with Peewee `_.\n* `Creating a lastpass clone with Flask and Peewee `_.\n* `Building a web-based encrypted file manager with Flask, peewee and S3 `_.\n* `Creating a bookmarking web-service that takes screenshots of your bookmarks `_.\n* `Building a pastebin, wiki and a bookmarking service using Flask and Peewee `_.\n* `Encrypted databases with Python and SQLCipher `_.\n* `Dear Diary: An Encrypted, Command-Line Diary with Peewee `_.\n* `Query Tree Structures in SQLite using Peewee and the Transitive Closure Extension `_.\n", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://github.com/keredson/peewee/", "keywords": "", "license": "", "maintainer": "", "maintainer_email": "", "name": "herman", "package_url": "https://pypi.org/project/herman/", "platform": "", "project_url": "https://pypi.org/project/herman/", "project_urls": { "Homepage": "http://github.com/keredson/peewee/" }, "release_url": "https://pypi.org/project/herman/1.2.5/", "requires_dist": null, "requires_python": "", "summary": "a little fork of peewee", "version": "1.2.5" }, "last_serial": 3432542, "releases": { "1.1.1": [ { "comment_text": "", "digests": { "md5": "5e599259ba97cb547fb5a77a5f9a955b", "sha256": "3cc98d874675fdfff9494bda97e8494e4dfcd8320ecf30ad8eba43e4c8176653" }, "downloads": -1, "filename": "herman-1.1.1.tar.gz", "has_sig": false, "md5_digest": "5e599259ba97cb547fb5a77a5f9a955b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 507152, "upload_time": "2016-08-25T20:27:57", "url": "https://files.pythonhosted.org/packages/da/64/5062a9bccb2f2e4b0c6c744c3c413f991b0197c514ed8c67fcf8dc282633/herman-1.1.1.tar.gz" } ], "1.2.0": [ { "comment_text": "", "digests": { "md5": "dd2f86c495fdaa0ab0b57b577a77ca70", "sha256": "8a81cd06aaa86855340ff14f9f3827f152a89324c817d31661ff1ea7d2733c98" }, "downloads": -1, "filename": "herman-1.2.0.tar.gz", "has_sig": false, "md5_digest": "dd2f86c495fdaa0ab0b57b577a77ca70", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 507157, "upload_time": "2016-08-25T20:28:19", "url": "https://files.pythonhosted.org/packages/f5/9f/ada00683dea4fdd59e7f144128a4f7d5770bbf4f622270b020a9dbac8c53/herman-1.2.0.tar.gz" } ], "1.2.1": [ { "comment_text": "", "digests": { "md5": "7bfcaab6d8da0c8739802e5766127166", "sha256": "2a2fa2a29adcca0516797b4064ef04ad82fd8adeeef4c8da867fc16ad2b71b28" }, "downloads": -1, "filename": "herman-1.2.1.tar.gz", "has_sig": false, "md5_digest": "7bfcaab6d8da0c8739802e5766127166", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 507156, "upload_time": "2016-08-25T20:51:34", "url": "https://files.pythonhosted.org/packages/a6/97/96370cc99d99489c0510ea24006883d0dc26a660f28e493fd8811999ed16/herman-1.2.1.tar.gz" } ], "1.2.2": [ { "comment_text": "", "digests": { "md5": "7a4df0ab0a86d76a9c95eb55f19dc021", "sha256": "fcd49d351e662ec69bf6ebbef8514e86d1abd83dd927751ed5f9c756e5a41d1f" }, "downloads": -1, "filename": "herman-1.2.2.tar.gz", "has_sig": false, "md5_digest": "7a4df0ab0a86d76a9c95eb55f19dc021", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 507611, "upload_time": "2016-12-02T19:12:55", "url": "https://files.pythonhosted.org/packages/0f/42/48a05e747769c3661640d9c64a2659a404651cccbd24980736adc8320db2/herman-1.2.2.tar.gz" } ], "1.2.3": [ { "comment_text": "", "digests": { "md5": "82f9a160a9974f63c00e931be729aefa", "sha256": "a258fadaaa08a446122c7c3aa6a19d9b6319fedbb394da4e1bc04f3253e131bf" }, "downloads": -1, "filename": "herman-1.2.3.tar.gz", "has_sig": false, "md5_digest": "82f9a160a9974f63c00e931be729aefa", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 520031, "upload_time": "2016-12-23T21:22:07", "url": "https://files.pythonhosted.org/packages/17/db/d759eb6f6ddabd7115ad65af55b78f7bb81c594c4afcbf574335c52a16dc/herman-1.2.3.tar.gz" } ], "1.2.4": [ { "comment_text": "", "digests": { "md5": "b21b2d130998c1dc80ee7ec652532059", "sha256": "3664aa54a6be95151b1fefa08e332b9eb8203a6c21187323db07b594f864c188" }, "downloads": -1, "filename": "herman-1.2.4.tar.gz", "has_sig": false, "md5_digest": "b21b2d130998c1dc80ee7ec652532059", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 520027, "upload_time": "2016-12-29T19:39:32", "url": "https://files.pythonhosted.org/packages/31/49/9cf90b310aea6ed7edbc0a4f81a3ee3cf410a6a75bc9a3a581276715fbc0/herman-1.2.4.tar.gz" } ], "1.2.5": [ { "comment_text": "", "digests": { "md5": "e7b4683722fa0b05fc80ae74becf0a50", "sha256": "558862559cfa410aab473e4bff27a562b6a2de5d60023da3de3d5bbe6b0ea20e" }, "downloads": -1, "filename": "herman-1.2.5.tar.gz", "has_sig": false, "md5_digest": "e7b4683722fa0b05fc80ae74becf0a50", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 517265, "upload_time": "2017-12-20T21:23:12", "url": "https://files.pythonhosted.org/packages/79/78/26a0167b209eb4fcfd038b59c55f53913a810902b746f27d38d8ceba5f4e/herman-1.2.5.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "e7b4683722fa0b05fc80ae74becf0a50", "sha256": "558862559cfa410aab473e4bff27a562b6a2de5d60023da3de3d5bbe6b0ea20e" }, "downloads": -1, "filename": "herman-1.2.5.tar.gz", "has_sig": false, "md5_digest": "e7b4683722fa0b05fc80ae74becf0a50", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 517265, "upload_time": "2017-12-20T21:23:12", "url": "https://files.pythonhosted.org/packages/79/78/26a0167b209eb4fcfd038b59c55f53913a810902b746f27d38d8ceba5f4e/herman-1.2.5.tar.gz" } ] }