ownSQL 1.0.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.
ownsql-1.0.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Timon W
4
+ Copyright (c) 2014-2019, 2021-2026 Hajime Nakagami
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
ownsql-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: ownSQL
3
+ Version: 1.0.0
4
+ Summary: A MySQL database driver for MicroPython, supporting MySQL Server 8.x and 9.x.
5
+ Home-page: https://github.com/TimonW-Dev/ownSQL
6
+ Author: TimonW-Dev
7
+ Author-email: timon.github@outlook.com
8
+ Project-URL: Bug Tracker, https://github.com/TimonW-Dev/ownSQL/issues
9
+ Project-URL: Source Code, https://github.com/TimonW-Dev/ownSQL
10
+ Keywords: micropython mysql esp8266 esp32 database driver
11
+ Classifier: Programming Language :: Python :: Implementation :: MicroPython
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: Other OS
15
+ Classifier: Topic :: Software Development :: Embedded Systems
16
+ Classifier: Topic :: Database
17
+ Requires-Python: >=3.4
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: author
21
+ Dynamic: author-email
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: keywords
27
+ Dynamic: license-file
28
+ Dynamic: project-url
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # ownSQL
33
+
34
+ **A MySQL database driver for MicroPython, supporting MySQL Server 8.x and 9.x.**
35
+
36
+ ownSQL was derived from `micropg_lite` and adapted from PostgreSQL to MySQL with the assistance of AI tools. `micropg`, the original project on which `micropg_lite` is based, was developed by [Hajime Nakagami](https://github.com/nakagami). Many thanks for the great work — without `micropg`, `ownSQL` would not exist!
37
+
38
+ For support, questions, or suggestions, please [open an issue](https://github.com/TimonW-Dev/ownSQL/issues) or contact me by email using the address linked in [my profile](https://github.com/TimonW-Dev). Pull requests are welcome. This is a voluntary open-source project, so support is provided as time allows.
39
+
40
+
41
+ ## 📥 Installation
42
+
43
+ ### Choose a file
44
+
45
+ We recommend [`ownSQL.py`](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.py). If you want to save RAM, you can use the precompiled [`ownSQL.mpy`](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.mpy) file. For most projects, the `.py` file is sufficient. The `.mpy` file may not be compatible with every MicroPython firmware version and is not as easy to edit or debug.
46
+
47
+ ### Install on your microcontroller
48
+
49
+ 1. Download the [ownSQL.py](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.py) file from this [repository](https://github.com/TimonW-Dev/ownSQL) to your local computer.
50
+ 2. Copy it to the `/lib` folder on your microcontroller using Thonny IDE or another file transfer tool. Create the folder if it does not exist.
51
+ - **Thonny tip:** Open **View** in the top bar and enable **Files** to upload files and create folders on the microcontroller.
52
+ 3. Import the library in your MicroPython script. The import name is the same for both file formats:
53
+
54
+ ```python
55
+ import ownSQL
56
+ ```
57
+
58
+ The files on your microcontroller should be arranged like this:
59
+
60
+ ```text
61
+ /
62
+ |- example.py
63
+ |- lib/
64
+ |- ownSQL.py
65
+ ```
66
+
67
+ ## 🚀 Examples
68
+
69
+ Establish a network connection before running `ownSQL` code. The SELECT example below includes a Wi-Fi connection template; the other examples assume that the connection is already established.
70
+
71
+ The [`examples/` folder](https://github.com/TimonW-Dev/ownSQL/tree/main/examples) contains:
72
+
73
+ - [`docker-full-testing-container-setup`](https://github.com/TimonW-Dev/ownSQL/blob/main/examples/docker-full-testing-container-setup), to create an empty MySQL container.
74
+ - [`exampleDatabase.sql`](https://github.com/TimonW-Dev/ownSQL/blob/main/examples/exampleDatabase.sql), the database used by the examples.
75
+ - Complete test scripts with WLAN connection templates, plus examples for creating and dropping tables and databases.
76
+
77
+ ### 🔍 SELECT (including Wi-Fi setup)
78
+
79
+ Replace the Wi-Fi credentials and server details with your own:
80
+
81
+ ```python
82
+ import ownSQL
83
+ import network
84
+ import time
85
+
86
+ ssid = 'wifissid'
87
+ password = 'secret'
88
+
89
+ # Connect to the network
90
+ wlan = network.WLAN(network.STA_IF)
91
+ wlan.active(True)
92
+ wlan.connect(ssid, password)
93
+
94
+ while not wlan.isconnected():
95
+ print("Wi-Fi status:", wlan.status())
96
+ time.sleep(1)
97
+
98
+ print("Wi-Fi connected")
99
+
100
+ conn = ownSQL.connect(host='127.0.0.1', # Replace with your server's IP address
101
+ user='root',
102
+ password='123456',
103
+ database='exampledatabase')
104
+ cur = conn.cursor()
105
+
106
+ cur.execute('select * from customers')
107
+ selectresult = cur.fetchall()
108
+ conn.close()
109
+
110
+ # Print the raw data
111
+ print(selectresult)
112
+
113
+ # Print the data as rows
114
+ for row in selectresult:
115
+ for value in row:
116
+ print(value, end="\t")
117
+ print()
118
+ ```
119
+
120
+ ### ➕ INSERT
121
+
122
+ ```python
123
+ conn = ownSQL.connect(
124
+ host='127.0.0.1', # Replace with your server's IP address
125
+ user='root',
126
+ password='123456',
127
+ database='exampledatabase'
128
+ )
129
+ cur = conn.cursor()
130
+
131
+ cur.execute(
132
+ 'INSERT INTO customers (id, firstName, lastName, email) values (%s, %s, %s, %s)',
133
+ ['5', 'David', 'Wilson', 'david.wilson@example.com']
134
+ )
135
+ conn.commit()
136
+ conn.close()
137
+ ```
138
+
139
+ ### ✏️ UPDATE
140
+
141
+ ```python
142
+ conn = ownSQL.connect(
143
+ host='127.0.0.1', # Replace with your server's IP address
144
+ user='root',
145
+ password='123456',
146
+ database='exampledatabase'
147
+ )
148
+ cur = conn.cursor()
149
+
150
+ cur.execute("update customers set firstName='UpdatedFirstName' where id=2;")
151
+ conn.commit()
152
+ conn.close()
153
+ ```
154
+
155
+ ### 🗑️ DELETE
156
+
157
+ ```python
158
+ conn = ownSQL.connect(
159
+ host='127.0.0.1', # Replace with your server's IP address
160
+ user='root',
161
+ password='123456',
162
+ database='exampledatabase'
163
+ )
164
+ cur = conn.cursor()
165
+
166
+ cur.execute("delete from customers where id=1;")
167
+ conn.commit()
168
+ conn.close()
169
+ ```
ownsql-1.0.0/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # ownSQL
2
+
3
+ **A MySQL database driver for MicroPython, supporting MySQL Server 8.x and 9.x.**
4
+
5
+ ownSQL was derived from `micropg_lite` and adapted from PostgreSQL to MySQL with the assistance of AI tools. `micropg`, the original project on which `micropg_lite` is based, was developed by [Hajime Nakagami](https://github.com/nakagami). Many thanks for the great work — without `micropg`, `ownSQL` would not exist!
6
+
7
+ For support, questions, or suggestions, please [open an issue](https://github.com/TimonW-Dev/ownSQL/issues) or contact me by email using the address linked in [my profile](https://github.com/TimonW-Dev). Pull requests are welcome. This is a voluntary open-source project, so support is provided as time allows.
8
+
9
+
10
+ ## 📥 Installation
11
+
12
+ ### Choose a file
13
+
14
+ We recommend [`ownSQL.py`](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.py). If you want to save RAM, you can use the precompiled [`ownSQL.mpy`](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.mpy) file. For most projects, the `.py` file is sufficient. The `.mpy` file may not be compatible with every MicroPython firmware version and is not as easy to edit or debug.
15
+
16
+ ### Install on your microcontroller
17
+
18
+ 1. Download the [ownSQL.py](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.py) file from this [repository](https://github.com/TimonW-Dev/ownSQL) to your local computer.
19
+ 2. Copy it to the `/lib` folder on your microcontroller using Thonny IDE or another file transfer tool. Create the folder if it does not exist.
20
+ - **Thonny tip:** Open **View** in the top bar and enable **Files** to upload files and create folders on the microcontroller.
21
+ 3. Import the library in your MicroPython script. The import name is the same for both file formats:
22
+
23
+ ```python
24
+ import ownSQL
25
+ ```
26
+
27
+ The files on your microcontroller should be arranged like this:
28
+
29
+ ```text
30
+ /
31
+ |- example.py
32
+ |- lib/
33
+ |- ownSQL.py
34
+ ```
35
+
36
+ ## 🚀 Examples
37
+
38
+ Establish a network connection before running `ownSQL` code. The SELECT example below includes a Wi-Fi connection template; the other examples assume that the connection is already established.
39
+
40
+ The [`examples/` folder](https://github.com/TimonW-Dev/ownSQL/tree/main/examples) contains:
41
+
42
+ - [`docker-full-testing-container-setup`](https://github.com/TimonW-Dev/ownSQL/blob/main/examples/docker-full-testing-container-setup), to create an empty MySQL container.
43
+ - [`exampleDatabase.sql`](https://github.com/TimonW-Dev/ownSQL/blob/main/examples/exampleDatabase.sql), the database used by the examples.
44
+ - Complete test scripts with WLAN connection templates, plus examples for creating and dropping tables and databases.
45
+
46
+ ### 🔍 SELECT (including Wi-Fi setup)
47
+
48
+ Replace the Wi-Fi credentials and server details with your own:
49
+
50
+ ```python
51
+ import ownSQL
52
+ import network
53
+ import time
54
+
55
+ ssid = 'wifissid'
56
+ password = 'secret'
57
+
58
+ # Connect to the network
59
+ wlan = network.WLAN(network.STA_IF)
60
+ wlan.active(True)
61
+ wlan.connect(ssid, password)
62
+
63
+ while not wlan.isconnected():
64
+ print("Wi-Fi status:", wlan.status())
65
+ time.sleep(1)
66
+
67
+ print("Wi-Fi connected")
68
+
69
+ conn = ownSQL.connect(host='127.0.0.1', # Replace with your server's IP address
70
+ user='root',
71
+ password='123456',
72
+ database='exampledatabase')
73
+ cur = conn.cursor()
74
+
75
+ cur.execute('select * from customers')
76
+ selectresult = cur.fetchall()
77
+ conn.close()
78
+
79
+ # Print the raw data
80
+ print(selectresult)
81
+
82
+ # Print the data as rows
83
+ for row in selectresult:
84
+ for value in row:
85
+ print(value, end="\t")
86
+ print()
87
+ ```
88
+
89
+ ### ➕ INSERT
90
+
91
+ ```python
92
+ conn = ownSQL.connect(
93
+ host='127.0.0.1', # Replace with your server's IP address
94
+ user='root',
95
+ password='123456',
96
+ database='exampledatabase'
97
+ )
98
+ cur = conn.cursor()
99
+
100
+ cur.execute(
101
+ 'INSERT INTO customers (id, firstName, lastName, email) values (%s, %s, %s, %s)',
102
+ ['5', 'David', 'Wilson', 'david.wilson@example.com']
103
+ )
104
+ conn.commit()
105
+ conn.close()
106
+ ```
107
+
108
+ ### ✏️ UPDATE
109
+
110
+ ```python
111
+ conn = ownSQL.connect(
112
+ host='127.0.0.1', # Replace with your server's IP address
113
+ user='root',
114
+ password='123456',
115
+ database='exampledatabase'
116
+ )
117
+ cur = conn.cursor()
118
+
119
+ cur.execute("update customers set firstName='UpdatedFirstName' where id=2;")
120
+ conn.commit()
121
+ conn.close()
122
+ ```
123
+
124
+ ### 🗑️ DELETE
125
+
126
+ ```python
127
+ conn = ownSQL.connect(
128
+ host='127.0.0.1', # Replace with your server's IP address
129
+ user='root',
130
+ password='123456',
131
+ database='exampledatabase'
132
+ )
133
+ cur = conn.cursor()
134
+
135
+ cur.execute("delete from customers where id=1;")
136
+ conn.commit()
137
+ conn.close()
138
+ ```
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: ownSQL
3
+ Version: 1.0.0
4
+ Summary: A MySQL database driver for MicroPython, supporting MySQL Server 8.x and 9.x.
5
+ Home-page: https://github.com/TimonW-Dev/ownSQL
6
+ Author: TimonW-Dev
7
+ Author-email: timon.github@outlook.com
8
+ Project-URL: Bug Tracker, https://github.com/TimonW-Dev/ownSQL/issues
9
+ Project-URL: Source Code, https://github.com/TimonW-Dev/ownSQL
10
+ Keywords: micropython mysql esp8266 esp32 database driver
11
+ Classifier: Programming Language :: Python :: Implementation :: MicroPython
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: Other OS
15
+ Classifier: Topic :: Software Development :: Embedded Systems
16
+ Classifier: Topic :: Database
17
+ Requires-Python: >=3.4
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: author
21
+ Dynamic: author-email
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: keywords
27
+ Dynamic: license-file
28
+ Dynamic: project-url
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # ownSQL
33
+
34
+ **A MySQL database driver for MicroPython, supporting MySQL Server 8.x and 9.x.**
35
+
36
+ ownSQL was derived from `micropg_lite` and adapted from PostgreSQL to MySQL with the assistance of AI tools. `micropg`, the original project on which `micropg_lite` is based, was developed by [Hajime Nakagami](https://github.com/nakagami). Many thanks for the great work — without `micropg`, `ownSQL` would not exist!
37
+
38
+ For support, questions, or suggestions, please [open an issue](https://github.com/TimonW-Dev/ownSQL/issues) or contact me by email using the address linked in [my profile](https://github.com/TimonW-Dev). Pull requests are welcome. This is a voluntary open-source project, so support is provided as time allows.
39
+
40
+
41
+ ## 📥 Installation
42
+
43
+ ### Choose a file
44
+
45
+ We recommend [`ownSQL.py`](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.py). If you want to save RAM, you can use the precompiled [`ownSQL.mpy`](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.mpy) file. For most projects, the `.py` file is sufficient. The `.mpy` file may not be compatible with every MicroPython firmware version and is not as easy to edit or debug.
46
+
47
+ ### Install on your microcontroller
48
+
49
+ 1. Download the [ownSQL.py](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.py) file from this [repository](https://github.com/TimonW-Dev/ownSQL) to your local computer.
50
+ 2. Copy it to the `/lib` folder on your microcontroller using Thonny IDE or another file transfer tool. Create the folder if it does not exist.
51
+ - **Thonny tip:** Open **View** in the top bar and enable **Files** to upload files and create folders on the microcontroller.
52
+ 3. Import the library in your MicroPython script. The import name is the same for both file formats:
53
+
54
+ ```python
55
+ import ownSQL
56
+ ```
57
+
58
+ The files on your microcontroller should be arranged like this:
59
+
60
+ ```text
61
+ /
62
+ |- example.py
63
+ |- lib/
64
+ |- ownSQL.py
65
+ ```
66
+
67
+ ## 🚀 Examples
68
+
69
+ Establish a network connection before running `ownSQL` code. The SELECT example below includes a Wi-Fi connection template; the other examples assume that the connection is already established.
70
+
71
+ The [`examples/` folder](https://github.com/TimonW-Dev/ownSQL/tree/main/examples) contains:
72
+
73
+ - [`docker-full-testing-container-setup`](https://github.com/TimonW-Dev/ownSQL/blob/main/examples/docker-full-testing-container-setup), to create an empty MySQL container.
74
+ - [`exampleDatabase.sql`](https://github.com/TimonW-Dev/ownSQL/blob/main/examples/exampleDatabase.sql), the database used by the examples.
75
+ - Complete test scripts with WLAN connection templates, plus examples for creating and dropping tables and databases.
76
+
77
+ ### 🔍 SELECT (including Wi-Fi setup)
78
+
79
+ Replace the Wi-Fi credentials and server details with your own:
80
+
81
+ ```python
82
+ import ownSQL
83
+ import network
84
+ import time
85
+
86
+ ssid = 'wifissid'
87
+ password = 'secret'
88
+
89
+ # Connect to the network
90
+ wlan = network.WLAN(network.STA_IF)
91
+ wlan.active(True)
92
+ wlan.connect(ssid, password)
93
+
94
+ while not wlan.isconnected():
95
+ print("Wi-Fi status:", wlan.status())
96
+ time.sleep(1)
97
+
98
+ print("Wi-Fi connected")
99
+
100
+ conn = ownSQL.connect(host='127.0.0.1', # Replace with your server's IP address
101
+ user='root',
102
+ password='123456',
103
+ database='exampledatabase')
104
+ cur = conn.cursor()
105
+
106
+ cur.execute('select * from customers')
107
+ selectresult = cur.fetchall()
108
+ conn.close()
109
+
110
+ # Print the raw data
111
+ print(selectresult)
112
+
113
+ # Print the data as rows
114
+ for row in selectresult:
115
+ for value in row:
116
+ print(value, end="\t")
117
+ print()
118
+ ```
119
+
120
+ ### ➕ INSERT
121
+
122
+ ```python
123
+ conn = ownSQL.connect(
124
+ host='127.0.0.1', # Replace with your server's IP address
125
+ user='root',
126
+ password='123456',
127
+ database='exampledatabase'
128
+ )
129
+ cur = conn.cursor()
130
+
131
+ cur.execute(
132
+ 'INSERT INTO customers (id, firstName, lastName, email) values (%s, %s, %s, %s)',
133
+ ['5', 'David', 'Wilson', 'david.wilson@example.com']
134
+ )
135
+ conn.commit()
136
+ conn.close()
137
+ ```
138
+
139
+ ### ✏️ UPDATE
140
+
141
+ ```python
142
+ conn = ownSQL.connect(
143
+ host='127.0.0.1', # Replace with your server's IP address
144
+ user='root',
145
+ password='123456',
146
+ database='exampledatabase'
147
+ )
148
+ cur = conn.cursor()
149
+
150
+ cur.execute("update customers set firstName='UpdatedFirstName' where id=2;")
151
+ conn.commit()
152
+ conn.close()
153
+ ```
154
+
155
+ ### 🗑️ DELETE
156
+
157
+ ```python
158
+ conn = ownSQL.connect(
159
+ host='127.0.0.1', # Replace with your server's IP address
160
+ user='root',
161
+ password='123456',
162
+ database='exampledatabase'
163
+ )
164
+ cur = conn.cursor()
165
+
166
+ cur.execute("delete from customers where id=1;")
167
+ conn.commit()
168
+ conn.close()
169
+ ```
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ ownSQL.py
4
+ setup.py
5
+ ./ownSQL.py
6
+ ownSQL.egg-info/PKG-INFO
7
+ ownSQL.egg-info/SOURCES.txt
8
+ ownSQL.egg-info/dependency_links.txt
9
+ ownSQL.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ ownSQL
ownsql-1.0.0/ownSQL.py ADDED
@@ -0,0 +1,567 @@
1
+ #########################################################################################
2
+ # MIT License
3
+ #
4
+ # Copyright (c) 2026 TimonW-Dev
5
+ # Copyright (c) 2014-2019, 2021-2026 Hajime Nakagami
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in all
15
+ # copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ # SOFTWARE.
24
+ #########################################################################################
25
+ # ownSQL is a MySQL database driver for MicroPython, supporting MySQL Server 8.x and 9.x.
26
+ # It was derived from micropg_lite and adapted from PostgreSQL to MySQL with the
27
+ # assistance of AI tools. micropg, the original project on which micropg_lite is based,
28
+ # was developed by Hajime Nakagami.
29
+ #########################################################################################
30
+
31
+ import socket
32
+
33
+
34
+ CLIENT_LONG_PASSWORD = 0x00000001
35
+ CLIENT_LONG_FLAG = 0x00000004
36
+ CLIENT_CONNECT_WITH_DB = 0x00000008
37
+ CLIENT_PROTOCOL_41 = 0x00000200
38
+ CLIENT_SSL = 0x00000800
39
+ CLIENT_TRANSACTIONS = 0x00002000
40
+ CLIENT_SECURE_CONNECTION = 0x00008000
41
+ CLIENT_PLUGIN_AUTH = 0x00080000
42
+
43
+
44
+ class Error(Exception):
45
+ pass
46
+
47
+
48
+ class OperationalError(Error):
49
+ pass
50
+
51
+
52
+ class ProgrammingError(Error):
53
+ pass
54
+
55
+
56
+ def _lost():
57
+ raise OperationalError("MySQL: lost connection")
58
+
59
+
60
+ def _lenenc_int(data, pos=0):
61
+ if pos >= len(data):
62
+ raise OperationalError("MySQL: truncated length-encoded integer")
63
+ first = data[pos]
64
+ pos += 1
65
+ if first < 0xfb:
66
+ return first, pos
67
+ if first == 0xfc:
68
+ if len(data) < pos + 2:
69
+ raise OperationalError("MySQL: truncated length-encoded integer")
70
+ return int.from_bytes(data[pos:pos + 2], "little"), pos + 2
71
+ if first == 0xfd:
72
+ if len(data) < pos + 3:
73
+ raise OperationalError("MySQL: truncated length-encoded integer")
74
+ return int.from_bytes(data[pos:pos + 3], "little"), pos + 3
75
+ if first == 0xfe:
76
+ if len(data) < pos + 8:
77
+ raise OperationalError("MySQL: truncated length-encoded integer")
78
+ return int.from_bytes(data[pos:pos + 8], "little"), pos + 8
79
+ if first == 0xfb:
80
+ return None, pos
81
+ raise OperationalError("MySQL: invalid length-encoded integer")
82
+
83
+
84
+ def _lenenc_bytes(data, pos=0):
85
+ size, pos = _lenenc_int(data, pos)
86
+ if size is None:
87
+ return None, pos
88
+ if size > len(data) - pos:
89
+ raise OperationalError("MySQL: truncated length-encoded value")
90
+ return data[pos:pos + size], pos + size
91
+
92
+
93
+ def _sha256(data):
94
+ import hashlib
95
+ return hashlib.sha256(data).digest()
96
+
97
+
98
+ def _auth_response(plugin, password, salt, tls_active=False):
99
+ if plugin == "sha256_password":
100
+ if not tls_active:
101
+ raise OperationalError(
102
+ "MySQL: sha256_password authentication requires use_ssl=True"
103
+ )
104
+ return password.encode("utf-8") + b"\x00"
105
+ if not password:
106
+ return b""
107
+ raw_password = password.encode("utf-8")
108
+ if plugin == "caching_sha2_password":
109
+ stage1 = _sha256(raw_password)
110
+ stage2 = _sha256(stage1)
111
+ stage3 = _sha256(salt + stage2)
112
+ return bytes(a ^ b for a, b in zip(stage1, stage3))
113
+ if plugin == "mysql_native_password":
114
+ import hashlib
115
+ stage1 = hashlib.sha1(raw_password).digest()
116
+ stage2 = hashlib.sha1(stage1).digest()
117
+ stage3 = hashlib.sha1(salt + stage2).digest()
118
+ return bytes(a ^ b for a, b in zip(stage1, stage3))
119
+ raise OperationalError("MySQL: unsupported authentication plugin: " + plugin)
120
+
121
+
122
+ def _query_with_params(query, params):
123
+ if not isinstance(params, (tuple, list)):
124
+ raise ProgrammingError("MySQL parameters must be a tuple or list")
125
+
126
+ result = []
127
+ param_index = 0
128
+ pos = 0
129
+ while pos < len(query):
130
+ char = query[pos]
131
+ if char == "%" and pos + 1 < len(query):
132
+ marker = query[pos + 1]
133
+ if marker == "%":
134
+ result.append("%")
135
+ pos += 2
136
+ continue
137
+ if marker == "s":
138
+ if param_index >= len(params):
139
+ raise ProgrammingError("not enough parameters for query")
140
+ result.append(_sql_literal(params[param_index]))
141
+ param_index += 1
142
+ pos += 2
143
+ continue
144
+ result.append(char)
145
+ pos += 1
146
+
147
+ if param_index != len(params):
148
+ raise ProgrammingError("too many parameters for query")
149
+ return "".join(result)
150
+
151
+
152
+ def _sql_literal(value):
153
+ if value is None:
154
+ return "NULL"
155
+ if value is True:
156
+ return "1"
157
+ if value is False:
158
+ return "0"
159
+ if isinstance(value, (int, float)):
160
+ return str(value)
161
+ import binascii
162
+ if isinstance(value, str):
163
+ encoded = value.encode("utf-8")
164
+ hex_value = binascii.hexlify(encoded).decode("ascii")
165
+ return "CONVERT(X'" + hex_value + "' USING utf8mb4)"
166
+ if isinstance(value, (bytes, bytearray)):
167
+ hex_value = binascii.hexlify(bytes(value)).decode("ascii")
168
+ return "X'" + hex_value + "'"
169
+ raise ProgrammingError("unsupported MySQL parameter type: " + type(value).__name__)
170
+
171
+
172
+ class Cursor:
173
+ def __init__(self, connection):
174
+ self.connection = connection
175
+ self.description = None
176
+ self.rowcount = -1
177
+ self.lastrowid = 0
178
+ self._rows = []
179
+
180
+ def execute(self, query, params=()):
181
+ if not self.connection:
182
+ raise OperationalError("MySQL: cursor is closed")
183
+ if not isinstance(query, str):
184
+ raise ProgrammingError("MySQL query must be a string")
185
+ self.description = None
186
+ self.rowcount = -1
187
+ self.lastrowid = 0
188
+ self._rows = []
189
+ query = _query_with_params(query, params)
190
+ self.connection.execute(query, self)
191
+ return self
192
+
193
+ def fetchall(self):
194
+ rows = self._rows
195
+ self._rows = []
196
+ return rows
197
+
198
+ def close(self):
199
+ self.connection = None
200
+ self._rows = []
201
+
202
+
203
+ class connect:
204
+ def __init__(self, host, user, password, database=None, port=3306, use_ssl=True):
205
+ self.encoding = "utf-8"
206
+ self.autocommit = False
207
+ self.sock = None
208
+ self.host = host
209
+ self._password = password
210
+ self._use_ssl = use_ssl
211
+ self._tls_active = False
212
+ self._seq = 0
213
+ self._in_transaction = False
214
+
215
+ sock = socket.socket()
216
+ try:
217
+ address = socket.getaddrinfo(host, port)[0][-1]
218
+ sock.connect(address)
219
+ self.sock = sock
220
+ self._handshake(user, password, database)
221
+ except Exception:
222
+ try:
223
+ sock.close()
224
+ except Exception:
225
+ pass
226
+ self.sock = None
227
+ raise
228
+
229
+ def _read(self, size):
230
+ sock = self.sock
231
+ if not sock:
232
+ _lost()
233
+ result = b""
234
+ while len(result) < size:
235
+ read = sock.read if hasattr(sock, "read") else sock.recv
236
+ chunk = read(size - len(result))
237
+ if not chunk:
238
+ _lost()
239
+ result += chunk
240
+ return result
241
+
242
+ def _write(self, data):
243
+ sock = self.sock
244
+ if not sock:
245
+ _lost()
246
+ write = sock.write if hasattr(sock, "write") else sock.send
247
+ view = memoryview(data)
248
+ offset = 0
249
+ while offset < len(data):
250
+ written = write(view[offset:])
251
+ if not written:
252
+ _lost()
253
+ offset += written
254
+
255
+ def _read_packet(self):
256
+ chunks = []
257
+ while True:
258
+ header = self._read(4)
259
+ size = header[0] | (header[1] << 8) | (header[2] << 16)
260
+ seq = header[3]
261
+ if seq != self._seq:
262
+ raise OperationalError("MySQL: invalid packet sequence")
263
+ self._seq = (self._seq + 1) & 0xff
264
+ chunks.append(self._read(size))
265
+ if size < 0xffffff:
266
+ return b"".join(chunks)
267
+
268
+ def _send_packet(self, payload):
269
+ offset = 0
270
+ size = len(payload)
271
+ while size - offset >= 0xffffff:
272
+ part = payload[offset:offset + 0xffffff]
273
+ self._write(bytes((0xff, 0xff, 0xff, self._seq)) + part)
274
+ self._seq = (self._seq + 1) & 0xff
275
+ offset += 0xffffff
276
+ tail = payload[offset:]
277
+ length = len(tail)
278
+ header = bytes((length & 0xff, (length >> 8) & 0xff, (length >> 16) & 0xff, self._seq))
279
+ self._write(header + tail)
280
+ self._seq = (self._seq + 1) & 0xff
281
+
282
+ def _handshake(self, user, password, database):
283
+ greeting = self._read_packet()
284
+ if greeting and greeting[0] == 0xff:
285
+ self._server_error(greeting)
286
+ if len(greeting) < 34 or greeting[0] != 10:
287
+ raise OperationalError("MySQL: unsupported or malformed server greeting")
288
+
289
+ pos = greeting.index(b"\x00", 1) + 1
290
+ pos += 4
291
+ salt = greeting[pos:pos + 8]
292
+ pos += 9
293
+ lower = int.from_bytes(greeting[pos:pos + 2], "little")
294
+ pos += 2
295
+ if pos >= len(greeting):
296
+ raise OperationalError("MySQL: incomplete server greeting")
297
+ pos += 1 # character set
298
+ pos += 2 # status flags
299
+ upper = int.from_bytes(greeting[pos:pos + 2], "little")
300
+ pos += 2
301
+ server_caps = lower | (upper << 16)
302
+
303
+ auth_len = greeting[pos] if pos < len(greeting) else 0
304
+ pos += 1 + 10
305
+ if server_caps & CLIENT_SECURE_CONNECTION and pos < len(greeting):
306
+ second_len = max(13, auth_len - 8) if auth_len else 13
307
+ second_salt = greeting[pos:pos + second_len]
308
+ if second_salt.endswith(b"\x00"):
309
+ second_salt = second_salt[:-1]
310
+ salt += second_salt
311
+ pos += second_len
312
+ plugin = "mysql_native_password"
313
+ if server_caps & CLIENT_PLUGIN_AUTH and pos < len(greeting):
314
+ plugin_end = greeting.find(b"\x00", pos)
315
+ if plugin_end < 0:
316
+ plugin_end = len(greeting)
317
+ plugin = greeting[pos:plugin_end].decode("ascii")
318
+ requested = (CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_PROTOCOL_41 |
319
+ CLIENT_TRANSACTIONS | CLIENT_SECURE_CONNECTION |
320
+ CLIENT_PLUGIN_AUTH)
321
+ if database:
322
+ requested |= CLIENT_CONNECT_WITH_DB
323
+ if self._use_ssl:
324
+ requested |= CLIENT_SSL
325
+ if not server_caps & CLIENT_PROTOCOL_41:
326
+ raise OperationalError("MySQL: server does not support protocol 4.1")
327
+ if not server_caps & CLIENT_SECURE_CONNECTION:
328
+ raise OperationalError("MySQL: server does not support secure authentication")
329
+ if not server_caps & CLIENT_PLUGIN_AUTH:
330
+ raise OperationalError("MySQL: server does not support authentication plugins")
331
+ client_caps = requested & server_caps
332
+ if database and not client_caps & CLIENT_CONNECT_WITH_DB:
333
+ raise OperationalError("MySQL: server does not support selecting a database at login")
334
+
335
+ if self._use_ssl:
336
+ if not server_caps & CLIENT_SSL:
337
+ raise OperationalError("MySQL: server does not support TLS")
338
+ self._seq = 1
339
+ ssl_request = (client_caps.to_bytes(4, "little") +
340
+ (1024 * 1024).to_bytes(4, "little") +
341
+ b"\x2d" + bytes(23))
342
+ self._send_packet(ssl_request)
343
+ import ssl
344
+ try:
345
+ self.sock = ssl.wrap_socket(self.sock, server_hostname=self.host)
346
+ except TypeError:
347
+ self.sock = ssl.wrap_socket(self.sock)
348
+ self._tls_active = True
349
+
350
+ auth = _auth_response(plugin, password, salt, self._tls_active)
351
+ response = (client_caps.to_bytes(4, "little") +
352
+ (1024 * 1024).to_bytes(4, "little") +
353
+ b"\x2d" + bytes(23) + user.encode("utf-8") + b"\x00" +
354
+ bytes((len(auth),)) + auth)
355
+ if database:
356
+ response += database.encode("utf-8") + b"\x00"
357
+ response += plugin.encode("ascii") + b"\x00"
358
+ self._send_packet(response)
359
+
360
+ while True:
361
+ packet = self._read_packet()
362
+ if not packet:
363
+ raise OperationalError("MySQL: empty authentication response")
364
+ if packet[0] == 0x00:
365
+ return
366
+ if packet[0] == 0xff:
367
+ self._server_error(packet)
368
+ if packet[0] == 0xfe:
369
+ end = packet.find(b"\x00", 1)
370
+ if end < 0:
371
+ raise OperationalError("MySQL: malformed authentication switch")
372
+ plugin = packet[1:end].decode("ascii")
373
+ salt = packet[end + 1:]
374
+ if salt.endswith(b"\x00"):
375
+ salt = salt[:-1]
376
+ self._send_packet(
377
+ _auth_response(plugin, password, salt, self._tls_active)
378
+ )
379
+ continue
380
+ if packet[0] == 0x01 and len(packet) > 1:
381
+ if packet[1] == 0x03:
382
+ continue
383
+ if packet[1] == 0x04:
384
+ if not self._tls_active:
385
+ raise OperationalError(
386
+ "MySQL: caching_sha2_password full authentication requires use_ssl=True"
387
+ )
388
+ self._send_packet(password.encode("utf-8") + b"\x00")
389
+ continue
390
+ raise OperationalError("MySQL: unexpected authentication response")
391
+
392
+ def _server_error(self, packet):
393
+ code = int.from_bytes(packet[1:3], "little") if len(packet) >= 3 else 0
394
+ pos = 3
395
+ state = ""
396
+ if len(packet) >= 9 and packet[3:4] == b"#":
397
+ state = " " + packet[4:9].decode("ascii", "replace")
398
+ pos = 9
399
+ message = packet[pos:].decode(self.encoding, "replace")
400
+ raise OperationalError("MySQL error %d%s: %s" % (code, state, message))
401
+
402
+ def _read_ok(self, packet, cursor):
403
+ pos = 1
404
+ affected, pos = _lenenc_int(packet, pos)
405
+ last_id, pos = _lenenc_int(packet, pos)
406
+ if len(packet) >= pos + 4:
407
+ status = int.from_bytes(packet[pos:pos + 2], "little")
408
+ pos += 4 # status and warnings
409
+ else:
410
+ status = 0
411
+ if cursor:
412
+ cursor.description = None
413
+ cursor.rowcount = affected if affected is not None else 0
414
+ cursor.lastrowid = last_id if last_id is not None else 0
415
+ cursor._rows = []
416
+ return status
417
+
418
+ def _column_definition(self, packet):
419
+ pos = 0
420
+ fields = []
421
+ for _ in range(6):
422
+ value, pos = _lenenc_bytes(packet, pos)
423
+ fields.append(value)
424
+ fixed_size, pos = _lenenc_int(packet, pos)
425
+ if fixed_size is None or len(packet) < pos + fixed_size:
426
+ raise OperationalError("MySQL: malformed column definition")
427
+ charset = int.from_bytes(packet[pos:pos + 2], "little")
428
+ length = int.from_bytes(packet[pos + 2:pos + 6], "little")
429
+ column_type = packet[pos + 6]
430
+ flags = int.from_bytes(packet[pos + 7:pos + 9], "little")
431
+ name = fields[4].decode(self.encoding, "replace") if fields[4] else ""
432
+ return (name, column_type, charset, flags, length)
433
+
434
+ def _decode_value(self, value, column):
435
+ if value is None:
436
+ return None
437
+ column_type, charset, flags = column[1], column[2], column[3]
438
+ if column_type in (249, 250, 251, 252):
439
+ return value if charset == 63 else value.decode(self.encoding, "replace")
440
+ if column_type in (15, 253, 254) and (charset == 63 or flags & 0x80):
441
+ return value
442
+ text = value.decode(self.encoding, "replace")
443
+ if column_type in (1, 2, 3, 8, 9, 13):
444
+ try:
445
+ return int(text)
446
+ except ValueError:
447
+ return text
448
+ if column_type in (4, 5):
449
+ try:
450
+ return float(text)
451
+ except ValueError:
452
+ return text
453
+ return text
454
+
455
+ def _read_rows(self, columns, cursor):
456
+ rows = []
457
+ while True:
458
+ packet = self._read_packet()
459
+ if packet and packet[0] == 0xff:
460
+ self._server_error(packet)
461
+ if packet and packet[0] == 0xfe and len(packet) < 9:
462
+ break
463
+ if packet and packet[0] == 0xfb:
464
+ raise OperationalError("MySQL: LOCAL INFILE is not supported")
465
+ pos = 0
466
+ row = []
467
+ for column in columns:
468
+ value, pos = _lenenc_bytes(packet, pos)
469
+ row.append(self._decode_value(value, column))
470
+ rows.append(tuple(row))
471
+ if cursor:
472
+ cursor._rows = rows
473
+ cursor.description = [
474
+ (column[0], column[1], None, column[4], None, None, None)
475
+ for column in columns
476
+ ]
477
+ cursor.rowcount = len(rows)
478
+ cursor.lastrowid = 0
479
+
480
+ def _run_query(self, query, cursor):
481
+ self._seq = 0
482
+ self._send_packet(b"\x03" + query.encode(self.encoding))
483
+ packet = self._read_packet()
484
+ if packet and packet[0] == 0xff:
485
+ self._server_error(packet)
486
+ if packet and packet[0] == 0x00:
487
+ self._read_ok(packet, cursor)
488
+ return
489
+ if packet and packet[0] == 0xfb:
490
+ raise OperationalError("MySQL: LOCAL INFILE is not supported")
491
+ column_count, _ = _lenenc_int(packet)
492
+ if column_count is None:
493
+ raise OperationalError("MySQL: malformed result set")
494
+ columns = []
495
+ for _ in range(column_count):
496
+ definition = self._read_packet()
497
+ if definition and definition[0] == 0xff:
498
+ self._server_error(definition)
499
+ columns.append(self._column_definition(definition))
500
+ terminator = self._read_packet()
501
+ if terminator and terminator[0] == 0xff:
502
+ self._server_error(terminator)
503
+ self._read_rows(columns, cursor)
504
+
505
+ def cursor(self):
506
+ return Cursor(self)
507
+
508
+ def execute(self, query, obj=None):
509
+ if not self.sock:
510
+ _lost()
511
+ if not isinstance(query, str):
512
+ raise ProgrammingError("MySQL query must be a string")
513
+ if not self.autocommit and not self._in_transaction:
514
+ self._run_query("START TRANSACTION", None)
515
+ self._in_transaction = True
516
+ self._run_query(query, obj)
517
+
518
+ def begin(self):
519
+ if self._in_transaction:
520
+ self.rollback()
521
+ self._run_query("START TRANSACTION", None)
522
+ self._in_transaction = True
523
+
524
+ def commit(self):
525
+ if self.sock and self._in_transaction:
526
+ self._run_query("COMMIT", None)
527
+ self._in_transaction = False
528
+
529
+ def rollback(self):
530
+ if self.sock and self._in_transaction:
531
+ self._run_query("ROLLBACK", None)
532
+ self._in_transaction = False
533
+
534
+ def close(self):
535
+ sock = self.sock
536
+ if sock:
537
+ try:
538
+ self._seq = 0
539
+ self._send_packet(b"\x01")
540
+ finally:
541
+ sock.close()
542
+ self.sock = None
543
+ self._in_transaction = False
544
+
545
+
546
+ def _quote_identifier(identifier):
547
+ if not isinstance(identifier, str) or not identifier:
548
+ raise ProgrammingError("database name must be a non-empty string")
549
+ return "`" + identifier.replace("`", "``") + "`"
550
+
551
+
552
+ def create_database(host, user, password, database, port=3306, use_ssl=True):
553
+ connection = connect(host, user, password, None, port, use_ssl)
554
+ try:
555
+ connection.autocommit = True
556
+ connection.execute("CREATE DATABASE " + _quote_identifier(database))
557
+ finally:
558
+ connection.close()
559
+
560
+
561
+ def drop_database(host, user, password, database, port=3306, use_ssl=True):
562
+ connection = connect(host, user, password, None, port, use_ssl)
563
+ try:
564
+ connection.autocommit = True
565
+ connection.execute("DROP DATABASE " + _quote_identifier(database))
566
+ finally:
567
+ connection.close()
ownsql-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
ownsql-1.0.0/setup.py ADDED
@@ -0,0 +1,38 @@
1
+ from pathlib import Path
2
+
3
+ from setuptools import setup
4
+
5
+ setup_dir = Path(__file__).resolve().parent
6
+ root_dir = setup_dir if (setup_dir / "ownSQL.py").is_file() else setup_dir.parent
7
+
8
+ with (root_dir / "README.md").open("r", encoding="utf-8") as fh:
9
+ long_description = fh.read()
10
+
11
+ setup(
12
+ name="ownSQL",
13
+ version="1.0.0", # Replaced automatically from the release tag by the publish workflow
14
+ author="TimonW-Dev",
15
+ author_email="timon.github@outlook.com",
16
+ description=(
17
+ "A MySQL database driver for MicroPython, supporting MySQL Server 8.x and 9.x."
18
+ ),
19
+ long_description=long_description,
20
+ long_description_content_type="text/markdown",
21
+ url="https://github.com/TimonW-Dev/ownSQL",
22
+ package_dir={"": "." if root_dir == setup_dir else ".."},
23
+ py_modules=["ownSQL"],
24
+ classifiers=[
25
+ "Programming Language :: Python :: Implementation :: MicroPython",
26
+ "Programming Language :: Python :: 3",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Operating System :: Other OS",
29
+ "Topic :: Software Development :: Embedded Systems",
30
+ "Topic :: Database",
31
+ ],
32
+ python_requires=">=3.4",
33
+ keywords="micropython mysql esp8266 esp32 database driver",
34
+ project_urls={
35
+ "Bug Tracker": "https://github.com/TimonW-Dev/ownSQL/issues",
36
+ "Source Code": "https://github.com/TimonW-Dev/ownSQL",
37
+ },
38
+ )