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 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,3 @@
1
+ DEPRECATED
2
+
3
+ A helper class for database connections
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from vmnp.db import Dbnp
2
+
3
+ __all__ = ["Dbnp"]
@@ -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,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/vmnp/__init__.py
5
+ src/vmnp/db.py
6
+ src/vmnp.egg-info/PKG-INFO
7
+ src/vmnp.egg-info/SOURCES.txt
8
+ src/vmnp.egg-info/dependency_links.txt
9
+ src/vmnp.egg-info/requires.txt
10
+ src/vmnp.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+
2
+ [mysqlclient]
3
+ mysqlclient
4
+
5
+ [pymysql]
6
+ PyMySQL
@@ -0,0 +1 @@
1
+ vmnp