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.
@@ -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)
@@ -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
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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
+