diff --git a/README.md b/README.md index a1d0014..48a9ed5 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,22 @@ -# Upstash Redis - python edition +# Upstash Redis Python SDK -upstash-redis is a connectionless, HTTP-based Redis client for python, designed to be used in serverless and serverful environments such as: +upstash-redis is a connectionless, HTTP-based Redis client for Python, designed to be used in serverless and serverful environments such as: - AWS Lambda - Vercel Serverless - Google Cloud Functions - and other environments where HTTP is preferred over TCP. Inspired by other Redis clients like [@upstash/redis](https://github.com/upstash/upstash-redis) and [redis-py](https://github.com/redis/redis-py), -the goal of this SDK is to provide a simple way to use Redis in HTTP-based environments. +the goal of this SDK is to provide a simple way to use Redis over the [Upstash REST API](https://docs.upstash.com/redis/features/restapi). -The SDK is currently compatible with python 3.8 and above. +The SDK is currently compatible with Python 3.8 and above. -- [Upstash Redis - python edition](#upstash-redis---python-edition) +- [Upstash Redis Python SDK](#upstash-redis-python-sdk) - [Quick Start](#quick-start) - [Install](#install) - - [PyPi](#pypi) + - [PyPI](#pypi) - [Usage](#usage) - [BITFIELD and BITFIELD\_RO](#bitfield-and-bitfield_ro) - [Custom commands](#custom-commands) @@ -32,7 +32,7 @@ The SDK is currently compatible with python 3.8 and above. ## Install -### PyPi +### PyPI ```bash pip install upstash-redis ``` @@ -42,10 +42,9 @@ To be able to use upstash-redis, you need to create a database on [Upstash](http and grab `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` from the console. ```python -# In this method, you can also give specific parameters for the Redis instance instance, such as rest_retries and so on - # for sync client from upstash_redis import Redis + redis = Redis(url="UPSTASH_REDIS_REST_URL", token="UPSTASH_REDIS_REST_TOKEN") # for async client @@ -96,16 +95,16 @@ One particular case is represented by these two chained commands, which are avai the `BITFIELD` and, respectively, `BITFIELD_RO` classes. Use the `execute` function to run the commands. ```python - redis.bitfield("test_key") - .incrby(encoding="i8", offset=100, increment=100) - .overflow("SAT") - .incrby(encoding="i8", offset=100, increment=100) - .execute() - - redis.bitfield_ro("test_key_2") - .get(encoding="u8", offset=0) - .get(encoding="u8", offset="#1") - .execute() +redis.bitfield("test_key") \ + .incrby(encoding="i8", offset=100, increment=100) \ + .overflow("SAT") \ + .incrby(encoding="i8", offset=100, increment=100) \ + .execute() + +redis.bitfield_ro("test_key_2") \ + .get(encoding="u8", offset=0) \ + .get(encoding="u8", offset="#1") \ + .execute() ``` ### Custom commands diff --git a/pyproject.toml b/pyproject.toml index 8edb6cd..d73a98d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,30 @@ [tool.poetry] -name = "upstash_redis" +name = "upstash-redis" version = "0.14.5" -description = "Serverless Redis Sdk from Upstash" +description = "Serverless Redis SDK from Upstash" +license = "MIT" authors = ["Upstash ", "Zgîmbău Tudor "] +maintainers = ["Upstash "] readme = "README.md" +repository = "https://github.com/upstash/redis-python" +keywords = ["Upstash Redis", "Serverless Redis"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Database", + "Topic :: Database :: Front-Ends", + "Topic :: Software Development :: Libraries", +] packages = [{ include = "upstash_redis" }] [tool.poetry.dependencies] diff --git a/tests/test_client.py b/tests/test_client.py index 627ebe8..fcad5cf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -39,3 +39,17 @@ async def coro() -> str: return result assert asyncio.run(coro()) == "hey" + + +def test_async_redis_reuse_in_different_event_loops() -> None: + redis = AsyncRedis.from_env(allow_telemetry=False) + + async def coro(close: bool) -> str: + result = await redis.ping("hey") + if close: + await redis.close() + + return result + + assert asyncio.run(coro(False)) == "hey" + assert asyncio.run(coro(True)) == "hey" diff --git a/tests/test_http.py b/tests/test_http.py index a83627a..80c771b 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -9,6 +9,7 @@ from pytest import mark, raises from requests import Session +from upstash_redis import __version__ from upstash_redis.errors import UpstashError from upstash_redis.http import async_execute, decode, make_headers, sync_execute @@ -188,7 +189,7 @@ def test_decode(arg: Any, expected: Any) -> None: True, { "Authorization": "Bearer token", - "Upstash-Telemetry-Sdk": "upstash_redis@python", + "Upstash-Telemetry-Sdk": f"py-upstash-redis@v{__version__}", "Upstash-Telemetry-Runtime": f"python@v{python_version()}", "Upstash-Telemetry-Platform": "unknown", }, @@ -200,7 +201,7 @@ def test_decode(arg: Any, expected: Any) -> None: { "Authorization": "Bearer token", "Upstash-Encoding": "base64", - "Upstash-Telemetry-Sdk": "upstash_redis@python", + "Upstash-Telemetry-Sdk": f"py-upstash-redis@v{__version__}", "Upstash-Telemetry-Runtime": f"python@v{python_version()}", "Upstash-Telemetry-Platform": "unknown", }, @@ -228,7 +229,7 @@ def test_make_headers_on_vercel() -> None: with patch("os.getenv", side_effect=lambda arg: arg if arg == "VERCEL" else None): assert make_headers("token", None, True) == { "Authorization": "Bearer token", - "Upstash-Telemetry-Sdk": "upstash_redis@python", + "Upstash-Telemetry-Sdk": f"py-upstash-redis@v{__version__}", "Upstash-Telemetry-Runtime": f"python@v{python_version()}", "Upstash-Telemetry-Platform": "vercel", } @@ -240,7 +241,7 @@ def test_make_headers_on_aws() -> None: ): assert make_headers("token", None, True) == { "Authorization": "Bearer token", - "Upstash-Telemetry-Sdk": "upstash_redis@python", + "Upstash-Telemetry-Sdk": f"py-upstash-redis@v{__version__}", "Upstash-Telemetry-Runtime": f"python@v{python_version()}", "Upstash-Telemetry-Platform": "aws", } diff --git a/upstash_redis/__init__.py b/upstash_redis/__init__.py index d1c73f1..3a8f7fc 100644 --- a/upstash_redis/__init__.py +++ b/upstash_redis/__init__.py @@ -1,3 +1,5 @@ +__version__ = "0.14.5" + from upstash_redis.client import Redis __all__ = ["Redis"] diff --git a/upstash_redis/asyncio/client.py b/upstash_redis/asyncio/client.py index 875f05d..0d82c23 100644 --- a/upstash_redis/asyncio/client.py +++ b/upstash_redis/asyncio/client.py @@ -38,7 +38,7 @@ def __init__( self._rest_retry_interval = rest_retry_interval self._headers = make_headers(token, rest_encoding, allow_telemetry) - self._session: Optional[ClientSession] = None + self._context_manager: Optional[_SessionContextManager] = None @classmethod def from_env( @@ -67,6 +67,9 @@ def from_env( ) async def __aenter__(self) -> "Redis": + self._context_manager = _SessionContextManager( + ClientSession(), close_session=False + ) return self async def __aexit__( @@ -81,28 +84,30 @@ async def close(self) -> None: """ Closes the resources associated with the client. """ - if self._session: - await self._session.close() + if self._context_manager: + await self._context_manager.close_session() + self._context_manager = None async def execute(self, command: List) -> RESTResultT: """ Executes the given command. """ - if not self._session: - # We had to initialize session here, under an - # async method, so that the session can bind - # to the running event loop. - self._session = ClientSession() - - res = await async_execute( - session=self._session, - url=self._url, - headers=self._headers, - encoding=self._rest_encoding, - retries=self._rest_retries, - retry_interval=self._rest_retry_interval, - command=command, - ) + context_manager = self._context_manager + if not context_manager: + context_manager = _SessionContextManager( + ClientSession(), close_session=True + ) + + async with context_manager: + res = await async_execute( + session=context_manager.session, + url=self._url, + headers=self._headers, + encoding=self._rest_encoding, + retries=self._rest_retries, + retry_interval=self._rest_retry_interval, + command=command, + ) main_command = command[0] if len(command) > 1 and main_command == "SCRIPT": @@ -112,3 +117,36 @@ async def execute(self, command: List) -> RESTResultT: return FORMATTERS[main_command](res, command) return res + + +class _SessionContextManager: + """ + Allows a session to be re-used in multiple async with + blocks when the `close_session` is False. + + Main use case is to re-use sessions in multiple HTTP + requests when the client is used in an async with block. + + The logic around the places in which we use this class is + required so that the same client can be re-used even in + different event loops, one after another. + """ + + def __init__(self, session: ClientSession, close_session: bool) -> None: + self.session = session + self._close_session = close_session + + async def close_session(self) -> None: + await self.session.close() + + async def __aenter__(self) -> ClientSession: + return self.session + + async def __aexit__( + self, + exc_type: Union[Type[BaseException], None], + exc_val: Union[BaseException, None], + exc_tb: Any, + ) -> None: + if self._close_session: + await self.close_session() diff --git a/upstash_redis/commands.py b/upstash_redis/commands.py index c7b86b2..2567e62 100644 --- a/upstash_redis/commands.py +++ b/upstash_redis/commands.py @@ -129,9 +129,7 @@ def echo(self, message: str) -> ResponseT: return self.execute(command) - def copy( - self, source: str, destination: str, replace: bool = False - ) -> ResponseT: + def copy(self, source: str, destination: str, replace: bool = False) -> ResponseT: """ See https://redis.io/commands/copy """ diff --git a/upstash_redis/http.py b/upstash_redis/http.py index ac89132..f200923 100644 --- a/upstash_redis/http.py +++ b/upstash_redis/http.py @@ -9,6 +9,7 @@ from aiohttp import ClientSession from requests import Session +from upstash_redis import __version__ from upstash_redis.errors import UpstashError from upstash_redis.typing import RESTResultT @@ -24,7 +25,7 @@ def make_headers( headers["Upstash-Encoding"] = encoding if allow_telemetry: - headers["Upstash-Telemetry-Sdk"] = "upstash_redis@python" + headers["Upstash-Telemetry-Sdk"] = f"py-upstash-redis@v{__version__}" headers["Upstash-Telemetry-Runtime"] = f"python@v{python_version()}" if os.getenv("VERCEL"): diff --git a/upstash_redis/typing.py b/upstash_redis/typing.py index 47d4a35..7ea0cd7 100644 --- a/upstash_redis/typing.py +++ b/upstash_redis/typing.py @@ -8,10 +8,12 @@ # "str" allows for "-inf" and "+inf". Not to be confused with the lexical min and max type (which is "str"). FloatMinMaxT = Union[float, str] + class GeoSearchResult(TypedDict, total=False): """ Represents the result of the geo-search related commands. """ + member: str distance: float hash: int