zydecodb 0.9.0b4__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.
- zydecodb-0.9.0b4/.gitignore +74 -0
- zydecodb-0.9.0b4/PKG-INFO +101 -0
- zydecodb-0.9.0b4/README.md +83 -0
- zydecodb-0.9.0b4/pyproject.toml +34 -0
- zydecodb-0.9.0b4/tests/test_conformance.py +136 -0
- zydecodb-0.9.0b4/tests/test_live.py +95 -0
- zydecodb-0.9.0b4/tests/test_protocol.py +84 -0
- zydecodb-0.9.0b4/zydecodb/__init__.py +43 -0
- zydecodb-0.9.0b4/zydecodb/_connection.py +120 -0
- zydecodb-0.9.0b4/zydecodb/_protocol.py +255 -0
- zydecodb-0.9.0b4/zydecodb/client.py +295 -0
- zydecodb-0.9.0b4/zydecodb/collection.py +129 -0
- zydecodb-0.9.0b4/zydecodb/errors.py +73 -0
- zydecodb-0.9.0b4/zydecodb/pool.py +119 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Node.js
|
|
2
|
+
node_modules/
|
|
3
|
+
dist/
|
|
4
|
+
experiments/**/*.js
|
|
5
|
+
|
|
6
|
+
# Compiled binaries
|
|
7
|
+
experiments/write_heavy/write_heavy
|
|
8
|
+
clients/go/user_backend
|
|
9
|
+
|
|
10
|
+
# Rust
|
|
11
|
+
/target
|
|
12
|
+
**/*.rs.bk
|
|
13
|
+
|
|
14
|
+
# Runtime data (dev)
|
|
15
|
+
*.sock
|
|
16
|
+
/.zydecodb-data/
|
|
17
|
+
/.zydecodb-run/
|
|
18
|
+
|
|
19
|
+
# Soak run artifacts (baselines in docs/soak-baselines/ are committed)
|
|
20
|
+
/soak-runs/**/data/
|
|
21
|
+
/soak-runs/**/wal/
|
|
22
|
+
/soak-runs/**/*.log
|
|
23
|
+
/soak-runs/**/*.jsonl
|
|
24
|
+
|
|
25
|
+
# Python
|
|
26
|
+
__pycache__/
|
|
27
|
+
*.py[cod]
|
|
28
|
+
.pytest_cache/
|
|
29
|
+
*.egg-info/
|
|
30
|
+
.venv/
|
|
31
|
+
|
|
32
|
+
# Fuzzing
|
|
33
|
+
/fuzz/corpus/
|
|
34
|
+
/fuzz/artifacts/
|
|
35
|
+
/fuzz/target/
|
|
36
|
+
|
|
37
|
+
# Coverage
|
|
38
|
+
/coverage/
|
|
39
|
+
*.profraw
|
|
40
|
+
lcov.info
|
|
41
|
+
|
|
42
|
+
# Secrets / credentials (never commit)
|
|
43
|
+
.env
|
|
44
|
+
.env.*
|
|
45
|
+
!.env.example
|
|
46
|
+
keys.toml
|
|
47
|
+
**/keys.toml
|
|
48
|
+
!config/zydecodb.keys.example.toml
|
|
49
|
+
*.pem
|
|
50
|
+
*.key
|
|
51
|
+
*.p12
|
|
52
|
+
*.pfx
|
|
53
|
+
tls.crt
|
|
54
|
+
tls.key
|
|
55
|
+
|
|
56
|
+
# IDE
|
|
57
|
+
/.idea/
|
|
58
|
+
/.vscode/
|
|
59
|
+
*.swp
|
|
60
|
+
.DS_Store
|
|
61
|
+
|
|
62
|
+
# Empty stub left over from an earlier packaging experiment
|
|
63
|
+
/.github/workflows/publish-python.yml
|
|
64
|
+
|
|
65
|
+
# Website / Marketing (deployed from this tree via scripts/deploy-website.sh;
|
|
66
|
+
# keep the marketing site and ops scripts local unless explicitly force-added)
|
|
67
|
+
/website/
|
|
68
|
+
/scripts/*
|
|
69
|
+
!/scripts/install.sh
|
|
70
|
+
!/scripts/deploy-website.sh
|
|
71
|
+
|
|
72
|
+
# Internal / Archived Docs
|
|
73
|
+
/docs/archive/
|
|
74
|
+
/docs/soak-baselines/
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zydecodb
|
|
3
|
+
Version: 0.9.0b4
|
|
4
|
+
Summary: Official Python driver for ZydecoDB.
|
|
5
|
+
Project-URL: Homepage, https://github.com/dataparade/zydecodb
|
|
6
|
+
Project-URL: Source, https://github.com/dataparade/zydecodb
|
|
7
|
+
Author: Dataparade
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: database,document-store,nosql,zydecodb
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# ZydecoDB Python driver
|
|
20
|
+
|
|
21
|
+
Official Python client for [ZydecoDB](../../README.md). Pure standard library,
|
|
22
|
+
no runtime dependencies.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install zydecodb
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires Python 3.9+. (Working from a checkout of this repo:
|
|
31
|
+
`pip install -e clients/python`.)
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from zydecodb import Client
|
|
37
|
+
|
|
38
|
+
# Plain TCP (localhost). For TLS: Client(..., api_key="YOUR_KEY", tls=True)
|
|
39
|
+
with Client("127.0.0.1", 9470, api_key="YOUR_KEY") as db:
|
|
40
|
+
users = db.collection("users")
|
|
41
|
+
users.create_index(["email"], unique=True)
|
|
42
|
+
|
|
43
|
+
uid = users.insert_one({"email": "ada@example.com", "name": "Ada", "age": 30})
|
|
44
|
+
|
|
45
|
+
for u in users.find({"age": {"$gte": 18}}, sort=[("age", True)]):
|
|
46
|
+
print(u["name"], u["age"])
|
|
47
|
+
|
|
48
|
+
users.update_one({"_id": uid}, {"$inc": {"age": 1}})
|
|
49
|
+
print(users.count_documents())
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What you get
|
|
53
|
+
|
|
54
|
+
- **Connection pooling.** `Client` owns a thread-safe pool (`pool_size`,
|
|
55
|
+
default 8) and is safe to share across threads.
|
|
56
|
+
- **Automatic retries with backoff.** Transient transport failures and server
|
|
57
|
+
`EngineBusy` responses are retried (full-jitter exponential backoff) for
|
|
58
|
+
operations that are safe to repeat. Operator updates and deletes are never
|
|
59
|
+
retried automatically.
|
|
60
|
+
- **Keepalive.** Idle pooled connections are validated with a `Ping` on
|
|
61
|
+
checkout and transparently replaced if dead.
|
|
62
|
+
- **Typed error taxonomy.** Non-OK responses raise a specific subclass:
|
|
63
|
+
`ConflictError` (unique-index violation), `AuthError`, `ServerBusyError`,
|
|
64
|
+
`InvalidRequestError`, or the base `ServerError` — each carrying the wire
|
|
65
|
+
`status` byte. Transport problems raise `ConnectionError`.
|
|
66
|
+
- **`Collection` API.** `insert_one/many`, `find`/`find_one`,
|
|
67
|
+
`update_one/many`, `delete_one/many`, `count_documents`, `distinct`,
|
|
68
|
+
`create_index`, with `$`-operators, sort, projection, and skip/limit.
|
|
69
|
+
Pagination is repeatable-read across pages.
|
|
70
|
+
- **Raw KV with TTL.** Side-channel `put` (with `expires_at`), `get`, and `delete` methods on `Client` for session data that needs a time-to-live.
|
|
71
|
+
- **TLS.** Pass `tls=True` for system CA defaults, or an `ssl.SSLContext` for custom roots / verification.
|
|
72
|
+
|
|
73
|
+
## Durability
|
|
74
|
+
|
|
75
|
+
Writes are durable (fsync-on-commit) by default. For latency-sensitive,
|
|
76
|
+
loss-tolerant writes, pass `relaxed=True` to acknowledge before the fsync.
|
|
77
|
+
It is available on every write: `insert_one`, `replace_one`, `update_one`,
|
|
78
|
+
`update_many`, `delete_one`, and `delete_many`.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
users.insert_one(doc, relaxed=True)
|
|
82
|
+
users.update_one({"_id": "ada"}, {"$inc": {"hits": 1}}, relaxed=True)
|
|
83
|
+
users.delete_many({"stale": True}, relaxed=True)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Running the tests
|
|
87
|
+
|
|
88
|
+
Unit tests for the wire codecs need no server:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd clients/python
|
|
92
|
+
pip install -e ".[dev]"
|
|
93
|
+
pytest tests/test_protocol.py
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Integration tests run against a live server selected by environment variables
|
|
97
|
+
(skipped automatically if it is unreachable):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
ZYDECODB_TEST_HOST=127.0.0.1 ZYDECODB_TEST_PORT=9470 pytest
|
|
101
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# ZydecoDB Python driver
|
|
2
|
+
|
|
3
|
+
Official Python client for [ZydecoDB](../../README.md). Pure standard library,
|
|
4
|
+
no runtime dependencies.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install zydecodb
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires Python 3.9+. (Working from a checkout of this repo:
|
|
13
|
+
`pip install -e clients/python`.)
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from zydecodb import Client
|
|
19
|
+
|
|
20
|
+
# Plain TCP (localhost). For TLS: Client(..., api_key="YOUR_KEY", tls=True)
|
|
21
|
+
with Client("127.0.0.1", 9470, api_key="YOUR_KEY") as db:
|
|
22
|
+
users = db.collection("users")
|
|
23
|
+
users.create_index(["email"], unique=True)
|
|
24
|
+
|
|
25
|
+
uid = users.insert_one({"email": "ada@example.com", "name": "Ada", "age": 30})
|
|
26
|
+
|
|
27
|
+
for u in users.find({"age": {"$gte": 18}}, sort=[("age", True)]):
|
|
28
|
+
print(u["name"], u["age"])
|
|
29
|
+
|
|
30
|
+
users.update_one({"_id": uid}, {"$inc": {"age": 1}})
|
|
31
|
+
print(users.count_documents())
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What you get
|
|
35
|
+
|
|
36
|
+
- **Connection pooling.** `Client` owns a thread-safe pool (`pool_size`,
|
|
37
|
+
default 8) and is safe to share across threads.
|
|
38
|
+
- **Automatic retries with backoff.** Transient transport failures and server
|
|
39
|
+
`EngineBusy` responses are retried (full-jitter exponential backoff) for
|
|
40
|
+
operations that are safe to repeat. Operator updates and deletes are never
|
|
41
|
+
retried automatically.
|
|
42
|
+
- **Keepalive.** Idle pooled connections are validated with a `Ping` on
|
|
43
|
+
checkout and transparently replaced if dead.
|
|
44
|
+
- **Typed error taxonomy.** Non-OK responses raise a specific subclass:
|
|
45
|
+
`ConflictError` (unique-index violation), `AuthError`, `ServerBusyError`,
|
|
46
|
+
`InvalidRequestError`, or the base `ServerError` — each carrying the wire
|
|
47
|
+
`status` byte. Transport problems raise `ConnectionError`.
|
|
48
|
+
- **`Collection` API.** `insert_one/many`, `find`/`find_one`,
|
|
49
|
+
`update_one/many`, `delete_one/many`, `count_documents`, `distinct`,
|
|
50
|
+
`create_index`, with `$`-operators, sort, projection, and skip/limit.
|
|
51
|
+
Pagination is repeatable-read across pages.
|
|
52
|
+
- **Raw KV with TTL.** Side-channel `put` (with `expires_at`), `get`, and `delete` methods on `Client` for session data that needs a time-to-live.
|
|
53
|
+
- **TLS.** Pass `tls=True` for system CA defaults, or an `ssl.SSLContext` for custom roots / verification.
|
|
54
|
+
|
|
55
|
+
## Durability
|
|
56
|
+
|
|
57
|
+
Writes are durable (fsync-on-commit) by default. For latency-sensitive,
|
|
58
|
+
loss-tolerant writes, pass `relaxed=True` to acknowledge before the fsync.
|
|
59
|
+
It is available on every write: `insert_one`, `replace_one`, `update_one`,
|
|
60
|
+
`update_many`, `delete_one`, and `delete_many`.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
users.insert_one(doc, relaxed=True)
|
|
64
|
+
users.update_one({"_id": "ada"}, {"$inc": {"hits": 1}}, relaxed=True)
|
|
65
|
+
users.delete_many({"stale": True}, relaxed=True)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Running the tests
|
|
69
|
+
|
|
70
|
+
Unit tests for the wire codecs need no server:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
cd clients/python
|
|
74
|
+
pip install -e ".[dev]"
|
|
75
|
+
pytest tests/test_protocol.py
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Integration tests run against a live server selected by environment variables
|
|
79
|
+
(skipped automatically if it is unreachable):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
ZYDECODB_TEST_HOST=127.0.0.1 ZYDECODB_TEST_PORT=9470 pytest
|
|
83
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zydecodb"
|
|
7
|
+
version = "0.9.0b4"
|
|
8
|
+
description = "Official Python driver for ZydecoDB."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Dataparade" }]
|
|
13
|
+
keywords = ["zydecodb", "database", "document-store", "nosql"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Database :: Front-Ends",
|
|
19
|
+
]
|
|
20
|
+
# Pure stdlib: no runtime dependencies.
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["pytest>=7"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/dataparade/zydecodb"
|
|
28
|
+
Source = "https://github.com/dataparade/zydecodb"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["zydecodb"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Conformance: the Python codec must match the shared wire vectors byte-for-byte.
|
|
2
|
+
|
|
3
|
+
The vectors in `clients/conformance/vectors.json` are generated from the Rust
|
|
4
|
+
server encoders (the protocol authority). Running the Python codec against them
|
|
5
|
+
proves it cannot silently drift from the server. See `clients/conformance/README.md`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from zydecodb import _protocol as proto
|
|
16
|
+
|
|
17
|
+
VECTORS_PATH = Path(__file__).resolve().parents[2] / "conformance" / "vectors.json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load():
|
|
21
|
+
with VECTORS_PATH.open(encoding="utf-8") as fh:
|
|
22
|
+
return json.load(fh)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
VECTORS = _load()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _json_field(s: str):
|
|
29
|
+
"""An opaque pre-serialized JSON field -> the object Python re-serializes to
|
|
30
|
+
the same bytes (empty string means "absent")."""
|
|
31
|
+
return None if s == "" else json.loads(s)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _encode_request(kind: str, inp: dict) -> bytes:
|
|
35
|
+
if kind == "Put":
|
|
36
|
+
return proto.encode_put(
|
|
37
|
+
bytes.fromhex(inp["key_hex"]),
|
|
38
|
+
bytes.fromhex(inp["value_hex"]),
|
|
39
|
+
expires_at=inp["expires_at"],
|
|
40
|
+
)
|
|
41
|
+
if kind == "Get":
|
|
42
|
+
return proto.encode_key(bytes.fromhex(inp["key_hex"]))
|
|
43
|
+
if kind == "Del":
|
|
44
|
+
return proto.encode_key(bytes.fromhex(inp["key_hex"]))
|
|
45
|
+
if kind == "DocPut":
|
|
46
|
+
return proto.encode_doc_put(
|
|
47
|
+
inp["collection"], inp["doc_id"], _json_field(inp["body_json"]),
|
|
48
|
+
relaxed=inp["relaxed"],
|
|
49
|
+
)
|
|
50
|
+
if kind == "DocDel":
|
|
51
|
+
return proto.encode_doc_del(inp["collection"], inp["doc_id"])
|
|
52
|
+
if kind == "IndexDef":
|
|
53
|
+
return proto.encode_index_def(
|
|
54
|
+
inp["collection"], inp["index_name"], inp["fields"], unique=inp["unique"]
|
|
55
|
+
)
|
|
56
|
+
if kind == "QueryById":
|
|
57
|
+
return proto.encode_query_by_id(inp["collection"], inp["doc_id"])
|
|
58
|
+
if kind == "QueryIndexRange":
|
|
59
|
+
return proto.encode_query_index_range(
|
|
60
|
+
inp["collection"], inp["index_name"],
|
|
61
|
+
lo=_json_field(inp["lo_json"]), hi=_json_field(inp["hi_json"]),
|
|
62
|
+
page_size=inp["limit"], cursor=bytes.fromhex(inp["cursor_hex"]),
|
|
63
|
+
)
|
|
64
|
+
if kind == "Find":
|
|
65
|
+
proj = inp["projection"]
|
|
66
|
+
mode = {"none": None, "include": proto.PROJ_INCLUDE, "exclude": proto.PROJ_EXCLUDE}[proj["mode"]]
|
|
67
|
+
projection = None if mode is None else (mode, proj["fields"])
|
|
68
|
+
return proto.encode_find(
|
|
69
|
+
inp["collection"], _json_field(inp["filter_json"]),
|
|
70
|
+
[tuple(s) for s in inp["sort"]], projection,
|
|
71
|
+
inp["skip"], inp["limit"], bytes.fromhex(inp["cursor_hex"]),
|
|
72
|
+
)
|
|
73
|
+
if kind == "Update":
|
|
74
|
+
return proto.encode_update(
|
|
75
|
+
inp["collection"], _json_field(inp["filter_json"]),
|
|
76
|
+
_json_field(inp["update_json"]), multi=inp["multi"], relaxed=inp["relaxed"],
|
|
77
|
+
)
|
|
78
|
+
if kind == "Delete":
|
|
79
|
+
return proto.encode_delete(
|
|
80
|
+
inp["collection"], _json_field(inp["filter_json"]),
|
|
81
|
+
multi=inp["multi"], relaxed=inp["relaxed"],
|
|
82
|
+
)
|
|
83
|
+
if kind == "Count":
|
|
84
|
+
return proto.encode_count(inp["collection"], _json_field(inp["filter_json"]))
|
|
85
|
+
if kind == "Distinct":
|
|
86
|
+
return proto.encode_distinct(
|
|
87
|
+
inp["collection"], inp["field"], _json_field(inp["filter_json"])
|
|
88
|
+
)
|
|
89
|
+
if kind == "SessionInit":
|
|
90
|
+
return inp["api_key"].encode("utf-8")
|
|
91
|
+
if kind == "Ping":
|
|
92
|
+
return b""
|
|
93
|
+
raise AssertionError(f"unhandled request kind: {kind}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.mark.parametrize("vec", VECTORS["requests"], ids=lambda v: v["name"])
|
|
97
|
+
def test_request_payload_matches(vec):
|
|
98
|
+
payload = _encode_request(vec["kind"], vec["input"])
|
|
99
|
+
assert payload.hex() == vec["payload_hex"], vec["name"]
|
|
100
|
+
envelope = proto.encode_header(vec["command"], len(payload)) + payload
|
|
101
|
+
assert envelope.hex() == vec["envelope_hex"], vec["name"]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.mark.parametrize("vec", VECTORS["responses"], ids=lambda v: v["name"])
|
|
105
|
+
def test_response_decode_matches(vec):
|
|
106
|
+
assert vec["kind"] == "QueryPage"
|
|
107
|
+
rows, cursor = proto.decode_page(bytes.fromhex(vec["bytes_hex"]))
|
|
108
|
+
expected_rows = vec["decoded"]["rows"]
|
|
109
|
+
assert len(rows) == len(expected_rows), vec["name"]
|
|
110
|
+
for (doc_id, body), exp in zip(rows, expected_rows):
|
|
111
|
+
assert doc_id.decode("utf-8") == exp["doc_id"]
|
|
112
|
+
assert body.decode("utf-8") == exp["body_json"]
|
|
113
|
+
expected_cursor = vec["decoded"]["next_cursor_hex"]
|
|
114
|
+
if expected_cursor is None:
|
|
115
|
+
assert cursor == b""
|
|
116
|
+
else:
|
|
117
|
+
assert cursor.hex() == expected_cursor
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_command_codes_match_vectors():
|
|
121
|
+
cmds = VECTORS["commands"]
|
|
122
|
+
assert proto.CMD_DOC_PUT == cmds["DocPut"]
|
|
123
|
+
assert proto.CMD_FIND == cmds["Find"]
|
|
124
|
+
assert proto.CMD_UPDATE == cmds["Update"]
|
|
125
|
+
assert proto.CMD_DELETE == cmds["Delete"]
|
|
126
|
+
assert proto.CMD_COUNT == cmds["Count"]
|
|
127
|
+
assert proto.CMD_INDEX_DEF == cmds["IndexDef"]
|
|
128
|
+
assert proto.CMD_SESSION_INIT == cmds["SessionInit"]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_status_codes_match_vectors():
|
|
132
|
+
st = VECTORS["statuses"]
|
|
133
|
+
assert proto.STATUS_OK == st["Ok"]
|
|
134
|
+
assert proto.STATUS_ENGINE_BUSY == st["EngineBusy"]
|
|
135
|
+
assert proto.STATUS_UNAUTHORIZED == st["Unauthorized"]
|
|
136
|
+
assert proto.STATUS_FORBIDDEN == st["Forbidden"]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Integration tests against a live ZydecoDB server.
|
|
2
|
+
|
|
3
|
+
Set ZYDECODB_TEST_HOST / ZYDECODB_TEST_PORT (and optionally ZYDECODB_TEST_API_KEY)
|
|
4
|
+
to point at a running server. The whole module is skipped when the server is not
|
|
5
|
+
reachable, so a plain `pytest` run stays green offline; CI starts a server first.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from zydecodb import Client, ConflictError
|
|
15
|
+
|
|
16
|
+
HOST = os.environ.get("ZYDECODB_TEST_HOST", "127.0.0.1")
|
|
17
|
+
PORT = int(os.environ.get("ZYDECODB_TEST_PORT", "9470"))
|
|
18
|
+
API_KEY = os.environ.get("ZYDECODB_TEST_API_KEY") or None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _server_up() -> bool:
|
|
22
|
+
try:
|
|
23
|
+
with socket.create_connection((HOST, PORT), timeout=1.0):
|
|
24
|
+
return True
|
|
25
|
+
except OSError:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
pytestmark = pytest.mark.skipif(
|
|
30
|
+
not _server_up(), reason=f"no ZydecoDB server at {HOST}:{PORT}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture()
|
|
35
|
+
def db():
|
|
36
|
+
client = Client(HOST, PORT, api_key=API_KEY)
|
|
37
|
+
yield client
|
|
38
|
+
client.close()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture()
|
|
42
|
+
def coll(db):
|
|
43
|
+
# A unique collection per test keeps runs isolated.
|
|
44
|
+
return db.collection(f"pytest_{uuid.uuid4().hex[:12]}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_ping(db):
|
|
48
|
+
db.ping()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_insert_find_update_delete(coll):
|
|
52
|
+
coll.create_index(["age"])
|
|
53
|
+
ids = coll.insert_many(
|
|
54
|
+
[
|
|
55
|
+
{"name": "Ada", "age": 30, "city": "London"},
|
|
56
|
+
{"name": "Bo", "age": 25, "city": "NOLA"},
|
|
57
|
+
{"name": "Cy", "age": 40, "city": "NOLA"},
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
assert len(ids) == 3
|
|
61
|
+
|
|
62
|
+
got = list(coll.find({"age": {"$gte": 30}}, sort=[("age", True)]))
|
|
63
|
+
assert [d["name"] for d in got] == ["Ada", "Cy"]
|
|
64
|
+
|
|
65
|
+
res = coll.update_one({"name": "Bo"}, {"$inc": {"age": 10}})
|
|
66
|
+
assert res["matched"] == 1 and res["modified"] == 1
|
|
67
|
+
assert coll.find_one({"name": "Bo"})["age"] == 35
|
|
68
|
+
|
|
69
|
+
assert coll.count_documents() == 3
|
|
70
|
+
assert sorted(coll.distinct("city")) == ["London", "NOLA"]
|
|
71
|
+
|
|
72
|
+
assert coll.delete_many({"city": "NOLA"}) == 2
|
|
73
|
+
assert coll.count_documents() == 1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_unique_index_conflict(coll):
|
|
77
|
+
coll.create_index(["email"], unique=True)
|
|
78
|
+
coll.insert_one({"email": "a@b.com"})
|
|
79
|
+
with pytest.raises(ConflictError):
|
|
80
|
+
coll.insert_one({"email": "a@b.com"})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_pagination_is_stable(coll):
|
|
84
|
+
coll.create_index(["n"])
|
|
85
|
+
coll.insert_many([{"n": i} for i in range(25)])
|
|
86
|
+
seen = [d["n"] for d in coll.find({"n": {"$gte": 0}}, page_size=10)]
|
|
87
|
+
assert sorted(seen) == list(range(25))
|
|
88
|
+
assert len(seen) == 25 # no duplicates across pages
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_get_by_id(coll):
|
|
92
|
+
doc_id = coll.insert_one({"name": "Zee"})
|
|
93
|
+
fetched = coll.get(doc_id)
|
|
94
|
+
assert fetched["name"] == "Zee"
|
|
95
|
+
assert coll.get("does-not-exist") is None
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Server-independent unit tests for the wire codecs and error taxonomy."""
|
|
2
|
+
|
|
3
|
+
import struct
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from zydecodb import (
|
|
8
|
+
AuthError,
|
|
9
|
+
ConflictError,
|
|
10
|
+
InvalidRequestError,
|
|
11
|
+
ServerBusyError,
|
|
12
|
+
ServerError,
|
|
13
|
+
)
|
|
14
|
+
from zydecodb import _protocol as proto
|
|
15
|
+
from zydecodb.collection import _encode_projection
|
|
16
|
+
from zydecodb.errors import from_status
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _encode_page(rows, cursor=b""):
|
|
20
|
+
out = struct.pack(">I", len(rows))
|
|
21
|
+
for doc_id, body in rows:
|
|
22
|
+
out += struct.pack(">I", len(doc_id)) + doc_id
|
|
23
|
+
out += struct.pack(">I", len(body)) + body
|
|
24
|
+
out += struct.pack(">I", len(cursor)) + cursor
|
|
25
|
+
return out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_decode_page_roundtrip():
|
|
29
|
+
rows = [(b"u1", b'{"a":1}'), (b"u2", b"")]
|
|
30
|
+
buf = _encode_page(rows, cursor=b"NEXT")
|
|
31
|
+
decoded_rows, cursor = proto.decode_page(buf)
|
|
32
|
+
assert decoded_rows == rows
|
|
33
|
+
assert cursor == b"NEXT"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_decode_empty_page():
|
|
37
|
+
decoded_rows, cursor = proto.decode_page(_encode_page([]))
|
|
38
|
+
assert decoded_rows == []
|
|
39
|
+
assert cursor == b""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_doc_put_carries_relaxed_flag():
|
|
43
|
+
durable = proto.encode_doc_put("c", "id", {"x": 1}, relaxed=False)
|
|
44
|
+
relaxed = proto.encode_doc_put("c", "id", {"x": 1}, relaxed=True)
|
|
45
|
+
assert durable[-1] == 0
|
|
46
|
+
assert relaxed[-1] == proto.FLAG_RELAXED
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_update_and_delete_trailing_flags():
|
|
50
|
+
upd = proto.encode_update("c", {"_id": "x"}, {"$set": {"n": 1}}, multi=True, relaxed=True)
|
|
51
|
+
# ...multi byte, then relaxed flag byte.
|
|
52
|
+
assert upd[-2] == 1 and upd[-1] == proto.FLAG_RELAXED
|
|
53
|
+
dele = proto.encode_delete("c", {"a": 1}, multi=False, relaxed=False)
|
|
54
|
+
assert dele[-2] == 0 and dele[-1] == 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize(
|
|
58
|
+
"status,exc",
|
|
59
|
+
[
|
|
60
|
+
(proto.STATUS_CONFLICT, ConflictError),
|
|
61
|
+
(proto.STATUS_UNAUTHORIZED, AuthError),
|
|
62
|
+
(proto.STATUS_FORBIDDEN, AuthError),
|
|
63
|
+
(proto.STATUS_ENGINE_BUSY, ServerBusyError),
|
|
64
|
+
(proto.STATUS_PROTOCOL_ERROR, InvalidRequestError),
|
|
65
|
+
(proto.STATUS_INVALID_KEY, InvalidRequestError),
|
|
66
|
+
(proto.STATUS_ERROR, ServerError),
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
def test_status_maps_to_exception(status, exc):
|
|
70
|
+
err = from_status(status, "Op", b"detail")
|
|
71
|
+
assert isinstance(err, exc)
|
|
72
|
+
assert err.status == status
|
|
73
|
+
assert "Op failed" in str(err)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_projection_encoding():
|
|
77
|
+
assert _encode_projection(None) is None
|
|
78
|
+
assert _encode_projection({"name": 1}) == (proto.PROJ_INCLUDE, ["name"])
|
|
79
|
+
assert _encode_projection({"secret": 0}) == (proto.PROJ_EXCLUDE, ["secret"])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_projection_rejects_mixed():
|
|
83
|
+
with pytest.raises(Exception):
|
|
84
|
+
_encode_projection({"a": 1, "b": 0})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""ZydecoDB Python driver.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
from zydecodb import Client
|
|
6
|
+
|
|
7
|
+
with Client("127.0.0.1", 9470, api_key="...") as db:
|
|
8
|
+
users = db.collection("users")
|
|
9
|
+
users.create_index(["email"], unique=True)
|
|
10
|
+
uid = users.insert_one({"email": "a@b.com", "name": "Ada"})
|
|
11
|
+
for u in users.find({"name": "Ada"}):
|
|
12
|
+
print(u)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .client import Client, generate_id
|
|
16
|
+
from .collection import Collection
|
|
17
|
+
from .errors import (
|
|
18
|
+
AuthError,
|
|
19
|
+
ConfigError,
|
|
20
|
+
ConflictError,
|
|
21
|
+
ConnectionError,
|
|
22
|
+
InvalidRequestError,
|
|
23
|
+
ServerBusyError,
|
|
24
|
+
ServerError,
|
|
25
|
+
ZydecoError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__version__ = "0.9.0b1"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Client",
|
|
32
|
+
"Collection",
|
|
33
|
+
"generate_id",
|
|
34
|
+
"ZydecoError",
|
|
35
|
+
"ConfigError",
|
|
36
|
+
"ConnectionError",
|
|
37
|
+
"ServerError",
|
|
38
|
+
"AuthError",
|
|
39
|
+
"ConflictError",
|
|
40
|
+
"InvalidRequestError",
|
|
41
|
+
"ServerBusyError",
|
|
42
|
+
"__version__",
|
|
43
|
+
]
|