{"action":"created","comment":{"url":"https://api.github.com/repos/closeio/tasktiger/pulls/comments/53690901","id":53690901,"diff_hunk":"@@ -1,30 +1,289 @@\n+import calendar\n import datetime\n import json\n-from ._internal import ERROR, QUEUED\n+import redis\n+import time\n \n-# This module contains helper methods to inspect and requeue tasks from queues.\n-# TODO: Use task objects wherever we can.\n+from ._internal import *\n+from .exceptions import TaskNotFound\n+\n+__all__ = ['Task']\n \n class Task(object):\n-    def __init__(self, tiger, queue, state, _data, _ts=None, _executions=None):\n+    def __init__(self, tiger, func=None, args=None, kwargs=None, queue=None,\n+                 hard_timeout=None, unique=None, lock=None, lock_key=None,\n+                 retry=None, retry_on=None, retry_method=None,\n+                 _data=None, _state=None, _ts=None, _executions=None):\n         \"\"\"\n-        Initializes a Task from the internal structure. Use tasks_from_queue to\n-        retrieve tasks from a given queue. Don't call this directly.\n+        Queues a task. See README.rst for an explanation of the options.\n         \"\"\"\n+\n+        if func and queue is None:\n+            queue = getattr(func, '_task_queue', tiger.config['DEFAULT_QUEUE'])\n+\n         self.tiger = tiger\n-        self.queue = queue\n-        self.state = state\n-        self.data = json.loads(_data)\n-        if _executions:\n-            self.executions = [json.loads(e) for e in _executions if e]\n+        self._func = func\n+        self._queue = queue\n+        self._state = _state\n+        self._ts = _ts\n+        self._executions = _executions or []\n+\n+        # Internal initialization based on raw data.\n+        if _data is not None:\n+            self._data = _data\n+            return\n+\n+        assert func\n+\n+        serialized_name = serialize_func_name(func)\n+\n+        if unique is None:\n+            unique = getattr(func, '_task_unique', False)\n+\n+        if lock is None:\n+            lock = getattr(func, '_task_lock', False)\n+\n+        if lock_key is None:\n+            lock_key = getattr(func, '_task_lock_key', None)\n+\n+        if retry is None:\n+            retry = getattr(func, '_task_retry', False)\n+\n+        if retry_on is None:\n+            retry_on = getattr(func, '_task_retry_on', None)\n+\n+        if retry_method is None:\n+            retry_method = getattr(func, '_task_retry_method', None)\n+\n+        if unique:\n+            task_id = gen_unique_id(serialized_name, args, kwargs)\n         else:\n-            self.executions = []\n-        self.id = self.data['id']\n-        self.ts = _ts\n-        self.func = self.data['func']\n-        self.args = self.data.get('args', [])\n-        self.kwargs = self.data.get('kwargs', {})\n-        self.unique = self.data.get('unique', False)\n+            task_id = gen_id()\n+\n+        task = {\n+            'id': task_id,\n+            'func': serialized_name,\n+        }\n+        if unique:\n+            task['unique'] = True\n+        if lock or lock_key:\n+            task['lock'] = True\n+            if lock_key:\n+                task['lock_key'] = lock_key\n+        if args:\n+            task['args'] = args\n+        if kwargs:\n+            task['kwargs'] = kwargs\n+        if hard_timeout:\n+            task['hard_timeout'] = hard_timeout\n+        if retry or retry_on or retry_method:\n+            if not retry_method:\n+                retry_method = tiger.config['DEFAULT_RETRY_METHOD']\n+\n+            retry_method = serialize_retry_method(retry_method)\n+\n+            task['retry_method'] = retry_method\n+            if retry_on:\n+                task['retry_on'] = [serialize_func_name(cls)\n+                                    for cls in retry_on]\n+\n+        self._data = task\n+\n+    @property\n+    def id(self):\n+        return self._data['id']\n+\n+    @property\n+    def data(self):\n+        return self._data\n+\n+    @property\n+    def state(self):\n+        return self._state\n+\n+    @property\n+    def queue(self):\n+        return self._queue\n+\n+    @property\n+    def serialized_func(self):\n+        return self._data['func']\n+\n+    @property\n+    def lock(self):\n+        return self._data.get('lock', False)\n+\n+    @property\n+    def lock_key(self):\n+        return self._data.get('lock_key')\n+\n+    @property\n+    def args(self):\n+        return self._data.get('args', [])\n+\n+    @property\n+    def kwargs(self):\n+        return self._data.get('kwargs', {})\n+\n+    @property\n+    def hard_timeout(self):\n+        return self._data.get('hard_timeout', None)\n+\n+    @property\n+    def unique(self):\n+        return self._data.get('unique', False)\n+\n+    @property\n+    def retry_method(self):\n+        if 'retry_method' in self._data:\n+            retry_func, retry_args = self._data['retry_method']\n+            return retry_func, retry_args\n+        else:\n+            return None\n+\n+    @property\n+    def retry_on(self):\n+        return self._data.get('retry_on')\n+\n+    @property\n+    def func(self):\n+        if not self._func:\n+            self._func = import_attribute(self.serialized_func)\n+        return self._func\n+\n+    @property\n+    def ts(self):\n+        \"\"\"\n+        The timestamp (datetime) of the task in the queue, or None, if the task\n+        hasn't been queued.\n+        \"\"\"\n+        return self._ts\n+\n+    @property\n+    def executions(self):\n+        return self._executions\n+\n+    def _move(self, from_state=None, to_state=None, when=None, mode=None):\n+        \"\"\"\n+        Internal helper to move a task from one state another (e.g. from QUEUED\n+        to DELAYED). The \"when\" argument indicates the timestamp of the task in\n+        the new state. If no to_state is specified, the task will be simply\n+        removed from the original state.\n+\n+        The \"mode\" param can be specified to define how the timestamp in the\n+        new state should be updated and is passed to the ZADD Redis script (see\n+        its documentation for details).\n+\n+        Raises TaskNotFound if the task is not in the expected state or not in\n+        the expected queue.\n+        \"\"\"\n+\n+        pipeline = self.tiger.connection.pipeline()\n+        scripts = self.tiger.scripts\n+        _key = self.tiger._key\n+\n+        from_state = from_state or self.state\n+        queue = self.queue\n+\n+        assert from_state\n+        assert queue\n+\n+        scripts.fail_if_not_in_zset(_key(from_state, queue), self.id,\n+                                    client=pipeline)\n+        if to_state:\n+            if not when:\n+                when = time.time()\n+            if mode:\n+                scripts.zadd(_key(to_state, queue), when, self.id,\n+                                  mode, client=pipeline)\n+            else:\n+                pipeline.zadd(_key(to_state, queue), self.id, when)\n+            pipeline.sadd(_key(to_state), queue)\n+        pipeline.zrem(_key(from_state, queue), self.id)\n+\n+        if not to_state: # Remove the task if necessary\n+            if self.unique:\n+                # Only delete if it's not in any other queue\n+                check_states = set([ACTIVE, QUEUED, ERROR, SCHEDULED])\n+                check_states.remove(from_state)\n+                # TODO: Do the following two in one call.\n+                scripts.delete_if_not_in_zsets(_key('task', self.id, 'executions'),\n+                                                    self.id, [\n+                    _key(state, queue) for state in check_states\n+                ], client=pipeline)\n+                scripts.delete_if_not_in_zsets(_key('task', self.id),\n+                                                    self.id, [\n+                    _key(state, queue) for state in check_states\n+                ], client=pipeline)\n+            else:\n+                # Safe to remove\n+                pipeline.delete(_key('task', self.id, 'executions'))\n+                pipeline.delete(_key('task', self.id))\n+\n+        scripts.srem_if_not_exists(_key(from_state), queue,\n+                _key(from_state, queue), client=pipeline)\n+\n+        if to_state == QUEUED:\n+            pipeline.publish(_key('activity'), queue)\n+\n+        try:\n+            pipeline.execute()\n+        except redis.ResponseError as e:\n+            if '<FAIL_IF_NOT_IN_ZSET>' in unicode(e):\n+                raise TaskNotFound('Task {} not found in queue \"{}\" in state \"{}\".'.format(\n+                    self.id, queue, from_state\n+                ))\n+            raise\n+        else:\n+            self._state = to_state\n+\n+    def execute(self):\n+        func = self.func\n+        if getattr(func, '_task_batch', False):\n+            return func([{'args': self.args, 'kwargs': self.kwargs}])\n+        else:\n+            return func(*self.args, **self.kwargs)\n+\n+    def delay(self, when=None):\n+        tiger = self.tiger\n+\n+        now = time.time()\n+        self._data['time_last_queued'] = now\n+        serialized_task = json.dumps(self._data)\n+\n+        # convert timedelta to datetime\n+        if isinstance(when, datetime.timedelta):\n+            when = datetime.datetime.utcnow() + when\n+\n+        # convert to unixtime\n+        if isinstance(when, datetime.datetime):","path":"tasktiger/task.py","position":277,"original_position":277,"commit_id":"661e67efe93202a47958e13e9acf536da17b9f14","original_commit_id":"661e67efe93202a47958e13e9acf536da17b9f14","user":{"login":"wojcikstefan","id":1718372,"avatar_url":"https://avatars.githubusercontent.com/u/1718372?v=3","gravatar_id":"","url":"https://api.github.com/users/wojcikstefan","html_url":"https://github.com/wojcikstefan","followers_url":"https://api.github.com/users/wojcikstefan/followers","following_url":"https://api.github.com/users/wojcikstefan/following{/other_user}","gists_url":"https://api.github.com/users/wojcikstefan/gists{/gist_id}","starred_url":"https://api.github.com/users/wojcikstefan/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wojcikstefan/subscriptions","organizations_url":"https://api.github.com/users/wojcikstefan/orgs","repos_url":"https://api.github.com/users/wojcikstefan/repos","events_url":"https://api.github.com/users/wojcikstefan/events{/privacy}","received_events_url":"https://api.github.com/users/wojcikstefan/received_events","type":"User","site_admin":false},"body":"Is there a scenario in which `when` is not `datetime.datetime` by now AND is a valid input? If not, maybe we should assert that `isinstance(when, (datetime.datetime, datetime.timedelta))` in the beginning of this method?","created_at":"2016-02-22T20:53:35Z","updated_at":"2016-02-22T20:53:35Z","html_url":"https://github.com/closeio/tasktiger/pull/12#discussion_r53690901","pull_request_url":"https://api.github.com/repos/closeio/tasktiger/pulls/12","_links":{"self":{"href":"https://api.github.com/repos/closeio/tasktiger/pulls/comments/53690901"},"html":{"href":"https://github.com/closeio/tasktiger/pull/12#discussion_r53690901"},"pull_request":{"href":"https://api.github.com/repos/closeio/tasktiger/pulls/12"}}},"pull_request":{"url":"https://api.github.com/repos/closeio/tasktiger/pulls/12","id":59717673,"html_url":"https://github.com/closeio/tasktiger/pull/12","diff_url":"https://github.com/closeio/tasktiger/pull/12.diff","patch_url":"https://github.com/closeio/tasktiger/pull/12.patch","issue_url":"https://api.github.com/repos/closeio/tasktiger/issues/12","number":12,"state":"open","locked":false,"title":"Task refactor","user":{"login":"thomasst","id":68381,"avatar_url":"https://avatars.githubusercontent.com/u/68381?v=3","gravatar_id":"","url":"https://api.github.com/users/thomasst","html_url":"https://github.com/thomasst","followers_url":"https://api.github.com/users/thomasst/followers","following_url":"https://api.github.com/users/thomasst/following{/other_user}","gists_url":"https://api.github.com/users/thomasst/gists{/gist_id}","starred_url":"https://api.github.com/users/thomasst/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thomasst/subscriptions","organizations_url":"https://api.github.com/users/thomasst/orgs","repos_url":"https://api.github.com/users/thomasst/repos","events_url":"https://api.github.com/users/thomasst/events{/privacy}","received_events_url":"https://api.github.com/users/thomasst/received_events","type":"User","site_admin":false},"body":"Refactored the code to make use of the Task class, which now can be used to queue new tasks as well as cancel/retry/inspect them, and is used internally as well.\r\n\r\nAdded a TaskNotFound exception which is raised when a task cannot be found or does not exist in the expected queue, so we know when e.g. canceling a scheduled task failed.\r\n\r\nThis also fixes #11.\r\n\r\n@closeio/engineering review?","created_at":"2016-02-17T22:13:50Z","updated_at":"2016-02-22T20:53:35Z","closed_at":null,"merged_at":null,"merge_commit_sha":"f231fd6def106f23d51b5e47806a06dc0791ef7a","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/closeio/tasktiger/pulls/12/commits","review_comments_url":"https://api.github.com/repos/closeio/tasktiger/pulls/12/comments","review_comment_url":"https://api.github.com/repos/closeio/tasktiger/pulls/comments{/number}","comments_url":"https://api.github.com/repos/closeio/tasktiger/issues/12/comments","statuses_url":"https://api.github.com/repos/closeio/tasktiger/statuses/661e67efe93202a47958e13e9acf536da17b9f14","head":{"label":"closeio:task-refactor","ref":"task-refactor","sha":"661e67efe93202a47958e13e9acf536da17b9f14","user":{"login":"closeio","id":437267,"avatar_url":"https://avatars.githubusercontent.com/u/437267?v=3","gravatar_id":"","url":"https://api.github.com/users/closeio","html_url":"https://github.com/closeio","followers_url":"https://api.github.com/users/closeio/followers","following_url":"https://api.github.com/users/closeio/following{/other_user}","gists_url":"https://api.github.com/users/closeio/gists{/gist_id}","starred_url":"https://api.github.com/users/closeio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/closeio/subscriptions","organizations_url":"https://api.github.com/users/closeio/orgs","repos_url":"https://api.github.com/users/closeio/repos","events_url":"https://api.github.com/users/closeio/events{/privacy}","received_events_url":"https://api.github.com/users/closeio/received_events","type":"Organization","site_admin":false},"repo":{"id":35581334,"name":"tasktiger","full_name":"closeio/tasktiger","owner":{"login":"closeio","id":437267,"avatar_url":"https://avatars.githubusercontent.com/u/437267?v=3","gravatar_id":"","url":"https://api.github.com/users/closeio","html_url":"https://github.com/closeio","followers_url":"https://api.github.com/users/closeio/followers","following_url":"https://api.github.com/users/closeio/following{/other_user}","gists_url":"https://api.github.com/users/closeio/gists{/gist_id}","starred_url":"https://api.github.com/users/closeio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/closeio/subscriptions","organizations_url":"https://api.github.com/users/closeio/orgs","repos_url":"https://api.github.com/users/closeio/repos","events_url":"https://api.github.com/users/closeio/events{/privacy}","received_events_url":"https://api.github.com/users/closeio/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/closeio/tasktiger","description":"Python task queue. Because celery is gross.","fork":false,"url":"https://api.github.com/repos/closeio/tasktiger","forks_url":"https://api.github.com/repos/closeio/tasktiger/forks","keys_url":"https://api.github.com/repos/closeio/tasktiger/keys{/key_id}","collaborators_url":"https://api.github.com/repos/closeio/tasktiger/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/closeio/tasktiger/teams","hooks_url":"https://api.github.com/repos/closeio/tasktiger/hooks","issue_events_url":"https://api.github.com/repos/closeio/tasktiger/issues/events{/number}","events_url":"https://api.github.com/repos/closeio/tasktiger/events","assignees_url":"https://api.github.com/repos/closeio/tasktiger/assignees{/user}","branches_url":"https://api.github.com/repos/closeio/tasktiger/branches{/branch}","tags_url":"https://api.github.com/repos/closeio/tasktiger/tags","blobs_url":"https://api.github.com/repos/closeio/tasktiger/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/closeio/tasktiger/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/closeio/tasktiger/git/refs{/sha}","trees_url":"https://api.github.com/repos/closeio/tasktiger/git/trees{/sha}","statuses_url":"https://api.github.com/repos/closeio/tasktiger/statuses/{sha}","languages_url":"https://api.github.com/repos/closeio/tasktiger/languages","stargazers_url":"https://api.github.com/repos/closeio/tasktiger/stargazers","contributors_url":"https://api.github.com/repos/closeio/tasktiger/contributors","subscribers_url":"https://api.github.com/repos/closeio/tasktiger/subscribers","subscription_url":"https://api.github.com/repos/closeio/tasktiger/subscription","commits_url":"https://api.github.com/repos/closeio/tasktiger/commits{/sha}","git_commits_url":"https://api.github.com/repos/closeio/tasktiger/git/commits{/sha}","comments_url":"https://api.github.com/repos/closeio/tasktiger/comments{/number}","issue_comment_url":"https://api.github.com/repos/closeio/tasktiger/issues/comments{/number}","contents_url":"https://api.github.com/repos/closeio/tasktiger/contents/{+path}","compare_url":"https://api.github.com/repos/closeio/tasktiger/compare/{base}...{head}","merges_url":"https://api.github.com/repos/closeio/tasktiger/merges","archive_url":"https://api.github.com/repos/closeio/tasktiger/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/closeio/tasktiger/downloads","issues_url":"https://api.github.com/repos/closeio/tasktiger/issues{/number}","pulls_url":"https://api.github.com/repos/closeio/tasktiger/pulls{/number}","milestones_url":"https://api.github.com/repos/closeio/tasktiger/milestones{/number}","notifications_url":"https://api.github.com/repos/closeio/tasktiger/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/closeio/tasktiger/labels{/name}","releases_url":"https://api.github.com/repos/closeio/tasktiger/releases{/id}","deployments_url":"https://api.github.com/repos/closeio/tasktiger/deployments","created_at":"2015-05-14T00:26:32Z","updated_at":"2016-02-18T22:40:02Z","pushed_at":"2016-02-22T20:18:29Z","git_url":"git://github.com/closeio/tasktiger.git","ssh_url":"git@github.com:closeio/tasktiger.git","clone_url":"https://github.com/closeio/tasktiger.git","svn_url":"https://github.com/closeio/tasktiger","homepage":"","size":125,"stargazers_count":261,"watchers_count":261,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":10,"mirror_url":null,"open_issues_count":2,"forks":10,"open_issues":2,"watchers":261,"default_branch":"master"}},"base":{"label":"closeio:master","ref":"master","sha":"31564d12845a47c5f41e0fa087828b83e2a0b742","user":{"login":"closeio","id":437267,"avatar_url":"https://avatars.githubusercontent.com/u/437267?v=3","gravatar_id":"","url":"https://api.github.com/users/closeio","html_url":"https://github.com/closeio","followers_url":"https://api.github.com/users/closeio/followers","following_url":"https://api.github.com/users/closeio/following{/other_user}","gists_url":"https://api.github.com/users/closeio/gists{/gist_id}","starred_url":"https://api.github.com/users/closeio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/closeio/subscriptions","organizations_url":"https://api.github.com/users/closeio/orgs","repos_url":"https://api.github.com/users/closeio/repos","events_url":"https://api.github.com/users/closeio/events{/privacy}","received_events_url":"https://api.github.com/users/closeio/received_events","type":"Organization","site_admin":false},"repo":{"id":35581334,"name":"tasktiger","full_name":"closeio/tasktiger","owner":{"login":"closeio","id":437267,"avatar_url":"https://avatars.githubusercontent.com/u/437267?v=3","gravatar_id":"","url":"https://api.github.com/users/closeio","html_url":"https://github.com/closeio","followers_url":"https://api.github.com/users/closeio/followers","following_url":"https://api.github.com/users/closeio/following{/other_user}","gists_url":"https://api.github.com/users/closeio/gists{/gist_id}","starred_url":"https://api.github.com/users/closeio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/closeio/subscriptions","organizations_url":"https://api.github.com/users/closeio/orgs","repos_url":"https://api.github.com/users/closeio/repos","events_url":"https://api.github.com/users/closeio/events{/privacy}","received_events_url":"https://api.github.com/users/closeio/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/closeio/tasktiger","description":"Python task queue. Because celery is gross.","fork":false,"url":"https://api.github.com/repos/closeio/tasktiger","forks_url":"https://api.github.com/repos/closeio/tasktiger/forks","keys_url":"https://api.github.com/repos/closeio/tasktiger/keys{/key_id}","collaborators_url":"https://api.github.com/repos/closeio/tasktiger/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/closeio/tasktiger/teams","hooks_url":"https://api.github.com/repos/closeio/tasktiger/hooks","issue_events_url":"https://api.github.com/repos/closeio/tasktiger/issues/events{/number}","events_url":"https://api.github.com/repos/closeio/tasktiger/events","assignees_url":"https://api.github.com/repos/closeio/tasktiger/assignees{/user}","branches_url":"https://api.github.com/repos/closeio/tasktiger/branches{/branch}","tags_url":"https://api.github.com/repos/closeio/tasktiger/tags","blobs_url":"https://api.github.com/repos/closeio/tasktiger/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/closeio/tasktiger/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/closeio/tasktiger/git/refs{/sha}","trees_url":"https://api.github.com/repos/closeio/tasktiger/git/trees{/sha}","statuses_url":"https://api.github.com/repos/closeio/tasktiger/statuses/{sha}","languages_url":"https://api.github.com/repos/closeio/tasktiger/languages","stargazers_url":"https://api.github.com/repos/closeio/tasktiger/stargazers","contributors_url":"https://api.github.com/repos/closeio/tasktiger/contributors","subscribers_url":"https://api.github.com/repos/closeio/tasktiger/subscribers","subscription_url":"https://api.github.com/repos/closeio/tasktiger/subscription","commits_url":"https://api.github.com/repos/closeio/tasktiger/commits{/sha}","git_commits_url":"https://api.github.com/repos/closeio/tasktiger/git/commits{/sha}","comments_url":"https://api.github.com/repos/closeio/tasktiger/comments{/number}","issue_comment_url":"https://api.github.com/repos/closeio/tasktiger/issues/comments{/number}","contents_url":"https://api.github.com/repos/closeio/tasktiger/contents/{+path}","compare_url":"https://api.github.com/repos/closeio/tasktiger/compare/{base}...{head}","merges_url":"https://api.github.com/repos/closeio/tasktiger/merges","archive_url":"https://api.github.com/repos/closeio/tasktiger/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/closeio/tasktiger/downloads","issues_url":"https://api.github.com/repos/closeio/tasktiger/issues{/number}","pulls_url":"https://api.github.com/repos/closeio/tasktiger/pulls{/number}","milestones_url":"https://api.github.com/repos/closeio/tasktiger/milestones{/number}","notifications_url":"https://api.github.com/repos/closeio/tasktiger/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/closeio/tasktiger/labels{/name}","releases_url":"https://api.github.com/repos/closeio/tasktiger/releases{/id}","deployments_url":"https://api.github.com/repos/closeio/tasktiger/deployments","created_at":"2015-05-14T00:26:32Z","updated_at":"2016-02-18T22:40:02Z","pushed_at":"2016-02-22T20:18:29Z","git_url":"git://github.com/closeio/tasktiger.git","ssh_url":"git@github.com:closeio/tasktiger.git","clone_url":"https://github.com/closeio/tasktiger.git","svn_url":"https://github.com/closeio/tasktiger","homepage":"","size":125,"stargazers_count":261,"watchers_count":261,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":10,"mirror_url":null,"open_issues_count":2,"forks":10,"open_issues":2,"watchers":261,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/closeio/tasktiger/pulls/12"},"html":{"href":"https://github.com/closeio/tasktiger/pull/12"},"issue":{"href":"https://api.github.com/repos/closeio/tasktiger/issues/12"},"comments":{"href":"https://api.github.com/repos/closeio/tasktiger/issues/12/comments"},"review_comments":{"href":"https://api.github.com/repos/closeio/tasktiger/pulls/12/comments"},"review_comment":{"href":"https://api.github.com/repos/closeio/tasktiger/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/closeio/tasktiger/pulls/12/commits"},"statuses":{"href":"https://api.github.com/repos/closeio/tasktiger/statuses/661e67efe93202a47958e13e9acf536da17b9f14"}}}}