Skip to content

Commit 56f40a7

Browse files
committed
add cache to optimize type checking
1 parent 5453d0b commit 56f40a7

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 = {}
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

@@ -146,6 +165,7 @@ class OpenApiModel:
146165
self._spec_property_naming,
147166
self._check_type,
148167
configuration=self._configuration,
168+
request_cache=None, # No cache available in model __setattr__
149169
)
150170
if isinstance(value, list):
151171
for x in value:
@@ -870,7 +890,6 @@ def order_response_types(required_types):
870890
of list or dict with class information inside it.
871891
:rtype: list
872892
"""
873-
874893
def index_getter(class_or_instance):
875894
if isinstance(class_or_instance, list):
876895
return COERCION_INDEX_BY_TYPE[list]
@@ -887,31 +906,11 @@ def order_response_types(required_types):
887906
raise ApiValueError("Unsupported type: %s" % class_or_instance)
888907

889908
sorted_types = sorted(required_types, key=index_getter)
890-
return sorted_types
891-
909+
return tuple(sorted_types)
892910

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

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

938961

939962
def get_possible_classes(cls, from_server_context):
@@ -945,7 +968,7 @@ def get_possible_classes(cls, from_server_context):
945968
return possible_classes
946969

947970

948-
def get_required_type_classes(required_types_mixed, spec_property_naming):
971+
def get_required_type_classes(required_types_mixed, spec_property_naming, request_cache=None):
949972
"""Converts the tuple required_types into a tuple and a dict described below.
950973

951974
:param required_types_mixed: Will contain either classes or instance of
@@ -965,6 +988,23 @@ def get_required_type_classes(required_types_mixed, spec_property_naming):
965988

966989
:rtype: tuple
967990
"""
991+
# PERFORMANCE: Cache expensive type class computation within request
992+
if request_cache is not None:
993+
cache_key = ('get_required_type_classes', _make_hashable(required_types_mixed), spec_property_naming)
994+
if cache_key in request_cache:
995+
return request_cache[cache_key]
996+
else:
997+
cache_key = None
998+
999+
result = _get_required_type_classes_impl(required_types_mixed, spec_property_naming)
1000+
1001+
if cache_key and request_cache is not None:
1002+
request_cache[cache_key] = result
1003+
return result
1004+
1005+
1006+
def _get_required_type_classes_impl(required_types_mixed, spec_property_naming):
1007+
"""Implementation of get_required_type_classes without caching."""
9681008
valid_classes = []
9691009
child_req_types_by_current_type = {}
9701010
for required_type in required_types_mixed:
@@ -1164,6 +1204,7 @@ def attempt_convert_item(
11641204
key_type=False,
11651205
must_convert=False,
11661206
check_type=True,
1207+
request_cache=None,
11671208
):
11681209
"""
11691210
:param input_value: The data to convert.
@@ -1262,7 +1303,7 @@ def is_valid_type(input_class_simple, valid_classes):
12621303

12631304

12641305
def validate_and_convert_types(
1265-
input_value, required_types_mixed, path_to_item, spec_property_naming, check_type, configuration=None
1306+
input_value, required_types_mixed, path_to_item, spec_property_naming, check_type, configuration=None, request_cache=None
12661307
):
12671308
"""Raises a TypeError is there is a problem, otherwise returns value.
12681309

@@ -1284,27 +1325,46 @@ def validate_and_convert_types(
12841325
:param configuration:: The configuration class to use when converting
12851326
file_type items.
12861327
:type configuration: Configuration
1328+
:param request_cache: Optional cache dict for storing validation results
1329+
within a single request to avoid redundant validations.
1330+
:type request_cache: dict
12871331

12881332
:return: The correctly typed value.
12891333

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

12951351
input_class_simple = get_simple_class(input_value)
12961352
valid_type = is_valid_type(input_class_simple, valid_classes)
12971353
if not valid_type:
12981354
# if input_value is not valid_type try to convert it
1299-
return attempt_convert_item(
1355+
result = attempt_convert_item(
13001356
input_value,
13011357
valid_classes,
13021358
path_to_item,
13031359
configuration,
13041360
spec_property_naming,
13051361
must_convert=True,
13061362
check_type=check_type,
1363+
request_cache=request_cache,
13071364
)
1365+
if cache_key and request_cache is not None:
1366+
request_cache[cache_key] = result
1367+
return result
13081368

13091369
# input_value's type is in valid_classes
13101370
if len(valid_classes) > 1 and configuration:
@@ -1313,64 +1373,87 @@ def validate_and_convert_types(
13131373
valid_classes, input_value, spec_property_naming, must_convert=False
13141374
)
13151375
if valid_classes_coercible:
1316-
return attempt_convert_item(
1376+
result = attempt_convert_item(
13171377
input_value,
13181378
valid_classes_coercible,
13191379
path_to_item,
13201380
configuration,
13211381
spec_property_naming,
13221382
check_type=check_type,
1383+
request_cache=request_cache,
13231384
)
1385+
if cache_key and request_cache is not None:
1386+
request_cache[cache_key] = result
1387+
return result
13241388

13251389
if child_req_types_by_current_type == {}:
13261390
# all types are of the required types and there are no more inner
13271391
# variables left to look at
1392+
if cache_key and request_cache is not None:
1393+
request_cache[cache_key] = input_value
13281394
return input_value
13291395
inner_required_types = child_req_types_by_current_type.get(type(input_value))
13301396
if inner_required_types is None:
13311397
# for this type, there are not more inner variables left to look at
1398+
if cache_key and request_cache is not None:
1399+
request_cache[cache_key] = input_value
13321400
return input_value
13331401
if isinstance(input_value, list):
13341402
if input_value == []:
13351403
# allow an empty list
13361404
return input_value
13371405
result = []
13381406
for index, inner_value in enumerate(input_value):
1339-
inner_path = list(path_to_item)
1340-
inner_path.append(index)
1407+
path_to_item.append(index)
13411408
try:
13421409
result.append(
13431410
validate_and_convert_types(
13441411
inner_value,
13451412
inner_required_types,
1346-
inner_path,
1413+
path_to_item,
13471414
spec_property_naming,
13481415
check_type,
13491416
configuration=configuration,
1417+
request_cache=request_cache,
13501418
)
13511419
)
13521420
except TypeError:
13531421
result.append(UnparsedObject(**inner_value))
1422+
finally:
1423+
# Restore path state
1424+
path_to_item.pop()
1425+
if cache_key and request_cache is not None:
1426+
request_cache[cache_key] = result
13541427
return result
13551428
elif isinstance(input_value, dict):
13561429
if input_value == {}:
13571430
# allow an empty dict
1431+
if cache_key and request_cache is not None:
1432+
request_cache[cache_key] = input_value
13581433
return input_value
13591434
result = {}
13601435
for inner_key, inner_val in input_value.items():
1361-
inner_path = list(path_to_item)
1362-
inner_path.append(inner_key)
1363-
if get_simple_class(inner_key) != str:
1364-
raise get_type_error(inner_key, inner_path, valid_classes, key_type=True)
1365-
result[inner_key] = validate_and_convert_types(
1366-
inner_val,
1367-
inner_required_types,
1368-
inner_path,
1369-
spec_property_naming,
1370-
check_type,
1371-
configuration=configuration,
1372-
)
1436+
path_to_item.append(inner_key)
1437+
try:
1438+
if get_simple_class(inner_key) != str:
1439+
raise get_type_error(inner_key, path_to_item, valid_classes, key_type=True)
1440+
result[inner_key] = validate_and_convert_types(
1441+
inner_val,
1442+
inner_required_types,
1443+
path_to_item,
1444+
spec_property_naming,
1445+
check_type,
1446+
configuration=configuration,
1447+
request_cache=request_cache,
1448+
)
1449+
finally:
1450+
# Restore path state
1451+
path_to_item.pop()
1452+
if cache_key and request_cache is not None:
1453+
request_cache[cache_key] = result
13731454
return result
1455+
if cache_key and request_cache is not None:
1456+
request_cache[cache_key] = input_value
13741457
return input_value
13751458

13761459

@@ -1581,6 +1664,7 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None):
15811664
constant_kwargs.get("_spec_property_naming", False),
15821665
constant_kwargs.get("_check_type", True),
15831666
configuration=constant_kwargs.get("_configuration"),
1667+
request_cache=None, # No cache available in this context
15841668
)
15851669
oneof_instances.append(oneof_instance)
15861670
if len(oneof_instances) != 1:

0 commit comments

Comments
 (0)