3333import re
3434from itertools import chain
3535from typing import (Any , cast , Dict , FrozenSet , Iterator , Iterable , List , Mapping ,
36- ClassVar , Optional , Sequence , Set , Tuple , Union )
36+ ClassVar , Optional , Sequence , Set , Tuple , Union , Pattern )
3737from google .api import annotations_pb2 # type: ignore
3838from google .api import client_pb2
3939from google .api import field_behavior_pb2
4040from google .api import http_pb2
4141from google .api import resource_pb2
42+ from google .api import routing_pb2
4243from google .api_core import exceptions
4344from google .api_core import path_template
4445from google .cloud import extended_operations_pb2 as ex_ops_pb2 # type: ignore
4748
4849from gapic import utils
4950from gapic .schema import metadata
51+ from gapic .utils import uri_sample
5052
5153
5254@dataclasses .dataclass (frozen = True )
@@ -763,6 +765,118 @@ class RetryInfo:
763765 retryable_exceptions : FrozenSet [exceptions .GoogleAPICallError ]
764766
765767
768+ @dataclasses .dataclass (frozen = True )
769+ class RoutingParameter :
770+ field : str
771+ path_template : str
772+
773+ def _split_into_segments (self , path_template ):
774+ segments = path_template .split ("/" )
775+ named_segment_ids = [i for i , x in enumerate (
776+ segments ) if "{" in x or "}" in x ]
777+ # bar/{foo}/baz, bar/{foo=one/two/three}/baz.
778+ assert len (named_segment_ids ) <= 2
779+ if len (named_segment_ids ) == 2 :
780+ # Need to merge a named segment.
781+ i , j = named_segment_ids
782+ segments = (
783+ segments [:i ] +
784+ [self ._merge_segments (segments [i : j + 1 ])] + segments [j + 1 :]
785+ )
786+ return segments
787+
788+ def _convert_segment_to_regex (self , segment ):
789+ # Named segment
790+ if "{" in segment :
791+ assert "}" in segment
792+ # Strip "{" and "}"
793+ segment = segment [1 :- 1 ]
794+ if "=" not in segment :
795+ # e.g. {foo} should be {foo=*}
796+ return self ._convert_segment_to_regex ("{" + f"{ segment } =*" + "}" )
797+ key , sub_path_template = segment .split ("=" )
798+ group_name = f"?P<{ key } >"
799+ sub_regex = self ._convert_to_regex (sub_path_template )
800+ return f"({ group_name } { sub_regex } )"
801+ # Wildcards
802+ if "**" in segment :
803+ # ?: nameless capture
804+ return ".*"
805+ if "*" in segment :
806+ return "[^/]+"
807+ # Otherwise it's collection ID segment: transformed identically.
808+ return segment
809+
810+ def _merge_segments (self , segments ):
811+ acc = segments [0 ]
812+ for x in segments [1 :]:
813+ # Don't add "/" if it's followed by a "**"
814+ # because "**" will eat it.
815+ if x == ".*" :
816+ acc += "(?:/.*)?"
817+ else :
818+ acc += "/"
819+ acc += x
820+ return acc
821+
822+ def _how_many_named_segments (self , path_template ):
823+ return path_template .count ("{" )
824+
825+ def _convert_to_regex (self , path_template ):
826+ if self ._how_many_named_segments (path_template ) > 1 :
827+ # This also takes care of complex patterns (i.e. {foo}~{bar})
828+ raise ValueError ("There must be exactly one named segment. {} has {}." .format (
829+ path_template , self ._how_many_named_segments (path_template )))
830+ segments = self ._split_into_segments (path_template )
831+ segment_regexes = [self ._convert_segment_to_regex (x ) for x in segments ]
832+ final_regex = self ._merge_segments (segment_regexes )
833+ return final_regex
834+
835+ def _to_regex (self , path_template : str ) -> Pattern :
836+ """Converts path_template into a Python regular expression string.
837+ Args:
838+ path_template (str): A path template corresponding to a resource name.
839+ It can only have 0 or 1 named segments. It can not contain complex resource ID path segments.
840+ See https://google.aip.dev/122, https://google.aip.dev/4222
841+ and https://google.aip.dev/client-libraries/4231 for more details.
842+ Returns:
843+ Pattern: A Pattern object that matches strings conforming to the path_template.
844+ """
845+ return re .compile (f"^{ self ._convert_to_regex (path_template )} $" )
846+
847+ def to_regex (self ) -> Pattern :
848+ return self ._to_regex (self .path_template )
849+
850+ @property
851+ def key (self ) -> Union [str , None ]:
852+ if self .path_template == "" :
853+ return self .field
854+ regex = self .to_regex ()
855+ group_names = list (regex .groupindex )
856+ # Only 1 named segment is allowed and so only 1 key.
857+ return group_names [0 ] if group_names else self .field
858+
859+ @property
860+ def sample_request (self ) -> str :
861+ """return json dict for sample request matching the uri template."""
862+ sample = uri_sample .sample_from_path_template (
863+ self .field , self .path_template )
864+ return json .dumps (sample )
865+
866+
867+ @dataclasses .dataclass (frozen = True )
868+ class RoutingRule :
869+ routing_parameters : List [RoutingParameter ]
870+
871+ @classmethod
872+ def try_parse_routing_rule (cls , routing_rule : routing_pb2 .RoutingRule ) -> Optional ['RoutingRule' ]:
873+ params = getattr (routing_rule , 'routing_parameters' )
874+ if not params :
875+ return None
876+ params = [RoutingParameter (x .field , x .path_template ) for x in params ]
877+ return cls (params )
878+
879+
766880@dataclasses .dataclass (frozen = True )
767881class HttpRule :
768882 """Representation of the method's http bindings."""
@@ -788,59 +902,18 @@ def sample_from_path_fields(paths: List[Tuple[Field, str, str]]) -> Dict[str, An
788902 Returns:
789903 A new nested dict with the templates instantiated.
790904 """
791-
792905 request : Dict [str , Any ] = {}
793906
794- def _sample_names () -> Iterator [str ]:
795- sample_num : int = 0
796- while True :
797- sample_num += 1
798- yield "sample{}" .format (sample_num )
799-
800- def add_field (obj , path , value ):
801- """Insert a field into a nested dict and return the (outer) dict.
802- Keys and sub-dicts are inserted if necessary to create the path.
803- e.g. if obj, as passed in, is {}, path is "a.b.c", and value is
804- "hello", obj will be updated to:
805- {'a':
806- {'b':
807- {
808- 'c': 'hello'
809- }
810- }
811- }
812-
813- Args:
814- obj: a (possibly) nested dict (parsed json)
815- path: a segmented field name, e.g. "a.b.c"
816- where each part is a dict key.
817- value: the value of the new key.
818- Returns:
819- obj, possibly modified
820- Raises:
821- AttributeError if the path references a key that is
822- not a dict.: e.g. path='a.b', obj = {'a':'abc'}
823- """
824-
825- segments = path .split ('.' )
826- leaf = segments .pop ()
827- subfield = obj
828- for segment in segments :
829- subfield = subfield .setdefault (segment , {})
830- subfield [leaf ] = value
831- return obj
832-
833- sample_names = _sample_names ()
907+ sample_names_ = uri_sample .sample_names ()
834908 for field , path , template in paths :
835909 sample_value = re .sub (
836910 r"(\*\*|\*)" ,
837- lambda n : next (sample_names ),
911+ lambda n : next (sample_names_ ),
838912 template or '*'
839913 ) if field .type == PrimitiveType .build (str ) else field .mock_value_original_type
840- add_field (request , path , sample_value )
914+ uri_sample . add_field (request , path , sample_value )
841915
842916 return request
843-
844917 sample = sample_from_path_fields (self .path_fields (method ))
845918 return sample
846919
@@ -982,6 +1055,18 @@ def field_headers(self) -> Sequence[str]:
9821055
9831056 return next ((tuple (pattern .findall (verb )) for verb in potential_verbs if verb ), ())
9841057
1058+ @property
1059+ def explicit_routing (self ):
1060+ return routing_pb2 .routing in self .options .Extensions
1061+
1062+ @property
1063+ def routing_rule (self ):
1064+ if self .explicit_routing :
1065+ routing_ext = self .options .Extensions [routing_pb2 .routing ]
1066+ routing_rule = RoutingRule .try_parse_routing_rule (routing_ext )
1067+ return routing_rule
1068+ return None
1069+
9851070 @property
9861071 def http_options (self ) -> List [HttpRule ]:
9871072 """Return a list of the http bindings for this method."""
0 commit comments