Skip to content

Commit 6ad86d2

Browse files
committed
FEATURE: Add client side caching and pre-loading support
1 parent 915e297 commit 6ad86d2

File tree

11 files changed

+338
-97
lines changed

11 files changed

+338
-97
lines changed

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ classifiers = [
2424
]
2525

2626

27-
# FIXME: add ansys-api-edb version
2827
dependencies = [
29-
"ansys-api-edb==1.0.10",
28+
"ansys-api-edb==1.0.11",
3029
"protobuf>=3.19.3,<5",
31-
"grpcio>=1.44.0"
30+
"grpcio>=1.44.0",
31+
"Django>=4.2.16"
3232
]
3333

3434
[project.urls]

src/ansys/edb/core/inner/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from ansys.api.edb.v1.edb_messages_pb2 import EDBObjMessage
44

5+
from ansys.edb.core.utility.cache import get_cache
6+
57

68
class ObjBase:
79
"""Provides the base object that all gRPC-related models extend from."""
@@ -14,6 +16,9 @@ def __init__(self, msg):
1416
msg : EDBObjMessage
1517
"""
1618
self._id = 0 if msg is None else msg.id
19+
cache = get_cache()
20+
if cache is not None:
21+
cache.add_from_cache_msg(msg.cache)
1722

1823
@property
1924
def is_null(self):

src/ansys/edb/core/inner/conn_obj.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ def get_client_prim_type_from_class():
4848
return client_obj
4949
return cls(None)
5050

51+
@property
52+
def obj_type(self):
53+
""":class:`LayoutObjType <ansys.edb.core.edb_defs.LayoutObjType>`: Layout object type.
54+
55+
This property is read-only.
56+
"""
57+
if self.layout_obj_type != LayoutObjType.INVALID_LAYOUT_OBJ:
58+
return super().obj_type
59+
else:
60+
return LayoutObjType(self.__stub.GetObjType(self.msg).type)
61+
5162
@classmethod
5263
def find_by_id(cls, layout, uid):
5364
"""Find a :term:`Connectable` object by database ID in a given layout.

src/ansys/edb/core/inner/factory.py

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,65 @@
11
"""This module allows for the creating of objects while avoid circular imports."""
22

33
from ansys.edb.core.edb_defs import LayoutObjType
4-
from ansys.edb.core.hierarchy import cell_instance
5-
from ansys.edb.core.hierarchy.group import Group
6-
from ansys.edb.core.hierarchy.pin_group import PinGroup
7-
from ansys.edb.core.layout import voltage_regulator
8-
from ansys.edb.core.primitive.primitive import PadstackInstance, Primitive
9-
from ansys.edb.core.session import StubAccessor, StubType
10-
from ansys.edb.core.terminal.terminals import Terminal, TerminalInstance
4+
from ansys.edb.core.inner.conn_obj import ConnObj
5+
6+
_type_creator_params_dict = None
7+
8+
9+
class _CreatorParams:
10+
def __init__(self, obj_type, do_cast=False):
11+
self.obj_type = obj_type
12+
self.do_cast = do_cast
13+
14+
15+
def _initialize_type_creator_params_dict():
16+
global _type_creator_params_dict
17+
18+
from ansys.edb.core.hierarchy.cell_instance import CellInstance
19+
from ansys.edb.core.hierarchy.group import Group
20+
from ansys.edb.core.hierarchy.pin_group import PinGroup
21+
from ansys.edb.core.layout.voltage_regulator import VoltageRegulator
22+
from ansys.edb.core.net.differential_pair import DifferentialPair
23+
from ansys.edb.core.net.extended_net import ExtendedNet
24+
from ansys.edb.core.net.net import Net
25+
from ansys.edb.core.net.net_class import NetClass
26+
from ansys.edb.core.primitive.primitive import PadstackInstance, Primitive
27+
from ansys.edb.core.terminal.terminals import Terminal, TerminalInstance
28+
29+
_type_creator_params_dict = {
30+
LayoutObjType.PRIMITIVE: _CreatorParams(Primitive, True),
31+
LayoutObjType.PADSTACK_INSTANCE: _CreatorParams(PadstackInstance),
32+
LayoutObjType.TERMINAL: _CreatorParams(Terminal, True),
33+
LayoutObjType.TERMINAL_INSTANCE: _CreatorParams(TerminalInstance),
34+
LayoutObjType.CELL_INSTANCE: _CreatorParams(CellInstance),
35+
LayoutObjType.GROUP: _CreatorParams(Group, True),
36+
LayoutObjType.PIN_GROUP: _CreatorParams(PinGroup),
37+
LayoutObjType.VOLTAGE_REGULATOR: _CreatorParams(VoltageRegulator),
38+
LayoutObjType.NET_CLASS: _CreatorParams(NetClass),
39+
LayoutObjType.EXTENDED_NET: _CreatorParams(ExtendedNet),
40+
LayoutObjType.DIFFERENTIAL_PAIR: _CreatorParams(DifferentialPair),
41+
LayoutObjType.NET: _CreatorParams(Net),
42+
}
43+
44+
45+
def _get_type_creator_dict():
46+
if _type_creator_params_dict is None:
47+
_initialize_type_creator_params_dict()
48+
return _type_creator_params_dict
49+
50+
51+
def create_obj(msg, obj_type, do_cast):
52+
"""Create an object from the provided message of the provided type."""
53+
obj = obj_type(msg)
54+
if do_cast:
55+
obj = obj.cast()
56+
return obj
57+
58+
59+
def create_lyt_obj(msg, lyt_obj_type):
60+
"""Create a layout object from the provided message of the type corresponding to the provided layout object type."""
61+
params = _get_type_creator_dict()[lyt_obj_type]
62+
return create_obj(msg, params.obj_type, params.do_cast)
1163

1264

1365
def create_conn_obj(msg):
@@ -21,21 +73,4 @@ def create_conn_obj(msg):
2173
-------
2274
ansys.edb.core.inner.ConnObj
2375
"""
24-
type = LayoutObjType(StubAccessor(StubType.connectable).__get__().GetObjType(msg).type)
25-
if type == LayoutObjType.PRIMITIVE:
26-
return Primitive(msg).cast()
27-
elif type == LayoutObjType.PADSTACK_INSTANCE:
28-
return PadstackInstance(msg)
29-
elif type == LayoutObjType.TERMINAL:
30-
return Terminal(msg).cast()
31-
elif type == LayoutObjType.TERMINAL_INSTANCE:
32-
return TerminalInstance(msg)
33-
elif type == LayoutObjType.CELL_INSTANCE:
34-
return cell_instance.CellInstance(msg)
35-
elif type == LayoutObjType.GROUP:
36-
return Group(msg).cast()
37-
elif type == LayoutObjType.PIN_GROUP:
38-
return PinGroup(msg)
39-
elif type == LayoutObjType.VOLTAGE_REGULATOR:
40-
return voltage_regulator.VoltageRegulator(msg)
41-
raise TypeError("Encountered an unknown layout object type.")
76+
return create_lyt_obj(msg, ConnObj(msg).obj_type)

src/ansys/edb/core/inner/interceptors.py

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
"""Client-side gRPC interceptors."""
22

33
import abc
4+
from collections import namedtuple
45
import logging
56

6-
from grpc import StatusCode, UnaryUnaryClientInterceptor
7+
from grpc import (
8+
ClientCallDetails,
9+
StatusCode,
10+
UnaryStreamClientInterceptor,
11+
UnaryUnaryClientInterceptor,
12+
)
713

814
from ansys.edb.core.inner.exceptions import EDBSessionException, ErrorCode, InvalidArgumentException
15+
from ansys.edb.core.utility.cache import get_cache
916

1017

11-
class Interceptor(UnaryUnaryClientInterceptor, metaclass=abc.ABCMeta):
18+
class Interceptor(UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor, metaclass=abc.ABCMeta):
1219
"""Provides the base interceptor class."""
1320

1421
def __init__(self, logger):
@@ -20,14 +27,21 @@ def __init__(self, logger):
2027
def _post_process(self, response):
2128
pass
2229

30+
def _continue_unary_unary(self, continuation, client_call_details, request):
31+
return continuation(client_call_details, request)
32+
2333
def intercept_unary_unary(self, continuation, client_call_details, request):
2434
"""Intercept a gRPC call."""
25-
response = continuation(client_call_details, request)
35+
response = self._continue_unary_unary(continuation, client_call_details, request)
2636

2737
self._post_process(response)
2838

2939
return response
3040

41+
def intercept_unary_stream(self, continuation, client_call_details, request):
42+
"""Intercept a gRPC streaming call."""
43+
return continuation(client_call_details, request)
44+
3145

3246
class LoggingInterceptor(Interceptor):
3347
"""Logs EDB errors on each request."""
@@ -76,3 +90,78 @@ def _post_process(self, response):
7690

7791
if exception is not None:
7892
raise exception
93+
94+
95+
class CachingInterceptor(Interceptor):
96+
"""Returns cached values if a given request has already been made and caching is enabled."""
97+
98+
def __init__(self, logger, rpc_counter):
99+
"""Initialize a caching interceptor with a logger and rpc counter."""
100+
super().__init__(logger)
101+
self._rpc_counter = rpc_counter
102+
self._reset_cache_entry_data()
103+
104+
def _reset_cache_entry_data(self):
105+
self._current_rpc_method = ""
106+
self._current_cache_key_details = None
107+
108+
def _should_log_traffic(self):
109+
return self._rpc_counter is not None
110+
111+
class _ClientCallDetails(
112+
namedtuple("_ClientCallDetails", ("method", "timeout", "metadata", "credentials")),
113+
ClientCallDetails,
114+
):
115+
pass
116+
117+
@classmethod
118+
def _get_client_call_details_with_caching_options(cls, client_call_details):
119+
if get_cache() is None:
120+
return client_call_details
121+
metadata = []
122+
if client_call_details.metadata is not None:
123+
metadata = list(client_call_details.metadata)
124+
metadata.append(("enable-caching", "1"))
125+
return cls._ClientCallDetails(
126+
client_call_details.method,
127+
client_call_details.timeout,
128+
metadata,
129+
client_call_details.credentials,
130+
)
131+
132+
def _continue_unary_unary(self, continuation, client_call_details, request):
133+
if self._should_log_traffic():
134+
self._current_rpc_method = client_call_details.method
135+
cache = get_cache()
136+
if cache is not None:
137+
method_tokens = client_call_details.method.strip("/").split("/")
138+
cache_key_details = method_tokens[0], method_tokens[1], request
139+
cached_response = cache.get(*cache_key_details)
140+
if cached_response is not None:
141+
return cached_response
142+
else:
143+
self._current_cache_key_details = cache_key_details
144+
return super()._continue_unary_unary(
145+
continuation,
146+
self._get_client_call_details_with_caching_options(client_call_details),
147+
request,
148+
)
149+
150+
def _cache_missed(self):
151+
return self._current_cache_key_details is not None
152+
153+
def _post_process(self, response):
154+
cache = get_cache()
155+
if cache is not None and self._cache_missed():
156+
cache.add(*self._current_cache_key_details, response.result())
157+
if self._should_log_traffic() and (cache is None or self._cache_missed()):
158+
self._rpc_counter[self._current_rpc_method] += 1
159+
self._reset_cache_entry_data()
160+
161+
def intercept_unary_stream(self, continuation, client_call_details, request):
162+
"""Intercept a gRPC streaming call."""
163+
return super().intercept_unary_stream(
164+
continuation,
165+
self._get_client_call_details_with_caching_options(client_call_details),
166+
request,
167+
)

src/ansys/edb/core/inner/messages.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,9 @@
8383
)
8484
from ansys.api.edb.v1.hierarchy_obj_pb2 import ObjectNameInLayoutMessage
8585
from ansys.api.edb.v1.inst_array_pb2 import InstArrayCreationMessage
86-
from ansys.api.edb.v1.layout_pb2 import (
87-
LayoutConvertP2VMessage,
88-
LayoutExpandedExtentMessage,
89-
LayoutGetItemsMessage,
90-
)
86+
from ansys.api.edb.v1.layout_pb2 import LayoutConvertP2VMessage, LayoutExpandedExtentMessage
9187
from ansys.api.edb.v1.material_def_pb2 import MaterialDefPropertiesMessage
9288
from ansys.api.edb.v1.mcad_model_pb2 import * # noqa
93-
from ansys.api.edb.v1.net_pb2 import NetGetLayoutObjMessage
9489
from ansys.api.edb.v1.package_def_pb2 import HeatSinkMessage, SetHeatSinkMessage
9590
from ansys.api.edb.v1.padstack_inst_term_pb2 import (
9691
PadstackInstTermCreationsMessage,
@@ -507,11 +502,6 @@ def point_3d_property_message(target, val):
507502
return Point3DPropertyMessage(target=edb_obj_message(target), value=point3d_message(val))
508503

509504

510-
def layout_get_items_message(layout, item_type):
511-
"""Convert to a ``LayoutGetItemsMessage`` object."""
512-
return LayoutGetItemsMessage(layout=layout.msg, obj_type=item_type.value)
513-
514-
515505
def layout_expanded_extent_message(
516506
layout, nets, extent, exp, exp_unitless, use_round_corner, num_increments
517507
):
@@ -1080,11 +1070,6 @@ def double_property_message(edb_obj, double):
10801070
return DoublePropertyMessage(target=edb_obj.msg, value=double)
10811071

10821072

1083-
def net_get_layout_obj_message(obj, layout_obj_type):
1084-
"""Convert to a ``NetGetLayoutObjMessage`` object."""
1085-
return NetGetLayoutObjMessage(net=edb_obj_message(obj), obj_type=layout_obj_type.value)
1086-
1087-
10881073
def differential_pair_creation_message(layout, name, pos_net, neg_net):
10891074
"""Convert to a ``DifferentialPairCreationMessage`` object."""
10901075
return DifferentialPairCreationMessage(

src/ansys/edb/core/inner/utils.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,38 @@
11
"""This module contains utility functions for API development work."""
2+
from ansys.api.edb.v1.layout_obj_pb2 import LayoutObjTargetMessage
3+
4+
from ansys.edb.core.inner.factory import create_lyt_obj
5+
from ansys.edb.core.utility.cache import get_cache
26

37

48
def map_list(iterable_to_operate_on, operator=None):
59
"""Apply the given operator to each member of an iterable and return the modified list.
610
711
Parameters
812
---------
9-
iterable_to_operate on
13+
iterable_to_operate_on
1014
operator
1115
"""
1216
return list(
1317
iterable_to_operate_on if operator is None else map(operator, iterable_to_operate_on)
1418
)
19+
20+
21+
def query_lyt_object_collection(owner, obj_type, unary_rpc, unary_streaming_rpc):
22+
"""For the provided request, retrieve a collection of objects using the unary_rpc or unary_streaming_rpc methods \
23+
depending on whether caching is enabled."""
24+
items = []
25+
cache = get_cache()
26+
request = LayoutObjTargetMessage(target=owner.msg, type=obj_type.value)
27+
28+
def add_msgs_to_items(edb_obj_collection_msg):
29+
nonlocal items
30+
for item in edb_obj_collection_msg.items:
31+
items.append(create_lyt_obj(item, obj_type))
32+
33+
if cache is None:
34+
add_msgs_to_items(unary_rpc(request))
35+
else:
36+
for streamed_items in unary_streaming_rpc(request):
37+
add_msgs_to_items(streamed_items)
38+
return items

0 commit comments

Comments
 (0)