{"id":15288716,"url":"https://github.com/itz-amethyst/cyrus","last_synced_at":"2025-10-05T12:32:47.130Z","repository":{"id":213126879,"uuid":"732375859","full_name":"itz-Amethyst/Cyrus","owner":"itz-Amethyst","description":"⏱️ Cyrus: Simplifying Caching in FastAPI. 📃","archived":false,"fork":false,"pushed_at":"2023-12-18T17:41:30.000Z","size":26,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-13T08:09:55.087Z","etag":null,"topics":["api-cache","cache","caching","fastapi","model","package","pip","redis","redis-cache","router"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/Cyrus-Kit/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/itz-Amethyst.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-12-16T13:24:46.000Z","updated_at":"2024-06-29T16:21:12.000Z","dependencies_parsed_at":"2023-12-18T20:51:18.589Z","dependency_job_id":null,"html_url":"https://github.com/itz-Amethyst/Cyrus","commit_stats":null,"previous_names":["itz-amethyst/perseus","itz-amethyst/cyrus"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itz-Amethyst%2FCyrus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itz-Amethyst%2FCyrus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itz-Amethyst%2FCyrus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itz-Amethyst%2FCyrus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/itz-Amethyst","download_url":"https://codeload.github.com/itz-Amethyst/Cyrus/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248681491,"owners_count":21144700,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-cache","cache","caching","fastapi","model","package","pip","redis","redis-cache","router"],"created_at":"2024-09-30T15:52:27.262Z","updated_at":"2025-10-05T12:32:42.085Z","avatar_url":"https://github.com/itz-Amethyst.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n\u003cp class=\"align-center\" style=\"font-weight: bold\"\u003e\n  ⏱️ Cyrus: Simplifying Caching in FastAPI. 📃\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://media.discordapp.net/attachments/921633563810627588/1186359324709228575/cyrus_1.jpg?ex=6592f638\u0026is=65808138\u0026hm=fef999c02bee2306eebea241d55aae1a2640f543b77c240ac8fe911d577e120c\u0026=\u0026format=webp\u0026width=723\u0026height=416\" alt=\"Cyrus\"/\u003e\n\u003c/p\u003e\n\n------\n\n## Cyrus\n\n[![PyPI version](https://badge.fury.io/py/Cyrus-kit.svg)](https://badge.fury.io/py/Cyrus-kit)\n![PyPI - Downloads](https://img.shields.io/pypi/dm/Cyrus-kit?color=%234DC71F)\n![PyPI - License](https://img.shields.io/pypi/l/Cyrus-kit?color=%25234DC71F)\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/Cyrus-kit)\n\n------\n\n- [Features](#features)\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Initialize Redis](#initialize-redis)\n  - [`@cache` Decorator](#cache-decorator)\n    - [Pre-defined Lifetimes](#pre-defined-lifetimes)\n  - [Cache Keys](#cache-keys)\n  - [Multiple Objects Caching](#multiple-objects-caching)\n- [Last Word](#last-word)\n\n## Features\n\n**Data Caching:**\n- Cache response data for async and non-async path operation functions.operation functions.\n- Tailor the lifespan of cached data for each API endpoint with ease.\n\n**Cache Management:**\n- Dynamically handle requests with `Cache-Control` headers containing `no-cache` or `no-store`, ensuring precise control over caching behavior.\n- Streamline responses for requests with `If-None-Match` headers, providing a status of `304 NOT MODIFIED` when the `ETag` for the requested resource matches the header value.\n\n\n## Installation\n\n`pip install Cyrus-Kit`\n\n## Usage\n\n### Initialize Redis\n\n#### Step 1: Create an Instance\n\nCreate a `Cyrus` instance when your application starts by [defining an event handler for the `\"startup\"` event](https://fastapi.tiangolo.com/advanced/events/) as shown below:\n\n```python {linenos=table}\nimport logging\nfrom fastapi import FastAPI , Request , Response\nfrom Cyrus import Cyrus\nfrom sqlalchemy.orm import Session\n\n# Your logger config if you have otherwise no need (default)\nlogger_system = logging.getLogger(__name__)\n\nREDIS_URL = \"redis-1523.c291.ap-northeast-1-2.ec2.cloud.redislabs.com\"\nREDIS_PASSWORD = \"1234\"\nREDIS_PORT = 1492\n\napp = FastAPI(title = \"FastAPI Redis Cache Example\")\n\n\n@app.on_event(\"startup\")\ndef startup():\n  redis_cache = Cyrus(\n    logger_system = logger_system ,\n    host_url = REDIS_URL ,\n    port = REDIS_PORT ,\n    password = REDIS_PASSWORD ,\n    prefix = \"myapi-cache\" ,\n    response_header = \"X-MyAPI-Cache\" ,\n    ignore_arg_types = [Request , Response , Session]\n  )\n```\n\nAfter creating the instance, the only required argument for this method is the URL for the Redis database (`host_url`). All other arguments are optional:\n\n- `host_url` (`str`) \u0026mdash; Redis database URL. (_**Required**_)\n- `prefix` (`str`) \u0026mdash; Prefix to add to every cache key stored in the Redis database. (_Optional_, defaults to `None`)\n- `response_header` (`str`) \u0026mdash; Name of the custom header field used to identify cache hits/misses. (_Optional_, defaults to `X-FastAPI-Cache`)\n- `ignore_arg_types` (`List[Type[object]]`) \u0026mdash; Cache keys are created (in part) by combining the name and value of each argument used to invoke a path operation function. If any of the arguments have no effect on the response (such as a `Request` or `Response` object), including their type in this list will ignore those arguments when the key is created. (_Optional_, defaults to `[Request, Response]`)\n  - The example shown here includes the `sqlalchemy.orm.Session` type, if your project uses SQLAlchemy as a dependency ([as demonstrated in the FastAPI docs](https://fastapi.tiangolo.com/tutorial/sql-databases/)), you should include `Session` in `ignore_arg_types` in order for cache keys to be created correctly.\n\n- `logger_system` (`logging.Logger`) \u0026mdash; Gets your custom logging config system if you provided for log operation, if not uses the default one.\n- `local` (`bool`) \u0026mdash; Set this to `True` if you use local redis server(Optional, defaults to `False`).\n- `password` (`str`) \u0026mdash; Password for Redis Cloud (Optional, defaults to `None`).\n- `port` (`int`) \u0026mdash; Port number for Redis Cloud (Optional, defaults to `0`).\n\n### `@cache` Decorator\n\nDecorating a path function with `@cache` enables caching for the endpoint. **Response data is only cached for `GET` operations**, If no arguments are provided, responses will be set to expire after one year.\n\n```python\n# WILL NOT be cached\n@app.get(\"/no_cache\")\ndef get_data():\n    return Response(status_code = 200, content = \"Data will not be Cached!\")\n\n# Will be cached for one year\n@app.get(\"/cached_data\")\n@cache()\nasync def get_cached_data():\n    return Response(status_code = 200, content = \"This Data cached for 1 year!\")\n```\n\nResponse data for the API endpoint at `/cached_data` will be cached by the Redis server. Log messages are written to console with logger system you provided or default one:\n\n```console\n18:53:02.081: |\u003cINFO\u003e| [client]: 12/16/2023 06:53:02 PM | CONNECT_BEGIN: Attempting to connect to Redis server...\n18:53:04.343: |\u003cINFO\u003e| [client]: 12/16/2023 06:53:04 PM | CONNECT_SUCCESS: Redis client is connected to server.\n18:53:10.523: |\u003cINFO\u003e| [client]: 12/16/2023 06:53:10 PM | KEY_ADDED_TO_CACHE: key=api.get_cached_data().\n18:53:12.103: |\u003cINFO\u003e| [client]: 12/16/2023 06:53:12 PM | KEY_FOUND_IN_CACHE: key=api.get_cached_data().\n```\n\nThe log messages indicate two successful **`200 OK`** responses for the same request (**`GET /cached_data`**). \n\n- The first request executed the `get_cached_data` function, storing the result in Redis under the key `api.get_cached_data()`. \n- The second request, however, did not execute the `get_cached_data` function. Instead, it retrieved the cached result and served it as the response.\n\nIn typical scenarios, response data should expire much sooner than one year. You can use the `expire` parameter to specify the number of seconds before the data is automatically deleted.\n\n```python\n# Will be cached for thirty seconds\n@app.get(\"/dynamic_data\")\n@cache(expire=30)\ndef get_dynamic_data(request: Request, response: Response):\n    return {\"success\": True, \"message\": \"this data should only be cached temporarily\"}\n```\n\n\u003e **NOTE!** `expire` can be either an `int` value or `timedelta` object. When the TTL is very short (like the example above) this results in a decorator that is expressive and requires minimal effort to parse visually. For durations an hour or longer (e.g., `@cache(expire=86400)`), IMHO, using a `timedelta` object is much easier to grok (`@cache(expire=timedelta(days=1))`).\n\n\n#### Pre-defined Lifetimes\n\nThe decorators listed below define several common durations and can be used in place of the `@cache` decorator:\n\n- `@cache_one_minute`\n- `@cache_one_hour`\n- `@cache_one_day`\n- `@cache_one_week`\n- `@cache_one_month`\n- `@cache_one_year`\n\nFor example, instead of `@cache(expire=timedelta(days=1))`, you could use:\n\n```python\nfrom Cyrus import cache_one_day\n\n@router.get(\"/{id}\", response_model = schemas.PostView)\n@cache_one_day()\ndef get_post_by_id(id:int, db: Session = Depends(get_db)):\n  \n    post = db.query(Post).get(id)\n    \n    if not post:\n        raise HTTPException(status_code = status.HTTP_404_NOT_FOUND, detail = f\"post with this id: {id} was not found\")\n    return post\n```\n\n### Cache Keys\n\nConsider the `/get_user` API route defined below. This is the first path function we have seen where the response depends on the value of an argument (`id: int`). This is a typical CRUD operation where `id` is used to retrieve a `User` record from a database. The API route also includes a dependency that injects a `Session` object (`db`) into the function, [per the instructions from the FastAPI docs](https://fastapi.tiangolo.com/tutorial/sql-databases/#create-a-dependency):\n\n```python\n@router.get('/{id}', response_model = schemas.UserView)\ndef get_user(id: str, db: Session = Depends(get_db)):\n    user = db.query(User).get(id)\n    if not user:\n        raise HTTPException(status_code = status.HTTP_404_NOT_FOUND, detail = f\"User with this id {id} does not exists\")\n    return user\n```\n\n#### Log Messages:\nYou can figure out what is happening in the log messages below:\n\n```console\nINFO:uvicorn.error:Application startup complete.\n18:04:19.690 |\u003cINFO\u003e| [client]: 12/18/2023 06:04:19 PM | KEY_ADDED_TO_CACHE: key=myapi-cache:app.routers.user.get_user(id=XOM-vquaelshNVXS)\n18:04:25.120 |\u003cINFO\u003e| [client]: 12/18/2023 06:04:25 PM | KEY_FOUND_IN_CACHE: key=myapi-cache:app.routers.user.get_user(id=XOM-vquaelshNVXS)\n```\n\nNow, every request for the same `id` generates the same key value (`myapi-cache:app.routers.get_user(id=XOM-vquaelshNVXS)`). As expected, the first request adds the key/value pair to the cache, and each subsequent request retrieves the value from the cache based on the key.\n\n### Multiple Objects Caching\n\nHere is an endpoint from one of my projects:\n\n```python\nfrom Cyrus import cache_one_week\n\n@router.get('/get_all_posts')\n@cache_one_week\ndef get_all_posts(db: Session = Depends(get_db)):\n    return  db.query(Post).all()\n```\n- **1** - The `cache_one_week` decorator is applied to the `get_all_posts` route.\n- **2** - The `get_all_posts` route fetches all posts from the `database` using SQLAlchemy.\n\nHere you can find more information:\n\n```console\nINFO:uvicorn.error:Application startup complete.\n18:12:13.690 |\u003cINFO\u003e| [client]: 12/18/2023 06:12:13 PM | KEY_ADDED_TO_CACHE: key=myapi-cache:app.routers.post.get_all_posts()\n18:12:39.120 |\u003cINFO\u003e| [client]: 12/18/2023 06:12:39 PM | KEY_FOUND_IN_CACHE: key=myapi-cache:app.routers.post.get_all_posts()\n```\n\n\u003e **NOTE!** The current implementation is `not optimal` for handling `multiple` return datasets. There are known issues and potential `bugs` in the existing code. `Contributions` and suggestions for `improvement` are highly encouraged! Feel free to contribute to make it better and more robust. Your input can help enhance the functionality.\n\n\n### Last Word\n🏆\nThank you for exploring our package! Your feedback, bug reports, and contributions are highly valued. If you encounter issues or have ideas for improvements, please open an issue or submit a pull request. Let's build and enhance this package together! 🚀\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fitz-amethyst%2Fcyrus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fitz-amethyst%2Fcyrus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fitz-amethyst%2Fcyrus/lists"}