Skip to content

Commit 9fcc4a4

Browse files
committed
add cache to optimize type checking
1 parent dcf9568 commit 9fcc4a4

File tree

4 files changed

+320
-91
lines changed

4 files changed

+320
-91
lines changed

.generator/src/generator/templates/api_client.j2

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ class ApiClient:
5050

5151
self.rest_client = self._build_rest_client()
5252
self.default_headers = {}
53+
54+
# Cache for validation performance optimization - persists across requests
55+
# Simple size limiting to prevent memory leaks
56+
self._validation_cache: Dict[str, Any] = {}
57+
self._validation_cache_max_size = 1000 # Configurable limit
5358
if self.configuration.compress:
5459
self.default_headers["Accept-Encoding"] = "gzip"
5560
# Set default User-Agent.
@@ -178,8 +183,23 @@ class ApiClient:
178183

179184
# store our data under the key of 'received_data' so users have some
180185
# context if they are deserializing a string and the data type is wrong
186+
187+
# Use ApiClient's validation cache for performance optimization across requests
188+
request_cache = self._validation_cache if check_type else None
189+
190+
# Simple cache size limiting to prevent memory leaks
191+
if request_cache is not None and len(request_cache) > self._validation_cache_max_size:
192+
# Remove 25% of cache entries when full (keep most recent 75%)
193+
items_to_keep = int(self._validation_cache_max_size * 0.75)
194+
cache_items = list(request_cache.items())
195+
request_cache.clear()
196+
# Keep the most recently added items (simple FIFO)
197+
for key, value in cache_items[-items_to_keep:]:
198+
request_cache[key] = value
199+
181200
deserialized_data = validate_and_convert_types(
182-
received_data, response_type, ["received_data"], True, check_type, configuration=self.configuration
201+
received_data, response_type, ["received_data"], True, check_type,
202+
configuration=self.configuration, request_cache=request_cache
183203
)
184204
return deserialized_data
185205

@@ -682,6 +702,7 @@ class Endpoint:
682702
self.api_client.configuration.spec_property_naming,
683703
self.api_client.configuration.check_input_type,
684704
configuration=self.api_client.configuration,
705+
request_cache=None, # No cache available for input validation
685706
)
686707
kwargs[key] = fixed_val
687708

.generator/src/generator/templates/model_utils.j2

Lines changed: 129 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ from contextlib import suppress
44
from datetime import date, datetime
55
from uuid import UUID
66
import enum
7+
from functools import lru_cache
78
import inspect
89
import io
910
import os
@@ -28,6 +29,24 @@ file_type = io.IOBase
2829
empty_dict = MappingProxyType({}) # type: ignore
2930

3031

32+
def _make_hashable(obj):
33+
"""Convert potentially unhashable objects to hashable representations for caching."""
34+
if isinstance(obj, (list, tuple)):
35+
return tuple(_make_hashable(item) for item in obj)
36+
elif isinstance(obj, dict):
37+
return tuple(sorted((_make_hashable(k), _make_hashable(v)) for k, v in obj.items()))
38+
elif isinstance(obj, set):
39+
return tuple(sorted(_make_hashable(item) for item in obj))
40+
elif hasattr(obj, '__name__'): # Classes and functions
41+
return obj.__name__
42+
else:
43+
try:
44+
hash(obj)
45+
return obj
46+
except TypeError:
47+
return str(obj)
48+
49+
3150
class UnsetType(enum.Enum):
3251
unset = 0
3352

@@ -149,6 +168,7 @@ class OpenApiModel:
149168
self._spec_property_naming,
150169
self._check_type,
151170
configuration=self._configuration,
171+
request_cache=None, # No cache available in model __setattr__
152172
)
153173
if isinstance(value, list):
154174
for x in value:
@@ -873,7 +893,6 @@ def order_response_types(required_types):
873893
of list or dict with class information inside it.
874894
:rtype: list
875895
"""
876-
877896
def index_getter(class_or_instance):
878897
if isinstance(class_or_instance, list):
879898
return COERCION_INDEX_BY_TYPE[list]
@@ -890,31 +909,11 @@ def order_response_types(required_types):
890909
raise ApiValueError("Unsupported type: %s" % class_or_instance)
891910

892911
sorted_types = sorted(required_types, key=index_getter)
893-
return sorted_types
894-
912+
return tuple(sorted_types)
895913

896-
def remove_uncoercible(required_types_classes, current_item, spec_property_naming, must_convert=True):
897-
"""Only keeps the type conversions that are possible.
898-
899-
:param required_types_classes: Classes that are required, these should be
900-
ordered by COERCION_INDEX_BY_TYPE.
901-
:type required_types_classes: tuple
902-
:param spec_property_naming: True if the variable names in the input data
903-
are serialized names as specified in the OpenAPI document. False if the
904-
variables names in the input data are python variable names in PEP-8 snake
905-
case.
906-
:type spec_property_naming: bool
907-
:param current_item: The current item (input data) to be converted.
908-
909-
:param must_convert: If True the item to convert is of the wrong type and
910-
we want a big list of coercibles if False, we want a limited list of coercibles.
911-
:type must_convert: bool
912-
913-
:return: The remaining coercible required types, classes only.
914-
:rtype: list
915-
"""
916-
current_type_simple = get_simple_class(current_item)
917914

915+
def _remove_uncoercible_impl(required_types_classes, current_type_simple, spec_property_naming, must_convert=True):
916+
"""Implementation of remove_uncoercible logic."""
918917
results_classes = []
919918
for required_type_class in required_types_classes:
920919
# convert our models to OpenApiModel
@@ -936,7 +935,31 @@ def remove_uncoercible(required_types_classes, current_item, spec_property_namin
936935
results_classes.append(required_type_class)
937936
elif class_pair in UPCONVERSION_TYPE_PAIRS:
938937
results_classes.append(required_type_class)
939-
return results_classes
938+
return tuple(results_classes)
939+
940+
941+
def remove_uncoercible(required_types_classes, current_item, spec_property_naming, must_convert=True):
942+
"""Only keeps the type conversions that are possible.
943+
944+
:param required_types_classes: Classes that are required, these should be
945+
ordered by COERCION_INDEX_BY_TYPE.
946+
:type required_types_classes: tuple
947+
:param spec_property_naming: True if the variable names in the input data
948+
are serialized names as specified in the OpenAPI document. False if the
949+
variables names in the input data are python variable names in PEP-8 snake
950+
case.
951+
:type spec_property_naming: bool
952+
:param current_item: The current item (input data) to be converted.
953+
954+
:param must_convert: If True the item to convert is of the wrong type and
955+
we want a big list of coercibles if False, we want a limited list of coercibles.
956+
:type must_convert: bool
957+
958+
:return: The remaining coercible required types, classes only.
959+
:rtype: list
960+
"""
961+
current_type_simple = get_simple_class(current_item)
962+
return list(_remove_uncoercible_impl(required_types_classes, current_type_simple, spec_property_naming, must_convert))
940963

941964

942965
def get_possible_classes(cls, from_server_context):
@@ -948,7 +971,7 @@ def get_possible_classes(cls, from_server_context):
948971
return possible_classes
949972

950973

951-
def get_required_type_classes(required_types_mixed, spec_property_naming):
974+
def get_required_type_classes(required_types_mixed, spec_property_naming, request_cache=None):
952975
"""Converts the tuple required_types into a tuple and a dict described below.
953976

954977
:param required_types_mixed: Will contain either classes or instance of
@@ -968,6 +991,23 @@ def get_required_type_classes(required_types_mixed, spec_property_naming):
968991

969992
:rtype: tuple
970993
"""
994+
# PERFORMANCE: Cache expensive type class computation within request
995+
if request_cache is not None:
996+
cache_key = ('get_required_type_classes', _make_hashable(required_types_mixed), spec_property_naming)
997+
if cache_key in request_cache:
998+
return request_cache[cache_key]
999+
else:
1000+
cache_key = None
1001+
1002+
result = _get_required_type_classes_impl(required_types_mixed, spec_property_naming)
1003+
1004+
if cache_key and request_cache is not None:
1005+
request_cache[cache_key] = result
1006+
return result
1007+
1008+
1009+
def _get_required_type_classes_impl(required_types_mixed, spec_property_naming):
1010+
"""Implementation of get_required_type_classes without caching."""
9711011
valid_classes = []
9721012
child_req_types_by_current_type = {}
9731013
for required_type in required_types_mixed:
@@ -1167,6 +1207,7 @@ def attempt_convert_item(
11671207
key_type=False,
11681208
must_convert=False,
11691209
check_type=True,
1210+
request_cache=None,
11701211
):
11711212
"""
11721213
:param input_value: The data to convert.
@@ -1265,7 +1306,7 @@ def is_valid_type(input_class_simple, valid_classes):
12651306

12661307

12671308
def validate_and_convert_types(
1268-
input_value, required_types_mixed, path_to_item, spec_property_naming, check_type, configuration=None
1309+
input_value, required_types_mixed, path_to_item, spec_property_naming, check_type, configuration=None, request_cache=None
12691310
):
12701311
"""Raises a TypeError is there is a problem, otherwise returns value.
12711312

@@ -1287,27 +1328,46 @@ def validate_and_convert_types(
12871328
:param configuration:: The configuration class to use when converting
12881329
file_type items.
12891330
:type configuration: Configuration
1331+
:param request_cache: Optional cache dict for storing validation results
1332+
within a single request to avoid redundant validations.
1333+
:type request_cache: dict
12901334

12911335
:return: The correctly typed value.
12921336

12931337
:raise: ApiTypeError
12941338
"""
1295-
results = get_required_type_classes(required_types_mixed, spec_property_naming)
1339+
# Per-request caching: Cache validation results within a single request
1340+
cache_key = None
1341+
if request_cache is not None:
1342+
try:
1343+
input_hash = _make_hashable(input_value)
1344+
cache_key = (input_hash, _make_hashable(required_types_mixed), tuple(path_to_item), spec_property_naming, check_type)
1345+
if cache_key in request_cache:
1346+
return request_cache[cache_key]
1347+
except (TypeError, AttributeError):
1348+
# If we can't create a cache key, proceed without caching
1349+
cache_key = None
1350+
1351+
results = get_required_type_classes(required_types_mixed, spec_property_naming, request_cache)
12961352
valid_classes, child_req_types_by_current_type = results
12971353

12981354
input_class_simple = get_simple_class(input_value)
12991355
valid_type = is_valid_type(input_class_simple, valid_classes)
13001356
if not valid_type:
13011357
# if input_value is not valid_type try to convert it
1302-
return attempt_convert_item(
1358+
result = attempt_convert_item(
13031359
input_value,
13041360
valid_classes,
13051361
path_to_item,
13061362
configuration,
13071363
spec_property_naming,
13081364
must_convert=True,
13091365
check_type=check_type,
1366+
request_cache=request_cache,
13101367
)
1368+
if cache_key and request_cache is not None:
1369+
request_cache[cache_key] = result
1370+
return result
13111371

13121372
# input_value's type is in valid_classes
13131373
if len(valid_classes) > 1 and configuration:
@@ -1316,64 +1376,87 @@ def validate_and_convert_types(
13161376
valid_classes, input_value, spec_property_naming, must_convert=False
13171377
)
13181378
if valid_classes_coercible:
1319-
return attempt_convert_item(
1379+
result = attempt_convert_item(
13201380
input_value,
13211381
valid_classes_coercible,
13221382
path_to_item,
13231383
configuration,
13241384
spec_property_naming,
13251385
check_type=check_type,
1386+
request_cache=request_cache,
13261387
)
1388+
if cache_key and request_cache is not None:
1389+
request_cache[cache_key] = result
1390+
return result
13271391

13281392
if child_req_types_by_current_type == {}:
13291393
# all types are of the required types and there are no more inner
13301394
# variables left to look at
1395+
if cache_key and request_cache is not None:
1396+
request_cache[cache_key] = input_value
13311397
return input_value
13321398
inner_required_types = child_req_types_by_current_type.get(type(input_value))
13331399
if inner_required_types is None:
13341400
# for this type, there are not more inner variables left to look at
1401+
if cache_key and request_cache is not None:
1402+
request_cache[cache_key] = input_value
13351403
return input_value
13361404
if isinstance(input_value, list):
13371405
if input_value == []:
13381406
# allow an empty list
13391407
return input_value
13401408
result = []
13411409
for index, inner_value in enumerate(input_value):
1342-
inner_path = list(path_to_item)
1343-
inner_path.append(index)
1410+
path_to_item.append(index)
13441411
try:
13451412
result.append(
13461413
validate_and_convert_types(
13471414
inner_value,
13481415
inner_required_types,
1349-
inner_path,
1416+
path_to_item,
13501417
spec_property_naming,
13511418
check_type,
13521419
configuration=configuration,
1420+
request_cache=request_cache,
13531421
)
13541422
)
13551423
except TypeError:
13561424
result.append(UnparsedObject(**inner_value))
1425+
finally:
1426+
# Restore path state
1427+
path_to_item.pop()
1428+
if cache_key and request_cache is not None:
1429+
request_cache[cache_key] = result
13571430
return result
13581431
elif isinstance(input_value, dict):
13591432
if input_value == {}:
13601433
# allow an empty dict
1434+
if cache_key and request_cache is not None:
1435+
request_cache[cache_key] = input_value
13611436
return input_value
13621437
result = {}
13631438
for inner_key, inner_val in input_value.items():
1364-
inner_path = list(path_to_item)
1365-
inner_path.append(inner_key)
1366-
if get_simple_class(inner_key) != str:
1367-
raise get_type_error(inner_key, inner_path, valid_classes, key_type=True)
1368-
result[inner_key] = validate_and_convert_types(
1369-
inner_val,
1370-
inner_required_types,
1371-
inner_path,
1372-
spec_property_naming,
1373-
check_type,
1374-
configuration=configuration,
1375-
)
1439+
path_to_item.append(inner_key)
1440+
try:
1441+
if get_simple_class(inner_key) != str:
1442+
raise get_type_error(inner_key, path_to_item, valid_classes, key_type=True)
1443+
result[inner_key] = validate_and_convert_types(
1444+
inner_val,
1445+
inner_required_types,
1446+
path_to_item,
1447+
spec_property_naming,
1448+
check_type,
1449+
configuration=configuration,
1450+
request_cache=request_cache,
1451+
)
1452+
finally:
1453+
# Restore path state
1454+
path_to_item.pop()
1455+
if cache_key and request_cache is not None:
1456+
request_cache[cache_key] = result
13761457
return result
1458+
if cache_key and request_cache is not None:
1459+
request_cache[cache_key] = input_value
13771460
return input_value
13781461

13791462

@@ -1611,6 +1694,7 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None):
16111694
constant_kwargs.get("_spec_property_naming", False),
16121695
constant_kwargs.get("_check_type", True),
16131696
configuration=constant_kwargs.get("_configuration"),
1697+
request_cache=None, # No cache available in this context
16141698
)
16151699
oneof_instances.append(oneof_instance)
16161700
if len(oneof_instances) != 1:

0 commit comments

Comments
 (0)