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.
Files changed (84) hide show
  1. redis_lua_py-0.5.0/.release-please-manifest.json +3 -0
  2. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/CHANGELOG.md +22 -0
  3. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/PKG-INFO +9 -1
  4. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/README.md +8 -0
  5. redis_lua_py-0.5.0/docs/guide/build-time.md +152 -0
  6. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/calling-redis-commands.md +27 -2
  7. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/keys-and-arguments.md +36 -0
  8. redis_lua_py-0.5.0/docs/guide/redis-functions.md +139 -0
  9. redis_lua_py-0.5.0/docs/reference/api.md +284 -0
  10. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/reference/errors.md +16 -6
  11. redis_lua_py-0.5.0/docs/reference/lua-vs-python.md +194 -0
  12. redis_lua_py-0.5.0/docs/reference/supported-subset.md +53 -0
  13. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/pyproject.toml +4 -1
  14. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/__init__.py +15 -6
  15. redis_lua_py-0.5.0/src/redis_lua_py/__main__.py +73 -0
  16. redis_lua_py-0.5.0/src/redis_lua_py/_compile.py +2457 -0
  17. redis_lua_py-0.5.0/src/redis_lua_py/_library.py +256 -0
  18. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_lua.py +76 -2
  19. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_script.py +110 -27
  20. redis_lua_py-0.5.0/src/redis_lua_py/codegen.py +221 -0
  21. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/errors.py +9 -0
  22. redis_lua_py-0.5.0/tests/codegen_scripts.py +41 -0
  23. redis_lua_py-0.5.0/tests/test_codegen.py +229 -0
  24. redis_lua_py-0.5.0/tests/test_control_flow.py +322 -0
  25. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_errors.py +51 -52
  26. redis_lua_py-0.5.0/tests/test_functions.py +223 -0
  27. redis_lua_py-0.5.0/tests/test_mistranslations.py +113 -0
  28. redis_lua_py-0.5.0/tests/test_strings_and_indexing.py +284 -0
  29. redis_lua_py-0.5.0/tests/test_table_stakes.py +377 -0
  30. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_typing.py +19 -1
  31. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/uv.lock +1 -1
  32. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/zensical.toml +2 -0
  33. redis_lua_py-0.3.0/.release-please-manifest.json +0 -3
  34. redis_lua_py-0.3.0/docs/reference/api.md +0 -144
  35. redis_lua_py-0.3.0/docs/reference/lua-vs-python.md +0 -88
  36. redis_lua_py-0.3.0/docs/reference/supported-subset.md +0 -31
  37. redis_lua_py-0.3.0/src/redis_lua_py/_compile.py +0 -1125
  38. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/README.md +0 -0
  39. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/banner-dark.svg +0 -0
  40. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/banner-light.svg +0 -0
  41. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/generate.py +0 -0
  42. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/logo.svg +0 -0
  43. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/logomark.svg +0 -0
  44. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/social-preview.png +0 -0
  45. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/assets/social-preview.svg +0 -0
  46. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/workflows/ci.yml +0 -0
  47. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/workflows/docs.yml +0 -0
  48. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/workflows/pr-title.yml +0 -0
  49. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.github/workflows/release.yml +0 -0
  50. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.gitignore +0 -0
  51. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/.python-version +0 -0
  52. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/CONTRIBUTING.md +0 -0
  53. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/LICENSE +0 -0
  54. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/assets/logo.svg +0 -0
  55. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/contributing.md +0 -0
  56. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/development.md +0 -0
  57. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/examples.md +0 -0
  58. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/async.md +0 -0
  59. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/binary-values.md +0 -0
  60. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/binding-a-client.md +0 -0
  61. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/constants.md +0 -0
  62. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/generated-lua.md +0 -0
  63. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/return-values.md +0 -0
  64. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/guide/testing.md +0 -0
  65. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/index.md +0 -0
  66. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/installation.md +0 -0
  67. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/quickstart.md +0 -0
  68. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/docs/stylesheets/extra.css +0 -0
  69. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/release-please-config.json +0 -0
  70. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/scripts/generate_commands.py +0 -0
  71. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_commands.py +0 -0
  72. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_runtime.py +0 -0
  73. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/src/redis_lua_py/py.typed +0 -0
  74. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/conftest.py +0 -0
  75. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/module_with_client.py +0 -0
  76. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/module_without_import.py +0 -0
  77. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_binary.py +0 -0
  78. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_binding.py +0 -0
  79. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_commands.py +0 -0
  80. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_compile.py +0 -0
  81. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_constants.py +0 -0
  82. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_execute.py +0 -0
  83. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_namespace.py +0 -0
  84. {redis_lua_py-0.3.0 → redis_lua_py-0.5.0}/tests/test_semantics.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.5.0"
3
+ }
@@ -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.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` and `cjson.encode` / `cjson.decode` pass through under their own
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).