22
33from __future__ import annotations
44
5+ import abc
56import asyncio
67import copy
78import dataclasses
@@ -107,6 +108,7 @@ async def connect(
107108 namespace : str = "default" ,
108109 api_key : Optional [str ] = None ,
109110 data_converter : temporalio .converter .DataConverter = temporalio .converter .DataConverter .default ,
111+ plugins : Sequence [Plugin ] = [],
110112 interceptors : Sequence [Interceptor ] = [],
111113 default_workflow_query_reject_condition : Optional [
112114 temporalio .common .QueryRejectCondition
@@ -132,6 +134,14 @@ async def connect(
132134 metadata doesn't already have an "authorization" key.
133135 data_converter: Data converter to use for all data conversions
134136 to/from payloads.
137+ plugins: Set of plugins that are chained together to allow
138+ intercepting and modifying client creation and service connection.
139+ The earlier plugins wrap the later ones.
140+
141+ Any plugins that also implement
142+ :py:class:`temporalio.worker.Plugin` will be used as worker
143+ plugins too so they should not be given when creating a
144+ worker.
135145 interceptors: Set of interceptors that are chained together to allow
136146 intercepting of client calls. The earlier interceptors wrap the
137147 later ones.
@@ -178,13 +188,21 @@ async def connect(
178188 runtime = runtime ,
179189 http_connect_proxy_config = http_connect_proxy_config ,
180190 )
191+
192+ root_plugin : Plugin = _RootPlugin ()
193+ for plugin in reversed (plugins ):
194+ root_plugin = plugin .init_client_plugin (root_plugin )
195+
196+ service_client = await root_plugin .connect_service_client (connect_config )
197+
181198 return Client (
182- await temporalio . service . ServiceClient . connect ( connect_config ) ,
199+ service_client ,
183200 namespace = namespace ,
184201 data_converter = data_converter ,
185202 interceptors = interceptors ,
186203 default_workflow_query_reject_condition = default_workflow_query_reject_condition ,
187204 header_codec_behavior = header_codec_behavior ,
205+ plugins = plugins ,
188206 )
189207
190208 def __init__ (
@@ -193,6 +211,7 @@ def __init__(
193211 * ,
194212 namespace : str = "default" ,
195213 data_converter : temporalio .converter .DataConverter = temporalio .converter .DataConverter .default ,
214+ plugins : Sequence [Plugin ] = [],
196215 interceptors : Sequence [Interceptor ] = [],
197216 default_workflow_query_reject_condition : Optional [
198217 temporalio .common .QueryRejectCondition
@@ -203,21 +222,31 @@ def __init__(
203222
204223 See :py:meth:`connect` for details on the parameters.
205224 """
206- # Iterate over interceptors in reverse building the impl
207- self ._impl : OutboundInterceptor = _ClientImpl (self )
208- for interceptor in reversed (list (interceptors )):
209- self ._impl = interceptor .intercept_client (self ._impl )
210-
211225 # Store the config for tracking
212- self . _config = ClientConfig (
226+ config = ClientConfig (
213227 service_client = service_client ,
214228 namespace = namespace ,
215229 data_converter = data_converter ,
216230 interceptors = interceptors ,
217231 default_workflow_query_reject_condition = default_workflow_query_reject_condition ,
218232 header_codec_behavior = header_codec_behavior ,
233+ plugins = plugins ,
219234 )
220235
236+ root_plugin : Plugin = _RootPlugin ()
237+ for plugin in reversed (plugins ):
238+ root_plugin = plugin .init_client_plugin (root_plugin )
239+
240+ self ._init_from_config (root_plugin .configure_client (config ))
241+
242+ def _init_from_config (self , config : ClientConfig ):
243+ self ._config = config
244+
245+ # Iterate over interceptors in reverse building the impl
246+ self ._impl : OutboundInterceptor = _ClientImpl (self )
247+ for interceptor in reversed (list (self ._config ["interceptors" ])):
248+ self ._impl = interceptor .intercept_client (self ._impl )
249+
221250 def config (self ) -> ClientConfig :
222251 """Config, as a dictionary, used to create this client.
223252
@@ -1507,6 +1536,7 @@ class ClientConfig(TypedDict, total=False):
15071536 Optional [temporalio .common .QueryRejectCondition ]
15081537 ]
15091538 header_codec_behavior : Required [HeaderCodecBehavior ]
1539+ plugins : Required [Sequence [Plugin ]]
15101540
15111541
15121542class WorkflowHistoryEventFilterType (IntEnum ):
@@ -7356,3 +7386,81 @@ async def _decode_user_metadata(
73567386 if not metadata .HasField ("details" )
73577387 else (await converter .decode ([metadata .details ]))[0 ],
73587388 )
7389+
7390+
7391+ class Plugin (abc .ABC ):
7392+ """Base class for client plugins that can intercept and modify client behavior.
7393+
7394+ Plugins allow customization of client creation and service connection processes
7395+ through a chain of responsibility pattern. Each plugin can modify the client
7396+ configuration or intercept service client connections.
7397+
7398+ If the plugin is also a temporalio.worker.Plugin, it will additionally be propagated as a worker plugin.
7399+ You should likley not also provide it to the worker as that will result in the plugin being applied twice.
7400+ """
7401+
7402+ def name (self ) -> str :
7403+ """Get the name of this plugin. Can be overridden if desired to provide a more appropriate name.
7404+
7405+ Returns:
7406+ The fully qualified name of the plugin class (module.classname).
7407+ """
7408+ return type (self ).__module__ + "." + type (self ).__qualname__
7409+
7410+ def init_client_plugin (self , next : Plugin ) -> Plugin :
7411+ """Initialize this plugin in the plugin chain.
7412+
7413+ This method sets up the chain of responsibility pattern by storing a reference
7414+ to the next plugin in the chain. It is called during client creation to build
7415+ the plugin chain. Note, this may be called twice in the case of :py:meth:`connect`.
7416+
7417+ Args:
7418+ next: The next plugin in the chain to delegate to.
7419+
7420+ Returns:
7421+ This plugin instance for method chaining.
7422+ """
7423+ self .next_client_plugin = next
7424+ return self
7425+
7426+ def configure_client (self , config : ClientConfig ) -> ClientConfig :
7427+ """Hook called when creating a client to allow modification of configuration.
7428+
7429+ This method is called during client creation and allows plugins to modify
7430+ the client configuration before the client is fully initialized. Plugins
7431+ can add interceptors, modify connection parameters, or change other settings.
7432+
7433+ Args:
7434+ config: The client configuration dictionary to potentially modify.
7435+
7436+ Returns:
7437+ The modified client configuration.
7438+ """
7439+ return self .next_client_plugin .configure_client (config )
7440+
7441+ async def connect_service_client (
7442+ self , config : temporalio .service .ConnectConfig
7443+ ) -> temporalio .service .ServiceClient :
7444+ """Hook called when connecting to the Temporal service.
7445+
7446+ This method is called during service client connection and allows plugins
7447+ to intercept or modify the connection process. Plugins can modify connection
7448+ parameters, add authentication, or provide custom connection logic.
7449+
7450+ Args:
7451+ config: The service connection configuration.
7452+
7453+ Returns:
7454+ The connected service client.
7455+ """
7456+ return await self .next_client_plugin .connect_service_client (config )
7457+
7458+
7459+ class _RootPlugin (Plugin ):
7460+ def configure_client (self , config : ClientConfig ) -> ClientConfig :
7461+ return config
7462+
7463+ async def connect_service_client (
7464+ self , config : temporalio .service .ConnectConfig
7465+ ) -> temporalio .service .ServiceClient :
7466+ return await temporalio .service .ServiceClient .connect (config )
0 commit comments