Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rename function option to return activity ID from uploaded activity #89

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,69 @@ generated from workouts are accepted without issues.
}
```

Using the `return_id` flag will make the code wait for Garmin to return
its internal identifier for the newly created activity, which can
be used for other methods such as renaming. The "internal ID" will
be contained within the resulting dictionary at
`result["detailedImportResult"]["successes"][0]["internalId"]`:

jat255 marked this conversation as resolved.
Show resolved Hide resolved
```python
with open("12129115726_ACTIVITY.fit", "rb") as f:
uploaded = garth.client.upload(f, return_id=True)
```

```python
{
'detailedImportResult': {
'uploadId': 212157427938,
'uploadUuid': {
'uuid': '6e56051d-1dd4-4f2c-b8ba-00a1a7d82eb3'
},
'owner': 2591602,
'fileSize': 5289,
'processingTime': 36,
'creationDate': '2023-09-29 01:58:19.113 GMT',
'ipAddress': None,
'fileName': '12129115726_ACTIVITY.fit',
'report': None,
'successes': [
{
'internalId': 17123456789,
'externalId': None,
'messages': None
}
],
'failures': []
}
}
```

## Renaming an activity

Using the "internal activity id" from above, an activity can be renamed using the
`rename` method. This snippet shows an example of uploading an activity, fetching its
jat255 marked this conversation as resolved.
Show resolved Hide resolved
id number, and then renaming it on Garmin Connect:

```python
import garth
from garth.exc import GarthException

garth.resume(".garth")
try:
garth.client.username
except GarthException:
# Session is expired. You'll need to log in again
garth.login("email", "password")
garth.save(".garth")

with open("your_fit_file.fit", "rb") as fp:
response = garth.client.upload(fp, return_id=True)

id_num = response["detailedImportResult"]["successes"][0]["internalId"]
garth.client.rename(id_num, "A better title than 'Virtual Cycling'")
```


jat255 marked this conversation as resolved.
Show resolved Hide resolved
## Stats resources

### Stress
Expand Down
86 changes: 83 additions & 3 deletions garth/http.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import base64
import json
import os
from time import sleep
jat255 marked this conversation as resolved.
Show resolved Hide resolved
from typing import IO, Any, Dict, Tuple
from urllib.parse import urljoin

Expand Down Expand Up @@ -184,18 +185,97 @@ def download(self, path: str, **kwargs) -> bytes:
return resp.content

def upload(
self, fp: IO[bytes], /, path: str = "/upload-service/upload"
self,
fp: IO[bytes],
/,
path: str = "/upload-service/upload",
return_id: bool = False,
) -> Dict[str, Any]:
"""Upload FIT file to Garmin Connect.

Args:
fp: open file pointer to FIT file
path: the API endpoint to use
return_id: Whether to return the Garmin internal activity ID
(default: ``False``). This requires polling to see if Garmin
Connect has finished processing the activity, so setting this
option to ``True`` may introduce some delay

Returns:
response dictionary with single key ``"detailedImportResult"``. If
``return_id=True``, will contain the identifier at the path
``result["detailedImportResult"]["successes"][0]["internalId"]``

Raises:
``AssertionErrror`` if the upload request returns no response
jat255 marked this conversation as resolved.
Show resolved Hide resolved
"""
fname = os.path.basename(fp.name)
files = {"file": (fname, fp)}
result = self.connectapi(
resp = self.request(
"POST",
"connectapi",
jat255 marked this conversation as resolved.
Show resolved Hide resolved
path,
method="POST",
files=files,
)
result = None if resp.status_code == 204 else resp.json()
assert result is not None, "No result from upload"

if return_id:
tries = 0
# get internal activity ID from garmin connect, try five
# times with increasing waits (it takes some time for Garmin
# connect to return an ID)
while tries < 5:
wait = (tries + 1) * 0.5
sleep(wait)
if "location" in resp.headers:
id_resp = self.request(
"GET",
"connectapi",
resp.headers["location"].replace(
"https://connectapi.garmin.com",
"",
),
api=True
)
if id_resp.status_code == 202:
continue
elif id_resp.status_code == 201:
print(id_resp.json()["detailedImportResult"])
result["detailedImportResult"]["successes"] = \
id_resp.json()["detailedImportResult"]["successes"]
break
jat255 marked this conversation as resolved.
Show resolved Hide resolved
return result

def rename(
self, activity_id: int, new_title: str
):
"""Rename an activity on Garmin Connect.

Args:
activity_id: the internal Garmin Connect activity id number
new_title: the new title to use for the activity
Raises:
``AssertionErrror`` if the rename request has an unexpected status
"""
response = self.request(
"POST",
"connect",
f"/activity-service/activity/{activity_id}",
api=True,
json = {
"activityId": activity_id,
"activityName": new_title
},
headers = {
"accept": "application/json, text/javascript, */*; q=0.01",
"di-backend": "connectapi.garmin.com",
"x-http-method-override": "PUT",
"content-type": "application/json"
}
)
assert response.status_code == 204, "Unexpected status from rename"

jat255 marked this conversation as resolved.
Show resolved Hide resolved
def dump(self, dir_path: str):
dir_path = os.path.expanduser(dir_path)
os.makedirs(dir_path, exist_ok=True)
Expand Down