{"action":"created","comment":{"url":"https://api.github.com/repos/dcos/dcos/pulls/comments/84553359","pull_request_review_id":5322320,"id":84553359,"diff_hunk":"@@ -0,0 +1,464 @@\n+import copy\n+import inspect\n+import json\n+import logging\n+from functools import partialmethod\n+\n+from gen.exceptions import ValidationError\n+\n+log = logging.getLogger(__name__)\n+\n+\n+json_prettyprint_args = {\n+    \"sort_keys\": True,\n+    \"indent\": 2,\n+    \"separators\": (',', ':')\n+}\n+\n+\n+def get_function_parameters(function):\n+    return set(inspect.signature(function).parameters)\n+\n+\n+def validate_arguments_strings(arguments: dict):\n+    errors = dict()\n+    # Validate that all keys and vlaues of arguments are strings\n+    for k, v in arguments.items():\n+        if not isinstance(k, str):\n+            errors[''] = \"All keys in arguments must be strings. '{}' isn't.\".format(k)\n+        if not isinstance(v, str):\n+            errors[k] = (\"All values in arguments must be strings. Value for argument {} isn't. \" +\n+                         \"Given value: {}\").format(k, v)\n+    if len(errors):\n+        raise ValidationError(errors, set())\n+\n+\n+class Setter:\n+\n+    # NOTE: value may either be a function or a string.\n+    def __init__(self, name, value, is_optional, conditions, is_user):\n+        assert isinstance(conditions, list)\n+        self.name = name\n+        self.is_optional = is_optional\n+        self.conditions = conditions\n+        self.is_user = is_user\n+\n+        def get_value():\n+            return value\n+\n+        if isinstance(value, str):\n+            self.calc = get_value\n+            self.parameters = set()\n+        else:\n+            assert callable(value), \"{} should be a string or callable. Got: {}\".format(name, value)\n+            self.calc = value\n+            self.parameters = get_function_parameters(value)\n+\n+    def __repr__(self):\n+        return \"<Setter {}{}{}, conditions: {}{}>\".format(\n+            self.name,\n+            \", optional\" if self.is_optional else \"\",\n+            \", user\" if self.is_user else \"\",\n+            self.conditions,\n+            \", parameters {}\".format(self.parameters))\n+\n+\n+class Scope:\n+    def __init__(self, name: str, cases=None):\n+        self.name = name\n+        self.cases = cases if cases else dict()\n+\n+    def add_case(self, value: str, target):\n+        assert isinstance(target, Target)\n+        self.cases[value] = target\n+\n+    def __iadd__(self, other):\n+        assert isinstance(other, Scope), \"Internal consistency error, expected Scope but got {}\".format(type(other))\n+\n+        # Must have the same name and same options in order to be merged (can't have a new\n+        # switch add new, unhandled cases to a switch already included / represented by this scope).\n+        assert self.name == other.name, \"Internal consistency error: Trying to merge scopes with \" \\\n+            \"different names: {} and {}\".format(self.name, other.name)\n+        assert self.cases.keys() == other.cases.keys(), \"Same name / switch variable introduced \" \\\n+            \"with a different set of possible cases. name: {}. First options: {}, Second \" \\\n+            \"options: {}\".format(self.name, self.cases.keys(), other.cases.keys())\n+\n+        # Merge the targets for the cases\n+        for name in self.cases:\n+            self.cases[name] += other.cases[name]\n+\n+        return self\n+\n+    def __eq__(self, other):\n+        assert isinstance(other, Scope)\n+        return self.name == other.name and self.cases == other.cases\n+\n+\n+class Target:\n+\n+    # TODO(cmaloney): Make a better API for working with and managing sub scopes. The current\n+    # dictionary of dictionaries is really hard to use right.\n+    def __init__(self, variables=None, sub_scopes=None):\n+        self.variables = variables if variables else set()\n+        self.sub_scopes = sub_scopes if sub_scopes else dict()\n+\n+    def add_variable(self, variable: str):\n+        self.variables.add(variable)\n+\n+    def add_scope(self, scope: Scope):\n+        self.sub_scopes[scope.name] = scope\n+\n+    def __iadd__(self, other):\n+        assert isinstance(other, Target), \"Internal consistency error, expected Target but got {}\".format(type(other))\n+        self.variables |= other.variables\n+\n+        # Add all scopes from the other to this, merging all common sub scopes.\n+        for name, scope in other.sub_scopes.items():\n+            if name in self.sub_scopes:\n+                self.sub_scopes[name] += scope\n+            else:\n+                self.sub_scopes[name] = scope\n+\n+        return self\n+\n+    def __eq__(self, other):\n+        assert isinstance(other, Target)\n+        return self.variables == other.variables and self.sub_scopes == other.sub_scopes\n+\n+\n+class Source:\n+    def __init__(self, entry=None, is_user=False,):\n+        self.setters = dict()\n+        self.validate = list()\n+        self.is_user = is_user\n+        if entry:\n+            self.add_entry(entry, False)\n+\n+    def add_setter(self, name, value, is_optional, conditions):\n+        self.setters.setdefault(name, list()).append(Setter(name, value, is_optional, conditions, self.is_user))\n+\n+    def add_conditional_scope(self, scope, conditions):\n+        # TODO(cmaloney): 'defaults' are the same as 'can' and 'must' is identical to 'arguments' except\n+        # that one takes functions and one takes strings. Simplify to just 'can', 'must'.\n+        assert scope.keys() <= {'validate', 'default', 'must', 'conditional'}\n+\n+        self.validate += scope.get('validate', list())\n+\n+        for name, fn in scope.get('must', dict()).items():\n+            self.add_setter(name, fn, False, conditions)\n+\n+        for name, fn in scope.get('default', dict()).items():\n+            self.add_setter(name, fn, True, conditions)\n+\n+        for name, cond_options in scope.get('conditional', dict()).items():\n+            for value, sub_scope in cond_options.items():\n+                self.add_conditional_scope(sub_scope, conditions + [(name, value)])\n+\n+    add_must = partialmethod(add_setter, is_optional=False, conditions=[])\n+\n+    def add_value_dict(self, value_dict):\n+        for name, value in value_dict.items():\n+            self.add_must(name, value)\n+\n+    def remove_setters(self, scope):\n+        def del_setter(name):\n+            if name in self.setters:\n+                del self.setters[name]\n+\n+        for name in scope.get('must', dict()).keys():\n+            del_setter(name)\n+\n+        for name in scope.get('default', dict()).keys():\n+            del_setter(name)\n+\n+        for name, cond_options in scope.get('conditional', dict()).items():\n+            if name in self.setters:\n+                raise NotImplementedError(\"Should conditional setters overwrite all setters?\")\n+\n+    def add_entry(self, entry, replace_existing):\n+        if replace_existing:\n+            self.remove_setters(entry)\n+\n+        self.add_conditional_scope(entry, [])\n+\n+\n+# NOTE: This exception should never escape the DFSArgumentCalculator\n+class CalculatorError(Exception):\n+    def __init__(self, message, chain=[]):\n+        assert isinstance(message, str)\n+        assert isinstance(chain, list)\n+        self.message = message\n+        self.chain = chain\n+        super().__init__(message)\n+\n+\n+# Depth first search argument calculator. Detects cycles, as well as unmet\n+# dependencies.\n+# TODO(cmaloney): Separate chain / path building when unwinding from the root\n+#                 error messages.\n+class DFSArgumentCalculator():\n+    def __init__(self, setters, validate_fns):\n+        self._setters = setters\n+        self._arguments = dict()\n+        self.__in_progress = set()\n+        self._errors = dict()\n+        self._unset = set()\n+\n+        # Re-arrange the validation functions so we can more easily access them by\n+        # argument name.\n+        self._validate_by_arg = dict()\n+        self._multi_arg_validate = dict()\n+\n+        for fn in validate_fns:\n+            parameters = get_function_parameters(fn)\n+            # Could build up the single and multi parameter validation function maps in the same\n+            # thing but the timing / handling of when and how we run single vs. multi-parameter\n+            # validation functions is fairly different, the extra bit here simplifies the later code.\n+            if len(parameters) == 1:\n+                self._validate_by_arg[parameters.pop()] = fn\n+                assert not parameters\n+            else:\n+                self._multi_arg_validate[frozenset(parameters)] = fn\n+\n+    def _calculate_argument(self, name):\n+        # Filter out any setters which have predicates / conditions which are\n+        # satisfiably false.\n+        def all_conditions_met(setter):\n+            for condition_name, condition_value in setter.conditions:\n+                try:\n+                    if self._get(condition_name) != condition_value:\n+                        return False\n+                except CalculatorError as ex:\n+                    raise CalculatorError(\n+                        ex.message,\n+                        ex.chain + ['trying to test condition {}={}'.format(condition_name, condition_value)]) from ex\n+            return True\n+\n+        # Find the right setter to calculate the argument.\n+        feasible = list(filter(all_conditions_met, self._setters.get(name, list())))\n+\n+        if len(feasible) == 0:\n+            self._unset.add(name)\n+            raise CalculatorError(\"no way to set\")\n+\n+        # Filtier out all optional setters if there is more than one way to set.\n+        if len(feasible) > 1:\n+            final_feasible = list(filter(lambda setter: not setter.is_optional, feasible))\n+            assert final_feasible, \"Had multiple optionals and no musts. Template internal error: {!r}\".format(feasible)\n+            feasible = final_feasible\n+\n+        # Must be calculated but user tried to provide.\n+        if len(feasible) == 2 and (feasible[0].is_user or feasible[1].is_user):\n+            self._errors[name] = (\"{} must be calculated, but was explicitly set in the \"\n+                                  \"configuration. Remove it from the configuration.\").format(name)\n+            raise CalculatorError(\"{} must be calculated but set twice\".format(name))\n+\n+        if len(feasible) > 1:\n+            self._errors[name] = \"Internal error: Multiple ways to set {}.\".format(name)\n+            raise CalculatorError(\"multiple ways to set\",\n+                                  [\"options: {}\".format(feasible)])\n+\n+        setter = feasible[0]\n+\n+        # Get values for the parameters, then call. the setter function.\n+        kwargs = {}\n+        for parameter in setter.parameters:\n+            kwargs[parameter] = self._get(parameter)\n+\n+        try:\n+            value = setter.calc(**kwargs)\n+        except AssertionError as ex:\n+            self._errors[name] = ex.args[0]\n+            raise CalculatorError(\"assertion while calc\")\n+\n+        if name in self._validate_by_arg:\n+            try:\n+                self._validate_by_arg[name](value)\n+            except AssertionError as ex:\n+                self._errors[name] = ex.args[0]\n+                raise CalculatorError(\"assertion while validate\")\n+\n+        return value\n+\n+    def _get(self, name):\n+        if name in self._arguments:\n+            if self._arguments[name] is None:\n+                raise CalculatorError(\"No way to set\", [name])\n+            return self._arguments[name]\n+\n+        # Detect cycles by checking if we're in the middle of calculating the\n+        # argument being asked for\n+        if name in self.__in_progress:\n+            raise CalculatorError(\"Internal error. cycle detected. re-encountered {}\".format(name))\n+\n+        self.__in_progress.add(name)\n+        try:\n+            self._arguments[name] = self._calculate_argument(name)\n+            return self._arguments[name]\n+        except CalculatorError as ex:\n+            self._arguments[name] = None\n+            raise CalculatorError(ex.message, ex.chain + ['while calculating {}'.format(name)]) from ex\n+        except:\n+            self._arguments[name] = None\n+            raise\n+        finally:\n+            self.__in_progress.remove(name)\n+\n+    def _calculate_target(self, target):\n+        def evaluate_var(name):\n+            try:\n+                self._get(name)\n+            except CalculatorError as ex:\n+                log.debug(\"Error calculating %s: %s. Chain: %s\", name, ex.message, ex.chain)\n+\n+        for name in target.variables:\n+            evaluate_var(name)\n+\n+        for name, sub_scope in target.sub_scopes.items():\n+            if name not in self._arguments:\n+                evaluate_var(name)\n+\n+            # If the internal arg is None, there was an error, don't check if it\n+            # is a legal choice.\n+            if self._arguments[name] is None:\n+                continue\n+\n+            choice = self._get(name)\n+\n+            if choice not in sub_scope.cases:\n+                self._errors[name] = \"Invalid choice {}. Must choose one of {}\".format(\n+                    choice, \", \".join(sorted(sub_scope.keys())))\n+                continue\n+\n+            self._calculate_target(sub_scope.cases[choice])\n+\n+        # Perform all multi-argument validations\n+        for parameter_set, validate_fn in self._multi_arg_validate.items():\n+            # Build up argument map for validate function. If any arguments are\n+            # unset then skip this validate function.\n+            kwargs = dict()\n+            skip = False\n+            for parameter in parameter_set:\n+                if parameter not in self._arguments or self._arguments[parameter] is None:\n+                    skip = True\n+                    break\n+                kwargs[parameter] = self._arguments[parameter]\n+            if skip:\n+                continue\n+\n+            # Call the validation function, catching AssertionErrors and turning them into errors in\n+            # the error dictionary.\n+            try:\n+                validate_fn(**kwargs)\n+            except AssertionError as ex:\n+                self._errors[parameter_set] = ex.args[0]\n+\n+        # TODO(cmaloney): Return per-target results with the path to calculate each argument and the","path":"gen/internals.py","position":356,"original_position":356,"commit_id":"185ee3b1ae30055c57f42bbc54c7e7b3b8940322","original_commit_id":"185ee3b1ae30055c57f42bbc54c7e7b3b8940322","user":{"login":"orsenthil","id":332330,"avatar_url":"https://avatars.githubusercontent.com/u/332330?v=3","gravatar_id":"","url":"https://api.github.com/users/orsenthil","html_url":"https://github.com/orsenthil","followers_url":"https://api.github.com/users/orsenthil/followers","following_url":"https://api.github.com/users/orsenthil/following{/other_user}","gists_url":"https://api.github.com/users/orsenthil/gists{/gist_id}","starred_url":"https://api.github.com/users/orsenthil/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/orsenthil/subscriptions","organizations_url":"https://api.github.com/users/orsenthil/orgs","repos_url":"https://api.github.com/users/orsenthil/repos","events_url":"https://api.github.com/users/orsenthil/events{/privacy}","received_events_url":"https://api.github.com/users/orsenthil/received_events","type":"User","site_admin":false},"body":"JIRA for TODO?","created_at":"2016-10-21T22:14:15Z","updated_at":"2016-10-21T22:18:20Z","html_url":"https://github.com/dcos/dcos/pull/868#discussion_r84553359","pull_request_url":"https://api.github.com/repos/dcos/dcos/pulls/868","_links":{"self":{"href":"https://api.github.com/repos/dcos/dcos/pulls/comments/84553359"},"html":{"href":"https://github.com/dcos/dcos/pull/868#discussion_r84553359"},"pull_request":{"href":"https://api.github.com/repos/dcos/dcos/pulls/868"}}},"pull_request":{"url":"https://api.github.com/repos/dcos/dcos/pulls/868","id":89515474,"html_url":"https://github.com/dcos/dcos/pull/868","diff_url":"https://github.com/dcos/dcos/pull/868.diff","patch_url":"https://github.com/dcos/dcos/pull/868.patch","issue_url":"https://api.github.com/repos/dcos/dcos/issues/868","number":868,"state":"open","locked":false,"title":"Rework gen/ to be made of three primitives: Source, Target, Scope and Resolver","user":{"login":"cmaloney","id":751088,"avatar_url":"https://avatars.githubusercontent.com/u/751088?v=3","gravatar_id":"","url":"https://api.github.com/users/cmaloney","html_url":"https://github.com/cmaloney","followers_url":"https://api.github.com/users/cmaloney/followers","following_url":"https://api.github.com/users/cmaloney/following{/other_user}","gists_url":"https://api.github.com/users/cmaloney/gists{/gist_id}","starred_url":"https://api.github.com/users/cmaloney/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cmaloney/subscriptions","organizations_url":"https://api.github.com/users/cmaloney/orgs","repos_url":"https://api.github.com/users/cmaloney/repos","events_url":"https://api.github.com/users/cmaloney/events{/privacy}","received_events_url":"https://api.github.com/users/cmaloney/received_events","type":"User","site_admin":false},"body":"This reworks the gen a bit to make the library more understandable in terms of what it is doing. It introduces four main primitives:\r\n\r\n`Source` is a source of configuration variables. A `Target` is a set of configuration variables which\r\nneed to be set to have a complete configuration. The `Resolver` figures out how to use a collection of\r\nsources to fill in a collection of targets and check for conflicts.\r\n\r\n`Source`s can be user-provided (a dictionary loaded out of a config file). They can also be provided\r\nby different contexts (AWS templates have a source of AWS which implies things likes resolvers). A\r\nthird common provider is deriving from other already given values (master count is derived from a\r\nmaster list if a master list is given).\r\n\r\n`Target`s are a set of variables which need to be set, as well as a set of variables which need to be\r\nset under certain conditions. The \"conditional\" variables you can think of as a \"Switch\". Based on\r\nthe value of the argument to the switch, one of multiple paths are going to be taken, and all\r\nconfiguration variables in that particular path are expected to be taken.\r\n\r\nThe `Resolver` takes a collection of sources and targets, then tries to calculate (and validate) all\r\nthe values needed to satisfy the targets. It keeps track of the path to get to a specific value, if\r\nthere are any conflicts between different sources (master_count is set to a conflicting / different\r\nvalue than the length of master_list).","created_at":"2016-10-16T04:25:08Z","updated_at":"2016-10-21T22:18:20Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0dd675bbc7890399489a587c17c9c16931ea1513","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/dcos/dcos/pulls/868/commits","review_comments_url":"https://api.github.com/repos/dcos/dcos/pulls/868/comments","review_comment_url":"https://api.github.com/repos/dcos/dcos/pulls/comments{/number}","comments_url":"https://api.github.com/repos/dcos/dcos/issues/868/comments","statuses_url":"https://api.github.com/repos/dcos/dcos/statuses/185ee3b1ae30055c57f42bbc54c7e7b3b8940322","head":{"label":"cmaloney:source-target-scope","ref":"source-target-scope","sha":"185ee3b1ae30055c57f42bbc54c7e7b3b8940322","user":{"login":"cmaloney","id":751088,"avatar_url":"https://avatars.githubusercontent.com/u/751088?v=3","gravatar_id":"","url":"https://api.github.com/users/cmaloney","html_url":"https://github.com/cmaloney","followers_url":"https://api.github.com/users/cmaloney/followers","following_url":"https://api.github.com/users/cmaloney/following{/other_user}","gists_url":"https://api.github.com/users/cmaloney/gists{/gist_id}","starred_url":"https://api.github.com/users/cmaloney/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cmaloney/subscriptions","organizations_url":"https://api.github.com/users/cmaloney/orgs","repos_url":"https://api.github.com/users/cmaloney/repos","events_url":"https://api.github.com/users/cmaloney/events{/privacy}","received_events_url":"https://api.github.com/users/cmaloney/received_events","type":"User","site_admin":false},"repo":{"id":56587823,"name":"dcos","full_name":"cmaloney/dcos","owner":{"login":"cmaloney","id":751088,"avatar_url":"https://avatars.githubusercontent.com/u/751088?v=3","gravatar_id":"","url":"https://api.github.com/users/cmaloney","html_url":"https://github.com/cmaloney","followers_url":"https://api.github.com/users/cmaloney/followers","following_url":"https://api.github.com/users/cmaloney/following{/other_user}","gists_url":"https://api.github.com/users/cmaloney/gists{/gist_id}","starred_url":"https://api.github.com/users/cmaloney/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cmaloney/subscriptions","organizations_url":"https://api.github.com/users/cmaloney/orgs","repos_url":"https://api.github.com/users/cmaloney/repos","events_url":"https://api.github.com/users/cmaloney/events{/privacy}","received_events_url":"https://api.github.com/users/cmaloney/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/cmaloney/dcos","description":"DC/OS Build and Release tools","fork":true,"url":"https://api.github.com/repos/cmaloney/dcos","forks_url":"https://api.github.com/repos/cmaloney/dcos/forks","keys_url":"https://api.github.com/repos/cmaloney/dcos/keys{/key_id}","collaborators_url":"https://api.github.com/repos/cmaloney/dcos/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/cmaloney/dcos/teams","hooks_url":"https://api.github.com/repos/cmaloney/dcos/hooks","issue_events_url":"https://api.github.com/repos/cmaloney/dcos/issues/events{/number}","events_url":"https://api.github.com/repos/cmaloney/dcos/events","assignees_url":"https://api.github.com/repos/cmaloney/dcos/assignees{/user}","branches_url":"https://api.github.com/repos/cmaloney/dcos/branches{/branch}","tags_url":"https://api.github.com/repos/cmaloney/dcos/tags","blobs_url":"https://api.github.com/repos/cmaloney/dcos/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/cmaloney/dcos/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/cmaloney/dcos/git/refs{/sha}","trees_url":"https://api.github.com/repos/cmaloney/dcos/git/trees{/sha}","statuses_url":"https://api.github.com/repos/cmaloney/dcos/statuses/{sha}","languages_url":"https://api.github.com/repos/cmaloney/dcos/languages","stargazers_url":"https://api.github.com/repos/cmaloney/dcos/stargazers","contributors_url":"https://api.github.com/repos/cmaloney/dcos/contributors","subscribers_url":"https://api.github.com/repos/cmaloney/dcos/subscribers","subscription_url":"https://api.github.com/repos/cmaloney/dcos/subscription","commits_url":"https://api.github.com/repos/cmaloney/dcos/commits{/sha}","git_commits_url":"https://api.github.com/repos/cmaloney/dcos/git/commits{/sha}","comments_url":"https://api.github.com/repos/cmaloney/dcos/comments{/number}","issue_comment_url":"https://api.github.com/repos/cmaloney/dcos/issues/comments{/number}","contents_url":"https://api.github.com/repos/cmaloney/dcos/contents/{+path}","compare_url":"https://api.github.com/repos/cmaloney/dcos/compare/{base}...{head}","merges_url":"https://api.github.com/repos/cmaloney/dcos/merges","archive_url":"https://api.github.com/repos/cmaloney/dcos/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/cmaloney/dcos/downloads","issues_url":"https://api.github.com/repos/cmaloney/dcos/issues{/number}","pulls_url":"https://api.github.com/repos/cmaloney/dcos/pulls{/number}","milestones_url":"https://api.github.com/repos/cmaloney/dcos/milestones{/number}","notifications_url":"https://api.github.com/repos/cmaloney/dcos/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/cmaloney/dcos/labels{/name}","releases_url":"https://api.github.com/repos/cmaloney/dcos/releases{/id}","deployments_url":"https://api.github.com/repos/cmaloney/dcos/deployments","created_at":"2016-04-19T10:33:02Z","updated_at":"2016-04-19T10:33:03Z","pushed_at":"2016-10-21T02:53:39Z","git_url":"git://github.com/cmaloney/dcos.git","ssh_url":"git@github.com:cmaloney/dcos.git","clone_url":"https://github.com/cmaloney/dcos.git","svn_url":"https://github.com/cmaloney/dcos","homepage":null,"size":2095,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"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":"dcos:master","ref":"master","sha":"fbcf50c96b9b5b2be07713f0764b77841bcc7701","user":{"login":"dcos","id":17648048,"avatar_url":"https://avatars.githubusercontent.com/u/17648048?v=3","gravatar_id":"","url":"https://api.github.com/users/dcos","html_url":"https://github.com/dcos","followers_url":"https://api.github.com/users/dcos/followers","following_url":"https://api.github.com/users/dcos/following{/other_user}","gists_url":"https://api.github.com/users/dcos/gists{/gist_id}","starred_url":"https://api.github.com/users/dcos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dcos/subscriptions","organizations_url":"https://api.github.com/users/dcos/orgs","repos_url":"https://api.github.com/users/dcos/repos","events_url":"https://api.github.com/users/dcos/events{/privacy}","received_events_url":"https://api.github.com/users/dcos/received_events","type":"Organization","site_admin":false},"repo":{"id":56184050,"name":"dcos","full_name":"dcos/dcos","owner":{"login":"dcos","id":17648048,"avatar_url":"https://avatars.githubusercontent.com/u/17648048?v=3","gravatar_id":"","url":"https://api.github.com/users/dcos","html_url":"https://github.com/dcos","followers_url":"https://api.github.com/users/dcos/followers","following_url":"https://api.github.com/users/dcos/following{/other_user}","gists_url":"https://api.github.com/users/dcos/gists{/gist_id}","starred_url":"https://api.github.com/users/dcos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dcos/subscriptions","organizations_url":"https://api.github.com/users/dcos/orgs","repos_url":"https://api.github.com/users/dcos/repos","events_url":"https://api.github.com/users/dcos/events{/privacy}","received_events_url":"https://api.github.com/users/dcos/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/dcos/dcos","description":"DC/OS Build and Release tools","fork":false,"url":"https://api.github.com/repos/dcos/dcos","forks_url":"https://api.github.com/repos/dcos/dcos/forks","keys_url":"https://api.github.com/repos/dcos/dcos/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dcos/dcos/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dcos/dcos/teams","hooks_url":"https://api.github.com/repos/dcos/dcos/hooks","issue_events_url":"https://api.github.com/repos/dcos/dcos/issues/events{/number}","events_url":"https://api.github.com/repos/dcos/dcos/events","assignees_url":"https://api.github.com/repos/dcos/dcos/assignees{/user}","branches_url":"https://api.github.com/repos/dcos/dcos/branches{/branch}","tags_url":"https://api.github.com/repos/dcos/dcos/tags","blobs_url":"https://api.github.com/repos/dcos/dcos/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dcos/dcos/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dcos/dcos/git/refs{/sha}","trees_url":"https://api.github.com/repos/dcos/dcos/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dcos/dcos/statuses/{sha}","languages_url":"https://api.github.com/repos/dcos/dcos/languages","stargazers_url":"https://api.github.com/repos/dcos/dcos/stargazers","contributors_url":"https://api.github.com/repos/dcos/dcos/contributors","subscribers_url":"https://api.github.com/repos/dcos/dcos/subscribers","subscription_url":"https://api.github.com/repos/dcos/dcos/subscription","commits_url":"https://api.github.com/repos/dcos/dcos/commits{/sha}","git_commits_url":"https://api.github.com/repos/dcos/dcos/git/commits{/sha}","comments_url":"https://api.github.com/repos/dcos/dcos/comments{/number}","issue_comment_url":"https://api.github.com/repos/dcos/dcos/issues/comments{/number}","contents_url":"https://api.github.com/repos/dcos/dcos/contents/{+path}","compare_url":"https://api.github.com/repos/dcos/dcos/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dcos/dcos/merges","archive_url":"https://api.github.com/repos/dcos/dcos/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dcos/dcos/downloads","issues_url":"https://api.github.com/repos/dcos/dcos/issues{/number}","pulls_url":"https://api.github.com/repos/dcos/dcos/pulls{/number}","milestones_url":"https://api.github.com/repos/dcos/dcos/milestones{/number}","notifications_url":"https://api.github.com/repos/dcos/dcos/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dcos/dcos/labels{/name}","releases_url":"https://api.github.com/repos/dcos/dcos/releases{/id}","deployments_url":"https://api.github.com/repos/dcos/dcos/deployments","created_at":"2016-04-13T20:31:58Z","updated_at":"2016-10-21T15:19:34Z","pushed_at":"2016-10-21T22:08:36Z","git_url":"git://github.com/dcos/dcos.git","ssh_url":"git@github.com:dcos/dcos.git","clone_url":"https://github.com/dcos/dcos.git","svn_url":"https://github.com/dcos/dcos","homepage":"https://dcos.io","size":2974,"stargazers_count":988,"watchers_count":988,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":172,"mirror_url":null,"open_issues_count":35,"forks":172,"open_issues":35,"watchers":988,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/dcos/dcos/pulls/868"},"html":{"href":"https://github.com/dcos/dcos/pull/868"},"issue":{"href":"https://api.github.com/repos/dcos/dcos/issues/868"},"comments":{"href":"https://api.github.com/repos/dcos/dcos/issues/868/comments"},"review_comments":{"href":"https://api.github.com/repos/dcos/dcos/pulls/868/comments"},"review_comment":{"href":"https://api.github.com/repos/dcos/dcos/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/dcos/dcos/pulls/868/commits"},"statuses":{"href":"https://api.github.com/repos/dcos/dcos/statuses/185ee3b1ae30055c57f42bbc54c7e7b3b8940322"}}}}
{"actor":{"display_login":"orsenthil"}}