pymydb-client 0.1.0__py3-none-any.whl
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.
- pymydb_client/__init__.py +25 -0
- pymydb_client/_compat.py +24 -0
- pymydb_client/connection.py +132 -0
- pymydb_client/crypto.py +23 -0
- pymydb_client/protocol.py +67 -0
- pymydb_client-0.1.0.dist-info/METADATA +64 -0
- pymydb_client-0.1.0.dist-info/RECORD +11 -0
- pymydb_client-0.1.0.dist-info/WHEEL +5 -0
- pymydb_client-0.1.0.dist-info/licenses/LICENSE +20 -0
- pymydb_client-0.1.0.dist-info/top_level.txt +1 -0
- pymydb_client-0.1.0.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""pymydb_client — pure-Python client for the MyDB database engine's mydbd
|
|
2
|
+
daemon. Speaks the same wire protocol as the C `mydb` CLI; no third-party
|
|
3
|
+
dependencies. Compatible with Python 2.7 and Python 3.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pymydb_client.connection import MyDBConnection, MyDBError, DEFAULT_PORT
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = ["connect", "MyDBConnection", "MyDBError", "DEFAULT_PORT"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def connect(host=None, port=DEFAULT_PORT, unix_path=None,
|
|
14
|
+
user=None, password=None):
|
|
15
|
+
"""Connect and authenticate to mydbd.
|
|
16
|
+
|
|
17
|
+
Either `host` (+ optional `port`, TCP) or `unix_path` (Unix socket) must
|
|
18
|
+
be given, but not both. `password` is prompted for interactively
|
|
19
|
+
(getpass) if omitted.
|
|
20
|
+
|
|
21
|
+
Returns an open, authenticated MyDBConnection. Raises MyDBError on
|
|
22
|
+
connect/auth failure.
|
|
23
|
+
"""
|
|
24
|
+
return MyDBConnection(host=host, port=port, unix_path=unix_path,
|
|
25
|
+
user=user, password=password)
|
pymydb_client/_compat.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Python 2.7 / 3.x compatibility shims. No external dependency (no `six`)."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
PY2 = sys.version_info[0] == 2
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_bytes(s):
|
|
9
|
+
"""Return `s` as bytes, encoding str->utf-8 on py3. On py2, str is
|
|
10
|
+
already bytes, so only unicode gets encoded."""
|
|
11
|
+
if isinstance(s, bytes):
|
|
12
|
+
return s
|
|
13
|
+
return s.encode("utf-8")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def to_text(b):
|
|
17
|
+
"""Return `b` as a text string, decoding utf-8 on both py2 and py3."""
|
|
18
|
+
if PY2:
|
|
19
|
+
if isinstance(b, unicode): # noqa: F821 (py2-only builtin)
|
|
20
|
+
return b
|
|
21
|
+
return b.decode("utf-8", "replace")
|
|
22
|
+
if isinstance(b, str):
|
|
23
|
+
return b
|
|
24
|
+
return b.decode("utf-8", "replace")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""MyDBConnection — Python counterpart of client/src/client_conn.c.
|
|
2
|
+
|
|
3
|
+
Speaks the same wire protocol (server/include/protocol.h) and the same
|
|
4
|
+
challenge-response auth (crypto/include/crypto.h) as the C `mydb` client, so
|
|
5
|
+
it can talk to mydbd over either the Unix socket or TCP.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import getpass
|
|
9
|
+
import socket as _socket
|
|
10
|
+
|
|
11
|
+
from pymydb_client import crypto
|
|
12
|
+
from pymydb_client import protocol
|
|
13
|
+
from pymydb_client._compat import to_bytes, to_text
|
|
14
|
+
|
|
15
|
+
DEFAULT_PORT = 4442
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MyDBError(Exception):
|
|
19
|
+
"""Raised for connect / auth / protocol failures."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MyDBConnection(object):
|
|
24
|
+
def __init__(self, host=None, port=DEFAULT_PORT, unix_path=None,
|
|
25
|
+
user=None, password=None):
|
|
26
|
+
if not user:
|
|
27
|
+
raise MyDBError("user is required")
|
|
28
|
+
if host and unix_path:
|
|
29
|
+
raise MyDBError("host and unix_path are mutually exclusive")
|
|
30
|
+
if not host and not unix_path:
|
|
31
|
+
raise MyDBError("one of host or unix_path is required")
|
|
32
|
+
|
|
33
|
+
self._sock = None
|
|
34
|
+
self._send_seq = 0
|
|
35
|
+
self._recv_seq = 0
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
if unix_path:
|
|
39
|
+
sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM)
|
|
40
|
+
sock.connect(unix_path)
|
|
41
|
+
else:
|
|
42
|
+
sock = _socket.create_connection((host, port))
|
|
43
|
+
except _socket.error as e:
|
|
44
|
+
raise MyDBError("cannot connect to MyDB server: {0}".format(e))
|
|
45
|
+
|
|
46
|
+
self._sock = sock
|
|
47
|
+
self._handshake(user, password)
|
|
48
|
+
|
|
49
|
+
def _send(self, ptype, payload):
|
|
50
|
+
try:
|
|
51
|
+
self._send_seq = protocol.send_packet(
|
|
52
|
+
self._sock, ptype, payload, self._send_seq)
|
|
53
|
+
except (protocol.ProtocolError, _socket.error) as e:
|
|
54
|
+
self.close()
|
|
55
|
+
raise MyDBError(str(e))
|
|
56
|
+
|
|
57
|
+
def _recv(self):
|
|
58
|
+
try:
|
|
59
|
+
ptype, payload, self._recv_seq = protocol.recv_packet(
|
|
60
|
+
self._sock, self._recv_seq)
|
|
61
|
+
return ptype, payload
|
|
62
|
+
except (protocol.ProtocolError, _socket.error) as e:
|
|
63
|
+
self.close()
|
|
64
|
+
raise MyDBError(str(e))
|
|
65
|
+
|
|
66
|
+
def _handshake(self, user, password):
|
|
67
|
+
user_bytes = to_bytes(user)
|
|
68
|
+
|
|
69
|
+
# 1. HANDSHAKE (server version) — read and ignore.
|
|
70
|
+
ptype, _payload = self._recv()
|
|
71
|
+
if ptype != protocol.PKT_HANDSHAKE:
|
|
72
|
+
self.close()
|
|
73
|
+
raise MyDBError("unexpected packet during handshake")
|
|
74
|
+
|
|
75
|
+
# 2. AUTH_INIT (username).
|
|
76
|
+
self._send(protocol.PKT_AUTH_INIT, user_bytes)
|
|
77
|
+
|
|
78
|
+
# 3. AUTH_CHALLENGE (salt || nonce).
|
|
79
|
+
ptype, payload = self._recv()
|
|
80
|
+
if (ptype != protocol.PKT_AUTH_CHALLENGE or
|
|
81
|
+
len(payload) != crypto.SALT_LEN + crypto.NONCE_LEN):
|
|
82
|
+
self.close()
|
|
83
|
+
raise MyDBError("authentication failed")
|
|
84
|
+
salt = payload[:crypto.SALT_LEN]
|
|
85
|
+
nonce = payload[crypto.SALT_LEN:]
|
|
86
|
+
|
|
87
|
+
# 4. response = SHA-256(nonce || SHA-256(salt || password)).
|
|
88
|
+
if password is None:
|
|
89
|
+
password = getpass.getpass("Password: ")
|
|
90
|
+
h1 = crypto.hash_password(password, salt)
|
|
91
|
+
response = crypto.compute_response(nonce, h1)
|
|
92
|
+
|
|
93
|
+
# 5. AUTH_RESPONSE = username '\0' response.
|
|
94
|
+
self._send(protocol.PKT_AUTH_RESPONSE, user_bytes + b"\x00" + response)
|
|
95
|
+
|
|
96
|
+
# 6. AUTH_OK / AUTH_ERR.
|
|
97
|
+
ptype, _payload = self._recv()
|
|
98
|
+
if ptype != protocol.PKT_AUTH_OK:
|
|
99
|
+
self.close()
|
|
100
|
+
raise MyDBError("authentication failed")
|
|
101
|
+
|
|
102
|
+
def query(self, sql):
|
|
103
|
+
"""Send one SQL statement, return the server's formatted result
|
|
104
|
+
text (same opaque string the C REPL prints — there is no
|
|
105
|
+
structured row/column protocol to parse)."""
|
|
106
|
+
if self._sock is None:
|
|
107
|
+
raise MyDBError("connection is closed")
|
|
108
|
+
self._send(protocol.PKT_QUERY, sql)
|
|
109
|
+
ptype, payload = self._recv()
|
|
110
|
+
if ptype != protocol.PKT_RESPONSE:
|
|
111
|
+
raise MyDBError("unexpected response packet")
|
|
112
|
+
return to_text(payload)
|
|
113
|
+
|
|
114
|
+
def close(self):
|
|
115
|
+
if self._sock is None:
|
|
116
|
+
return
|
|
117
|
+
try:
|
|
118
|
+
self._send_seq = protocol.send_packet(
|
|
119
|
+
self._sock, protocol.PKT_QUIT, b"", self._send_seq)
|
|
120
|
+
except Exception:
|
|
121
|
+
pass # best effort, matches client_conn_close
|
|
122
|
+
try:
|
|
123
|
+
self._sock.close()
|
|
124
|
+
finally:
|
|
125
|
+
self._sock = None
|
|
126
|
+
|
|
127
|
+
def __enter__(self):
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
131
|
+
self.close()
|
|
132
|
+
return False
|
pymydb_client/crypto.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Challenge-response auth hashing — mirrors crypto/include/crypto.h.
|
|
2
|
+
|
|
3
|
+
response = SHA-256(nonce || SHA-256(salt || password))
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
|
|
8
|
+
from pymydb_client._compat import to_bytes
|
|
9
|
+
|
|
10
|
+
SALT_LEN = 16
|
|
11
|
+
NONCE_LEN = 32
|
|
12
|
+
SHA256_DIGEST_LEN = 32
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def hash_password(password, salt):
|
|
16
|
+
"""SHA-256(salt || password). `password` is treated as raw text (no NUL
|
|
17
|
+
terminator, matching crypto_hash_password)."""
|
|
18
|
+
return hashlib.sha256(salt + to_bytes(password)).digest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def compute_response(nonce, h1):
|
|
22
|
+
"""SHA-256(nonce || h1)."""
|
|
23
|
+
return hashlib.sha256(nonce + h1).digest()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Wire protocol framing — mirrors server/include/protocol.h exactly.
|
|
2
|
+
|
|
3
|
+
Every packet is a 9-byte header (length, type, seq_no; length and seq_no in
|
|
4
|
+
network byte order) followed by `length` payload bytes. Each direction
|
|
5
|
+
(send/recv) keeps its own sequence counter starting at 0.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import struct
|
|
9
|
+
|
|
10
|
+
from pymydb_client._compat import to_bytes
|
|
11
|
+
|
|
12
|
+
HEADER_SIZE = 9
|
|
13
|
+
MAX_PAYLOAD = 65536
|
|
14
|
+
|
|
15
|
+
PKT_HANDSHAKE = 1
|
|
16
|
+
PKT_AUTH_INIT = 2
|
|
17
|
+
PKT_AUTH_CHALLENGE = 3
|
|
18
|
+
PKT_AUTH_RESPONSE = 4
|
|
19
|
+
PKT_AUTH_OK = 5
|
|
20
|
+
PKT_AUTH_ERR = 6
|
|
21
|
+
PKT_QUERY = 7
|
|
22
|
+
PKT_RESPONSE = 8
|
|
23
|
+
PKT_QUIT = 9
|
|
24
|
+
|
|
25
|
+
_HEADER_FMT = "!IBI" # network-order: uint32 length, uint8 type, uint32 seq_no
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProtocolError(Exception):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _recv_exact(sock, n):
|
|
33
|
+
chunks = []
|
|
34
|
+
remaining = n
|
|
35
|
+
while remaining > 0:
|
|
36
|
+
chunk = sock.recv(remaining)
|
|
37
|
+
if not chunk:
|
|
38
|
+
raise ProtocolError("connection closed")
|
|
39
|
+
chunks.append(chunk)
|
|
40
|
+
remaining -= len(chunk)
|
|
41
|
+
return b"".join(chunks)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def send_packet(sock, ptype, payload, seq):
|
|
45
|
+
"""Send one packet. `seq` is the current send counter; returns the next
|
|
46
|
+
value (mirrors proto_send's seq post-increment)."""
|
|
47
|
+
payload = to_bytes(payload) if payload else b""
|
|
48
|
+
if len(payload) > MAX_PAYLOAD:
|
|
49
|
+
raise ProtocolError("payload too large")
|
|
50
|
+
header = struct.pack(_HEADER_FMT, len(payload), ptype, seq)
|
|
51
|
+
sock.sendall(header + payload)
|
|
52
|
+
return seq + 1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def recv_packet(sock, expected_seq):
|
|
56
|
+
"""Receive one packet, validating the sequence number. Returns
|
|
57
|
+
(ptype, payload, next_expected_seq)."""
|
|
58
|
+
header = _recv_exact(sock, HEADER_SIZE)
|
|
59
|
+
length, ptype, seq_no = struct.unpack(_HEADER_FMT, header)
|
|
60
|
+
|
|
61
|
+
if seq_no != expected_seq:
|
|
62
|
+
raise ProtocolError("sequence mismatch (connection dropped)")
|
|
63
|
+
if length > MAX_PAYLOAD:
|
|
64
|
+
raise ProtocolError("oversized packet")
|
|
65
|
+
|
|
66
|
+
payload = _recv_exact(sock, length) if length > 0 else b""
|
|
67
|
+
return ptype, payload, expected_seq + 1
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pymydb-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-Python client for the MyDB database engine (mydbd)
|
|
5
|
+
Home-page: https://github.com/Hasnat056/pymydb-client
|
|
6
|
+
Author: Hasnat Akram
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 2.7
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Database
|
|
13
|
+
Requires-Python: >=2.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# pymydb-client
|
|
19
|
+
|
|
20
|
+
Pure-Python client for [MyDB](https://github.com/Hasnat056/MyDB-Database-Engine),
|
|
21
|
+
a relational database engine with a custom binary wire protocol served by its
|
|
22
|
+
`mydbd` daemon. No third-party dependencies (just `socket`, `hashlib`,
|
|
23
|
+
`struct`, `getpass` from the standard library); compatible with Python 2.7
|
|
24
|
+
through 3.x.
|
|
25
|
+
|
|
26
|
+
It reimplements the same challenge-response auth and framing the C `mydb` CLI
|
|
27
|
+
uses, so it can talk to `mydbd` over either a Unix domain socket or TCP.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pymydb-client
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from pymydb_client import connect
|
|
39
|
+
|
|
40
|
+
# Over TCP (mydbd's default TCP listener, 0.0.0.0:4442)
|
|
41
|
+
conn = connect(host="127.0.0.1", port=4442, user="root", password="yourpass")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
print(conn.query("SHOW DATABASES;"))
|
|
45
|
+
conn.close()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Context manager form:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
with connect(host="127.0.0.1", port=4442, user="root", password="yourpass") as conn:
|
|
52
|
+
print(conn.query("SELECT id, name FROM users;"))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`query(sql)` returns the same formatted text a SQL client would print (a
|
|
56
|
+
table for `SELECT`, or an OK/error message) — the wire protocol carries plain
|
|
57
|
+
text, not structured rows.
|
|
58
|
+
|
|
59
|
+
Omit `password` to be prompted interactively via `getpass`. Connect/auth/query
|
|
60
|
+
failures raise `pymydb_client.MyDBError`.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pymydb_client/__init__.py,sha256=44GZaFZSEHDaTXvrt9hsYUAhv2ZdpuGgkFKLv8Aolr0,926
|
|
2
|
+
pymydb_client/_compat.py,sha256=jkYyss-Db2ZmJx12_EEoEubgL7AEt4DQqAWDl3BK_bY,662
|
|
3
|
+
pymydb_client/connection.py,sha256=Zl40Cc6UUQcQgB0ztYHhgDepP6cM9GhtOoewUPmW7RA,4458
|
|
4
|
+
pymydb_client/crypto.py,sha256=aNNHW53QCLaCxG_so8HMQBMTQtt5XWi7fNZwlNdcj5g,585
|
|
5
|
+
pymydb_client/protocol.py,sha256=sPIL5qUJj3AoBBjNKHTKLQU2PGmNuUSVWUuNnxZE4Ho,1925
|
|
6
|
+
pymydb_client-0.1.0.dist-info/licenses/LICENSE,sha256=m65Xv-_hUHTZnY-WN0bEGPqohfi96c6kR7PPds8bz4s,1068
|
|
7
|
+
pymydb_client-0.1.0.dist-info/METADATA,sha256=hm5ZOA__Lxq-lcHoUSpHO_gs6fhLDlftxA2m93pOnKk,1853
|
|
8
|
+
pymydb_client-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
9
|
+
pymydb_client-0.1.0.dist-info/top_level.txt,sha256=Bg97aM4UKHOC6sb6uSeV8Uw-K183Hgv9N1GxGvnkYU8,14
|
|
10
|
+
pymydb_client-0.1.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
11
|
+
pymydb_client-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hasnat Akram
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pymydb_client
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|