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
42_NOW = datetime.datetime.utcnow  # To be replaced by tests.
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        """
90        return iter(reversed(self._stack))
91
92    def push(self, resource):
93        """Push a resource onto our stack.
94        """
95        self._stack.append(resource)
96
97    def pop(self):
98        """Pop a resource from our stack.
99
100        :rtype: object
101        :returns: the top-most resource, after removing it.
102        :raises IndexError: if the stack is empty.
103        """
104        return self._stack.pop()
105
106    @property
107    def top(self):
108        """Get the top-most resource
109
110        :rtype: object
111        :returns: the top-most item, or None if the stack is empty.
112        """
113        if self._stack:
114            return self._stack[-1]
115
116
117def _ensure_tuple_or_list(arg_name, tuple_or_list):
118    """Ensures an input is a tuple or list.
119
120    This effectively reduces the iterable types allowed to a very short
121    whitelist: list and tuple.
122
123    :type arg_name: str
124    :param arg_name: Name of argument to use in error message.
125
126    :type tuple_or_list: sequence of str
127    :param tuple_or_list: Sequence to be verified.
128
129    :rtype: list of str
130    :returns: The ``tuple_or_list`` passed in cast to a ``list``.
131    :raises TypeError: if the ``tuple_or_list`` is not a tuple or list.
132    """
133    if not isinstance(tuple_or_list, (tuple, list)):
134        raise TypeError(
135            "Expected %s to be a tuple or list. "
136            "Received %r" % (arg_name, tuple_or_list)
137        )
138    return list(tuple_or_list)
139
140
141def _determine_default_project(project=None):
142    """Determine default project ID explicitly or implicitly as fall-back.
143
144    See :func:`google.auth.default` for details on how the default project
145    is determined.
146
147    :type project: str
148    :param project: Optional. The project name to use as default.
149
150    :rtype: str or ``NoneType``
151    :returns: Default project if it can be determined.
152    """
153    if project is None:
154        _, project = google.auth.default()
155    return project
156
157
158def _millis(when):
159    """Convert a zone-aware datetime to integer milliseconds.
160
161    :type when: :class:`datetime.datetime`
162    :param when: the datetime to convert
163
164    :rtype: int
165    :returns: milliseconds since epoch for ``when``
166    """
167    micros = _microseconds_from_datetime(when)
168    return micros // 1000
169
170
171def _datetime_from_microseconds(value):
172    """Convert timestamp to datetime, assuming UTC.
173
174    :type value: float
175    :param value: The timestamp to convert
176
177    :rtype: :class:`datetime.datetime`
178    :returns: The datetime object created from the value.
179    """
180    return _EPOCH + datetime.timedelta(microseconds=value)
181
182
183def _microseconds_from_datetime(value):
184    """Convert non-none datetime to microseconds.
185
186    :type value: :class:`datetime.datetime`
187    :param value: The timestamp to convert.
188
189    :rtype: int
190    :returns: The timestamp, in microseconds.
191    """
192    if not value.tzinfo:
193        value = value.replace(tzinfo=UTC)
194    # Regardless of what timezone is on the value, convert it to UTC.
195    value = value.astimezone(UTC)
196    # Convert the datetime to a microsecond timestamp.
197    return int(calendar.timegm(value.timetuple()) * 1e6) + value.microsecond
198
199
200def _millis_from_datetime(value):
201    """Convert non-none datetime to timestamp, assuming UTC.
202
203    :type value: :class:`datetime.datetime`
204    :param value: (Optional) the timestamp
205
206    :rtype: int, or ``NoneType``
207    :returns: the timestamp, in milliseconds, or None
208    """
209    if value is not None:
210        return _millis(value)
211
212
213def _date_from_iso8601_date(value):
214    """Convert a ISO8601 date string to native datetime date
215
216    :type value: str
217    :param value: The date string to convert
218
219    :rtype: :class:`datetime.date`
220    :returns: A datetime date object created from the string
221
222    """
223    return datetime.datetime.strptime(value, "%Y-%m-%d").date()
224
225
226def _time_from_iso8601_time_naive(value):
227    """Convert a zoneless ISO8601 time string to naive datetime time
228
229    :type value: str
230    :param value: The time string to convert
231
232    :rtype: :class:`datetime.time`
233    :returns: A datetime time object created from the string
234    :raises ValueError: if the value does not match a known format.
235    """
236    if len(value) == 8:  # HH:MM:SS
237        fmt = _TIMEONLY_NO_FRACTION
238    elif len(value) == 15:  # HH:MM:SS.micros
239        fmt = _TIMEONLY_W_MICROS
240    else:
241        raise ValueError("Unknown time format: {}".format(value))
242    return datetime.datetime.strptime(value, fmt).time()
243
244
245def _rfc3339_to_datetime(dt_str):
246    """Convert a microsecond-precision timestamp to a native datetime.
247
248    :type dt_str: str
249    :param dt_str: The string to convert.
250
251    :rtype: :class:`datetime.datetime`
252    :returns: The datetime object created from the string.
253    """
254    return datetime.datetime.strptime(dt_str, _RFC3339_MICROS).replace(tzinfo=UTC)
255
256
257def _rfc3339_nanos_to_datetime(dt_str):
258    """Convert a nanosecond-precision timestamp to a native datetime.
259
260    .. note::
261
262       Python datetimes do not support nanosecond precision;  this function
263       therefore truncates such values to microseconds.
264
265    :type dt_str: str
266    :param dt_str: The string to convert.
267
268    :rtype: :class:`datetime.datetime`
269    :returns: The datetime object created from the string.
270    :raises ValueError: If the timestamp does not match the RFC 3339
271                        regular expression.
272    """
273    with_nanos = _RFC3339_NANOS.match(dt_str)
274    if with_nanos is None:
275        raise ValueError(
276            "Timestamp: %r, does not match pattern: %r"
277            % (dt_str, _RFC3339_NANOS.pattern)
278        )
279    bare_seconds = datetime.datetime.strptime(
280        with_nanos.group("no_fraction"), _RFC3339_NO_FRACTION
281    )
282    fraction = with_nanos.group("nanos")
283    if fraction is None:
284        micros = 0
285    else:
286        scale = 9 - len(fraction)
287        nanos = int(fraction) * (10 ** scale)
288        micros = nanos // 1000
289    return bare_seconds.replace(microsecond=micros, tzinfo=UTC)
290
291
292def _datetime_to_rfc3339(value, ignore_zone=True):
293    """Convert a timestamp to a string.
294
295    :type value: :class:`datetime.datetime`
296    :param value: The datetime object to be converted to a string.
297
298    :type ignore_zone: bool
299    :param ignore_zone: If True, then the timezone (if any) of the datetime
300                        object is ignored.
301
302    :rtype: str
303    :returns: The string representing the datetime stamp.
304    """
305    if not ignore_zone and value.tzinfo is not None:
306        # Convert to UTC and remove the time zone info.
307        value = value.replace(tzinfo=None) - value.utcoffset()
308
309    return value.strftime(_RFC3339_MICROS)
310
311
312def _to_bytes(value, encoding="ascii"):
313    """Converts a string value to bytes, if necessary.
314
315    :type value: str / bytes or unicode
316    :param value: The string/bytes value to be converted.
317
318    :type encoding: str
319    :param encoding: The encoding to use to convert unicode to bytes. Defaults
320                     to "ascii", which will not allow any characters from
321                     ordinals larger than 127. Other useful values are
322                     "latin-1", which which will only allows byte ordinals
323                     (up to 255) and "utf-8", which will encode any unicode
324                     that needs to be.
325
326    :rtype: str / bytes
327    :returns: The original value converted to bytes (if unicode) or as passed
328              in if it started out as bytes.
329    :raises TypeError: if the value could not be converted to bytes.
330    """
331    result = value.encode(encoding) if isinstance(value, str) else value
332    if isinstance(result, bytes):
333        return result
334    else:
335        raise TypeError("%r could not be converted to bytes" % (value,))
336
337
338def _bytes_to_unicode(value):
339    """Converts bytes to a unicode value, if necessary.
340
341    :type value: bytes
342    :param value: bytes value to attempt string conversion on.
343
344    :rtype: str
345    :returns: The original value converted to unicode (if bytes) or as passed
346              in if it started out as unicode.
347
348    :raises ValueError: if the value could not be converted to unicode.
349    """
350    result = value.decode("utf-8") if isinstance(value, bytes) else value
351    if isinstance(result, str):
352        return result
353    else:
354        raise ValueError("%r could not be converted to unicode" % (value,))
355
356
357def _from_any_pb(pb_type, any_pb):
358    """Converts an Any protobuf to the specified message type
359
360    Args:
361        pb_type (type): the type of the message that any_pb stores an instance
362            of.
363        any_pb (google.protobuf.any_pb2.Any): the object to be converted.
364
365    Returns:
366        pb_type: An instance of the pb_type message.
367
368    Raises:
369        TypeError: if the message could not be converted.
370    """
371    msg = pb_type()
372    if not any_pb.Unpack(msg):
373        raise TypeError(
374            "Could not convert {} to {}".format(
375                any_pb.__class__.__name__, pb_type.__name__
376            )
377        )
378
379    return msg
380
381
382def _pb_timestamp_to_datetime(timestamp_pb):
383    """Convert a Timestamp protobuf to a datetime object.
384
385    :type timestamp_pb: :class:`google.protobuf.timestamp_pb2.Timestamp`
386    :param timestamp_pb: A Google returned timestamp protobuf.
387
388    :rtype: :class:`datetime.datetime`
389    :returns: A UTC datetime object converted from a protobuf timestamp.
390    """
391    return _EPOCH + datetime.timedelta(
392        seconds=timestamp_pb.seconds, microseconds=(timestamp_pb.nanos / 1000.0)
393    )
394
395
396def _pb_timestamp_to_rfc3339(timestamp_pb):
397    """Convert a Timestamp protobuf to an RFC 3339 string.
398
399    :type timestamp_pb: :class:`google.protobuf.timestamp_pb2.Timestamp`
400    :param timestamp_pb: A Google returned timestamp protobuf.
401
402    :rtype: str
403    :returns: An RFC 3339 formatted timestamp string.
404    """
405    timestamp = _pb_timestamp_to_datetime(timestamp_pb)
406    return _datetime_to_rfc3339(timestamp)
407
408
409def _datetime_to_pb_timestamp(when):
410    """Convert a datetime object to a Timestamp protobuf.
411
412    :type when: :class:`datetime.datetime`
413    :param when: the datetime to convert
414
415    :rtype: :class:`google.protobuf.timestamp_pb2.Timestamp`
416    :returns: A timestamp protobuf corresponding to the object.
417    """
418    ms_value = _microseconds_from_datetime(when)
419    seconds, micros = divmod(ms_value, 10 ** 6)
420    nanos = micros * 10 ** 3
421    return timestamp_pb2.Timestamp(seconds=seconds, nanos=nanos)
422
423
424def _timedelta_to_duration_pb(timedelta_val):
425    """Convert a Python timedelta object to a duration protobuf.
426
427    .. note::
428
429        The Python timedelta has a granularity of microseconds while
430        the protobuf duration type has a duration of nanoseconds.
431
432    :type timedelta_val: :class:`datetime.timedelta`
433    :param timedelta_val: A timedelta object.
434
435    :rtype: :class:`google.protobuf.duration_pb2.Duration`
436    :returns: A duration object equivalent to the time delta.
437    """
438    duration_pb = duration_pb2.Duration()
439    duration_pb.FromTimedelta(timedelta_val)
440    return duration_pb
441
442
443def _duration_pb_to_timedelta(duration_pb):
444    """Convert a duration protobuf to a Python timedelta object.
445
446    .. note::
447
448        The Python timedelta has a granularity of microseconds while
449        the protobuf duration type has a duration of nanoseconds.
450
451    :type duration_pb: :class:`google.protobuf.duration_pb2.Duration`
452    :param duration_pb: A protobuf duration object.
453
454    :rtype: :class:`datetime.timedelta`
455    :returns: The converted timedelta object.
456    """
457    return datetime.timedelta(
458        seconds=duration_pb.seconds, microseconds=(duration_pb.nanos / 1000.0)
459    )
460
461
462def _name_from_project_path(path, project, template):
463    """Validate a URI path and get the leaf object's name.
464
465    :type path: str
466    :param path: URI path containing the name.
467
468    :type project: str
469    :param project: (Optional) The project associated with the request. It is
470                    included for validation purposes.  If passed as None,
471                    disables validation.
472
473    :type template: str
474    :param template: Template regex describing the expected form of the path.
475                     The regex must have two named groups, 'project' and
476                     'name'.
477
478    :rtype: str
479    :returns: Name parsed from ``path``.
480    :raises ValueError: if the ``path`` is ill-formed or if the project from
481                        the ``path`` does not agree with the ``project``
482                        passed in.
483    """
484    if isinstance(template, str):
485        template = re.compile(template)
486
487    match = template.match(path)
488
489    if not match:
490        raise ValueError(
491            'path "%s" did not match expected pattern "%s"' % (path, template.pattern)
492        )
493
494    if project is not None:
495        found_project = match.group("project")
496        if found_project != project:
497            raise ValueError(
498                "Project from client (%s) should agree with "
499                "project from resource(%s)." % (project, found_project)
500            )
501
502    return match.group("name")
503
504
505def make_secure_channel(credentials, user_agent, host, extra_options=()):
506    """Makes a secure channel for an RPC service.
507
508    Uses / depends on gRPC.
509
510    :type credentials: :class:`google.auth.credentials.Credentials`
511    :param credentials: The OAuth2 Credentials to use for creating
512                        access tokens.
513
514    :type user_agent: str
515    :param user_agent: The user agent to be used with API requests.
516
517    :type host: str
518    :param host: The host for the service.
519
520    :type extra_options: tuple
521    :param extra_options: (Optional) Extra gRPC options used when creating the
522                          channel.
523
524    :rtype: :class:`grpc._channel.Channel`
525    :returns: gRPC secure channel with credentials attached.
526    """
527    target = "%s:%d" % (host, http.client.HTTPS_PORT)
528    http_request = google.auth.transport.requests.Request()
529
530    user_agent_option = ("grpc.primary_user_agent", user_agent)
531    options = (user_agent_option,) + extra_options
532    return google.auth.transport.grpc.secure_authorized_channel(
533        credentials, http_request, target, options=options
534    )
535
536
537def make_secure_stub(credentials, user_agent, stub_class, host, extra_options=()):
538    """Makes a secure stub for an RPC service.
539
540    Uses / depends on gRPC.
541
542    :type credentials: :class:`google.auth.credentials.Credentials`
543    :param credentials: The OAuth2 Credentials to use for creating
544                        access tokens.
545
546    :type user_agent: str
547    :param user_agent: The user agent to be used with API requests.
548
549    :type stub_class: type
550    :param stub_class: A gRPC stub type for a given service.
551
552    :type host: str
553    :param host: The host for the service.
554
555    :type extra_options: tuple
556    :param extra_options: (Optional) Extra gRPC options passed when creating
557                          the channel.
558
559    :rtype: object, instance of ``stub_class``
560    :returns: The stub object used to make gRPC requests to a given API.
561    """
562    channel = make_secure_channel(
563        credentials, user_agent, host, extra_options=extra_options
564    )
565    return stub_class(channel)
566
567
568def make_insecure_stub(stub_class, host, port=None):
569    """Makes an insecure stub for an RPC service.
570
571    Uses / depends on gRPC.
572
573    :type stub_class: type
574    :param stub_class: A gRPC stub type for a given service.
575
576    :type host: str
577    :param host: The host for the service. May also include the port
578                 if ``port`` is unspecified.
579
580    :type port: int
581    :param port: (Optional) The port for the service.
582
583    :rtype: object, instance of ``stub_class``
584    :returns: The stub object used to make gRPC requests to a given API.
585    """
586    if port is None:
587        target = host
588    else:
589        # NOTE: This assumes port != http.client.HTTPS_PORT:
590        target = "%s:%d" % (host, port)
591    channel = grpc.insecure_channel(target)
592    return stub_class(channel)
593