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

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

26 import pandas as pd

27 import warnings

28 from mysql.connector.errors import IntegrityError

29 from sqlalchemy.exc import SQLAlchemyError

30 from utils4.reporterror import reporterror

31 from utils4.user_interface import ui

32# locals

33 from _dbi_base import _DBIBase

34

35

36 class _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 dblib.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 # Added in s3ddb v0.7.0.dev1:

98 # Updated to use a context manager in an attempt to

99 # alleviate the '2055 Lost Connection' and

100 # System Error 32 BrokenPipeError.

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

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

103 cur.callproc(proc, params)

104 result = cur.stored_results()

105 cur.close()

106 df = self._result_to_df__stored(result=result)

107 success = not df.empty

108 except SQLAlchemyError as err:

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

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

111 except Exception as err:

112 reporterror(error=err)

113 return (df, success) if return_status else df

114

115 def call_procedure_update(self,

116 proc: str,

117 params: list=None,

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

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

120

121 Note:

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

123 status flag and the optional last row ID.

124

125 If results are desired, please use the

126 :meth:~`call_procedure` method.

127

128 Args:

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

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

131 the USP. Defaults to None.

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

133 inserted row. Defaults to False.

134

135 Returns:

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

137 returned if the procedure completed successfully, otherwise

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

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

140 are returned as::

141

142 (id, success_flag)

143

144 """

145 try:

146 rowid = None

147 success = False

148 # Added in s3ddb v0.7.0.dev1:

149 # Updated to use a context manager in an attempt to

150 # alleviate the '2055 Lost Connection' and

151 # System Error 32 BrokenPipeError.

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

153 cur = conn.connection.cursor()

154 cur.callproc(proc, params)

155 conn.connection.connection.commit()

156 if return_id:

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

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

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

160 # obtained here:

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

162 rowid = cur.fetchone()[0]

163 cur.close()

164 success = True

165 except IntegrityError as ierr:

166 # Duplicate entry: errno = 1062

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

168 ui.print_alert(text=msg)

169 except Exception as err:

170 reporterror(err)

171 return (rowid, success) if return_id else success

172

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

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

175

176 Note:

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

178

179 \*args, iterable_item

180

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

182 the *last* parameter.

183

184 Args:

185 *args (Union[str, int, float]): Positional arguments to be

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

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

188 order received, followed by the iterable item.

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

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

191 the database.

192

193 Returns:

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

195

196 """

197 try:

198 success = False

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

200 cur = conn.connection.cursor()

201 for i in iterable:

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

203 conn.connection.connection.commit()

204 cur.close()

205 success = True

206 except Exception as err:

207 reporterror(err)

208 return success

209

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

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

212 handling.

213

214 .. warning::

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

216 :meth:`~call_procedure_update` instead.

217

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

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

220 control the errors.

221

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

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

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

225

226 Args:

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

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

229 the USP. Defaults to None.

230

231 """

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

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

234 cur.callproc(proc, params)

235 conn.connection.connection.commit()

236 cur.close()

237

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

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

240

241 Args:

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

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

244 not exist. Defaults to False.

245

246 Returns:

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

248

249 """

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

251 'table_name': table_name}

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

253 'where table_schema = :schema '

254 'and table_name = :table_name')

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

256 if (not exists) & verbose:

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

258 ui.print_warning(text=msg)

259 return exists