vmnp 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.
- vmnp-0.1.0/LICENSE +21 -0
- vmnp-0.1.0/PKG-INFO +20 -0
- vmnp-0.1.0/README.md +3 -0
- vmnp-0.1.0/pyproject.toml +27 -0
- vmnp-0.1.0/setup.cfg +4 -0
- vmnp-0.1.0/src/vmnp/__init__.py +3 -0
- vmnp-0.1.0/src/vmnp/db.py +320 -0
- vmnp-0.1.0/src/vmnp.egg-info/PKG-INFO +20 -0
- vmnp-0.1.0/src/vmnp.egg-info/SOURCES.txt +10 -0
- vmnp-0.1.0/src/vmnp.egg-info/dependency_links.txt +1 -0
- vmnp-0.1.0/src/vmnp.egg-info/requires.txt +6 -0
- vmnp-0.1.0/src/vmnp.egg-info/top_level.txt +1 -0
vmnp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [year] [fullname]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
vmnp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vmnp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MySQL database helper with connection management and CRUD utilities
|
|
5
|
+
Author-email: Mike Saysell <michael.saysell@ellipsedata.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Provides-Extra: mysqlclient
|
|
13
|
+
Requires-Dist: mysqlclient; extra == "mysqlclient"
|
|
14
|
+
Provides-Extra: pymysql
|
|
15
|
+
Requires-Dist: PyMySQL; extra == "pymysql"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
DEPRECATED
|
|
19
|
+
|
|
20
|
+
A helper class for database connections
|
vmnp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vmnp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Mike Saysell", email="michael.saysell@ellipsedata.com" }
|
|
10
|
+
]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICEN[CS]E*"]
|
|
13
|
+
description = "MySQL database helper with connection management and CRUD utilities"
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
requires-python = ">=3.8"
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
mysqlclient = ["mysqlclient"]
|
|
24
|
+
pymysql = ["PyMySQL"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
vmnp-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
import MySQLdb
|
|
6
|
+
except ImportError:
|
|
7
|
+
try:
|
|
8
|
+
import pymysql
|
|
9
|
+
|
|
10
|
+
pymysql.install_as_MySQLdb()
|
|
11
|
+
except:
|
|
12
|
+
sys.exit("one of MySQLdb (mysqlclient) or PyMySQL needs to be installed")
|
|
13
|
+
|
|
14
|
+
import MySQLdb.converters
|
|
15
|
+
from MySQLdb.constants import FIELD_TYPE
|
|
16
|
+
|
|
17
|
+
### TODO: convert to connection pooling for thread-safe database connections
|
|
18
|
+
# try:
|
|
19
|
+
# from DBUtils.PersistentDB import PersistentDB
|
|
20
|
+
# except ImportError:
|
|
21
|
+
# pass
|
|
22
|
+
# # sys.exit("DBUtils needs to be installed")
|
|
23
|
+
# https://cito.github.io/DBUtils/UsersGuide.html#functionality
|
|
24
|
+
# https://stackoverflow.com/questions/47711689/error-while-using-pymysql-in-flask
|
|
25
|
+
# https://stackoverflow.com/questions/32658679/how-to-create-a-mysql-connection-pool-or-any-better-way-to-initialize-the-multip
|
|
26
|
+
# https://dev.mysql.com/doc/connector-python/en/connector-python-connection-pooling.html
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Create a Dbnp class and a dbh handle
|
|
30
|
+
class Dbnp:
|
|
31
|
+
dbh = None
|
|
32
|
+
config = {
|
|
33
|
+
"host": "localhost",
|
|
34
|
+
"port": 3306,
|
|
35
|
+
"db": "information_schema",
|
|
36
|
+
"user": "readonly",
|
|
37
|
+
"passwd": "",
|
|
38
|
+
"ssl": {
|
|
39
|
+
"ca": "/etc/mysql/ssl/ca-cert.pem",
|
|
40
|
+
"cert": "/etc/mysql/ssl/client-cert.pem",
|
|
41
|
+
"key": "/etc/mysql/ssl/client-key.pem",
|
|
42
|
+
"check_hostname": False,
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
convert_dict = MySQLdb.converters.conversions.copy()
|
|
47
|
+
# convert_dict[FIELD_TYPE.TINY] = str
|
|
48
|
+
# convert_dict[FIELD_TYPE.SHORT] = str
|
|
49
|
+
# convert_dict[FIELD_TYPE.LONG] = str
|
|
50
|
+
# convert_dict[FIELD_TYPE.FLOAT] = str
|
|
51
|
+
convert_dict[FIELD_TYPE.DOUBLE] = str
|
|
52
|
+
convert_dict[FIELD_TYPE.DECIMAL] = str
|
|
53
|
+
convert_dict[FIELD_TYPE.NEWDECIMAL] = str
|
|
54
|
+
# convert_dict[FIELD_TYPE.LONGLONG] = str
|
|
55
|
+
# convert_dict[FIELD_TYPE.INT24] = str
|
|
56
|
+
# convert_dict[FIELD_TYPE.YEAR] = str
|
|
57
|
+
# convert_dict[FIELD_TYPE.SET] = str
|
|
58
|
+
convert_dict[FIELD_TYPE.TIMESTAMP] = str
|
|
59
|
+
convert_dict[FIELD_TYPE.DATETIME] = str
|
|
60
|
+
convert_dict[FIELD_TYPE.TIME] = str
|
|
61
|
+
convert_dict[FIELD_TYPE.DATE] = str
|
|
62
|
+
|
|
63
|
+
def connect(self):
|
|
64
|
+
# TODO: read_default_file and read_default_group options!
|
|
65
|
+
self.dbh = MySQLdb.connect(
|
|
66
|
+
host=self.config["host"],
|
|
67
|
+
port=self.config["port"],
|
|
68
|
+
db=self.config["db"],
|
|
69
|
+
user=self.config["user"],
|
|
70
|
+
passwd=self.config["passwd"],
|
|
71
|
+
ssl=self.config["ssl"],
|
|
72
|
+
charset="utf8mb4",
|
|
73
|
+
use_unicode=True,
|
|
74
|
+
conv=self.convert_dict,
|
|
75
|
+
)
|
|
76
|
+
self.dbh.autocommit(True)
|
|
77
|
+
|
|
78
|
+
def reconnect(self):
|
|
79
|
+
try:
|
|
80
|
+
self.dbh.ping(True)
|
|
81
|
+
self.dbh.autocommit(
|
|
82
|
+
True
|
|
83
|
+
) # ping seems to change it to True anyway, irrespective of original setting
|
|
84
|
+
except:
|
|
85
|
+
# ping doesn't reconnect if connection is closed: InterfaceError or 'MySQL server has gone away'
|
|
86
|
+
if self.dbh is not None and self.dbh.open:
|
|
87
|
+
self.dbh.close()
|
|
88
|
+
self.connect()
|
|
89
|
+
|
|
90
|
+
def dbconfig(self, host, port, db, user, passwd, ssl=None):
|
|
91
|
+
if host is not None:
|
|
92
|
+
self.config["host"] = host
|
|
93
|
+
if port is not None:
|
|
94
|
+
self.config["port"] = port
|
|
95
|
+
if db is not None:
|
|
96
|
+
self.config["db"] = db
|
|
97
|
+
if user is not None:
|
|
98
|
+
self.config["user"] = user
|
|
99
|
+
if passwd is not None:
|
|
100
|
+
self.config["passwd"] = passwd
|
|
101
|
+
if ssl is not None:
|
|
102
|
+
self.config["ssl"]["ca"] = ssl["ca"]
|
|
103
|
+
self.config["ssl"]["cert"] = ssl["cert"]
|
|
104
|
+
self.config["ssl"]["key"] = ssl["key"]
|
|
105
|
+
|
|
106
|
+
def cursor(self):
|
|
107
|
+
return self.dbh.cursor(MySQLdb.cursors.DictCursor)
|
|
108
|
+
|
|
109
|
+
def query(self, sql, vals=None):
|
|
110
|
+
try:
|
|
111
|
+
cursor = self.cursor()
|
|
112
|
+
# cursor.execute("SELECT @@autocommit")
|
|
113
|
+
# cursor.execute("SELECT CONNECTION_ID() AS connection_id")
|
|
114
|
+
# print(cursor.fetchone())
|
|
115
|
+
cursor.execute(sql, vals)
|
|
116
|
+
except AttributeError:
|
|
117
|
+
# database connection has never been opened
|
|
118
|
+
if self.dbh is None:
|
|
119
|
+
self.connect()
|
|
120
|
+
cursor = self.cursor()
|
|
121
|
+
cursor.execute(sql, vals)
|
|
122
|
+
else:
|
|
123
|
+
raise
|
|
124
|
+
except MySQLdb.InterfaceError:
|
|
125
|
+
# database connection has closed (eg, maxscale restarted)
|
|
126
|
+
self.reconnect()
|
|
127
|
+
cursor = self.cursor()
|
|
128
|
+
cursor.execute(sql, vals)
|
|
129
|
+
except MySQLdb.OperationalError as e:
|
|
130
|
+
errnum, errmsg = e.args
|
|
131
|
+
if errnum == 2006 or errnum == 2013:
|
|
132
|
+
# MySQL server has gone away
|
|
133
|
+
# Lost connection to MySQL server during query
|
|
134
|
+
self.reconnect()
|
|
135
|
+
cursor = self.cursor()
|
|
136
|
+
cursor.execute(sql, vals)
|
|
137
|
+
else:
|
|
138
|
+
raise
|
|
139
|
+
# self.dbh.commit() # disabled with autocommit=1. (needed as future selects will be part of the same transaction (REPEATABLE READ))
|
|
140
|
+
return cursor
|
|
141
|
+
|
|
142
|
+
def quote(self, s=None):
|
|
143
|
+
# return "'" + MySQLdb.escape_string(s).decode('utf-8') + "'"
|
|
144
|
+
return "'" + s.replace("'", "\\'") + "'"
|
|
145
|
+
|
|
146
|
+
def quote_identifier(self, s1=None, s2=None):
|
|
147
|
+
r = ""
|
|
148
|
+
for s in [s1, s2]:
|
|
149
|
+
if s is not None:
|
|
150
|
+
q = "`" + s.replace("`", "``") + "`"
|
|
151
|
+
if r == "":
|
|
152
|
+
r = q
|
|
153
|
+
else:
|
|
154
|
+
r = r + "." + q
|
|
155
|
+
return r
|
|
156
|
+
|
|
157
|
+
def rstr(self, r):
|
|
158
|
+
if r is not None and isinstance(r, dict):
|
|
159
|
+
for k, v in r.items():
|
|
160
|
+
if v is None:
|
|
161
|
+
r[k] = None
|
|
162
|
+
elif isinstance(v, bytes):
|
|
163
|
+
r[k] = v.decode("utf-8")
|
|
164
|
+
else:
|
|
165
|
+
r[k] = str(v)
|
|
166
|
+
|
|
167
|
+
def error(self):
|
|
168
|
+
return MySQLdb.Error
|
|
169
|
+
|
|
170
|
+
def warning(self):
|
|
171
|
+
return MySQLdb.Warning
|
|
172
|
+
|
|
173
|
+
def __del__(self):
|
|
174
|
+
if self.dbh:
|
|
175
|
+
self.dbh.close()
|
|
176
|
+
|
|
177
|
+
#
|
|
178
|
+
# CRUD HELPERS
|
|
179
|
+
#
|
|
180
|
+
|
|
181
|
+
def create_row(self, db, table, row):
|
|
182
|
+
cols = []
|
|
183
|
+
hold = []
|
|
184
|
+
vals = []
|
|
185
|
+
for key, val in row.items():
|
|
186
|
+
cols.append(self.quote_identifier(key))
|
|
187
|
+
if val is not None and val.startswith("_now"):
|
|
188
|
+
m = re.search("^_now\((\d+)\)$", val)
|
|
189
|
+
if m:
|
|
190
|
+
hold.append("NOW(%s)" % m.group(1))
|
|
191
|
+
else:
|
|
192
|
+
hold.append("NOW()")
|
|
193
|
+
else:
|
|
194
|
+
hold.append("%s")
|
|
195
|
+
vals.append(val)
|
|
196
|
+
|
|
197
|
+
sql = "INSERT INTO %s (%s) VALUES (%s)" % (
|
|
198
|
+
self.quote_identifier(db, table),
|
|
199
|
+
",".join(cols),
|
|
200
|
+
",".join(hold),
|
|
201
|
+
)
|
|
202
|
+
return self.query(sql, vals)
|
|
203
|
+
|
|
204
|
+
def read_row(self, db, table, keys, vals):
|
|
205
|
+
keys, vals = self._get_keys_vals(keys, vals)
|
|
206
|
+
where = self._make_where(keys)
|
|
207
|
+
sql = "SELECT * FROM %s WHERE %s" % (self.quote_identifier(db, table), where)
|
|
208
|
+
cur = self.query(sql, vals)
|
|
209
|
+
return cur.fetchone()
|
|
210
|
+
|
|
211
|
+
def update_row(self, db, table, row, keys, vals=None):
|
|
212
|
+
keys, vals = self._get_keys_vals(keys, vals, row)
|
|
213
|
+
do_create = False
|
|
214
|
+
if len(vals) > 0:
|
|
215
|
+
res = self.read_row(db, table, keys, vals)
|
|
216
|
+
if res:
|
|
217
|
+
# TODO: compare vals with res, only update the columns that have changed
|
|
218
|
+
upset = []
|
|
219
|
+
upval = []
|
|
220
|
+
for key, val in row.items():
|
|
221
|
+
if val is not None and val.startswith("_now"):
|
|
222
|
+
m = re.search("^_now\((\d+)\)$", val)
|
|
223
|
+
if m:
|
|
224
|
+
upset.append(
|
|
225
|
+
self.quote_identifier(key) + "=NOW(%s)" % m.group(1)
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
upset.append(self.quote_identifier(key) + "=NOW()")
|
|
229
|
+
else:
|
|
230
|
+
upset.append(self.quote_identifier(key) + "=%s")
|
|
231
|
+
upval.append(val)
|
|
232
|
+
upval += vals
|
|
233
|
+
where = self._make_where(keys)
|
|
234
|
+
sql = "UPDATE %s SET %s WHERE %s" % (
|
|
235
|
+
self.quote_identifier(db, table),
|
|
236
|
+
", ".join(upset),
|
|
237
|
+
where,
|
|
238
|
+
)
|
|
239
|
+
return self.query(sql, upval)
|
|
240
|
+
else:
|
|
241
|
+
do_create = True
|
|
242
|
+
else:
|
|
243
|
+
do_create = True
|
|
244
|
+
|
|
245
|
+
if do_create:
|
|
246
|
+
return self.create_row(db, table, row)
|
|
247
|
+
|
|
248
|
+
def delete_row(self, db, table, keys, vals):
|
|
249
|
+
keys, vals = self._get_keys_vals(keys, vals)
|
|
250
|
+
where = self._make_where(keys)
|
|
251
|
+
sql = "DELETE FROM %s WHERE %s" % (self.quote_identifier(db, table), where)
|
|
252
|
+
return self.query(sql, vals)
|
|
253
|
+
|
|
254
|
+
def _get_keys_vals(self, keys, vals, row=None):
|
|
255
|
+
if keys is None:
|
|
256
|
+
keys = []
|
|
257
|
+
if vals is None:
|
|
258
|
+
vals = []
|
|
259
|
+
if row is None:
|
|
260
|
+
row = {}
|
|
261
|
+
|
|
262
|
+
if type(keys) != list:
|
|
263
|
+
keys = [keys]
|
|
264
|
+
if type(vals) != list:
|
|
265
|
+
vals = [vals]
|
|
266
|
+
|
|
267
|
+
if not vals and keys:
|
|
268
|
+
if type(keys) == list:
|
|
269
|
+
for k in keys:
|
|
270
|
+
if k in row:
|
|
271
|
+
vals.append(row[k])
|
|
272
|
+
else:
|
|
273
|
+
break
|
|
274
|
+
elif keys in row:
|
|
275
|
+
vals.append(row[keys])
|
|
276
|
+
|
|
277
|
+
return keys, vals
|
|
278
|
+
|
|
279
|
+
def _make_where(self, keys=None):
|
|
280
|
+
if keys is None:
|
|
281
|
+
where = "1"
|
|
282
|
+
elif type(keys) == list:
|
|
283
|
+
ws = []
|
|
284
|
+
for k in keys:
|
|
285
|
+
ws.append(self.quote_identifier(k) + "=%s")
|
|
286
|
+
where = " AND ".join(ws)
|
|
287
|
+
else:
|
|
288
|
+
where = self.quote_identifier(keys) + "=%s"
|
|
289
|
+
|
|
290
|
+
return where
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
"""
|
|
294
|
+
From the MySQLdb User Guide:
|
|
295
|
+
|
|
296
|
+
The MySQL protocol can not handle multiple threads using the same
|
|
297
|
+
connection at once. Some earlier versions of MySQLdb utilized
|
|
298
|
+
locking to achieve a threadsafety of 2. While this is not terribly
|
|
299
|
+
hard to accomplish using the standard Cursor class (which uses
|
|
300
|
+
mysql_store_result()), it is complicated by SSCursor (which uses
|
|
301
|
+
mysql_use_result(); with the latter you must ensure all the rows
|
|
302
|
+
have been read before another query can be executed. It is further
|
|
303
|
+
complicated by the addition of transactions, since transactions
|
|
304
|
+
start when a cursor execute a query, but end when COMMIT or
|
|
305
|
+
ROLLBACK is executed by the Connection object. Two threads simply
|
|
306
|
+
cannot share a connection while a transaction is in progress, in
|
|
307
|
+
addition to not being able to share it during query execution.
|
|
308
|
+
This excessively complicated the code to the point where it just
|
|
309
|
+
isn't worth it.
|
|
310
|
+
|
|
311
|
+
The general upshot of this is: Don't share connections between
|
|
312
|
+
threads. It's really not worth your effort or mine, and in the
|
|
313
|
+
end, will probably hurt performance, since the MySQL server runs a
|
|
314
|
+
separate thread for each connection. You can certainly do things
|
|
315
|
+
like cache connections in a pool, and give those connections to
|
|
316
|
+
one thread at a time. If you let two threads use a connection
|
|
317
|
+
simultaneously, the MySQL client library will probably upchuck and
|
|
318
|
+
die. You have been warned.
|
|
319
|
+
|
|
320
|
+
"""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vmnp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MySQL database helper with connection management and CRUD utilities
|
|
5
|
+
Author-email: Mike Saysell <michael.saysell@ellipsedata.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Provides-Extra: mysqlclient
|
|
13
|
+
Requires-Dist: mysqlclient; extra == "mysqlclient"
|
|
14
|
+
Provides-Extra: pymysql
|
|
15
|
+
Requires-Dist: PyMySQL; extra == "pymysql"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
DEPRECATED
|
|
19
|
+
|
|
20
|
+
A helper class for database connections
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vmnp
|