fix!: drop support for Python 2.7 / 3.5 (#212)

Drop 'six' module

Drop 'u"' prefixes for text

Remove other Python 2.7 workarounds

Drop use of 'pytz'

Dxpand range to allow 'google-auth' 2.x versions

Remove 'general_helpers.wraps': except for a backward-compatibility
import, 'functools.wraps' does everything wee need on Python >= 3.6.

Remove 'packaging' dependency

Release-As: 2.0.0b1

Closes #74.

Closes #215.
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index c7860ad..358404f 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,4 +1,3 @@
-.. Generated by synthtool. DO NOT EDIT!
 ############
 Contributing
 ############
@@ -22,7 +21,7 @@
   documentation.
 
 - The feature must work fully on the following CPython versions:
-  2.7, 3.6, 3.7, 3.8 and 3.9 on both UNIX and Windows.
+  3.6, 3.7, 3.8 and 3.9 on both UNIX and Windows.
 
 - The feature must not add unnecessary dependencies (where
   "unnecessary" is of course subjective, but new dependencies should
@@ -77,8 +76,8 @@
 
   .. note::
 
-    The unit tests and system tests are described in the
-    ``noxfile.py`` files in each directory.
+    The unit tests tests are described in the ``noxfile.py`` files
+    in each directory.
 
 .. nox: https://pypi.org/project/nox/
 
@@ -133,29 +132,6 @@
   "Function-Under-Test"), which is PEP8-incompliant, but more readable.
   Some also use a local variable, ``MUT`` (short for "Module-Under-Test").
 
-********************
-Running System Tests
-********************
-
-- To run system tests, you can execute::
-
-   # Run all system tests
-   $ nox -s system
-
-   # Run a single system test
-   $ nox -s system-3.8 -- -k <name of test>
-
-
-  .. note::
-
-      System tests are only configured to run under Python 2.7 and 3.8.
-      For expediency, we do not run them in older versions of Python 3.
-
-  This alone will not run the tests. You'll need to change some local
-  auth settings and change some configuration in your project to
-  run all the tests.
-
-- System tests will be run against an actual project. You should use local credentials from gcloud when possible. See `Best practices for application authentication <https://cloud.google.com/docs/authentication/best-practices-applications#local_development_and_testing_with_the>`__. Some tests require a service account. For those tests see `Authenticating as a service account <https://cloud.google.com/docs/authentication/production>`__.
 
 *************
 Test Coverage
@@ -221,13 +197,11 @@
 
 We support:
 
--  `Python 2.7`_
 -  `Python 3.6`_
 -  `Python 3.7`_
 -  `Python 3.8`_
 -  `Python 3.9`_
 
-.. _Python 2.7: https://docs.python.org/2.7/
 .. _Python 3.6: https://docs.python.org/3.6/
 .. _Python 3.7: https://docs.python.org/3.7/
 .. _Python 3.8: https://docs.python.org/3.8/
@@ -239,7 +213,7 @@
 .. _config: https://github.com/googleapis/python-api-core/blob/master/noxfile.py
 
 
-We also explicitly decided to support Python 3 beginning with version 2.7.
+We also explicitly decided to support Python 3 beginning with version 3.6.
 Reasons for this include:
 
 -  Encouraging use of newest versions of Python 3
diff --git a/README.rst b/README.rst
index 244043e..d94f3e8 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
 Core Library for Google Client Libraries
 ========================================
 
-|pypi| |versions| 
+|pypi| |versions|
 
 This library is not meant to stand-alone. Instead it defines
 common helpers used by all Google API clients. For more information, see the
@@ -16,8 +16,13 @@
 
 Supported Python Versions
 -------------------------
-Python >= 3.5
+Python >= 3.6
 
-Deprecated Python Versions
---------------------------
-Python == 2.7. Python 2.7 support will be removed on January 1, 2020.
+
+Unsupported Python Versions
+---------------------------
+
+Python == 2.7, Python == 3.5.
+
+The last version of this library compatible with Python 2.7 and 3.5 is
+`google-api_core==1.31.1`.
diff --git a/docs/auth.rst b/docs/auth.rst
index faf0228..a9b296d 100644
--- a/docs/auth.rst
+++ b/docs/auth.rst
@@ -103,25 +103,6 @@
 
 .. _google-auth-guide: https://googleapis.dev/python/google-auth/latest/user-guide.html#service-account-private-key-files
 
-
-Google App Engine Standard First Generation Environment
--------------------------------------------------------
-
-These credentials are used only in the legacy Python 2.7
-`First Generation Standard Environment`_. All other App Engine
-runtimes use Compute Engine credentials.
-
-.. _First Generation Standard Environment: https://cloud.google.com/appengine/docs/standard/runtimes
-
-To create
-:class:`credentials <google.auth.app_engine.Credentials>`
-just for Google App Engine:
-
-.. code:: python
-
-    from google.auth import app_engine
-    credentials = app_engine.Credentials()
-
 Google Compute Engine Environment
 ---------------------------------
 
diff --git a/google/api_core/bidi.py b/google/api_core/bidi.py
index be52d97..56a021a 100644
--- a/google/api_core/bidi.py
+++ b/google/api_core/bidi.py
@@ -17,11 +17,10 @@
 import collections
 import datetime
 import logging
+import queue as queue_module
 import threading
 import time
 
-from six.moves import queue
-
 from google.api_core import exceptions
 
 _LOGGER = logging.getLogger(__name__)
@@ -71,7 +70,7 @@
     CPU consumed by spinning is pretty minuscule.
 
     Args:
-        queue (queue.Queue): The request queue.
+        queue (queue_module.Queue): The request queue.
         period (float): The number of seconds to wait for items from the queue
             before checking if the RPC is cancelled. In practice, this
             determines the maximum amount of time the request consumption
@@ -108,7 +107,7 @@
         while True:
             try:
                 item = self._queue.get(timeout=self._period)
-            except queue.Empty:
+            except queue_module.Empty:
                 if not self._is_active():
                     _LOGGER.debug(
                         "Empty queue and inactive call, exiting request " "generator."
@@ -247,7 +246,7 @@
         self._start_rpc = start_rpc
         self._initial_request = initial_request
         self._rpc_metadata = metadata
-        self._request_queue = queue.Queue()
+        self._request_queue = queue_module.Queue()
         self._request_generator = None
         self._is_active = False
         self._callbacks = []
@@ -645,6 +644,7 @@
                 # Keeping the lock throughout avoids that.
                 # In the future, we could use `Condition.wait_for` if we drop
                 # Python 2.7.
+                # See: https://github.com/googleapis/python-api-core/issues/211
                 with self._wake:
                     while self._paused:
                         _LOGGER.debug("paused, waiting for waking.")
diff --git a/google/api_core/client_info.py b/google/api_core/client_info.py
index adca5f3..d7f4367 100644
--- a/google/api_core/client_info.py
+++ b/google/api_core/client_info.py
@@ -42,7 +42,7 @@
 
     Args:
         python_version (str): The Python interpreter version, for example,
-            ``'2.7.13'``.
+            ``'3.9.6'``.
         grpc_version (Optional[str]): The gRPC library version.
         api_core_version (str): The google-api-core library version.
         gapic_version (Optional[str]): The sversion of gapic-generated client
diff --git a/google/api_core/client_options.py b/google/api_core/client_options.py
index 57000e9..be5523d 100644
--- a/google/api_core/client_options.py
+++ b/google/api_core/client_options.py
@@ -101,7 +101,7 @@
     """Construct a client options object from a mapping object.
 
     Args:
-        options (six.moves.collections_abc.Mapping): A mapping object with client options.
+        options (collections.abc.Mapping): A mapping object with client options.
             See the docstring for ClientOptions for details on valid arguments.
     """
 
diff --git a/google/api_core/datetime_helpers.py b/google/api_core/datetime_helpers.py
index e52fb1d..78268ef 100644
--- a/google/api_core/datetime_helpers.py
+++ b/google/api_core/datetime_helpers.py
@@ -18,12 +18,10 @@
 import datetime
 import re
 
-import pytz
-
 from google.protobuf import timestamp_pb2
 
 
-_UTC_EPOCH = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc)
+_UTC_EPOCH = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
 _RFC3339_MICROS = "%Y-%m-%dT%H:%M:%S.%fZ"
 _RFC3339_NO_FRACTION = "%Y-%m-%dT%H:%M:%S"
 # datetime.strptime cannot handle nanosecond precision:  parse w/ regex
@@ -83,9 +81,9 @@
         int: Microseconds since the unix epoch.
     """
     if not value.tzinfo:
-        value = value.replace(tzinfo=pytz.utc)
+        value = value.replace(tzinfo=datetime.timezone.utc)
     # Regardless of what timezone is on the value, convert it to UTC.
-    value = value.astimezone(pytz.utc)
+    value = value.astimezone(datetime.timezone.utc)
     # Convert the datetime to a microsecond timestamp.
     return int(calendar.timegm(value.timetuple()) * 1e6) + value.microsecond
 
@@ -156,7 +154,7 @@
         nanos = int(fraction) * (10 ** scale)
         micros = nanos // 1000
 
-    return bare_seconds.replace(microsecond=micros, tzinfo=pytz.utc)
+    return bare_seconds.replace(microsecond=micros, tzinfo=datetime.timezone.utc)
 
 
 from_rfc3339_nanos = from_rfc3339  # from_rfc3339_nanos method was deprecated.
@@ -256,7 +254,7 @@
             bare.minute,
             bare.second,
             nanosecond=nanos,
-            tzinfo=pytz.UTC,
+            tzinfo=datetime.timezone.utc,
         )
 
     def timestamp_pb(self):
@@ -265,7 +263,11 @@
         Returns:
             (:class:`~google.protobuf.timestamp_pb2.Timestamp`): Timestamp message
         """
-        inst = self if self.tzinfo is not None else self.replace(tzinfo=pytz.UTC)
+        inst = (
+            self
+            if self.tzinfo is not None
+            else self.replace(tzinfo=datetime.timezone.utc)
+        )
         delta = inst - _UTC_EPOCH
         seconds = int(delta.total_seconds())
         nanos = self._nanosecond or self.microsecond * 1000
@@ -292,5 +294,5 @@
             bare.minute,
             bare.second,
             nanosecond=stamp.nanos,
-            tzinfo=pytz.UTC,
+            tzinfo=datetime.timezone.utc,
         )
diff --git a/google/api_core/exceptions.py b/google/api_core/exceptions.py
index 412fc2e..13be917 100644
--- a/google/api_core/exceptions.py
+++ b/google/api_core/exceptions.py
@@ -21,8 +21,7 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
-import six
-from six.moves import http_client
+import http.client
 
 try:
     import grpc
@@ -56,7 +55,6 @@
     pass
 
 
[email protected]_2_unicode_compatible
 class RetryError(GoogleAPIError):
     """Raised when a function has exhausted all of its available retries.
 
@@ -92,9 +90,7 @@
         return cls
 
 
[email protected]_2_unicode_compatible
[email protected]_metaclass(_GoogleAPICallErrorMeta)
-class GoogleAPICallError(GoogleAPIError):
+class GoogleAPICallError(GoogleAPIError, metaclass=_GoogleAPICallErrorMeta):
     """Base class for exceptions raised by calling API methods.
 
     Args:
@@ -153,25 +149,25 @@
 class MovedPermanently(Redirection):
     """Exception mapping a ``301 Moved Permanently`` response."""
 
-    code = http_client.MOVED_PERMANENTLY
+    code = http.client.MOVED_PERMANENTLY
 
 
 class NotModified(Redirection):
     """Exception mapping a ``304 Not Modified`` response."""
 
-    code = http_client.NOT_MODIFIED
+    code = http.client.NOT_MODIFIED
 
 
 class TemporaryRedirect(Redirection):
     """Exception mapping a ``307 Temporary Redirect`` response."""
 
-    code = http_client.TEMPORARY_REDIRECT
+    code = http.client.TEMPORARY_REDIRECT
 
 
 class ResumeIncomplete(Redirection):
     """Exception mapping a ``308 Resume Incomplete`` response.
 
-    .. note:: :attr:`http_client.PERMANENT_REDIRECT` is ``308``, but Google
+    .. note:: :attr:`http.client.PERMANENT_REDIRECT` is ``308``, but Google
         APIs differ in their use of this status code.
     """
 
@@ -185,7 +181,7 @@
 class BadRequest(ClientError):
     """Exception mapping a ``400 Bad Request`` response."""
 
-    code = http_client.BAD_REQUEST
+    code = http.client.BAD_REQUEST
 
 
 class InvalidArgument(BadRequest):
@@ -210,7 +206,7 @@
 class Unauthorized(ClientError):
     """Exception mapping a ``401 Unauthorized`` response."""
 
-    code = http_client.UNAUTHORIZED
+    code = http.client.UNAUTHORIZED
 
 
 class Unauthenticated(Unauthorized):
@@ -222,7 +218,7 @@
 class Forbidden(ClientError):
     """Exception mapping a ``403 Forbidden`` response."""
 
-    code = http_client.FORBIDDEN
+    code = http.client.FORBIDDEN
 
 
 class PermissionDenied(Forbidden):
@@ -235,20 +231,20 @@
     """Exception mapping a ``404 Not Found`` response or a
     :attr:`grpc.StatusCode.NOT_FOUND` error."""
 
-    code = http_client.NOT_FOUND
+    code = http.client.NOT_FOUND
     grpc_status_code = grpc.StatusCode.NOT_FOUND if grpc is not None else None
 
 
 class MethodNotAllowed(ClientError):
     """Exception mapping a ``405 Method Not Allowed`` response."""
 
-    code = http_client.METHOD_NOT_ALLOWED
+    code = http.client.METHOD_NOT_ALLOWED
 
 
 class Conflict(ClientError):
     """Exception mapping a ``409 Conflict`` response."""
 
-    code = http_client.CONFLICT
+    code = http.client.CONFLICT
 
 
 class AlreadyExists(Conflict):
@@ -266,26 +262,25 @@
 class LengthRequired(ClientError):
     """Exception mapping a ``411 Length Required`` response."""
 
-    code = http_client.LENGTH_REQUIRED
+    code = http.client.LENGTH_REQUIRED
 
 
 class PreconditionFailed(ClientError):
     """Exception mapping a ``412 Precondition Failed`` response."""
 
-    code = http_client.PRECONDITION_FAILED
+    code = http.client.PRECONDITION_FAILED
 
 
 class RequestRangeNotSatisfiable(ClientError):
     """Exception mapping a ``416 Request Range Not Satisfiable`` response."""
 
-    code = http_client.REQUESTED_RANGE_NOT_SATISFIABLE
+    code = http.client.REQUESTED_RANGE_NOT_SATISFIABLE
 
 
 class TooManyRequests(ClientError):
     """Exception mapping a ``429 Too Many Requests`` response."""
 
-    # http_client does not define a constant for this in Python 2.
-    code = 429
+    code = http.client.TOO_MANY_REQUESTS
 
 
 class ResourceExhausted(TooManyRequests):
@@ -312,7 +307,7 @@
     """Exception mapping a ``500 Internal Server Error`` response. or a
     :attr:`grpc.StatusCode.INTERNAL` error."""
 
-    code = http_client.INTERNAL_SERVER_ERROR
+    code = http.client.INTERNAL_SERVER_ERROR
     grpc_status_code = grpc.StatusCode.INTERNAL if grpc is not None else None
 
 
@@ -332,28 +327,28 @@
     """Exception mapping a ``501 Not Implemented`` response or a
     :attr:`grpc.StatusCode.UNIMPLEMENTED` error."""
 
-    code = http_client.NOT_IMPLEMENTED
+    code = http.client.NOT_IMPLEMENTED
     grpc_status_code = grpc.StatusCode.UNIMPLEMENTED if grpc is not None else None
 
 
 class BadGateway(ServerError):
     """Exception mapping a ``502 Bad Gateway`` response."""
 
-    code = http_client.BAD_GATEWAY
+    code = http.client.BAD_GATEWAY
 
 
 class ServiceUnavailable(ServerError):
     """Exception mapping a ``503 Service Unavailable`` response or a
     :attr:`grpc.StatusCode.UNAVAILABLE` error."""
 
-    code = http_client.SERVICE_UNAVAILABLE
+    code = http.client.SERVICE_UNAVAILABLE
     grpc_status_code = grpc.StatusCode.UNAVAILABLE if grpc is not None else None
 
 
 class GatewayTimeout(ServerError):
     """Exception mapping a ``504 Gateway Timeout`` response."""
 
-    code = http_client.GATEWAY_TIMEOUT
+    code = http.client.GATEWAY_TIMEOUT
 
 
 class DeadlineExceeded(GatewayTimeout):
diff --git a/google/api_core/future/base.py b/google/api_core/future/base.py
index e7888ca..f300586 100644
--- a/google/api_core/future/base.py
+++ b/google/api_core/future/base.py
@@ -16,11 +16,8 @@
 
 import abc
 
-import six
 
-
[email protected]_metaclass(abc.ABCMeta)
-class Future(object):
+class Future(object, metaclass=abc.ABCMeta):
     # pylint: disable=missing-docstring
     # We inherit the interfaces here from concurrent.futures.
 
diff --git a/google/api_core/gapic_v1/__init__.py b/google/api_core/gapic_v1/__init__.py
index 6632047..e5b7ad3 100644
--- a/google/api_core/gapic_v1/__init__.py
+++ b/google/api_core/gapic_v1/__init__.py
@@ -12,18 +12,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import sys
-
 from google.api_core.gapic_v1 import client_info
 from google.api_core.gapic_v1 import config
+from google.api_core.gapic_v1 import config_async
 from google.api_core.gapic_v1 import method
+from google.api_core.gapic_v1 import method_async
 from google.api_core.gapic_v1 import routing_header
 
-__all__ = ["client_info", "config", "method", "routing_header"]
-
-if sys.version_info >= (3, 6):
-    from google.api_core.gapic_v1 import config_async  # noqa: F401
-    from google.api_core.gapic_v1 import method_async  # noqa: F401
-
-    __all__.append("config_async")
-    __all__.append("method_async")
+__all__ = [
+    "client_info",
+    "config",
+    "config_async",
+    "method",
+    "method_async",
+    "routing_header",
+]
diff --git a/google/api_core/gapic_v1/client_info.py b/google/api_core/gapic_v1/client_info.py
index bdc2ce4..fab0f54 100644
--- a/google/api_core/gapic_v1/client_info.py
+++ b/google/api_core/gapic_v1/client_info.py
@@ -33,7 +33,7 @@
 
     Args:
         python_version (str): The Python interpreter version, for example,
-            ``'2.7.13'``.
+            ``'3.9.6'``.
         grpc_version (Optional[str]): The gRPC library version.
         api_core_version (str): The google-api-core library version.
         gapic_version (Optional[str]): The sversion of gapic-generated client
diff --git a/google/api_core/gapic_v1/config.py b/google/api_core/gapic_v1/config.py
index 29e8645..9c72287 100644
--- a/google/api_core/gapic_v1/config.py
+++ b/google/api_core/gapic_v1/config.py
@@ -21,7 +21,6 @@
 import collections
 
 import grpc
-import six
 
 from google.api_core import exceptions
 from google.api_core import retry
@@ -130,24 +129,20 @@
     # Grab all the retry codes
     retry_codes_map = {
         name: retry_codes
-        for name, retry_codes in six.iteritems(interface_config.get("retry_codes", {}))
+        for name, retry_codes in interface_config.get("retry_codes", {}).items()
     }
 
     # Grab all of the retry params
     retry_params_map = {
         name: retry_params
-        for name, retry_params in six.iteritems(
-            interface_config.get("retry_params", {})
-        )
+        for name, retry_params in interface_config.get("retry_params", {}).items()
     }
 
     # Iterate through all the API methods and create a flat MethodConfig
     # instance for each one.
     method_configs = {}
 
-    for method_name, method_params in six.iteritems(
-        interface_config.get("methods", {})
-    ):
+    for method_name, method_params in interface_config.get("methods", {}).items():
         retry_params_name = method_params.get("retry_params_name")
 
         if retry_params_name is not None:
diff --git a/google/api_core/gapic_v1/method.py b/google/api_core/gapic_v1/method.py
index 8bf8256..79722c0 100644
--- a/google/api_core/gapic_v1/method.py
+++ b/google/api_core/gapic_v1/method.py
@@ -18,7 +18,8 @@
 pagination, and long-running operations to gRPC methods.
 """
 
-from google.api_core import general_helpers
+import functools
+
 from google.api_core import grpc_helpers
 from google.api_core import timeout
 from google.api_core.gapic_v1 import client_info
@@ -110,26 +111,22 @@
         self._timeout = timeout
         self._metadata = metadata
 
-    def __call__(self, *args, **kwargs):
+    def __call__(self, *args, timeout=DEFAULT, retry=DEFAULT, **kwargs):
         """Invoke the low-level RPC with retry, timeout, and metadata."""
-        # Note: Due to Python 2 lacking keyword-only arguments we use kwargs to
-        # extract the retry and timeout params.
-        timeout_ = _determine_timeout(
+        timeout = _determine_timeout(
             self._timeout,
-            kwargs.pop("timeout", self._timeout),
+            timeout,
             # Use only the invocation-specified retry only for this, as we only
             # want to adjust the timeout deadline if the *user* specified
             # a different retry.
-            kwargs.get("retry", None),
+            retry,
         )
 
-        retry = kwargs.pop("retry", self._retry)
-
         if retry is DEFAULT:
             retry = self._retry
 
         # Apply all applicable decorators.
-        wrapped_func = _apply_decorators(self._target, [retry, timeout_])
+        wrapped_func = _apply_decorators(self._target, [retry, timeout])
 
         # Add the user agent metadata to the call.
         if self._metadata is not None:
@@ -237,7 +234,7 @@
     else:
         user_agent_metadata = None
 
-    return general_helpers.wraps(func)(
+    return functools.wraps(func)(
         _GapicCallable(
             func, default_retry, default_timeout, metadata=user_agent_metadata
         )
diff --git a/google/api_core/gapic_v1/method_async.py b/google/api_core/gapic_v1/method_async.py
index 76e5757..84c99aa 100644
--- a/google/api_core/gapic_v1/method_async.py
+++ b/google/api_core/gapic_v1/method_async.py
@@ -17,7 +17,9 @@
 pagination, and long-running operations to gRPC methods.
 """
 
-from google.api_core import general_helpers, grpc_helpers_async
+import functools
+
+from google.api_core import grpc_helpers_async
 from google.api_core.gapic_v1 import client_info
 from google.api_core.gapic_v1.method import _GapicCallable
 from google.api_core.gapic_v1.method import DEFAULT  # noqa: F401
@@ -41,6 +43,6 @@
 
     metadata = [client_info.to_grpc_metadata()] if client_info is not None else None
 
-    return general_helpers.wraps(func)(
+    return functools.wraps(func)(
         _GapicCallable(func, default_retry, default_timeout, metadata=metadata)
     )
diff --git a/google/api_core/gapic_v1/routing_header.py b/google/api_core/gapic_v1/routing_header.py
index 3fb12a6..a7bcb5a 100644
--- a/google/api_core/gapic_v1/routing_header.py
+++ b/google/api_core/gapic_v1/routing_header.py
@@ -20,9 +20,7 @@
 Generally, these headers are specified as gRPC metadata.
 """
 
-import sys
-
-from six.moves.urllib.parse import urlencode
+from urllib.parse import urlencode
 
 ROUTING_METADATA_KEY = "x-goog-request-params"
 
@@ -37,9 +35,6 @@
     Returns:
         str: The routing header string.
     """
-    if sys.version_info[0] < 3:
-        # Python 2 does not have the "safe" parameter for urlencode.
-        return urlencode(params).replace("%2F", "/")
     return urlencode(
         params,
         # Per Google API policy (go/api-url-encoding), / is not encoded.
diff --git a/google/api_core/general_helpers.py b/google/api_core/general_helpers.py
index d2d0c44..fba7802 100644
--- a/google/api_core/general_helpers.py
+++ b/google/api_core/general_helpers.py
@@ -12,22 +12,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Helpers for general Python functionality."""
-
-import functools
-
-import six
-
-
-# functools.partial objects lack several attributes present on real function
-# objects. In Python 2 wraps fails on this so use a restricted set instead.
-_PARTIAL_VALID_ASSIGNMENTS = ("__doc__",)
-
-
-def wraps(wrapped):
-    """A functools.wraps helper that handles partial objects on Python 2."""
-    # https://github.com/google/pytype/issues/322
-    if isinstance(wrapped, functools.partial):  # pytype: disable=wrong-arg-types
-        return six.wraps(wrapped, assigned=_PARTIAL_VALID_ASSIGNMENTS)
-    else:
-        return six.wraps(wrapped)
+# This import for backward compatibiltiy only.
+from functools import wraps  # noqa: F401 pragma: NO COVER
diff --git a/google/api_core/grpc_helpers.py b/google/api_core/grpc_helpers.py
index 62d9e53..594df98 100644
--- a/google/api_core/grpc_helpers.py
+++ b/google/api_core/grpc_helpers.py
@@ -15,13 +15,12 @@
 """Helpers for :mod:`grpc`."""
 
 import collections
+import functools
 
 import grpc
 import pkg_resources
-import six
 
 from google.api_core import exceptions
-from google.api_core import general_helpers
 import google.auth
 import google.auth.credentials
 import google.auth.transport.grpc
@@ -61,12 +60,12 @@
     """Map errors for Unary-Unary and Stream-Unary gRPC callables."""
     _patch_callable_name(callable_)
 
-    @six.wraps(callable_)
+    @functools.wraps(callable_)
     def error_remapped_callable(*args, **kwargs):
         try:
             return callable_(*args, **kwargs)
         except grpc.RpcError as exc:
-            six.raise_from(exceptions.from_grpc_error(exc), exc)
+            raise exceptions.from_grpc_error(exc) from exc
 
     return error_remapped_callable
 
@@ -80,7 +79,7 @@
         # to retrieve the first result, in order to fail, in order to trigger a retry.
         try:
             if prefetch_first_result:
-                self._stored_first_result = six.next(self._wrapped)
+                self._stored_first_result = next(self._wrapped)
         except TypeError:
             # It is possible the wrapped method isn't an iterable (a grpc.Call
             # for instance). If this happens don't store the first result.
@@ -93,7 +92,7 @@
         """This iterator is also an iterable that returns itself."""
         return self
 
-    def next(self):
+    def __next__(self):
         """Get the next response from the stream.
 
         Returns:
@@ -104,13 +103,10 @@
                 result = self._stored_first_result
                 del self._stored_first_result
                 return result
-            return six.next(self._wrapped)
+            return next(self._wrapped)
         except grpc.RpcError as exc:
             # If the stream has already returned data, we cannot recover here.
-            six.raise_from(exceptions.from_grpc_error(exc), exc)
-
-    # Alias needed for Python 2/3 support.
-    __next__ = next
+            raise exceptions.from_grpc_error(exc) from exc
 
     # grpc.Call & grpc.RpcContext interface
 
@@ -148,7 +144,7 @@
     """
     _patch_callable_name(callable_)
 
-    @general_helpers.wraps(callable_)
+    @functools.wraps(callable_)
     def error_remapped_callable(*args, **kwargs):
         try:
             result = callable_(*args, **kwargs)
@@ -161,7 +157,7 @@
                 result, prefetch_first_result=prefetch_first
             )
         except grpc.RpcError as exc:
-            six.raise_from(exceptions.from_grpc_error(exc), exc)
+            raise exceptions.from_grpc_error(exc) from exc
 
     return error_remapped_callable
 
diff --git a/google/api_core/iam.py b/google/api_core/iam.py
index 59e5387..4437c70 100644
--- a/google/api_core/iam.py
+++ b/google/api_core/iam.py
@@ -52,14 +52,10 @@
 """
 
 import collections
+import collections.abc
 import operator
 import warnings
 
-try:
-    from collections import abc as collections_abc
-except ImportError:  # Python 2.7
-    import collections as collections_abc
-
 # Generic IAM roles
 
 OWNER_ROLE = "roles/owner"
@@ -84,7 +80,7 @@
     pass
 
 
-class Policy(collections_abc.MutableMapping):
+class Policy(collections.abc.MutableMapping):
     """IAM Policy
 
     Args:
diff --git a/google/api_core/operations_v1/__init__.py b/google/api_core/operations_v1/__init__.py
index bc9befc..d7f963e 100644
--- a/google/api_core/operations_v1/__init__.py
+++ b/google/api_core/operations_v1/__init__.py
@@ -14,11 +14,7 @@
 
 """Package for interacting with the google.longrunning.operations meta-API."""
 
-import sys
-
+from google.api_core.operations_v1.operations_async_client import OperationsAsyncClient
 from google.api_core.operations_v1.operations_client import OperationsClient
 
-__all__ = ["OperationsClient"]
-if sys.version_info >= (3, 6, 0):
-    from google.api_core.operations_v1.operations_async_client import OperationsAsyncClient  # noqa: F401
-    __all__.append("OperationsAsyncClient")
+__all__ = ["OperationsAsyncClient", "OperationsClient"]
diff --git a/google/api_core/page_iterator.py b/google/api_core/page_iterator.py
index 49879bc..7ddc5cb 100644
--- a/google/api_core/page_iterator.py
+++ b/google/api_core/page_iterator.py
@@ -81,8 +81,6 @@
 
 import abc
 
-import six
-
 
 class Page(object):
     """Single page of results in an iterator.
@@ -127,18 +125,15 @@
         """The :class:`Page` is an iterator of items."""
         return self
 
-    def next(self):
+    def __next__(self):
         """Get the next value in the page."""
-        item = six.next(self._item_iter)
+        item = next(self._item_iter)
         result = self._item_to_value(self._parent, item)
         # Since we've successfully got the next value from the
         # iterator, we update the number of remaining.
         self._remaining -= 1
         return result
 
-    # Alias needed for Python 2/3 support.
-    __next__ = next
-
 
 def _item_to_value_identity(iterator, item):
     """An item to value transformer that returns the item un-changed."""
@@ -147,8 +142,7 @@
     return item
 
 
[email protected]_metaclass(abc.ABCMeta)
-class Iterator(object):
+class Iterator(object, metaclass=abc.ABCMeta):
     """A generic class for iterating through API list responses.
 
     Args:
@@ -235,9 +229,6 @@
             self.__active_iterator = iter(self)
         return next(self.__active_iterator)
 
-    # Preserve Python 2 compatibility.
-    next = __next__
-
     def _page_iter(self, increment):
         """Generator of pages of API responses.
 
@@ -484,7 +475,7 @@
                   there are no pages left.
         """
         try:
-            items = six.next(self._gax_page_iter)
+            items = next(self._gax_page_iter)
             page = Page(self, items, self.item_to_value)
             self.next_page_token = self._gax_page_iter.page_token or None
             return page
diff --git a/google/api_core/path_template.py b/google/api_core/path_template.py
index f202d40..c5969c1 100644
--- a/google/api_core/path_template.py
+++ b/google/api_core/path_template.py
@@ -28,8 +28,6 @@
 import functools
 import re
 
-import six
-
 # Regular expression for extracting variable parts from a path template.
 # The variables can be expressed as:
 #
@@ -83,7 +81,7 @@
     name = match.group("name")
     if name is not None:
         try:
-            return six.text_type(named_vars[name])
+            return str(named_vars[name])
         except KeyError:
             raise ValueError(
                 "Named variable '{}' not specified and needed by template "
@@ -91,7 +89,7 @@
             )
     elif positional is not None:
         try:
-            return six.text_type(positional_vars.pop(0))
+            return str(positional_vars.pop(0))
         except IndexError:
             raise ValueError(
                 "Positional variable not specified and needed by template "
diff --git a/google/api_core/protobuf_helpers.py b/google/api_core/protobuf_helpers.py
index 8aff79a..896e89c 100644
--- a/google/api_core/protobuf_helpers.py
+++ b/google/api_core/protobuf_helpers.py
@@ -15,6 +15,7 @@
 """Helpers for :mod:`protobuf`."""
 
 import collections
+import collections.abc
 import copy
 import inspect
 
@@ -22,11 +23,6 @@
 from google.protobuf import message
 from google.protobuf import wrappers_pb2
 
-try:
-    from collections import abc as collections_abc
-except ImportError:  # Python 2.7
-    import collections as collections_abc
-
 
 _SENTINEL = object()
 _WRAPPER_TYPES = (
@@ -179,7 +175,7 @@
     # If we get something else, complain.
     if isinstance(msg_or_dict, message.Message):
         answer = getattr(msg_or_dict, key, default)
-    elif isinstance(msg_or_dict, collections_abc.Mapping):
+    elif isinstance(msg_or_dict, collections.abc.Mapping):
         answer = msg_or_dict.get(key, default)
     else:
         raise TypeError(
@@ -204,7 +200,7 @@
     """Set helper for protobuf Messages."""
     # Attempt to set the value on the types of objects we know how to deal
     # with.
-    if isinstance(value, (collections_abc.MutableSequence, tuple)):
+    if isinstance(value, (collections.abc.MutableSequence, tuple)):
         # Clear the existing repeated protobuf message of any elements
         # currently inside it.
         while getattr(msg, key):
@@ -212,13 +208,13 @@
 
         # Write our new elements to the repeated field.
         for item in value:
-            if isinstance(item, collections_abc.Mapping):
+            if isinstance(item, collections.abc.Mapping):
                 getattr(msg, key).add(**item)
             else:
                 # protobuf's RepeatedCompositeContainer doesn't support
                 # append.
                 getattr(msg, key).extend([item])
-    elif isinstance(value, collections_abc.Mapping):
+    elif isinstance(value, collections.abc.Mapping):
         # Assign the dictionary values to the protobuf message.
         for item_key, item_value in value.items():
             set(getattr(msg, key), item_key, item_value)
@@ -241,7 +237,7 @@
         TypeError: If ``msg_or_dict`` is not a Message or dictionary.
     """
     # Sanity check: Is our target object valid?
-    if not isinstance(msg_or_dict, (collections_abc.MutableMapping, message.Message)):
+    if not isinstance(msg_or_dict, (collections.abc.MutableMapping, message.Message)):
         raise TypeError(
             "set() expected a dict or protobuf message, got {!r}.".format(
                 type(msg_or_dict)
@@ -254,12 +250,12 @@
     # If a subkey exists, then get that object and call this method
     # recursively against it using the subkey.
     if subkey is not None:
-        if isinstance(msg_or_dict, collections_abc.MutableMapping):
+        if isinstance(msg_or_dict, collections.abc.MutableMapping):
             msg_or_dict.setdefault(basekey, {})
         set(get(msg_or_dict, basekey), subkey, value)
         return
 
-    if isinstance(msg_or_dict, collections_abc.MutableMapping):
+    if isinstance(msg_or_dict, collections.abc.MutableMapping):
         msg_or_dict[key] = value
     else:
         _set_field_on_message(msg_or_dict, key, value)
diff --git a/google/api_core/retry.py b/google/api_core/retry.py
index 8496793..d39f97c 100644
--- a/google/api_core/retry.py
+++ b/google/api_core/retry.py
@@ -63,11 +63,9 @@
 import time
 
 import requests.exceptions
-import six
 
 from google.api_core import datetime_helpers
 from google.api_core import exceptions
-from google.api_core import general_helpers
 from google.auth import exceptions as auth_exceptions
 
 _LOGGER = logging.getLogger(__name__)
@@ -201,15 +199,12 @@
 
         if deadline_datetime is not None:
             if deadline_datetime <= now:
-                six.raise_from(
-                    exceptions.RetryError(
-                        "Deadline of {:.1f}s exceeded while calling {}".format(
-                            deadline, target
-                        ),
-                        last_exc,
+                raise exceptions.RetryError(
+                    "Deadline of {:.1f}s exceeded while calling {}".format(
+                        deadline, target
                     ),
                     last_exc,
-                )
+                ) from last_exc
             else:
                 time_to_deadline = (deadline_datetime - now).total_seconds()
                 sleep = min(time_to_deadline, sleep)
@@ -222,7 +217,6 @@
     raise ValueError("Sleep generator stopped yielding sleep values.")
 
 
[email protected]_2_unicode_compatible
 class Retry(object):
     """Exponential retry decorator.
 
@@ -276,7 +270,7 @@
         if self._on_error is not None:
             on_error = self._on_error
 
-        @general_helpers.wraps(func)
+        @functools.wraps(func)
         def retry_wrapped_func(*args, **kwargs):
             """A wrapper that calls target function with retry."""
             target = functools.partial(func, *args, **kwargs)
diff --git a/google/api_core/timeout.py b/google/api_core/timeout.py
index 17c1bea..7323218 100644
--- a/google/api_core/timeout.py
+++ b/google/api_core/timeout.py
@@ -54,11 +54,9 @@
 from __future__ import unicode_literals
 
 import datetime
-
-import six
+import functools
 
 from google.api_core import datetime_helpers
-from google.api_core import general_helpers
 
 _DEFAULT_INITIAL_TIMEOUT = 5.0  # seconds
 _DEFAULT_MAXIMUM_TIMEOUT = 30.0  # seconds
@@ -68,7 +66,6 @@
 _DEFAULT_DEADLINE = None
 
 
[email protected]_2_unicode_compatible
 class ConstantTimeout(object):
     """A decorator that adds a constant timeout argument.
 
@@ -95,7 +92,7 @@
             Callable: The wrapped function.
         """
 
-        @general_helpers.wraps(func)
+        @functools.wraps(func)
         def func_with_timeout(*args, **kwargs):
             """Wrapped function that adds timeout."""
             kwargs["timeout"] = self._timeout
@@ -140,7 +137,6 @@
         timeout = timeout * multiplier
 
 
[email protected]_2_unicode_compatible
 class ExponentialTimeout(object):
     """A decorator that adds an exponentially increasing timeout argument.
 
@@ -207,7 +203,7 @@
             self._initial, self._maximum, self._multiplier, self._deadline
         )
 
-        @general_helpers.wraps(func)
+        @functools.wraps(func)
         def func_with_timeout(*args, **kwargs):
             """Wrapped function that adds timeout."""
             kwargs["timeout"] = next(timeouts)
diff --git a/google/api_core/version.py b/google/api_core/version.py
index 5fd0b13..c9cdad6 100644
--- a/google/api_core/version.py
+++ b/google/api_core/version.py
@@ -12,4 +12,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-__version__ = "1.31.1"
+__version__ = "2.0.0-b1"
diff --git a/noxfile.py b/noxfile.py
index 2f11137..a8f464e 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -111,13 +111,13 @@
         session.run(*pytest_args)
 
 
[email protected](python=["2.7", "3.6", "3.7", "3.8", "3.9"])
[email protected](python=["3.6", "3.7", "3.8", "3.9"])
 def unit(session):
     """Run the unit test suite."""
     default(session)
 
 
[email protected](python=["2.7", "3.6", "3.7", "3.8", "3.9"])
[email protected](python=["3.6", "3.7", "3.8", "3.9"])
 def unit_grpc_gcp(session):
     """Run the unit test suite with grpcio-gcp installed."""
     constraints_path = str(
@@ -137,7 +137,6 @@
     session.run("python", "setup.py", "check", "--restructuredtext", "--strict")
 
 
-# No 2.7 due to https://github.com/google/importlab/issues/26.
 # No 3.7 because pytype supports up to 3.6 only.
 @nox.session(python="3.6")
 def pytype(session):
diff --git a/owlbot.py b/owlbot.py
index 5c590e4..451f7c4 100644
--- a/owlbot.py
+++ b/owlbot.py
@@ -22,8 +22,15 @@
 # ----------------------------------------------------------------------------
 # Add templated files
 # ----------------------------------------------------------------------------
-templated_files = common.py_library(cov_level=100)
-s.move(templated_files, excludes=["noxfile.py", ".flake8", ".coveragerc", "setup.cfg"])
+excludes = [
+    "noxfile.py",  # pytype
+    "setup.cfg",  # pytype
+    ".flake8",  # flake8-import-order, layout
+    ".coveragerc",  # layout
+    "CONTRIBUTING.rst",  # no systests
+]
+templated_files = common.py_library(microgenerator=True, cov_level=100)
+s.move(templated_files, excludes=excludes)
 
 # Add pytype support
 s.replace(
diff --git a/setup.py b/setup.py
index 26fab7e..d98c69c 100644
--- a/setup.py
+++ b/setup.py
@@ -31,12 +31,9 @@
 dependencies = [
     "googleapis-common-protos >= 1.6.0, < 2.0dev",
     "protobuf >= 3.12.0",
-    "google-auth >= 1.25.0, < 2.0dev",
+    "google-auth >= 1.25.0, < 3.0dev",
     "requests >= 2.18.0, < 3.0.0dev",
     "setuptools >= 40.3.0",
-    "packaging >= 14.3",
-    "six >= 1.13.0",
-    "pytz",
     'futures >= 3.2.0; python_version < "3.2"',
 ]
 extras = {
@@ -86,10 +83,7 @@
         "Intended Audience :: Developers",
         "License :: OSI Approved :: Apache Software License",
         "Programming Language :: Python",
-        "Programming Language :: Python :: 2",
-        "Programming Language :: Python :: 2.7",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.5",
         "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
@@ -102,7 +96,7 @@
     namespace_packages=namespaces,
     install_requires=dependencies,
     extras_require=extras,
-    python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*",
+    python_requires=">=3.6",
     include_package_data=True,
     zip_safe=False,
 )
diff --git a/testing/constraints-2.7.txt b/testing/constraints-2.7.txt
deleted file mode 100644
index 246c89d..0000000
--- a/testing/constraints-2.7.txt
+++ /dev/null
@@ -1 +0,0 @@
-googleapis-common-protos >= 1.6.0, < 1.53dev
diff --git a/testing/constraints-3.6.txt b/testing/constraints-3.6.txt
index ff5b4a7..2544e8e 100644
--- a/testing/constraints-3.6.txt
+++ b/testing/constraints-3.6.txt
@@ -11,7 +11,6 @@
 requests==2.18.0
 setuptools==40.3.0
 packaging==14.3
-six==1.13.0
 grpcio==1.29.0
 grpcio-gcp==0.2.2
 grpcio-gcp==0.2.2
diff --git a/tests/unit/test_bidi.py b/tests/unit/test_bidi.py
index 602d640..b876d9a 100644
--- a/tests/unit/test_bidi.py
+++ b/tests/unit/test_bidi.py
@@ -14,12 +14,12 @@
 
 import datetime
 import logging
+import queue
 import threading
 
 import grpc
 import mock
 import pytest
-from six.moves import queue
 
 from google.api_core import bidi
 from google.api_core import exceptions
@@ -221,18 +221,12 @@
 
 
 class ClosedCall(object):
-    # NOTE: This is needed because defining `.next` on an **instance**
-    #       rather than the **class** will not be iterable in Python 2.
-    #       This is problematic since a `Mock` just sets members.
-
     def __init__(self, exception):
         self.exception = exception
 
     def __next__(self):
         raise self.exception
 
-    next = __next__  # Python 2
-
     def is_active(self):
         return False
 
@@ -354,8 +348,6 @@
             raise item
         return item
 
-    next = __next__  # Python 2
-
     def is_active(self):
         return self._is_active
 
diff --git a/tests/unit/test_datetime_helpers.py b/tests/unit/test_datetime_helpers.py
index 4ddcf36..5f5470a 100644
--- a/tests/unit/test_datetime_helpers.py
+++ b/tests/unit/test_datetime_helpers.py
@@ -16,7 +16,6 @@
 import datetime
 
 import pytest
-import pytz
 
 from google.api_core import datetime_helpers
 from google.protobuf import timestamp_pb2
@@ -31,7 +30,7 @@
 
 
 def test_to_milliseconds():
-    dt = datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc)
+    dt = datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=datetime.timezone.utc)
     assert datetime_helpers.to_milliseconds(dt) == 1000
 
 
@@ -42,7 +41,7 @@
 
 
 def test_to_microseconds_non_utc():
-    zone = pytz.FixedOffset(-1)
+    zone = datetime.timezone(datetime.timedelta(minutes=-1))
     dt = datetime.datetime(1970, 1, 1, 0, 0, 0, tzinfo=zone)
     assert datetime_helpers.to_microseconds(dt) == ONE_MINUTE_IN_MICROSECONDS
 
@@ -56,7 +55,7 @@
 def test_from_microseconds():
     five_mins_from_epoch_in_microseconds = 5 * ONE_MINUTE_IN_MICROSECONDS
     five_mins_from_epoch_datetime = datetime.datetime(
-        1970, 1, 1, 0, 5, 0, tzinfo=pytz.utc
+        1970, 1, 1, 0, 5, 0, tzinfo=datetime.timezone.utc
     )
 
     result = datetime_helpers.from_microseconds(five_mins_from_epoch_in_microseconds)
@@ -78,28 +77,28 @@
 def test_from_rfc3339():
     value = "2009-12-17T12:44:32.123456Z"
     assert datetime_helpers.from_rfc3339(value) == datetime.datetime(
-        2009, 12, 17, 12, 44, 32, 123456, pytz.utc
+        2009, 12, 17, 12, 44, 32, 123456, datetime.timezone.utc
     )
 
 
 def test_from_rfc3339_nanos():
     value = "2009-12-17T12:44:32.123456Z"
     assert datetime_helpers.from_rfc3339_nanos(value) == datetime.datetime(
-        2009, 12, 17, 12, 44, 32, 123456, pytz.utc
+        2009, 12, 17, 12, 44, 32, 123456, datetime.timezone.utc
     )
 
 
 def test_from_rfc3339_without_nanos():
     value = "2009-12-17T12:44:32Z"
     assert datetime_helpers.from_rfc3339(value) == datetime.datetime(
-        2009, 12, 17, 12, 44, 32, 0, pytz.utc
+        2009, 12, 17, 12, 44, 32, 0, datetime.timezone.utc
     )
 
 
 def test_from_rfc3339_nanos_without_nanos():
     value = "2009-12-17T12:44:32Z"
     assert datetime_helpers.from_rfc3339_nanos(value) == datetime.datetime(
-        2009, 12, 17, 12, 44, 32, 0, pytz.utc
+        2009, 12, 17, 12, 44, 32, 0, datetime.timezone.utc
     )
 
 
@@ -119,7 +118,7 @@
 def test_from_rfc3339_with_truncated_nanos(truncated, micros):
     value = "2009-12-17T12:44:32.{}Z".format(truncated)
     assert datetime_helpers.from_rfc3339(value) == datetime.datetime(
-        2009, 12, 17, 12, 44, 32, micros, pytz.utc
+        2009, 12, 17, 12, 44, 32, micros, datetime.timezone.utc
     )
 
 
@@ -148,7 +147,7 @@
 def test_from_rfc3339_nanos_with_truncated_nanos(truncated, micros):
     value = "2009-12-17T12:44:32.{}Z".format(truncated)
     assert datetime_helpers.from_rfc3339_nanos(value) == datetime.datetime(
-        2009, 12, 17, 12, 44, 32, micros, pytz.utc
+        2009, 12, 17, 12, 44, 32, micros, datetime.timezone.utc
     )
 
 
@@ -171,20 +170,20 @@
 
 
 def test_to_rfc3339_with_utc():
-    value = datetime.datetime(2016, 4, 5, 13, 30, 0, tzinfo=pytz.utc)
+    value = datetime.datetime(2016, 4, 5, 13, 30, 0, tzinfo=datetime.timezone.utc)
     expected = "2016-04-05T13:30:00.000000Z"
     assert datetime_helpers.to_rfc3339(value, ignore_zone=False) == expected
 
 
 def test_to_rfc3339_with_non_utc():
-    zone = pytz.FixedOffset(-60)
+    zone = datetime.timezone(datetime.timedelta(minutes=-60))
     value = datetime.datetime(2016, 4, 5, 13, 30, 0, tzinfo=zone)
     expected = "2016-04-05T14:30:00.000000Z"
     assert datetime_helpers.to_rfc3339(value, ignore_zone=False) == expected
 
 
 def test_to_rfc3339_with_non_utc_ignore_zone():
-    zone = pytz.FixedOffset(-60)
+    zone = datetime.timezone(datetime.timedelta(minutes=-60))
     value = datetime.datetime(2016, 4, 5, 13, 30, 0, tzinfo=zone)
     expected = "2016-04-05T13:30:00.000000Z"
     assert datetime_helpers.to_rfc3339(value, ignore_zone=True) == expected
@@ -283,7 +282,7 @@
     def test_from_rfc3339_wo_fraction():
         timestamp = "2016-12-20T21:13:47Z"
         expected = datetime_helpers.DatetimeWithNanoseconds(
-            2016, 12, 20, 21, 13, 47, tzinfo=pytz.UTC
+            2016, 12, 20, 21, 13, 47, tzinfo=datetime.timezone.utc
         )
         stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
         assert stamp == expected
@@ -292,7 +291,7 @@
     def test_from_rfc3339_w_partial_precision():
         timestamp = "2016-12-20T21:13:47.1Z"
         expected = datetime_helpers.DatetimeWithNanoseconds(
-            2016, 12, 20, 21, 13, 47, microsecond=100000, tzinfo=pytz.UTC
+            2016, 12, 20, 21, 13, 47, microsecond=100000, tzinfo=datetime.timezone.utc
         )
         stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
         assert stamp == expected
@@ -301,7 +300,7 @@
     def test_from_rfc3339_w_full_precision():
         timestamp = "2016-12-20T21:13:47.123456789Z"
         expected = datetime_helpers.DatetimeWithNanoseconds(
-            2016, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=pytz.UTC
+            2016, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=datetime.timezone.utc
         )
         stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
         assert stamp == expected
@@ -332,7 +331,9 @@
         stamp = datetime_helpers.DatetimeWithNanoseconds(
             2016, 12, 20, 21, 13, 47, 123456
         )
-        delta = stamp.replace(tzinfo=pytz.UTC) - datetime_helpers._UTC_EPOCH
+        delta = (
+            stamp.replace(tzinfo=datetime.timezone.utc) - datetime_helpers._UTC_EPOCH
+        )
         seconds = int(delta.total_seconds())
         nanos = 123456000
         timestamp = timestamp_pb2.Timestamp(seconds=seconds, nanos=nanos)
@@ -341,7 +342,7 @@
     @staticmethod
     def test_timestamp_pb_w_nanos():
         stamp = datetime_helpers.DatetimeWithNanoseconds(
-            2016, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=pytz.UTC
+            2016, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=datetime.timezone.utc
         )
         delta = stamp - datetime_helpers._UTC_EPOCH
         timestamp = timestamp_pb2.Timestamp(
@@ -351,7 +352,9 @@
 
     @staticmethod
     def test_from_timestamp_pb_wo_nanos():
-        when = datetime.datetime(2016, 12, 20, 21, 13, 47, 123456, tzinfo=pytz.UTC)
+        when = datetime.datetime(
+            2016, 12, 20, 21, 13, 47, 123456, tzinfo=datetime.timezone.utc
+        )
         delta = when - datetime_helpers._UTC_EPOCH
         seconds = int(delta.total_seconds())
         timestamp = timestamp_pb2.Timestamp(seconds=seconds)
@@ -361,11 +364,13 @@
         assert _to_seconds(when) == _to_seconds(stamp)
         assert stamp.microsecond == 0
         assert stamp.nanosecond == 0
-        assert stamp.tzinfo == pytz.UTC
+        assert stamp.tzinfo == datetime.timezone.utc
 
     @staticmethod
     def test_from_timestamp_pb_w_nanos():
-        when = datetime.datetime(2016, 12, 20, 21, 13, 47, 123456, tzinfo=pytz.UTC)
+        when = datetime.datetime(
+            2016, 12, 20, 21, 13, 47, 123456, tzinfo=datetime.timezone.utc
+        )
         delta = when - datetime_helpers._UTC_EPOCH
         seconds = int(delta.total_seconds())
         timestamp = timestamp_pb2.Timestamp(seconds=seconds, nanos=123456789)
@@ -375,7 +380,7 @@
         assert _to_seconds(when) == _to_seconds(stamp)
         assert stamp.microsecond == 123456
         assert stamp.nanosecond == 123456789
-        assert stamp.tzinfo == pytz.UTC
+        assert stamp.tzinfo == datetime.timezone.utc
 
 
 def _to_seconds(value):
@@ -387,5 +392,5 @@
     Returns:
         int: Microseconds since the unix epoch.
     """
-    assert value.tzinfo is pytz.UTC
+    assert value.tzinfo is datetime.timezone.utc
     return calendar.timegm(value.timetuple())
diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py
index fb29015..1059945 100644
--- a/tests/unit/test_exceptions.py
+++ b/tests/unit/test_exceptions.py
@@ -12,12 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import http.client
 import json
 
 import grpc
 import mock
 import requests
-from six.moves import http_client
 
 from google.api_core import exceptions
 
@@ -50,8 +50,8 @@
 
 def test_from_http_status():
     message = "message"
-    exception = exceptions.from_http_status(http_client.NOT_FOUND, message)
-    assert exception.code == http_client.NOT_FOUND
+    exception = exceptions.from_http_status(http.client.NOT_FOUND, message)
+    assert exception.code == http.client.NOT_FOUND
     assert exception.message == message
     assert exception.errors == []
 
@@ -61,11 +61,11 @@
     errors = ["1", "2"]
     response = mock.sentinel.response
     exception = exceptions.from_http_status(
-        http_client.NOT_FOUND, message, errors=errors, response=response
+        http.client.NOT_FOUND, message, errors=errors, response=response
     )
 
     assert isinstance(exception, exceptions.NotFound)
-    assert exception.code == http_client.NOT_FOUND
+    assert exception.code == http.client.NOT_FOUND
     assert exception.message == message
     assert exception.errors == errors
     assert exception.response == response
@@ -82,7 +82,7 @@
 def make_response(content):
     response = requests.Response()
     response._content = content
-    response.status_code = http_client.NOT_FOUND
+    response.status_code = http.client.NOT_FOUND
     response.request = requests.Request(
         method="POST", url="https://example.com"
     ).prepare()
@@ -95,7 +95,7 @@
     exception = exceptions.from_http_response(response)
 
     assert isinstance(exception, exceptions.NotFound)
-    assert exception.code == http_client.NOT_FOUND
+    assert exception.code == http.client.NOT_FOUND
     assert exception.message == "POST https://example.com/: unknown error"
     assert exception.response == response
 
@@ -106,7 +106,7 @@
     exception = exceptions.from_http_response(response)
 
     assert isinstance(exception, exceptions.NotFound)
-    assert exception.code == http_client.NOT_FOUND
+    assert exception.code == http.client.NOT_FOUND
     assert exception.message == "POST https://example.com/: message"
 
 
@@ -120,7 +120,7 @@
     exception = exceptions.from_http_response(response)
 
     assert isinstance(exception, exceptions.NotFound)
-    assert exception.code == http_client.NOT_FOUND
+    assert exception.code == http.client.NOT_FOUND
     assert exception.message == "POST https://example.com/: json message"
     assert exception.errors == ["1", "2"]
 
@@ -131,22 +131,22 @@
     exception = exceptions.from_http_response(response)
 
     assert isinstance(exception, exceptions.NotFound)
-    assert exception.code == http_client.NOT_FOUND
+    assert exception.code == http.client.NOT_FOUND
     assert exception.message == "POST https://example.com/: unknown error"
 
 
 def test_from_http_response_json_unicode_content():
     response = make_response(
         json.dumps(
-            {"error": {"message": u"\u2019 message", "errors": ["1", "2"]}}
+            {"error": {"message": "\u2019 message", "errors": ["1", "2"]}}
         ).encode("utf-8")
     )
 
     exception = exceptions.from_http_response(response)
 
     assert isinstance(exception, exceptions.NotFound)
-    assert exception.code == http_client.NOT_FOUND
-    assert exception.message == u"POST https://example.com/: \u2019 message"
+    assert exception.code == http.client.NOT_FOUND
+    assert exception.message == "POST https://example.com/: \u2019 message"
     assert exception.errors == ["1", "2"]
 
 
@@ -155,7 +155,7 @@
     exception = exceptions.from_grpc_status(grpc.StatusCode.OUT_OF_RANGE, message)
     assert isinstance(exception, exceptions.BadRequest)
     assert isinstance(exception, exceptions.OutOfRange)
-    assert exception.code == http_client.BAD_REQUEST
+    assert exception.code == http.client.BAD_REQUEST
     assert exception.grpc_status_code == grpc.StatusCode.OUT_OF_RANGE
     assert exception.message == message
     assert exception.errors == []
@@ -166,7 +166,7 @@
     exception = exceptions.from_grpc_status(11, message)
     assert isinstance(exception, exceptions.BadRequest)
     assert isinstance(exception, exceptions.OutOfRange)
-    assert exception.code == http_client.BAD_REQUEST
+    assert exception.code == http.client.BAD_REQUEST
     assert exception.grpc_status_code == grpc.StatusCode.OUT_OF_RANGE
     assert exception.message == message
     assert exception.errors == []
@@ -203,7 +203,7 @@
 
     assert isinstance(exception, exceptions.BadRequest)
     assert isinstance(exception, exceptions.InvalidArgument)
-    assert exception.code == http_client.BAD_REQUEST
+    assert exception.code == http.client.BAD_REQUEST
     assert exception.grpc_status_code == grpc.StatusCode.INVALID_ARGUMENT
     assert exception.message == message
     assert exception.errors == [error]
diff --git a/tests/unit/test_general_helpers.py b/tests/unit/test_general_helpers.py
deleted file mode 100644
index 027d489..0000000
--- a/tests/unit/test_general_helpers.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# Copyright 2017, 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
-#
-#     http://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.
-
-import functools
-
-from google.api_core import general_helpers
-
-
-def test_wraps_normal_func():
-    def func():
-        return 42
-
-    @general_helpers.wraps(func)
-    def replacement():
-        return func()
-
-    assert replacement() == 42
-
-
-def test_wraps_partial():
-    def func():
-        return 42
-
-    partial = functools.partial(func)
-
-    @general_helpers.wraps(partial)
-    def replacement():
-        return func()
-
-    assert replacement() == 42
diff --git a/tests/unit/test_page_iterator.py b/tests/unit/test_page_iterator.py
index 668cf39..a44e998 100644
--- a/tests/unit/test_page_iterator.py
+++ b/tests/unit/test_page_iterator.py
@@ -17,7 +17,6 @@
 
 import mock
 import pytest
-import six
 
 from google.api_core import page_iterator
 
@@ -56,17 +55,17 @@
         assert item_to_value.call_count == 0
         assert page.remaining == 100
 
-        assert six.next(page) == 10
+        assert next(page) == 10
         assert item_to_value.call_count == 1
         item_to_value.assert_called_with(parent, 10)
         assert page.remaining == 99
 
-        assert six.next(page) == 11
+        assert next(page) == 11
         assert item_to_value.call_count == 2
         item_to_value.assert_called_with(parent, 11)
         assert page.remaining == 98
 
-        assert six.next(page) == 12
+        assert next(page) == 12
         assert item_to_value.call_count == 3
         item_to_value.assert_called_with(parent, 12)
         assert page.remaining == 97
@@ -197,17 +196,17 @@
         # Consume items and check the state of the iterator.
         assert iterator.num_results == 0
 
-        assert six.next(items_iter) == item1
+        assert next(items_iter) == item1
         assert iterator.num_results == 1
 
-        assert six.next(items_iter) == item2
+        assert next(items_iter) == item2
         assert iterator.num_results == 2
 
-        assert six.next(items_iter) == item3
+        assert next(items_iter) == item3
         assert iterator.num_results == 3
 
         with pytest.raises(StopIteration):
-            six.next(items_iter)
+            next(items_iter)
 
     def test___iter__(self):
         iterator = PageIteratorImpl(None, None)
@@ -289,16 +288,16 @@
 
         items_iter = iter(iterator)
 
-        val1 = six.next(items_iter)
+        val1 = next(items_iter)
         assert val1 == item1
         assert iterator.num_results == 1
 
-        val2 = six.next(items_iter)
+        val2 = next(items_iter)
         assert val2 == item2
         assert iterator.num_results == 2
 
         with pytest.raises(StopIteration):
-            six.next(items_iter)
+            next(items_iter)
 
         api_request.assert_called_once_with(method="GET", path=path, query_params={})
 
@@ -503,7 +502,7 @@
             items_iter = iter(iterator.pages)
             npages = int(math.ceil(float(n_results) / page_size))
             for ipage in range(npages):
-                assert list(six.next(items_iter)) == [
+                assert list(next(items_iter)) == [
                     dict(name=str(i))
                     for i in range(
                         ipage * page_size, min((ipage + 1) * page_size, n_results),
@@ -512,11 +511,11 @@
         else:
             items_iter = iter(iterator)
             for i in range(n_results):
-                assert six.next(items_iter) == dict(name=str(i))
+                assert next(items_iter) == dict(name=str(i))
                 assert iterator.num_results == i + 1
 
         with pytest.raises(StopIteration):
-            six.next(items_iter)
+            next(items_iter)
 
 
 class TestGRPCIterator(object):
@@ -621,7 +620,7 @@
         self.page_token = page_token
 
     def next(self):
-        return six.next(self._pages)
+        return next(self._pages)
 
     __next__ = next