Coverage for /var/devmt/py/dbilib_0.4.1/dbilib/_dbi_mysql.py: 100%

61 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-27 21:25 +0100

1#!/usr/bin/env python3 

2# -*- coding: utf-8 -*- 

3""" 

4:Purpose: This module contains the library's *MySQL* database methods 

5 and attribute accessors; which are a specialised version of 

6 the :class:`_dbi_base._DBIBase` class methods. 

7 

8:Platform: Linux/Windows | Python 3.10+ 

9:Developer: J Berendt 

10:Email: support@s3dev.uk 

11 

12:Comments: n/a 

13 

14:Example: 

15 

16 For class-specific usage examples, please refer to the docstring 

17 for the following classes: 

18 

19 - :class:`_DBIMySQL` 

20 

21""" 

22# pylint: disable=wrong-import-order 

23# Silence the spurious IDE-based error. 

24# pylint: disable=import-error 

25 

26import pandas as pd 

27import warnings 

28from mysql.connector.errors import IntegrityError 

29from sqlalchemy.exc import SQLAlchemyError 

30from utils4.reporterror import reporterror 

31from utils4.user_interface import ui 

32# locals 

33from _dbi_base import _DBIBase 

34 

35 

36class _DBIMySQL(_DBIBase): 

37 """This *private* class holds the methods and properties which are 

38 used for accessing MySQL-like databases, including MariaDB. 

39 

40 Note: 

41 This class is *not* designed to be interacted with directly. 

42 

43 Rather, please use the :class:`database.DBInterface` class 

44 instead, as the proper interface class has an automatic switch 

45 for database interfaces, based on the ``sqlalchemy.Engine`` 

46 object which is created from the connection string. 

47 

48 Args: 

49 connstr (str): The database-specific SQLAlchemy connection 

50 string. 

51 

52 :Example Use: 

53 

54 This low-level generalised class is designed to be inherited by 

55 the calling/wrapping class as:: 

56 

57 >>> from dbilib.database import DBInterface 

58 

59 class MyDB(DBInterface): 

60 

61 def __init__(self, connstr: str): 

62 super().__init__(connstr=('mysql+mysqlconnector://' 

63 '<user>:<pwd>@<host>:<port>/' 

64 '<db_name>')) 

65 

66 """ 

67 

68 # The __init__ method is implemented in the parent class. 

69 

70 def call_procedure(self, 

71 proc: str, 

72 params: list | tuple = None, 

73 return_status: bool=False) -> pd.DataFrame | tuple[pd.DataFrame | bool]: 

74 """Call a stored procedure, and return as a DataFrame. 

75 

76 Args: 

77 proc (str): Name of the stored procedure to call. 

78 params (list | tuple, optional): A list (or tuple) of 

79 parameters to pass into the procedure. Defaults to None. 

80 return_status (bool, optional): Return the method's success 

81 status. Defaults to False. 

82 

83 Returns: 

84 pd.DataFrame | tuple[pd.DataFrame | bool]: 

85 If the ``return_status`` argument is True, a tuple of the 

86 data and the method's return status is returned as:: 

87 

88 (df, status) 

89 

90 Otherwise, only the data is returned, as a pd.DataFrame. 

91 

92 """ 

93 warnings.simplefilter('ignore') 

94 df = pd.DataFrame() 

95 success = False 

96 try: 

97 # Use a context manager in an attempt to alleviate the 

98 # '2055 Lost Connection' and System Error 32 BrokenPipeError. 

99 with self.engine.connect() as conn: 

100 cur = conn.connection.cursor(buffered=True) 

101 cur.callproc(proc, params) 

102 result = cur.stored_results() 

103 conn.connection.connection.commit() 

104 cur.close() 

105 df = self._result_to_df__stored(result=result) 

106 success = not df.empty 

107 except SQLAlchemyError as err: 

108 msg = f'Error occurred while running the USP: {proc}.' 

109 self._report_sa_error(msg=msg, error=err) 

110 except Exception as err: 

111 reporterror(error=err) 

112 return (df, success) if return_status else df 

113 

114 def call_procedure_update(self, 

115 proc: str, 

116 params: list=None, 

117 return_id: bool=False) -> bool | tuple: 

118 """Call an *update* or *insert* stored procedure. 

119 

120 Note: 

121 Results are *not* returned from this call, only a boolean 

122 status flag and the optional last row ID. 

123 

124 If results are desired, please use the 

125 :meth:~`call_procedure` method. 

126 

127 Args: 

128 proc (str): Name of the stored procedure to call. 

129 params (list, optional): A list of parameters to pass into 

130 the USP. Defaults to None. 

131 return_id (bool, optional): Return the ID of the last 

132 inserted row. Defaults to False. 

133 

134 Returns: 

135 bool | tuple: If ``return_id`` is False, True is 

136 returned if the procedure completed successfully, otherwise 

137 False. If ``return_id`` is True, a tuple containing the 

138 ID of the last inserted row and the execution success flag 

139 are returned as:: 

140 

141 (id, success_flag) 

142 

143 """ 

144 try: 

145 rowid = None 

146 success = False 

147 # Use a context manager in an attempt to alleviate the 

148 # '2055 Lost Connection' and System Error 32 BrokenPipeError. 

149 with self.engine.connect() as conn: 

150 cur = conn.connection.cursor() 

151 cur.callproc(proc, params) 

152 conn.connection.connection.commit() 

153 if return_id: 

154 # The cur.lastrowid is zero as the mysql_insert_id() 

155 # function call applied to a CALL and not the statement 

156 # within the procedure. Therefore, it must be manually 

157 # obtained here: 

158 cur.execute('SELECT LAST_INSERT_ID()') 

159 rowid = cur.fetchone()[0] 

160 cur.close() 

161 success = True 

162 except IntegrityError as ierr: 

163 # Duplicate entry: errno = 1062 

164 msg = f'{self._PREFIX} {ierr}' 

165 ui.print_alert(text=msg) 

166 except Exception as err: 

167 reporterror(err) 

168 return (rowid, success) if return_id else success 

169 

170 def call_procedure_update_many(self, *args, proc: str, iterable: list | tuple) -> bool: 

171 r"""Call an *update* or *insert* stored procedure for an iterable. 

172 

173 Note: 

174 The arguments are passed into the USP in the following order: 

175 

176 \*args, iterable_item 

177 

178 Ensure the USP is designed to accept the iterable item as 

179 the *last* parameter. 

180 

181 Args: 

182 *args (str | int | float): Positional arguments to be 

183 passed into the USP, in front of each iterable item. 

184 Note: The parameters are passed into the USP in the 

185 order received, followed by the iterable item. 

186 proc (str): Name of the stored procedure to call. 

187 iterable (list | tuple): List of items to be loaded into 

188 the database. 

189 

190 Returns: 

191 bool: True if the update was successful, otherwise False. 

192 

193 """ 

194 try: 

195 success = False 

196 with self.engine.connect() as conn: 

197 cur = conn.connection.cursor() 

198 for i in iterable: 

199 cur.callproc(proc, [*args, i]) 

200 conn.connection.connection.commit() 

201 cur.close() 

202 success = True 

203 except Exception as err: 

204 reporterror(err) 

205 return success 

206 

207 def call_procedure_update_raw(self, proc: str, params: list=None): 

208 """Call an *update* or *insert* stored procedure, without error 

209 handling. 

210 

211 .. warning:: 

212 This method is **unprotected**, perhaps use 

213 :meth:`~call_procedure_update` instead. 

214 

215 This 'raw' method *does not* contain an error handler. It is 

216 (by design) the responsibility of the caller to contain and 

217 control the errors. 

218 

219 The purpose of this raw method is to enable the caller method to 

220 contain and control the errors which might be generated from a 

221 USP call, for example a **duplicate key** error. 

222 

223 Args: 

224 proc (str): Name of the stored procedure to call. 

225 params (list, optional): A list of parameters to pass into 

226 the USP. Defaults to None. 

227 

228 """ 

229 with self._engine.connect() as conn: 

230 cur = conn.connection.cursor(buffered=True) 

231 cur.callproc(proc, params) 

232 conn.connection.connection.commit() 

233 cur.close() 

234 

235 def table_exists(self, table_name: str, verbose: bool=False) -> bool: 

236 """Using the ``engine`` object, test if the given table exists. 

237 

238 Args: 

239 table_name (str): Name of the table to test. 

240 verbose (bool, optional): Print a message if the table does 

241 not exist. Defaults to False. 

242 

243 Returns: 

244 bool: True if the given table exists, otherwise False. 

245 

246 """ 

247 params = {'schema': self._engine.url.database, 

248 'table_name': table_name} 

249 stmt = ('select count(*) from information_schema.tables ' 

250 'where table_schema = :schema ' 

251 'and table_name = :table_name') 

252 exists = bool(self.execute_query(stmt, params=params, raw=True)[0][0]) 

253 if (not exists) & verbose: 

254 msg = f'Table does not exist: {self._engine.url.database}.{table_name}' 

255 ui.print_warning(text=msg) 

256 return exists