Skip to content

Commit

Permalink
Do some final touches (#32)
Browse files Browse the repository at this point in the history
* Do some final touches

Updated the pyproject.toml with more metadata, and improved the
README slightly.

* add client version to telemetry

* allow client to be reused in different event loops for the lambda environments

* adjust telemetry sdk format

* remove unnecessary assert
  • Loading branch information
mdumandag authored Jul 10, 2023
1 parent 7eed1d1 commit deada8c
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 47 deletions.
37 changes: 18 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

<!-- toc -->

- [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)
Expand All @@ -32,7 +32,7 @@ The SDK is currently compatible with python 3.8 and above.

## Install

### PyPi
### PyPI
```bash
pip install upstash-redis
```
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 23 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <support@upstash.com>", "Zgîmbău Tudor <tudor.zgimbau@gmail.com>"]
maintainers = ["Upstash <support@upstash.com>"]
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]
Expand Down
14 changes: 14 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
9 changes: 5 additions & 4 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
},
Expand All @@ -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",
},
Expand Down Expand Up @@ -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",
}
Expand All @@ -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",
}
Expand Down
2 changes: 2 additions & 0 deletions upstash_redis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
__version__ = "0.14.5"

from upstash_redis.client import Redis

__all__ = ["Redis"]
74 changes: 56 additions & 18 deletions upstash_redis/asyncio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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__(
Expand All @@ -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":
Expand All @@ -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()
4 changes: 1 addition & 3 deletions upstash_redis/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down
3 changes: 2 additions & 1 deletion upstash_redis/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"):
Expand Down
2 changes: 2 additions & 0 deletions upstash_redis/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit deada8c

Please sign in to comment.