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.
Files changed (68) hide show
  1. pydirs-0.1.1/.github/workflows/ci.yml +133 -0
  2. pydirs-0.1.1/.github/workflows/release.yml +161 -0
  3. pydirs-0.1.1/.gitignore +31 -0
  4. pydirs-0.1.1/CHANGELOG.md +112 -0
  5. pydirs-0.1.1/Cargo.lock +858 -0
  6. pydirs-0.1.1/Cargo.toml +35 -0
  7. pydirs-0.1.1/LICENSE +21 -0
  8. pydirs-0.1.1/PKG-INFO +500 -0
  9. pydirs-0.1.1/README.md +470 -0
  10. pydirs-0.1.1/benches/store_bench.rs +111 -0
  11. pydirs-0.1.1/benchmarks/README.md +42 -0
  12. pydirs-0.1.1/benchmarks/compare.py +185 -0
  13. pydirs-0.1.1/docs/api.md +669 -0
  14. pydirs-0.1.1/docs/architecture.md +224 -0
  15. pydirs-0.1.1/docs/persistence.md +246 -0
  16. pydirs-0.1.1/docs/releasing.md +117 -0
  17. pydirs-0.1.1/examples/mem_probe.rs +115 -0
  18. pydirs-0.1.1/pyproject.toml +47 -0
  19. pydirs-0.1.1/python/pydirs/__init__.py +37 -0
  20. pydirs-0.1.1/python/pydirs/__main__.py +7 -0
  21. pydirs-0.1.1/python/pydirs/_api.py +537 -0
  22. pydirs-0.1.1/python/pydirs/_cache.py +199 -0
  23. pydirs-0.1.1/python/pydirs/_cli.py +204 -0
  24. pydirs-0.1.1/python/pydirs/_client.py +597 -0
  25. pydirs-0.1.1/python/pydirs/_memoize.py +163 -0
  26. pydirs-0.1.1/python/pydirs/_pydantic.py +70 -0
  27. pydirs-0.1.1/python/pydirs/_pydirs.pyi +107 -0
  28. pydirs-0.1.1/python/pydirs/_serializers.py +110 -0
  29. pydirs-0.1.1/python/pydirs/_server.py +32 -0
  30. pydirs-0.1.1/python/pydirs/py.typed +0 -0
  31. pydirs-0.1.1/src/core/evict.rs +108 -0
  32. pydirs-0.1.1/src/core/expiry.rs +61 -0
  33. pydirs-0.1.1/src/core/mod.rs +13 -0
  34. pydirs-0.1.1/src/core/persist/codec.rs +219 -0
  35. pydirs-0.1.1/src/core/persist/lock.rs +202 -0
  36. pydirs-0.1.1/src/core/persist/mod.rs +701 -0
  37. pydirs-0.1.1/src/core/persist/snapshot.rs +326 -0
  38. pydirs-0.1.1/src/core/persist/wal.rs +214 -0
  39. pydirs-0.1.1/src/core/store.rs +2110 -0
  40. pydirs-0.1.1/src/core/value.rs +224 -0
  41. pydirs-0.1.1/src/lib.rs +805 -0
  42. pydirs-0.1.1/src/server/commands.rs +440 -0
  43. pydirs-0.1.1/src/server/mod.rs +244 -0
  44. pydirs-0.1.1/src/server/resp.rs +406 -0
  45. pydirs-0.1.1/src/server/tests.rs +574 -0
  46. pydirs-0.1.1/tests/leaks.rs +429 -0
  47. pydirs-0.1.1/tests/persist.rs +641 -0
  48. pydirs-0.1.1/tests/python/conftest.py +77 -0
  49. pydirs-0.1.1/tests/python/fake_resp.py +93 -0
  50. pydirs-0.1.1/tests/python/helpers.py +12 -0
  51. pydirs-0.1.1/tests/python/test_auth_admin.py +353 -0
  52. pydirs-0.1.1/tests/python/test_basic.py +247 -0
  53. pydirs-0.1.1/tests/python/test_cli.py +161 -0
  54. pydirs-0.1.1/tests/python/test_cli_persist.py +260 -0
  55. pydirs-0.1.1/tests/python/test_client.py +484 -0
  56. pydirs-0.1.1/tests/python/test_client_features.py +225 -0
  57. pydirs-0.1.1/tests/python/test_eviction.py +87 -0
  58. pydirs-0.1.1/tests/python/test_json.py +189 -0
  59. pydirs-0.1.1/tests/python/test_keys_ttl.py +170 -0
  60. pydirs-0.1.1/tests/python/test_leaks.py +396 -0
  61. pydirs-0.1.1/tests/python/test_no_pydantic.py +83 -0
  62. pydirs-0.1.1/tests/python/test_persist.py +600 -0
  63. pydirs-0.1.1/tests/python/test_pydantic.py +249 -0
  64. pydirs-0.1.1/tests/python/test_server.py +179 -0
  65. pydirs-0.1.1/tests/python/test_shared_api.py +208 -0
  66. pydirs-0.1.1/tests/python/test_structures.py +152 -0
  67. pydirs-0.1.1/tests/python/test_sugar.py +71 -0
  68. 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
@@ -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