zydecodb 0.9.0b1__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.

Potentially problematic release.


This version of zydecodb might be problematic. Click here for more details.

@@ -0,0 +1,73 @@
1
+ # Node.js
2
+ node_modules/
3
+ dist/
4
+ experiments/**/*.js
5
+
6
+ # Compiled binaries
7
+ experiments/write_heavy/write_heavy
8
+
9
+ # Rust
10
+ /target
11
+ **/*.rs.bk
12
+
13
+ # Runtime data (dev)
14
+ *.sock
15
+ /.zydecodb-data/
16
+ /.zydecodb-run/
17
+
18
+ # Soak run artifacts (baselines in docs/soak-baselines/ are committed)
19
+ /soak-runs/**/data/
20
+ /soak-runs/**/wal/
21
+ /soak-runs/**/*.log
22
+ /soak-runs/**/*.jsonl
23
+
24
+ # Python
25
+ __pycache__/
26
+ *.py[cod]
27
+ .pytest_cache/
28
+ *.egg-info/
29
+ .venv/
30
+
31
+ # Fuzzing
32
+ /fuzz/corpus/
33
+ /fuzz/artifacts/
34
+ /fuzz/target/
35
+
36
+ # Coverage
37
+ /coverage/
38
+ *.profraw
39
+ lcov.info
40
+
41
+ # Secrets / credentials (never commit)
42
+ .env
43
+ .env.*
44
+ !.env.example
45
+ keys.toml
46
+ **/keys.toml
47
+ !config/zydecodb.keys.example.toml
48
+ *.pem
49
+ *.key
50
+ *.p12
51
+ *.pfx
52
+ tls.crt
53
+ tls.key
54
+
55
+ # IDE
56
+ /.idea/
57
+ /.vscode/
58
+ *.swp
59
+ .DS_Store
60
+
61
+ # Empty stub left over from an earlier packaging experiment
62
+ /.github/workflows/publish-python.yml
63
+
64
+ # Website / Marketing (deployed from this tree via scripts/deploy-website.sh;
65
+ # keep the marketing site and ops scripts local unless explicitly force-added)
66
+ /website/
67
+ /scripts/*
68
+ !/scripts/install.sh
69
+ !/scripts/deploy-website.sh
70
+
71
+ # Internal / Archived Docs
72
+ /docs/archive/
73
+ /docs/soak-baselines/
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: zydecodb
3
+ Version: 0.9.0b1
4
+ Summary: Official Python driver for ZydecoDB — a MongoDB-style document store without the fluff.
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) — a MongoDB-style
22
+ document store without the fluff. Pure standard library, 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
+ - **MongoDB-style `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) — a MongoDB-style
4
+ document store without the fluff. Pure standard library, 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
+ - **MongoDB-style `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.0b1"
8
+ description = "Official Python driver for ZydecoDB — a MongoDB-style document store without the fluff."
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
+ ]