google-api-core 2.34.0__tar.gz → 2.35.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. {google_api_core-2.34.0/google_api_core.egg-info → google_api_core-2.35.0}/PKG-INFO +1 -1
  2. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/_python_package_support.py +27 -3
  3. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/path_template.py +45 -2
  4. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/version.py +1 -1
  5. {google_api_core-2.34.0 → google_api_core-2.35.0/google_api_core.egg-info}/PKG-INFO +1 -1
  6. {google_api_core-2.34.0 → google_api_core-2.35.0}/pyproject.toml +2 -0
  7. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/test_grpc_helpers_async.py +9 -3
  8. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_grpc_helpers.py +8 -2
  9. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_path_template.py +294 -0
  10. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_python_package_support.py +51 -3
  11. {google_api_core-2.34.0 → google_api_core-2.35.0}/LICENSE +0 -0
  12. {google_api_core-2.34.0 → google_api_core-2.35.0}/MANIFEST.in +0 -0
  13. {google_api_core-2.34.0 → google_api_core-2.35.0}/README.rst +0 -0
  14. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/__init__.py +0 -0
  15. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/_feature_gating_helpers.py +0 -0
  16. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/_python_version_support.py +0 -0
  17. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/_rest_streaming_base.py +0 -0
  18. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/bidi.py +0 -0
  19. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/bidi_async.py +0 -0
  20. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/bidi_base.py +0 -0
  21. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/client_info.py +0 -0
  22. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/client_logging.py +0 -0
  23. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/client_options.py +0 -0
  24. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/datetime_helpers.py +0 -0
  25. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/exceptions.py +0 -0
  26. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/extended_operation.py +0 -0
  27. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/future/__init__.py +0 -0
  28. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/future/_helpers.py +0 -0
  29. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/future/async_future.py +0 -0
  30. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/future/base.py +0 -0
  31. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/future/polling.py +0 -0
  32. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/__init__.py +0 -0
  33. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/client_info.py +0 -0
  34. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/config.py +0 -0
  35. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/config_async.py +0 -0
  36. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/method.py +0 -0
  37. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/method_async.py +0 -0
  38. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/requests.py +0 -0
  39. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/routing_header.py +0 -0
  40. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/general_helpers.py +0 -0
  41. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/grpc_helpers.py +0 -0
  42. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/grpc_helpers_async.py +0 -0
  43. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/iam.py +0 -0
  44. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operation.py +0 -0
  45. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operation_async.py +0 -0
  46. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/__init__.py +0 -0
  47. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/abstract_operations_base_client.py +0 -0
  48. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/abstract_operations_client.py +0 -0
  49. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/operations_async_client.py +0 -0
  50. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/operations_client.py +0 -0
  51. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/operations_client_config.py +0 -0
  52. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/operations_rest_client_async.py +0 -0
  53. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/pagers.py +0 -0
  54. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/pagers_async.py +0 -0
  55. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/pagers_base.py +0 -0
  56. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/transports/__init__.py +0 -0
  57. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/transports/base.py +0 -0
  58. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/transports/rest.py +0 -0
  59. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/operations_v1/transports/rest_asyncio.py +0 -0
  60. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/page_iterator.py +0 -0
  61. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/page_iterator_async.py +0 -0
  62. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/protobuf_helpers.py +0 -0
  63. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/py.typed +0 -0
  64. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/rest_helpers.py +0 -0
  65. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/rest_streaming.py +0 -0
  66. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/rest_streaming_async.py +0 -0
  67. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/retry/__init__.py +0 -0
  68. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/retry/retry_base.py +0 -0
  69. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/retry/retry_streaming.py +0 -0
  70. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/retry/retry_streaming_async.py +0 -0
  71. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/retry/retry_unary.py +0 -0
  72. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/retry/retry_unary_async.py +0 -0
  73. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/retry_async.py +0 -0
  74. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/timeout.py +0 -0
  75. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/universe.py +0 -0
  76. {google_api_core-2.34.0 → google_api_core-2.35.0}/google/api_core/version_header.py +0 -0
  77. {google_api_core-2.34.0 → google_api_core-2.35.0}/google_api_core.egg-info/SOURCES.txt +0 -0
  78. {google_api_core-2.34.0 → google_api_core-2.35.0}/google_api_core.egg-info/dependency_links.txt +0 -0
  79. {google_api_core-2.34.0 → google_api_core-2.35.0}/google_api_core.egg-info/requires.txt +0 -0
  80. {google_api_core-2.34.0 → google_api_core-2.35.0}/google_api_core.egg-info/top_level.txt +0 -0
  81. {google_api_core-2.34.0 → google_api_core-2.35.0}/setup.cfg +0 -0
  82. {google_api_core-2.34.0 → google_api_core-2.35.0}/setup.py +0 -0
  83. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/__init__.py +0 -0
  84. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/__init__.py +0 -0
  85. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/future/__init__.py +0 -0
  86. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/future/test_async_future.py +0 -0
  87. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/gapic/test_config_async.py +0 -0
  88. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/gapic/test_method_async.py +0 -0
  89. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/operations_v1/__init__.py +0 -0
  90. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/operations_v1/test_operations_async_client.py +0 -0
  91. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/retry/__init__.py +0 -0
  92. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/retry/test_retry_streaming_async.py +0 -0
  93. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/retry/test_retry_unary_async.py +0 -0
  94. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/test_bidi_async.py +0 -0
  95. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/test_operation_async.py +0 -0
  96. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/test_page_iterator_async.py +0 -0
  97. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/asyncio/test_rest_streaming_async.py +0 -0
  98. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/conftest.py +0 -0
  99. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/helpers.py +0 -0
  100. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/__init__.py +0 -0
  101. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/future/__init__.py +0 -0
  102. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/future/test__helpers.py +0 -0
  103. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/future/test_polling.py +0 -0
  104. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/gapic/test_client_info.py +0 -0
  105. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/gapic/test_config.py +0 -0
  106. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/gapic/test_method.py +0 -0
  107. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/gapic/test_requests.py +0 -0
  108. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/gapic/test_routing_header.py +0 -0
  109. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/operations_v1/__init__.py +0 -0
  110. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/operations_v1/test_operations_client.py +0 -0
  111. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/operations_v1/test_operations_rest_client.py +0 -0
  112. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/retry/__init__.py +0 -0
  113. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/retry/test_retry_base.py +0 -0
  114. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/retry/test_retry_imports.py +0 -0
  115. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/retry/test_retry_streaming.py +0 -0
  116. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/retry/test_retry_unary.py +0 -0
  117. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_bidi.py +0 -0
  118. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_client_info.py +0 -0
  119. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_client_logging.py +0 -0
  120. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_client_options.py +0 -0
  121. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_datetime_helpers.py +0 -0
  122. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_exceptions.py +0 -0
  123. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_extended_operation.py +0 -0
  124. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_feature_gating_helpers.py +0 -0
  125. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_iam.py +0 -0
  126. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_operation.py +0 -0
  127. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_packaging.py +0 -0
  128. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_page_iterator.py +0 -0
  129. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_protobuf_helpers.py +0 -0
  130. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_python_version_support.py +0 -0
  131. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_rest_helpers.py +0 -0
  132. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_rest_streaming.py +0 -0
  133. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_timeout.py +0 -0
  134. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_universe.py +0 -0
  135. {google_api_core-2.34.0 → google_api_core-2.35.0}/tests/unit/test_version_header.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-api-core
3
- Version: 2.34.0
3
+ Version: 2.35.0
4
4
  Summary: Google API client core library
5
5
  Author-email: Google LLC <googleapis-packages@google.com>
6
6
  License: Apache 2.0
@@ -30,14 +30,37 @@ ParsedVersion = Tuple[int, ...]
30
30
  # about deprecated and unsupported versions.
31
31
  DependencyConstraint = namedtuple(
32
32
  "DependencyConstraint",
33
- ["package_name", "minimum_fully_supported_version", "recommended_version"],
33
+ [
34
+ "package_name",
35
+ "minimum_fully_supported_version",
36
+ "recommended_version",
37
+ "message_template",
38
+ ],
39
+ defaults=(None, None),
34
40
  )
41
+
42
+ PQC_GRPC_WARNING_TEMPLATE = (
43
+ "Package {consumer_package} depends on {dependency_package}, currently installed at version {version_used_string}. "
44
+ "grpcio < 1.83.0 does not support Post-Quantum Cryptography (PQC). "
45
+ "Support for non-PQC environments is deprecated. In October 2026, "
46
+ "Google Cloud Python packages will raise their minimum requirements "
47
+ "(including google-api-core, grpcio, and grpcio-status) to enforce grpcio >= 1.83.0. "
48
+ "For more details on Google Cloud's post-quantum security migration, visit: "
49
+ "https://cloud.google.com/security/resources/post-quantum-cryptography"
50
+ )
51
+
35
52
  _PACKAGE_DEPENDENCY_WARNINGS = [
36
53
  DependencyConstraint(
37
54
  "google.protobuf",
38
55
  minimum_fully_supported_version="6.33.5",
39
56
  recommended_version="6.x",
40
- )
57
+ ),
58
+ DependencyConstraint(
59
+ "grpcio",
60
+ minimum_fully_supported_version="1.83.0",
61
+ recommended_version="1.83.x",
62
+ message_template=PQC_GRPC_WARNING_TEMPLATE,
63
+ ),
41
64
  ]
42
65
 
43
66
 
@@ -162,7 +185,7 @@ def warn_deprecation_for_versions_less_than(
162
185
  ) = _get_distribution_and_import_packages(consumer_import_package)
163
186
 
164
187
  recommendation = (
165
- " (we recommend {recommended_version})" if recommended_version else ""
188
+ f" (we recommend {recommended_version})" if recommended_version else ""
166
189
  )
167
190
  message_template = message_template or _flatten_message(
168
191
  """
@@ -222,4 +245,5 @@ def check_dependency_versions(
222
245
  package_info.package_name,
223
246
  package_info.minimum_fully_supported_version,
224
247
  recommended_version=package_info.recommended_version,
248
+ message_template=package_info.message_template,
225
249
  )
@@ -28,6 +28,7 @@ from __future__ import unicode_literals
28
28
  import copy
29
29
  import functools
30
30
  import re
31
+ import urllib.parse
31
32
  from collections import deque
32
33
 
33
34
  # Regular expression for extracting variable parts from a path template.
@@ -62,6 +63,41 @@ _SINGLE_SEGMENT_PATTERN = r"([^/]+)"
62
63
  _MULTI_SEGMENT_PATTERN = r"(.+)"
63
64
 
64
65
 
66
+ def _extract_and_validate_wildcards(
67
+ val: str, template_str: str | None, property_name: str = "property"
68
+ ) -> None:
69
+ """Extract and validate wildcard variables against path traversal.
70
+
71
+ Ensures that values matched against single-segment wildcards ('*') are
72
+ not '.' or '..', and values matched against multi-segment wildcards ('**')
73
+ do not contain '.' or '..' segments.
74
+
75
+ If the value does not match the sub-template structure, validation is
76
+ deferred to allow subsequent bindings to be evaluated.
77
+
78
+ Args:
79
+ val (str): The raw string value to validate.
80
+ template_str (str | None): The template string of the variable (e.g.
81
+ 'projects/*/locations/*').
82
+ property_name (str): The name of the property being validated, for use
83
+ in error messages.
84
+
85
+ Raises:
86
+ ValueError: If a wildcard value contains invalid dot segments.
87
+ """
88
+ tmpl = template_str or "*"
89
+ m = re.fullmatch(_generate_pattern_for_template(tmpl), val)
90
+ if m is not None:
91
+ groups = m.groups()
92
+ for g in groups:
93
+ if "." in g and any(seg in (".", "..") for seg in g.split("/")):
94
+ if "**" in tmpl or "/" in tmpl:
95
+ raise ValueError(
96
+ f"Value for {property_name} must not contain segments that are exactly . or .. ."
97
+ )
98
+ raise ValueError(f"Invalid value {g} for {property_name}.")
99
+
100
+
65
101
  def _expand_variable_match(positional_vars, named_vars, match):
66
102
  """Expand a matched variable with its value.
67
103
 
@@ -81,9 +117,13 @@ def _expand_variable_match(positional_vars, named_vars, match):
81
117
  """
82
118
  positional = match.group("positional")
83
119
  name = match.group("name")
120
+ template = match.group("template")
121
+
84
122
  if name is not None:
85
123
  try:
86
- return str(named_vars[name])
124
+ val = str(named_vars[name])
125
+ _extract_and_validate_wildcards(val, template, name)
126
+ return urllib.parse.quote(val, safe="/")
87
127
  except KeyError:
88
128
  raise ValueError(
89
129
  "Named variable '{}' not specified and needed by template "
@@ -91,7 +131,9 @@ def _expand_variable_match(positional_vars, named_vars, match):
91
131
  )
92
132
  elif positional is not None:
93
133
  try:
94
- return str(positional_vars.pop(0))
134
+ val = str(positional_vars.pop(0))
135
+ _extract_and_validate_wildcards(val, positional, "positional variable")
136
+ return urllib.parse.quote(val, safe="/")
95
137
  except IndexError:
96
138
  raise ValueError(
97
139
  "Positional variable not specified and needed by template "
@@ -159,6 +201,7 @@ def _replace_variable_with_pattern(match):
159
201
  raise ValueError("Unknown template expression {}".format(match.group(0)))
160
202
 
161
203
 
204
+ @functools.lru_cache(maxsize=256)
162
205
  def _generate_pattern_for_template(tmpl):
163
206
  """Generate a pattern that can validate a path template.
164
207
 
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "2.34.0"
15
+ __version__ = "2.35.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-api-core
3
- Version: 2.34.0
3
+ Version: 2.35.0
4
4
  Summary: Google API client core library
5
5
  Author-email: Google LLC <googleapis-packages@google.com>
6
6
  License: Apache 2.0
@@ -85,6 +85,8 @@ filterwarnings = [
85
85
  "error",
86
86
  # Prevent Python version warnings from interfering with tests
87
87
  "ignore:.* Python version .*:FutureWarning",
88
+ # TODO: Remove once b/544782314 is resolved (when grpcio >= 1.83.0 is enforced)
89
+ "ignore:.*does not support Post-Quantum Cryptography.*:FutureWarning",
88
90
  # Remove once https://github.com/pytest-dev/pytest-cov/issues/621 is fixed
89
91
  "ignore:.*The --rsyncdir command line argument and rsyncdirs config variable are deprecated:DeprecationWarning",
90
92
  # Remove once https://github.com/protocolbuffers/protobuf/issues/12186 is fixed
@@ -421,9 +421,15 @@ def test_create_channel_implicit_with_default_host(
421
421
  assert channel is grpc_secure_channel.return_value
422
422
 
423
423
  google_auth_default.assert_called_once_with(scopes=None, default_scopes=None)
424
- auth_metadata_plugin.assert_called_once_with(
425
- mock.sentinel.credentials, mock.sentinel.Request, default_host=default_host
426
- )
424
+ assert auth_metadata_plugin.call_count == 1
425
+ assert auth_metadata_plugin.call_args.args == (
426
+ mock.sentinel.credentials,
427
+ mock.sentinel.Request,
428
+ )
429
+ assert auth_metadata_plugin.call_args.kwargs["default_host"] == default_host
430
+ # google-auth >= 2.56.3 supports suppress_metrics_header; older versions omit it during fallback
431
+ if "suppress_metrics_header" in auth_metadata_plugin.call_args.kwargs:
432
+ assert auth_metadata_plugin.call_args.kwargs["suppress_metrics_header"] is True
427
433
  grpc_secure_channel.assert_called_once_with(
428
434
  expected_target, composite_creds, compression=None
429
435
  )
@@ -453,9 +453,15 @@ def test_create_channel_implicit_with_default_host(
453
453
  assert channel is grpc_secure_channel.return_value
454
454
 
455
455
  google_auth_default.assert_called_once_with(scopes=None, default_scopes=None)
456
- auth_metadata_plugin.assert_called_once_with(
457
- mock.sentinel.credentials, mock.sentinel.Request, default_host=default_host
456
+ assert auth_metadata_plugin.call_count == 1
457
+ assert auth_metadata_plugin.call_args.args == (
458
+ mock.sentinel.credentials,
459
+ mock.sentinel.Request,
458
460
  )
461
+ assert auth_metadata_plugin.call_args.kwargs["default_host"] == default_host
462
+ # google-auth >= 2.56.3 supports suppress_metrics_header; older versions omit it during fallback
463
+ if "suppress_metrics_header" in auth_metadata_plugin.call_args.kwargs:
464
+ assert auth_metadata_plugin.call_args.kwargs["suppress_metrics_header"] is True
459
465
 
460
466
  grpc_secure_channel.assert_called_once_with(
461
467
  expected_target, composite_creds, compression=None
@@ -64,6 +64,29 @@ from google.api_core import path_template
64
64
  {"name": "parent/child/object"},
65
65
  "/v1/a/parent/child/object",
66
66
  ],
67
+ # Encoding / Metacharacters in positional and named params
68
+ ["/v1/*", ["..?$httpMethod=DELETE#"], {}, "/v1/..%3F%24httpMethod%3DDELETE%23"],
69
+ ["/v1/**", ["path/sub/with/?and#"], {}, "/v1/path/sub/with/%3Fand%23"],
70
+ [
71
+ "/v1/{name}",
72
+ [],
73
+ {"name": "..?$httpMethod=DELETE#"},
74
+ "/v1/..%3F%24httpMethod%3DDELETE%23",
75
+ ],
76
+ [
77
+ "/v1/{name=**}",
78
+ [],
79
+ {"name": "path/sub/with/?and#"},
80
+ "/v1/path/sub/with/%3Fand%23",
81
+ ],
82
+ [
83
+ "/v3/{session=projects/*/locations/*/agents/*/sessions/*}:detectIntent",
84
+ [],
85
+ {
86
+ "session": "projects/cx/locations/global/agents/a1/sessions/..?$httpMethod=DELETE#"
87
+ },
88
+ "/v3/projects/cx/locations/global/agents/a1/sessions/..%3F%24httpMethod%3DDELETE%23:detectIntent",
89
+ ],
67
90
  ],
68
91
  )
69
92
  def test_expand_success(tmpl, args, kwargs, expected_result):
@@ -651,3 +674,274 @@ def helper_test_transcode(http_options_list, expected_result_list):
651
674
  if expected_result_list[2]:
652
675
  expected_result["body"] = expected_result_list[2]
653
676
  return (http_options, expected_result)
677
+
678
+
679
+ @pytest.mark.parametrize(
680
+ "tmpl, args, kwargs, expected_err_match",
681
+ [
682
+ # Positional * with . or ..
683
+ [
684
+ "/v1/*",
685
+ ["."],
686
+ {},
687
+ r"^Invalid value \. for positional variable\.$",
688
+ ],
689
+ [
690
+ "/v1/*",
691
+ [".."],
692
+ {},
693
+ r"^Invalid value \.\. for positional variable\.$",
694
+ ],
695
+ # Named matching * with . or ..
696
+ [
697
+ "/compute/v1/projects/{project}/regions/{region}/addresses",
698
+ [],
699
+ {"project": "my-project", "region": ".."},
700
+ r"^Invalid value \.\. for region\.$",
701
+ ],
702
+ [
703
+ "/compute/v1/projects/{project}/regions/{region}/addresses",
704
+ [],
705
+ {"project": "my-project", "region": "."},
706
+ r"^Invalid value \. for region\.$",
707
+ ],
708
+ # Sub-template named matching * with . or ..
709
+ [
710
+ "/v2/{parent=projects/*/locations/*}/content:inspect",
711
+ [],
712
+ {"parent": "projects/my-project/locations/.."},
713
+ r"^Value for parent must not contain segments that are exactly \. or \.\. \.$",
714
+ ],
715
+ [
716
+ "/v2/{parent=projects/*/locations/*}/content:inspect",
717
+ [],
718
+ {"parent": "projects/my-project/locations/."},
719
+ r"^Value for parent must not contain segments that are exactly \. or \.\. \.$",
720
+ ],
721
+ [
722
+ "/v2/{parent=projects/*/locations/*}/content:inspect",
723
+ [],
724
+ {"parent": "projects/../locations/us-central1"},
725
+ r"^Value for parent must not contain segments that are exactly \. or \.\. \.$",
726
+ ],
727
+ [
728
+ "/v2/{parent=projects/*/locations/*}/content:inspect",
729
+ [],
730
+ {"parent": "projects/./locations/us-central1"},
731
+ r"^Value for parent must not contain segments that are exactly \. or \.\. \.$",
732
+ ],
733
+ ],
734
+ )
735
+ def test_path_traversal_dots_validation_star(tmpl, args, kwargs, expected_err_match):
736
+ with pytest.raises(ValueError, match=expected_err_match):
737
+ path_template.expand(tmpl, *args, **kwargs)
738
+
739
+
740
+ @pytest.mark.parametrize(
741
+ "name_val, expected_path",
742
+ [
743
+ (
744
+ "projects/my-project/monitoredResourceDescriptors/instance/my-instance",
745
+ "/v3/projects/my-project/monitoredResourceDescriptors/instance/my-instance",
746
+ ),
747
+ (
748
+ "projects/my-project/monitoredResourceDescriptors/a/b/c/d/e",
749
+ "/v3/projects/my-project/monitoredResourceDescriptors/a/b/c/d/e",
750
+ ),
751
+ (
752
+ "projects/my-project/monitoredResourceDescriptors/instance...foo/bar",
753
+ "/v3/projects/my-project/monitoredResourceDescriptors/instance...foo/bar",
754
+ ),
755
+ (
756
+ "projects/my-project/monitoredResourceDescriptors/...",
757
+ "/v3/projects/my-project/monitoredResourceDescriptors/...",
758
+ ),
759
+ ],
760
+ )
761
+ def test_path_traversal_dots_validation_double_star_valid(name_val, expected_path):
762
+ assert (
763
+ path_template.expand(
764
+ "/v3/{name=projects/*/monitoredResourceDescriptors/**}",
765
+ name=name_val,
766
+ )
767
+ == expected_path
768
+ )
769
+
770
+
771
+ @pytest.mark.parametrize(
772
+ "name_val",
773
+ [
774
+ "projects/my-project/monitoredResourceDescriptors/instance/my-instance/..",
775
+ "projects/my-project/monitoredResourceDescriptors/instance/my-instance/.",
776
+ "projects/my-project/monitoredResourceDescriptors/instance/my-instance/../..",
777
+ "projects/my-project/monitoredResourceDescriptors/instance/../my-instance/..",
778
+ "projects/my-project/monitoredResourceDescriptors/instance/../..",
779
+ "projects/my-project/monitoredResourceDescriptors/a/b/../../../c/d/e/..",
780
+ "projects/my-project/monitoredResourceDescriptors/a/b/c/d/e/../../../..",
781
+ "projects/my-project/monitoredResourceDescriptors/instance//..",
782
+ ],
783
+ )
784
+ def test_path_traversal_dots_validation_double_star_invalid(name_val):
785
+ with pytest.raises(
786
+ ValueError,
787
+ match=r"^Value for name must not contain segments that are exactly \. or \.\. \.$",
788
+ ):
789
+ path_template.expand(
790
+ "/v3/{name=projects/*/monitoredResourceDescriptors/**}",
791
+ name=name_val,
792
+ )
793
+
794
+
795
+ @pytest.mark.parametrize(
796
+ "tmpl, kwargs, expected_result",
797
+ [
798
+ ["/v1/{name}", {"name": "abc-._~"}, "/v1/abc-._~"],
799
+ ["/v1/{name=**}", {"name": "abc-._~/"}, "/v1/abc-._~/"],
800
+ ["/v1/{name}", {"name": "a/b"}, "/v1/a/b"],
801
+ ["/v1/{name}", {"name": "a$b?c=d#e f"}, "/v1/a%24b%3Fc%3Dd%23e%20f"],
802
+ ],
803
+ )
804
+ def test_percent_encoding_unreserved_characters(tmpl, kwargs, expected_result):
805
+ result = path_template.expand(tmpl, **kwargs)
806
+ assert result == expected_result
807
+ # For single-segment with '/', validate should fail because '/' is preserved
808
+ if "/" in kwargs.get("name", "") and tmpl == "/v1/{name}":
809
+ assert not path_template.validate(tmpl, result)
810
+
811
+
812
+ @pytest.mark.parametrize(
813
+ "tmpl, args, kwargs, expected_err_match",
814
+ [
815
+ (
816
+ "/v1/**",
817
+ [".."],
818
+ {},
819
+ r"^Value for positional variable must not contain segments that are exactly \. or \.\. \.$",
820
+ ),
821
+ (
822
+ "/v1/**",
823
+ ["a/./b"],
824
+ {},
825
+ r"^Value for positional variable must not contain segments that are exactly \. or \.\. \.$",
826
+ ),
827
+ (
828
+ "/v1/{name=**}",
829
+ [],
830
+ {"name": ".."},
831
+ r"^Value for name must not contain segments that are exactly \. or \.\. \.$",
832
+ ),
833
+ (
834
+ "/v1/{name=**}",
835
+ [],
836
+ {"name": "a/../b"},
837
+ r"^Value for name must not contain segments that are exactly \. or \.\. \.$",
838
+ ),
839
+ ],
840
+ )
841
+ def test_path_traversal_dots_validation_bare_double_star(
842
+ tmpl, args, kwargs, expected_err_match
843
+ ):
844
+ with pytest.raises(ValueError, match=expected_err_match):
845
+ path_template.expand(tmpl, *args, **kwargs)
846
+
847
+
848
+ @pytest.mark.parametrize(
849
+ "name_val, expected_err_match",
850
+ [
851
+ (
852
+ "projects/../locations/us-central1/buckets/my-bucket",
853
+ r"^Value for name must not contain segments that are exactly \. or \.\. \.$",
854
+ ),
855
+ (
856
+ "projects/my-project/locations/./buckets/my-bucket",
857
+ r"^Value for name must not contain segments that are exactly \. or \.\. \.$",
858
+ ),
859
+ (
860
+ "projects/my-project/locations/us-central1/buckets/foo/../bar",
861
+ r"^Value for name must not contain segments that are exactly \. or \.\. \.$",
862
+ ),
863
+ (
864
+ "projects/my-project/locations/us-central1/buckets/.",
865
+ r"^Value for name must not contain segments that are exactly \. or \.\. \.$",
866
+ ),
867
+ (
868
+ "projects/my-project/locations/us-central1/buckets/..",
869
+ r"^Value for name must not contain segments that are exactly \. or \.\. \.$",
870
+ ),
871
+ ],
872
+ )
873
+ def test_path_traversal_mixed_subtemplate(name_val, expected_err_match):
874
+ tmpl = "/v1/{name=projects/*/locations/*/buckets/**}"
875
+ with pytest.raises(ValueError, match=expected_err_match):
876
+ path_template.expand(tmpl, name=name_val)
877
+
878
+
879
+ @pytest.mark.parametrize(
880
+ "tmpl, args, kwargs, expected_err_match",
881
+ [
882
+ (
883
+ "/v1/*/**",
884
+ [".", "a/b"],
885
+ {},
886
+ r"^Invalid value \. for positional variable\.$",
887
+ ),
888
+ (
889
+ "/v1/*/**",
890
+ ["..", "a/b"],
891
+ {},
892
+ r"^Invalid value \.\. for positional variable\.$",
893
+ ),
894
+ (
895
+ "/v1/*/**",
896
+ ["a", "b/../c"],
897
+ {},
898
+ r"^Value for positional variable must not contain segments that are exactly \. or \.\. \.$",
899
+ ),
900
+ (
901
+ "/v1/*/**",
902
+ ["a", "b/./c"],
903
+ {},
904
+ r"^Value for positional variable must not contain segments that are exactly \. or \.\. \.$",
905
+ ),
906
+ ],
907
+ )
908
+ def test_path_traversal_multiple_positional_wildcards(
909
+ tmpl, args, kwargs, expected_err_match
910
+ ):
911
+ with pytest.raises(ValueError, match=expected_err_match):
912
+ path_template.expand(tmpl, *args, **kwargs)
913
+
914
+
915
+ @pytest.mark.parametrize(
916
+ "tmpl, kwargs, expected_result",
917
+ [
918
+ ("/v1/{name}", {"name": ".hidden"}, "/v1/.hidden"),
919
+ ("/v1/{name}", {"name": "..hidden"}, "/v1/..hidden"),
920
+ ("/v1/{name}", {"name": "file.txt"}, "/v1/file.txt"),
921
+ ("/v1/{name=**}", {"name": "a/.hidden/b"}, "/v1/a/.hidden/b"),
922
+ ("/v1/{name=**}", {"name": "a/..hidden/b"}, "/v1/a/..hidden/b"),
923
+ ("/v1/{name=**}", {"name": "a/file.tar.gz/b"}, "/v1/a/file.tar.gz/b"),
924
+ ],
925
+ )
926
+ def test_valid_dot_containing_names(tmpl, kwargs, expected_result):
927
+ assert path_template.expand(tmpl, **kwargs) == expected_result
928
+
929
+
930
+ def test_transcode_routing_with_path_traversal_fallback():
931
+ http_options = [
932
+ {"method": "get", "uri": "/v1/{name=projects/*/locations/*}"},
933
+ {"method": "get", "uri": "/v1/{name=projects/*}"},
934
+ ]
935
+
936
+ # Non-matching subtemplate structure falls through to secondary binding
937
+ result = path_template.transcode(http_options, None, name="projects/my-project")
938
+ assert result["uri"] == "/v1/projects/my-project"
939
+
940
+ # Structurally matching subtemplate with invalid dot segment raises immediately
941
+ with pytest.raises(
942
+ ValueError,
943
+ match=r"^Value for name must not contain segments that are exactly \. or \.\. \.$",
944
+ ):
945
+ path_template.transcode(
946
+ http_options, None, name="projects/my-project/locations/."
947
+ )
@@ -18,6 +18,7 @@ from unittest.mock import patch
18
18
  import pytest
19
19
 
20
20
  from google.api_core._python_package_support import (
21
+ PQC_GRPC_WARNING_TEMPLATE,
21
22
  DependencyConstraint,
22
23
  DependencyVersion,
23
24
  check_dependency_versions,
@@ -37,7 +38,6 @@ def test_get_dependency_version(mocker, version_string_to_test):
37
38
  parse_version_to_tuple(version_string_to_test), version_string_to_test
38
39
  )
39
40
  assert get_dependency_version("some-package") == expected
40
-
41
41
  mock_importlib.assert_called_once_with("some-package")
42
42
 
43
43
  # Test package not found
@@ -112,6 +112,22 @@ def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_pack
112
112
  in str(record[0].message)
113
113
  )
114
114
 
115
+ # Case 6: Recommended version is properly formatted.
116
+ mock_get_packages.reset_mock()
117
+ mock_get_packages.side_effect = [
118
+ ("dep-package (dep.package)", "dep-package"),
119
+ ("my-package (my.package)", "my-package"),
120
+ ]
121
+ mock_get_version.return_value = DependencyVersion(
122
+ parse_version_to_tuple("1.0.0"), "1.0.0"
123
+ )
124
+ with pytest.warns(FutureWarning) as record:
125
+ warn_deprecation_for_versions_less_than(
126
+ "my.package", "dep.package", "2.0.0", recommended_version="3.0.0"
127
+ )
128
+ assert len(record) == 1
129
+ assert "version 2.0.0 or higher (we recommend 3.0.0)." in str(record[0].message)
130
+
115
131
 
116
132
  @patch(
117
133
  "google.api_core._python_package_support.warn_deprecation_for_versions_less_than"
@@ -125,8 +141,40 @@ def test_check_dependency_versions_with_custom_warnings(mock_warn):
125
141
 
126
142
  assert mock_warn.call_count == 2
127
143
  mock_warn.assert_any_call(
128
- "my-consumer", "pkg1", "1.0.0", recommended_version="2.0.0"
144
+ "my-consumer",
145
+ "pkg1",
146
+ "1.0.0",
147
+ recommended_version="2.0.0",
148
+ message_template=None,
149
+ )
150
+ mock_warn.assert_any_call(
151
+ "my-consumer",
152
+ "pkg2",
153
+ "2.0.0",
154
+ recommended_version="3.0.0",
155
+ message_template=None,
156
+ )
157
+
158
+
159
+ @patch(
160
+ "google.api_core._python_package_support.warn_deprecation_for_versions_less_than"
161
+ )
162
+ def test_check_dependency_versions_default(mock_warn):
163
+ """Test check_dependency_versions with default package dependency warnings."""
164
+ check_dependency_versions("my-consumer")
165
+
166
+ assert mock_warn.call_count == 2
167
+ mock_warn.assert_any_call(
168
+ "my-consumer",
169
+ "google.protobuf",
170
+ "6.33.5",
171
+ recommended_version="6.x",
172
+ message_template=None,
129
173
  )
130
174
  mock_warn.assert_any_call(
131
- "my-consumer", "pkg2", "2.0.0", recommended_version="3.0.0"
175
+ "my-consumer",
176
+ "grpcio",
177
+ "1.83.0",
178
+ recommended_version="1.83.x",
179
+ message_template=PQC_GRPC_WARNING_TEMPLATE,
132
180
  )