redis-lua-py 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- redis_lua_py-0.5.0/.release-please-manifest.json +3 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/CHANGELOG.md +10 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/PKG-INFO +9 -1
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/README.md +8 -0
- redis_lua_py-0.5.0/docs/guide/build-time.md +152 -0
- redis_lua_py-0.5.0/docs/guide/redis-functions.md +139 -0
- redis_lua_py-0.5.0/docs/reference/api.md +284 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/reference/errors.md +16 -5
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/reference/lua-vs-python.md +81 -6
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/reference/supported-subset.md +18 -8
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/pyproject.toml +4 -1
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/__init__.py +15 -6
- redis_lua_py-0.5.0/src/redis_lua_py/__main__.py +73 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_compile.py +875 -81
- redis_lua_py-0.5.0/src/redis_lua_py/_library.py +256 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_lua.py +24 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_script.py +82 -36
- redis_lua_py-0.5.0/src/redis_lua_py/codegen.py +221 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/errors.py +9 -0
- redis_lua_py-0.5.0/tests/codegen_scripts.py +41 -0
- redis_lua_py-0.5.0/tests/test_codegen.py +229 -0
- redis_lua_py-0.5.0/tests/test_control_flow.py +322 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_errors.py +29 -34
- redis_lua_py-0.5.0/tests/test_functions.py +223 -0
- redis_lua_py-0.5.0/tests/test_strings_and_indexing.py +284 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_typing.py +19 -1
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/uv.lock +1 -1
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/zensical.toml +2 -0
- redis_lua_py-0.4.0/.release-please-manifest.json +0 -3
- redis_lua_py-0.4.0/docs/reference/api.md +0 -146
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/README.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/banner-dark.svg +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/banner-light.svg +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/generate.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/logo.svg +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/logomark.svg +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/social-preview.png +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/social-preview.svg +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/workflows/ci.yml +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/workflows/docs.yml +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/workflows/pr-title.yml +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/workflows/release.yml +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.gitignore +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.python-version +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/CONTRIBUTING.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/LICENSE +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/assets/logo.svg +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/contributing.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/development.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/examples.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/async.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/binary-values.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/binding-a-client.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/calling-redis-commands.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/constants.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/generated-lua.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/keys-and-arguments.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/return-values.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/testing.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/index.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/installation.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/quickstart.md +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/stylesheets/extra.css +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/release-please-config.json +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/scripts/generate_commands.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_commands.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_runtime.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/py.typed +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/conftest.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/module_with_client.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/module_without_import.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_binary.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_binding.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_commands.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_compile.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_constants.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_execute.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_mistranslations.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_namespace.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_semantics.py +0 -0
- {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_table_stakes.py +0 -0
|
@@ -7,6 +7,16 @@ are written by [release-please](https://github.com/googleapis/release-please)
|
|
|
7
7
|
from the conventional commit subjects on `main`; see
|
|
8
8
|
[CONTRIBUTING.md](CONTRIBUTING.md) for how a release is cut.
|
|
9
9
|
|
|
10
|
+
## [0.5.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.4.0...v0.5.0) (2026-09-12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
* compile continue, try/except/else/finally, raise and assert ([#11](https://github.com/IgnaceMaes/redis-lua-py/issues/11)) ([aeca575](https://github.com/IgnaceMaes/redis-lua-py/commit/aeca5757fffe42e88b49b18fa4d3fe91897fad2e))
|
|
16
|
+
* compile membership, slicing, string methods and formatting, and key variables ([#13](https://github.com/IgnaceMaes/redis-lua-py/issues/13)) ([4880c81](https://github.com/IgnaceMaes/redis-lua-py/commit/4880c8156d849755259dea9a8c29cf69392ecd7e))
|
|
17
|
+
* compile redis functions libraries, and script flags ([#15](https://github.com/IgnaceMaes/redis-lua-py/issues/15)) ([5980f03](https://github.com/IgnaceMaes/redis-lua-py/commit/5980f0317acbe9a7a96ffaff029f2d76143b7e32))
|
|
18
|
+
* generate lua ahead of time for code that ships without redis-lua-py ([#14](https://github.com/IgnaceMaes/redis-lua-py/issues/14)) ([83d6b9d](https://github.com/IgnaceMaes/redis-lua-py/commit/83d6b9da0b540cefc0be9ec51fbead54c4d0fade))
|
|
19
|
+
|
|
10
20
|
## [0.4.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.3.0...v0.4.0) (2026-09-12)
|
|
11
21
|
|
|
12
22
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: redis-lua-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Write Redis Lua scripts as real Python functions, not strings.
|
|
5
5
|
Project-URL: Homepage, https://ignacemaes.com/redis-lua-py/
|
|
6
6
|
Project-URL: Documentation, https://ignacemaes.com/redis-lua-py/
|
|
@@ -144,6 +144,14 @@ script cache is cold once per script rather than once per environment.
|
|
|
144
144
|
the same script object, and
|
|
145
145
|
**[`bind`](https://ignacemaes.com/redis-lua-py/guide/binding-a-client/)** when
|
|
146
146
|
passing the client every time gets repetitive.
|
|
147
|
+
- **[Build-time generation](https://ignacemaes.com/redis-lua-py/guide/build-time/)**
|
|
148
|
+
for libraries — `python -m redis_lua_py generate` writes the Lua to a module
|
|
149
|
+
of plain strings, so your users never depend on this package, and `--check`
|
|
150
|
+
keeps it current in CI.
|
|
151
|
+
- **[Redis Functions](https://ignacemaes.com/redis-lua-py/guide/redis-functions/)** —
|
|
152
|
+
the same Python compiles into a function library, loaded with `FUNCTION LOAD`
|
|
153
|
+
on first use and called with `FCALL`, and scripts take Redis 7 flags such as
|
|
154
|
+
`no-writes`.
|
|
147
155
|
- **[The gaps between Lua and Python](https://ignacemaes.com/redis-lua-py/reference/lua-vs-python/)**
|
|
148
156
|
are closed or refused — truthiness, 1-based indexing, `false` versus `nil`,
|
|
149
157
|
block scope, and the nil that truncates a returned table.
|
|
@@ -118,6 +118,14 @@ script cache is cold once per script rather than once per environment.
|
|
|
118
118
|
the same script object, and
|
|
119
119
|
**[`bind`](https://ignacemaes.com/redis-lua-py/guide/binding-a-client/)** when
|
|
120
120
|
passing the client every time gets repetitive.
|
|
121
|
+
- **[Build-time generation](https://ignacemaes.com/redis-lua-py/guide/build-time/)**
|
|
122
|
+
for libraries — `python -m redis_lua_py generate` writes the Lua to a module
|
|
123
|
+
of plain strings, so your users never depend on this package, and `--check`
|
|
124
|
+
keeps it current in CI.
|
|
125
|
+
- **[Redis Functions](https://ignacemaes.com/redis-lua-py/guide/redis-functions/)** —
|
|
126
|
+
the same Python compiles into a function library, loaded with `FUNCTION LOAD`
|
|
127
|
+
on first use and called with `FCALL`, and scripts take Redis 7 flags such as
|
|
128
|
+
`no-writes`.
|
|
121
129
|
- **[The gaps between Lua and Python](https://ignacemaes.com/redis-lua-py/reference/lua-vs-python/)**
|
|
122
130
|
are closed or refused — truthiness, 1-based indexing, `false` versus `nil`,
|
|
123
131
|
block scope, and the nil that truncates a returned table.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Shipping without the dependency
|
|
2
|
+
|
|
3
|
+
Calling a script needs redis-lua-py at runtime: it compiles the body when the
|
|
4
|
+
module is imported, and resolves keys and arguments on every call. In an
|
|
5
|
+
application that is a dependency you chose. In a library it is a dependency
|
|
6
|
+
every one of your users inherits, along with the import-time compile, for Lua
|
|
7
|
+
that never changes between your releases.
|
|
8
|
+
|
|
9
|
+
So a library can compile ahead of time instead. You write the scripts with
|
|
10
|
+
`@script`, as anywhere else, and generate plain Lua from them while you
|
|
11
|
+
develop. What you ship is that Lua: no import of this package, no compile
|
|
12
|
+
step, nothing added to your dependencies.
|
|
13
|
+
|
|
14
|
+
## Generate a module of strings
|
|
15
|
+
|
|
16
|
+
Keep the scripts outside the package you ship, and redis-lua-py in your
|
|
17
|
+
development dependencies:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv add --dev redis-lua-py
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
myproj/
|
|
25
|
+
├── pyproject.toml
|
|
26
|
+
├── redis_scripts/
|
|
27
|
+
│ └── limits.py # the @script functions; never shipped
|
|
28
|
+
└── src/myproj/
|
|
29
|
+
├── _lua.py # generated, checked in, shipped
|
|
30
|
+
└── limits.py # the code that runs them
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then, from the project root:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m redis_lua_py generate redis_scripts.limits --out src/myproj/_lua.py
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The output imports nothing. Each script is a string constant, named after the
|
|
40
|
+
function in capitals, with a comment recording the order its keys and
|
|
41
|
+
arguments go in:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
"""Redis Lua scripts compiled by redis-lua-py from redis_scripts.limits. Do not edit.
|
|
45
|
+
|
|
46
|
+
Regenerate with:
|
|
47
|
+
|
|
48
|
+
python -m redis_lua_py generate redis_scripts.limits --out src/myproj/_lua.py
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"RATE_LIMIT",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# rate_limit -- KEYS: key; ARGV: limit, ttl
|
|
56
|
+
RATE_LIMIT = """\
|
|
57
|
+
-- rate_limit
|
|
58
|
+
-- Generated by redis-lua-py from redis_scripts/limits.py:6. Do not edit.
|
|
59
|
+
local key = KEYS[1]
|
|
60
|
+
local limit = tonumber(ARGV[1])
|
|
61
|
+
local ttl = tonumber(ARGV[2])
|
|
62
|
+
local current = redis.call('INCR', key)
|
|
63
|
+
if current == 1 then
|
|
64
|
+
redis.call('EXPIRE', key, ttl)
|
|
65
|
+
end
|
|
66
|
+
if current > limit then
|
|
67
|
+
return -1
|
|
68
|
+
end
|
|
69
|
+
return limit - current
|
|
70
|
+
"""
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Your library runs it the way it would run any Lua, through redis-py:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from redis import Redis
|
|
77
|
+
|
|
78
|
+
from ._lua import RATE_LIMIT
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Limiter:
|
|
82
|
+
def __init__(self, client: Redis) -> None:
|
|
83
|
+
self._rate_limit = client.register_script(RATE_LIMIT)
|
|
84
|
+
|
|
85
|
+
def hit(self, key: str, limit: int, ttl: int) -> int:
|
|
86
|
+
return self._rate_limit(keys=[key], args=[limit, ttl])
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Every path in the file is relative to the project root, so it comes out
|
|
90
|
+
byte-for-byte the same on every machine, and regenerating it without a change
|
|
91
|
+
to the scripts leaves it untouched.
|
|
92
|
+
|
|
93
|
+
## Or one `.lua` file per script
|
|
94
|
+
|
|
95
|
+
Pass a directory instead of a `.py` path:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python -m redis_lua_py generate redis_scripts.limits --out src/myproj/lua/
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
That writes `rate_limit.lua` and a file for every other script, for a library
|
|
102
|
+
that already loads its Lua from files. When a script is renamed or removed, its
|
|
103
|
+
old file is deleted. Only files carrying the generated header are ever
|
|
104
|
+
deleted, so hand-written Lua in the same directory is safe. The flip side is
|
|
105
|
+
that a script compiled with `header=False` leaves its old file behind when it
|
|
106
|
+
is renamed.
|
|
107
|
+
|
|
108
|
+
## Keep it current
|
|
109
|
+
|
|
110
|
+
Checking generated code in only works if something notices when it goes stale.
|
|
111
|
+
`--check` writes nothing, and exits 1 with a diff if the output no longer
|
|
112
|
+
matches the scripts. Run it in CI:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
- run: uv run python -m redis_lua_py generate redis_scripts.limits --out src/myproj/_lua.py --check
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Or as a test, which fails with the same diff:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from redis_lua_py import codegen
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_generated_lua_is_current():
|
|
125
|
+
codegen.check("redis_scripts.limits", "src/myproj/_lua.py")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A test imports `redis_scripts.limits` like any other module, so the project
|
|
129
|
+
root has to be on the path. With pytest, set `pythonpath = ["."]` under
|
|
130
|
+
`[tool.pytest.ini_options]`.
|
|
131
|
+
|
|
132
|
+
## What you give up
|
|
133
|
+
|
|
134
|
+
The generated Lua is identical to what a `@script` call would send, so the
|
|
135
|
+
behaviour inside Redis is the same. What stays behind is the caller's side:
|
|
136
|
+
|
|
137
|
+
- **Argument checking.** A call through `CompiledScript` refuses a missing,
|
|
138
|
+
misspelled or duplicated argument, and a value such as `None` that has no
|
|
139
|
+
Redis representation. With a plain string, you pass `keys` and `args` in the
|
|
140
|
+
order the comment records, and nothing checks them.
|
|
141
|
+
- **Booleans.** A `CompiledScript` sends `True` as `1`. redis-py refuses a
|
|
142
|
+
`bool` outright, so convert it yourself.
|
|
143
|
+
- **Cluster pipelines.** A `CompiledScript` switches to `EVAL` on a redis-py
|
|
144
|
+
cluster pipeline, which refuses `EVALSHA`. A registered string does not.
|
|
145
|
+
|
|
146
|
+
Keep writing tests that call the scripts through `@script` against
|
|
147
|
+
[fakeredis](testing.md#run-the-behaviour-without-a-server) as well: those run
|
|
148
|
+
the same Lua, with the checks still in place.
|
|
149
|
+
|
|
150
|
+
The script module itself is ordinary Python, so point your type checker and
|
|
151
|
+
linter at it along with the rest of the project. Lines in the generated file
|
|
152
|
+
are as long as the Lua's, so leave that one file out of any line-length rule.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Redis Functions
|
|
2
|
+
|
|
3
|
+
A script is sent with `EVALSHA`, and lives in a cache the server is free to
|
|
4
|
+
drop. Since Redis 7 there is an alternative: a **function library**. It is
|
|
5
|
+
loaded once with `FUNCTION LOAD`, persists and replicates with the data, and
|
|
6
|
+
each function in it is called by name with `FCALL`.
|
|
7
|
+
|
|
8
|
+
The same Python compiles to either. A body follows exactly the rules of a
|
|
9
|
+
script's.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from redis_lua_py import Key, Library, redis
|
|
13
|
+
|
|
14
|
+
limits = Library("limits")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@limits.function
|
|
18
|
+
def hit(key: Key, ttl: int) -> int:
|
|
19
|
+
current = redis.incr(key)
|
|
20
|
+
if current == 1:
|
|
21
|
+
redis.expire(key, ttl)
|
|
22
|
+
return current
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@limits.function(flags=["no-writes"])
|
|
26
|
+
def peek(key: Key) -> int:
|
|
27
|
+
return int(redis.get(key) or 0)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
hit(client, key="user:42", ttl=60) # FCALL hit 1 user:42 60
|
|
32
|
+
peek(client, key="user:42") # FCALL_RO peek 1 user:42
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A function is called like a script: pass a sync client and you get a value,
|
|
36
|
+
an async one and you get an awaitable, and [`bind`](binding-a-client.md)
|
|
37
|
+
works the same way.
|
|
38
|
+
|
|
39
|
+
## Loading
|
|
40
|
+
|
|
41
|
+
There is nothing to do up front. When a call finds that the server does not
|
|
42
|
+
have the library, it loads it with `FUNCTION LOAD REPLACE` and tries again.
|
|
43
|
+
|
|
44
|
+
To load it explicitly, at deploy time for instance:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
limits.load(client)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
!!! warning "Load before a pipeline"
|
|
51
|
+
|
|
52
|
+
A call queued in a pipeline cannot load the library halfway through
|
|
53
|
+
`execute()`. Call `load()` before queueing calls to a library the server
|
|
54
|
+
may not have yet.
|
|
55
|
+
|
|
56
|
+
On Redis Cluster, redis-py sends `FUNCTION LOAD` to every primary, and routes
|
|
57
|
+
`FCALL` on the function's keys, as it does a script.
|
|
58
|
+
|
|
59
|
+
## What the library looks like
|
|
60
|
+
|
|
61
|
+
`limits.lua` is the whole library, exactly as `FUNCTION LOAD` receives it:
|
|
62
|
+
|
|
63
|
+
```lua
|
|
64
|
+
#!lua name=limits
|
|
65
|
+
-- Generated by redis-lua-py. Do not edit.
|
|
66
|
+
-- Python truthiness: 0, '', empty tables and nil are all false.
|
|
67
|
+
local function __truthy(v)
|
|
68
|
+
if v == nil or v == false then return false end
|
|
69
|
+
if v == 0 or v == '' then return false end
|
|
70
|
+
if type(v) == 'table' and next(v) == nil then return false end
|
|
71
|
+
return true
|
|
72
|
+
end
|
|
73
|
+
-- Python's `a or b`: a when it is truthy by Python's rules, otherwise b.
|
|
74
|
+
local function __or(a, b)
|
|
75
|
+
if __truthy(a) then return a end
|
|
76
|
+
return b
|
|
77
|
+
end
|
|
78
|
+
-- hit, from limits.py:6
|
|
79
|
+
redis.register_function{
|
|
80
|
+
function_name = 'hit',
|
|
81
|
+
callback = function(KEYS, ARGV)
|
|
82
|
+
local key = KEYS[1]
|
|
83
|
+
local ttl = tonumber(ARGV[1])
|
|
84
|
+
local current = redis.call('INCR', key)
|
|
85
|
+
if current == 1 then
|
|
86
|
+
redis.call('EXPIRE', key, ttl)
|
|
87
|
+
end
|
|
88
|
+
return current
|
|
89
|
+
end,
|
|
90
|
+
}
|
|
91
|
+
-- peek, from limits.py:14
|
|
92
|
+
redis.register_function{
|
|
93
|
+
function_name = 'peek',
|
|
94
|
+
callback = function(KEYS, ARGV)
|
|
95
|
+
local key = KEYS[1]
|
|
96
|
+
return tonumber(__or(redis.call('GET', key), 0))
|
|
97
|
+
end,
|
|
98
|
+
flags = {'no-writes'},
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A helper any function needs is emitted once, at the top, where every callback
|
|
103
|
+
can see it. The callback names its parameters `KEYS` and `ARGV`, so a function
|
|
104
|
+
body compiles to exactly what the same body would as a script.
|
|
105
|
+
|
|
106
|
+
Library and function names take letters, digits and underscores, which is all
|
|
107
|
+
Redis accepts. Function names are global on the server, so two libraries
|
|
108
|
+
cannot both define a `hit`.
|
|
109
|
+
|
|
110
|
+
## Flags
|
|
111
|
+
|
|
112
|
+
`flags` takes the flags Redis defines: `no-writes`, `allow-oom`,
|
|
113
|
+
`allow-stale`, `no-cluster` and `allow-cross-slot-keys`. A `no-writes`
|
|
114
|
+
function is called with `FCALL_RO`, which a replica accepts.
|
|
115
|
+
|
|
116
|
+
Scripts take the same flags. Redis 7 reads them from a `#!lua` line at the very
|
|
117
|
+
top of the script, which is where they go:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
@script(flags=["no-writes"])
|
|
121
|
+
def read(key: Key) -> bytes:
|
|
122
|
+
return redis.get(key)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```lua
|
|
126
|
+
#!lua flags=no-writes
|
|
127
|
+
-- read
|
|
128
|
+
-- Generated by redis-lua-py from app/cache.py:12. Do not edit.
|
|
129
|
+
local key = KEYS[1]
|
|
130
|
+
return redis.call('GET', key)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A misspelled flag is refused at import, like everything else.
|
|
134
|
+
|
|
135
|
+
## Testing
|
|
136
|
+
|
|
137
|
+
fakeredis has neither `FUNCTION` nor script flags. Test a library against a
|
|
138
|
+
real server, or check its source as a golden file: `limits.lua` is the whole
|
|
139
|
+
of it, as [a script's `.lua` is](testing.md#snapshot-the-lua).
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
Everything in `redis_lua_py.__all__`. The package is small on purpose: one
|
|
4
|
+
decorator, a library for Redis Functions, one annotation, two namespaces, and
|
|
5
|
+
the errors.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from redis_lua_py import Key, Library, call, cjson, redis, script
|
|
9
|
+
from redis_lua_py import BoundScript, CompiledScript, LibraryFunction
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## `script`
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
def script(
|
|
16
|
+
func: Callable[..., R] | None = None,
|
|
17
|
+
/,
|
|
18
|
+
*,
|
|
19
|
+
name: str | None = None,
|
|
20
|
+
header: bool = True,
|
|
21
|
+
flags: Iterable[str] = (),
|
|
22
|
+
) -> CompiledScript[R] | Callable[[Callable[..., R]], CompiledScript[R]]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Compile a function into a Redis Lua script. Usable bare or called:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
@script
|
|
29
|
+
def touch(key: Key) -> int: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@script(name="touch_v2", header=False)
|
|
33
|
+
def touch(key: Key) -> int: ...
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Parameter | Meaning |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `name` | overrides the name in the generated header and in errors |
|
|
39
|
+
| `header` | `False` drops the provenance comment entirely, for anyone who wants the script body and nothing else |
|
|
40
|
+
| `flags` | script flags for Redis 7, such as `no-writes`, written on a `#!lua` first line; see [Redis Functions](../guide/redis-functions.md#flags) |
|
|
41
|
+
|
|
42
|
+
Parameters annotated [`Key`](#key) become `KEYS`, in declaration order; every
|
|
43
|
+
other parameter becomes `ARGV`. See
|
|
44
|
+
[Keys and arguments](../guide/keys-and-arguments.md).
|
|
45
|
+
|
|
46
|
+
The return annotation describes what the *caller* gets back, and is carried
|
|
47
|
+
through to the call: `-> int` makes the script a `CompiledScript[int]`. The
|
|
48
|
+
compiler itself does not read it. See
|
|
49
|
+
[What the caller gets](../guide/return-values.md).
|
|
50
|
+
|
|
51
|
+
Define scripts at module level, where they compile once at import.
|
|
52
|
+
|
|
53
|
+
Raises [`UnsupportedSyntax`](errors.md#unsupportedsyntax) at decoration time,
|
|
54
|
+
pointing at the line at fault, if the body strays outside
|
|
55
|
+
[the supported subset](supported-subset.md).
|
|
56
|
+
|
|
57
|
+
## `Key`
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
class Key(str)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Marks a parameter as a Redis key. A `str` subclass, so it is inert at runtime
|
|
64
|
+
and the annotation is the whole of it.
|
|
65
|
+
|
|
66
|
+
Getting this right matters: Redis Cluster routes a script by its declared keys,
|
|
67
|
+
so a key passed as an argument will be invisible to the router.
|
|
68
|
+
|
|
69
|
+
## `redis`
|
|
70
|
+
|
|
71
|
+
The script namespace. `redis.incr(key)` becomes `redis.call('INCR', key)`;
|
|
72
|
+
underscores split into subcommand tokens, so `redis.script_load(x)` becomes
|
|
73
|
+
`redis.call('SCRIPT', 'LOAD', x)`.
|
|
74
|
+
|
|
75
|
+
Names are checked at compile time against Redis' own command table;
|
|
76
|
+
`redis.call(...)` itself is never checked and is the escape hatch. See
|
|
77
|
+
[Calling Redis commands](../guide/calling-redis-commands.md).
|
|
78
|
+
|
|
79
|
+
The compiler identifies the namespace by value rather than by the name it is
|
|
80
|
+
imported under, so every alias works and nothing is reserved.
|
|
81
|
+
|
|
82
|
+
## `call`
|
|
83
|
+
|
|
84
|
+
An alias of [`redis`](#redis), for modules that would rather not rename
|
|
85
|
+
anything.
|
|
86
|
+
|
|
87
|
+
## `cjson`
|
|
88
|
+
|
|
89
|
+
The JSON library Redis exposes to scripts: `cjson.encode` and `cjson.decode`,
|
|
90
|
+
which pass through under their own names.
|
|
91
|
+
|
|
92
|
+
## `CompiledScript`
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
class CompiledScript(Generic[R])
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
What `@script` returns. Calling it runs `EVALSHA` and falls back to `EVAL` the
|
|
99
|
+
first time, or whenever the server has dropped the script from its cache.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
script(client, /, *positional, **keyword) -> R # sync client
|
|
103
|
+
script(client, /, *positional, **keyword) -> Awaitable[R] # async client
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| Attribute | Type | What it is |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| `name` | `str` | the script's name, in the header and in errors |
|
|
109
|
+
| `lua` | `str` | the full Lua source, exactly as sent to Redis |
|
|
110
|
+
| `params` | `tuple[str, ...]` | every parameter, in declaration order |
|
|
111
|
+
| `keys` | `tuple[str, ...]` | the parameters annotated `Key` or `list[Key]`, in `KEYS` order |
|
|
112
|
+
| `args` | `tuple[str, ...]` | everything else, in `ARGV` order |
|
|
113
|
+
| `variadic_key` | `str \| None` | the `list[Key]` parameter, which fills the rest of `KEYS` |
|
|
114
|
+
| `variadic_arg` | `str \| None` | the list parameter that fills the rest of `ARGV` |
|
|
115
|
+
| `doc` | `str \| None` | the function's docstring |
|
|
116
|
+
| `source` | `str` | where it was defined, repo-relative |
|
|
117
|
+
|
|
118
|
+
### `bind`
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
def bind(self, client) -> BoundScript[R] # sync client
|
|
122
|
+
def bind(self, client) -> BoundScript[Awaitable[R]] # async client
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Attach a client, so calls do not have to pass one. See
|
|
126
|
+
[Binding a client](../guide/binding-a-client.md).
|
|
127
|
+
|
|
128
|
+
## `BoundScript`
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
class BoundScript(Generic[T])
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A script with its client already attached, produced by
|
|
135
|
+
[`bind`](#bind). The type parameter is what a call returns: the script's own
|
|
136
|
+
return type for a sync client, an awaitable of it for an async one.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
bound(*positional, **keyword) -> T
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Exposes `name`, `lua`, `params`, `keys`, `args` and `doc` from the script it
|
|
143
|
+
wraps, and leaves that script usable against any other client. A
|
|
144
|
+
[`LibraryFunction`](#libraryfunction) binds the same way.
|
|
145
|
+
|
|
146
|
+
## `Library`
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
class Library(name: str)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
A Redis Functions library. `name` is the library name Redis registers, and
|
|
153
|
+
takes letters, digits and underscores. See
|
|
154
|
+
[Redis Functions](../guide/redis-functions.md).
|
|
155
|
+
|
|
156
|
+
### `function`
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
def function(
|
|
160
|
+
self,
|
|
161
|
+
func: Callable[..., R] | None = None,
|
|
162
|
+
/,
|
|
163
|
+
*,
|
|
164
|
+
name: str | None = None,
|
|
165
|
+
flags: Iterable[str] = (),
|
|
166
|
+
) -> LibraryFunction[R] | Callable[[Callable[..., R]], LibraryFunction[R]]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Compile a function into the library, under the same rules as
|
|
170
|
+
[`script`](#script). Usable bare or called.
|
|
171
|
+
|
|
172
|
+
| Parameter | Meaning |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `name` | the function name Redis registers, instead of the Python name |
|
|
175
|
+
| `flags` | function flags, such as `no-writes`; a `no-writes` function is called with `FCALL_RO` |
|
|
176
|
+
|
|
177
|
+
### `load`
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
def load(self, client) -> Any
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Load the library with `FUNCTION LOAD REPLACE`. Calling a function loads the
|
|
184
|
+
library when the server lacks it, so this is only needed before queueing calls
|
|
185
|
+
in a pipeline, or to load at deploy time.
|
|
186
|
+
|
|
187
|
+
| Attribute | Type | What it is |
|
|
188
|
+
| --- | --- | --- |
|
|
189
|
+
| `name` | `str` | the library name |
|
|
190
|
+
| `lua` | `str` | the library source, exactly as `FUNCTION LOAD` receives it |
|
|
191
|
+
| `functions` | `tuple[LibraryFunction, ...]` | the functions, in the order they were added |
|
|
192
|
+
|
|
193
|
+
## `LibraryFunction`
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
class LibraryFunction(Generic[R])
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
What `Library.function` returns. Calling it sends `FCALL`, or `FCALL_RO` for a
|
|
200
|
+
`no-writes` function, and loads the library first if the server does not have
|
|
201
|
+
it.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
function(client, /, *positional, **keyword) -> R # sync client
|
|
205
|
+
function(client, /, *positional, **keyword) -> Awaitable[R] # async client
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
It has the same `name`, `params`, `keys`, `args`, `variadic_key`,
|
|
209
|
+
`variadic_arg`, `doc` and `source` as a [`CompiledScript`](#compiledscript),
|
|
210
|
+
and a `bind` that works the same way, plus:
|
|
211
|
+
|
|
212
|
+
| Attribute | Type | What it is |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| `library` | `Library` | the library the function belongs to |
|
|
215
|
+
| `lua` | `str` | the source of that whole library |
|
|
216
|
+
| `flags` | `tuple[str, ...]` | the function's flags |
|
|
217
|
+
| `read_only` | `bool` | whether it is flagged `no-writes`, and so called with `FCALL_RO` |
|
|
218
|
+
|
|
219
|
+
## `codegen`
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from redis_lua_py import codegen
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Compile scripts to Lua ahead of time, for code that should not depend on this
|
|
226
|
+
package at runtime. See
|
|
227
|
+
[Shipping without the dependency](../guide/build-time.md). Every function takes
|
|
228
|
+
the module as a module object or a dotted name, and `out` as a path:
|
|
229
|
+
|
|
230
|
+
- ending in `.py`, for one module holding every script as a string constant
|
|
231
|
+
and importing nothing
|
|
232
|
+
- anything else, for a directory with one `.lua` file per script
|
|
233
|
+
|
|
234
|
+
### `codegen.generate`
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
def generate(module: ModuleType | str, out: str | Path) -> list[Path]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Write the scripts under `out`, and return the paths that changed. A file that
|
|
241
|
+
already holds the right content is left alone. In a directory, a `.lua` file
|
|
242
|
+
generated earlier for a script that no longer exists is removed.
|
|
243
|
+
|
|
244
|
+
### `codegen.check`
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
def check(module: ModuleType | str, out: str | Path) -> None
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Raise [`StaleLuaError`](errors.md#staleluaerror), with a diff, unless `out`
|
|
251
|
+
holds exactly what `generate` would write. Writes nothing.
|
|
252
|
+
|
|
253
|
+
### `codegen.render`
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
def render(module: ModuleType | str, out: str | Path) -> dict[Path, str]
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The files `generate` would write, and what each would hold.
|
|
260
|
+
|
|
261
|
+
### `codegen.collect`
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
def collect(module: ModuleType | str) -> dict[str, CompiledScript[Any]]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Every script a module holds, by the name it is bound to, in definition order.
|
|
268
|
+
A script bound to two names is collected once, under the first.
|
|
269
|
+
|
|
270
|
+
### The command line
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
python -m redis_lua_py generate MODULE --out PATH [--check]
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`generate` or, with `--check`, `check`, exiting 1 on an out-of-date output, a
|
|
277
|
+
module that cannot be imported, or a script that does not compile. Installing
|
|
278
|
+
the package also provides the same command as `redis-lua-py`.
|
|
279
|
+
|
|
280
|
+
## Errors and warnings
|
|
281
|
+
|
|
282
|
+
`CompileError`, `UnsupportedSyntax`, `ScriptArgumentError`, `StaleLuaError`,
|
|
283
|
+
`RedisLuaError`, `RedisLuaWarning` and `NilTruncationWarning` have
|
|
284
|
+
[their own page](errors.md).
|
|
@@ -4,11 +4,11 @@ Every error this package raises carries a caret under the line at fault, and a
|
|
|
4
4
|
hint saying what to write instead.
|
|
5
5
|
|
|
6
6
|
```
|
|
7
|
-
|
|
7
|
+
chained comparisons are not supported
|
|
8
8
|
File "/srv/app/limits.py", line 12
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
hint:
|
|
9
|
+
if 0 < n < limit:
|
|
10
|
+
^
|
|
11
|
+
hint: Split 'a < b < c' into 'a < b and b < c'.
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## The hierarchy
|
|
@@ -17,7 +17,8 @@ Lua 5.1 has no 'continue' statement
|
|
|
17
17
|
RedisLuaError
|
|
18
18
|
├── CompileError
|
|
19
19
|
│ └── UnsupportedSyntax
|
|
20
|
-
|
|
20
|
+
├── ScriptArgumentError
|
|
21
|
+
└── StaleLuaError
|
|
21
22
|
|
|
22
23
|
RedisLuaWarning (a UserWarning)
|
|
23
24
|
└── NilTruncationWarning
|
|
@@ -53,6 +54,16 @@ is the only error in the list raised at call time rather than at import.
|
|
|
53
54
|
rate_limit() has no parameter 'tll'; did you mean 'ttl'? (parameters: key, limit, ttl)
|
|
54
55
|
```
|
|
55
56
|
|
|
57
|
+
## `StaleLuaError`
|
|
58
|
+
|
|
59
|
+
Lua generated ahead of time no longer matches the scripts it came from. Raised
|
|
60
|
+
by [`codegen.check`](api.md#codegencheck), with a diff and the command that
|
|
61
|
+
regenerates the output. See
|
|
62
|
+
[Shipping without the dependency](../guide/build-time.md#keep-it-current).
|
|
63
|
+
|
|
64
|
+
It is also an `AssertionError`, so a test that calls `check` reports a failed
|
|
65
|
+
assertion rather than an error in the test itself.
|
|
66
|
+
|
|
56
67
|
## `RedisLuaWarning`
|
|
57
68
|
|
|
58
69
|
Base class for every warning this package raises. A `UserWarning`, so it is
|