1010
1111import json
1212import os
13- import re
1413import sys
1514import threading
15+ from contextlib import suppress
1616from ctypes import (
1717 CDLL ,
1818 POINTER ,
2929from ctypes .util import find_library
3030from functools import wraps
3131from types import TracebackType
32- from typing import Any , Callable , Dict , List , Optional , Type , TypeVar , Union
32+ from typing import Any , Callable , Dict , List , Optional , Tuple , Type , TypeVar , Union
3333
3434from senzing_abstract import ENGINE_EXCEPTION_MAP
3535
@@ -82,54 +82,63 @@ def __exit__(
8282# -----------------------------------------------------------------------------
8383
8484
85- # TODO Not just catching ctypes exceptions, now also catching entity/record json building exceptions
86- # TODO Check functions without try/except are caught by this
87- def catch_exceptions (func_to_decorate : Callable [P , T ]) -> Callable [P , T ]:
85+ def catch_non_sz_exceptions (func_to_decorate : Callable [P , T ]) -> Callable [P , T ]:
8886 """
89- # TODO
87+ The Python SDK methods convert Python types to ctypes and utilize helper functions. If incorrect types/values are
88+ used standard library exceptions are raised not SzError based exceptions as the Senzing library hasn't been called
89+ yet. Raise the original Python exception type and append information to identify the SDK method called, accepted
90+ arguments & types and the arguments and types the SDK method received.
9091
9192 :meta private:
9293 """
9394
9495 @wraps (func_to_decorate )
95- def catch_inner (* args : P .args , ** kwargs : P .kwargs ) -> T :
96- method_name = func_to_decorate .__name__
97- module_name = func_to_decorate .__module__
98- basic_msg = f"wrong type for an argument when calling { module_name } .{ method_name } "
99-
96+ def wrapped_func (* args : P .args , ** kwargs : P .kwargs ) -> T : # pylint: disable=too-many-locals
10097 try :
10198 return func_to_decorate (* args , ** kwargs )
102- except ArgumentError as err :
103- # Checking can find the information from ctypes.Argument error, works currently but could change in future?
104- # If can locate what we are looking for from ctypes.ArgumentError, give a more detailed and useful exception message
105- # Current message from ctypes: ctypes.ArgumentError: argument 2: TypeError: wrong type
106- bad_arg_match = None
107- if err .args :
108- bad_arg_match = re .match (r"argument (\d+):" , err .args [0 ])
109-
110- if bad_arg_match :
111- bad_arg_index = bad_arg_match .group (1 )
112- try :
113- bad_arg_index = int (bad_arg_index )
114- bad_arg_value = args [bad_arg_index ]
115- bad_arg_type = type (bad_arg_value )
116- bad_arg_tuple = list (func_to_decorate .__annotations__ .items ())[bad_arg_index - 1 ]
117- except (IndexError , ValueError ):
118- raise TypeError (basic_msg ) from err
119-
120- if len (bad_arg_tuple ) != 2 :
121- raise TypeError (basic_msg ) from err
122-
123- raise TypeError (
124- f"wrong type for argument { bad_arg_tuple [0 ]} , expected { bad_arg_tuple [1 ]} but received { bad_arg_type .__name__ } when calling { module_name } .{ method_name } "
125- ) from None
126-
127- raise TypeError (basic_msg ) from err
128- # NOTE Catch TypeError from the test in as_uintptr_t()
129- except TypeError as err :
130- raise TypeError (f"{ basic_msg } - { err } " ) from None
131-
132- return catch_inner
99+ # ctypes.ArgumentError from converting python types to ctypes before call to Senzing library
100+ except (ArgumentError , TypeError , ValueError , json .JSONDecodeError ) as err :
101+ annotations_dict = func_to_decorate .__annotations__
102+
103+ # Remove kwargs and return type, not needed initially
104+ with suppress (KeyError ):
105+ del annotations_dict ["return" ]
106+ del annotations_dict ["kwargs" ]
107+
108+ # Get the wrapped functions argument names and types and build a string of accepted arguments and types
109+ func_arg_names , func_arg_types = zip (* annotations_dict .items ())
110+ func_zip = list (zip (func_arg_names , func_arg_types ))
111+ accepts = ", " .join (
112+ [f"{ tup [0 ]} : { tup [1 ] if isinstance (tup [1 ], str ) else tup [1 ].__name__ } " for tup in func_zip ]
113+ )
114+
115+ # Get the values of the arguments received by the wrapped function and build a string of received arguments
116+ # and types
117+ received_arg_values = list (args [1 :])
118+ # If no kwargs, arguments are in order of the wrapped function signature, get the first x argument names
119+ if received_arg_values and not kwargs :
120+ func_arg_names = func_arg_names [: len (received_arg_values )]
121+
122+ if kwargs :
123+ kwargs_arg_names , kwargs_arg_values = zip (* kwargs .items ())
124+ # First get the names of all required arguments
125+ non_default_args : Tuple [Any ] = func_to_decorate .__defaults__ if func_to_decorate .__defaults__ else ()
126+ func_arg_names = func_arg_names [: len (func_arg_names ) - len (non_default_args )]
127+ # Add any kwarg arguments to the set of function arguments names
128+ func_arg_names = func_arg_names + kwargs_arg_names
129+ # Add the value of the kwargs
130+ received_arg_values .extend (list (kwargs_arg_values ))
131+ arg_zip = list (zip (func_arg_names , received_arg_values ))
132+ received = ", " .join ([f"{ tup [0 ]} : { type (tup [1 ]).__name__ } " for tup in arg_zip ])
133+
134+ err_msg = f"{ err } - { func_to_decorate .__module__ } .{ func_to_decorate .__name__ } accepts - { accepts } - but received - { received } "
135+
136+ # Convert ctypes ArgumentError to a TypeError for simplicity, a user shouldn't need ctypes in their code
137+ err_class = TypeError if err .__class__ .__name__ == "ArgumentError" else err .__class__
138+
139+ raise err_class (err_msg ) from err
140+
141+ return wrapped_func
133142
134143
135144# -----------------------------------------------------------------------------
@@ -153,7 +162,7 @@ def load_sz_library(lib: str = "") -> CDLL:
153162 f"ERROR: Unable to load the Senzing library: { err } \n "
154163 "ERROR: Did you remember to setup your environment by sourcing the setupEnv file?\n "
155164 "ERROR: For more information: https://senzing.zendesk.com/hc/en-us/articles/115002408867-Introduction-G2-Quickstart\n "
156- "ERROR: If you are running Ubuntu or Debian also review the ssl and crypto information at https://senzing.zendesk.com/hc/en-us/articles/115010259947-System-Requirements\n " ,
165+ "ERROR: If you are running Ubuntu or Debian also review the ssl and crypto information at https://senzing.zendesk.com/hc/en-us/articles/115010259947-System-Requirements" ,
157166 )
158167 raise sdk_exception (1 ) from err
159168
@@ -187,45 +196,6 @@ def check_result_rc(
187196# -----------------------------------------------------------------------------
188197
189198
190- def check_type_is_list (to_check : Any ) -> None :
191- """
192- Check the input type is a list, if not raise TypeError.
193-
194- :meta private:
195- """
196- if not isinstance (to_check , list ):
197- raise TypeError (f"expected type list, got { type (to_check ).__name__ } " )
198-
199-
200- def check_list_types (to_check : List [Any ]) -> None :
201- """
202- Check the elements of a list for:
203- - All the same type
204- - If a list of tuples
205- - The number of elements in each tuple is the same
206- - The number of elements in each tuple is of the expected size
207-
208- :meta private:
209- """
210- if not to_check :
211- return
212-
213- # Check all elements in the list are of the same type
214- types = all (isinstance (elem , type (to_check [0 ])) for elem in to_check [1 :])
215- if not types :
216- raise TypeError (f"elements in the list are not of the same type - { to_check } " )
217-
218- # If elements are tuples check they are the same size and correct size
219- if isinstance (to_check [0 ], tuple ):
220- num_elements = set (len (elem ) for elem in to_check )
221- if len (num_elements ) > 1 :
222- raise TypeError (f"number of tuple elements for each tuple are not of the same size - { to_check } " )
223-
224- number_of_tuples = num_elements .pop ()
225- if number_of_tuples != 2 :
226- raise TypeError (f"number of elements in a tuple is { number_of_tuples } , expected 2 - { to_check } " )
227-
228-
229199# TODO - Investigate adding and recalling is working correctly
230200def escape_json_str (to_escape : str ) -> str :
231201 """
@@ -234,7 +204,9 @@ def escape_json_str(to_escape: str) -> str:
234204 :meta private:
235205 """
236206 if not isinstance (to_escape , str ):
237- raise TypeError (f"expected a str, got{ to_escape } " )
207+ # TODO - Ant -
208+ raise TypeError ("expected a str" )
209+ # return to_escape
238210 # TODO ensure_ascii=False = èAnt\\n👍
239211 # TODO =True = \\u00e8Ant\\n\\ud83d\\udc4d'
240212 # print(f"{to_escape=}")
@@ -247,7 +219,9 @@ def build_dsrc_code_json(dsrc_code: str) -> str:
247219 """
248220 Build JSON string of single data source code.
249221
250- {"DSRC_CODE": "CUSTOMERS"}
222+ Input: CUSTOMERS
223+
224+ Output: {"DSRC_CODE": "CUSTOMERS"}
251225
252226 :meta private:
253227 """
@@ -258,56 +232,65 @@ def build_data_sources_json(dsrc_codes: list[str]) -> str:
258232 """
259233 Build JSON string of data source codes.
260234
261- {"DATA_SOURCES": ["REFERENCE", "CUSTOMERS"]}'
235+ Input: ["REFERENCE", "CUSTOMERS"]
236+
237+ Output: {"DATA_SOURCES": ["REFERENCE", "CUSTOMERS"]}'
262238
263239 :meta private:
264240 """
265- check_type_is_list ((dsrc_codes ))
266- check_list_types (dsrc_codes )
267- dsrcs = ", " .join ([f"{ escape_json_str (code )} " for code in dsrc_codes ])
241+
242+ try :
243+ dsrcs = ", " .join ([f"{ escape_json_str (code )} " for code in dsrc_codes ])
244+ except (TypeError , ValueError ) as err :
245+ raise err .__class__ (err )
246+
268247 return f"{ START_DSRC_JSON } { dsrcs } { END_JSON } "
269248
270249
271250def build_entities_json (entity_ids : Union [List [int ], None ]) -> str :
272251 """
273252 Build JSON string of entity ids.
274253
275- {"ENTITIES": [{"ENTITY_ID": 1}, {"ENTITY_ID": 100002}]}
254+ Input: [1, 100002]
255+
256+ Output: {"ENTITIES": [{"ENTITY_ID": 1}, {"ENTITY_ID": 100002}]}
276257
277258 :meta private:
278259 """
279- # NOTE This is needed if required_data_sources is sent to find_path_*, avoid_* could
280- # NOTE be set to None (default) or []
281- if not entity_ids or len (entity_ids ) == 0 :
260+ if not entity_ids or (isinstance (entity_ids , list ) and len (entity_ids ) == 0 ):
282261 return ""
283262
284- check_type_is_list (entity_ids )
285- check_list_types (entity_ids )
286- entities = ", " .join ([f'{{"ENTITY_ID": { id } }}' for id in entity_ids ])
263+ try :
264+ entities = ", " .join ([f'{{"ENTITY_ID": { id } }}' for id in entity_ids ])
265+ except (TypeError , ValueError ) as err :
266+ raise err .__class__ (err )
267+
287268 return f"{ START_ENTITIES_JSON } { entities } { END_JSON } "
288269
289270
290271def build_records_json (record_keys : Union [List [tuple [str , str ]], None ]) -> str :
291272 """
292273 Build JSON string of data source and record ids.
293274
294- {"RECORDS":[{"DATA_SOURCE":"CUSTOMERS","RECORD_ID":"1001"},{"DATA_SOURCE":"WATCHLIST","RECORD_ID":"1007"}]}
275+ From: [("CUSTOMERS", "1001"), ("WATCHLIST", 1007)]
276+
277+ To: {"RECORDS":[{"DATA_SOURCE":"CUSTOMERS","RECORD_ID":"1001"},{"DATA_SOURCE":"WATCHLIST","RECORD_ID":"1007"}]}
295278
296279 :meta private:
297280 """
298- # NOTE This is needed if required_data_sources is sent to find_path_*, avoid_* could
299- # NOTE be set to None (default) or []
300- if not record_keys or len (record_keys ) == 0 :
281+ if not record_keys or (isinstance (record_keys , list ) and len (record_keys ) == 0 ):
301282 return ""
302283
303- check_type_is_list (record_keys )
304- check_list_types (record_keys )
305- records = ", " .join (
306- [
307- f'{{"DATA_SOURCE": { escape_json_str (ds )} , "RECORD_ID": { escape_json_str (rec_id )} }}'
308- for ds , rec_id in record_keys
309- ]
310- )
284+ try :
285+ records = ", " .join (
286+ [
287+ f'{{"DATA_SOURCE": { escape_json_str (ds )} , "RECORD_ID": { escape_json_str (rec_id )} }}'
288+ for ds , rec_id in record_keys
289+ ]
290+ )
291+ except (TypeError , ValueError ) as err :
292+ raise err .__class__ (err )
293+
311294 return f"{ START_RECORDS_JSON } { records } { END_JSON } "
312295
313296
@@ -428,7 +411,7 @@ def engine_exception(
428411 get_last_exception_code : Callable [[], int ],
429412) -> Exception :
430413 """
431- Generate a new Senzing Exception based on the SDK product_id & error_id .
414+ Generate a Senzing error .
432415
433416 :meta private:
434417 """
0 commit comments