fix: drop six dependency (#1452)

Fixes #1446 🦕
diff --git a/googleapiclient/_helpers.py b/googleapiclient/_helpers.py
index ddbd0e2..eb5e090 100644
--- a/googleapiclient/_helpers.py
+++ b/googleapiclient/_helpers.py
@@ -17,10 +17,7 @@
 import functools
 import inspect
 import logging
-import warnings
-
-import six
-from six.moves import urllib
+import urllib
 
 
 logger = logging.getLogger(__name__)
@@ -135,7 +132,7 @@
 
         return positional_wrapper
 
-    if isinstance(max_positional_args, six.integer_types):
+    if isinstance(max_positional_args, int):
         return positional_decorator
     else:
         args, _, _, defaults = inspect.getargspec(max_positional_args)
@@ -156,7 +153,7 @@
     """
     urlencoded_params = urllib.parse.parse_qs(content)
     params = {}
-    for key, value in six.iteritems(urlencoded_params):
+    for key, value in urlencoded_params.items():
         if len(value) != 1:
             msg = "URL-encoded content contains a repeated value:" "%s -> %s" % (
                 key,
diff --git a/googleapiclient/channel.py b/googleapiclient/channel.py
index efff0f6..70af779 100644
--- a/googleapiclient/channel.py
+++ b/googleapiclient/channel.py
@@ -76,7 +76,6 @@
 
 from googleapiclient import errors
 from googleapiclient import _helpers as util
-import six
 
 
 # The unix time epoch starts at midnight 1970.
@@ -104,7 +103,7 @@
 
 def _upper_header_keys(headers):
     new_headers = {}
-    for k, v in six.iteritems(headers):
+    for k, v in headers.items():
         new_headers[k.upper()] = v
     return new_headers
 
@@ -244,7 +243,7 @@
     Args:
       resp: dict, The response from a watch() method.
     """
-        for json_name, param_name in six.iteritems(CHANNEL_PARAMS):
+        for json_name, param_name in CHANNEL_PARAMS.items():
             value = resp.get(json_name)
             if value is not None:
                 setattr(self, param_name, value)
diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py
index 3273899..1b7aedd 100644
--- a/googleapiclient/discovery.py
+++ b/googleapiclient/discovery.py
@@ -17,31 +17,26 @@
 A client library for Google's discovery based APIs.
 """
 from __future__ import absolute_import
-import six
 
 __author__ = "jcgregorio@google.com (Joe Gregorio)"
 __all__ = ["build", "build_from_document", "fix_method_name", "key2param"]
 
-from six.moves import http_client
-from six.moves.urllib.parse import urljoin  
-
-
 # Standard library imports
 import copy
 from collections import OrderedDict
-
-try:
-    from email.generator import BytesGenerator
-except ImportError:
-    from email.generator import Generator as BytesGenerator
+import collections.abc
+from email.generator import BytesGenerator
 from email.mime.multipart import MIMEMultipart
 from email.mime.nonmultipart import MIMENonMultipart
+import http.client as http_client
+import io
 import json
 import keyword
 import logging
 import mimetypes
 import os
 import re
+import urllib
 
 # Third-party imports
 import httplib2
@@ -506,7 +501,7 @@
 
     if client_options is None:
         client_options = google.api_core.client_options.ClientOptions()
-    if isinstance(client_options, six.moves.collections_abc.Mapping):
+    if isinstance(client_options, collections.abc.Mapping):
         client_options = google.api_core.client_options.from_dict(client_options)
 
     if http is not None:
@@ -519,9 +514,9 @@
             if option is not None:
                 raise ValueError("Arguments http and {} are mutually exclusive".format(name))
 
-    if isinstance(service, six.string_types):
+    if isinstance(service, str):
         service = json.loads(service)
-    elif isinstance(service, six.binary_type):
+    elif isinstance(service, bytes):
         service = json.loads(service.decode("utf-8"))
 
     if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)):
@@ -534,7 +529,7 @@
         raise InvalidJsonError()
 
     # If an API Endpoint is provided on client options, use that as the base URL
-    base = urljoin(service["rootUrl"], service["servicePath"])
+    base = urllib.parse.urljoin(service["rootUrl"], service["servicePath"])
     if client_options.api_endpoint:
         base = client_options.api_endpoint
 
@@ -630,7 +625,7 @@
         if "mtlsRootUrl" in service and (
             not client_options or not client_options.api_endpoint
         ):
-            mtls_endpoint = urljoin(service["mtlsRootUrl"], service["servicePath"])
+            mtls_endpoint = urllib.parse.urljoin(service["mtlsRootUrl"], service["servicePath"])
             use_mtls_endpoint = os.getenv(GOOGLE_API_USE_MTLS_ENDPOINT, "auto")
 
             if not use_mtls_endpoint in ("never", "auto", "always"):
@@ -759,7 +754,7 @@
     parameters = method_desc.setdefault("parameters", {})
 
     # Add in the parameters common to all methods.
-    for name, description in six.iteritems(root_desc.get("parameters", {})):
+    for name, description in root_desc.get("parameters", {}).items():
         parameters[name] = description
 
     # Add in undocumented query parameters.
@@ -875,7 +870,7 @@
     # exception here is the case of media uploads, where url will be an
     # absolute url.
     if url.startswith("http://") or url.startswith("https://"):
-        return urljoin(base, url)
+        return urllib.parse.urljoin(base, url)
     new_base = base if base.endswith("/") else base + "/"
     new_url = url[1:] if url.startswith("/") else url
     return new_base + new_url
@@ -943,7 +938,7 @@
     """
         parameters = method_desc.get("parameters", {})
         sorted_parameters = OrderedDict(sorted(parameters.items()))
-        for arg, desc in six.iteritems(sorted_parameters):
+        for arg, desc in sorted_parameters.items():
             param = key2param(arg)
             self.argmap[param] = arg
 
@@ -997,9 +992,9 @@
     def method(self, **kwargs):
         # Don't bother with doc string, it will be over-written by createMethod.
 
-        for name in six.iterkeys(kwargs):
+        for name in kwargs:
             if name not in parameters.argmap:
-                raise TypeError('Got an unexpected keyword argument "%s"' % name)
+                raise TypeError('Got an unexpected keyword argument {}'.format(name))
 
         # Remove args that have a value of None.
         keys = list(kwargs.keys())
@@ -1016,9 +1011,9 @@
                 ):
                     raise TypeError('Missing required parameter "%s"' % name)
 
-        for name, regex in six.iteritems(parameters.pattern_params):
+        for name, regex in parameters.pattern_params.items():
             if name in kwargs:
-                if isinstance(kwargs[name], six.string_types):
+                if isinstance(kwargs[name], str):
                     pvalues = [kwargs[name]]
                 else:
                     pvalues = kwargs[name]
@@ -1029,13 +1024,13 @@
                             % (name, pvalue, regex)
                         )
 
-        for name, enums in six.iteritems(parameters.enum_params):
+        for name, enums in parameters.enum_params.items():
             if name in kwargs:
                 # We need to handle the case of a repeated enum
                 # name differently, since we want to handle both
                 # arg='value' and arg=['value1', 'value2']
                 if name in parameters.repeated_params and not isinstance(
-                    kwargs[name], six.string_types
+                    kwargs[name], str
                 ):
                     values = kwargs[name]
                 else:
@@ -1049,7 +1044,7 @@
 
         actual_query_params = {}
         actual_path_params = {}
-        for key, value in six.iteritems(kwargs):
+        for key, value in kwargs.items():
             to_type = parameters.param_types.get(key, "string")
             # For repeated parameters we cast each member of the list.
             if key in parameters.repeated_params and type(value) == type([]):
@@ -1086,7 +1081,7 @@
 
         if media_filename:
             # Ensure we end up with a valid MediaUpload object.
-            if isinstance(media_filename, six.string_types):
+            if isinstance(media_filename, str):
                 if media_mime_type is None:
                     logger.warning(
                         "media_mime_type argument not specified: trying to auto-detect for %s",
@@ -1144,7 +1139,7 @@
                     msgRoot.attach(msg)
                     # encode the body: note that we can't use `as_string`, because
                     # it plays games with `From ` lines.
-                    fp = six.BytesIO()
+                    fp = io.BytesIO()
                     g = _BytesGenerator(fp, mangle_from_=False)
                     g.flatten(msgRoot, unixfrom=False)
                     body = fp.getvalue()
@@ -1218,7 +1213,7 @@
         enumDesc = paramdesc.get("enumDescriptions", [])
         if enum and enumDesc:
             docs.append("    Allowed values\n")
-            for (name, desc) in six.moves.zip(enum, enumDesc):
+            for (name, desc) in zip(enum, enumDesc):
                 docs.append("      %s - %s\n" % (name, desc))
     if "response" in methodDesc:
         if methodName.endswith("_media"):
@@ -1415,7 +1410,7 @@
 
         # Add basic methods to Resource
         if "methods" in resourceDesc:
-            for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
+            for methodName, methodDesc in resourceDesc["methods"].items():
                 fixedMethodName, method = createMethod(
                     methodName, methodDesc, rootDesc, schema
                 )
@@ -1463,7 +1458,7 @@
 
                 return (methodName, methodResource)
 
-            for methodName, methodDesc in six.iteritems(resourceDesc["resources"]):
+            for methodName, methodDesc in resourceDesc["resources"].items():
                 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
                 self._set_dynamic_attr(
                     fixedMethodName, method.__get__(self, self.__class__)
@@ -1475,7 +1470,7 @@
         # type either the method's request (query parameters) or request body.
         if "methods" not in resourceDesc:
             return
-        for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
+        for methodName, methodDesc in resourceDesc["methods"].items():
             nextPageTokenName = _findPageTokenName(
                 _methodProperties(methodDesc, schema, "response")
             )
diff --git a/googleapiclient/http.py b/googleapiclient/http.py
index 0dd9c32..1b661e1 100644
--- a/googleapiclient/http.py
+++ b/googleapiclient/http.py
@@ -19,15 +19,13 @@
 actual HTTP request.
 """
 from __future__ import absolute_import
-import six
 
 __author__ = "jcgregorio@google.com (Joe Gregorio)"
 
-from six import BytesIO, StringIO
-from six.moves.urllib.parse import urlparse, urlunparse, quote, unquote
-
 import copy
 import httplib2
+import http.client as http_client
+import io
 import json
 import logging
 import mimetypes
@@ -35,6 +33,7 @@
 import random
 import socket
 import time
+import urllib
 import uuid
 
 # TODO(issue 221): Remove this conditional import jibbajabba.
@@ -76,11 +75,6 @@
 
 _LEGACY_BATCH_URI = "https://www.googleapis.com/batch"
 
-if six.PY2:
-    # That's a builtin python3 exception, nonexistent in python2.
-    # Defined to None to avoid NameError while trying to catch it
-    ConnectionError = None
-
 
 def _should_retry_response(resp_status, content):
     """Determines whether a response should be retried.
@@ -104,7 +98,7 @@
 
     # For 403 errors, we have to check for the `reason` in the response to
     # determine if we should retry.
-    if resp_status == six.moves.http_client.FORBIDDEN:
+    if resp_status == http_client.FORBIDDEN:
         # If there's no details about the 403 type, don't retry.
         if not content:
             return False
@@ -175,7 +169,7 @@
     resp = None
     content = None
     exception = None
-    for retry_num in six.moves.range(num_retries + 1):
+    for retry_num in range(num_retries + 1):
         if retry_num > 0:
             # Sleep before retrying.
             sleep_time = rand() * 2 ** retry_num
@@ -634,7 +628,7 @@
 class MediaInMemoryUpload(MediaIoBaseUpload):
     """MediaUpload for a chunk of bytes.
 
-    DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
+    DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or io.StringIO for
     the stream.
     """
 
@@ -648,7 +642,7 @@
     ):
         """Create a new MediaInMemoryUpload.
 
-        DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
+        DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or io.StringIO for
         the stream.
 
         Args:
@@ -660,7 +654,7 @@
           resumable: bool, True if this is a resumable upload. False means upload
             in a single request.
         """
-        fd = BytesIO(body)
+        fd = io.BytesIO(body)
         super(MediaInMemoryUpload, self).__init__(
             fd, mimetype, chunksize=chunksize, resumable=resumable
         )
@@ -710,7 +704,7 @@
         self._rand = random.random
 
         self._headers = {}
-        for k, v in six.iteritems(request.headers):
+        for k, v in request.headers.items():
             # allow users to supply custom headers by setting them on the request
             # but strip out the ones that are set by default on requests generated by
             # API methods like Drive's files().get(fileId=...)
@@ -917,8 +911,8 @@
             self.method = "POST"
             self.headers["x-http-method-override"] = "GET"
             self.headers["content-type"] = "application/x-www-form-urlencoded"
-            parsed = urlparse(self.uri)
-            self.uri = urlunparse(
+            parsed = urllib.parse.urlparse(self.uri)
+            self.uri = urllib.parse.urlunparse(
                 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None, None)
             )
             self.body = parsed.query
@@ -1077,7 +1071,7 @@
                 size,
             )
 
-        for retry_num in six.moves.range(num_retries + 1):
+        for retry_num in range(num_retries + 1):
             if retry_num > 0:
                 self._sleep(self._rand() * 2 ** retry_num)
                 LOGGER.warning(
@@ -1298,7 +1292,7 @@
         # NB: we intentionally leave whitespace between base/id and '+', so RFC2822
         # line folding works properly on Python 3; see
         # https://github.com/googleapis/google-api-python-client/issues/164
-        return "<%s + %s>" % (self._base_id, quote(id_))
+        return "<%s + %s>" % (self._base_id, urllib.parse.quote(id_))
 
     def _header_to_id(self, header):
         """Convert a Content-ID header value to an id.
@@ -1321,7 +1315,7 @@
             raise BatchError("Invalid value for Content-ID: %s" % header)
         base, id_ = header[1:-1].split(" + ", 1)
 
-        return unquote(id_)
+        return urllib.parse.unquote(id_)
 
     def _serialize_request(self, request):
         """Convert an HttpRequest object into a string.
@@ -1333,8 +1327,8 @@
           The request as a string in application/http format.
         """
         # Construct status line
-        parsed = urlparse(request.uri)
-        request_line = urlunparse(
+        parsed = urllib.parse.urlparse(request.uri)
+        request_line = urllib.parse.urlunparse(
             ("", "", parsed.path, parsed.params, parsed.query, "")
         )
         status_line = request.method + " " + request_line + " HTTP/1.1\n"
@@ -1353,7 +1347,7 @@
         if "content-type" in headers:
             del headers["content-type"]
 
-        for key, value in six.iteritems(headers):
+        for key, value in headers.items():
             msg[key] = value
         msg["Host"] = parsed.netloc
         msg.set_unixfrom(None)
@@ -1363,7 +1357,7 @@
             msg["content-length"] = str(len(request.body))
 
         # Serialize the mime message.
-        fp = StringIO()
+        fp = io.StringIO()
         # maxheaderlen=0 means don't line wrap headers.
         g = Generator(fp, maxheaderlen=0)
         g.flatten(msg, unixfrom=False)
@@ -1488,7 +1482,7 @@
 
         # encode the body: note that we can't use `as_string`, because
         # it plays games with `From ` lines.
-        fp = StringIO()
+        fp = io.StringIO()
         g = Generator(fp, mangle_from_=False)
         g.flatten(message, unixfrom=False)
         body = fp.getvalue()
@@ -1509,8 +1503,7 @@
         header = "content-type: %s\r\n\r\n" % resp["content-type"]
         # PY3's FeedParser only accepts unicode. So we should decode content
         # here, and encode each payload again.
-        if six.PY3:
-            content = content.decode("utf-8")
+        content = content.decode("utf-8")
         for_parser = header + content
 
         parser = FeedParser()
@@ -1526,7 +1519,7 @@
             request_id = self._header_to_id(part["Content-ID"])
             response, content = self._deserialize_response(part.get_payload())
             # We encode content here to emulate normal http response.
-            if isinstance(content, six.text_type):
+            if isinstance(content, str):
                 content = content.encode("utf-8")
             self._responses[request_id] = (response, content)
 
@@ -1813,7 +1806,8 @@
         # Remember the request so after the fact this mock can be examined
         self.request_sequence.append((uri, method, body, headers))
         resp, content = self._iterable.pop(0)
-        content = six.ensure_binary(content)
+        if isinstance(content, str):
+            content = content.encode("utf-8")
 
         if content == b"echo_request_headers":
             content = headers
@@ -1826,7 +1820,7 @@
                 content = body
         elif content == b"echo_request_uri":
             content = uri
-        if isinstance(content, six.text_type):
+        if isinstance(content, str):
             content = content.encode("utf-8")
         return httplib2.Response(resp), content
 
diff --git a/googleapiclient/mimeparse.py b/googleapiclient/mimeparse.py
index 6051628..a105667 100644
--- a/googleapiclient/mimeparse.py
+++ b/googleapiclient/mimeparse.py
@@ -23,7 +23,6 @@
 """
 from __future__ import absolute_import
 from functools import reduce
-import six
 
 __version__ = "0.1.3"
 __author__ = "Joe Gregorio"
@@ -105,7 +104,7 @@
                 lambda x, y: x + y,
                 [
                     1
-                    for (key, value) in six.iteritems(target_params)
+                    for (key, value) in target_params.items()
                     if key != "q" and key in params and value == params[key]
                 ],
                 0,
diff --git a/googleapiclient/model.py b/googleapiclient/model.py
index f58549c..b853a4f 100644
--- a/googleapiclient/model.py
+++ b/googleapiclient/model.py
@@ -20,7 +20,6 @@
 object representation.
 """
 from __future__ import absolute_import
-import six
 
 __author__ = "jcgregorio@google.com (Joe Gregorio)"
 
@@ -28,8 +27,7 @@
 import logging
 import platform
 import pkg_resources
-
-from six.moves.urllib.parse import urlencode
+import urllib
 
 from googleapiclient.errors import HttpError
 
@@ -112,11 +110,11 @@
         if dump_request_response:
             LOGGER.info("--request-start--")
             LOGGER.info("-headers-start-")
-            for h, v in six.iteritems(headers):
+            for h, v in headers.items():
                 LOGGER.info("%s: %s", h, v)
             LOGGER.info("-headers-end-")
             LOGGER.info("-path-parameters-start-")
-            for h, v in six.iteritems(path_params):
+            for h, v in path_params.items():
                 LOGGER.info("%s: %s", h, v)
             LOGGER.info("-path-parameters-end-")
             LOGGER.info("body: %s", body)
@@ -175,22 +173,22 @@
         if self.alt_param is not None:
             params.update({"alt": self.alt_param})
         astuples = []
-        for key, value in six.iteritems(params):
+        for key, value in params.items():
             if type(value) == type([]):
                 for x in value:
                     x = x.encode("utf-8")
                     astuples.append((key, x))
             else:
-                if isinstance(value, six.text_type) and callable(value.encode):
+                if isinstance(value, str) and callable(value.encode):
                     value = value.encode("utf-8")
                 astuples.append((key, value))
-        return "?" + urlencode(astuples)
+        return "?" + urllib.parse.urlencode(astuples)
 
     def _log_response(self, resp, content):
         """Logs debugging information about the response if requested."""
         if dump_request_response:
             LOGGER.info("--response-start--")
-            for h, v in six.iteritems(resp):
+            for h, v in resp.items():
                 LOGGER.info("%s: %s", h, v)
             if content:
                 LOGGER.info(content)
@@ -385,7 +383,7 @@
       body=makepatch(original, item)).execute()
   """
     patch = {}
-    for key, original_value in six.iteritems(original):
+    for key, original_value in original.items():
         modified_value = modified.get(key, None)
         if modified_value is None:
             # Use None to signal that the element is deleted
diff --git a/googleapiclient/schema.py b/googleapiclient/schema.py
index 00f8588..95767ef 100644
--- a/googleapiclient/schema.py
+++ b/googleapiclient/schema.py
@@ -57,7 +57,6 @@
 The constructor takes a discovery document in which to look up named schema.
 """
 from __future__ import absolute_import
-import six
 
 # TODO(jcgregorio) support format, enum, minimum, maximum
 
@@ -255,7 +254,7 @@
             if "properties" in schema:
                 properties = schema.get("properties", {})
                 sorted_properties = OrderedDict(sorted(properties.items()))
-                for pname, pschema in six.iteritems(sorted_properties):
+                for pname, pschema in sorted_properties.items():
                     self.emitBegin('"%s": ' % pname)
                     self._to_str_impl(pschema)
             elif "additionalProperties" in schema: