pydirs 0.1.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.
- pydirs-0.1.1/.github/workflows/ci.yml +133 -0
- pydirs-0.1.1/.github/workflows/release.yml +161 -0
- pydirs-0.1.1/.gitignore +31 -0
- pydirs-0.1.1/CHANGELOG.md +112 -0
- pydirs-0.1.1/Cargo.lock +858 -0
- pydirs-0.1.1/Cargo.toml +35 -0
- pydirs-0.1.1/LICENSE +21 -0
- pydirs-0.1.1/PKG-INFO +500 -0
- pydirs-0.1.1/README.md +470 -0
- pydirs-0.1.1/benches/store_bench.rs +111 -0
- pydirs-0.1.1/benchmarks/README.md +42 -0
- pydirs-0.1.1/benchmarks/compare.py +185 -0
- pydirs-0.1.1/docs/api.md +669 -0
- pydirs-0.1.1/docs/architecture.md +224 -0
- pydirs-0.1.1/docs/persistence.md +246 -0
- pydirs-0.1.1/docs/releasing.md +117 -0
- pydirs-0.1.1/examples/mem_probe.rs +115 -0
- pydirs-0.1.1/pyproject.toml +47 -0
- pydirs-0.1.1/python/pydirs/__init__.py +37 -0
- pydirs-0.1.1/python/pydirs/__main__.py +7 -0
- pydirs-0.1.1/python/pydirs/_api.py +537 -0
- pydirs-0.1.1/python/pydirs/_cache.py +199 -0
- pydirs-0.1.1/python/pydirs/_cli.py +204 -0
- pydirs-0.1.1/python/pydirs/_client.py +597 -0
- pydirs-0.1.1/python/pydirs/_memoize.py +163 -0
- pydirs-0.1.1/python/pydirs/_pydantic.py +70 -0
- pydirs-0.1.1/python/pydirs/_pydirs.pyi +107 -0
- pydirs-0.1.1/python/pydirs/_serializers.py +110 -0
- pydirs-0.1.1/python/pydirs/_server.py +32 -0
- pydirs-0.1.1/python/pydirs/py.typed +0 -0
- pydirs-0.1.1/src/core/evict.rs +108 -0
- pydirs-0.1.1/src/core/expiry.rs +61 -0
- pydirs-0.1.1/src/core/mod.rs +13 -0
- pydirs-0.1.1/src/core/persist/codec.rs +219 -0
- pydirs-0.1.1/src/core/persist/lock.rs +202 -0
- pydirs-0.1.1/src/core/persist/mod.rs +701 -0
- pydirs-0.1.1/src/core/persist/snapshot.rs +326 -0
- pydirs-0.1.1/src/core/persist/wal.rs +214 -0
- pydirs-0.1.1/src/core/store.rs +2110 -0
- pydirs-0.1.1/src/core/value.rs +224 -0
- pydirs-0.1.1/src/lib.rs +805 -0
- pydirs-0.1.1/src/server/commands.rs +440 -0
- pydirs-0.1.1/src/server/mod.rs +244 -0
- pydirs-0.1.1/src/server/resp.rs +406 -0
- pydirs-0.1.1/src/server/tests.rs +574 -0
- pydirs-0.1.1/tests/leaks.rs +429 -0
- pydirs-0.1.1/tests/persist.rs +641 -0
- pydirs-0.1.1/tests/python/conftest.py +77 -0
- pydirs-0.1.1/tests/python/fake_resp.py +93 -0
- pydirs-0.1.1/tests/python/helpers.py +12 -0
- pydirs-0.1.1/tests/python/test_auth_admin.py +353 -0
- pydirs-0.1.1/tests/python/test_basic.py +247 -0
- pydirs-0.1.1/tests/python/test_cli.py +161 -0
- pydirs-0.1.1/tests/python/test_cli_persist.py +260 -0
- pydirs-0.1.1/tests/python/test_client.py +484 -0
- pydirs-0.1.1/tests/python/test_client_features.py +225 -0
- pydirs-0.1.1/tests/python/test_eviction.py +87 -0
- pydirs-0.1.1/tests/python/test_json.py +189 -0
- pydirs-0.1.1/tests/python/test_keys_ttl.py +170 -0
- pydirs-0.1.1/tests/python/test_leaks.py +396 -0
- pydirs-0.1.1/tests/python/test_no_pydantic.py +83 -0
- pydirs-0.1.1/tests/python/test_persist.py +600 -0
- pydirs-0.1.1/tests/python/test_pydantic.py +249 -0
- pydirs-0.1.1/tests/python/test_server.py +179 -0
- pydirs-0.1.1/tests/python/test_shared_api.py +208 -0
- pydirs-0.1.1/tests/python/test_structures.py +152 -0
- pydirs-0.1.1/tests/python/test_sugar.py +71 -0
- pydirs-0.1.1/tests/python/test_threads.py +42 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Runs only when a GitHub Release is created, not on every push or pull request.
|
|
4
|
+
on:
|
|
5
|
+
release:
|
|
6
|
+
types: [created]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
CARGO_TERM_COLOR: always
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lint:
|
|
16
|
+
name: Lint and Rust tests (no Python feature)
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
22
|
+
with:
|
|
23
|
+
components: rustfmt, clippy
|
|
24
|
+
|
|
25
|
+
- uses: Swatinem/rust-cache@v2
|
|
26
|
+
|
|
27
|
+
- name: Check formatting
|
|
28
|
+
run: cargo fmt --all -- --check
|
|
29
|
+
|
|
30
|
+
- name: Clippy (warnings are errors)
|
|
31
|
+
run: cargo clippy --no-default-features --all-targets -- -D warnings
|
|
32
|
+
|
|
33
|
+
- name: Rust tests
|
|
34
|
+
run: cargo test --no-default-features
|
|
35
|
+
|
|
36
|
+
test:
|
|
37
|
+
name: Python ${{ matrix.python }} on ${{ matrix.os }}
|
|
38
|
+
needs: lint
|
|
39
|
+
runs-on: ${{ matrix.os }}
|
|
40
|
+
strategy:
|
|
41
|
+
fail-fast: false
|
|
42
|
+
matrix:
|
|
43
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
44
|
+
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
|
|
48
|
+
- uses: actions/setup-python@v5
|
|
49
|
+
with:
|
|
50
|
+
python-version: ${{ matrix.python }}
|
|
51
|
+
|
|
52
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
53
|
+
|
|
54
|
+
- uses: Swatinem/rust-cache@v2
|
|
55
|
+
|
|
56
|
+
- name: Install build and test tools
|
|
57
|
+
run: pip install maturin "pytest>=7" pytest-asyncio
|
|
58
|
+
|
|
59
|
+
# `maturin develop` needs a venv and mis-detects the interpreter inside one on Windows,
|
|
60
|
+
# so build a wheel against the setup-python interpreter and install it.
|
|
61
|
+
- name: Build wheel (release)
|
|
62
|
+
run: maturin build --release --out dist
|
|
63
|
+
|
|
64
|
+
- name: Install wheel
|
|
65
|
+
run: pip install --no-index --find-links dist pydirs
|
|
66
|
+
|
|
67
|
+
# No version pin: pip resolves a pydantic release that supports each Python in the matrix.
|
|
68
|
+
- name: Install pydantic (optional extra, exercised by tests)
|
|
69
|
+
run: pip install pydantic
|
|
70
|
+
|
|
71
|
+
- name: Run Python tests (leak tests run in the leaks job)
|
|
72
|
+
run: pytest -m "not leak"
|
|
73
|
+
|
|
74
|
+
leaks:
|
|
75
|
+
name: Memory-leak tests (Linux)
|
|
76
|
+
needs: lint
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
env:
|
|
79
|
+
# Leak tests read this to scale iteration counts down under valgrind.
|
|
80
|
+
PYDIRS_VALGRIND: "1"
|
|
81
|
+
steps:
|
|
82
|
+
- uses: actions/checkout@v4
|
|
83
|
+
|
|
84
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
85
|
+
|
|
86
|
+
- uses: Swatinem/rust-cache@v2
|
|
87
|
+
|
|
88
|
+
- name: Install valgrind and jq
|
|
89
|
+
run: |
|
|
90
|
+
sudo apt-get update
|
|
91
|
+
sudo apt-get install -y valgrind jq
|
|
92
|
+
|
|
93
|
+
- name: Build Rust test binaries
|
|
94
|
+
run: |
|
|
95
|
+
cargo test --no-default-features --no-run --message-format=json > cargo-test-build.json
|
|
96
|
+
# Both the lib unit-test binary and the tests/leaks.rs binary must build.
|
|
97
|
+
TARGETS='["_pydirs", "leaks"]'
|
|
98
|
+
jq -r --argjson targets "$TARGETS" '
|
|
99
|
+
select(.reason == "compiler-artifact" and .profile.test == true and .executable != null)
|
|
100
|
+
| select(.target.name as $n | $targets | index($n))
|
|
101
|
+
| .executable' cargo-test-build.json > test-binaries.txt
|
|
102
|
+
echo "Test binaries to check:"
|
|
103
|
+
cat test-binaries.txt
|
|
104
|
+
if [ ! -s test-binaries.txt ]; then
|
|
105
|
+
echo "::error::No lib test binary found in cargo JSON output."
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
- name: Run Rust tests under valgrind
|
|
110
|
+
run: |
|
|
111
|
+
while read -r bin; do
|
|
112
|
+
echo "::group::valgrind $bin"
|
|
113
|
+
valgrind --leak-check=full --errors-for-leak-kinds=definite \
|
|
114
|
+
--error-exitcode=1 "$bin" --test-threads=1
|
|
115
|
+
echo "::endgroup::"
|
|
116
|
+
done < test-binaries.txt
|
|
117
|
+
|
|
118
|
+
- uses: actions/setup-python@v5
|
|
119
|
+
with:
|
|
120
|
+
python-version: "3.12"
|
|
121
|
+
|
|
122
|
+
- name: Build and install extension (release)
|
|
123
|
+
run: |
|
|
124
|
+
python -m venv .venv
|
|
125
|
+
echo "$PWD/.venv/bin" >> "$GITHUB_PATH"
|
|
126
|
+
echo "VIRTUAL_ENV=$PWD/.venv" >> "$GITHUB_ENV"
|
|
127
|
+
. .venv/bin/activate
|
|
128
|
+
pip install maturin "pytest>=7" pytest-asyncio pydantic
|
|
129
|
+
maturin develop --release
|
|
130
|
+
|
|
131
|
+
- name: Run Python leak tests
|
|
132
|
+
run: |
|
|
133
|
+
pytest -m leak -v
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Only runs when a GitHub Release is published. Pushes, tags and manual
|
|
4
|
+
# dispatch do NOT trigger this workflow.
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
check-version:
|
|
14
|
+
name: Verify tag matches Cargo.toml version
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Compare release tag with package version
|
|
20
|
+
env:
|
|
21
|
+
TAG: ${{ github.event.release.tag_name }}
|
|
22
|
+
run: |
|
|
23
|
+
TAG_VERSION="${TAG#v}"
|
|
24
|
+
CARGO_VERSION=$(grep -m1 '^version' Cargo.toml | sed -E 's/^version *= *"([^"]+)".*/\1/')
|
|
25
|
+
echo "Release tag: ${TAG} (version ${TAG_VERSION})"
|
|
26
|
+
echo "Cargo.toml version: ${CARGO_VERSION}"
|
|
27
|
+
if [ "${TAG_VERSION}" != "${CARGO_VERSION}" ]; then
|
|
28
|
+
echo "::error::Release tag '${TAG}' does not match Cargo.toml version '${CARGO_VERSION}'. Bump Cargo.toml and re-release."
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
build-wheels:
|
|
33
|
+
name: Wheels ${{ matrix.name }}
|
|
34
|
+
needs: check-version
|
|
35
|
+
runs-on: ${{ matrix.runs-on }}
|
|
36
|
+
strategy:
|
|
37
|
+
fail-fast: false
|
|
38
|
+
matrix:
|
|
39
|
+
include:
|
|
40
|
+
- name: linux-x86_64
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
target: x86_64
|
|
43
|
+
manylinux: auto
|
|
44
|
+
- name: linux-aarch64
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
target: aarch64
|
|
47
|
+
manylinux: auto
|
|
48
|
+
- name: musllinux-x86_64
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
target: x86_64
|
|
51
|
+
manylinux: musllinux_1_2
|
|
52
|
+
- name: musllinux-aarch64
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
target: aarch64
|
|
55
|
+
manylinux: musllinux_1_2
|
|
56
|
+
- name: macos-x86_64
|
|
57
|
+
runs-on: macos-latest
|
|
58
|
+
target: x86_64
|
|
59
|
+
manylinux: ""
|
|
60
|
+
- name: macos-aarch64
|
|
61
|
+
runs-on: macos-latest
|
|
62
|
+
target: aarch64
|
|
63
|
+
manylinux: ""
|
|
64
|
+
- name: windows-x64
|
|
65
|
+
runs-on: windows-latest
|
|
66
|
+
target: x64
|
|
67
|
+
manylinux: ""
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v4
|
|
70
|
+
|
|
71
|
+
- uses: PyO3/maturin-action@v1
|
|
72
|
+
with:
|
|
73
|
+
target: ${{ matrix.target }}
|
|
74
|
+
manylinux: ${{ matrix.manylinux }}
|
|
75
|
+
args: --release --out dist
|
|
76
|
+
|
|
77
|
+
- uses: actions/upload-artifact@v4
|
|
78
|
+
with:
|
|
79
|
+
name: dist-wheels-${{ matrix.name }}
|
|
80
|
+
path: dist/*.whl
|
|
81
|
+
if-no-files-found: error
|
|
82
|
+
|
|
83
|
+
build-sdist:
|
|
84
|
+
name: Source distribution
|
|
85
|
+
needs: check-version
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
|
|
90
|
+
- uses: PyO3/maturin-action@v1
|
|
91
|
+
with:
|
|
92
|
+
command: sdist
|
|
93
|
+
args: --out dist
|
|
94
|
+
|
|
95
|
+
- uses: actions/upload-artifact@v4
|
|
96
|
+
with:
|
|
97
|
+
name: dist-sdist
|
|
98
|
+
path: dist/*.tar.gz
|
|
99
|
+
if-no-files-found: error
|
|
100
|
+
|
|
101
|
+
smoke-test:
|
|
102
|
+
name: Smoke test on Python ${{ matrix.python }}
|
|
103
|
+
needs: [build-wheels, build-sdist]
|
|
104
|
+
runs-on: ubuntu-latest
|
|
105
|
+
strategy:
|
|
106
|
+
fail-fast: true
|
|
107
|
+
matrix:
|
|
108
|
+
python: ["3.9", "3.14"]
|
|
109
|
+
steps:
|
|
110
|
+
- uses: actions/checkout@v4
|
|
111
|
+
|
|
112
|
+
- uses: actions/setup-python@v5
|
|
113
|
+
with:
|
|
114
|
+
python-version: ${{ matrix.python }}
|
|
115
|
+
|
|
116
|
+
- uses: actions/download-artifact@v4
|
|
117
|
+
with:
|
|
118
|
+
pattern: dist-*
|
|
119
|
+
path: dist
|
|
120
|
+
merge-multiple: true
|
|
121
|
+
|
|
122
|
+
- name: Install wheel from artifacts (no index)
|
|
123
|
+
run: |
|
|
124
|
+
pip install --no-index --find-links dist pydirs
|
|
125
|
+
pip list | grep -i pydirs
|
|
126
|
+
|
|
127
|
+
- name: Import and round-trip a value
|
|
128
|
+
working-directory: /tmp
|
|
129
|
+
run: |
|
|
130
|
+
python -c "
|
|
131
|
+
import pydirs
|
|
132
|
+
from pydirs import Cache
|
|
133
|
+
print('pydirs', pydirs.__version__)
|
|
134
|
+
with Cache() as c:
|
|
135
|
+
assert c.set('smoke', 'ok') is True
|
|
136
|
+
assert c.get('smoke') == b'ok'
|
|
137
|
+
print('smoke test passed')
|
|
138
|
+
"
|
|
139
|
+
|
|
140
|
+
publish:
|
|
141
|
+
name: Publish to PyPI
|
|
142
|
+
needs: [check-version, build-wheels, build-sdist, smoke-test]
|
|
143
|
+
runs-on: ubuntu-latest
|
|
144
|
+
permissions:
|
|
145
|
+
# Needed for PyPI trusted publishing (OIDC). Also harmless with an API token.
|
|
146
|
+
id-token: write
|
|
147
|
+
contents: read
|
|
148
|
+
steps:
|
|
149
|
+
- uses: actions/download-artifact@v4
|
|
150
|
+
with:
|
|
151
|
+
pattern: dist-*
|
|
152
|
+
path: dist
|
|
153
|
+
merge-multiple: true
|
|
154
|
+
|
|
155
|
+
- name: List artifacts to publish
|
|
156
|
+
run: ls -la dist
|
|
157
|
+
|
|
158
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
159
|
+
with:
|
|
160
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
161
|
+
skip-existing: true
|
pydirs-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Rust
|
|
2
|
+
/target/
|
|
3
|
+
**/*.rs.bk
|
|
4
|
+
Cargo.lock.bak
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*.so
|
|
10
|
+
*.pyd
|
|
11
|
+
*.egg-info/
|
|
12
|
+
.eggs/
|
|
13
|
+
build/
|
|
14
|
+
dist/
|
|
15
|
+
.venv/
|
|
16
|
+
venv/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
|
|
21
|
+
# maturin
|
|
22
|
+
*.whl
|
|
23
|
+
|
|
24
|
+
# Editors and OS
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
*.swp
|
|
28
|
+
.DS_Store
|
|
29
|
+
|
|
30
|
+
PLAN.md
|
|
31
|
+
.claude/
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.1] - 2026-10-15
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Renamed the project to `pydirs` (PyPI distribution, import name, native module
|
|
15
|
+
`_pydirs`, `pydirs-server` command, `PYDIRS_PASSWORD`), because `tinycache-rs`
|
|
16
|
+
could not be published under the old name.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Reopening a data directory right after dropping a store no longer fails with a
|
|
21
|
+
spurious "locked" error while the sweeper thread releases its last reference.
|
|
22
|
+
- `Server.stop` keeps waking the accept thread until it exits, so shutdown cannot hang.
|
|
23
|
+
- Release and CI workflows: `id-token: write` for PyPI publishing, wheel-based CI
|
|
24
|
+
install that works on Windows.
|
|
25
|
+
|
|
26
|
+
## [0.1.0] - 2026-10-14
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Rust core: sharded in-memory store (64 shards by default, `parking_lot` locks)
|
|
31
|
+
with string, hash, list and set values.
|
|
32
|
+
- Key expiry with `ex` / `px` on `set`, `expire`, `pexpire`, `persist`, `ttl`
|
|
33
|
+
and `pttl`. Lazy expiry on read plus a background sweeper thread.
|
|
34
|
+
- Optional memory limit (`max_memory`) with `noeviction`, `allkeys-lru` and
|
|
35
|
+
`volatile-lru` policies using sampled LRU.
|
|
36
|
+
- Python package `pydirs` (distribution `pydirs`) built with PyO3 and
|
|
37
|
+
maturin against the `abi3-py39` stable ABI, supporting Python 3.9 through 3.14.
|
|
38
|
+
- `Cache` class with dict-style access, context-manager support and a `decode`
|
|
39
|
+
option for `str` reads.
|
|
40
|
+
- `memoize(ttl=...)` decorator on `Cache` instances.
|
|
41
|
+
- JSON support using the standard library `json` module only (no pickle):
|
|
42
|
+
`set_json`, `get_json`, `mget_json`, `mset_json`, `hset_json`, `hget_json`,
|
|
43
|
+
`lpush_json`, `rpush_json` and `lrange_json`.
|
|
44
|
+
- `Cache(serializer="json")` routes `set`, `get`, `mget`, `mset`, dict-style
|
|
45
|
+
access and `memoize` through JSON, so dicts, lists and other JSON-compatible
|
|
46
|
+
values round-trip. The default remains `serializer="raw"`.
|
|
47
|
+
- Rejection of `NaN` and `Infinity` so stored values are strictly valid JSON.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- The core stores each string key and value in one allocation, with 40-byte table
|
|
52
|
+
entries backed by `hashbrown`. Memory accounting rounds to 16-byte allocation
|
|
53
|
+
granules and counts shard bucket arrays, so `used_memory` tracks RSS growth more
|
|
54
|
+
closely. Constants are calibrated with `examples/mem_probe.rs`.
|
|
55
|
+
- `Cache` and `Client` share one API mixin, so both support the same
|
|
56
|
+
serializers, JSON, pydantic, decoding and `memoize`.
|
|
57
|
+
- Optional pydantic support (`pip install "pydirs[pydantic]"`, pydantic
|
|
58
|
+
v2 only): `set_model`, `get_model`, `mget_model`, `hset_model` and
|
|
59
|
+
`hget_model`. The package works without pydantic, which is imported lazily.
|
|
60
|
+
- `memoize` returns real pydantic model instances when the return annotation
|
|
61
|
+
names a pydantic type. It accepts a `model=` override, supports async
|
|
62
|
+
functions, keeps a cached `None` distinct from a miss, and keys
|
|
63
|
+
pydantic-model arguments by `model_dump_json()`.
|
|
64
|
+
- `Server` class: RESP2 TCP server compatible with `redis-py`. Supports
|
|
65
|
+
strings, keys, hashes, lists, sets, `PING`, `ECHO`, `SETEX`, `DECRBY`,
|
|
66
|
+
`FLUSHDB`, `SELECT` (database 0), `COMMAND`, `AUTH`, `INFO`, `SAVE`, `BGSAVE`,
|
|
67
|
+
`LASTSAVE` and `QUIT`. `Server` is a context manager, and `port` reports the
|
|
68
|
+
port actually bound.
|
|
69
|
+
- Password authentication: `Server(password=...)` and `Client(password=...)`.
|
|
70
|
+
Until a connection sends a correct `AUTH`, only `AUTH` and `QUIT` are accepted,
|
|
71
|
+
and other commands get `NOAUTH`. A wrong password gets `WRONGPASS`.
|
|
72
|
+
- Persistence: `Cache(persist_dir=..., fsync="everysec"|"always"|"never",
|
|
73
|
+
snapshot_interval=300)`, with `save()`, `persistent` and `persist_stats`. The
|
|
74
|
+
design is a snapshot plus a write-ahead log in one directory, with a `LOCK`
|
|
75
|
+
file. `close()` (and leaving a `with` block) writes a final snapshot. See
|
|
76
|
+
`docs/persistence.md` for the guarantees and their limits.
|
|
77
|
+
- Persistence exceptions: `CacheLockedError` (a subclass of `OSError`) when
|
|
78
|
+
another live process holds the directory, and `CorruptDataError` (a subclass of
|
|
79
|
+
`ValueError`) when the snapshot fails its checksum or structure check.
|
|
80
|
+
- `AuthenticationError` (a subclass of `ConnectionError`) for missing or wrong
|
|
81
|
+
passwords on a `Client`.
|
|
82
|
+
- `Client` administration: `info()`, `save()`, `bgsave()` and `lastsave()`.
|
|
83
|
+
`used_memory` on a `Client` reads `INFO`.
|
|
84
|
+
- `Client`: thread-safe, pure-Python RESP2 client for a running server, with the
|
|
85
|
+
same API as `Cache`. It has a connection pool (`pool_size`), per-socket
|
|
86
|
+
`timeout`, `AUTH` via `password`, and retries an idempotent command once after
|
|
87
|
+
a dropped connection. Non-idempotent commands are never retried.
|
|
88
|
+
- `Pipeline` (`client.pipeline()`) sends several commands in one write. Results
|
|
89
|
+
are in `p.results` and `execute()`.
|
|
90
|
+
- Serializer protocol: `Cache(serializer=...)` accepts `"raw"`, `"json"`, or any
|
|
91
|
+
object with `dumps` and `loads` methods. `JsonSerializer` and
|
|
92
|
+
`PydanticSerializer` are provided.
|
|
93
|
+
- Command-line server: `pydirs-server` and `python -m pydirs`, with
|
|
94
|
+
`--host`, `--port`, `--max-memory`, `--policy`, `--shards`, `--dir`, `--fsync`,
|
|
95
|
+
`--snapshot-interval`, `--requirepass` (or `PYDIRS_PASSWORD`),
|
|
96
|
+
`--allow-insecure` and `--version`. It refuses non-loopback binds without a
|
|
97
|
+
password (exit code 2). A locked or corrupt data directory exits with code 1.
|
|
98
|
+
On SIGINT or SIGTERM it stops and, with `--dir`, writes a final snapshot.
|
|
99
|
+
- Memory-leak tests: a Rust counting-allocator suite (`tests/leaks.rs`), Python
|
|
100
|
+
RSS and fd tests under the `leak` marker, and a Linux `leaks` CI job running
|
|
101
|
+
the Rust tests under valgrind.
|
|
102
|
+
- The GIL is released around every core call so Python threads run in parallel.
|
|
103
|
+
- Prebuilt wheels for Linux (manylinux and musllinux, x86_64 and aarch64),
|
|
104
|
+
macOS (x86_64 and arm64) and Windows (x64), plus a source distribution.
|
|
105
|
+
- CI workflow: Rust lint and tests, and a Python test matrix over Ubuntu, macOS
|
|
106
|
+
and Windows.
|
|
107
|
+
- Release workflow: publishes to PyPI when a GitHub Release is published, after
|
|
108
|
+
verifying that the tag matches the `Cargo.toml` version.
|
|
109
|
+
|
|
110
|
+
[Unreleased]: https://github.com/4ursmile/pydirs/compare/v0.1.1...HEAD
|
|
111
|
+
[0.1.1]: https://github.com/4ursmile/pydirs/releases/tag/v0.1.1
|
|
112
|
+
[0.1.0]: https://github.com/4ursmile/pydirs/releases/tag/v0.1.0
|