text: from datetime import timedelta
  import urllib.request
  import urllib.error
  import urllib.parse
  import cloudscraper
  import re
  import sys
  import json
  import logging
  
  log = logging.getLogger('garmin')
  
  
  class LoginSucceeded(Exception):
      pass
  
  
  class LoginFailed(Exception):
      pass
  
  
  class APIException(Exception):
      pass
  
  
  class GarminConnect(object):
      LOGIN_URL = 'https://connect.garmin.com/signin'
      UPLOAD_URL = 'https://connect.garmin.com/modern/proxy/upload-service/upload/.fit'
  
      def create_opener(self, cookie):
          this = self
  
          class _HTTPRedirectHandler(urllib.request.HTTPRedirectHandler):
              def http_error_302(self, req, fp, code, msg, headers):
                  if req.get_full_url() == this.LOGIN_URL:
                      raise LoginSucceeded
  
                  return urllib.request.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
  
          return urllib.request.build_opener(_HTTPRedirectHandler, urllib.request.HTTPCookieProcessor(cookie))
  
      # From https://github.com/cpfair/tapiriik
  
      def _get_session(self, record=None, email=None, password=None):
          session = cloudscraper.CloudScraper()
  
          # JSIG CAS, cool I guess.
          # Not quite OAuth though, so I'll continue to collect raw credentials.
          # Commented stuff left in case this ever breaks because of missing parameters...
          data = {
              'username': email,
              'password': password,
              '_eventId': 'submit',
              'embed': 'true',
              # 'displayNameRequired': 'false'
          }
          params = {
              'service': 'https://connect.garmin.com/modern',
              # 'redirectAfterAccountLoginUrl': 'http://connect.garmin.com/modern',
              # 'redirectAfterAccountCreationUrl': 'http://connect.garmin.com/modern',
              # 'webhost': 'olaxpw-connect00.garmin.com',
              'clientId': 'GarminConnect',
              'gauthHost': 'https://sso.garmin.com/sso',
              # 'rememberMeShown': 'true',
              # 'rememberMeChecked': 'false',
              'consumeServiceTicket': 'false',
              # 'id': 'gauth-widget',
              # 'embedWidget': 'false',
              # 'cssUrl': 'https://static.garmincdn.com/com.garmin.connect/ui/src-css/gauth-custom.css',
              # 'source': 'http://connect.garmin.com/en-US/signin',
              # 'createAccountShown': 'true',
              # 'openCreateAccount': 'false',
              # 'usernameShown': 'true',
              # 'displayNameShown': 'false',
              # 'initialFocus': 'true',
              # 'locale': 'en'
          }
  
          headers = {
                  'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36',
                  'Referer': 'https://jhartman.pl',
                  'origin': 'https://sso.garmin.com'
              }
  
          # I may never understand what motivates people to mangle a perfectly good protocol like HTTP in the ways they do...
          preResp = session.get('https://sso.garmin.com/sso/signin', params=params, headers=headers)
          if preResp.status_code != 200:
              raise APIException('SSO prestart error %s %s' % (preResp.status_code, preResp.text))
  
          ssoResp = session.post('https://sso.garmin.com/sso/login', params=params, data=data, allow_redirects=False, headers=headers)
          
          if ssoResp.status_code != 200 or 'temporarily unavailable' in ssoResp.text:
              raise APIException('SSO error %s %s' % (ssoResp.status_code, ssoResp.text))
  
          if '>sendEvent(\'FAIL\')' in ssoResp.text:
              raise APIException('Invalid login')
          
          if '>sendEvent(\'ACCOUNT_LOCKED\')' in ssoResp.text:
              raise APIException('Account Locked')
  
          if 'renewPassword' in ssoResp.text:
              raise APIException('Reset password')
  
          # self.print_cookies(cookies=session.cookies)
  
          # ...AND WE'RE NOT DONE YET!
  
          gcRedeemResp = session.get('https://connect.garmin.com/modern',
                                     allow_redirects=False,
                                     headers=headers)
          if gcRedeemResp.status_code != 302:
              raise APIException(f'GC redeem-start error {gcRedeemResp.status_code} {gcRedeemResp.text}')
  
          url_prefix = 'https://connect.garmin.com'
  
          # There are 6 redirects that need to be followed to get the correct cookie
          # ... :(
          max_redirect_count = 7
          current_redirect_count = 1
          while True:
              url = gcRedeemResp.headers['location']
  
              # Fix up relative redirects.
              if url.startswith('/'):
                  url = url_prefix + url
              url_prefix = '/'.join(url.split('/')[:3])
              gcRedeemResp = session.get(url, allow_redirects=False)
  
              if (current_redirect_count >= max_redirect_count and
                  gcRedeemResp.status_code != 200):
                  raise APIException(f'GC redeem {current_redirect_count}/'
                                     '{max_redirect_count} error '
                                     '{gcRedeemResp.status_code} '
                                     '{gcRedeemResp.text}')
  
              if gcRedeemResp.status_code in [200, 404]:
                  break
  
              current_redirect_count += 1
              if current_redirect_count > max_redirect_count:
                  break
  
          # self.print_cookies(session.cookies)
  
          session.headers.update(headers)
  
          return session
  
      def print_cookies(self, cookies):
          log.debug('Cookies: ')
          for key, value in list(cookies.items()):
              log.debug(' %s = %s', key, value)
  
      def login(self, username, password):
  
          session = self._get_session(email=username, password=password)
  
          try:
              dashboard = session.get('http://connect.garmin.com/modern')
  
              userdata_json_str = re.search(r'VIEWER_SOCIAL_PROFILE\s*=\s*JSON\.parse\((.+)\);$', dashboard.text, re.MULTILINE).group(1)
              userdata = json.loads(json.loads(userdata_json_str))
              username = userdata['displayName']
  
              log.info('Garmin Connect User Name: %s', username)
  
          except Exception as e:
              log.error(e)
              log.error('Unable to retrieve Garmin username! Most likely: '
                        'incorrect Garmin login or password!')
  
          return session
  
      def upload_file(self, f, session):
          files = {
              'data': (
                  'withings.fit', f
              )
          }
  
          res = session.post(self.UPLOAD_URL,
                             files=files,
                             headers={'nk': 'NT'})
  
          try:
              resp = res.json()
  
              if 'detailedImportResult' not in resp:
                  raise KeyError
          except (ValueError, KeyError):
              if res.status_code == 204:   # HTTP result 204 - 'no content'
                  log.error('No data to upload, try to use --fromdate and --todate')
              else:
                  log.error('Bad response during GC upload: %s', res.status_code)
  
          return res.status_code in [200, 201, 204]
  