redis-lua-py 0.3.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.3.0 → redis_lua_py-0.5.0}/CHANGELOG.md +22 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/PKG-INFO +9 -1
- {redis_lua_py-0.3.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.3.0 → redis_lua_py-0.5.0}/docs/guide/calling-redis-commands.md +27 -2
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/keys-and-arguments.md +36 -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.3.0 → redis_lua_py-0.5.0}/docs/reference/errors.md +16 -6
- redis_lua_py-0.5.0/docs/reference/lua-vs-python.md +194 -0
- redis_lua_py-0.5.0/docs/reference/supported-subset.md +53 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/pyproject.toml +4 -1
- {redis_lua_py-0.3.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.5.0/src/redis_lua_py/_compile.py +2457 -0
- redis_lua_py-0.5.0/src/redis_lua_py/_library.py +256 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_lua.py +76 -2
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_script.py +110 -27
- redis_lua_py-0.5.0/src/redis_lua_py/codegen.py +221 -0
- {redis_lua_py-0.3.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.3.0 → redis_lua_py-0.5.0}/tests/test_errors.py +51 -52
- redis_lua_py-0.5.0/tests/test_functions.py +223 -0
- redis_lua_py-0.5.0/tests/test_mistranslations.py +113 -0
- redis_lua_py-0.5.0/tests/test_strings_and_indexing.py +284 -0
- redis_lua_py-0.5.0/tests/test_table_stakes.py +377 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_typing.py +19 -1
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/uv.lock +1 -1
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/zensical.toml +2 -0
- redis_lua_py-0.3.0/.release-please-manifest.json +0 -3
- redis_lua_py-0.3.0/docs/reference/api.md +0 -144
- redis_lua_py-0.3.0/docs/reference/lua-vs-python.md +0 -88
- redis_lua_py-0.3.0/docs/reference/supported-subset.md +0 -31
- redis_lua_py-0.3.0/src/redis_lua_py/_compile.py +0 -1125
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/README.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/banner-dark.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/banner-light.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/generate.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/logo.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/logomark.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/social-preview.png +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/social-preview.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/workflows/ci.yml +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/workflows/docs.yml +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/workflows/pr-title.yml +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/workflows/release.yml +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.gitignore +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.python-version +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/CONTRIBUTING.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/LICENSE +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/assets/logo.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/contributing.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/development.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/examples.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/async.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/binary-values.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/binding-a-client.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/constants.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/generated-lua.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/return-values.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/testing.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/index.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/installation.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/quickstart.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/stylesheets/extra.css +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/release-please-config.json +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/scripts/generate_commands.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_commands.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_runtime.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/py.typed +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/conftest.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/module_with_client.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/module_without_import.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_binary.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_binding.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_commands.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_compile.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_constants.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_execute.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_namespace.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_semantics.py +0 -0
|
@@ -7,6 +7,28 @@ 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
|
+
|
|
20
|
+
## [0.4.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.3.0...v0.4.0) (2026-09-12)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
* compile variable keys, splat calls, and/or values, helper functions and dict loops ([#10](https://github.com/IgnaceMaes/redis-lua-py/issues/10)) ([271f3f6](https://github.com/IgnaceMaes/redis-lua-py/commit/271f3f6be23d78246d91cde5de4fec05cee28832))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
* stop silently mistranslating reassigned parameters, infinity, == None and set_repl ([#8](https://github.com/IgnaceMaes/redis-lua-py/issues/8)) ([930c382](https://github.com/IgnaceMaes/redis-lua-py/commit/930c38283f366fd9042c4cc20cdf7eeb34e2d8a5))
|
|
31
|
+
|
|
10
32
|
## [0.3.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.2.1...v0.3.0) (2026-09-12)
|
|
11
33
|
|
|
12
34
|
|
|
@@ -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.
|
|
@@ -5,8 +5,33 @@ split into subcommand tokens, so `redis.script_load(x)` compiles to
|
|
|
5
5
|
`redis.call('SCRIPT', 'LOAD', x)`.
|
|
6
6
|
|
|
7
7
|
`redis.pcall`, `redis.error_reply`, `redis.status_reply`, `redis.sha1hex`,
|
|
8
|
-
`redis.log`
|
|
9
|
-
names.
|
|
8
|
+
`redis.log`, `redis.set_repl`, `redis.acl_check_cmd` and `cjson.encode` /
|
|
9
|
+
`cjson.decode` pass through under their own names. So do the constants they
|
|
10
|
+
take: `redis.LOG_WARNING` and the other log levels, `redis.REPL_ALL` and the
|
|
11
|
+
other replication modes, and `redis.REDIS_VERSION`.
|
|
12
|
+
|
|
13
|
+
## Splatting a list into a command
|
|
14
|
+
|
|
15
|
+
A starred argument compiles to `unpack`, which is how a list becomes the
|
|
16
|
+
arguments of a command:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
@script
|
|
20
|
+
def push_all(queue: Key, jobs: list[str]) -> int:
|
|
21
|
+
return redis.rpush(queue, *jobs)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```lua
|
|
25
|
+
return redis.call('RPUSH', queue, unpack(jobs))
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It has to be the last argument, because Lua's `unpack` only expands there.
|
|
29
|
+
|
|
30
|
+
!!! warning "unpack has a limit"
|
|
31
|
+
|
|
32
|
+
`unpack` puts every element on Lua's stack at once, and Redis' Lua refuses
|
|
33
|
+
past roughly eight thousand values with "too many results to unpack".
|
|
34
|
+
Split a list that can grow that large into chunks, one command per chunk.
|
|
10
35
|
|
|
11
36
|
## Names are checked, not just uppercased
|
|
12
37
|
|
|
@@ -46,6 +46,42 @@ the value is only being carried.
|
|
|
46
46
|
`bool` encodes to `"1"` or `"0"`. Paired with an `int` annotation that deletes
|
|
47
47
|
the `1 if flag else 0` from the call site: pass `True`, and the body gets `1`.
|
|
48
48
|
|
|
49
|
+
## A variable number of keys or arguments
|
|
50
|
+
|
|
51
|
+
Annotate a parameter `list[Key]` to take any number of keys, or `list[str]`,
|
|
52
|
+
`list[int]` and so on to take any number of arguments. Its elements fill
|
|
53
|
+
whatever is left of `KEYS` or `ARGV` after the fixed parameters, wherever the
|
|
54
|
+
list is declared, and arrive in the body as a table:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
@script
|
|
58
|
+
def delete_tagged(tag: Key, keys: list[Key], stamp: int) -> int:
|
|
59
|
+
redis.set(tag, stamp)
|
|
60
|
+
return redis.delete(*keys)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
delete_tagged(client, tag="purged", keys=["a", "b", "c"], stamp=1700000000)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```lua
|
|
67
|
+
local tag = KEYS[1]
|
|
68
|
+
local stamp = tonumber(ARGV[1])
|
|
69
|
+
local keys = {}
|
|
70
|
+
for __i1 = 2, #KEYS do
|
|
71
|
+
keys[#keys + 1] = KEYS[__i1]
|
|
72
|
+
end
|
|
73
|
+
redis.call('SET', tag, stamp)
|
|
74
|
+
return redis.call('DEL', unpack(keys))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A script can take one list of keys and one list of arguments, since `KEYS`
|
|
78
|
+
and `ARGV` only have positions to tell them apart. Every element of a
|
|
79
|
+
`list[Key]` is a declared key, so Redis Cluster routes on all of them. An
|
|
80
|
+
element annotation of `int` or `float` converts each element, as it would a
|
|
81
|
+
single argument. Passing a string where a list is expected raises
|
|
82
|
+
[`ScriptArgumentError`](../reference/errors.md#scriptargumenterror) rather
|
|
83
|
+
than spreading it into characters.
|
|
84
|
+
|
|
49
85
|
## Positional or keyword
|
|
50
86
|
|
|
51
87
|
Scripts accept either; keyword is clearer at the call site and is what the
|
|
@@ -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).
|