cyberjunky / python-garminconnect

Python 3 API wrapper for Garmin Connect to get activity statistics
MIT License
965 stars 151 forks source link

Uploading Fit Activity file. #140

Closed jbspillman closed 1 year ago

jbspillman commented 1 year ago

I get the following error when trying to upload fit files from Garmin or Wahoo. Tried with both types and it is rather interesting that I get two different errors. I can GET most information fine. fit file from garmin 11396743974_ACTIVITY.fit

DEBUG:urllib3.connectionpool:https://connect.garmin.com:443 "GET /modern/ HTTP/1.1" 200 None
DEBUG:garminconnect:Session response 200
DEBUG:garminconnect:Display name is MYNAMEIS
DEBUG:garminconnect:Unit system is statute_us
DEBUG:garminconnect:Fullname is MYNAMEIS
DEBUG:garminconnect:URL: https://connect.garmin.com/modern/proxy/upload-service/upload
DEBUG:garminconnect:Headers: {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0', 'NK': 'NT'}
DEBUG:garminconnect:Data: None
-------------------- api.get_full_name() --------------------
"MYNAMEIS"
-------------------------------------------------------------
DEBUG:urllib3.connectionpool:https://connect.garmin.com:443 "POST /modern/proxy/upload-service/upload HTTP/1.1" 403 None
Traceback (most recent call last):
  File "XXXXXX\Python310\site-packages\garminconnect\__init__.py", line 103, in post
    response.raise_for_status()
  File "XXXXXX\Python310\site-packages\requests\models.py", line 1021, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: https://connect.garmin.com/modern/proxy/upload-service/upload

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "YYYYYYYYYYY\garmin_example.py", line 471, in <module>
    display_json(f"api.upload_activity({activityfile})", api.upload_activity(activityfile))
  File "XXXXXX\Python310\site-packages\garminconnect\__init__.py", line 740, in upload_activity
    return self.modern_rest_client.post(url, files=files).json()
  File "XXXXXX\Python310\site-packages\garminconnect\__init__.py", line 112, in post
    raise GarminConnectConnectionError(f"403 Forbidden error: {url}") from err
garminconnect.GarminConnectConnectionError: 403 Forbidden error: https://connect.garmin.com/modern/proxy/upload-service/upload

fit file from wahoo 2023-07-09-155504-ELEMNT_ROAM_46E1-102-0.fit

DEBUG:urllib3.connectionpool:https://connect.garmin.com:443 "GET /modern/?ticket=adsadasdasdasd HTTP/1.1" 302 0
-------------------- api.get_full_name() --------------------
"MYNAMEIS"
-------------------------------------------------------------
DEBUG:urllib3.connectionpool:https://connect.garmin.com:443 "GET /modern/ HTTP/1.1" 200 None
DEBUG:garminconnect:Session response 200
DEBUG:garminconnect:Display name is MYNAMEIS
DEBUG:garminconnect:Unit system is statute_us
DEBUG:garminconnect:Fullname is MYNAMEIS
DEBUG:garminconnect:URL: https://connect.garmin.com/modern/proxy/upload-service/upload
DEBUG:garminconnect:Headers: {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0', 'NK': 'NT'}
DEBUG:garminconnect:Data: None
DEBUG:urllib3.connectionpool:https://connect.garmin.com:443 "POST /modern/proxy/upload-service/upload HTTP/1.1" 409 386
Traceback (most recent call last):
  File "YYYYYYYYYYY\garmin_example.py", line 473, in <module>
    display_json(f"api.upload_activity({activityfile})", api.upload_activity(activityfile))
  File "XXXXXX\Python310\site-packages\garminconnect\__init__.py", line 740, in upload_activity
    return self.modern_rest_client.post(url, files=files).json()
AttributeError: 'NoneType' object has no attribute 'json'
jingwei1982 commented 1 year ago

For the 2nd issue, if you have uploaded the fit file, when you upload it again, the repsonse will be None, so will cause the attributeError.

cyberjunky commented 1 year ago

@jbspillman can you try this with latest version and let me know how that works?

jbspillman commented 1 year ago

So I did a test ride and ran my Wahoo Element and Wahoo Roam at same time. Same errors.

##########################################################################
File from Wahoo Element:
python3 detect_enc.py
file: 2023-09-16-234904-ELEMNT 7D7D-88-0.fit                       Encoding:                      Windows-1254

Executing: Upload activity data from file ''

DEBUG:urllib3.connectionpool:https://connectapi.garmin.com:443 "POST /upload-service/upload HTTP/1.1" 202 320
-------------------- api.upload_activity(/home/spillman/Dropbox/Apps/WahooFitness/2023-09-16-234904-ELEMNT 7D7D-88-0.fit) --------------------
Traceback (most recent call last):
  File "/mnt/Drives/01000/Tools/garmin_connect/example.py", line 667, in <module>
    switch(api, option)
  File "/mnt/Drives/01000/Tools/garmin_connect/example.py", line 472, in switch
    display_json(
  File "/mnt/Drives/01000/Tools/garmin_connect/example.py", line 108, in display_json
    print(json.dumps(output, indent=4))
  File "/usr/lib/python3.10/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/usr/lib/python3.10/json/encoder.py", line 201, in encode
    chunks = list(chunks)
  File "/usr/lib/python3.10/json/encoder.py", line 438, in _iterencode
    o = _default(o)
  File "/usr/lib/python3.10/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type Response is not JSON serializable

##########################################################################

##########################################################################
File from Wahoo Roam:
python3 detect_enc.py
file: 2023-09-16-234901-ELEMNT ROAM 46E1-118-0.fit                 Encoding:                      Windows-1254

Executing: Upload activity data from file ''

DEBUG:urllib3.connectionpool:https://connectapi.garmin.com:443 "POST /upload-service/upload HTTP/1.1" 202 327
-------------------- api.upload_activity(/home/spillman/Dropbox/Apps/WahooFitness/2023-09-16-234901-ELEMNT ROAM 46E1-118-0.fit) --------------------
Traceback (most recent call last):
  File "/mnt/Drives/01000/Tools/garmin_connect/example.py", line 668, in <module>
    switch(api, option)
  File "/mnt/Drives/01000/Tools/garmin_connect/example.py", line 472, in switch
    display_json(
  File "/mnt/Drives/01000/Tools/garmin_connect/example.py", line 108, in display_json
    print(json.dumps(output, indent=4))
  File "/usr/lib/python3.10/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/usr/lib/python3.10/json/encoder.py", line 201, in encode
    chunks = list(chunks)
  File "/usr/lib/python3.10/json/encoder.py", line 438, in _iterencode
    o = _default(o)
  File "/usr/lib/python3.10/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type Response is not JSON serializable

However. both activities now show in my Garmin Activities... Which I guess makes sense as the api code is 202. TLDR : Looks like the data being returned to display_json() is not working when it trying to use json.dumps()

def display_json(api_call, output):
    """Format API output for better readability."""

    dashed = "-" * 20
    header = f"{dashed} {api_call} {dashed}"
    footer = "-" * len(header)

    print(header)
    # print(json.dumps(output, indent=4))
    print(output)
    print(footer)
cyberjunky commented 1 year ago

@jbspillman Can you try to change that routine like this, to so see if result is acceptable? Otherwise we can test for and parse Reponse object, and get better output.

def display_json(api_call, output):
    """Format API output for better readability."""

    dashed = "-" * 20
    header = f"{dashed} {api_call} {dashed}"
    footer = "-" * len(header)

    print(header)

    if isinstance(output, (int, str, dict, list)):
        print(json.dumps(output, indent=4))
    else:
        print(output)

    print(footer)
jbspillman commented 1 year ago

@jbspillman Can you try to change that routine like this, to so see if result is acceptable? Otherwise we can test for and parse Reponse object, and get better output.

def display_json(api_call, output):
    """Format API output for better readability."""

    dashed = "-" * 20
    header = f"{dashed} {api_call} {dashed}"
    footer = "-" * len(header)

    print(header)

    if isinstance(output, (int, str, dict, list)):
        print(json.dumps(output, indent=4))
    else:
        print(output)

    print(footer)

This is what I ended up doing... I am sure that your enhancement would work though now that I looked into the error message. My code is running in automated fashion, checking periodically in my Dropbox folder for a new fit file from my Wahoo.

If found, it upoads the activity_file with the switch(api, "s") function. Then I noticed if the activity is already uploaded it will throw a 409 error.

def display_json(api_call, output):
    """Format API output for better readability."""

    dashed = "-" * 20
    header = f"{dashed} {api_call} {dashed}"
    footer = "-" * len(header)

    if "local_api.upload_activity" in api_call:
        if "409" in str(output):
            output = {"status": 409, "Message": "File Exists", "File": activity_file}
            output = json.dumps(output, indent=4)
            print(header)
            print(output)
            print(footer)
            return True
    else:
        try:
            output = json.dumps(output, indent=4)
        except TypeError:
            output = output.json()
            output = json.dumps(output, indent=4)

        print(header)
        print(output)
        print(footer)

        if "local_api.upload_activity" in api_call:
            if "creationDate" in output:
                return True
            else:
                return False

I also modified /garth/http.py

        try:
            self.last_resp.raise_for_status()
        except HTTPError as e:
            if "upload" in url:
                error = str(e)
                if "409" in error:
                    #print("ERROR:", "File already Uploaded")
                    return 409, "file exists"
            else:
                raise GarthHTTPError(
                    msg="Error in request",
                    error=e,
                )
        return self.last_resp
matin commented 1 year ago

Fixed in #161