Skip to content

Commit 0cef5d6

Browse files
authored
118 ant 1 (#119)
* #118 - Save point * #118 - Save point * #118 - Save point * #118 - Save point * #118 - Save point * #118 - Save point * #118 - Save point * #118 - Update setup and changelog
1 parent c80c052 commit 0cef5d6

9 files changed

Lines changed: 134 additions & 183 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning].
77

88
## [Unreleased]
99

10+
## [0.2.1] - 2024-11-22
11+
12+
### Added in 0.2.1.
13+
14+
- Improvements to helpers
15+
1016
## [0.2.0] - 2024-11-04
1117

1218
### Deleted in 0.2.0.

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = senzing_core
3-
version = 0.2.0
3+
version = 0.2.1
44
author = senzing
55
author_email = support@senzing.com
66
description = Python SDK for Senzing API

src/senzing/_helpers.py

Lines changed: 91 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010

1111
import json
1212
import os
13-
import re
1413
import sys
1514
import threading
15+
from contextlib import suppress
1616
from ctypes import (
1717
CDLL,
1818
POINTER,
@@ -29,7 +29,7 @@
2929
from ctypes.util import find_library
3030
from functools import wraps
3131
from 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

3434
from 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
230200
def 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

271250
def 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

290271
def 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
"""

src/senzing/szconfig.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
as_python_str,
3030
as_str,
3131
build_dsrc_code_json,
32-
catch_exceptions,
32+
catch_non_sz_exceptions,
3333
check_result_rc,
3434
load_sz_library,
3535
)
@@ -184,7 +184,7 @@ def __del__(self) -> None:
184184
# SzConfig methods
185185
# -------------------------------------------------------------------------
186186

187-
@catch_exceptions
187+
@catch_non_sz_exceptions
188188
def add_data_source(
189189
self,
190190
config_handle: int,
@@ -200,7 +200,7 @@ def add_data_source(
200200
self.check_result(result.return_code)
201201
return as_python_str(result.response)
202202

203-
@catch_exceptions
203+
@catch_non_sz_exceptions
204204
def close_config(self, config_handle: int, **kwargs: Any) -> None:
205205
result = self.library_handle.SzConfig_close_helper(as_c_uintptr_t(config_handle))
206206
self.check_result(result)
@@ -210,7 +210,7 @@ def create_config(self, **kwargs: Any) -> int:
210210
self.check_result(result.return_code)
211211
return result.response # type: ignore[no-any-return]
212212

213-
@catch_exceptions
213+
@catch_non_sz_exceptions
214214
def delete_data_source(
215215
self,
216216
config_handle: int,
@@ -226,21 +226,21 @@ def delete_data_source(
226226
def _destroy(self, **kwargs: Any) -> None:
227227
_ = self.library_handle.SzConfig_destroy()
228228

229-
@catch_exceptions
229+
@catch_non_sz_exceptions
230230
def export_config(self, config_handle: int, **kwargs: Any) -> str:
231231
result = self.library_handle.SzConfig_save_helper(as_c_uintptr_t(config_handle))
232232
with FreeCResources(self.library_handle, result.response):
233233
self.check_result(result.return_code)
234234
return as_python_str(result.response)
235235

236-
@catch_exceptions
236+
@catch_non_sz_exceptions
237237
def get_data_sources(self, config_handle: int, **kwargs: Any) -> str:
238238
result = self.library_handle.SzConfig_listDataSources_helper(as_c_uintptr_t(config_handle))
239239
with FreeCResources(self.library_handle, result.response):
240240
self.check_result(result.return_code)
241241
return as_python_str(result.response)
242242

243-
@catch_exceptions
243+
@catch_non_sz_exceptions
244244
def _initialize(
245245
self,
246246
instance_name: str,
@@ -255,7 +255,7 @@ def _initialize(
255255
)
256256
self.check_result(result)
257257

258-
@catch_exceptions
258+
@catch_non_sz_exceptions
259259
def import_config(self, config_definition: str, **kwargs: Any) -> int:
260260
result = self.library_handle.SzConfig_load_helper(as_c_char_p(config_definition))
261261
self.check_result(result.return_code)

0 commit comments

Comments
 (0)