Skip to content

Commit

Permalink
add redis extension (#39)
Browse files Browse the repository at this point in the history
* add redis extension

* add redis documentation
  • Loading branch information
livioribeiro authored Feb 19, 2024
1 parent b53c73a commit 08b19e1
Show file tree
Hide file tree
Showing 26 changed files with 923 additions and 91 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: 3.x
- run: pip install "mkdocs-material>=9.4,<9.5"
- run: pip install "mkdocs-material>=9.5,<9.6"
- run: mkdocs gh-deploy --force
24 changes: 18 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ on:
branches:
- main

env:
POSTGRES_DB: test_db

jobs:
test:
runs-on: ubuntu-latest
Expand All @@ -28,16 +25,31 @@ jobs:
image: postgres:16.1-alpine
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: ${{ env.POSTGRES_DB }}
POSTGRES_DB: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: bitnami/redis:7.2.4
env:
REDIS_PASSWORD: password
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- run: pip install poetry
- run: poetry install --with test --extras jinja --extras sqlalchemy --no-interaction
- run: poetry install
--with test
--extras jinja
--extras sqlalchemy
--extras redis
--no-interaction
- env:
POSTGRES_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/${{ env.POSTGRES_DB }}
POSTGRES_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/test
REDIS_URL: redis://default:password@redis:6379/0
run: poetry run pytest
2 changes: 1 addition & 1 deletion docs/extensions/jinja.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Jinja

This extension offers support for Jinja templates.
This extension provides support for Jinja templates.

## Usage

Expand Down
214 changes: 214 additions & 0 deletions docs/extensions/redis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# Redis

This extension provides support for connecting to Redis servers. It registers the
`redis.asyncio.Redis` service.

## Usage

First install the `redis` extra:

```shell
pip install selva[redis]
```

Define the configuration properties:

=== "configuration/settings.yaml"

```yaml
extensions:
- selva.ext.data.redis # (1)

data:
redis:
default: # (2)
url: redis://localhost:6379/0
other: # (3)
url: redis://localhost:6379/1
```

1. Activate the sqlalchemy extension
2. "default" connection will be registered without a name
3. Connection registered with name "other"

Inject the `Redis` service:

```python
from typing import Annotated
from redis.asyncio import Redis
from selva.di import service, Inject


@service
class MyService:
# default service
redis: Annotated[Redis, Inject]

# named service
other_redis: Annotated[Redis, Inject(name="other")]
```

Redis connections can also be defined with username and password separated from
the url, or even with individual components:

=== "configuration/settings.yaml"

```yaml
data:
redis:
url_username_password: # (1)
url: redis://localhost:6379/0
username: user
password: pass

individual_components: # (2)
host: localhost
port: 6379
db: 0
username: user
password: pass
```

1. Username and password separated from the redis url
2. Each component defined individually

## Using environment variables

=== "configuration/settings.yaml"

```yaml
data:
redis:
default:
url: "${REDIS_URL}" # (1)

other: # (2)
url: "${REDIS_URL}"
username: "${REDIS_USERNAME}"
password: "${REDIS_PASSWORD}"

another: # (3)
host: "${REDIS_HOST}"
port: ${REDIS_PORT}
db: "${REDIS_DB}"
username: "${REDIS_USERNAME}"
password: "${REDIS_PASSWORD}"
```

1. Can be define with just the environment variable `SELVA__DATA__REDIS__DEFAULT__URL`
2. Can be defined with just the environment variables:
- `SELVA__DATA__REDIS__OTHER__URL`
- `SELVA__DATA__REDIS__OTHER__USERNAME`
- `SELVA__DATA__REDIS__OTHER__PASSWORD`
3. Can be defined with just the environment variables:
- `SELVA__DATA__REDIS__ANOTHER__HOST`
- `SELVA__DATA__REDIS__ANOTHER__PORT`
- `SELVA__DATA__REDIS__ANOTHER__DB`
- `SELVA__DATA__REDIS__ANOTHER__USERNAME`
- `SELVA__DATA__REDIS__ANOTHER__PASSWORD`

## Example

=== "application/controller.py"

```python
from typing import Annotated

from redis.asyncio import Redis

from asgikit.responses import respond_json

from selva.di import Inject
from selva.web import controller, get


@controller
class Controller:
redis: Annotated[Redis, Inject]

async def initialize(self):
await self.redis.set("number", 0, nx=True, ex=60)

@get
async def index(self, request):
number = await self.redis.incr("number")
await respond_json(request.response, {"number": number})
```

=== "configuration/settings.yaml"

```yaml
data:
redis:
default:
url: "redis://localhost:6379/0"
```

## Configuration options

Selva offers several options to configure Redis. If you need more control over
the SQLAlchemy services, you can create your own `redis.asyncio.Redis` outside
of the DI context.

The available options are shown below:

```yaml
data:
redis:
default:
url: ""
host: ""
port: 6379
db: 0
username: ""
password: ""
options: # (1)
socket_timeout: 1.0
socket_connect_timeout: 1.0
socket_keepalive: false
socket_keepalive_options: {}
unix_socket_path: ""
encoding: ""
encoding_errors: "strict" # or "ignore", "replace"
decode_responses: false
retry_on_timeout: false
retry_on_error: []
ssl: false
ssl_keyfile: ""
ssl_certfile: ""
ssl_cert_reqs: ""
ssl_ca_certs: ""
ssl_ca_data: ""
ssl_check_hostname: false
max_connections: 1
single_connection_client: false
health_check_interval: 1
client_name: ""
lib_name: ""
lib_version: ""
auto_close_connection_pool: false
protocol: 3
retry:
retries: 1
supported_errors: [] # (2)
backoff: # (3)
no_backoff:
constant:
backoff: 1
exponential:
cap: 1
base: 1
full_jitter:
cap: 1
base: 1
equal_jitter:
cap: 1
base: 1
decorrelated_jitter:
cap: 1
base: 1
```
1. `options` values are described in [`redis.asyncio.Redis`](https://redis.readthedocs.io/en/stable/connections.html#async-client).
2. Dotted path to python classes.
3. Only one option in `backoff` should be set.
5 changes: 3 additions & 2 deletions docs/extensions/sqlalchemy.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ injection context.

## Usage

Install SQLAlchemy python package and the database driver:
Install SQLAlchemy extra and a database driver that supports async:

```shell
pip install selva[sqlalchemy] aiosqlite asyncpg aiomysql oracledb
Expand Down Expand Up @@ -53,12 +53,13 @@ class MyService:
# default service
sessionmaker: Annotated[async_sessionmaker, Inject]

# named services
sessionmaker_postgres: Annotated[async_sessionmaker, Inject(name="postgres")]
sessionmaker_mysql: Annotated[async_sessionmaker, Inject(name="mysql")]
sessionmaker_oracle: Annotated[async_sessionmaker, Inject(name="oracle")]
```

Database connection can also be defined with username and password separated from
Database connections can also be defined with username and password separated from
the url, or even with individual components:

```yaml
Expand Down
Empty file.
17 changes: 17 additions & 0 deletions examples/redis/application/controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Annotated

from asgikit.responses import respond_json
from selva.di import Inject
from selva.web import controller, get

from .service import RedisService


@controller
class Controller:
redis_service: Annotated[RedisService, Inject]

@get
async def index(self, request):
number = await self.redis_service.get_incr()
await respond_json(request.response, {"number": number})
17 changes: 17 additions & 0 deletions examples/redis/application/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Annotated

from redis.asyncio import Redis

from selva.di import service, Inject


@service
class RedisService:
redis: Annotated[Redis, Inject]

async def initialize(self):
await self.redis.set("number", 0, ex=60)

async def get_incr(self) -> int:
return await self.redis.incr("number")

16 changes: 16 additions & 0 deletions examples/redis/configuration/settings.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
extensions:
- selva.ext.data.redis

data:
redis:
default:
host: "localhost"
options:
retry:
retries: 1
backoff:
constant:
backoff: 1

other:
url: "redis://localhost:6379/1"
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ nav:
- Extensions:
- extensions/overview.md
- extensions/sqlalchemy.md
- extensions/redis.md
- extensions/jinja.md
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ python-dotenv = "^1.0"
"ruamel.yaml" = "^0.18"
jinja2 = { version = "^3.1", optional = true }
SQLAlchemy = { version = "^2.0", optional = true }
redis = { version = "^5.0", optional = true }

[tool.poetry.extras]
jinja = ["jinja2"]
sqlalchemy = ["SQLAlchemy"]
redis = ["redis"]

[tool.poetry.group.dev]
optional = true
Expand Down Expand Up @@ -74,7 +76,7 @@ ruff = "^0.2"
optional = true

[tool.poetry.group.docs.dependencies]
mkdocs-material = "^9.4"
mkdocs-material = "^9.5"

[tool.pytest.ini_options]
asyncio_mode = "auto"
Expand Down
5 changes: 4 additions & 1 deletion src/selva/configuration/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@
"paths": ["resources/templates"],
"jinja": {},
},
"data": {"sqlalchemy": {}},
"data": {
"redis": {},
"sqlalchemy": {},
},
}
Loading

0 comments on commit 08b19e1

Please sign in to comment.