-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Expand file tree
/
Copy pathwrappers.py
More file actions
2506 lines (2121 loc) · 92.8 KB
/
wrappers.py
File metadata and controls
2506 lines (2121 loc) · 92.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module containing wrapper classes around meta-descriptors.
This module contains dataclasses which wrap the descriptor protos
defined in google/protobuf/descriptor.proto (which are descriptors that
describe descriptors).
These wrappers exist in order to provide useful helper methods and
generally ease access to things in templates (in particular, documentation,
certain aggregate views of things, etc.)
Reading of underlying descriptor properties in templates *is* okay, a
``__getattr__`` method which consistently routes in this way is provided.
Documentation is consistently at ``{thing}.meta.doc``.
"""
import collections
import copy
import dataclasses
import functools
import json
import keyword
import re
from itertools import chain
from typing import (
Any,
cast,
Dict,
FrozenSet,
Iterator,
Iterable,
List,
Mapping,
ClassVar,
Optional,
Sequence,
Set,
Tuple,
Union,
Pattern,
)
from google.api import annotations_pb2 # type: ignore
from google.api import client_pb2
from google.api import field_behavior_pb2
from google.api import field_info_pb2
from google.api import http_pb2
from google.api import resource_pb2
from google.api import routing_pb2
from google.api_core import exceptions
from google.api_core import path_template
from google.cloud import extended_operations_pb2 as ex_ops_pb2 # type: ignore
from google.protobuf import descriptor_pb2 # type: ignore
from google.protobuf.json_format import MessageToDict # type: ignore
from gapic import utils
from gapic.schema import metadata
from gapic.utils import cached_proto_context
from gapic.utils import uri_sample
from gapic.utils import make_private
@dataclasses.dataclass(frozen=True)
class Field:
"""Description of a field."""
field_pb: descriptor_pb2.FieldDescriptorProto
message: Optional["MessageType"] = None
enum: Optional["EnumType"] = None
meta: metadata.Metadata = dataclasses.field(
default_factory=metadata.Metadata,
)
oneof: Optional[str] = None
def __getattr__(self, name):
return getattr(self.field_pb, name)
def __hash__(self):
# The only sense in which it is meaningful to say a field is equal to
# another field is if they are the same, i.e. they live in the same
# message type under the same moniker, i.e. they have the same id.
return id(self)
@property
def name(self) -> str:
"""Used to prevent collisions with python keywords"""
name = self.field_pb.name
return (
name + "_"
if name in utils.RESERVED_NAMES and self.meta.address.is_proto_plus_type
else name
)
@utils.cached_property
def ident(self) -> metadata.FieldIdentifier:
"""Return the identifier to be used in templates."""
mapping: Optional[Tuple[Field, Field]] = None
if self.map:
mapping = (self.type.fields["key"], self.type.fields["value"])
return metadata.FieldIdentifier(
ident=self.type.ident,
repeated=self.repeated,
mapping=mapping,
)
@property
def is_primitive(self) -> bool:
"""Return True if the field is a primitive, False otherwise."""
return isinstance(self.type, PrimitiveType)
@property
def map(self) -> bool:
"""Return True if this field is a map, False otherwise."""
return bool(self.repeated and self.message and self.message.map)
@property
def operation_field(self) -> Optional[str]:
return self.options.Extensions[ex_ops_pb2.operation_field]
@property
def operation_request_field(self) -> Optional[str]:
return self.options.Extensions[ex_ops_pb2.operation_request_field]
@property
def operation_response_field(self) -> Optional[str]:
return self.options.Extensions[ex_ops_pb2.operation_response_field]
@utils.cached_property
def mock_value_original_type(
self,
) -> Union[bool, str, bytes, int, float, Dict[str, Any], List[Any], None]:
visited_messages = set()
def recursive_mock_original_type(field):
if field.message:
# Return messages as dicts and let the message ctor handle the conversion.
if field.message in visited_messages:
return {}
visited_messages.add(field.message)
if field.map:
# Not worth the hassle, just return an empty map.
return {}
adr = field.type.meta.address
if adr.name == "Any" and adr.package == ("google", "protobuf"):
# If it is Any type pack a random but validly encoded type,
# Duration in this specific case.
msg_dict = {
"type_url": "type.googleapis.com/google.protobuf.Duration",
"value": b"\x08\x0c\x10\xdb\x07",
}
else:
msg_dict = {
f.name: recursive_mock_original_type(f)
for f in field.message.fields.values()
}
return [msg_dict] if field.repeated else msg_dict
if field.enum:
# First Truthy value, fallback to the first value
answer = next(
(v for v in field.type.values if v.number), field.type.values[0]
).number
if field.repeated:
answer = [answer]
return answer
answer = field.primitive_mock() or None
# If this is a repeated field, then the mock answer should
# be a list.
if field.repeated:
first_item = field.primitive_mock(suffix=1) or None
second_item = field.primitive_mock(suffix=2) or None
answer = [first_item, second_item]
return answer
return recursive_mock_original_type(self)
def merged_mock_value(self, other_mock: Dict[Any, Any]):
mock = self.mock_value_original_type
if isinstance(mock, dict) and isinstance(other_mock, dict):
mock = copy.deepcopy(mock)
mock.update(other_mock)
return mock
@utils.cached_property
def mock_value(self) -> str:
visited_fields: Set["Field"] = set()
stack = [self]
answer = "{}"
while stack:
expr = stack.pop()
answer = answer.format(expr.inner_mock(stack, visited_fields))
return answer
def inner_mock(self, stack, visited_fields) -> str:
"""Return a repr of a valid, usually truthy mock value."""
# For primitives, send a truthy value computed from the
# field name.
answer = "None"
if isinstance(self.type, PrimitiveType):
answer = self.primitive_mock_as_str()
# If this is an enum, select the first truthy value (or the zero
# value if nothing else exists).
if isinstance(self.type, EnumType):
# Note: The slightly-goofy [:2][-1] lets us gracefully fall
# back to index 0 if there is only one element.
mock_value = self.type.values[:2][-1]
answer = f"{self.type.ident}.{mock_value.name}"
# If this is another message, set one value on the message.
if (
not self.map # Maps are handled separately
and isinstance(self.type, MessageType)
and len(self.type.fields)
# Nested message types need to terminate eventually
and self not in visited_fields
):
sub = next(iter(self.type.fields.values()))
stack.append(sub)
visited_fields.add(self)
# Don't do the recursive rendering here, just set up
# where the nested value should go with the double {}.
answer = f"{self.type.ident}({sub.name}={{}})"
if self.map:
# Maps are a special case because they're represented internally as
# a list of a generated type with two fields: 'key' and 'value'.
answer = "{{{}: {}}}".format(
self.type.fields["key"].mock_value,
self.type.fields["value"].mock_value,
)
elif self.repeated:
# If this is a repeated field, then the mock answer should
# be a list.
answer = f"[{answer}]"
# Done; return the mock value.
return answer
def primitive_mock(
self, suffix: int = 0
) -> Union[bool, str, bytes, int, float, List[Any], None]:
"""Generate a valid mock for a primitive type. This function
returns the original (Python) type.
If a suffix is provided, generate a slightly different mock
using the provided integer.
"""
answer: Union[bool, str, bytes, int, float, List[Any], None] = None
if not isinstance(self.type, PrimitiveType):
raise TypeError(
f"'primitive_mock' can only be used for "
f"PrimitiveType, but type is {self.type}"
)
else:
if self.type.python_type == bool:
answer = True
elif self.type.python_type == str:
if self.name == "type_url":
# It is most likely a mock for Any type. We don't really care
# which mock value to put, so lets put a value which makes
# Any deserializer happy, which will wtill work even if it
# is not Any.
answer = "type.googleapis.com/google.protobuf.Empty"
else:
answer = (
f"{self.name}_value{suffix}" if suffix else f"{self.name}_value"
)
elif self.type.python_type == bytes:
answer_str = (
f"{self.name}_blob{suffix}" if suffix else f"{self.name}_blob"
)
answer = bytes(answer_str, encoding="utf-8")
elif self.type.python_type == int:
answer = sum([ord(i) for i in self.name]) + suffix
elif self.type.python_type == float:
name_sum = sum([ord(i) for i in self.name]) + suffix
answer = name_sum * pow(10, -1 * len(str(name_sum)))
else: # Impossible; skip coverage checks.
raise TypeError(
"Unrecognized PrimitiveType. This should "
"never happen; please file an issue."
)
return answer
def primitive_mock_as_str(self) -> str:
"""Like primitive mock, but return the mock as a string."""
answer = self.primitive_mock()
if isinstance(answer, str):
answer = f"'{answer}'"
else:
answer = str(answer)
return answer
@property
def proto_type(self) -> str:
"""Return the proto type constant to be used in templates."""
return cast(
str,
descriptor_pb2.FieldDescriptorProto.Type.Name(
self.field_pb.type,
),
)[len("TYPE_") :]
@property
def repeated(self) -> bool:
"""Return True if this is a repeated field, False otherwise.
Returns:
bool: Whether this field is repeated.
"""
return self.label == descriptor_pb2.FieldDescriptorProto.Label.Value(
"LABEL_REPEATED"
) # type: ignore
@property
def required(self) -> bool:
"""Return True if this is a required field, False otherwise.
Returns:
bool: Whether this field is required.
"""
return (
field_behavior_pb2.FieldBehavior.Value("REQUIRED")
in self.options.Extensions[field_behavior_pb2.field_behavior]
)
@property
def uuid4(self) -> bool:
"""
Return True if the format of this field is a Universally
Unique Identifier, version 4 field, False otherwise.
Returns:
bool: Whether this field is UUID4.
"""
return self.options.Extensions[
field_info_pb2.field_info
].format == field_info_pb2.FieldInfo.Format.Value("UUID4")
@property
def resource_reference(self) -> Optional[str]:
"""Return a resource reference type if it exists.
This is only applicable for string fields.
Example: "translate.googleapis.com/Glossary"
"""
return (
self.options.Extensions[resource_pb2.resource_reference].type
or self.options.Extensions[resource_pb2.resource_reference].child_type
or None
)
@utils.cached_property
def type(self) -> Union["MessageType", "EnumType", "PrimitiveType"]:
"""Return the type of this field."""
# If this is a message or enum, return the appropriate thing.
if self.type_name and self.message:
return self.message
if self.type_name and self.enum:
return self.enum
# This is a primitive. Return the corresponding Python type.
# The enum values used here are defined in:
# Repository: https://github.com/google/protobuf/
# Path: src/google/protobuf/descriptor.proto
#
# The values are used here because the code would be excessively
# verbose otherwise, and this is guaranteed never to change.
#
# 10, 11, and 14 are intentionally missing. They correspond to
# group (unused), message (covered above), and enum (covered above).
if self.field_pb.type in (1, 2):
return PrimitiveType.build(float)
if self.field_pb.type in (3, 4, 5, 6, 7, 13, 15, 16, 17, 18):
return PrimitiveType.build(int)
if self.field_pb.type == 8:
return PrimitiveType.build(bool)
if self.field_pb.type == 9:
return PrimitiveType.build(str)
if self.field_pb.type == 12:
return PrimitiveType.build(bytes)
# This should never happen.
raise TypeError(
f"Unrecognized protobuf type: {self.field_pb.type}. "
"This code should not be reachable; please file a bug."
)
@cached_proto_context
def with_context(
self,
*,
collisions: Set[str],
visited_messages: Optional[Set["MessageType"]] = None,
) -> "Field":
"""Return a derivative of this field with the provided context.
This method is used to address naming collisions. The returned
``Field`` object aliases module names to avoid naming collisions
in the file being written.
"""
return dataclasses.replace(
self,
message=(
self.message.with_context(
collisions=collisions,
skip_fields=(
self.message in visited_messages if visited_messages else False
),
visited_messages=visited_messages,
)
if self.message
else None
),
enum=self.enum.with_context(collisions=collisions) if self.enum else None,
meta=self.meta.with_context(collisions=collisions),
)
def add_to_address_allowlist(
self,
*,
address_allowlist: Set["metadata.Address"],
resource_messages: Dict[str, "MessageType"],
) -> None:
"""Adds to the set of Addresses of wrapper objects to be included in selective GAPIC generation.
This method is used to create an allowlist of addresses to be used to filter out unneeded
services, methods, messages, and enums at a later step.
Args:
address_allowlist (Set[metadata.Address]): A set of allowlisted metadata.Address
objects to add to. Only the addresses of the allowlisted methods, the services
containing these methods, and messages/enums those methods use will be part of the
final address_allowlist. The set may be modified during this call.
resource_messages (Dict[str, wrappers.MessageType]): A dictionary mapping the unified
resource type name of a resource message to the corresponding MessageType object
representing that resource message. Only resources with a message representation
should be included in the dictionary.
Returns:
None
"""
if self.message:
self.message.add_to_address_allowlist(
address_allowlist=address_allowlist,
resource_messages=resource_messages,
)
if self.enum:
self.enum.add_to_address_allowlist(
address_allowlist=address_allowlist,
)
if self.resource_reference and self.resource_reference in resource_messages:
# The message types in resource_message are different objects, but should be
# defined the same as the MessageTypes we're traversing here.
resource_messages[self.resource_reference].add_to_address_allowlist(
address_allowlist=address_allowlist,
resource_messages=resource_messages,
)
@dataclasses.dataclass(frozen=True)
class FieldHeader:
raw: str
@property
def disambiguated(self) -> str:
return self.raw + "_" if self.raw in utils.RESERVED_NAMES else self.raw
@dataclasses.dataclass(frozen=True)
class Oneof:
"""Description of a field."""
oneof_pb: descriptor_pb2.OneofDescriptorProto
def __getattr__(self, name):
return getattr(self.oneof_pb, name)
@dataclasses.dataclass(frozen=True)
class MessageType:
"""Description of a message (defined with the ``message`` keyword)."""
# Class attributes
# https://google.aip.dev/122
PATH_ARG_RE = re.compile(r"\{([a-zA-Z0-9_\-]+)(?:=\*\*)?\}")
# Instance attributes
message_pb: descriptor_pb2.DescriptorProto
fields: Mapping[str, Field]
nested_enums: Mapping[str, "EnumType"]
nested_messages: Mapping[str, "MessageType"]
meta: metadata.Metadata = dataclasses.field(
default_factory=metadata.Metadata,
)
oneofs: Optional[Mapping[str, "Oneof"]] = None
def __getattr__(self, name):
return getattr(self.message_pb, name)
def __hash__(self):
# Identity is sufficiently unambiguous.
return hash(self.ident)
def oneof_fields(self, include_optional=False):
oneof_fields = collections.defaultdict(list)
for field in self.fields.values():
# Only include proto3 optional oneofs if explicitly looked for.
if field.oneof and not field.proto3_optional or include_optional:
oneof_fields[field.oneof].append(field)
return oneof_fields
@utils.cached_property
def extended_operation_request_fields(self) -> Sequence[Field]:
"""
If this message is the request for a method that uses extended operations,
return the fields that correspond to operation request fields in the operation message.
"""
return tuple(f for f in self.fields.values() if f.operation_request_field)
@utils.cached_property
def extended_operation_response_fields(self) -> Sequence[Field]:
"""
If this message is the request for a method that uses extended operations,
return the fields that correspond to operation response fields in the polling message.
"""
return tuple(f for f in self.fields.values() if f.operation_response_field)
@utils.cached_property
def differently_named_extended_operation_fields(self) -> Optional[Dict[str, Field]]:
if not self.is_extended_operation:
return None
def canonical_name(field):
return OperationResponseMapping.Name(field.operation_field).lower()
OperationResponseMapping = ex_ops_pb2.OperationResponseMapping
default_field_names = [
k.lower()
# The first variant is UNKNOWN
for k in ex_ops_pb2.OperationResponseMapping.keys()[1:]
]
return {
canonical_name(f): f
for f in self.fields.values()
if f.operation_field and f.name not in default_field_names
}
@utils.cached_property
def is_extended_operation(self) -> bool:
if not self.name == "Operation":
return False
name, status, error_code, error_message = False, False, False, False
duplicate_msg = f"Message '{self.name}' has multiple fields with the same operation response mapping: {{}}"
for f in self.fields.values():
maybe_op_mapping = f.options.Extensions[ex_ops_pb2.operation_field]
OperationResponseMapping = ex_ops_pb2.OperationResponseMapping
if maybe_op_mapping == OperationResponseMapping.NAME:
if name:
raise TypeError(duplicate_msg.format("name"))
name = True
if maybe_op_mapping == OperationResponseMapping.STATUS:
if status:
raise TypeError(duplicate_msg.format("status"))
status = True
if maybe_op_mapping == OperationResponseMapping.ERROR_CODE:
if error_code:
raise TypeError(duplicate_msg.format("error_code"))
error_code = True
if maybe_op_mapping == OperationResponseMapping.ERROR_MESSAGE:
if error_message:
raise TypeError(duplicate_msg.format("error_message"))
error_message = True
return name and status and error_code and error_message
@utils.cached_property
def extended_operation_status_field(self) -> Optional[Field]:
STATUS = ex_ops_pb2.OperationResponseMapping.STATUS
return next(
(
f
for f in self.fields.values()
if f.options.Extensions[ex_ops_pb2.operation_field] == STATUS
),
None,
)
@utils.cached_property
def required_fields(self) -> Sequence["Field"]:
required_fields = [field for field in self.fields.values() if field.required]
return required_fields
@utils.cached_property
def field_types(self) -> Sequence[Union["MessageType", "EnumType"]]:
answer = tuple(
field.type for field in self.fields.values() if field.message or field.enum
)
return answer
@utils.cached_property
def recursive_field_types(self) -> Sequence[Union["MessageType", "EnumType"]]:
"""Return all composite fields used in this proto's messages."""
types: Set[Union["MessageType", "EnumType"]] = set()
stack = [iter(self.fields.values())]
while stack:
fields_iter = stack.pop()
for field in fields_iter:
if field.message and field.type not in types:
stack.append(iter(field.message.fields.values()))
if not field.is_primitive:
types.add(field.type)
return tuple(types)
@utils.cached_property
def recursive_resource_fields(self) -> FrozenSet[Field]:
all_fields = chain(
self.fields.values(),
(
field
for t in self.recursive_field_types
if isinstance(t, MessageType)
for field in t.fields.values()
),
)
return frozenset(
f
for f in all_fields
if (
f.options.Extensions[resource_pb2.resource_reference].type
or f.options.Extensions[resource_pb2.resource_reference].child_type
)
)
@property
def map(self) -> bool:
"""Return True if the given message is a map, False otherwise."""
return self.message_pb.options.map_entry
@property
def ident(self) -> metadata.Address:
"""Return the identifier data to be used in templates."""
return self.meta.address
@property
def resource_path(self) -> Optional[str]:
"""If this message describes a resource, return the path to the resource.
If there are multiple paths, returns the first one."""
return next(iter(self.options.Extensions[resource_pb2.resource].pattern), None)
def _apply_domain_heuristic(self, raw_type: str) -> str:
"""Determines if a resource is foreign and adds a prefix to prevent
[no-redef] AST collisions."""
if not raw_type:
return ""
if "/" not in raw_type:
return raw_type
# Extract the root domain and final resource name, bypassing any nested paths.
# (e.g., "ces.googleapis.com/Project/Location/Tool" -> "ces.googleapis.com" and "Tool")
resource_parts = raw_type.split('/')
domain, short_name = resource_parts[0], resource_parts[-1]
domain_prefix = domain.split('.', 1)[0]
try:
native_package = self.meta.address.package
# 2. If the domain prefix isn't natively in the package namespace, it's foreign
if domain_prefix and native_package and domain_prefix not in native_package:
return f"{domain_prefix}_{short_name}"
except (AttributeError, TypeError):
# 3. Safe fallback if meta, address, or package are missing/None on this wrapper
pass
return short_name
@property
def resource_type(self) -> Optional[str]:
resource = self.options.Extensions[resource_pb2.resource]
return self._apply_domain_heuristic(resource.type) if resource else None
@property
def resource_type_full_path(self) -> Optional[str]:
resource = self.options.Extensions[resource_pb2.resource]
return resource.type if resource else None
@property
def resource_path_args(self) -> Sequence[str]:
return self.PATH_ARG_RE.findall(self.resource_path or "")
@property
def resource_path_formatted(self) -> str:
"""
Returns a formatted version of `resource_path`. This re-writes
patterns like: 'projects/{project}/metricDescriptors/{metric_descriptor=**}'
to 'projects/{project}/metricDescriptors/{metric_descriptor}
so it can be used in an f-string.
"""
return self.PATH_ARG_RE.sub(r"{\g<1>}", self.resource_path or "")
@utils.cached_property
def path_regex_str(self) -> str:
# The indirection here is a little confusing:
# we're using the resource path template as the base of a regex,
# with each resource ID segment being captured by a regex.
# E.g., the path schema
# kingdoms/{kingdom}/phyla/{phylum}
# becomes the regex
# ^kingdoms/(?P<kingdom>.+?)/phyla/(?P<phylum>.+?)$
parsing_regex_str = (
"^"
+ self.PATH_ARG_RE.sub(
# We can't just use (?P<name>[^/]+) because segments may be
# separated by delimiters other than '/'.
# Multiple delimiter characters within one schema are allowed,
# e.g.
# as/{a}-{b}/cs/{c}%{d}_{e}
# This is discouraged but permitted by AIP4231
lambda m: "(?P<{name}>.+?)".format(name=m.groups()[0]),
self.resource_path or "",
)
+ "$"
)
# Special case for wildcard resource names
if parsing_regex_str == "^*$":
parsing_regex_str = "^.*$"
return parsing_regex_str
def get_field(
self, *field_path: str, collisions: Optional[Set[str]] = None
) -> Field:
"""Return a field arbitrarily deep in this message's structure.
This method recursively traverses the message tree to return the
requested inner-field.
Traversing through repeated fields is not supported; a repeated field
may be specified if and only if it is the last field in the path.
Args:
field_path (Sequence[str]): The field path.
Returns:
~.Field: A field object.
Raises:
KeyError: If a repeated field is used in the non-terminal position
in the path.
"""
# This covers the case when field_path is a string path.
if len(field_path) == 1 and "." in field_path[0]:
field_path = tuple(field_path[0].split("."))
# If collisions are not explicitly specified, retrieve them
# from this message's address.
# This ensures that calls to `get_field` will return a field with
# the same context, regardless of the number of levels through the
# chain (in order to avoid infinite recursion on circular references,
# we only shallowly bind message references held by fields; this
# binds deeply in the one spot where that might be a problem).
collisions = collisions or self.meta.address.collisions
# Get the first field in the path.
first_field = field_path[0]
cursor = self.fields[
first_field + ("_" if first_field in utils.RESERVED_NAMES else "")
]
# Base case: If this is the last field in the path, return it outright.
if len(field_path) == 1:
return cursor.with_context(
collisions=collisions,
visited_messages=set({self}),
)
# Quick check: If cursor is a repeated field, then raise an exception.
# Repeated fields are only permitted in the terminal position.
if cursor.repeated:
raise KeyError(
f"The {cursor.name} field is repeated; unable to use "
"`get_field` to retrieve its children.\n"
"This exception usually indicates that a "
"google.api.method_signature annotation uses a repeated field "
"in the fields list in a position other than the end.",
)
# Quick check: If this cursor has no message, there is a problem.
if not cursor.message:
raise KeyError(
f"Field {'.'.join(field_path)} could not be resolved from "
f"{cursor.name}.",
)
# Recursion case: Pass the remainder of the path to the sub-field's
# message.
return cursor.message.get_field(*field_path[1:], collisions=collisions)
@cached_proto_context
def with_context(
self,
*,
collisions: Set[str],
skip_fields: bool = False,
visited_messages: Optional[Set["MessageType"]] = None,
) -> "MessageType":
"""Return a derivative of this message with the provided context.
This method is used to address naming collisions. The returned
``MessageType`` object aliases module names to avoid naming collisions
in the file being written.
The ``skip_fields`` argument will omit applying the context to the
underlying fields. This provides for an "exit" in the case of circular
references.
"""
visited_messages = visited_messages or set()
visited_messages = visited_messages | {self}
return dataclasses.replace(
self,
fields=(
{
k: v.with_context(
collisions=collisions, visited_messages=visited_messages
)
for k, v in self.fields.items()
}
if not skip_fields
else self.fields
),
nested_enums={
k: v.with_context(collisions=collisions)
for k, v in self.nested_enums.items()
},
nested_messages={
k: v.with_context(
collisions=collisions,
skip_fields=skip_fields,
visited_messages=visited_messages,
)
for k, v in self.nested_messages.items()
},
meta=self.meta.with_context(collisions=collisions),
)
def add_to_address_allowlist(
self,
*,
address_allowlist: Set["metadata.Address"],
resource_messages: Dict[str, "MessageType"],
) -> None:
"""Adds to the set of Addresses of wrapper objects to be included in selective GAPIC generation.
This method is used to create an allowlist of addresses to be used to filter out unneeded
services, methods, messages, and enums at a later step.
Args:
address_allowlist (Set[metadata.Address]): A set of allowlisted metadata.Address
objects to add to. Only the addresses of the allowlisted methods, the services
containing these methods, and messages/enums those methods use will be part of the
final address_allowlist. The set may be modified during this call.
resource_messages (Dict[str, wrappers.MessageType]): A dictionary mapping the unified
resource type name of a resource message to the corresponding MessageType object
representing that resource message. Only resources with a message representation
should be included in the dictionary.
Returns:
None
"""
if self.ident not in address_allowlist:
address_allowlist.add(self.ident)
for field in self.fields.values():
field.add_to_address_allowlist(
address_allowlist=address_allowlist,
resource_messages=resource_messages,
)
for enum in self.nested_enums.values():
enum.add_to_address_allowlist(
address_allowlist=address_allowlist,
)
for message in self.nested_messages.values():
message.add_to_address_allowlist(
address_allowlist=address_allowlist,
resource_messages=resource_messages,
)
@dataclasses.dataclass(frozen=True)
class EnumValueType:
"""Description of an enum value."""
enum_value_pb: descriptor_pb2.EnumValueDescriptorProto
meta: metadata.Metadata = dataclasses.field(
default_factory=metadata.Metadata,
)
def __getattr__(self, name):
return getattr(self.enum_value_pb, name)
@dataclasses.dataclass(frozen=True)
class EnumType:
"""Description of an enum (defined with the ``enum`` keyword.)"""
enum_pb: descriptor_pb2.EnumDescriptorProto
values: List[EnumValueType]
meta: metadata.Metadata = dataclasses.field(
default_factory=metadata.Metadata,
)
def __hash__(self):
# Identity is sufficiently unambiguous.
return hash(self.ident)
def __getattr__(self, name):
return getattr(self.enum_pb, name)
@property
def resource_path(self) -> Optional[str]:
# This is a minor duck-typing workaround for the resource_messages
# property in the Service class: we need to check fields recursively
# to see if they're resources, and recursive_field_types includes enums
return None
@property
def ident(self) -> metadata.Address:
"""Return the identifier data to be used in templates."""
return self.meta.address
@cached_proto_context
def with_context(self, *, collisions: Set[str]) -> "EnumType":
"""Return a derivative of this enum with the provided context.
This method is used to address naming collisions. The returned
``EnumType`` object aliases module names to avoid naming collisions in
the file being written.
"""
return (
dataclasses.replace(
self,
meta=self.meta.with_context(collisions=collisions),
)
if collisions
else self
)
def add_to_address_allowlist(
self, *, address_allowlist: Set["metadata.Address"]
) -> None:
"""Adds to the set of Addresses of wrapper objects to be included in selective GAPIC generation.
This method is used to create an allowlist of addresses to be used to filter out unneeded
services, methods, messages, and enums at a later step.
Args:
address_allowlist (Set[metadata.Address]): A set of allowlisted metadata.Address
objects to add to. Only the addresses of the allowlisted methods, the services
containing these methods, and messages/enums those methods use will be part of the
final address_allowlist. The set may be modified during this call.
Returns:
None