redis-lua-py 0.2.0__tar.gz → 0.2.1__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 (70) hide show
  1. redis_lua_py-0.2.1/.github/workflows/docs.yml +46 -0
  2. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.gitignore +3 -0
  3. redis_lua_py-0.2.1/.release-please-manifest.json +3 -0
  4. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/CHANGELOG.md +7 -0
  5. redis_lua_py-0.2.1/PKG-INFO +210 -0
  6. redis_lua_py-0.2.1/README.md +185 -0
  7. redis_lua_py-0.2.1/docs/assets/logo.svg +4 -0
  8. redis_lua_py-0.2.1/docs/contributing.md +67 -0
  9. redis_lua_py-0.2.1/docs/development.md +53 -0
  10. redis_lua_py-0.2.1/docs/examples.md +110 -0
  11. redis_lua_py-0.2.1/docs/guide/async.md +29 -0
  12. redis_lua_py-0.2.1/docs/guide/binary-values.md +30 -0
  13. redis_lua_py-0.2.1/docs/guide/binding-a-client.md +23 -0
  14. redis_lua_py-0.2.1/docs/guide/calling-redis-commands.md +95 -0
  15. redis_lua_py-0.2.1/docs/guide/constants.md +36 -0
  16. redis_lua_py-0.2.1/docs/guide/generated-lua.md +52 -0
  17. redis_lua_py-0.2.1/docs/guide/keys-and-arguments.md +60 -0
  18. redis_lua_py-0.2.1/docs/guide/return-values.md +53 -0
  19. redis_lua_py-0.2.1/docs/guide/testing.md +50 -0
  20. redis_lua_py-0.2.1/docs/index.md +85 -0
  21. redis_lua_py-0.2.1/docs/installation.md +34 -0
  22. redis_lua_py-0.2.1/docs/quickstart.md +98 -0
  23. redis_lua_py-0.2.1/docs/reference/api.md +144 -0
  24. redis_lua_py-0.2.1/docs/reference/errors.md +77 -0
  25. redis_lua_py-0.2.1/docs/reference/lua-vs-python.md +88 -0
  26. redis_lua_py-0.2.1/docs/reference/supported-subset.md +31 -0
  27. redis_lua_py-0.2.1/docs/stylesheets/extra.css +26 -0
  28. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/pyproject.toml +6 -2
  29. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/src/redis_lua_py/__init__.py +1 -1
  30. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/uv.lock +265 -1
  31. redis_lua_py-0.2.1/zensical.toml +78 -0
  32. redis_lua_py-0.2.0/.release-please-manifest.json +0 -3
  33. redis_lua_py-0.2.0/PKG-INFO +0 -565
  34. redis_lua_py-0.2.0/README.md +0 -543
  35. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.github/assets/README.md +0 -0
  36. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.github/assets/banner-dark.svg +0 -0
  37. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.github/assets/banner-light.svg +0 -0
  38. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.github/assets/generate.py +0 -0
  39. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.github/assets/logo.svg +0 -0
  40. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.github/assets/logomark.svg +0 -0
  41. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.github/assets/social-preview.png +0 -0
  42. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.github/assets/social-preview.svg +0 -0
  43. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.github/workflows/ci.yml +0 -0
  44. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.github/workflows/pr-title.yml +0 -0
  45. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.github/workflows/release.yml +0 -0
  46. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/.python-version +0 -0
  47. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/CONTRIBUTING.md +0 -0
  48. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/LICENSE +0 -0
  49. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/release-please-config.json +0 -0
  50. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/scripts/generate_commands.py +0 -0
  51. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/src/redis_lua_py/_commands.py +0 -0
  52. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/src/redis_lua_py/_compile.py +0 -0
  53. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/src/redis_lua_py/_lua.py +0 -0
  54. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/src/redis_lua_py/_runtime.py +0 -0
  55. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/src/redis_lua_py/_script.py +0 -0
  56. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/src/redis_lua_py/errors.py +0 -0
  57. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/src/redis_lua_py/py.typed +0 -0
  58. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/conftest.py +0 -0
  59. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/module_with_client.py +0 -0
  60. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/module_without_import.py +0 -0
  61. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/test_binary.py +0 -0
  62. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/test_binding.py +0 -0
  63. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/test_commands.py +0 -0
  64. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/test_compile.py +0 -0
  65. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/test_constants.py +0 -0
  66. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/test_errors.py +0 -0
  67. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/test_execute.py +0 -0
  68. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/test_namespace.py +0 -0
  69. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/test_semantics.py +0 -0
  70. {redis_lua_py-0.2.0 → redis_lua_py-0.2.1}/tests/test_typing.py +0 -0
@@ -0,0 +1,46 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ # A pull request only builds, so a broken link fails review rather than main.
7
+ pull_request:
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+ pages: write
13
+ id-token: write
14
+
15
+ # One deploy at a time, and never cancel one in flight.
16
+ concurrency:
17
+ group: pages
18
+ cancel-in-progress: false
19
+
20
+ jobs:
21
+ build:
22
+ name: Build
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: astral-sh/setup-uv@v5
27
+ with:
28
+ enable-cache: true
29
+ - run: uv sync --dev
30
+ # Link validation is on in zensical.toml, so this fails on a dead link.
31
+ - run: uv run zensical build --clean
32
+ - uses: actions/upload-pages-artifact@v4
33
+ with:
34
+ path: site
35
+
36
+ deploy:
37
+ name: Deploy
38
+ if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
39
+ needs: build
40
+ runs-on: ubuntu-latest
41
+ environment:
42
+ name: github-pages
43
+ url: ${{ steps.deployment.outputs.page_url }}
44
+ steps:
45
+ - uses: actions/deploy-pages@v4
46
+ id: deployment
@@ -22,3 +22,6 @@ htmlcov/
22
22
  .idea/
23
23
  .vscode/
24
24
  .DS_Store
25
+
26
+ # Docs site
27
+ site/
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.2.1"
3
+ }
@@ -7,6 +7,13 @@ 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.2.1](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.2.0...v0.2.1) (2026-09-12)
11
+
12
+
13
+ ### Documentation
14
+
15
+ * add a documentation site built with zensical ([#4](https://github.com/IgnaceMaes/redis-lua-py/issues/4)) ([f877f97](https://github.com/IgnaceMaes/redis-lua-py/commit/f877f97c0b7c8126ab62ef2324aff99106434ee4))
16
+
10
17
  ## [0.2.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.1.0...v0.2.0) (2026-09-12)
11
18
 
12
19
  Everything raised by the first adoption report of 0.1.0
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.5
2
+ Name: redis-lua-py
3
+ Version: 0.2.1
4
+ Summary: Write Redis Lua scripts as real Python functions, not strings.
5
+ Project-URL: Homepage, https://ignacemaes.com/redis-lua-py/
6
+ Project-URL: Documentation, https://ignacemaes.com/redis-lua-py/
7
+ Project-URL: Repository, https://github.com/ignacemaes/redis-lua-py
8
+ Project-URL: Issues, https://github.com/ignacemaes/redis-lua-py/issues
9
+ Project-URL: Changelog, https://github.com/ignacemaes/redis-lua-py/blob/main/CHANGELOG.md
10
+ Author: Ignace Maes
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: eval,evalsha,lua,redis,scripting,transpiler
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Lua
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: redis>=5.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ <p align="center">
27
+ <picture>
28
+ <source media="(prefers-color-scheme: dark)" srcset="./.github/assets/banner-dark.svg">
29
+ <img alt="redis-lua-py: Redis Lua scripts as real Python functions." src="./.github/assets/banner-light.svg" width="860">
30
+ </picture>
31
+ </p>
32
+
33
+ <p align="center">
34
+ <a href="https://pypi.org/project/redis-lua-py/"><img alt="PyPI" src="https://img.shields.io/pypi/v/redis-lua-py?color=%230070F3&label=pypi"></a>
35
+ <a href="https://pypi.org/project/redis-lua-py/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/redis-lua-py?color=%230070F3"></a>
36
+ <a href="https://github.com/ignacemaes/redis-lua-py/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/ignacemaes/redis-lua-py/ci.yml?branch=main&color=%230070F3&label=ci"></a>
37
+ <a href="./LICENSE"><img alt="license" src="https://img.shields.io/pypi/l/redis-lua-py?color=%230070F3"></a>
38
+ </p>
39
+
40
+ <p align="center">
41
+ Write Redis Lua scripts as real Python functions, not as strings.<br>
42
+ Compiled at import, checked by <code>mypy</code>, sent with <code>EVALSHA</code>. Sync and async redis-py.
43
+ </p>
44
+
45
+ <p align="center">
46
+ <b><a href="https://ignacemaes.com/redis-lua-py/">Documentation</a></b> ·
47
+ <a href="https://ignacemaes.com/redis-lua-py/quickstart/">Quickstart</a> ·
48
+ <a href="https://ignacemaes.com/redis-lua-py/reference/api/">API reference</a> ·
49
+ <a href="./CHANGELOG.md">Changelog</a>
50
+ </p>
51
+
52
+ ```python
53
+ from redis_lua_py import Key, redis, script
54
+
55
+
56
+ @script
57
+ def rate_limit(key: Key, limit: int, ttl: int) -> int:
58
+ current = redis.incr(key)
59
+ if current == 1:
60
+ redis.expire(key, ttl)
61
+ if current > limit:
62
+ return -1
63
+ return limit - current
64
+ ```
65
+
66
+ The body is never executed by Python. It is read as source when the module is
67
+ imported, compiled to Lua, and sent to Redis with `EVALSHA`. Your editor
68
+ highlights it, your linter sees it, and `mypy` checks the signature — none of
69
+ which is true of a string.
70
+
71
+ Define scripts at module level, where they compile once at import. A script
72
+ defined inside a function recompiles on every call, and one defined through
73
+ `exec` has no source to read and is refused.
74
+
75
+ ```python
76
+ from redis import Redis
77
+
78
+ client = Redis()
79
+ remaining = rate_limit(client, key="user:42", limit=10, ttl=60)
80
+ ```
81
+
82
+ Importing the client as `from redis import Redis` leaves the name `redis` free
83
+ for the script namespace, so the two never collide.
84
+
85
+ ## Install
86
+
87
+ ```bash
88
+ uv add redis-lua-py
89
+ ```
90
+
91
+ Python 3.11+, and redis-py 5.0+ as the only dependency.
92
+
93
+ ## What it compiles to
94
+
95
+ Nothing is hidden. Every script exposes the Lua it produced:
96
+
97
+ ```python
98
+ >>> print(rate_limit.lua)
99
+ ```
100
+
101
+ ```lua
102
+ -- rate_limit
103
+ -- Generated by redis-lua-py from src/limits.py:6. Do not edit.
104
+ local key = KEYS[1]
105
+ local limit = tonumber(ARGV[1])
106
+ local ttl = tonumber(ARGV[2])
107
+ local current = redis.call('INCR', key)
108
+ if current == 1 then
109
+ redis.call('EXPIRE', key, ttl)
110
+ end
111
+ if current > limit then
112
+ return -1
113
+ end
114
+ return limit - current
115
+ ```
116
+
117
+ Read it in review, paste it into `redis-cli`, check it into a golden test. The
118
+ point of this library is to generate Lua you would have been willing to write.
119
+
120
+ The header is part of the body, and the body is what `EVALSHA` hashes, so the
121
+ path in it is relative to your project root rather than absolute — the same
122
+ script has the same SHA on a laptop, in CI and in a container, and the server's
123
+ script cache is cold once per script rather than once per environment.
124
+
125
+ ## What else it does
126
+
127
+ - **[Keys and arguments](https://ignacemaes.com/redis-lua-py/guide/keys-and-arguments/)** —
128
+ a parameter annotated `Key` becomes `KEYS`, which is what Redis Cluster
129
+ routes on; an `int` or `float` is wrapped in `tonumber` for you.
130
+ - **[Command names are checked](https://ignacemaes.com/redis-lua-py/guide/calling-redis-commands/)**
131
+ at compile time against Redis' own command table, so `redis.expires(...)` is
132
+ refused where you can see it rather than raised inside a script whose whole
133
+ purpose was to be atomic.
134
+ - **[Constants are folded](https://ignacemaes.com/redis-lua-py/guide/constants/)** —
135
+ a module-level `int`, `float`, `str`, `bytes` or `bool` is read once, at
136
+ import, and written into the script as a literal.
137
+ - **[Binary values survive](https://ignacemaes.com/redis-lua-py/guide/binary-values/)** —
138
+ nothing here decodes, and `bytes` is a passthrough in both directions.
139
+ - **[The caller's side is typed](https://ignacemaes.com/redis-lua-py/guide/return-values/)** —
140
+ a script is a `CompiledScript[R]`, and an async client gives you
141
+ `Awaitable[R]`.
142
+ - **[Sync and async](https://ignacemaes.com/redis-lua-py/guide/async/)** from
143
+ the same script object, and
144
+ **[`bind`](https://ignacemaes.com/redis-lua-py/guide/binding-a-client/)** when
145
+ passing the client every time gets repetitive.
146
+ - **[The gaps between Lua and Python](https://ignacemaes.com/redis-lua-py/reference/lua-vs-python/)**
147
+ are closed or refused — truthiness, 1-based indexing, `false` versus `nil`,
148
+ block scope, and the nil that truncates a returned table.
149
+ - **[Anything outside the supported subset](https://ignacemaes.com/redis-lua-py/reference/supported-subset/)**
150
+ raises at import, with a caret under the line at fault.
151
+
152
+ Full documentation: **[ignacemaes.com/redis-lua-py](https://ignacemaes.com/redis-lua-py/)**.
153
+
154
+ ## Testing your scripts
155
+
156
+ [fakeredis](https://github.com/cunla/fakeredis-py) embeds a real Lua
157
+ interpreter, so your script executes for real against an in-process server:
158
+
159
+ ```python
160
+ import fakeredis
161
+
162
+
163
+ def test_rate_limit_refuses_past_the_limit():
164
+ client = fakeredis.FakeRedis()
165
+
166
+ assert rate_limit(client, key="u:42", limit=2, ttl=60) == 1
167
+ assert rate_limit(client, key="u:42", limit=2, ttl=60) == 0
168
+ assert rate_limit(client, key="u:42", limit=2, ttl=60) == -1
169
+ ```
170
+
171
+ Install it with `uv add --dev "fakeredis[lua]"`; the `lua` extra is what brings
172
+ the interpreter. `.lua` is the whole script, so a golden snapshot is a string
173
+ comparison — see
174
+ [Testing your scripts](https://ignacemaes.com/redis-lua-py/guide/testing/).
175
+
176
+ ## Development
177
+
178
+ ```bash
179
+ uv sync
180
+ uv run pytest
181
+ uv run ruff check
182
+ uv run mypy
183
+ ```
184
+
185
+ Tests run against [fakeredis](https://github.com/cunla/fakeredis-py), which
186
+ executes real Lua, so `uv run pytest` needs no server. Set `REDIS_URL` to also
187
+ run them against a live Redis:
188
+
189
+ ```bash
190
+ REDIS_URL=redis://localhost:6379/0 uv run pytest
191
+ ```
192
+
193
+ `src/redis_lua_py/_commands.py` is generated from the Redis source. Refresh it
194
+ when a Redis release adds commands:
195
+
196
+ ```bash
197
+ uv run python scripts/generate_commands.py 8.10.1
198
+ ```
199
+
200
+ The docs site is built with [Zensical](https://zensical.org); `uv run zensical
201
+ serve` previews it with live reload.
202
+
203
+ Pull requests are squash-merged and their titles must follow
204
+ [Conventional Commits](https://www.conventionalcommits.org/): the title becomes
205
+ the changelog entry and decides the version bump. See
206
+ [CONTRIBUTING.md](CONTRIBUTING.md).
207
+
208
+ ## License
209
+
210
+ MIT
@@ -0,0 +1,185 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="./.github/assets/banner-dark.svg">
4
+ <img alt="redis-lua-py: Redis Lua scripts as real Python functions." src="./.github/assets/banner-light.svg" width="860">
5
+ </picture>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://pypi.org/project/redis-lua-py/"><img alt="PyPI" src="https://img.shields.io/pypi/v/redis-lua-py?color=%230070F3&label=pypi"></a>
10
+ <a href="https://pypi.org/project/redis-lua-py/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/redis-lua-py?color=%230070F3"></a>
11
+ <a href="https://github.com/ignacemaes/redis-lua-py/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/ignacemaes/redis-lua-py/ci.yml?branch=main&color=%230070F3&label=ci"></a>
12
+ <a href="./LICENSE"><img alt="license" src="https://img.shields.io/pypi/l/redis-lua-py?color=%230070F3"></a>
13
+ </p>
14
+
15
+ <p align="center">
16
+ Write Redis Lua scripts as real Python functions, not as strings.<br>
17
+ Compiled at import, checked by <code>mypy</code>, sent with <code>EVALSHA</code>. Sync and async redis-py.
18
+ </p>
19
+
20
+ <p align="center">
21
+ <b><a href="https://ignacemaes.com/redis-lua-py/">Documentation</a></b> ·
22
+ <a href="https://ignacemaes.com/redis-lua-py/quickstart/">Quickstart</a> ·
23
+ <a href="https://ignacemaes.com/redis-lua-py/reference/api/">API reference</a> ·
24
+ <a href="./CHANGELOG.md">Changelog</a>
25
+ </p>
26
+
27
+ ```python
28
+ from redis_lua_py import Key, redis, script
29
+
30
+
31
+ @script
32
+ def rate_limit(key: Key, limit: int, ttl: int) -> int:
33
+ current = redis.incr(key)
34
+ if current == 1:
35
+ redis.expire(key, ttl)
36
+ if current > limit:
37
+ return -1
38
+ return limit - current
39
+ ```
40
+
41
+ The body is never executed by Python. It is read as source when the module is
42
+ imported, compiled to Lua, and sent to Redis with `EVALSHA`. Your editor
43
+ highlights it, your linter sees it, and `mypy` checks the signature — none of
44
+ which is true of a string.
45
+
46
+ Define scripts at module level, where they compile once at import. A script
47
+ defined inside a function recompiles on every call, and one defined through
48
+ `exec` has no source to read and is refused.
49
+
50
+ ```python
51
+ from redis import Redis
52
+
53
+ client = Redis()
54
+ remaining = rate_limit(client, key="user:42", limit=10, ttl=60)
55
+ ```
56
+
57
+ Importing the client as `from redis import Redis` leaves the name `redis` free
58
+ for the script namespace, so the two never collide.
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ uv add redis-lua-py
64
+ ```
65
+
66
+ Python 3.11+, and redis-py 5.0+ as the only dependency.
67
+
68
+ ## What it compiles to
69
+
70
+ Nothing is hidden. Every script exposes the Lua it produced:
71
+
72
+ ```python
73
+ >>> print(rate_limit.lua)
74
+ ```
75
+
76
+ ```lua
77
+ -- rate_limit
78
+ -- Generated by redis-lua-py from src/limits.py:6. Do not edit.
79
+ local key = KEYS[1]
80
+ local limit = tonumber(ARGV[1])
81
+ local ttl = tonumber(ARGV[2])
82
+ local current = redis.call('INCR', key)
83
+ if current == 1 then
84
+ redis.call('EXPIRE', key, ttl)
85
+ end
86
+ if current > limit then
87
+ return -1
88
+ end
89
+ return limit - current
90
+ ```
91
+
92
+ Read it in review, paste it into `redis-cli`, check it into a golden test. The
93
+ point of this library is to generate Lua you would have been willing to write.
94
+
95
+ The header is part of the body, and the body is what `EVALSHA` hashes, so the
96
+ path in it is relative to your project root rather than absolute — the same
97
+ script has the same SHA on a laptop, in CI and in a container, and the server's
98
+ script cache is cold once per script rather than once per environment.
99
+
100
+ ## What else it does
101
+
102
+ - **[Keys and arguments](https://ignacemaes.com/redis-lua-py/guide/keys-and-arguments/)** —
103
+ a parameter annotated `Key` becomes `KEYS`, which is what Redis Cluster
104
+ routes on; an `int` or `float` is wrapped in `tonumber` for you.
105
+ - **[Command names are checked](https://ignacemaes.com/redis-lua-py/guide/calling-redis-commands/)**
106
+ at compile time against Redis' own command table, so `redis.expires(...)` is
107
+ refused where you can see it rather than raised inside a script whose whole
108
+ purpose was to be atomic.
109
+ - **[Constants are folded](https://ignacemaes.com/redis-lua-py/guide/constants/)** —
110
+ a module-level `int`, `float`, `str`, `bytes` or `bool` is read once, at
111
+ import, and written into the script as a literal.
112
+ - **[Binary values survive](https://ignacemaes.com/redis-lua-py/guide/binary-values/)** —
113
+ nothing here decodes, and `bytes` is a passthrough in both directions.
114
+ - **[The caller's side is typed](https://ignacemaes.com/redis-lua-py/guide/return-values/)** —
115
+ a script is a `CompiledScript[R]`, and an async client gives you
116
+ `Awaitable[R]`.
117
+ - **[Sync and async](https://ignacemaes.com/redis-lua-py/guide/async/)** from
118
+ the same script object, and
119
+ **[`bind`](https://ignacemaes.com/redis-lua-py/guide/binding-a-client/)** when
120
+ passing the client every time gets repetitive.
121
+ - **[The gaps between Lua and Python](https://ignacemaes.com/redis-lua-py/reference/lua-vs-python/)**
122
+ are closed or refused — truthiness, 1-based indexing, `false` versus `nil`,
123
+ block scope, and the nil that truncates a returned table.
124
+ - **[Anything outside the supported subset](https://ignacemaes.com/redis-lua-py/reference/supported-subset/)**
125
+ raises at import, with a caret under the line at fault.
126
+
127
+ Full documentation: **[ignacemaes.com/redis-lua-py](https://ignacemaes.com/redis-lua-py/)**.
128
+
129
+ ## Testing your scripts
130
+
131
+ [fakeredis](https://github.com/cunla/fakeredis-py) embeds a real Lua
132
+ interpreter, so your script executes for real against an in-process server:
133
+
134
+ ```python
135
+ import fakeredis
136
+
137
+
138
+ def test_rate_limit_refuses_past_the_limit():
139
+ client = fakeredis.FakeRedis()
140
+
141
+ assert rate_limit(client, key="u:42", limit=2, ttl=60) == 1
142
+ assert rate_limit(client, key="u:42", limit=2, ttl=60) == 0
143
+ assert rate_limit(client, key="u:42", limit=2, ttl=60) == -1
144
+ ```
145
+
146
+ Install it with `uv add --dev "fakeredis[lua]"`; the `lua` extra is what brings
147
+ the interpreter. `.lua` is the whole script, so a golden snapshot is a string
148
+ comparison — see
149
+ [Testing your scripts](https://ignacemaes.com/redis-lua-py/guide/testing/).
150
+
151
+ ## Development
152
+
153
+ ```bash
154
+ uv sync
155
+ uv run pytest
156
+ uv run ruff check
157
+ uv run mypy
158
+ ```
159
+
160
+ Tests run against [fakeredis](https://github.com/cunla/fakeredis-py), which
161
+ executes real Lua, so `uv run pytest` needs no server. Set `REDIS_URL` to also
162
+ run them against a live Redis:
163
+
164
+ ```bash
165
+ REDIS_URL=redis://localhost:6379/0 uv run pytest
166
+ ```
167
+
168
+ `src/redis_lua_py/_commands.py` is generated from the Redis source. Refresh it
169
+ when a Redis release adds commands:
170
+
171
+ ```bash
172
+ uv run python scripts/generate_commands.py 8.10.1
173
+ ```
174
+
175
+ The docs site is built with [Zensical](https://zensical.org); `uv run zensical
176
+ serve` previews it with live reload.
177
+
178
+ Pull requests are squash-merged and their titles must follow
179
+ [Conventional Commits](https://www.conventionalcommits.org/): the title becomes
180
+ the changelog entry and decides the version bump. See
181
+ [CONTRIBUTING.md](CONTRIBUTING.md).
182
+
183
+ ## License
184
+
185
+ MIT
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" role="img" aria-label="redis-lua-py">
2
+ <rect width="64" height="64" rx="14" fill="#000000"/>
3
+ <g transform="translate(7.6 7.6) scale(0.76)"><defs><mask id="l"><rect width="64" height="64" fill="#fff"/><path d="M40 15.8 L64.2 40 L40 64.2 L15.8 40 Z" fill="#000"/></mask></defs><g fill="none" stroke="#FFFFFF" stroke-width="6.0" stroke-linejoin="miter"><path mask="url(#l)" d="M8 8 H36 V36 H8 Z"/><path d="M40 20 L60 40 L40 60 L20 40 Z"/></g></g>
4
+ </svg>
@@ -0,0 +1,67 @@
1
+ # Contributing
2
+
3
+ ## Getting set up
4
+
5
+ ```bash
6
+ uv sync --dev
7
+ uv run pytest && uv run ruff check && uv run ruff format --check && uv run mypy
8
+ ```
9
+
10
+ See [Development](development.md) for running the suite against a live Redis
11
+ and for regenerating the command table.
12
+
13
+ ## Commit messages
14
+
15
+ Pull requests are squash-merged, so **the PR title becomes the commit subject
16
+ on `main`**, and that subject is the only thing the release tooling reads. It
17
+ must follow [Conventional Commits](https://www.conventionalcommits.org/):
18
+
19
+ ```
20
+ feat: compile `pcall` into a protected call
21
+ fix: hoist names assigned in both arms of an if
22
+ docs: document the Key annotation
23
+ ```
24
+
25
+ | Prefix | Changelog section | Version bump |
26
+ | --- | --- | --- |
27
+ | `feat:` | Added | minor |
28
+ | `fix:` | Fixed | patch |
29
+ | `perf:` | Performance | patch |
30
+ | `refactor:` | Changed | patch |
31
+ | `deps:` | Dependencies | patch |
32
+ | `docs:` | Documentation | patch |
33
+ | `chore:` `test:` `ci:` `build:` `style:` | omitted | none |
34
+
35
+ A `!` after the prefix (`feat!:`) or a `BREAKING CHANGE:` paragraph in the PR
36
+ body marks a breaking change. While the version is below 1.0 that bumps the
37
+ minor, not the major — `0.1.0` becomes `0.2.0`, never `1.0.0`. Promoting the
38
+ project to 1.0 is a deliberate act: set `"release-as": "1.0.0"` once in
39
+ `release-please-config.json`, or land a commit with a `Release-As: 1.0.0`
40
+ footer.
41
+
42
+ The subject is copied verbatim into the changelog, so write it as a lowercase
43
+ phrase with no trailing period. `.github/workflows/pr-title.yml` enforces this
44
+ on every pull request.
45
+
46
+ ## Releasing
47
+
48
+ Releases are cut by merging a pull request; nobody edits a version by hand.
49
+
50
+ 1. Land your PR on `main`.
51
+ 2. `.github/workflows/release.yml` runs
52
+ [release-please](https://github.com/googleapis/release-please), which keeps
53
+ **one open PR** titled something like `chore(main): release 0.2.0`. Each
54
+ merge to `main` refreshes it. The PR bumps `version` in `pyproject.toml`,
55
+ `__version__` in `src/redis_lua_py/__init__.py`, and prepends the new
56
+ section to `CHANGELOG.md`. Review it like any other PR — the changelog body
57
+ is editable before you merge.
58
+ 3. Merge the release PR. That tags `vX.Y.Z`, publishes a GitHub release, and
59
+ the same workflow then builds the sdist and wheel and uploads them to PyPI
60
+ via [trusted publishing](https://docs.pypi.org/trusted-publishers/) — no API
61
+ token is stored anywhere.
62
+
63
+ `.release-please-manifest.json` records the last released version and is
64
+ updated by the release PR. Don't edit it by hand.
65
+
66
+ The full text lives in
67
+ [CONTRIBUTING.md](https://github.com/ignacemaes/redis-lua-py/blob/main/CONTRIBUTING.md).
@@ -0,0 +1,53 @@
1
+ # Development
2
+
3
+ ```bash
4
+ uv sync
5
+ uv run pytest
6
+ uv run ruff check
7
+ uv run mypy
8
+ ```
9
+
10
+ ## The test suite
11
+
12
+ Tests run against [fakeredis](https://github.com/cunla/fakeredis-py), which
13
+ executes real Lua, so `uv run pytest` needs no server. Set `REDIS_URL` to also
14
+ run them against a live Redis:
15
+
16
+ ```bash
17
+ REDIS_URL=redis://localhost:6379/0 uv run pytest
18
+ ```
19
+
20
+ CI runs both, and the two agree — including on reply conversion, which is the
21
+ part you would most want a real server for.
22
+
23
+ ## The command table
24
+
25
+ `src/redis_lua_py/_commands.py` is generated, not written. It is the table
26
+ command names are checked against, and it comes from the command definitions
27
+ in the Redis source. Refresh it when a Redis release adds commands, and commit
28
+ the result:
29
+
30
+ ```bash
31
+ uv run python scripts/generate_commands.py 8.10.1
32
+ ```
33
+
34
+ A stale table can never block a caller: `redis.call('NEW.CMD', ...)` is
35
+ deliberately never checked. See
36
+ [Calling Redis commands](guide/calling-redis-commands.md#the-escape-hatch).
37
+
38
+ ## The docs site
39
+
40
+ This site is built with [Zensical](https://zensical.org). Serve it locally
41
+ with live reload:
42
+
43
+ ```bash
44
+ uv run zensical serve
45
+ ```
46
+
47
+ Build it the way CI does:
48
+
49
+ ```bash
50
+ uv run zensical build --clean
51
+ ```
52
+
53
+ `.github/workflows/docs.yml` publishes `main` to GitHub Pages on every push.