55import urllib .parse
66from dataclasses import dataclass
77from enum import Enum
8+ from functools import cached_property
89from typing import (
910 TYPE_CHECKING ,
1011 Any ,
@@ -35,39 +36,34 @@ class _LinkPath:
3536 """The URL path shape for one link type.
3637
3738 Every link path is /namespaces/{namespace}/{keyword}/{id}/{run_id} with an optional trailing
38- segment, so a link type is fully described by which keyword names it, what follows the run ID,
39- and which proto field holds the ID.
39+ segment, so a link type is fully described by which keyword names it and what follows the
40+ run ID.
4041 """
4142
4243 keyword : str
4344 tail : str | None
44- id_field : str
4545 # A standalone Nexus operation need not have a run ID, so its path may carry an empty segment
4646 # there. The other link types always address a specific run.
4747 run_id_required : bool = True
4848
49+ @cached_property
50+ def regex (self ) -> re .Pattern [str ]:
51+ """The pattern matching a URL path of this shape."""
52+ run_id = "[^/]+" if self .run_id_required else "[^/]*"
53+ tail = f"/{ self .tail } " if self .tail else ""
54+ return re .compile (
55+ rf"^/namespaces/(?P<namespace>[^/]+)/{ self .keyword } "
56+ rf"/(?P<id>[^/]+)/(?P<run_id>{ run_id } ){ tail } $"
57+ )
58+
4959
5060_LINK_PATHS : dict [_LinkType , _LinkPath ] = {
51- _LinkType .WORKFLOW_EVENT : _LinkPath ("workflows" , "history" , "workflow_id" ),
52- _LinkType .WORKFLOW : _LinkPath ("workflows" , None , "workflow_id" ),
61+ _LinkType .WORKFLOW_EVENT : _LinkPath ("workflows" , "history" ),
62+ _LinkType .WORKFLOW : _LinkPath ("workflows" , None ),
5363 _LinkType .NEXUS_OPERATION : _LinkPath (
54- "nexus-operations" , "details" , "operation_id" , run_id_required = False
64+ "nexus-operations" , "details" , run_id_required = False
5565 ),
56- _LinkType .ACTIVITY : _LinkPath ("activities" , "details" , "activity_id" ),
57- }
58-
59-
60- def _link_path_regex (path : _LinkPath ) -> re .Pattern [str ]:
61- run_id = "[^/]+" if path .run_id_required else "[^/]*"
62- tail = f"/{ path .tail } " if path .tail else ""
63- return re .compile (
64- rf"^/namespaces/(?P<namespace>[^/]+)/{ path .keyword } "
65- rf"/(?P<id>[^/]+)/(?P<run_id>{ run_id } ){ tail } $"
66- )
67-
68-
69- _LINK_PATH_REGEXES : dict [_LinkType , re .Pattern [str ]] = {
70- link_type : _link_path_regex (path ) for link_type , path in _LINK_PATHS .items ()
66+ _LinkType .ACTIVITY : _LinkPath ("activities" , "details" ),
7167}
7268
7369
@@ -277,10 +273,16 @@ def _temporal_nexus_url(path: str, *, query_params: str | None = "") -> str:
277273 return f"temporal://{ urllib .parse .urlunparse (('' , '' , path , '' , query_params or '' , '' ))} "
278274
279275
280- def _parse_link_url (
281- link : nexusrpc .Link , link_type : _LinkType
282- ) -> tuple [str , str , str , dict [str , list [str ]]] | None :
283- """Return (namespace, id, run_id, query params), or None if the URL is not a link of this type.
276+ @dataclass (frozen = True )
277+ class _ParsedLinkUrl :
278+ namespace : str
279+ id : str
280+ run_id : str
281+ query_params : dict [str , list [str ]]
282+
283+
284+ def _parse_link_url (link : nexusrpc .Link , link_type : _LinkType ) -> _ParsedLinkUrl | None :
285+ """Return the parts of a link URL, or None if the URL is not a link of this type.
284286
285287 The path is matched exactly, so the workflow and workflow-event shapes -- which differ only by
286288 the trailing /history -- cannot be mistaken for one another.
@@ -291,18 +293,18 @@ def _parse_link_url(
291293 f"Invalid Nexus link: { link } . Expected scheme { _URL_SCHEME !r} , got { url .scheme !r} "
292294 )
293295 return None
294- regex = _LINK_PATH_REGEXES [link_type ]
296+ regex = _LINK_PATHS [link_type ]. regex
295297 match = regex .match (url .path )
296298 if not match :
297299 logger .warning (
298300 f"Invalid Nexus link: { link } . Expected path to match { regex .pattern } "
299301 )
300302 return None
301- return (
302- urllib .parse .unquote (match .group ("namespace" )),
303- urllib .parse .unquote (match .group ("id" )),
304- urllib .parse .unquote (match .group ("run_id" )),
305- urllib .parse .parse_qs (url .query ),
303+ return _ParsedLinkUrl (
304+ namespace = urllib .parse .unquote (match .group ("namespace" )),
305+ id = urllib .parse .unquote (match .group ("id" )),
306+ run_id = urllib .parse .unquote (match .group ("run_id" )),
307+ query_params = urllib .parse .parse_qs (url .query ),
306308 )
307309
308310
@@ -329,7 +331,7 @@ def nexus_link_to_workflow_event_link(
329331 parsed = _parse_link_url (link , _LinkType .WORKFLOW_EVENT )
330332 if parsed is None :
331333 return None
332- namespace , workflow_id , run_id , query_params = parsed
334+ query_params = parsed . query_params
333335 try :
334336 request_id_ref = None
335337 event_ref = None
@@ -350,9 +352,9 @@ def nexus_link_to_workflow_event_link(
350352 return None
351353
352354 workflow_event_link = temporalio .api .common .v1 .Link .WorkflowEvent (
353- namespace = namespace ,
354- workflow_id = workflow_id ,
355- run_id = run_id ,
355+ namespace = parsed . namespace ,
356+ workflow_id = parsed . id ,
357+ run_id = parsed . run_id ,
356358 event_ref = event_ref ,
357359 request_id_ref = request_id_ref ,
358360 )
@@ -366,17 +368,18 @@ def nexus_link_to_workflow_link(
366368 parsed = _parse_link_url (link , _LinkType .WORKFLOW )
367369 if parsed is None :
368370 return None
369- namespace , workflow_id , run_id , query_params = parsed
370371 try :
371- reason = _optional_single_query_param (query_params , LINK_REASON_PARAM_NAME )
372+ reason = _optional_single_query_param (
373+ parsed .query_params , LINK_REASON_PARAM_NAME
374+ )
372375 except ValueError as err :
373376 logger .warning (f"Invalid Nexus link: { link } . { err } " )
374377 return None
375378
376379 workflow_link = temporalio .api .common .v1 .Link .Workflow (
377- namespace = namespace ,
378- workflow_id = workflow_id ,
379- run_id = run_id ,
380+ namespace = parsed . namespace ,
381+ workflow_id = parsed . id ,
382+ run_id = parsed . run_id ,
380383 reason = reason ,
381384 )
382385 return temporalio .api .common .v1 .Link (workflow = workflow_link )
@@ -393,12 +396,11 @@ def nexus_link_to_nexus_operation_link(
393396 parsed = _parse_link_url (nexus_link , _LinkType .NEXUS_OPERATION )
394397 if parsed is None :
395398 return None
396- namespace , operation_id , run_id , _ = parsed
397399 return temporalio .api .common .v1 .Link (
398400 nexus_operation = temporalio .api .common .v1 .Link .NexusOperation (
399- namespace = namespace ,
400- operation_id = operation_id ,
401- run_id = run_id ,
401+ namespace = parsed . namespace ,
402+ operation_id = parsed . id ,
403+ run_id = parsed . run_id ,
402404 )
403405 )
404406
@@ -410,12 +412,11 @@ def nexus_link_to_activity_link(
410412 parsed = _parse_link_url (nexus_link , _LinkType .ACTIVITY )
411413 if parsed is None :
412414 return None
413- namespace , activity_id , run_id , _ = parsed
414415 return temporalio .api .common .v1 .Link (
415416 activity = temporalio .api .common .v1 .Link .Activity (
416- namespace = namespace ,
417- activity_id = activity_id ,
418- run_id = run_id ,
417+ namespace = parsed . namespace ,
418+ activity_id = parsed . id ,
419+ run_id = parsed . run_id ,
419420 )
420421 )
421422
0 commit comments