nusadb 0.1.0__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.
nusadb-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: nusadb
3
+ Version: 0.1.0
4
+ Summary: Pure-Python PEP-249 (DB-API 2.0) driver for NusaDB (Nusa Wire Protocol)
5
+ Author: NusaDB Authors
6
+ License: Apache-2.0
7
+ Project-URL: Repository, https://github.com/nusadb/python
8
+ Project-URL: Issues, https://github.com/nusadb/python/issues
9
+ Keywords: nusadb,database,sql,dbapi,pep249
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Database
12
+ Classifier: Topic :: Database :: Front-Ends
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ Provides-Extra: sqlalchemy
17
+ Requires-Dist: SQLAlchemy>=1.4; extra == "sqlalchemy"
18
+
19
+ # nusadb — Python driver for NusaDB
20
+
21
+ A pure-Python, dependency-free [PEP-249 / DB-API 2.0](https://peps.python.org/pep-0249/)
22
+ driver that speaks the [Nusa Wire Protocol](../../docs/wire-protocol.md)
23
+ (`PROTOCOL_VERSION 1.1`) directly over a socket. No C extension; standard library only.
24
+ `cursor.description[i][1]` (`type_code`) carries each column's NusaDB type name (protocol 1.1).
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install ./drivers/python # from the repo, or
30
+ pip install nusadb # once published
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+ import nusadb
37
+
38
+ conn = nusadb.connect(host="127.0.0.1", port=5678, user="nusa-root", database="nusadb")
39
+ cur = conn.cursor()
40
+
41
+ cur.execute("CREATE TABLE t (id INT NOT NULL, name TEXT)")
42
+ cur.execute("INSERT INTO t VALUES ($1, $2)", [1, "alice"])
43
+
44
+ cur.execute("SELECT id, name FROM t WHERE id = $1", [1])
45
+ for row in cur:
46
+ print(row) # (1, 'alice') — INT decodes to int, TEXT to str
47
+
48
+ conn.close()
49
+ ```
50
+
51
+ ### Value types
52
+
53
+ Each column's wire type tag (protocol 1.1) drives decoding, so values come back as the natural
54
+ Python type — full typed DB-API fidelity rather than everything-as-string:
55
+
56
+ | NusaDB type | Python type |
57
+ | --- | --- |
58
+ | `BOOL` | `bool` |
59
+ | `INT` | `int` |
60
+ | `FLOAT` | `float` |
61
+ | `NUMERIC` | `decimal.Decimal` |
62
+ | `DATE` / `TIME` / `TIMESTAMP` (and `…TZ`) | `datetime.date` / `time` / `datetime` |
63
+ | `UUID` | `uuid.UUID` |
64
+ | `JSON` | parsed `dict` / `list` / scalar |
65
+ | `ARRAY` | `list` (elements stay `str` — the wire array tag carries no element type) |
66
+ | `BYTEA` | `bytes` |
67
+ | `TEXT` / `VARCHAR` / everything else | `str` |
68
+
69
+ A value that does not parse as its declared tag falls back to the raw text, so an unexpected wire
70
+ form never raises mid-fetch. `cursor.description[i][1]` (`type_code`) still carries the type name.
71
+
72
+ ### Bulk insert (`executemany`)
73
+
74
+ `cursor.executemany(sql, seq_of_parameters)` runs one statement once per parameter set (DB-API 2.0);
75
+ `cursor.rowcount` is the total affected. The wire protocol has no batch pipeline, so this is one
76
+ round-trip per set.
77
+
78
+ ```python
79
+ cur.executemany("INSERT INTO t VALUES ($1, $2)", [[1, "a"], [2, "b"], [3, "c"]])
80
+ ```
81
+
82
+ ### Bulk load / export (`COPY`)
83
+
84
+ For high-throughput load/export, `conn.copy_in` / `conn.copy_out` drive the `COPY` sub-protocol —
85
+ one round-trip for the whole dataset. Move bytes in the server's text format (tab-delimited fields,
86
+ `\N` for SQL `NULL`, one row per line); you write the `COPY` statement with any `WITH (...)` options.
87
+
88
+ ```python
89
+ import io
90
+
91
+ # Bulk load from any binary file-like (read(size) -> bytes).
92
+ loaded = conn.copy_in("COPY t (id, name) FROM STDIN", io.BytesIO(b"1\talice\n2\t\\N\n"))
93
+
94
+ # Bulk export into any binary file-like (write(bytes)).
95
+ sink = io.BytesIO()
96
+ exported = conn.copy_out("COPY t TO STDOUT", sink)
97
+ ```
98
+
99
+ A `COPY` the server refuses (bad SQL, an RLS-protected table) raises; the connection stays usable.
100
+
101
+ ### Parameters
102
+
103
+ Placeholders are positional **`$1`, `$2`, …** (the server's native
104
+ marker). `paramstyle` is reported as `"numeric"`. Pass the bound values as a
105
+ sequence to `execute` / `executemany`; `None` is SQL `NULL`. Values are sent in the
106
+ wire text format — bind with an explicit `CAST(... AS type)` when a numeric-looking
107
+ string must land in a non-numeric column.
108
+
109
+ ### TLS
110
+
111
+ Pass an `ssl.SSLContext` to `connect(ssl=...)`. The server uses implicit TLS 1.3, so
112
+ the TLS session is established before any protocol frame.
113
+
114
+ ```python
115
+ import ssl, nusadb
116
+ ctx = ssl.create_default_context(cafile="ca.pem")
117
+ conn = nusadb.connect(host="db.example", port=5678, user="u", database="nusadb",
118
+ password="…", ssl=ctx)
119
+ ```
120
+
121
+ ### Authentication
122
+
123
+ If the server runs with `--auth-user USER:PASSWORD`, pass `password=`; the driver
124
+ performs the SCRAM-SHA-256 handshake and verifies the server's signature (mutual auth).
125
+
126
+ ### Connection pool
127
+
128
+ ```python
129
+ from nusadb import Pool
130
+
131
+ pool = Pool(max_size=10, host="127.0.0.1", port=5678, user="nusa-root", database="nusadb")
132
+ with pool.connection() as conn:
133
+ conn.cursor().execute("SELECT 1")
134
+ ```
135
+
136
+ ## Transactions
137
+
138
+ By default the connection is in **autocommit** mode (each statement is its own
139
+ transaction). Pass `autocommit=False` for the standard transactional model: the
140
+ connection opens a transaction lazily before the first statement, and
141
+ `commit()`/`rollback()` send `COMMIT`/`ROLLBACK`.
142
+
143
+ Inside a transaction, `savepoint(name)` marks a point you can later undo to with
144
+ `rollback_to(name)` (the transaction stays open) or forget with `release_savepoint(name)`:
145
+
146
+ ```python
147
+ cur.execute("INSERT INTO t VALUES (1)")
148
+ conn.savepoint("sp1")
149
+ cur.execute("INSERT INTO t VALUES (2)")
150
+ conn.rollback_to("sp1") # undoes (2), keeps (1); the transaction continues
151
+ conn.commit()
152
+ ```
153
+
154
+ ```python
155
+ conn = nusadb.connect(port=5678, user="nusa-root", database="nusadb", autocommit=False)
156
+ cur = conn.cursor()
157
+ cur.execute("INSERT INTO t VALUES (1)")
158
+ conn.rollback() # discards the insert
159
+ cur.execute("INSERT INTO t VALUES (2)")
160
+ conn.commit() # persists it
161
+ ```
162
+
163
+ ## Notifications (LISTEN/NOTIFY)
164
+
165
+ `listen(channel)` subscribes the connection; a `notify(channel, payload)` from any connection on the
166
+ same database is then delivered asynchronously. Collect delivered notifications with `poll(timeout)`
167
+ (seconds; `0` polls without blocking, `None` blocks) or drain the ones buffered during other queries
168
+ with `notifications()`:
169
+
170
+ ```python
171
+ conn.listen("orders")
172
+ # ... elsewhere: other.notify("orders", "42")
173
+ note = conn.poll(5.0) # -> Notification(pid, channel, payload), or None on timeout
174
+ print(note.channel, note.payload)
175
+ conn.unlisten("orders")
176
+ ```
177
+
178
+ ## SQLAlchemy
179
+
180
+ A SQLAlchemy dialect ships in `nusadb.sqlalchemy`. Once this package is installed its
181
+ `sqlalchemy.dialects` entry point registers it, so `create_engine("nusadb://…")` works
182
+ directly:
183
+
184
+ ```python
185
+ from sqlalchemy import create_engine
186
+ engine = create_engine("nusadb://nusa-root@127.0.0.1:5678/nusadb")
187
+ ```
188
+
189
+ Without installing, register it explicitly:
190
+
191
+ ```python
192
+ from sqlalchemy.dialects import registry
193
+ registry.register("nusadb", "nusadb.sqlalchemy", "NusaDialect")
194
+ ```
195
+
196
+ It supports full ORM use — declarative models, `metadata.create_all`,
197
+ insert/query/update/delete, pagination (`.limit()`/`.offset()`), joins, aggregates, and real
198
+ transactions — over the transactional connection, rewriting SQLAlchemy's `:1` markers to the
199
+ server's `$1`. `LIMIT`/`OFFSET` are inlined as constants (the server rejects a bound row count).
200
+ Reflection (`inspect(engine).get_columns(...)`) reports each column's server-side `default` and
201
+ infers `autoincrement` from a `nextval(...)` default, so Alembic autogenerate sees them.
202
+ Install the extra with `pip install nusadb[sqlalchemy]`.
203
+
204
+ ## Tests
205
+
206
+ ```bash
207
+ cargo build -p nusadb-server # the tests boot this binary
208
+ python -m unittest discover -s drivers/python/tests
209
+ ```
210
+
211
+ The tests start a real `nusadb-server` on an ephemeral port and exercise simple and
212
+ parameterised queries, `executemany`, errors, the pool, cancellation, and SCRAM auth.
213
+
214
+ ## License
215
+
216
+ Apache-2.0.
nusadb-0.1.0/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # nusadb — Python driver for NusaDB
2
+
3
+ A pure-Python, dependency-free [PEP-249 / DB-API 2.0](https://peps.python.org/pep-0249/)
4
+ driver that speaks the [Nusa Wire Protocol](../../docs/wire-protocol.md)
5
+ (`PROTOCOL_VERSION 1.1`) directly over a socket. No C extension; standard library only.
6
+ `cursor.description[i][1]` (`type_code`) carries each column's NusaDB type name (protocol 1.1).
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install ./drivers/python # from the repo, or
12
+ pip install nusadb # once published
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ import nusadb
19
+
20
+ conn = nusadb.connect(host="127.0.0.1", port=5678, user="nusa-root", database="nusadb")
21
+ cur = conn.cursor()
22
+
23
+ cur.execute("CREATE TABLE t (id INT NOT NULL, name TEXT)")
24
+ cur.execute("INSERT INTO t VALUES ($1, $2)", [1, "alice"])
25
+
26
+ cur.execute("SELECT id, name FROM t WHERE id = $1", [1])
27
+ for row in cur:
28
+ print(row) # (1, 'alice') — INT decodes to int, TEXT to str
29
+
30
+ conn.close()
31
+ ```
32
+
33
+ ### Value types
34
+
35
+ Each column's wire type tag (protocol 1.1) drives decoding, so values come back as the natural
36
+ Python type — full typed DB-API fidelity rather than everything-as-string:
37
+
38
+ | NusaDB type | Python type |
39
+ | --- | --- |
40
+ | `BOOL` | `bool` |
41
+ | `INT` | `int` |
42
+ | `FLOAT` | `float` |
43
+ | `NUMERIC` | `decimal.Decimal` |
44
+ | `DATE` / `TIME` / `TIMESTAMP` (and `…TZ`) | `datetime.date` / `time` / `datetime` |
45
+ | `UUID` | `uuid.UUID` |
46
+ | `JSON` | parsed `dict` / `list` / scalar |
47
+ | `ARRAY` | `list` (elements stay `str` — the wire array tag carries no element type) |
48
+ | `BYTEA` | `bytes` |
49
+ | `TEXT` / `VARCHAR` / everything else | `str` |
50
+
51
+ A value that does not parse as its declared tag falls back to the raw text, so an unexpected wire
52
+ form never raises mid-fetch. `cursor.description[i][1]` (`type_code`) still carries the type name.
53
+
54
+ ### Bulk insert (`executemany`)
55
+
56
+ `cursor.executemany(sql, seq_of_parameters)` runs one statement once per parameter set (DB-API 2.0);
57
+ `cursor.rowcount` is the total affected. The wire protocol has no batch pipeline, so this is one
58
+ round-trip per set.
59
+
60
+ ```python
61
+ cur.executemany("INSERT INTO t VALUES ($1, $2)", [[1, "a"], [2, "b"], [3, "c"]])
62
+ ```
63
+
64
+ ### Bulk load / export (`COPY`)
65
+
66
+ For high-throughput load/export, `conn.copy_in` / `conn.copy_out` drive the `COPY` sub-protocol —
67
+ one round-trip for the whole dataset. Move bytes in the server's text format (tab-delimited fields,
68
+ `\N` for SQL `NULL`, one row per line); you write the `COPY` statement with any `WITH (...)` options.
69
+
70
+ ```python
71
+ import io
72
+
73
+ # Bulk load from any binary file-like (read(size) -> bytes).
74
+ loaded = conn.copy_in("COPY t (id, name) FROM STDIN", io.BytesIO(b"1\talice\n2\t\\N\n"))
75
+
76
+ # Bulk export into any binary file-like (write(bytes)).
77
+ sink = io.BytesIO()
78
+ exported = conn.copy_out("COPY t TO STDOUT", sink)
79
+ ```
80
+
81
+ A `COPY` the server refuses (bad SQL, an RLS-protected table) raises; the connection stays usable.
82
+
83
+ ### Parameters
84
+
85
+ Placeholders are positional **`$1`, `$2`, …** (the server's native
86
+ marker). `paramstyle` is reported as `"numeric"`. Pass the bound values as a
87
+ sequence to `execute` / `executemany`; `None` is SQL `NULL`. Values are sent in the
88
+ wire text format — bind with an explicit `CAST(... AS type)` when a numeric-looking
89
+ string must land in a non-numeric column.
90
+
91
+ ### TLS
92
+
93
+ Pass an `ssl.SSLContext` to `connect(ssl=...)`. The server uses implicit TLS 1.3, so
94
+ the TLS session is established before any protocol frame.
95
+
96
+ ```python
97
+ import ssl, nusadb
98
+ ctx = ssl.create_default_context(cafile="ca.pem")
99
+ conn = nusadb.connect(host="db.example", port=5678, user="u", database="nusadb",
100
+ password="…", ssl=ctx)
101
+ ```
102
+
103
+ ### Authentication
104
+
105
+ If the server runs with `--auth-user USER:PASSWORD`, pass `password=`; the driver
106
+ performs the SCRAM-SHA-256 handshake and verifies the server's signature (mutual auth).
107
+
108
+ ### Connection pool
109
+
110
+ ```python
111
+ from nusadb import Pool
112
+
113
+ pool = Pool(max_size=10, host="127.0.0.1", port=5678, user="nusa-root", database="nusadb")
114
+ with pool.connection() as conn:
115
+ conn.cursor().execute("SELECT 1")
116
+ ```
117
+
118
+ ## Transactions
119
+
120
+ By default the connection is in **autocommit** mode (each statement is its own
121
+ transaction). Pass `autocommit=False` for the standard transactional model: the
122
+ connection opens a transaction lazily before the first statement, and
123
+ `commit()`/`rollback()` send `COMMIT`/`ROLLBACK`.
124
+
125
+ Inside a transaction, `savepoint(name)` marks a point you can later undo to with
126
+ `rollback_to(name)` (the transaction stays open) or forget with `release_savepoint(name)`:
127
+
128
+ ```python
129
+ cur.execute("INSERT INTO t VALUES (1)")
130
+ conn.savepoint("sp1")
131
+ cur.execute("INSERT INTO t VALUES (2)")
132
+ conn.rollback_to("sp1") # undoes (2), keeps (1); the transaction continues
133
+ conn.commit()
134
+ ```
135
+
136
+ ```python
137
+ conn = nusadb.connect(port=5678, user="nusa-root", database="nusadb", autocommit=False)
138
+ cur = conn.cursor()
139
+ cur.execute("INSERT INTO t VALUES (1)")
140
+ conn.rollback() # discards the insert
141
+ cur.execute("INSERT INTO t VALUES (2)")
142
+ conn.commit() # persists it
143
+ ```
144
+
145
+ ## Notifications (LISTEN/NOTIFY)
146
+
147
+ `listen(channel)` subscribes the connection; a `notify(channel, payload)` from any connection on the
148
+ same database is then delivered asynchronously. Collect delivered notifications with `poll(timeout)`
149
+ (seconds; `0` polls without blocking, `None` blocks) or drain the ones buffered during other queries
150
+ with `notifications()`:
151
+
152
+ ```python
153
+ conn.listen("orders")
154
+ # ... elsewhere: other.notify("orders", "42")
155
+ note = conn.poll(5.0) # -> Notification(pid, channel, payload), or None on timeout
156
+ print(note.channel, note.payload)
157
+ conn.unlisten("orders")
158
+ ```
159
+
160
+ ## SQLAlchemy
161
+
162
+ A SQLAlchemy dialect ships in `nusadb.sqlalchemy`. Once this package is installed its
163
+ `sqlalchemy.dialects` entry point registers it, so `create_engine("nusadb://…")` works
164
+ directly:
165
+
166
+ ```python
167
+ from sqlalchemy import create_engine
168
+ engine = create_engine("nusadb://nusa-root@127.0.0.1:5678/nusadb")
169
+ ```
170
+
171
+ Without installing, register it explicitly:
172
+
173
+ ```python
174
+ from sqlalchemy.dialects import registry
175
+ registry.register("nusadb", "nusadb.sqlalchemy", "NusaDialect")
176
+ ```
177
+
178
+ It supports full ORM use — declarative models, `metadata.create_all`,
179
+ insert/query/update/delete, pagination (`.limit()`/`.offset()`), joins, aggregates, and real
180
+ transactions — over the transactional connection, rewriting SQLAlchemy's `:1` markers to the
181
+ server's `$1`. `LIMIT`/`OFFSET` are inlined as constants (the server rejects a bound row count).
182
+ Reflection (`inspect(engine).get_columns(...)`) reports each column's server-side `default` and
183
+ infers `autoincrement` from a `nextval(...)` default, so Alembic autogenerate sees them.
184
+ Install the extra with `pip install nusadb[sqlalchemy]`.
185
+
186
+ ## Tests
187
+
188
+ ```bash
189
+ cargo build -p nusadb-server # the tests boot this binary
190
+ python -m unittest discover -s drivers/python/tests
191
+ ```
192
+
193
+ The tests start a real `nusadb-server` on an ephemeral port and exercise simple and
194
+ parameterised queries, `executemany`, errors, the pool, cancellation, and SCRAM auth.
195
+
196
+ ## License
197
+
198
+ Apache-2.0.
@@ -0,0 +1,65 @@
1
+ """nusadb — the Python driver for NusaDB (PEP-249 / DB-API 2.0).
2
+
3
+ Speaks the Nusa Wire Protocol (``PROTOCOL_VERSION 1.1``, see
4
+ ``docs/wire-protocol.md``) directly over a socket — no C extension, stdlib only.
5
+
6
+ Example::
7
+
8
+ import nusadb
9
+
10
+ conn = nusadb.connect(host="127.0.0.1", port=5678, user="nusa-root", database="nusadb")
11
+ cur = conn.cursor()
12
+ cur.execute("CREATE TABLE t (id INT NOT NULL, name TEXT)")
13
+ cur.execute("INSERT INTO t VALUES ($1, $2)", [1, "alice"])
14
+ cur.execute("SELECT id, name FROM t WHERE id = $1", [1])
15
+ print(cur.fetchall())
16
+ conn.close()
17
+
18
+ Parameter markers are positional ``$1``, ``$2`` (the server's native form);
19
+ ``paramstyle`` is reported as ``"numeric"``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from .connection import Connection, Cursor, Notification, connect
25
+ from .exceptions import (
26
+ DatabaseError,
27
+ DataError,
28
+ Error,
29
+ IntegrityError,
30
+ InterfaceError,
31
+ InternalError,
32
+ NotSupportedError,
33
+ OperationalError,
34
+ ProgrammingError,
35
+ Warning,
36
+ )
37
+ from .pool import Pool
38
+
39
+ # PEP-249 module globals.
40
+ apilevel = "2.0"
41
+ threadsafety = 1 # threads may share the module but not connections
42
+ paramstyle = "numeric" # positional markers, written ``$1`` (server-native)
43
+
44
+ __version__ = "0.1.0"
45
+
46
+ __all__ = [
47
+ "connect",
48
+ "Connection",
49
+ "Cursor",
50
+ "Notification",
51
+ "Pool",
52
+ "apilevel",
53
+ "threadsafety",
54
+ "paramstyle",
55
+ "Warning",
56
+ "Error",
57
+ "InterfaceError",
58
+ "DatabaseError",
59
+ "DataError",
60
+ "OperationalError",
61
+ "IntegrityError",
62
+ "InternalError",
63
+ "ProgrammingError",
64
+ "NotSupportedError",
65
+ ]