{ "info": { "author": "Edward Newell", "author_email": "edward.newell@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Topic :: Software Development :: Build Tools" ], "description": ".. tastypy documentation master file, created by\n sphinx-quickstart on tue jan 31 15:18:42 2017.\n you can adapt this file completely to your liking, but it should at least\n contain the root `toctree` directive.\n\ntastypy\n=======\n\n.. py:module:: tastypy\n\nThis documentation is best viewed on \n`readthedocs `_.\n\n``tastypy`` provides a dict-like datastructure that transparently persists to\ndisk, making the data available after a program crashes or exits. The\ndatastructure's iterators yield keys and values in the order in which keys were\nfirst added.\n\nThis is helpful whenever you want a persistent key-value store, but don't want\nto create a database. For example, you can store partial results from a\nlong-running program, and allow the program to pick up where it left off after\na crash or interruption.\n\nIncluded:\n\n - |POD|_ (alias for |PersistentOrderedDict|_): a persistent dict-like mapping.\n - |Tracker|_: a subclass of |POD|_ specifically for tracking the state of\n repetitive tasks in a long running program (for example, use it as a queue\n for URLs in a crawler).\n\nMultiprocessing-safe versions are also included:\n\n - |SharedPOD|_ (alias for |SharedPersistentOrderedDict|_)\n - |SharedTracker|_\n\n.. NOTE::\n\n Please report any bugs and request features by opening an issue at the\n project's `github page `_. \n\nInstall\n=======\n.. code-block:: bash\n\n pip install tastypy\n\n.. _PersistentOrderedDict:\n.. _POD:\n\n``PersistentOrderedDict``\n=========================\n\nThe ``tastypy.POD`` (short alias for ``tastypy.PersistentOrderedDict``) is a\ndict-like datastructure that transparently synchronizes to disk. Supply a path\nwhen creating a ``POD``, and the data will be persisted using files at that\nlocation:\n\n.. code-block:: python\n\n >>> from tastypy import POD\n >>> my_pod = POD('path/to/my.pod')\n >>> my_pod['foo'] = 'bar'\n >>> exit()\n\nPreviously-stored data can then be accessed after the original program\nterminates:\n\n.. code-block:: python\n\n >>> from tastypy import POD\n >>> my_pod = POD('path/to/my.pod')\n >>> my_pod['foo']\n 'bar'\n\n``POD``\\ s are meant to feel like ``dict``\\ s in most respects. They support\nthe same iteration mechanisms, a similar implementation of ``update()``, and\ntheir ``len`` corresponds to their number of entries.\n\nJSON -- general, simple, secure\n-------------------------------\nData is serialized in JSON format using the builtin ``json`` module for\nserialization and deserialization. JSON is general enough to represent pretty\nmuch any data, and unlike pickles, it is secure, application-independant, and\ninteroperable across programs and python versions. The persistence files are\nhuman-readable, and easily hacked manually or with other tools.\n\nWhile there are advantages to using ``json``, there are also some limitations.\nOnly json-serializable data can be stored in a ``POD``: which includes\nstring-like, number-like, list-like, and dict-like objects (and arbitrarily\nnested combinations). In a serialization-deserialization cycle, string-likes\nwill be coerced to ``unicode``\\ s, list-likes to ``list``\\ s, and dict-likes to\n``dict``\\ s. It's actually a great idea to keep your data decoupled from your\nprograms where possible, so sticking to these very universal data types is\nprobably an *enabling* constraint.\n\nThere is, however, one quirk of ``json`` that can be quite unexpected: \n\n.. WARNING:: \n\n ``json.encode()`` converts integer keys of ``dict``\\ s to ``unicode``\\ s\n to comply with the JSON specification. This quirk is inherited by \n ``tastypy``:\n\n .. code-block:: python\n\n >>> my_pod[1] = {1:1}\n >>> my_pod.sync(); my_pod.revert() # do a serialize/deserialize cycle\n >>> my_pod[1]\n {'1':1}\n\n Notice how the key in the stored ``dict`` turned from ``1`` into ``'1'``. \n \n\nSynchronization\n---------------\n\nGenerally you don't need to think about synchronization---that's the goal\nof ``tastypy``. Still, it's good to understand how it works, and how not to\nbreak it.\n\nAny changes made by keying into the ``POD`` will\nbe properly synchronized. However, if you make a reference to a mutable type stored\nin the ``POD``, and then mutate it using *that* reference, there is no way for\nthe ``POD`` to know about it, and that change will not be persisted.\n\nSo, for example:\n\n.. code-block:: python\n\n >>> my_pod['key'] = []\n >>> my_list = my_pod['key']\n >>> my_list.append(42) # BAD! This won't be sync'd!\n >>> my_pod['key'].append(42) # GOOD! This will be sync'd!\n\n``POD``\\ s keep track of values that were changed in memory, and synchronize to\ndisk whenever enough values have changed (by default, 1000), or when the\nprogram terminates. (The synchronization threshold can be set using the\n``sync_at`` argument when creating the ``POD``.)\n\nCan data be lost?\n~~~~~~~~~~~~~~~~~\n\"Dirty\" values---values that differ in memory and on disk---can be considered\nas having the same status as data that you ``.write()`` to a file object open\nfor writing. If the program exits, crashes from an uncaught exception, or\nreceives a SIGTERM or SIGINT (e.g. from ctrl-C), data *will* be synchronized.\nBut, in the exceptional cases that the Python interpreter segfaults or the\nprogram receives a SIGKILL, no synchronization is possible, so unsynchronized\ndata would be lost.\n\nCan I manually control synchronization?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nNormally you won't need to, but you can. To do a one-time synchronization of\nall dirty values immediately, do :py:meth:`POD.sync()\n`. To synchronize a specific value use\n:py:meth:`POD.sync_key(key) `. To flag a\nkey dirty for the next synchronization, use :py:meth:`POD.mark_dirty(key)\n`. To get the set of dirty keys, do\n:py:meth:`POD.dirty() `. You can suspend\nautomatic synchronization using :py:meth:`POD.hold()\n`, and reactivate it using :py:meth:`POD.unhold()\n`. To drop all un-synchronized changes and\nrevert to the state stored on disk do :py:meth:`POD.revert()\n`. See the |podref|_.\n\nOpening multiple ``POD``\\ s at same location is safe\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nConceptually, opening multiple ``POD``\\ s to the same location on disk might seem\nlike opening multiple file handles in write mode to the same location.\n\nFor files this isn't safe---when one file object flushes, it will likely \noverwrite data recently written by another.\nBut ``POD``\\ s open to the same location on disk act like singletons---so they\nactually reference the same underlying data, making stale overwrites a\nnon-problem. Of course, the situation is completely different if you want\nmultiple processes to interact with ``POD``\\ s at the same location---for that\nyou should use a |sharedpodintro|_.\n\n(It's possible to open a ``POD`` with isolated memory by passing\n``clone=False`` when creating it---but you shouldn't need to do that.)\n\n.. _podref:\n\n``PersistentOrderedDict`` reference\n-----------------------------------\n\n.. py:class:: POD\n\n Alias for ``tastypy.PersistentOrderedDict``.\n\n.. autoclass:: PersistentOrderedDict\n :member-order: bysource\n :members:\n\n.. _sharedpodintro:\n.. _SharedPersistentOrderedDict:\n.. _SharedPOD:\n\nMultiprocessing with ``SharedPOD``\\ s\n=====================================\nTo have multiple processes use ``POD``\\ s directed at the same location, you\nneed to use a ``SharedPOD``, which handles synchronization between processes.\nOpen a single ``SharedPOD`` instance and then distribute it to the children\n(e.g. by passing it over a ``Pipe`` or ``Queue``, or as an argument to a\n``multiprocessing.Process`` or ``multiprocessing.Pool``).\n\n.. WARNING::\n\n Do not create multiple ``SharedPOD`` instances pointing to the same\n location on disk. Make one ``SharedPOD`` (per location on disk) and share\n it with other processes.\n\nThe ``SharedPOD`` starts a server process with an underlying\n``POD``, and acts as a broker, forwarding method calls to the server and taking\nback responses, while safely interleaving each processes' access.\nChanges made using a ``SharedPOD`` are immediately visible to all processes.\n\n\n.. _writetosharedpods:\n\nWriting to shared ``SharedPOD``\\ s\n----------------------------------\nThe ``SharedPOD`` has to use a different strategy to ensure that data is\ncorrectly synchronized. It isn't enough to mark values as dirty: the new values\nneed to be forwarded to the underlying server.\n\nThis means that you need to explicitly signal when an operation can mutate the\n``SharedPOD``. Any time you do something to a ``SharedPOD`` that can mutate\nit, you should perform it on the ``SharedPOD.set`` attribute instead of on the\n``ShardPOD`` itself.\n\nSo, instead of doing this:\n\n.. code-block:: python\n\n shared_pod = tastypy.SharedPOD('my.pod')\n\n shared_pod['foo'] = {'bar':0, 'baz':[]} # BAD! Won't sync.\n shared_pod.set['foo'] = {'bar':4, 'baz':[]} # Good!\n\n shared_pod['foo']['bar'] += 1 # BAD! Won't sync.\n shared_pod.set['foo']['bar'] += 1 # Good!\n\n shared_pod['foo']['baz'].append('fizz') # BAD! Won't sync.\n shared_pod.set['foo']['baz'].append('fizz') # Good!\n\nThe ``SharedPOD``\\ |s| ``.set`` attribute uses some tricks to capture\narbitrarily deep \"keying\" and \"indexing\", method calls, arguments, and tell\nwhen it's being operated on by operators like ``+=``, slice assignments like\n``shared_pod.set['a'][:] = [4]``, and the like. It then forwards this\ninformation to be handled and synchronized appropriately.\n\nJust be sure to leave *off* the ``.set`` when you *access* values:\n\n.. code-block:: python\n\n >>> print shared_pod.set['foo']['baz'][0]\n \n >>> print shared_pod['foo']['baz'][0]\n fizz\n\nAvoiding raciness\n-----------------\nThe ``SharedPOD`` eliminates any raciness problems related to it's internal\nsynchronization to disk, and it ensures that each process holding the same\n``SharedPOD`` always sees the most up-to-date values, whether sync'd to disk or\nnot.\n\nHowever, that doesn't prevent you from introducing your own raciness in how you \nuse ``SharedPOD``\\ s (or any other shared datastructure for that matter).\n\nIssues generally arise when you read some shared value, and take an action\nbased on that value, while other processes might modify it. Usually a safe\npolicy is to have different processes read/write to non-overlapping subsets of\nthe ``SharedPOD``\\ |s| keys.\n\nBut if you can't or don't want to set up your program that way, then use a\nlocked context to avoid raciness. To demonstrate that, we'll use the\nprototypical example that can introduce a race condition: incrementing a value.\nSuppose that we have a worker function, that will be executed by a bunch of\ndifferent workers, that looks like this:\n\n.. code-block:: python\n\n def work(pod):\n pod.set['some-key'] += 1\n\nThat may look fine, but the ``+=`` operator really corresponds to first\ncomputing the sum ``old_val + 1`` and *then* assigning it back to the variable.\nAs process A is doing the ``+=``, process B could come along and update the\nvarable, doing its update after A computed the sum but before A assigns it\nback. So, B's update would be lost. To temporarily prevent other processes\nfrom modifying the ``SharedPOD``, use the :py:meth:`SharedPOD.locked()\n` context manager, like so:\n\n.. code-block:: python\n\n def work(shared_pod):\n with shared_pod.locked():\n shared_pod.set['some-key'] += 1\n\nSo, when exactly do you need to do that? First off, any of ``SharedPOD``\\ |s|\nown methods, or methods defined *on* its values can be treated as *atomic*,\nbecause internally a lock will be acuired before calling the method, and\nreleased afterward.\n\nSo you don't need a locked context for something like\n``pod.set['some-list'].append('item')``, or \n``del pod.set['some-dict']['some-key']``.\nContrary to the above you also don't need a locked context when using the\n``+=`` operator on values that *are mutable objects that\nimplement* ``__iadd__`` and perform the operation *inplace*. For example, \nyou don't need a lock for augmented assignment to a list, e.g. \n``pod.set['I-store-a-list'] += [1]``.\n\nYou need a locked context if:\n\n - You read a value from a ``SharedPOD``\n - You take action based on that value\n - It would be bad if that value changed before finishing that action\n - Other processes can modify that value\n\n\n``SharedPOD`` multiprocessing example\n-------------------------------------\n\nThe following example shows how you can use a ``SharedPOD`` in a multiprocessed\nprogram. In this example, each worker reads / writes to it's own subset of the\n``SharedPOD`` so locking isn't necessary:\n\n.. code-block:: python\n\n from multiprocessing import Process\n import tastypy\n\n def work(pod, proc_num, num_procs):\n for i in pod:\n if i%num_procs == proc_num:\n pod.set[i] = i**2\n\n def run_multiproc():\n num_procs = 5 \n init = [(i, None) for i in range(25)]\n pod = tastypy.SharedPOD('my.pod', init=init)\n procs = []\n for i in range(num_procs):\n proc = Process(target=work, args=(pod, i, num_procs))\n proc.start()\n procs.append(proc)\n\n for proc in procs:\n proc.join()\n\n for key, val in pod.iteritems():\n print key, val \n\n if __name__ == '__main__':\n run_multiproc()\n\nIf you run it, you'll see something like this:\n\n.. code-block:: bash\n\n $ python shared_pod_example.py\n 0 0\n 1 1\n 2 4\n 3 9\n 4 16\n 5 25\n 6 36\n 7 49\n 8 64\n 9 81\n 10 100\n 11 121\n 12 144\n 13 169\n 14 196\n 15 225\n 16 256\n 17 289\n 18 324\n 19 361\n 20 400\n 21 441\n 22 484\n 23 529\n 24 576\n\n.. _sharedpodreference:\n\nSharedPersistentOrderedDict reference\n-------------------------------------\n\n.. py:class:: SharedPOD\n\n Alias for :py:class:`SharedPersistentOrderedDict`.\n\n.. autoclass:: SharedPersistentOrderedDict(path, init={}, gzipped=False, file_size=1000, sync_at=1000)\n\n .. py:attribute:: set\n\n Attribute that accepts all mutable operations on the ``SharedPOD``. \n E.g.:\n\n .. code-block:: python\n\n shared_pod['some']['key'] += 42 # BAD!, won't sync\n shared_pod.set['some']['key'] += 42 # Good!\n\n shared_pod['some']['list'].append('forty-two') # BAD!, won't sync\n shared_pod.set['some']['list'].append('forty-two') # Good!\n\n .. automethod:: close()\n .. automethod:: locked()\n .. automethod:: lock()\n .. automethod:: unlock()\n\n *The following methods are functionally equivalent to those of* ``POD``:\n\n .. py:method:: update()\n .. py:method:: mark_dirty()\n .. py:method:: sync()\n .. py:method:: sync_key()\n .. py:method:: hold()\n\n Note that the underlying ``POD`` continues to track changes from all\n processes while automatic synchronization is suspended.\n\n .. py:method:: unhold()\n .. py:method:: revert()\n .. py:method:: iteritems()\n .. py:method:: iterkeys()\n .. py:method:: itervalues()\n .. py:method:: items()\n .. py:method:: keys()\n .. py:method:: values()\n\n\n\n\n.. _ProgressTracker:\n.. _Tracker:\n\n``ProgressTracker``\n===================\n\nThe ``tastypy.Tracker`` (short for ``tastypy.ProgressTracker``) is a subclass\nof |POD|_ that helps track the progress of long-running programs that\ninvolve performing many repetitive tasks, so that the program can pick up where\nit left off in case of a crash. \n\nEach value in a tracker represents one task and stores whether that task is\ndone, aborted, and how many times it has been tried, along with other data you\nmight want to associate to it.\n\nOften in a long-running program, you want to attempt any tasks that have\nnot been done successfully, but only attempt tasks some maximum number of times.\n\nTo motivate the ``ProgressTracker`` and illustrate how it works, let's imagine\nthat we are crawling a website. We'll begin by implementing that using a\nregular |POD|_ to keep track of the URLs that need to be crawled. Then we'll\nsee how the ``Tracker`` can support that usecase. First using a |POD|_:\n\n.. code-block:: python\n\n def crawl(seed_url):\n\n url_pod = tastypy.POD('urls.pod')\n if seed_url not in url_pod:\n url_pod[seed_url] = {'tries':0, 'done':False}\n\n for url in url_pod:\n\n # If we've fetched this url already, skip it\n if url_pod[url]['done']:\n continue\n\n # If we've tried this url too many times, skip it\n if url_pod[url]['tries'] > 3:\n continue\n\n # Record that an attempt is being made to crawl this url\n url_pod[url]['tries'] += 1\n\n # Attempt to crawl the url, move on if we don't succeed\n success, found_urls = crawl(url)\n if not success:\n continue\n\n # Add the new urls we found, and mark this url done\n for found_url in found_urls:\n if url not in url_pod:\n url_pod[url] = {'tries':0, 'done':False}\n url_pod[url]['done'] = True\n\nAs you can see, we use the |POD|_ to keep track of URLs as they are discovered,\nalong with which ones have been fetched already, and how many times each one\nhas been tried. Any time this program is started up, it will only attempt to\ncrawl URLs that haven't yet been crawled successfully, while ignoring any that\nhave already been tried at least 3 times.\n\nThe ``Tracker`` provides some facilities to support this usecase. All entries\nin a ``Tracker`` are dictionaries that minimally have a ``_done`` flag that\ndefaults to ``False``, an ``_aborted`` flag that also defaults to ``False``, and\na ``_tries`` counter that defaults to ``0``. ``Tracker``\\ s have various\nmethods to help keep track of tasks, and let you iterate over only tasks that\naren't done, aborted, or tried too many times. Using a ``Tracker``, the program\nwould look like this:\n\n.. code-block:: python\n\n def crawl(seed_url):\n\n url_tracker = tastypy.POD('urls.tracker', max_tries=3)\n url_tracker.add_if_absent(seed_url)\n\n for url in url_tracker.try_keys():\n\n # Attempt to crawl the url, move on if we don't succeed\n success, found_urls = crawl(url)\n if not success:\n continue\n\n # Add the new urls we found, and mark this url done\n url_tracker.add_many_if_absent(found_urls)\n url_tracker.mark_done(url)\n\nIn the above code block, the ``try_keys()`` iterator is used to iterate over\njust the tasks that aren't done, aborted, or already tried ``max_tries`` times,\nwhile incrementing the ``_tries`` on each task that gets yielded. The\n``add_if_absent(key)`` method is used to initialize a new task with zero tries,\nbut only if that task isn't already in the Tracker. The ``mark_done(key)``\nmethod is used to mark a task done. See the Tracker reference for the other\nconvenience methods for tracking the progress of long-running programs.\n\nNote that you can (and should!) use the ``Tracker`` to store other data related\nto tasks---such as task outputs / results. Just remember that the entry for\neach task is a ``dict`` that minimally contain ``_tries``, ``_done``,\nand ``_aborted`` keys, so don't overwrite these with values that don't make \nsense!\n\n.. _progtracker:\n\n``ProgressTracker`` reference\n-----------------------------\n\n.. autoclass:: ProgressTracker\n\n ``ProgressTracker`` supports all of the methods provided by \n :py:class:`POD `\\ s, with one small\n difference to the update function, and adds many methods for managing\n tasks.\n\n .. automethod:: update\n\n ``ProgressTracker`` adds the following methods to those provided by \n :py:class:`POD `:\n\n *Add tasks*\n\n .. automethod:: add\n .. automethod:: add_if_absent\n .. automethod:: add_many\n .. automethod:: add_many_if_absent\n\n *Change the status of tasks*\n\n .. automethod:: mark_done\n .. automethod:: mark_not_done\n .. automethod:: increment_tries\n .. automethod:: decrement_tries\n .. automethod:: reset_tries\n .. automethod:: abort\n .. automethod:: unabort\n\n *Check the status of tasks*\n\n .. automethod:: done\n .. automethod:: tries\n .. automethod:: aborted\n\n .. _gates:\n\n *Gates to decide if a task should be done*\n\n .. automethod:: should_do\n .. automethod:: should_do_add\n .. automethod:: should_try\n .. automethod:: should_try_add\n\n .. _iterators:\n\n *Iterate over tasks to be done*\n\n .. automethod:: todo_items\n .. automethod:: todo_keys\n .. automethod:: todo_values\n .. automethod:: try_items\n .. automethod:: try_keys\n .. automethod:: try_values\n\n *Check the status of all tasks*\n \n .. automethod:: num_done\n .. automethod:: fraction_done\n .. automethod:: percent_done\n .. automethod:: percent_not_done\n\n .. automethod:: num_tried\n .. automethod:: fraction_tried\n .. automethod:: percent_tried\n .. automethod:: percent_not_tried\n\n .. automethod:: num_aborted\n .. automethod:: fraction_aborted\n .. automethod:: percent_aborted\n .. automethod:: percent_not_aborted\n\n\n\n\n\n.. _SharedProgressTracker:\n.. _SharedTracker:\n\nMultiprocessing with ``SharedProgressTracker``\\ s\n=================================================\nJust as you can distribute a ``SharedPOD`` to multiple processes, you can\ndistribute a ``SharedTracker`` (short alias for ``SharedProgressTracker``) to\nmultiple processes.\n\nThe same basic usage applies. A single ``SharedTracker`` should be made and\ndistributed to child processes using a ``Queue``, ``Pipe``, or in the arguments\nto a ``Process`` or ``Pool``. All of the ``Tracker``\\ |s| own methods for \nupdating\nthe state of a task (such as ``mark_done(key)`` or ``increment_tries(key)``)\nare guaranteed to synchronized properly to disk. \nIf you want to add or mutate your own data stored on a task, then as for\n|POD|_\\ s, perform mutation operations on the ``Tracker.set`` attribute, not on\nthe ``Tracker`` itself. See |writetosharedpods|_ for an explanation.\n\n``SharedProgressTracker`` reference\n-----------------------------------\n\n.. py:class:: SharedTracker\n\n Alias for :py:class:`SharedProgressTracker`.\n\n.. autoclass:: SharedProgressTracker(path, max_tries=0, init={}, gzipped=False, file_size=1000, sync_at=1000)\n\n.. |writetosharedpods| replace:: Writing to ``SharedPOD``\\ s\n.. |podref| replace:: ``POD`` reference\n.. |pod_ref| replace:: ``POD``\n.. |s| replace:: |rsquo|\\ s\n.. |em| unicode:: 0x2014 .. em-dash\n.. |rsquo| unicode:: 0x2019 .. right single quote\n.. |sharedpodintro| replace:: ``SharedPOD``\n.. |progtracker| replace:: ``ProgressTracker``\n.. |sharedpodreference| replace:: ``SharedPersistentOrderedDict``\n.. |PersistentOrderedDict| replace:: ``PersistentOrderedDict``\n.. |SharedPersistentOrderedDict| replace:: ``SharedPersistentOrderedDict``\n.. |POD| replace:: ``POD``\n.. |SharedPOD| replace:: ``SharedPOD``\n.. |ProgressTracker| replace:: ``ProgressTracker``\n.. |Tracker| replace:: ``Tracker``\n.. |SharedProgressTracker| replace:: ``SharedProgressTracker``\n.. |SharedTracker| replace:: ``SharedTracker``", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/enewe101/tastypy", "keywords": "datastructure database python persistence storage dictionary dict", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "tastypy", "package_url": "https://pypi.org/project/tastypy/", "platform": "", "project_url": "https://pypi.org/project/tastypy/", "project_urls": { "Homepage": "https://github.com/enewe101/tastypy" }, "release_url": "https://pypi.org/project/tastypy/0.0.3/", "requires_dist": null, "requires_python": "", "summary": "simple python datastructures that transparently persist to disk", "version": "0.0.3" }, "last_serial": 3075522, "releases": { "0.0.0": [ { "comment_text": "", "digests": { "md5": "ef247f359b8973faff6846d9b15ec8d2", "sha256": "d848ac56b765506c9e9ea253475bf77d0673831a916b0e3dd8a65e4f43102f4a" }, "downloads": -1, "filename": "tastypy-0.0.0.tar.gz", "has_sig": false, "md5_digest": "ef247f359b8973faff6846d9b15ec8d2", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 66985, "upload_time": "2017-02-08T00:49:29", "url": "https://files.pythonhosted.org/packages/97/e8/f5d3f9d6b38412f0beb7c6ff48fe1f92638ef33b3036b4a762033b5897a5/tastypy-0.0.0.tar.gz" } ], "0.0.1": [ { "comment_text": "", "digests": { "md5": "ff44114743d8e70847fb20bce41f4837", "sha256": "26d33919c475b39ab36007c2e1dbb2802d1228707675e3f4409b909ca1d22c73" }, "downloads": -1, "filename": "tastypy-0.0.1.tar.gz", "has_sig": false, "md5_digest": "ff44114743d8e70847fb20bce41f4837", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 67026, "upload_time": "2017-02-08T00:52:42", "url": "https://files.pythonhosted.org/packages/43/9a/6b60c63e619f8b9c3fe19ebe34f29a4f6caf0e7e73214e5fe34c9528c500/tastypy-0.0.1.tar.gz" } ], "0.0.2": [ { "comment_text": "", "digests": { "md5": "040c48dfebcd6572a9e7c7767bd0a859", "sha256": "b2bf2d7eea4f25d4f03a0a360abbdce932aa249546678c4d7fd4498830cb9205" }, "downloads": -1, "filename": "tastypy-0.0.2.tar.gz", "has_sig": false, "md5_digest": "040c48dfebcd6572a9e7c7767bd0a859", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 40500, "upload_time": "2017-08-05T17:20:23", "url": "https://files.pythonhosted.org/packages/36/7c/9c26a5e1b42fa0ee0b4bbc071c6ef1b97e91195e684a9b845ab1779368f9/tastypy-0.0.2.tar.gz" } ], "0.0.3": [ { "comment_text": "", "digests": { "md5": "5c846bfe9b59c8a35142e3e0cb3540d3", "sha256": "e7f10072f9a3b91c4af4bd96954b7527941b2fd09ef7fe60f2b7fde5ac3fbe0c" }, "downloads": -1, "filename": "tastypy-0.0.3.tar.gz", "has_sig": false, "md5_digest": "5c846bfe9b59c8a35142e3e0cb3540d3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 40864, "upload_time": "2017-08-05T21:18:44", "url": "https://files.pythonhosted.org/packages/73/2b/f62c548b021ee9419b62f0075cd3713a811625645b31bdc223c85a7e2557/tastypy-0.0.3.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "5c846bfe9b59c8a35142e3e0cb3540d3", "sha256": "e7f10072f9a3b91c4af4bd96954b7527941b2fd09ef7fe60f2b7fde5ac3fbe0c" }, "downloads": -1, "filename": "tastypy-0.0.3.tar.gz", "has_sig": false, "md5_digest": "5c846bfe9b59c8a35142e3e0cb3540d3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 40864, "upload_time": "2017-08-05T21:18:44", "url": "https://files.pythonhosted.org/packages/73/2b/f62c548b021ee9419b62f0075cd3713a811625645b31bdc223c85a7e2557/tastypy-0.0.3.tar.gz" } ] }