# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['aquiche', 'aquiche.utils']

package_data = \
{'': ['*']}

extras_require = \
{':python_version < "3.10"': ['typing-extensions>=4.4,<5.0']}

entry_points = \
{'console_scripts': ['lint = ci:lint', 'test = ci:test']}

setup_kwargs = {
    'name': 'aquiche',
    'version': '1.2.0',
    'description': 'Async cache in-memory',
    'long_description': '# aquiche\n\nFast, unopinionated, minimalist cache decorator for [python](https://www.python.org).\n\n[<img alt="Link to the github repository" src="https://img.shields.io/badge/github-jammymalina/aquiche-8da0cb?style=for-the-badge&labelColor=555555&logo=github">](https://github.com/jammymalina/aquiche)\n[<img alt="Link to the build" src="https://img.shields.io/github/workflow/status/jammymalina/aquiche/CI?style=for-the-badge">](https://github.com/jammymalina/aquiche/actions?query=branch%3Amain+)\n[<img alt="Link to PyPi" src="https://img.shields.io/pypi/v/aquiche?style=for-the-badge">](https://pypi.org/project/aquiche)\n\n```python\nimport httpx\nfrom aquiche import alru_cache\n\n@alru_cache(wrap_async_exit_stack=True)\nasync def get_client() -> httpx.AsyncClient:\n    return httpx.AsyncClient(base_url="https://api.data.io")\n\n\n@alru_cache(expiration="10minutes")\nasync def get_data(id: str) -> dict:\n    client = get_client()\n    res = await client.get(f"/data/{id}")\n    return res.json()\n\nasync def main() -> None:\n    data = await get_data("id1")\n    print("Received the data!!!")\n```\n\n## Installation\n\nThis is a [python](https://www.python.org) module available through the\n[pypi registry](https://pypi.org).\n\nBefore installing, [download and install Python](https://www.python.org/downloads).\nPython 3.8 or higher is required.\n\nInstallation is done using the\n[`pip install` command](https://packaging.python.org/en/latest/tutorials/installing-packages):\n\n```console\n$ pip install aquiche\n```\n\n## Features\n\n- Robust caching\n- Safe to use with both async and multithreaded applications\n- Works with both "sync" functions and async functions/coroutines\n- No cache stampede/cascading failure - no multiple redundant calls when your cache is being hit at the same time\n- Wide range of options for setting the expiration\n- Negative cache support\n- Focus on high performance\n- Contains typings\n- High test coverage\n\n## Description\n\nThe decorator is very similar to [`functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache).\n\nDecorator to wrap a function with a memoizing callable that saves up to the maxsize most recent calls. It can save time when an expensive or IO bound function is periodically called with the same arguments. The decorator works both with sync and async functions. It is safe to use in multithreaded and async applications.\n\nSince a dictionary is used to cache results, the positional and keyword arguments to the function must be hashable or the key parameter has to be provided.\n\nDistinct argument patterns may be considered to be distinct calls with separate cache entries. For example, f(a=1, b=2) and f(b=2, a=1) differ in their keyword argument order and may have two separate cache entries. If you provide the key parameter the `str.format()` is used to generate the key. `Args` and `kwargs` are passed to the format function. Named `args` are passed to the function both as `args` and they are included in the `kwargs` as well. The special option "single key" can be used for the functions with no arguments or with only self/cls argument. In that case the key will have the same value no matter the `args` or `kwargs`.\n\nThe wrapped function is instrumented with a `cache_parameters()` function that returns a dataclass showing the values for all the set parameters. This is for information purposes only. Mutating the values has no effect.\n\nTo help measure the effectiveness of the cache and tune the maxsize parameter, the wrapped function is instrumented with a `cache_info()` function that returns a dataclass showing hits, misses, maxsize, current_size, and last_expiration_check. This function has to be awaited when the decorator is wrapping an async function.\n\nThe decorator also provides a `cache_clear()` function for clearing or invalidating the cache. The function has to be awaited when the decorator is wrapping an async function.\n\nExpired items can be removed from the cache with the use of `remove_expired()` function. The function has to be awaited when the decorator is wrapping an async function.\n\nThe original underlying function is accessible through the **wrapped** attribute. This is useful for introspection, bypassing the cache, or for rewrapping the function with a different cache.\n\nThe cache keeps references to the arguments and returns values until they age out of the cache or until the cache is cleared. The cache can be periodically checked for expired items.\n\nIf a method is cached, the self instance argument is included in the cache. If you are caching a method it is strongly recommended to add `__eq__` and `__hash__` methods to the class or set the key parameter.\n\n## Guide\n\nThe decorator can simply be used without any parameters, both `@alru_cache` and `@alru_cache()` are supported. If you want to modify the caching behavior below you can find the list of parameters that can be set.\n\n### Enable/Disable\n\nThe cache can be enabled or disabled. **It is not checked actively during the runtime!** You cannot update the value once the function is wrapped. If you want to check actively if the cache is enabled use the expiration param.\n\n```python\n@alru_cache(enabled=True|False)\ndef cache_function(value: str) -> int:\n    return len(value)\n```\n\n### Key\n\nIt is possible to override the default cache key generation either with the custom template or with one of the special options. If the custom template is provided the `str.format()` will be used to generate the key. Both the `*args` and `**kwargs` are passed to the `str.format()`. `Kwargs` are extended with the named `args` and the defaults are applied.\n\nIf you want the cache key to always result in the same value the special option `Key.SINGLE_KEY` can be used. This option is only recommended for functions where it is desired to ignore `args` and `kwargs`. The size of this cache will never exceed 1.\n\n```python\nfrom aquiche import alru_cache, Key\n\n@alru_cache(key="env:{environment.id}:id:{user[id]}")\nasync def get_username(environment: object, user: dict) -> str:\n    return user["username"]\n\n@alru_cache(key=Key.SINGLE_KEY)\nasync def get_user_count() -> int:\n    return 10\n```\n\n### Maxsize\n\nThe maxsize param behaves the same as in `functools.lru_cache`. If set to `None` the cache can grow without bound. If set, the memoizing decorator saves up to the maxsize most recent calls. It is set to `None` by default.\n\n```python\nfrom aquiche import alru_cache\n\n@alru_cache(maxsize=5)\nasync def cache_function_limited(value: str) -> int:\n    return len(value)\n\n@alru_cache(maxsize=None)\nasync def cache_function_unlimited(value: str) -> int:\n    return len(value)\n```\n\n### Expiration\n\nIt is possible to set when the function call expires. Expired functions call will automatically be called again the next time the function is being called. To save memory it is possible to set `expired_items_auto_removal_period` to automatically remove items after a certain period of time. If set to `None` expired items are not removed but stay in the cache. It is set to `None` by default. The decorated function still needs to be called for the removal to happen - the expiration removal task is not automatically scheduled. It is recommended to rather use `maxsize` to limit the memory consumption and keep the param set to `None`.\n\nPossible expiration types/values:\n\n- `None`, the function call never expires\n- `True|False`, if set to `True` the value is expired and the function will be called again\n- `int|float`, based on the value it will either be treated as the `timedelta` or unix timestamp\n- `datetime`, TTL (Time To Live)/the expiration date, if it contains no timezone the UTC timezone is automatically added\n- `time`, the function call will expire today at this time\n- `timedelta`, TTR (Time To Refresh/refresh interval), the function call will refresh the value each `timedelta` period\n- `datetime|time|timedelta` string, the string that can be parsed to `datetime|time|timedelta`, supported formats: ISO 8601, human-readable formats, uses the same (or nearly the same) resolution as [pydantic](https://pydantic-docs.helpmanual.io)\n- Data pointer string e.g. `$.response.data.expiry`, the pointer can point to any of the other expiration values\n- Function, the `CachedItem` object (for more information see the example below) will be passed as an argument to the function. It is possible to return any of the other expiration types from the function\n- Async function/coroutine, the `CachedItem` object (for more information see the example below) will be passed as an argument to the function. It can only be used when decorating an async function/coroutine. It is possible to return any of the other expiration types from the function\n\nThe expiration types like True, False or datetime are best used in combination with a data pointer or a function/coroutine. It is strongly advised not to set these directly in the decorator.\n\nThe expiration is set to `None` by default.\n\n#### Datetime, Date, and Time Expiration\n\n```python\nfrom aquiche import alru_cache\nfrom datetime import datetime, timedelta\n\n# Datetime expiration\n# The record should be removed after 1 day (the function needs to be called)\n@alru_cache(expiration="2012-04-23T09:15:00Z", expired_items_auto_removal_period="1 day")\nasync def get_data(value: str) -> int:\n    return len(value)\n\n# Datetime expiration\n@alru_cache(\n    expiration=datetime(2017, 5, 5, 19, 27, 24),\n    expired_items_auto_removal_period="1 hour"\n)\nasync def get_data(value: str) -> int:\n    return len(value)\n\n# Date expiration, expires at midnight (UTC)\n@alru_cache(\n    expiration="2012-04-56",\n    expired_items_auto_removal_period=timedelta(seconds=300),\n)\nasync def get_data(value: str) -> int:\n    return len(value)\n\n# Time expiration, expires the same day\n# If the time does not contain timezone it defaults to UTC\n@alru_cache(expiration="11:05:00Z", expired_items_auto_removal_period="10 minutes")\nasync def get_data(value: str) -> int:\n    return len(value)\n\n"""\nAnother possible datetime/date/time values:\n\n# values in seconds\n\'1494012444.883309\' -> datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)\n1_494_012_444.883_309 -> datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)\n\'1494012444\' -> datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)\nb\'1494012444\' -> datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)\n1_494_012_444 -> datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)\n\n# values in ms\n\'1494012444000.883309\' -> datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)\n\'-1494012444000.883309\' -> datetime(1922, 8, 29, 4, 32, 35, 999117, tzinfo=timezone.utc)\n1_494_012_444_000 -> datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)\n\'2012-04-23T09:15:00\' -> datetime(2012, 4, 23, 9, 15)\n\'2012-4-9 4:8:16\' -> datetime(2012, 4, 9, 4, 8, 16)\n\'2012-04-23T09:15:00Z\' -> datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)\n\'2012-4-9 4:8:16-0320\' -> datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))\n\'2012-04-23T10:20:30.400+02:30\'\n    -> datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(150))\n\'2012-04-23T10:20:30.400+02\'\n    -> datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(120))\n\'2012-04-23T10:20:30.400-02\'\n    -> datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))\nb\'2012-04-23T10:20:30.400-02\'\n    -> datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))\ndatetime(2017, 5, 5) -> datetime(2017, 5, 5)\n0 -> datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)\n\nd1494012444.883309\' -> date(2017, 5, 5)\nd\'1494012444.883309\' -> date(2017, 5, 5)\nd_494_012_444.883_309 -> date(2017, 5, 5)\nd1494012444\' -> date(2017, 5, 5)\nd_494_012_444 -> date(2017, 5, 5)\nd -> date(1970, 1, 1)\nd2012-04-23\' -> date(2012, 4, 23)\nd\'2012-04-23\' -> date(2012, 4, 23)\nd2012-4-9\' -> date(2012, 4, 9)\ndate(2012, 4, 9) -> date(2012, 4, 9)\n\n\'09:15:00\' -> time(9, 15)\n\'10:10\' -> time(10, 10)\n\'10:20:30.400\' -> time(10, 20, 30, 400_000)\nb\'10:20:30.400\' -> time(10, 20, 30, 400_000)\n\'4:8:16\' -> time(4, 8, 16)\ntime(4, 8, 16) -> time(4, 8, 16)\n3610 -> time(1, 0, 10)\n3600.5 -> time(1, 0, 0, 500000)\n86400 - 1 -> time(23, 59, 59)\n\'11:05:00-05:30\' -> time(11, 5, 0, tzinfo=create_tz(-330))\n\'11:05:00-0530\' -> time(11, 5, 0, tzinfo=create_tz(-330))\n\'11:05:00Z\' -> time(11, 5, 0, tzinfo=timezone.utc)\n\'11:05:00+00\' -> time(11, 5, 0, tzinfo=timezone.utc)\n\'11:05-06\' -> time(11, 5, 0, tzinfo=create_tz(-360))\n\'11:05+06\' -> time(11, 5, 0, tzinfo=create_tz(360))\n"""\n```\n\n#### Duration/Timedelta Expiration\n\n```python\nfrom aquiche import alru_cache\nfrom datetime import timedelta\n\n# Refreshes a value every 10 minutes\n@alru_cache(expiration=timedelta(minutes=10))\nasync def cache_function(value: str) -> int:\n    return len(value)\n\n# Refreshes a value every 1 day, 42 minutes and 6 seconds\n# This can be shortened to `1d42m6s`\n@alru_cache(expiration="1 day 42 minutes 6 seconds")\nasync def cache_function(value: str) -> int:\n    return len(value)\n\n# Refreshes a value every minute\n@alru_cache(expiration="1m")\nasync def cache_function(value: str) -> int:\n    return len(value)\n\n"""\nOther possible refresh/timedelta values\n\ntimedelta(seconds=30), timedelta(seconds=30)\n\'30\' -> timedelta(seconds=30)\n30 -> timedelta(seconds=30)\n30.1 -> timedelta(seconds=30, milliseconds=100)\n9.9e-05 -> timedelta(microseconds=99)\n\nminutes seconds\n\'15:30\' -> timedelta(minutes=15, seconds=30)\n\'5:30\' -> timedelta(minutes=5, seconds=30)\n\nhours minutes seconds\n\'10:15:30\' -> timedelta(hours=10, minutes=15, seconds=30)\n\'1:15:30\' -> timedelta(hours=1, minutes=15, seconds=30)\n\'100:200:300\' -> timedelta(hours=100, minutes=200, seconds=300)\n\ndays\n\'4 15:30\' -> timedelta(days=4, minutes=15, seconds=30)\n\'4 10:15:30\' -> timedelta(days=4, hours=10, minutes=15, seconds=30)\n\nfractions of seconds\n\'15:30.1\' -> timedelta(minutes=15, seconds=30, milliseconds=100)\n\'15:30.01\' -> timedelta(minutes=15, seconds=30, milliseconds=10)\n\'15:30.001\' -> timedelta(minutes=15, seconds=30, milliseconds=1)\n\'15:30.0001\' -> timedelta(minutes=15, seconds=30, microseconds=100)\n\'15:30.00001\' -> timedelta(minutes=15, seconds=30, microseconds=10)\n\'15:30.000001\' -> timedelta(minutes=15, seconds=30, microseconds=1)\nb\'15:30.000001\' -> timedelta(minutes=15, seconds=30, microseconds=1)\n\niso_8601\n\'P4Y\', errors.DurationError\n\'P4M\', errors.DurationError\n\'P4W\', errors.DurationError\n\'P4D\' -> timedelta(days=4)\n\'P0.5D\' -> timedelta(hours=12)\n\'PT5H\' -> timedelta(hours=5)\n\'PT5M\' -> timedelta(minutes=5)\n\'PT5S\' -> timedelta(seconds=5)\n\'PT0.000005S\' -> timedelta(microseconds=5)\nb\'PT0.000005S\' -> timedelta(microseconds=5)\n\n\n\'1m 10h 10s\' -> timedelta(seconds=36070)\n\'1minute 10hour 10second\' -> timedelta(seconds=36070)\n\'1minutes 10hours 10seconds\' -> timedelta(seconds=36070)\n\'1 minutes 2 days 10 hours 10 seconds\' -> timedelta(seconds=208870)\n\'1 m 2 d 10 h 10 s\' -> \'1 minute 2 days 10 hours 10 seconds\'\n    -> timedelta(seconds=208870)\n\'1 minute 2 day 10 hours 10 seconds\' -> timedelta(seconds=208870)\n"""\n```\n\n#### Data Pointer Expiration\n\nThe pointer can point to any expiration value. The data pointer can resolve either dictionaries or objects. If you need more flexibility or you are facing issues with the pointer it is recommended to use function/coroutine expiration. That way you ensure you are returning the right value.\n\n```python\nfrom aquiche import alru_cache\nfrom typing import Any\n\n@alru_cache(expiration="$.token.expiration")\nasync def cache_function(value: str) -> Any:\n    expiry = True\n\n    if value == "user":\n        expiry = "30 minutes"\n    if value == "service":\n        expiry = "12 hours"\n    if value == "role":\n        expiry = "2022-01-30T00:00:00+0000"\n\n    return {"token": {"expiration": expiry}}\n```\n\n#### Function and Coroutine Expiration\n\nIt is possible to use both functions and coroutines to set the expiration. If the decorated function is a sync one then it is possible to only use the sync function to set the expiration. `CachedItem` object is passed to the expiration function.\n\n```python\nfrom aquiche import alru_cache, CachedItem\n\n"""\n@dataclass\nclass CachedItem:\n    value: Any -> the returned value or the raised exception*\n    last_fetched: datetime -> when was the value last fetched\n    is_error: bool -> set to true if the value contains a raised exception*\n\n*Only possible when the negative cache is enabled\n"""\n\nasync def is_item_expired_async(item: CachedItem) -> bool:\n    return item.value > 1\n\ndef is_item_expired(item: CachedItem) -> str | datetime:\n    if item.value > 10:\n        return "10s"\n    if item.value > 5:\n        return datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)\n    return "1 day"\n\n# Async function/Coroutine\n# It can be used with both sync and async function expirations\n@alru_cache(expiration=is_item_expired_async)\nasync def cache_function_async_1(value: str) -> int:\n    return len(value)\n\n@alru_cache(expiration=is_item_expired)\nasync def cache_function_async_2(value: str) -> int:\n    return len(value)\n\n# Sync function can only be used with sync function expiration\n@alru_cache(expiration=is_item_expired)\ndef cache_function_sync(value: str) -> int:\n    return len(value)\n\n# Using this function would throw an error\n# @alru_cache(expiration=is_item_expired_async)\n# def cache_function_sync(value: str) -> int:\n#     return len(value)\n```\n\n### "Wrapping" the Async Clients in AsyncExitStack\n\nThe param `wrap_async_exit_stack` simplifies caching of the async clients. When the function is called the returned client(s) (or any other value for that matter) enters the async context. When the client expires or the cache is cleared, the context is automatically cleaned up and renewed if needed. It is recommended to manually clear the cache on the shutdown so the context is closed.\n\n```python\nimport httpx\nfrom database_client import DatabaseClient\nfrom aquiche import alru_cache\nfrom typing import Any\n\n@alru_cache(wrap_async_exit_stack=True)\nasync def get_client() -> httpx.AsyncClient:\n    return httpx.AsyncClient(base_url="https://api.data.io")\n\n\nclient = await get_client()\n# Perform the needed actions\nawait client.post("/raspberry_pi")\n# Once done clear the cache\nawait get_client.clear_cache()\n\n"""\nIn case of returning multiple clients a list of data pointers can be used to wrap them\nIf the data pointer does not point to any value from the result then the error is thrown\nTo prevent the error from happening you can append suffix :ignore_missing to the pointer\n"""\n@alru_cache(\n    wrap_async_exit_stack=["$.clients.database:ignore_missing", "$.clients.http"]\n)\ndef get_clients() -> Any:\n    return {\n        "token": "p@ss123",\n        "clients": {\n            # Both clients will use the same AsyncExitStack context\n            "http": httpx.AsyncClient(),\n            "database": DatabaseClient()\n        }\n    }\n```\n\n### Negative Caching\n\nIt is possible to enable the negative caching. If the negative caching is enabled the errors are not raised but rather cached as if they were the actual results of the function call. The negative cache has a separate expiration which is by default set to 10 seconds. It is recommended to always set the negative cache expiration, preferably to a short duration. The negative caching is disabled by default.\n\n```python\nfrom aquiche import alru_cache\n\n# the function can now return exceptions as well\n@alru_cache(negative_cache=True, negative_expiration="30seconds")\nasync def cache_function(value: str) -> int | Exception:\n    if value == "invalid":\n        raise Exception("Invalid data")\n    return len(value)\n\nawait cache_function("invalid")\nresult = await cache_function("invalid")\n\nassert isinstance(error, Exception)\nassert str(error) == "Invalid data"\n```\n\n### Retry with Exponential Backoff\n\nThe function calls can be retried if they fail. To retry the function call `retry_count` can be set to desired number of retries. The function call is retried with exponential backoff. To set the exponential backoff use the `backoff_in_seconds` param. Both `retry_count` and `backoff_in_seconds` are set to 0 by default.\n\n```python\nfrom aquiche import alru_cache\nimport random\n\n@alru_cache(negative_cache=True, retry_count=3, backoff_in_seconds=2)\nasync def cache_function(value: str) -> int | Exception:\n    if random.randint(1, 10) > 2:\n        raise Exception("Doom has fallen upon us")\n    return len(value)\n```\n\n### Clearing the Cache & Removing Expired Items\n\nThe cache can be cleared either individually or all cached functions can be cleared with a clear all function. The contexts are automatically cleaned up on the cache clear if they were created with the use of `wrap_async_exit_stack` param. If you are only using the decorator with the sync functions there is a sync version of the clear all function which only clears the "sync" function caches.\n\nTo remove the expired items `remove_expired()` function can be called.\n\n```python\nfrom aquiche import alru_cache, clear_all, clear_all_sync\n\n@alru_cache(wrap_async_exit_stack=True)\nasync def cache_function_async(value: str) -> Client:\n    return Client()\n\n@alru_cache(retry_count=3, backoff_in_seconds=2)\ndef cache_function_sync(value: str) -> int:\n    return len(value)\n\n# Clears individual function caches\nawait cache_function_async.clear_cache()\ncache_function_sync.clear_cache()\n\n# Clears both cache_function_sync and cache_function_async\'s cache\nawait clear_all()\n\n# Clears only cache_function_sync\'s cache\nclear_all_sync()\n\n# Removes expired items\nawait cache_function_async.remove_expired()\ncache_function_sync.remove_expired()\n```\n',
    'author': 'Jakub Jantosik',
    'author_email': 'jakub.jantosik@gmail.com',
    'maintainer': 'None',
    'maintainer_email': 'None',
    'url': 'None',
    'packages': packages,
    'package_data': package_data,
    'extras_require': extras_require,
    'entry_points': entry_points,
    'python_requires': '>=3.8,<4.0',
}


setup(**setup_kwargs)
