{"action":"created","comment":{"url":"https://api.github.com/repos/cpfair/tapiriik/pulls/comments/55254069","id":55254069,"diff_hunk":"@@ -0,0 +1,256 @@\n+from tapiriik.settings import WEB_ROOT, HEXOSKIN_CLIENT_SECRET, HEXOSKIN_CLIENT_ID\r\n+from tapiriik.services.service_base import ServiceAuthenticationType, ServiceBase\r\n+from tapiriik.services.service_record import ServiceRecord\r\n+from tapiriik.services.interchange import UploadedActivity, ActivityType, ActivityStatistic, ActivityStatisticUnit\r\n+from tapiriik.services.api import APIException, UserException, UserExceptionType, APIExcludeActivity\r\n+from tapiriik.services.tcx import TCXIO\r\n+\r\n+from django.core.urlresolvers import reverse\r\n+from datetime import datetime, timedelta\r\n+from urllib.parse import urlencode\r\n+import requests\r\n+import logging\r\n+import pytz\r\n+import time\r\n+\r\n+logger = logging.getLogger(__name__)\r\n+serverRoot = \"https://api.hexoskin.com/\"\r\n+\r\n+\r\n+class HexoskinService(ServiceBase):\r\n+    \"\"\"Define the base service object\"\"\"\r\n+    ID = \"hexoskin\"\r\n+    DisplayName = \"Hexoskin\"\r\n+    DisplayAbbreviation = \"Hx\"\r\n+    AuthenticationType = ServiceAuthenticationType.OAuth\r\n+    UserProfileURL = serverRoot + \"api/account/\"\r\n+    UserActivityURL = serverRoot + \"api/range/\"\r\n+    AuthenticationNoFrame = True  # They don't prevent the iframe, it just looks really ugly.\r\n+    LastUpload = None\r\n+\r\n+    SupportsHR = SupportsCadence = SupportsTemp = True\r\n+\r\n+    SupportsActivityDeletion = False\r\n+\r\n+    # For mapping common->Hexoskin; no ambiguity in Hexoskin activity type\r\n+    _activityTypeMappings = {\r\n+        ActivityType.Cycling: \"/api/activitytype/1/\",\r\n+        ActivityType.MountainBiking: \"/api/activitytype/1/\",\r\n+        ActivityType.Hiking: \"/api/activitytype/5/\",\r\n+        ActivityType.Running: \"/api/activitytype/6/\",\r\n+        ActivityType.Walking: \"/api/activitytype/15/\",\r\n+        ActivityType.Snowboarding: \"/api/activitytype/13/\",\r\n+        ActivityType.Skating: \"/api/activitytype/11/\",\r\n+        ActivityType.CrossCountrySkiing: \"/api/activitytype/3/\",\r\n+        ActivityType.DownhillSkiing: \"/api/activitytype/4/\",\r\n+        ActivityType.Swimming: \"/api/activitytype/14/\",\r\n+        ActivityType.Gym: \"/api/activitytype/28/\",\r\n+        ActivityType.Rowing: \"/api/activitytype/9/\",\r\n+        ActivityType.Elliptical: \"/api/activitytype/997/\",\r\n+        ActivityType.Other:\"/api/activitytype/997/\"\r\n+    }\r\n+\r\n+\r\n+    # For mapping Hexoskin->common\r\n+    def _reverseActivityTypeMappings(self, key):\r\n+        _reverseActivityTypeMappingsKeys = {\r\n+            \"/api/activitytype/1/\": ActivityType.Cycling,\r\n+            \"/api/activitytype/3/\": ActivityType.CrossCountrySkiing,\r\n+            \"/api/activitytype/4/\": ActivityType.DownhillSkiing,\r\n+            \"/api/activitytype/5/\": ActivityType.Hiking,\r\n+            \"/api/activitytype/6/\": ActivityType.Running,\r\n+            \"/api/activitytype/7/\": ActivityType.MountainBiking,\r\n+            \"/api/activitytype/9/\": ActivityType.Rowing,\r\n+            \"/api/activitytype/10/\": ActivityType.Running,\r\n+            \"/api/activitytype/11/\": ActivityType.Skating,\r\n+            \"/api/activitytype/13/\": ActivityType.Snowboarding,\r\n+            \"/api/activitytype/14/\": ActivityType.Swimming,\r\n+            \"/api/activitytype/15/\": ActivityType.Walking,\r\n+            \"/api/activitytype/24/\": ActivityType.Running,\r\n+            \"/api/activitytype/28/\": ActivityType.Gym,\r\n+        }\r\n+        if key in _reverseActivityTypeMappingsKeys.keys():\r\n+            return _reverseActivityTypeMappingsKeys[key]\r\n+        else:\r\n+            return ActivityType.Other\r\n+\r\n+    SupportedActivities = list(_activityTypeMappings.keys())\r\n+\r\n+\r\n+    def UserUploadedActivityURL(self, uploadId):\r\n+        return serverRoot + \"api/range/%d/\" % uploadId\r\n+\r\n+\r\n+    def WebInit(self):\r\n+        \"\"\"\r\n+        prepare the oauth process request. Done separately because it needs to be\r\n+        initialized on page display\r\n+        \"\"\"\r\n+        from uuid import uuid4\r\n+        params = {'scope':'readwrite',\r\n+                  'client_id':HEXOSKIN_CLIENT_ID,\r\n+                  'response_type':'code',\r\n+                  'state': str(uuid4()),\r\n+                  'redirect_uri':WEB_ROOT + reverse(\"oauth_return\", kwargs={\"service\": \"hexoskin\"})}\r\n+        self.UserAuthorizationURL = serverRoot + \"api/connect/oauth2/auth/?\" + urlencode(params)\r\n+\r\n+\r\n+    def _apiHeaders(self, serviceRecord):\r\n+        return {\"Authorization\": \"Bearer \" + serviceRecord.Authorization[\"OAuthToken\"]}\r\n+\r\n+\r\n+    def RetrieveAuthorizationToken(self, req, level):\r\n+        \"\"\"In OAuth flow, retrieve the Authorization Token\"\"\"\r\n+        code = req.GET.get(\"code\")\r\n+        params = {\"grant_type\": \"authorization_code\", \"code\": code,\"client_id\":HEXOSKIN_CLIENT_ID, \"client_secret\": HEXOSKIN_CLIENT_SECRET, \"redirect_uri\": WEB_ROOT + reverse(\"oauth_return\", kwargs={\"service\": \"hexoskin\"})}\r\n+        path = serverRoot + \"api/connect/oauth2/token/\"\r\n+        response = requests.post(path, params=params, auth=(HEXOSKIN_CLIENT_ID,HEXOSKIN_CLIENT_SECRET))\r\n+\r\n+        if response.status_code != 200:\r\n+            raise APIException(\"Invalid code\")\r\n+\r\n+        data = response.json()\r\n+        authorizationData = {\"OAuthToken\": data[\"access_token\"]}\r\n+        id_resp = requests.get(serverRoot + \"api/account/\", headers=self._apiHeaders(ServiceRecord({\"Authorization\": authorizationData})))\r\n+        return (id_resp.json()['objects'][0]['id'], authorizationData)\r\n+\r\n+\r\n+    def RevokeAuthorization(self, serviceRecord):\r\n+        \"\"\"Delete authorization token\"\"\"\r\n+        path = serverRoot + \"api/oauth2token/232/\"\r\n+        headers = self._apiHeaders(serviceRecord)\r\n+        result = requests.delete(path, headers=headers)\r\n+        if result.status_code is not 204:\r\n+            APIException(\"Revoking token was unsuccessful\")\r\n+\r\n+\r\n+    def _is_ride_valid(self, ride):\r\n+        # Sync only top-level activities, and exclude rest, rest test and sleep\r\n+        valid = (\r\n+            ride['rank'] is 0\r\n+            and ride['context']['activitytype'] is not None\r\n+            and not any(y in ride['context']['activitytype'] for y in ['/8/', '/12/', '/106/'])\r\n+        )\r\n+        return valid\r\n+\r\n+    def DownloadActivityList(self, serviceRecord, exhaustive=False):\r\n+        \"\"\"\r\n+        Get list of user's activities in Hexoskin and return it to tapiriik database\r\n+        \"\"\"\r\n+        logger.debug('Hexoskin - starting to download activity list for user %s' % serviceRecord.ExternalID)\r\n+        activities = []\r\n+        exclusions = []\r\n+        if exhaustive:\r\n+            listEnd = (datetime.now() + timedelta(days=1.5) - datetime(1970,1,1)).total_seconds()*256\r\n+            listStart = (datetime(day=21, month=8, year=1985) - datetime(1970,1,1)).total_seconds()*256  # The distant past\r\n+            resp = requests.get(serverRoot + \"api/range/?user=%s&limit=100&rank=0&start__range=%s,%s\" % (serviceRecord.ExternalID, int(listStart), int(listEnd)), headers=self._apiHeaders(serviceRecord))\r\n+        else:\r\n+            listEnd = (datetime.now() + timedelta(days=1.5) - datetime(1970,1,1)).total_seconds()*256\r\n+            listStart = (datetime.now() - timedelta(days=30) - datetime(1970,1,1)).total_seconds()*256\r\n+            resp = requests.get(serverRoot + \"api/range/?user=%s&limit=30&rank=0&start__range=%s,%s\" % (serviceRecord.ExternalID, int(listStart), int(listEnd)), headers=self._apiHeaders(serviceRecord))\r\n+        if resp.status_code == 401:\r\n+            raise APIException(\"No authorization to retrieve activity list\", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True))\r\n+        try:\r\n+            reqdata = resp.json()['objects']\r\n+        except ValueError:\r\n+            logger.debug(\"Failed parsing hexoskin list response %s - %s\" % (resp.status_code, resp.text))\r\n+            raise APIException(\"Failed parsing hexoskin list response %s - %s\" % (resp.status_code, resp.text))\r\n+        for ride in reqdata:\r\n+            try:\r\n+                if not (ride['status'] == 'complete'):\r\n+                    pass  # exclude that range for now, without excluding it in the future\r\n+                elif self._is_ride_valid(ride):\r\n+                    activity = UploadedActivity()\r\n+                    activity.StartTime = pytz.utc.localize(datetime.fromtimestamp(ride['start']/256.0))\r\n+                    activity.EndTime = pytz.utc.localize(datetime.fromtimestamp(ride['end']/256.0))\r\n+                    activity.ServiceData = {\"ActivityID\": ride[\"id\"]}\r\n+                    activity.Type = self._reverseActivityTypeMappings(ride['context']['activitytype'])\r\n+                    for metric in ride['metrics']:\r\n+                        # TODO check for IDs instead of titles\r\n+                        if metric['resource_uri'] == '/api/metric/17/':  # Cadence\r\n+                            activity.Stats.RunCadence.update(ActivityStatistic(ActivityStatisticUnit.StepsPerMinute, value=metric['value']))\r\n+                        if metric['resource_uri'] == '/api/metric/44/':  # Heart rate Average\r\n+                            activity.Stats.HR.update(ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=metric[\"value\"]))\r\n+                        if metric['resource_uri'] == '/api/metric/46/':  # Heart rate Max\r\n+                            activity.Stats.HR.update(ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, max=metric[\"value\"]))\r\n+                        if metric['resource_uri'] == '/api/metric/71/':  # Step count\r\n+                            activity.Stats.Strides.update(ActivityStatistic(ActivityStatisticUnit.Strides, value=metric[\"value\"]))\r\n+                        if metric['resource_uri'] == '/api/metric/149/':  # Energy kcal\r\n+                            activity.Stats.Energy.update(ActivityStatistic(ActivityStatisticUnit.Kilocalories, value=metric['value']))\r\n+                        if metric['resource_uri'] == '/api/metric/501/':  # Speed Max\r\n+                            activity.Stats.Speed.update(ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, max=metric[\"value\"]))\r\n+                        if metric['resource_uri'] == '/api/metric/502/':  # Speed Avg\r\n+                            activity.Stats.Speed.update(ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, avg=metric[\"value\"]))\r\n+                        if metric['resource_uri'] == '/api/metric/2038/':  # Distance\r\n+                            activity.Stats.Distance.update(ActivityStatistic(ActivityStatisticUnit.Meters, value=metric['value']))\r\n+\r\n+                    activity.Name = ride[\"name\"]\r\n+                    activity.Stationary = False\r\n+\r\n+                    ride_track = requests.get(serverRoot + \"api/track/?range=%s\" % ride['id'], headers=self._apiHeaders(serviceRecord))\r\n+                    time.sleep(0.2)\r\n+                    activity.GPS = (True if ride_track.json()['objects'] else False)\r\n+\r\n+                    activity.CalculateUID()\r\n+                    activities.append(activity)\r\n+                else:\r\n+                    exclusions.append(APIExcludeActivity(\"Unsupported activity type %s\" % ride['context']['activitytype'], activity_id=ride[\"id\"], user_exception=UserException(UserExceptionType.Other)))\r\n+            except TypeError as e:\r\n+                logger.debug(\"Failed parsing ranges url, response: %s\\n%s\" % (resp.url, resp.content))\r\n+                raise e\r\n+        logger.debug('Hexoskin - %s activities found, %s excluded. Activities' % ([x.ServiceData['ActivityID'] for x in activities], [x.ExternalActivityID for x in exclusions]))\r\n+        return activities, exclusions\r\n+\r\n+\r\n+    def DownloadActivity(self, serviceRecord, activity):\r\n+        \"\"\"Extract activity from Hexoskin\"\"\"\r\n+        activityID = activity.ServiceData[\"ActivityID\"]\r\n+        logger.debug('Hexoskin - Extracting activity %s' % activityID)\r\n+        headers = self._apiHeaders(serviceRecord)\r\n+        headers.update({\"Accept\":\"application/vnd.garmin.tcx+xml\"})\r\n+        range_tcx = requests.get(serverRoot + \"api/range/%s/\" % (str(activityID)), headers=headers)\r\n+        TCXIO.Parse(range_tcx.content, activity)\r\n+        activity.Notes = activity.Name\r\n+        activity.Name = '%s - Hx' % (activity.Type)\r\n+        return activity\r\n+\r\n+\r\n+    def UploadActivity(self, serviceRecord, activity):\r\n+        \"\"\"Import data into Hexoskin using TCX format\"\"\"\r\n+        tcx_data = TCXIO.Dump(activity)\r\n+        headers = self._apiHeaders(serviceRecord)\r\n+        headers.update({\"Content-Type\":\"application/vnd.garmin.tcx+xml\"})\r\n+\r\n+        range_tcx = requests.post(serverRoot + \"api/import/\", data=tcx_data.encode('utf-8'), headers=headers)\r\n+        import_id = str(range_tcx.json()['resource_uri'])\r\n+        # TODO In line below, fix the way the resource_uri is constructed when the /importfile/ fix is deployed\r\n+        uploaded = False\r\n+        process_start_time = datetime.now()\r\n+        while uploaded is False:\r\n+            time.sleep(1)\r\n+            range_upload_status = requests.get(serverRoot + import_id, headers=headers).json()\r\n+            if (datetime.now() - process_start_time).total_seconds() < 20:\r\n+                if range_upload_status['results'] is not None and 'error' in range_upload_status['results']:\r\n+                    err = range_upload_status['results']['error']\r\n+                    logger.debug('Hexoskin - Import range failed, see error')\r\n+                    raise APIException('Error uploading to Hexoskin: %s' % err)\r\n+                elif range_upload_status['progress'] == 1 and range_upload_status['results']:\r\n+                    for entry in range_upload_status['results']:\r\n+                        if 'resource_uri' in entry.keys() and \"range\" in entry['resource_uri'] and entry['rank'] == 0:\r\n+                            upload_id = entry['id']\r\n+                            logger.debug('Hexoskin - Imported range %s' % upload_id)\r\n+                            uploaded = True\r\n+                            break\r\n+            else:\r\n+                raise APIException('Timeout uploading activity for import id %s' % import_id)  # when the '0' bug is fixed, change this\r\n+        return upload_id\r\n+\r\n+\r\n+    def DeleteCachedData(self, serviceRecord):\r\n+        \"\"\"No cached data\"\"\"\r\n+        pass\r\n+\r\n+\r\n+    def DeleteActivity(self, serviceRecord, uploadId):\r\n+        \"\"\"We would rather have users delete data from their dashboard instead of an automatic tool\"\"\"\r","path":"tapiriik/services/Hexoskin/hexoskin.py","position":255,"original_position":255,"commit_id":"63b5147d8da5a9901a55875507ce4a6194590d4f","original_commit_id":"63b5147d8da5a9901a55875507ce4a6194590d4f","user":{"login":"antoine-gagne","id":17576679,"avatar_url":"https://avatars.githubusercontent.com/u/17576679?v=3","gravatar_id":"","url":"https://api.github.com/users/antoine-gagne","html_url":"https://github.com/antoine-gagne","followers_url":"https://api.github.com/users/antoine-gagne/followers","following_url":"https://api.github.com/users/antoine-gagne/following{/other_user}","gists_url":"https://api.github.com/users/antoine-gagne/gists{/gist_id}","starred_url":"https://api.github.com/users/antoine-gagne/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/antoine-gagne/subscriptions","organizations_url":"https://api.github.com/users/antoine-gagne/orgs","repos_url":"https://api.github.com/users/antoine-gagne/repos","events_url":"https://api.github.com/users/antoine-gagne/events{/privacy}","received_events_url":"https://api.github.com/users/antoine-gagne/received_events","type":"User","site_admin":false},"body":"The way activities are created on the server makes this a bit non-trivial, but I'll see what I can do.","created_at":"2016-03-07T19:08:15Z","updated_at":"2016-03-07T19:08:15Z","html_url":"https://github.com/cpfair/tapiriik/pull/211#discussion_r55254069","pull_request_url":"https://api.github.com/repos/cpfair/tapiriik/pulls/211","_links":{"self":{"href":"https://api.github.com/repos/cpfair/tapiriik/pulls/comments/55254069"},"html":{"href":"https://github.com/cpfair/tapiriik/pull/211#discussion_r55254069"},"pull_request":{"href":"https://api.github.com/repos/cpfair/tapiriik/pulls/211"}}},"pull_request":{"url":"https://api.github.com/repos/cpfair/tapiriik/pulls/211","id":61920993,"html_url":"https://github.com/cpfair/tapiriik/pull/211","diff_url":"https://github.com/cpfair/tapiriik/pull/211.diff","patch_url":"https://github.com/cpfair/tapiriik/pull/211.patch","issue_url":"https://api.github.com/repos/cpfair/tapiriik/issues/211","number":211,"state":"open","locked":false,"title":"Initial commit for Hexoskin","user":{"login":"antoine-gagne","id":17576679,"avatar_url":"https://avatars.githubusercontent.com/u/17576679?v=3","gravatar_id":"","url":"https://api.github.com/users/antoine-gagne","html_url":"https://github.com/antoine-gagne","followers_url":"https://api.github.com/users/antoine-gagne/followers","following_url":"https://api.github.com/users/antoine-gagne/following{/other_user}","gists_url":"https://api.github.com/users/antoine-gagne/gists{/gist_id}","starred_url":"https://api.github.com/users/antoine-gagne/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/antoine-gagne/subscriptions","organizations_url":"https://api.github.com/users/antoine-gagne/orgs","repos_url":"https://api.github.com/users/antoine-gagne/repos","events_url":"https://api.github.com/users/antoine-gagne/events{/privacy}","received_events_url":"https://api.github.com/users/antoine-gagne/received_events","type":"User","site_admin":false},"body":"I coded an integration for the Hexoskin service and wanted to offer it to the Tapiriik community. Tested it on a local server and everything should be in working condition.\r\nPlease email me for API keys if you decide to integrate the pull request.","created_at":"2016-03-07T14:05:31Z","updated_at":"2016-03-07T19:08:15Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0d6749b2d7dca6877a7cf297600db3878aac79dc","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/cpfair/tapiriik/pulls/211/commits","review_comments_url":"https://api.github.com/repos/cpfair/tapiriik/pulls/211/comments","review_comment_url":"https://api.github.com/repos/cpfair/tapiriik/pulls/comments{/number}","comments_url":"https://api.github.com/repos/cpfair/tapiriik/issues/211/comments","statuses_url":"https://api.github.com/repos/cpfair/tapiriik/statuses/63b5147d8da5a9901a55875507ce4a6194590d4f","head":{"label":"antoine-gagne:hexoskin","ref":"hexoskin","sha":"63b5147d8da5a9901a55875507ce4a6194590d4f","user":{"login":"antoine-gagne","id":17576679,"avatar_url":"https://avatars.githubusercontent.com/u/17576679?v=3","gravatar_id":"","url":"https://api.github.com/users/antoine-gagne","html_url":"https://github.com/antoine-gagne","followers_url":"https://api.github.com/users/antoine-gagne/followers","following_url":"https://api.github.com/users/antoine-gagne/following{/other_user}","gists_url":"https://api.github.com/users/antoine-gagne/gists{/gist_id}","starred_url":"https://api.github.com/users/antoine-gagne/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/antoine-gagne/subscriptions","organizations_url":"https://api.github.com/users/antoine-gagne/orgs","repos_url":"https://api.github.com/users/antoine-gagne/repos","events_url":"https://api.github.com/users/antoine-gagne/events{/privacy}","received_events_url":"https://api.github.com/users/antoine-gagne/received_events","type":"User","site_admin":false},"repo":{"id":53139565,"name":"tapiriik","full_name":"antoine-gagne/tapiriik","owner":{"login":"antoine-gagne","id":17576679,"avatar_url":"https://avatars.githubusercontent.com/u/17576679?v=3","gravatar_id":"","url":"https://api.github.com/users/antoine-gagne","html_url":"https://github.com/antoine-gagne","followers_url":"https://api.github.com/users/antoine-gagne/followers","following_url":"https://api.github.com/users/antoine-gagne/following{/other_user}","gists_url":"https://api.github.com/users/antoine-gagne/gists{/gist_id}","starred_url":"https://api.github.com/users/antoine-gagne/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/antoine-gagne/subscriptions","organizations_url":"https://api.github.com/users/antoine-gagne/orgs","repos_url":"https://api.github.com/users/antoine-gagne/repos","events_url":"https://api.github.com/users/antoine-gagne/events{/privacy}","received_events_url":"https://api.github.com/users/antoine-gagne/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/antoine-gagne/tapiriik","description":"tapiriik keeps your fitness in sync","fork":true,"url":"https://api.github.com/repos/antoine-gagne/tapiriik","forks_url":"https://api.github.com/repos/antoine-gagne/tapiriik/forks","keys_url":"https://api.github.com/repos/antoine-gagne/tapiriik/keys{/key_id}","collaborators_url":"https://api.github.com/repos/antoine-gagne/tapiriik/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/antoine-gagne/tapiriik/teams","hooks_url":"https://api.github.com/repos/antoine-gagne/tapiriik/hooks","issue_events_url":"https://api.github.com/repos/antoine-gagne/tapiriik/issues/events{/number}","events_url":"https://api.github.com/repos/antoine-gagne/tapiriik/events","assignees_url":"https://api.github.com/repos/antoine-gagne/tapiriik/assignees{/user}","branches_url":"https://api.github.com/repos/antoine-gagne/tapiriik/branches{/branch}","tags_url":"https://api.github.com/repos/antoine-gagne/tapiriik/tags","blobs_url":"https://api.github.com/repos/antoine-gagne/tapiriik/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/antoine-gagne/tapiriik/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/antoine-gagne/tapiriik/git/refs{/sha}","trees_url":"https://api.github.com/repos/antoine-gagne/tapiriik/git/trees{/sha}","statuses_url":"https://api.github.com/repos/antoine-gagne/tapiriik/statuses/{sha}","languages_url":"https://api.github.com/repos/antoine-gagne/tapiriik/languages","stargazers_url":"https://api.github.com/repos/antoine-gagne/tapiriik/stargazers","contributors_url":"https://api.github.com/repos/antoine-gagne/tapiriik/contributors","subscribers_url":"https://api.github.com/repos/antoine-gagne/tapiriik/subscribers","subscription_url":"https://api.github.com/repos/antoine-gagne/tapiriik/subscription","commits_url":"https://api.github.com/repos/antoine-gagne/tapiriik/commits{/sha}","git_commits_url":"https://api.github.com/repos/antoine-gagne/tapiriik/git/commits{/sha}","comments_url":"https://api.github.com/repos/antoine-gagne/tapiriik/comments{/number}","issue_comment_url":"https://api.github.com/repos/antoine-gagne/tapiriik/issues/comments{/number}","contents_url":"https://api.github.com/repos/antoine-gagne/tapiriik/contents/{+path}","compare_url":"https://api.github.com/repos/antoine-gagne/tapiriik/compare/{base}...{head}","merges_url":"https://api.github.com/repos/antoine-gagne/tapiriik/merges","archive_url":"https://api.github.com/repos/antoine-gagne/tapiriik/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/antoine-gagne/tapiriik/downloads","issues_url":"https://api.github.com/repos/antoine-gagne/tapiriik/issues{/number}","pulls_url":"https://api.github.com/repos/antoine-gagne/tapiriik/pulls{/number}","milestones_url":"https://api.github.com/repos/antoine-gagne/tapiriik/milestones{/number}","notifications_url":"https://api.github.com/repos/antoine-gagne/tapiriik/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/antoine-gagne/tapiriik/labels{/name}","releases_url":"https://api.github.com/repos/antoine-gagne/tapiriik/releases{/id}","deployments_url":"https://api.github.com/repos/antoine-gagne/tapiriik/deployments","created_at":"2016-03-04T14:09:36Z","updated_at":"2016-03-04T14:09:37Z","pushed_at":"2016-03-04T20:37:59Z","git_url":"git://github.com/antoine-gagne/tapiriik.git","ssh_url":"git@github.com:antoine-gagne/tapiriik.git","clone_url":"https://github.com/antoine-gagne/tapiriik.git","svn_url":"https://github.com/antoine-gagne/tapiriik","homepage":"https://tapiriik.com","size":3864,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"cpfair:master","ref":"master","sha":"f4c6236de616ba42e29d5667dbd8f422359bb199","user":{"login":"cpfair","id":271275,"avatar_url":"https://avatars.githubusercontent.com/u/271275?v=3","gravatar_id":"","url":"https://api.github.com/users/cpfair","html_url":"https://github.com/cpfair","followers_url":"https://api.github.com/users/cpfair/followers","following_url":"https://api.github.com/users/cpfair/following{/other_user}","gists_url":"https://api.github.com/users/cpfair/gists{/gist_id}","starred_url":"https://api.github.com/users/cpfair/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cpfair/subscriptions","organizations_url":"https://api.github.com/users/cpfair/orgs","repos_url":"https://api.github.com/users/cpfair/repos","events_url":"https://api.github.com/users/cpfair/events{/privacy}","received_events_url":"https://api.github.com/users/cpfair/received_events","type":"User","site_admin":false},"repo":{"id":7812229,"name":"tapiriik","full_name":"cpfair/tapiriik","owner":{"login":"cpfair","id":271275,"avatar_url":"https://avatars.githubusercontent.com/u/271275?v=3","gravatar_id":"","url":"https://api.github.com/users/cpfair","html_url":"https://github.com/cpfair","followers_url":"https://api.github.com/users/cpfair/followers","following_url":"https://api.github.com/users/cpfair/following{/other_user}","gists_url":"https://api.github.com/users/cpfair/gists{/gist_id}","starred_url":"https://api.github.com/users/cpfair/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cpfair/subscriptions","organizations_url":"https://api.github.com/users/cpfair/orgs","repos_url":"https://api.github.com/users/cpfair/repos","events_url":"https://api.github.com/users/cpfair/events{/privacy}","received_events_url":"https://api.github.com/users/cpfair/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/cpfair/tapiriik","description":"tapiriik keeps your fitness in sync","fork":false,"url":"https://api.github.com/repos/cpfair/tapiriik","forks_url":"https://api.github.com/repos/cpfair/tapiriik/forks","keys_url":"https://api.github.com/repos/cpfair/tapiriik/keys{/key_id}","collaborators_url":"https://api.github.com/repos/cpfair/tapiriik/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/cpfair/tapiriik/teams","hooks_url":"https://api.github.com/repos/cpfair/tapiriik/hooks","issue_events_url":"https://api.github.com/repos/cpfair/tapiriik/issues/events{/number}","events_url":"https://api.github.com/repos/cpfair/tapiriik/events","assignees_url":"https://api.github.com/repos/cpfair/tapiriik/assignees{/user}","branches_url":"https://api.github.com/repos/cpfair/tapiriik/branches{/branch}","tags_url":"https://api.github.com/repos/cpfair/tapiriik/tags","blobs_url":"https://api.github.com/repos/cpfair/tapiriik/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/cpfair/tapiriik/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/cpfair/tapiriik/git/refs{/sha}","trees_url":"https://api.github.com/repos/cpfair/tapiriik/git/trees{/sha}","statuses_url":"https://api.github.com/repos/cpfair/tapiriik/statuses/{sha}","languages_url":"https://api.github.com/repos/cpfair/tapiriik/languages","stargazers_url":"https://api.github.com/repos/cpfair/tapiriik/stargazers","contributors_url":"https://api.github.com/repos/cpfair/tapiriik/contributors","subscribers_url":"https://api.github.com/repos/cpfair/tapiriik/subscribers","subscription_url":"https://api.github.com/repos/cpfair/tapiriik/subscription","commits_url":"https://api.github.com/repos/cpfair/tapiriik/commits{/sha}","git_commits_url":"https://api.github.com/repos/cpfair/tapiriik/git/commits{/sha}","comments_url":"https://api.github.com/repos/cpfair/tapiriik/comments{/number}","issue_comment_url":"https://api.github.com/repos/cpfair/tapiriik/issues/comments{/number}","contents_url":"https://api.github.com/repos/cpfair/tapiriik/contents/{+path}","compare_url":"https://api.github.com/repos/cpfair/tapiriik/compare/{base}...{head}","merges_url":"https://api.github.com/repos/cpfair/tapiriik/merges","archive_url":"https://api.github.com/repos/cpfair/tapiriik/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/cpfair/tapiriik/downloads","issues_url":"https://api.github.com/repos/cpfair/tapiriik/issues{/number}","pulls_url":"https://api.github.com/repos/cpfair/tapiriik/pulls{/number}","milestones_url":"https://api.github.com/repos/cpfair/tapiriik/milestones{/number}","notifications_url":"https://api.github.com/repos/cpfair/tapiriik/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/cpfair/tapiriik/labels{/name}","releases_url":"https://api.github.com/repos/cpfair/tapiriik/releases{/id}","deployments_url":"https://api.github.com/repos/cpfair/tapiriik/deployments","created_at":"2013-01-25T02:43:42Z","updated_at":"2016-03-03T09:48:59Z","pushed_at":"2016-03-07T14:05:31Z","git_url":"git://github.com/cpfair/tapiriik.git","ssh_url":"git@github.com:cpfair/tapiriik.git","clone_url":"https://github.com/cpfair/tapiriik.git","svn_url":"https://github.com/cpfair/tapiriik","homepage":"https://tapiriik.com","size":4093,"stargazers_count":706,"watchers_count":706,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":146,"mirror_url":null,"open_issues_count":109,"forks":146,"open_issues":109,"watchers":706,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/cpfair/tapiriik/pulls/211"},"html":{"href":"https://github.com/cpfair/tapiriik/pull/211"},"issue":{"href":"https://api.github.com/repos/cpfair/tapiriik/issues/211"},"comments":{"href":"https://api.github.com/repos/cpfair/tapiriik/issues/211/comments"},"review_comments":{"href":"https://api.github.com/repos/cpfair/tapiriik/pulls/211/comments"},"review_comment":{"href":"https://api.github.com/repos/cpfair/tapiriik/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/cpfair/tapiriik/pulls/211/commits"},"statuses":{"href":"https://api.github.com/repos/cpfair/tapiriik/statuses/63b5147d8da5a9901a55875507ce4a6194590d4f"}}}}