33
33
import re
34
34
from itertools import chain
35
35
from 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 )
37
37
from google .api import annotations_pb2 # type: ignore
38
38
from google .api import client_pb2
39
39
from google .api import field_behavior_pb2
40
40
from google .api import http_pb2
41
41
from google .api import resource_pb2
42
+ from google .api import routing_pb2
42
43
from google .api_core import exceptions
43
44
from google .api_core import path_template
44
45
from google .cloud import extended_operations_pb2 as ex_ops_pb2 # type: ignore
47
48
48
49
from gapic import utils
49
50
from gapic .schema import metadata
51
+ from gapic .utils import uri_sample
50
52
51
53
52
54
@dataclasses .dataclass (frozen = True )
@@ -763,6 +765,118 @@ class RetryInfo:
763
765
retryable_exceptions : FrozenSet [exceptions .GoogleAPICallError ]
764
766
765
767
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
+
766
880
@dataclasses .dataclass (frozen = True )
767
881
class HttpRule :
768
882
"""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
788
902
Returns:
789
903
A new nested dict with the templates instantiated.
790
904
"""
791
-
792
905
request : Dict [str , Any ] = {}
793
906
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 ()
834
908
for field , path , template in paths :
835
909
sample_value = re .sub (
836
910
r"(\*\*|\*)" ,
837
- lambda n : next (sample_names ),
911
+ lambda n : next (sample_names_ ),
838
912
template or '*'
839
913
) 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 )
841
915
842
916
return request
843
-
844
917
sample = sample_from_path_fields (self .path_fields (method ))
845
918
return sample
846
919
@@ -982,6 +1055,18 @@ def field_headers(self) -> Sequence[str]:
982
1055
983
1056
return next ((tuple (pattern .findall (verb )) for verb in potential_verbs if verb ), ())
984
1057
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
+
985
1070
@property
986
1071
def http_options (self ) -> List [HttpRule ]:
987
1072
"""Return a list of the http bindings for this method."""
0 commit comments