Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/cloud/_helpers/__init__.py: 50%

149 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:40 +0000

1# Copyright 2014 Google LLC 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15"""Shared helpers for Google Cloud packages. 

16 

17This module is not part of the public API surface. 

18""" 

19 

20from __future__ import absolute_import 

21 

22import calendar 

23import datetime 

24import http.client 

25import os 

26import re 

27from threading import local as Local 

28from typing import Union 

29 

30import google.auth 

31import google.auth.transport.requests 

32from google.protobuf import duration_pb2 

33from google.protobuf import timestamp_pb2 

34 

35try: 

36 import grpc 

37 import google.auth.transport.grpc 

38except ImportError: # pragma: NO COVER 

39 grpc = None 

40 

41# `google.cloud._helpers._NOW` is deprecated 

42_NOW = datetime.datetime.utcnow 

43UTC = datetime.timezone.utc # Singleton instance to be used throughout. 

44_EPOCH = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) 

45 

46_RFC3339_MICROS = "%Y-%m-%dT%H:%M:%S.%fZ" 

47_RFC3339_NO_FRACTION = "%Y-%m-%dT%H:%M:%S" 

48_TIMEONLY_W_MICROS = "%H:%M:%S.%f" 

49_TIMEONLY_NO_FRACTION = "%H:%M:%S" 

50# datetime.strptime cannot handle nanosecond precision: parse w/ regex 

51_RFC3339_NANOS = re.compile( 

52 r""" 

53 (?P<no_fraction> 

54 \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2} # YYYY-MM-DDTHH:MM:SS 

55 ) 

56 ( # Optional decimal part 

57 \. # decimal point 

58 (?P<nanos>\d{1,9}) # nanoseconds, maybe truncated 

59 )? 

60 Z # Zulu 

61""", 

62 re.VERBOSE, 

63) 

64# NOTE: Catching this ImportError is a workaround for GAE not supporting the 

65# "pwd" module which is imported lazily when "expanduser" is called. 

66_USER_ROOT: Union[str, None] 

67try: 

68 _USER_ROOT = os.path.expanduser("~") 

69except ImportError: # pragma: NO COVER 

70 _USER_ROOT = None 

71_GCLOUD_CONFIG_FILE = os.path.join("gcloud", "configurations", "config_default") 

72_GCLOUD_CONFIG_SECTION = "core" 

73_GCLOUD_CONFIG_KEY = "project" 

74 

75 

76class _LocalStack(Local): 

77 """Manage a thread-local LIFO stack of resources. 

78 

79 Intended for use in :class:`google.cloud.datastore.batch.Batch.__enter__`, 

80 :class:`google.cloud.storage.batch.Batch.__enter__`, etc. 

81 """ 

82 

83 def __init__(self): 

84 super(_LocalStack, self).__init__() 

85 self._stack = [] 

86 

87 def __iter__(self): 

88 """Iterate the stack in LIFO order.""" 

89 return iter(reversed(self._stack)) 

90 

91 def push(self, resource): 

92 """Push a resource onto our stack.""" 

93 self._stack.append(resource) 

94 

95 def pop(self): 

96 """Pop a resource from our stack. 

97 

98 :rtype: object 

99 :returns: the top-most resource, after removing it. 

100 :raises IndexError: if the stack is empty. 

101 """ 

102 return self._stack.pop() 

103 

104 @property 

105 def top(self): 

106 """Get the top-most resource 

107 

108 :rtype: object 

109 :returns: the top-most item, or None if the stack is empty. 

110 """ 

111 if self._stack: 

112 return self._stack[-1] 

113 

114 

115def _ensure_tuple_or_list(arg_name, tuple_or_list): 

116 """Ensures an input is a tuple or list. 

117 

118 This effectively reduces the iterable types allowed to a very short 

119 allowlist: list and tuple. 

120 

121 :type arg_name: str 

122 :param arg_name: Name of argument to use in error message. 

123 

124 :type tuple_or_list: sequence of str 

125 :param tuple_or_list: Sequence to be verified. 

126 

127 :rtype: list of str 

128 :returns: The ``tuple_or_list`` passed in cast to a ``list``. 

129 :raises TypeError: if the ``tuple_or_list`` is not a tuple or list. 

130 """ 

131 if not isinstance(tuple_or_list, (tuple, list)): 

132 raise TypeError( 

133 "Expected %s to be a tuple or list. " 

134 "Received %r" % (arg_name, tuple_or_list) 

135 ) 

136 return list(tuple_or_list) 

137 

138 

139def _determine_default_project(project=None): 

140 """Determine default project ID explicitly or implicitly as fall-back. 

141 

142 See :func:`google.auth.default` for details on how the default project 

143 is determined. 

144 

145 :type project: str 

146 :param project: Optional. The project name to use as default. 

147 

148 :rtype: str or ``NoneType`` 

149 :returns: Default project if it can be determined. 

150 """ 

151 if project is None: 

152 _, project = google.auth.default() 

153 return project 

154 

155 

156def _millis(when): 

157 """Convert a zone-aware datetime to integer milliseconds. 

158 

159 :type when: :class:`datetime.datetime` 

160 :param when: the datetime to convert 

161 

162 :rtype: int 

163 :returns: milliseconds since epoch for ``when`` 

164 """ 

165 micros = _microseconds_from_datetime(when) 

166 return micros // 1000 

167 

168 

169def _datetime_from_microseconds(value): 

170 """Convert timestamp to datetime, assuming UTC. 

171 

172 :type value: float 

173 :param value: The timestamp to convert 

174 

175 :rtype: :class:`datetime.datetime` 

176 :returns: The datetime object created from the value. 

177 """ 

178 return _EPOCH + datetime.timedelta(microseconds=value) 

179 

180 

181def _microseconds_from_datetime(value): 

182 """Convert non-none datetime to microseconds. 

183 

184 :type value: :class:`datetime.datetime` 

185 :param value: The timestamp to convert. 

186 

187 :rtype: int 

188 :returns: The timestamp, in microseconds. 

189 """ 

190 if not value.tzinfo: 

191 value = value.replace(tzinfo=UTC) 

192 # Regardless of what timezone is on the value, convert it to UTC. 

193 value = value.astimezone(UTC) 

194 # Convert the datetime to a microsecond timestamp. 

195 return int(calendar.timegm(value.timetuple()) * 1e6) + value.microsecond 

196 

197 

198def _millis_from_datetime(value): 

199 """Convert non-none datetime to timestamp, assuming UTC. 

200 

201 :type value: :class:`datetime.datetime` 

202 :param value: (Optional) the timestamp 

203 

204 :rtype: int, or ``NoneType`` 

205 :returns: the timestamp, in milliseconds, or None 

206 """ 

207 if value is not None: 

208 return _millis(value) 

209 

210 

211def _date_from_iso8601_date(value): 

212 """Convert a ISO8601 date string to native datetime date 

213 

214 :type value: str 

215 :param value: The date string to convert 

216 

217 :rtype: :class:`datetime.date` 

218 :returns: A datetime date object created from the string 

219 

220 """ 

221 return datetime.datetime.strptime(value, "%Y-%m-%d").date() 

222 

223 

224def _time_from_iso8601_time_naive(value): 

225 """Convert a zoneless ISO8601 time string to naive datetime time 

226 

227 :type value: str 

228 :param value: The time string to convert 

229 

230 :rtype: :class:`datetime.time` 

231 :returns: A datetime time object created from the string 

232 :raises ValueError: if the value does not match a known format. 

233 """ 

234 if len(value) == 8: # HH:MM:SS 

235 fmt = _TIMEONLY_NO_FRACTION 

236 elif len(value) == 15: # HH:MM:SS.micros 

237 fmt = _TIMEONLY_W_MICROS 

238 else: 

239 raise ValueError("Unknown time format: {}".format(value)) 

240 return datetime.datetime.strptime(value, fmt).time() 

241 

242 

243def _rfc3339_to_datetime(dt_str): 

244 """Convert a microsecond-precision timestamp to a native datetime. 

245 

246 :type dt_str: str 

247 :param dt_str: The string to convert. 

248 

249 :rtype: :class:`datetime.datetime` 

250 :returns: The datetime object created from the string. 

251 """ 

252 return datetime.datetime.strptime(dt_str, _RFC3339_MICROS).replace(tzinfo=UTC) 

253 

254 

255def _rfc3339_nanos_to_datetime(dt_str): 

256 """Convert a nanosecond-precision timestamp to a native datetime. 

257 

258 .. note:: 

259 

260 Python datetimes do not support nanosecond precision; this function 

261 therefore truncates such values to microseconds. 

262 

263 :type dt_str: str 

264 :param dt_str: The string to convert. 

265 

266 :rtype: :class:`datetime.datetime` 

267 :returns: The datetime object created from the string. 

268 :raises ValueError: If the timestamp does not match the RFC 3339 

269 regular expression. 

270 """ 

271 with_nanos = _RFC3339_NANOS.match(dt_str) 

272 if with_nanos is None: 

273 raise ValueError( 

274 "Timestamp: %r, does not match pattern: %r" 

275 % (dt_str, _RFC3339_NANOS.pattern) 

276 ) 

277 bare_seconds = datetime.datetime.strptime( 

278 with_nanos.group("no_fraction"), _RFC3339_NO_FRACTION 

279 ) 

280 fraction = with_nanos.group("nanos") 

281 if fraction is None: 

282 micros = 0 

283 else: 

284 scale = 9 - len(fraction) 

285 nanos = int(fraction) * (10**scale) 

286 micros = nanos // 1000 

287 return bare_seconds.replace(microsecond=micros, tzinfo=UTC) 

288 

289 

290def _datetime_to_rfc3339(value, ignore_zone=True): 

291 """Convert a timestamp to a string. 

292 

293 :type value: :class:`datetime.datetime` 

294 :param value: The datetime object to be converted to a string. 

295 

296 :type ignore_zone: bool 

297 :param ignore_zone: If True, then the timezone (if any) of the datetime 

298 object is ignored. 

299 

300 :rtype: str 

301 :returns: The string representing the datetime stamp. 

302 """ 

303 if not ignore_zone and value.tzinfo is not None: 

304 # Convert to UTC and remove the time zone info. 

305 value = value.replace(tzinfo=None) - value.utcoffset() 

306 

307 return value.strftime(_RFC3339_MICROS) 

308 

309 

310def _to_bytes(value, encoding="ascii"): 

311 """Converts a string value to bytes, if necessary. 

312 

313 :type value: str / bytes or unicode 

314 :param value: The string/bytes value to be converted. 

315 

316 :type encoding: str 

317 :param encoding: The encoding to use to convert unicode to bytes. Defaults 

318 to "ascii", which will not allow any characters from 

319 ordinals larger than 127. Other useful values are 

320 "latin-1", which which will only allows byte ordinals 

321 (up to 255) and "utf-8", which will encode any unicode 

322 that needs to be. 

323 

324 :rtype: str / bytes 

325 :returns: The original value converted to bytes (if unicode) or as passed 

326 in if it started out as bytes. 

327 :raises TypeError: if the value could not be converted to bytes. 

328 """ 

329 result = value.encode(encoding) if isinstance(value, str) else value 

330 if isinstance(result, bytes): 

331 return result 

332 else: 

333 raise TypeError("%r could not be converted to bytes" % (value,)) 

334 

335 

336def _bytes_to_unicode(value): 

337 """Converts bytes to a unicode value, if necessary. 

338 

339 :type value: bytes 

340 :param value: bytes value to attempt string conversion on. 

341 

342 :rtype: str 

343 :returns: The original value converted to unicode (if bytes) or as passed 

344 in if it started out as unicode. 

345 

346 :raises ValueError: if the value could not be converted to unicode. 

347 """ 

348 result = value.decode("utf-8") if isinstance(value, bytes) else value 

349 if isinstance(result, str): 

350 return result 

351 else: 

352 raise ValueError("%r could not be converted to unicode" % (value,)) 

353 

354 

355def _from_any_pb(pb_type, any_pb): 

356 """Converts an Any protobuf to the specified message type 

357 

358 Args: 

359 pb_type (type): the type of the message that any_pb stores an instance 

360 of. 

361 any_pb (google.protobuf.any_pb2.Any): the object to be converted. 

362 

363 Returns: 

364 pb_type: An instance of the pb_type message. 

365 

366 Raises: 

367 TypeError: if the message could not be converted. 

368 """ 

369 msg = pb_type() 

370 if not any_pb.Unpack(msg): 

371 raise TypeError( 

372 "Could not convert {} to {}".format( 

373 any_pb.__class__.__name__, pb_type.__name__ 

374 ) 

375 ) 

376 

377 return msg 

378 

379 

380def _pb_timestamp_to_datetime(timestamp_pb): 

381 """Convert a Timestamp protobuf to a datetime object. 

382 

383 :type timestamp_pb: :class:`google.protobuf.timestamp_pb2.Timestamp` 

384 :param timestamp_pb: A Google returned timestamp protobuf. 

385 

386 :rtype: :class:`datetime.datetime` 

387 :returns: A UTC datetime object converted from a protobuf timestamp. 

388 """ 

389 return _EPOCH + datetime.timedelta( 

390 seconds=timestamp_pb.seconds, microseconds=(timestamp_pb.nanos / 1000.0) 

391 ) 

392 

393 

394def _pb_timestamp_to_rfc3339(timestamp_pb): 

395 """Convert a Timestamp protobuf to an RFC 3339 string. 

396 

397 :type timestamp_pb: :class:`google.protobuf.timestamp_pb2.Timestamp` 

398 :param timestamp_pb: A Google returned timestamp protobuf. 

399 

400 :rtype: str 

401 :returns: An RFC 3339 formatted timestamp string. 

402 """ 

403 timestamp = _pb_timestamp_to_datetime(timestamp_pb) 

404 return _datetime_to_rfc3339(timestamp) 

405 

406 

407def _datetime_to_pb_timestamp(when): 

408 """Convert a datetime object to a Timestamp protobuf. 

409 

410 :type when: :class:`datetime.datetime` 

411 :param when: the datetime to convert 

412 

413 :rtype: :class:`google.protobuf.timestamp_pb2.Timestamp` 

414 :returns: A timestamp protobuf corresponding to the object. 

415 """ 

416 ms_value = _microseconds_from_datetime(when) 

417 seconds, micros = divmod(ms_value, 10**6) 

418 nanos = micros * 10**3 

419 return timestamp_pb2.Timestamp(seconds=seconds, nanos=nanos) 

420 

421 

422def _timedelta_to_duration_pb(timedelta_val): 

423 """Convert a Python timedelta object to a duration protobuf. 

424 

425 .. note:: 

426 

427 The Python timedelta has a granularity of microseconds while 

428 the protobuf duration type has a duration of nanoseconds. 

429 

430 :type timedelta_val: :class:`datetime.timedelta` 

431 :param timedelta_val: A timedelta object. 

432 

433 :rtype: :class:`google.protobuf.duration_pb2.Duration` 

434 :returns: A duration object equivalent to the time delta. 

435 """ 

436 duration_pb = duration_pb2.Duration() 

437 duration_pb.FromTimedelta(timedelta_val) 

438 return duration_pb 

439 

440 

441def _duration_pb_to_timedelta(duration_pb): 

442 """Convert a duration protobuf to a Python timedelta object. 

443 

444 .. note:: 

445 

446 The Python timedelta has a granularity of microseconds while 

447 the protobuf duration type has a duration of nanoseconds. 

448 

449 :type duration_pb: :class:`google.protobuf.duration_pb2.Duration` 

450 :param duration_pb: A protobuf duration object. 

451 

452 :rtype: :class:`datetime.timedelta` 

453 :returns: The converted timedelta object. 

454 """ 

455 return datetime.timedelta( 

456 seconds=duration_pb.seconds, microseconds=(duration_pb.nanos / 1000.0) 

457 ) 

458 

459 

460def _name_from_project_path(path, project, template): 

461 """Validate a URI path and get the leaf object's name. 

462 

463 :type path: str 

464 :param path: URI path containing the name. 

465 

466 :type project: str 

467 :param project: (Optional) The project associated with the request. It is 

468 included for validation purposes. If passed as None, 

469 disables validation. 

470 

471 :type template: str 

472 :param template: Template regex describing the expected form of the path. 

473 The regex must have two named groups, 'project' and 

474 'name'. 

475 

476 :rtype: str 

477 :returns: Name parsed from ``path``. 

478 :raises ValueError: if the ``path`` is ill-formed or if the project from 

479 the ``path`` does not agree with the ``project`` 

480 passed in. 

481 """ 

482 if isinstance(template, str): 

483 template = re.compile(template) 

484 

485 match = template.match(path) 

486 

487 if not match: 

488 raise ValueError( 

489 'path "%s" did not match expected pattern "%s"' % (path, template.pattern) 

490 ) 

491 

492 if project is not None: 

493 found_project = match.group("project") 

494 if found_project != project: 

495 raise ValueError( 

496 "Project from client (%s) should agree with " 

497 "project from resource(%s)." % (project, found_project) 

498 ) 

499 

500 return match.group("name") 

501 

502 

503def make_secure_channel(credentials, user_agent, host, extra_options=()): 

504 """Makes a secure channel for an RPC service. 

505 

506 Uses / depends on gRPC. 

507 

508 :type credentials: :class:`google.auth.credentials.Credentials` 

509 :param credentials: The OAuth2 Credentials to use for creating 

510 access tokens. 

511 

512 :type user_agent: str 

513 :param user_agent: The user agent to be used with API requests. 

514 

515 :type host: str 

516 :param host: The host for the service. 

517 

518 :type extra_options: tuple 

519 :param extra_options: (Optional) Extra gRPC options used when creating the 

520 channel. 

521 

522 :rtype: :class:`grpc._channel.Channel` 

523 :returns: gRPC secure channel with credentials attached. 

524 """ 

525 target = "%s:%d" % (host, http.client.HTTPS_PORT) 

526 http_request = google.auth.transport.requests.Request() 

527 

528 user_agent_option = ("grpc.primary_user_agent", user_agent) 

529 options = (user_agent_option,) + extra_options 

530 return google.auth.transport.grpc.secure_authorized_channel( 

531 credentials, http_request, target, options=options 

532 ) 

533 

534 

535def make_secure_stub(credentials, user_agent, stub_class, host, extra_options=()): 

536 """Makes a secure stub for an RPC service. 

537 

538 Uses / depends on gRPC. 

539 

540 :type credentials: :class:`google.auth.credentials.Credentials` 

541 :param credentials: The OAuth2 Credentials to use for creating 

542 access tokens. 

543 

544 :type user_agent: str 

545 :param user_agent: The user agent to be used with API requests. 

546 

547 :type stub_class: type 

548 :param stub_class: A gRPC stub type for a given service. 

549 

550 :type host: str 

551 :param host: The host for the service. 

552 

553 :type extra_options: tuple 

554 :param extra_options: (Optional) Extra gRPC options passed when creating 

555 the channel. 

556 

557 :rtype: object, instance of ``stub_class`` 

558 :returns: The stub object used to make gRPC requests to a given API. 

559 """ 

560 channel = make_secure_channel( 

561 credentials, user_agent, host, extra_options=extra_options 

562 ) 

563 return stub_class(channel) 

564 

565 

566def make_insecure_stub(stub_class, host, port=None): 

567 """Makes an insecure stub for an RPC service. 

568 

569 Uses / depends on gRPC. 

570 

571 :type stub_class: type 

572 :param stub_class: A gRPC stub type for a given service. 

573 

574 :type host: str 

575 :param host: The host for the service. May also include the port 

576 if ``port`` is unspecified. 

577 

578 :type port: int 

579 :param port: (Optional) The port for the service. 

580 

581 :rtype: object, instance of ``stub_class`` 

582 :returns: The stub object used to make gRPC requests to a given API. 

583 """ 

584 if port is None: 

585 target = host 

586 else: 

587 # NOTE: This assumes port != http.client.HTTPS_PORT: 

588 target = "%s:%d" % (host, port) 

589 channel = grpc.insecure_channel(target) 

590 return stub_class(channel)