py-memoize


Namepy-memoize JSON
Version 1.1.4 PyPI version JSON
download
home_pagehttps://github.com/DreamLab/memoize
SummaryCaching library for asynchronous Python applications (both based on asyncio and Tornado) that handles dogpiling properly and provides a configurable & extensible API.
upload_time2024-02-15 18:39:37
maintainerDreamLab
docs_urlNone
authorMichal Zmuda
requires_python
licenseApache License 2.0
keywords python cache tornado asyncio
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            .. image:: https://img.shields.io/pypi/v/py-memoize.svg
    :target: https://pypi.org/project/py-memoize

.. image:: https://img.shields.io/pypi/pyversions/py-memoize.svg
    :target: https://pypi.org/project/py-memoize

.. image:: https://readthedocs.org/projects/memoize/badge/?version=latest
    :target: https://memoize.readthedocs.io/en/latest/?badge=latest

.. image:: https://github.com/DreamLab/memoize/actions/workflows/tox-tests.yml/badge.svg
    :target: https://github.com/DreamLab/memoize/actions/workflows/tox-tests.yml

.. image:: https://github.com/DreamLab/memoize/actions/workflows/non-test-tox.yml/badge.svg
    :target: https://github.com/DreamLab/memoize/actions/workflows/non-test-tox.yml

Extended docs (including API docs) available at `memoize.readthedocs.io <https://memoize.readthedocs.io>`_.

What & Why
==========

**What:** Caching library for asynchronous Python applications.

**Why:** Python deserves library that works in async world
(for instance handles `dog-piling <https://en.wikipedia.org/wiki/Cache_stampede>`_ )
and has a proper, extensible API.

Etymology
=========

*In computing, memoization or memoisation is an optimization technique
used primarily to speed up computer programs by storing the results of
expensive function calls and returning the cached result when the same
inputs occur again. (…) The term “memoization” was coined by Donald
Michie in 1968 and is derived from the Latin word “memorandum” (“to be
remembered”), usually truncated as “memo” in the English language, and
thus carries the meaning of “turning [the results of] a function into
something to be remembered.”*
~ `Wikipedia <https://en.wikipedia.org/wiki/Memoization>`_

Getting Started
===============

Installation
------------

Basic Installation
~~~~~~~~~~~~~~~~~~

To get you up & running all you need is to install:

.. code-block:: bash

   pip install py-memoize

Installation of Extras
~~~~~~~~~~~~~~~~~~~~~~

If you are going to use ``memoize`` with tornado add a dependency on extra:

.. code-block:: bash

   pip install py-memoize[tornado]

To harness the power of `ujson <https://pypi.org/project/ujson/>`_ (if JSON SerDe is used) install extra:

.. code-block:: bash

   pip install py-memoize[ujson]

Usage
-----

Provided examples use default configuration to cache results in memory.
For configuration options see `Configurability`_.

You can use ``memoize`` with both `asyncio <https://docs.python.org/3/library/asyncio.html>`_
and `Tornado <https://github.com/tornadoweb/tornado>`_ -  please see the appropriate example:

asyncio
~~~~~~~

To apply default caching configuration use:

..
    _example_source: examples/basic/basic_asyncio.py

.. code-block:: python

    import asyncio
    import random
    from memoize.wrapper import memoize


    @memoize()
    async def expensive_computation():
        return 'expensive-computation-' + str(random.randint(1, 100))


    async def main():
        print(await expensive_computation())
        print(await expensive_computation())
        print(await expensive_computation())


    if __name__ == "__main__":
        asyncio.get_event_loop().run_until_complete(main())


Tornado
~~~~~~~

If your project is based on Tornado use:

..
    _example_source: examples/basic/basic_tornado.py

.. code-block:: python

    import random

    from tornado import gen
    from tornado.ioloop import IOLoop

    from memoize.wrapper import memoize


    @memoize()
    @gen.coroutine
    def expensive_computation():
        return 'expensive-computation-' + str(random.randint(1, 100))


    @gen.coroutine
    def main():
        result1 = yield expensive_computation()
        print(result1)
        result2 = yield expensive_computation()
        print(result2)
        result3 = yield expensive_computation()
        print(result3)


    if __name__ == "__main__":
        IOLoop.current().run_sync(main)



Features
========

Async-first
-----------

Asynchronous programming is often seen as a huge performance boost in python programming.
But with all the benefits it brings there are also new concurrency-related caveats
like `dog-piling <https://en.wikipedia.org/wiki/Cache_stampede>`_.

This library is built async-oriented from the ground-up, what manifests in, for example,
in `Dog-piling proofness`_ or `Async cache storage`_.


Tornado & asyncio support
-------------------------

No matter what are you using, build-in `asyncio <https://docs.python.org/3/library/asyncio.html>`_
or its predecessor `Tornado <https://github.com/tornadoweb/tornado>`_
*memoize* has you covered as you can use it with both.
**This may come handy if you are planning a migration from Tornado to asyncio.**

Under the hood *memoize* detects if you are using *Tornado* or *asyncio*
(by checking if *Tornado* is installed and available to import).

If have *Tornado* installed but your application uses *asyncio* IO-loop,
set ``MEMOIZE_FORCE_ASYNCIO=1`` environment variable to force using *asyncio* and ignore *Tornado* instalation.


Configurability
---------------

With *memoize* you have under control:

* timeout applied to the cached method;
* key generation strategy (see `memoize.key.KeyExtractor`);
  already provided strategies use arguments (both positional & keyword) and method name (or reference);
* storage for cached entries/items (see `memoize.storage.CacheStorage`);
  in-memory storage is already provided;
  for convenience of implementing new storage adapters some SerDe (`memoize.serde.SerDe`) are provided;
* eviction strategy (see `memoize.eviction.EvictionStrategy`);
  least-recently-updated strategy is already provided;
* entry builder (see `memoize.entrybuilder.CacheEntryBuilder`)
  which has control over ``update_after``  & ``expires_after`` described in `Tunable eviction & async refreshing`_

All of these elements are open for extension (you can implement and plug-in your own).
Please contribute!

Example how to customize default config (everything gets overridden):

..
    _example_source: examples/configuration/custom_configuration.py

.. code-block:: python

    from datetime import timedelta

    from memoize.configuration import MutableCacheConfiguration, DefaultInMemoryCacheConfiguration
    from memoize.entrybuilder import ProvidedLifeSpanCacheEntryBuilder
    from memoize.eviction import LeastRecentlyUpdatedEvictionStrategy
    from memoize.key import EncodedMethodNameAndArgsKeyExtractor
    from memoize.storage import LocalInMemoryCacheStorage
    from memoize.wrapper import memoize


    @memoize(configuration=MutableCacheConfiguration
             .initialized_with(DefaultInMemoryCacheConfiguration())
             .set_method_timeout(value=timedelta(minutes=2))
             .set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(minutes=2),
                                                                  expire_after=timedelta(minutes=5)))
             .set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2048))
             .set_key_extractor(EncodedMethodNameAndArgsKeyExtractor(skip_first_arg_as_self=False))
             .set_storage(LocalInMemoryCacheStorage())
             )
    async def cached():
        return 'dummy'


Still, you can use default configuration which:

* sets timeout for underlying method to 2 minutes;
* uses in-memory storage;
* uses method instance & arguments to infer cache key;
* stores up to 4096 elements in cache and evicts entries according to least recently updated policy;
* refreshes elements after 10 minutes & ignores unrefreshed elements after 30 minutes.

If that satisfies you, just use default config:

..
    _example_source: examples/configuration/default_configuration.py

.. code-block:: python

    from memoize.configuration import DefaultInMemoryCacheConfiguration
    from memoize.wrapper import memoize


    @memoize(configuration=DefaultInMemoryCacheConfiguration())
    async def cached():
        return 'dummy'

Also, if you want to stick to the building blocks of the default configuration, but need to adjust some basic params:

..
    _example_source: examples/configuration/default_customized_configuration.py

.. code-block:: python

    from datetime import timedelta

    from memoize.configuration import DefaultInMemoryCacheConfiguration
    from memoize.wrapper import memoize


    @memoize(configuration=DefaultInMemoryCacheConfiguration(capacity=4096, method_timeout=timedelta(minutes=2),
                                                             update_after=timedelta(minutes=10),
                                                             expire_after=timedelta(minutes=30)))
    async def cached():
        return 'dummy'

Tunable eviction & async refreshing
-----------------------------------

Sometimes caching libraries allow providing TTL only. This may result in a scenario where when the cache entry expires
latency is increased as the new value needs to be recomputed.
To mitigate this periodic extra latency multiple delays are often used. In the case of *memoize* there are two
(see `memoize.entrybuilder.ProvidedLifeSpanCacheEntryBuilder`):

* ``update_after`` defines delay after which background/async update is executed;
* ``expire_after`` defines delay after which entry is considered outdated and invalid.

This allows refreshing cached value in the background without any observable latency.
Moreover, if some of those background refreshes fail they will be retried still in the background.
Due to this beneficial feature, it is recommended to ``update_after`` be significantly shorter than ``expire_after``.

Dog-piling proofness
--------------------

If some resource is accessed asynchronously `dog-piling <https://en.wikipedia.org/wiki/Cache_stampede>`_ may occur.
Caches designed for synchronous python code
(like built-in `LRU <https://docs.python.org/3.3/library/functools.html#lru_cache>`_)
will allow multiple concurrent tasks to observe a miss for the same resource and will proceed to flood underlying/cached
backend with requests for the same resource.


As it breaks the purpose of caching (as backend effectively sometimes is not protected with cache)
*memoize* has built-in dog-piling protection.

Under the hood, concurrent requests for the same resource (cache key) get collapsed to a single request to the backend.
When the resource is fetched all requesters obtain the result.
On failure, all requesters get an exception (same happens on timeout).

An example of what it all is about:

..
    _example_source: examples/dogpiling/dogpiling_asyncio.py

.. code-block:: python

    import asyncio
    from datetime import timedelta

    from aiocache import cached, SimpleMemoryCache  # version 0.11.1 (latest) used as example of other cache implementation

    from memoize.configuration import DefaultInMemoryCacheConfiguration
    from memoize.wrapper import memoize

    # scenario configuration
    concurrent_requests = 5
    request_batches_execution_count = 50
    cached_value_ttl_ms = 200
    delay_between_request_batches_ms = 70

    # results/statistics
    unique_calls_under_memoize = 0
    unique_calls_under_different_cache = 0


    @memoize(configuration=DefaultInMemoryCacheConfiguration(update_after=timedelta(milliseconds=cached_value_ttl_ms)))
    async def cached_with_memoize():
        global unique_calls_under_memoize
        unique_calls_under_memoize += 1
        await asyncio.sleep(0.01)
        return unique_calls_under_memoize


    @cached(ttl=cached_value_ttl_ms / 1000, cache=SimpleMemoryCache)
    async def cached_with_different_cache():
        global unique_calls_under_different_cache
        unique_calls_under_different_cache += 1
        await asyncio.sleep(0.01)
        return unique_calls_under_different_cache


    async def main():
        for i in range(request_batches_execution_count):
            await asyncio.gather(*[x() for x in [cached_with_memoize] * concurrent_requests])
            await asyncio.gather(*[x() for x in [cached_with_different_cache] * concurrent_requests])
            await asyncio.sleep(delay_between_request_batches_ms / 1000)

        print("Memoize generated {} unique backend calls".format(unique_calls_under_memoize))
        print("Other cache generated {} unique backend calls".format(unique_calls_under_different_cache))
        predicted = (delay_between_request_batches_ms * request_batches_execution_count) // cached_value_ttl_ms
        print("Predicted (according to TTL) {} unique backend calls".format(predicted))

        # Printed:
        # Memoize generated 17 unique backend calls
        # Other cache generated 85 unique backend calls
        # Predicted (according to TTL) 17 unique backend calls

    if __name__ == "__main__":
        asyncio.get_event_loop().run_until_complete(main())


Async cache storage
-------------------

Interface for cache storage allows you to fully harness benefits of asynchronous programming
(see interface of `memoize.storage.CacheStorage`).


Currently *memoize* provides only in-memory storage for cache values (internally at *RASP* we have others).
If you want (for instance) Redis integration, you need to implement one (please contribute!)
but *memoize* will optimally use your async implementation from the start.

Manual Invalidation
-------------------

You could also invalidate entries manually.
To do so you need to create instance of `memoize.invalidation.InvalidationSupport`)
and pass it alongside cache configuration.
Then you could just pass args and kwargs for which you want to invalidate entry.

..
    _example_source: memoize/invalidation.py

.. code-block:: python

    from memoize.configuration import DefaultInMemoryCacheConfiguration
    from memoize.invalidation import InvalidationSupport


    import asyncio
    import random
    from memoize.wrapper import memoize

    invalidation = InvalidationSupport()


    @memoize(configuration=DefaultInMemoryCacheConfiguration(), invalidation=invalidation)
    async def expensive_computation(*args, **kwargs):
        return 'expensive-computation-' + str(random.randint(1, 100))


    async def main():
        print(await expensive_computation('arg1', kwarg='kwarg1'))
        print(await expensive_computation('arg1', kwarg='kwarg1'))

        print("Invalidation #1")
        await invalidation.invalidate_for_arguments(('arg1',), {'kwarg': 'kwarg1'})

        print(await expensive_computation('arg1', kwarg='kwarg1'))
        print(await expensive_computation('arg1', kwarg='kwarg1'))

        print("Invalidation #2")
        await invalidation.invalidate_for_arguments(('arg1',), {'kwarg': 'kwarg1'})

        print(await expensive_computation('arg1', kwarg='kwarg1'))

        # Sample output:
        #
        # expensive - computation - 98
        # expensive - computation - 98
        # Invalidation  # 1
        # expensive - computation - 73
        # expensive - computation - 73
        # Invalidation  # 2
        # expensive - computation - 59

    if __name__ == "__main__":
        asyncio.get_event_loop().run_until_complete(main())

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/DreamLab/memoize",
    "name": "py-memoize",
    "maintainer": "DreamLab",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "python cache tornado asyncio",
    "author": "Michal Zmuda",
    "author_email": "zmu.michal@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/8d/8d/24e153abc9ddc03361744a5d1e4e596cb5023c08980134ccbde9ec868490/py-memoize-1.1.4.tar.gz",
    "platform": "Any",
    "description": ".. image:: https://img.shields.io/pypi/v/py-memoize.svg\n    :target: https://pypi.org/project/py-memoize\n\n.. image:: https://img.shields.io/pypi/pyversions/py-memoize.svg\n    :target: https://pypi.org/project/py-memoize\n\n.. image:: https://readthedocs.org/projects/memoize/badge/?version=latest\n    :target: https://memoize.readthedocs.io/en/latest/?badge=latest\n\n.. image:: https://github.com/DreamLab/memoize/actions/workflows/tox-tests.yml/badge.svg\n    :target: https://github.com/DreamLab/memoize/actions/workflows/tox-tests.yml\n\n.. image:: https://github.com/DreamLab/memoize/actions/workflows/non-test-tox.yml/badge.svg\n    :target: https://github.com/DreamLab/memoize/actions/workflows/non-test-tox.yml\n\nExtended docs (including API docs) available at `memoize.readthedocs.io <https://memoize.readthedocs.io>`_.\n\nWhat & Why\n==========\n\n**What:** Caching library for asynchronous Python applications.\n\n**Why:** Python deserves library that works in async world\n(for instance handles `dog-piling <https://en.wikipedia.org/wiki/Cache_stampede>`_ )\nand has a proper, extensible API.\n\nEtymology\n=========\n\n*In computing, memoization or memoisation is an optimization technique\nused primarily to speed up computer programs by storing the results of\nexpensive function calls and returning the cached result when the same\ninputs occur again. (\u2026) The term \u201cmemoization\u201d was coined by Donald\nMichie in 1968 and is derived from the Latin word \u201cmemorandum\u201d (\u201cto be\nremembered\u201d), usually truncated as \u201cmemo\u201d in the English language, and\nthus carries the meaning of \u201cturning [the results of] a function into\nsomething to be remembered.\u201d*\n~ `Wikipedia <https://en.wikipedia.org/wiki/Memoization>`_\n\nGetting Started\n===============\n\nInstallation\n------------\n\nBasic Installation\n~~~~~~~~~~~~~~~~~~\n\nTo get you up & running all you need is to install:\n\n.. code-block:: bash\n\n   pip install py-memoize\n\nInstallation of Extras\n~~~~~~~~~~~~~~~~~~~~~~\n\nIf you are going to use ``memoize`` with tornado add a dependency on extra:\n\n.. code-block:: bash\n\n   pip install py-memoize[tornado]\n\nTo harness the power of `ujson <https://pypi.org/project/ujson/>`_ (if JSON SerDe is used) install extra:\n\n.. code-block:: bash\n\n   pip install py-memoize[ujson]\n\nUsage\n-----\n\nProvided examples use default configuration to cache results in memory.\nFor configuration options see `Configurability`_.\n\nYou can use ``memoize`` with both `asyncio <https://docs.python.org/3/library/asyncio.html>`_\nand `Tornado <https://github.com/tornadoweb/tornado>`_ -  please see the appropriate example:\n\nasyncio\n~~~~~~~\n\nTo apply default caching configuration use:\n\n..\n    _example_source: examples/basic/basic_asyncio.py\n\n.. code-block:: python\n\n    import asyncio\n    import random\n    from memoize.wrapper import memoize\n\n\n    @memoize()\n    async def expensive_computation():\n        return 'expensive-computation-' + str(random.randint(1, 100))\n\n\n    async def main():\n        print(await expensive_computation())\n        print(await expensive_computation())\n        print(await expensive_computation())\n\n\n    if __name__ == \"__main__\":\n        asyncio.get_event_loop().run_until_complete(main())\n\n\nTornado\n~~~~~~~\n\nIf your project is based on Tornado use:\n\n..\n    _example_source: examples/basic/basic_tornado.py\n\n.. code-block:: python\n\n    import random\n\n    from tornado import gen\n    from tornado.ioloop import IOLoop\n\n    from memoize.wrapper import memoize\n\n\n    @memoize()\n    @gen.coroutine\n    def expensive_computation():\n        return 'expensive-computation-' + str(random.randint(1, 100))\n\n\n    @gen.coroutine\n    def main():\n        result1 = yield expensive_computation()\n        print(result1)\n        result2 = yield expensive_computation()\n        print(result2)\n        result3 = yield expensive_computation()\n        print(result3)\n\n\n    if __name__ == \"__main__\":\n        IOLoop.current().run_sync(main)\n\n\n\nFeatures\n========\n\nAsync-first\n-----------\n\nAsynchronous programming is often seen as a huge performance boost in python programming.\nBut with all the benefits it brings there are also new concurrency-related caveats\nlike `dog-piling <https://en.wikipedia.org/wiki/Cache_stampede>`_.\n\nThis library is built async-oriented from the ground-up, what manifests in, for example,\nin `Dog-piling proofness`_ or `Async cache storage`_.\n\n\nTornado & asyncio support\n-------------------------\n\nNo matter what are you using, build-in `asyncio <https://docs.python.org/3/library/asyncio.html>`_\nor its predecessor `Tornado <https://github.com/tornadoweb/tornado>`_\n*memoize* has you covered as you can use it with both.\n**This may come handy if you are planning a migration from Tornado to asyncio.**\n\nUnder the hood *memoize* detects if you are using *Tornado* or *asyncio*\n(by checking if *Tornado* is installed and available to import).\n\nIf have *Tornado* installed but your application uses *asyncio* IO-loop,\nset ``MEMOIZE_FORCE_ASYNCIO=1`` environment variable to force using *asyncio* and ignore *Tornado* instalation.\n\n\nConfigurability\n---------------\n\nWith *memoize* you have under control:\n\n* timeout applied to the cached method;\n* key generation strategy (see `memoize.key.KeyExtractor`);\n  already provided strategies use arguments (both positional & keyword) and method name (or reference);\n* storage for cached entries/items (see `memoize.storage.CacheStorage`);\n  in-memory storage is already provided;\n  for convenience of implementing new storage adapters some SerDe (`memoize.serde.SerDe`) are provided;\n* eviction strategy (see `memoize.eviction.EvictionStrategy`);\n  least-recently-updated strategy is already provided;\n* entry builder (see `memoize.entrybuilder.CacheEntryBuilder`)\n  which has control over ``update_after``  & ``expires_after`` described in `Tunable eviction & async refreshing`_\n\nAll of these elements are open for extension (you can implement and plug-in your own).\nPlease contribute!\n\nExample how to customize default config (everything gets overridden):\n\n..\n    _example_source: examples/configuration/custom_configuration.py\n\n.. code-block:: python\n\n    from datetime import timedelta\n\n    from memoize.configuration import MutableCacheConfiguration, DefaultInMemoryCacheConfiguration\n    from memoize.entrybuilder import ProvidedLifeSpanCacheEntryBuilder\n    from memoize.eviction import LeastRecentlyUpdatedEvictionStrategy\n    from memoize.key import EncodedMethodNameAndArgsKeyExtractor\n    from memoize.storage import LocalInMemoryCacheStorage\n    from memoize.wrapper import memoize\n\n\n    @memoize(configuration=MutableCacheConfiguration\n             .initialized_with(DefaultInMemoryCacheConfiguration())\n             .set_method_timeout(value=timedelta(minutes=2))\n             .set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(minutes=2),\n                                                                  expire_after=timedelta(minutes=5)))\n             .set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2048))\n             .set_key_extractor(EncodedMethodNameAndArgsKeyExtractor(skip_first_arg_as_self=False))\n             .set_storage(LocalInMemoryCacheStorage())\n             )\n    async def cached():\n        return 'dummy'\n\n\nStill, you can use default configuration which:\n\n* sets timeout for underlying method to 2 minutes;\n* uses in-memory storage;\n* uses method instance & arguments to infer cache key;\n* stores up to 4096 elements in cache and evicts entries according to least recently updated policy;\n* refreshes elements after 10 minutes & ignores unrefreshed elements after 30 minutes.\n\nIf that satisfies you, just use default config:\n\n..\n    _example_source: examples/configuration/default_configuration.py\n\n.. code-block:: python\n\n    from memoize.configuration import DefaultInMemoryCacheConfiguration\n    from memoize.wrapper import memoize\n\n\n    @memoize(configuration=DefaultInMemoryCacheConfiguration())\n    async def cached():\n        return 'dummy'\n\nAlso, if you want to stick to the building blocks of the default configuration, but need to adjust some basic params:\n\n..\n    _example_source: examples/configuration/default_customized_configuration.py\n\n.. code-block:: python\n\n    from datetime import timedelta\n\n    from memoize.configuration import DefaultInMemoryCacheConfiguration\n    from memoize.wrapper import memoize\n\n\n    @memoize(configuration=DefaultInMemoryCacheConfiguration(capacity=4096, method_timeout=timedelta(minutes=2),\n                                                             update_after=timedelta(minutes=10),\n                                                             expire_after=timedelta(minutes=30)))\n    async def cached():\n        return 'dummy'\n\nTunable eviction & async refreshing\n-----------------------------------\n\nSometimes caching libraries allow providing TTL only. This may result in a scenario where when the cache entry expires\nlatency is increased as the new value needs to be recomputed.\nTo mitigate this periodic extra latency multiple delays are often used. In the case of *memoize* there are two\n(see `memoize.entrybuilder.ProvidedLifeSpanCacheEntryBuilder`):\n\n* ``update_after`` defines delay after which background/async update is executed;\n* ``expire_after`` defines delay after which entry is considered outdated and invalid.\n\nThis allows refreshing cached value in the background without any observable latency.\nMoreover, if some of those background refreshes fail they will be retried still in the background.\nDue to this beneficial feature, it is recommended to ``update_after`` be significantly shorter than ``expire_after``.\n\nDog-piling proofness\n--------------------\n\nIf some resource is accessed asynchronously `dog-piling <https://en.wikipedia.org/wiki/Cache_stampede>`_ may occur.\nCaches designed for synchronous python code\n(like built-in `LRU <https://docs.python.org/3.3/library/functools.html#lru_cache>`_)\nwill allow multiple concurrent tasks to observe a miss for the same resource and will proceed to flood underlying/cached\nbackend with requests for the same resource.\n\n\nAs it breaks the purpose of caching (as backend effectively sometimes is not protected with cache)\n*memoize* has built-in dog-piling protection.\n\nUnder the hood, concurrent requests for the same resource (cache key) get collapsed to a single request to the backend.\nWhen the resource is fetched all requesters obtain the result.\nOn failure, all requesters get an exception (same happens on timeout).\n\nAn example of what it all is about:\n\n..\n    _example_source: examples/dogpiling/dogpiling_asyncio.py\n\n.. code-block:: python\n\n    import asyncio\n    from datetime import timedelta\n\n    from aiocache import cached, SimpleMemoryCache  # version 0.11.1 (latest) used as example of other cache implementation\n\n    from memoize.configuration import DefaultInMemoryCacheConfiguration\n    from memoize.wrapper import memoize\n\n    # scenario configuration\n    concurrent_requests = 5\n    request_batches_execution_count = 50\n    cached_value_ttl_ms = 200\n    delay_between_request_batches_ms = 70\n\n    # results/statistics\n    unique_calls_under_memoize = 0\n    unique_calls_under_different_cache = 0\n\n\n    @memoize(configuration=DefaultInMemoryCacheConfiguration(update_after=timedelta(milliseconds=cached_value_ttl_ms)))\n    async def cached_with_memoize():\n        global unique_calls_under_memoize\n        unique_calls_under_memoize += 1\n        await asyncio.sleep(0.01)\n        return unique_calls_under_memoize\n\n\n    @cached(ttl=cached_value_ttl_ms / 1000, cache=SimpleMemoryCache)\n    async def cached_with_different_cache():\n        global unique_calls_under_different_cache\n        unique_calls_under_different_cache += 1\n        await asyncio.sleep(0.01)\n        return unique_calls_under_different_cache\n\n\n    async def main():\n        for i in range(request_batches_execution_count):\n            await asyncio.gather(*[x() for x in [cached_with_memoize] * concurrent_requests])\n            await asyncio.gather(*[x() for x in [cached_with_different_cache] * concurrent_requests])\n            await asyncio.sleep(delay_between_request_batches_ms / 1000)\n\n        print(\"Memoize generated {} unique backend calls\".format(unique_calls_under_memoize))\n        print(\"Other cache generated {} unique backend calls\".format(unique_calls_under_different_cache))\n        predicted = (delay_between_request_batches_ms * request_batches_execution_count) // cached_value_ttl_ms\n        print(\"Predicted (according to TTL) {} unique backend calls\".format(predicted))\n\n        # Printed:\n        # Memoize generated 17 unique backend calls\n        # Other cache generated 85 unique backend calls\n        # Predicted (according to TTL) 17 unique backend calls\n\n    if __name__ == \"__main__\":\n        asyncio.get_event_loop().run_until_complete(main())\n\n\nAsync cache storage\n-------------------\n\nInterface for cache storage allows you to fully harness benefits of asynchronous programming\n(see interface of `memoize.storage.CacheStorage`).\n\n\nCurrently *memoize* provides only in-memory storage for cache values (internally at *RASP* we have others).\nIf you want (for instance) Redis integration, you need to implement one (please contribute!)\nbut *memoize* will optimally use your async implementation from the start.\n\nManual Invalidation\n-------------------\n\nYou could also invalidate entries manually.\nTo do so you need to create instance of `memoize.invalidation.InvalidationSupport`)\nand pass it alongside cache configuration.\nThen you could just pass args and kwargs for which you want to invalidate entry.\n\n..\n    _example_source: memoize/invalidation.py\n\n.. code-block:: python\n\n    from memoize.configuration import DefaultInMemoryCacheConfiguration\n    from memoize.invalidation import InvalidationSupport\n\n\n    import asyncio\n    import random\n    from memoize.wrapper import memoize\n\n    invalidation = InvalidationSupport()\n\n\n    @memoize(configuration=DefaultInMemoryCacheConfiguration(), invalidation=invalidation)\n    async def expensive_computation(*args, **kwargs):\n        return 'expensive-computation-' + str(random.randint(1, 100))\n\n\n    async def main():\n        print(await expensive_computation('arg1', kwarg='kwarg1'))\n        print(await expensive_computation('arg1', kwarg='kwarg1'))\n\n        print(\"Invalidation #1\")\n        await invalidation.invalidate_for_arguments(('arg1',), {'kwarg': 'kwarg1'})\n\n        print(await expensive_computation('arg1', kwarg='kwarg1'))\n        print(await expensive_computation('arg1', kwarg='kwarg1'))\n\n        print(\"Invalidation #2\")\n        await invalidation.invalidate_for_arguments(('arg1',), {'kwarg': 'kwarg1'})\n\n        print(await expensive_computation('arg1', kwarg='kwarg1'))\n\n        # Sample output:\n        #\n        # expensive - computation - 98\n        # expensive - computation - 98\n        # Invalidation  # 1\n        # expensive - computation - 73\n        # expensive - computation - 73\n        # Invalidation  # 2\n        # expensive - computation - 59\n\n    if __name__ == \"__main__\":\n        asyncio.get_event_loop().run_until_complete(main())\n",
    "bugtrack_url": null,
    "license": "Apache License 2.0",
    "summary": "Caching library for asynchronous Python applications (both based on asyncio and Tornado) that handles dogpiling properly and provides a configurable & extensible API.",
    "version": "1.1.4",
    "project_urls": {
        "Homepage": "https://github.com/DreamLab/memoize"
    },
    "split_keywords": [
        "python",
        "cache",
        "tornado",
        "asyncio"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "28eadc4c2b8b86cd47cfbd2bbdf080df166d1217cfbe460d52661d19e62f41eb",
                "md5": "c96412a63872a8c3ad7f7c2a914de9d3",
                "sha256": "cab2839b246ea6e9f933453e22e42f19b42c9c6d6fcd9d02a9106fda51c2d4e3"
            },
            "downloads": -1,
            "filename": "py_memoize-1.1.4-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "c96412a63872a8c3ad7f7c2a914de9d3",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 22672,
            "upload_time": "2024-02-15T18:39:36",
            "upload_time_iso_8601": "2024-02-15T18:39:36.034656Z",
            "url": "https://files.pythonhosted.org/packages/28/ea/dc4c2b8b86cd47cfbd2bbdf080df166d1217cfbe460d52661d19e62f41eb/py_memoize-1.1.4-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "8d8d24e153abc9ddc03361744a5d1e4e596cb5023c08980134ccbde9ec868490",
                "md5": "15d7fca51b20a89990d14a20f118aaba",
                "sha256": "923a37dec4cc086a0ac971a8b79efff1a55c9af4e641a7decdec87e851e6eb76"
            },
            "downloads": -1,
            "filename": "py-memoize-1.1.4.tar.gz",
            "has_sig": false,
            "md5_digest": "15d7fca51b20a89990d14a20f118aaba",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 22802,
            "upload_time": "2024-02-15T18:39:37",
            "upload_time_iso_8601": "2024-02-15T18:39:37.841635Z",
            "url": "https://files.pythonhosted.org/packages/8d/8d/24e153abc9ddc03361744a5d1e4e596cb5023c08980134ccbde9ec868490/py-memoize-1.1.4.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-02-15 18:39:37",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "DreamLab",
    "github_project": "memoize",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "py-memoize"
}
        
Elapsed time: 0.19711s