{"action":"created","comment":{"url":"https://api.github.com/repos/dcos/dcos/pulls/comments/89046832","pull_request_review_id":9596709,"id":89046832,"diff_hunk":"@@ -237,138 +372,234 @@ def all_conditions_met(setter):\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+        feasible = list(filter(all_conditions_met, self._setters.get(resolvable.name, list())))\n \n         if len(feasible) == 0:\n-            self._unset.add(name)\n-            raise CalculatorError(\"no way to set\")\n+            self._unset.add(resolvable.name)\n+            raise SkipError(\"no way to calculate. Must be set in configuration.\")\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+        # TODO(cmaloney): As long as all paths to set the value evaluate to the same value then\n+        # having more than one way to calculate is fine. This is true of both multiple internal /\n+        # system setters, and having multiple user setters.\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+            raise CalculatorError(\"{} must be calculated, but was explicitly set in the \"\n+                                  \"configuration. Remove it from the configuration.\").format(resolvable.name)\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+            raise CalculatorError(\"Internal error: Multiple ways to set {}.\".format(resolvable.name))\n \n         setter = feasible[0]\n \n-        # Get values for the parameters, then call. the setter function.\n+        # Get values for the parameters of the setter than call it to calculate the value.\n         kwargs = {}\n         for parameter in setter.parameters:\n-            kwargs[parameter] = self._get(parameter)\n+            # TODO(cmaloney): Should catch the exceptions, and wrap with the context of what\n+            # parameter caused the error to happen so we have a path back to the source value that\n+            # caused the problem.\n+            # TODO(cmaloney): Should evaluate all parameters, even if an early one errors, and\n+            # collect all the error messages to let the user know of as many errors as possible as\n+            # early as possible.\n+            kwargs[parameter] = self._resolve_name(parameter)\n \n         try:\n             value = setter.calc(**kwargs)\n+            validate_fns = self._validate_by_arg.get(resolvable.name)\n+            if validate_fns is not None:\n+                for validate_fn in validate_fns:\n+                    validate_fn(value)\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+            raise CalculatorError(ex.args[0], [ex]) from ex\n+\n+        return value, setter\n+\n+    @contextmanager\n+    def _stack_layer(self, name):\n+        assert name not in self._eval_stack, \"name shouldn't repeat in eval stack (means we \" \\\n+            \"entered a loop, which is bad): {} {}\".format(name, self._eval_stack)\n+        self._eval_stack.append(name)\n+        yield\n+        foo = self._eval_stack.pop()\n+        assert foo == name, \"Internal consistency error: Unwinding stack seems to not be the order it was built in...\"\n+\n+    def _ensure_finalized(self, resolvable):\n+        if resolvable.is_finalized:\n+            return\n+\n+        # If we're in the middle of resolving it already and find it again, that indicates there\n+        # was a circular dependency / cycle. Raise an error so that all the resolvers depending on\n+        # it (including itself) get put into an error state / marked appropriately.\n+        if resolvable in self._eval_stack:\n+            # TODO(cmaloney): do this properly, attach right metadata to error message\n+            raise CalculatorError(\n+                \"Internal error. cycle detected. re-encountered {}\".format(resolvable.name),\n+                [(resolvable, copy.copy(self._eval_stack))])\n+\n+        # Calculate the value, noting that we're in the context of calculating it.\n+        try:\n+            with self._stack_layer(resolvable.name):\n+                resolvable.finalize_value(*self._calculate(resolvable))\n+        except CalculatorError as ex:\n+            resolvable.finalize_error(ex)\n+            self._errors[resolvable.name] = ex.args[0]\n+        except SkipError as ex:\n+            resolvable.finalize_error(ex)\n+        except Exception as ex:\n+            msg = \"Unexpected exception: {}\".format(ex)\n+            resolvable.finalize_error(CalculatorError(msg, [ex]))\n+            self._errors[resolvable.name] = msg\n+            raise\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+    def _resolve_name(self, name):\n+        try:\n+            resolvable = 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+            # Ensure the resolvable is resolved\n+            self._ensure_finalized(resolvable)\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+            log.debug(\"Error calculating %s: %s. Chain: %s\", name, ex.message, ex.chain)\n+            raise\n+        except SkipError as ex:\n+            log.debug(\"Skipping error for key %s: %s\", name, ex)\n             raise\n         finally:\n-            self.__in_progress.remove(name)\n+            # The resolvable must have either\n+            assert resolvable.is_finalized, \"_ensure_finalized is supposed to always finalize a \" \\\n+                \"resolvable but didn't: {}\".format(resolvable)\n+\n+        # If the resolvable is in an error state, raise it so that all the resolvables\n+        # depending on it will be put into an error state.\n+        if resolvable.is_error:\n+            # TODO(cmaloney): Should re-raise the original error with it's original context.\n+            raise SkipError(\n+                \"Value depended upon {} has an error: {}\".format(resolvable.name, resolvable.error),\n+                [(name, copy.copy(self._eval_stack))])\n+\n+        return resolvable.value\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+        finalized_arguments = dict()\n \n+        # TODO(cmaloney): All the arguments depended upon by the arguments resolved here should be\n+        # included in the target's full set of finalized arguments.\n         for name in target.variables:\n-            evaluate_var(name)\n+            self._ensure_finalized(self._arguments[name])\n \n         for name, sub_scope in target.sub_scopes.items():\n-            if name not in self._arguments:\n-                evaluate_var(name)\n+            self._ensure_finalized(self._arguments[name])\n+            resolvable = self._arguments[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+            assert resolvable.is_finalized, \" _resolve_name should have resulted in finalization \" \\\n+                \"of {}\".format(resolvable)\n+\n+            # Tried solving for the condition but couldn't, so we can't check\n+            # the sub-scope because we don't know which one to check.\n+            if resolvable.is_error:\n                 continue\n \n-            choice = self._get(name)\n+            assert resolvable.is_resolved, \"uhhh: {}\".format(resolvable)\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+            # Must be in the cases since we add validation functions for all switches automatically.\n+            assert resolvable.value in sub_scope.cases\n \n-            self._calculate_target(sub_scope.cases[choice])\n+            # Calculate all the items in the sub-scope\n+            sub_target = sub_scope.cases[resolvable.value]\n+            self._calculate_target(sub_target)\n \n+            # This .update() is safe because Resolver guarantees each argument\n+            # only ever has one value / resolvable.\n+            finalized_arguments.update(sub_target.arguments)\n+\n+        target.finalize(finalized_arguments)\n+\n+    # Force calculation of all arguments by accessing the arguments in this\n+    # scope and recursively all sub-scopes.\n+    def resolve(self):\n+        assert not self._resolved, \"Resolvers should only be resolved once\"\n+        self._resolved = True","path":"gen/internals.py","position":457,"original_position":457,"commit_id":"5df5a1e1c432704b9757def4b60a02203d80e623","original_commit_id":"5df5a1e1c432704b9757def4b60a02203d80e623","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":"So there is a function which looks just like that: https://github.com/dcos/dcos/blob/master/gen/internals.py#L374. The \"Resolver\" class is an internal implementation detail of it. No one other than that one function, resolve_configuration, should ever see / deal with an unresolved Resolver class.\r\n\r\nas far as why a class: I think it makes a lot easier to track the common state, as well as environment (things like ArgumentStack, context, and all the validation functions). They all are located together, and easily accessible by the members.\r\n\r\nThe resolver class once resolved, then provides a very precise interface to retrieve how the resolution ran (the status_dict), as well as all arguments which were calculated (.arguments), and ideally even indexing into the targets it was given eventually (currently it's easier for callers to just look at the resolver objects they passed in because the python objects are passed by reference. Eventually I want to attach names to the targets, which should make for better error messages).\r\n\r\nAnother advantage of the class, is that when something goes wrong, all the state of the world is centralized, which makes it a lot easier to print out what that state is, rather than having multiple inter-dependent objects, or trying to track / pass it through as multiple function arguments (or even one big function argument passed to a half dozen functions). This also means that when we find configuration errors (things like the unset configuration, or when a validation function fails), that all the state / world is available and around\r\n\r\nThe class also takes multiple things in, and gives multiple things out. The goal is that the resolver has a simple API to always use it. It keeps track of partially complete state (If an error happens halfway through resolving, it could be inspected / probed because all that state is there). When there are issues in resolving configuration, the bits are always all there. This in particular I think will be helpful to resolve: https://github.com/dcos/dcos/blob/master/dcos_installer/async_server.py#L116. Doing that requires we keep track of what caused what, and can always trace back to the individual fields present in the configuration ui of the web installer.\r\n\r\nArguably, could make the resolve() call part of the constructor, which would give you more of the API you're asking about, but from my perspective it is an important different change to the class from just setting up the environment required for resolving.","created_at":"2016-11-22T05:47:04Z","updated_at":"2016-11-22T05:47:04Z","html_url":"https://github.com/dcos/dcos/pull/958#discussion_r89046832","pull_request_url":"https://api.github.com/repos/dcos/dcos/pulls/958","_links":{"self":{"href":"https://api.github.com/repos/dcos/dcos/pulls/comments/89046832"},"html":{"href":"https://github.com/dcos/dcos/pull/958#discussion_r89046832"},"pull_request":{"href":"https://api.github.com/repos/dcos/dcos/pulls/958"}}},"pull_request":{"url":"https://api.github.com/repos/dcos/dcos/pulls/958","id":93859915,"html_url":"https://github.com/dcos/dcos/pull/958","diff_url":"https://github.com/dcos/dcos/pull/958.diff","patch_url":"https://github.com/dcos/dcos/pull/958.patch","issue_url":"https://api.github.com/repos/dcos/dcos/issues/958","number":958,"state":"open","locked":false,"title":"gen: Add the concept of a Resolvable, Resolve config per-Target","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":"\r\n - Targets now get finalized with results. This also means they need to be constructued per-use.\r\n - validate_configuration() no longer exists, as resolve_configuration does exactly the same thing,\r\n   users of validate_configuration() are migrated to just use resolve_configuration() once.\r\n - The per-target results aren't yet plugged into various places, that will come in a later pr\r\n - Introduce a notion of variables \"visited\" to calculate a particular variable.\r\n - Introduce a concept of a \"Resolvable\". A resolvable tracks the resolution state of a particular configuration value. (Unresolved, being resolved, error). It will eventually also include a \"late resolution\" (late binding) state. The introduction centralizes the logic for tracking that, and makes it harder to get wrong. Resolvables also keep track of what caused them to come into existence / get resolved.\r\n - Switches / sub-scopes now generate single-argument \"validate\" functions rather than having custom logic to validate their values when we reached the switch they were used in (See yield_implicit_validates)\r\n - Renames DFSArgumentCalculator to Resolver. A resolver is now only usable once, takes a set of setters (taken from a set of sources) and targets, then resolves them all at once. The goal being that the resolution of all targets produces a single, consistent / valid against eachother full final configuration. Resolver now owns / builds the status dictionary itself, and makes it accessible to callers\r\n - Adds a new unit test for gen internals code\r\n\r\nThings this works towards enabling\r\n - Typed parameters. \"Resolvables\" can be focused around a type (as discovered by python type annotations on function parameters)\r\n - \"nested\" configuration. Currently all config must be one top level dictionary. In theory though we could use nested dictionaries and flatten them ({\"a\": {\"b\": {\"c\": \"d\"}}} could be accessed as `a_b_c`.\r\n - Late-binding. Resolvables do a much better job of tracking / keeping contained the resolution state, code which was currently spread about DFSArgumentCalculator previously. The code to track \"unset\" and spread it through the tree, is very similar to the code needed to track \"late bound\" and that property spreading / chaining through the tree.\r\n - \"secret\" / private data such as aws keys in config.yaml. Things like the AWS keys should be explicitly protected / marked so they don't show up in things like the user config.yaml inside the cluster / on every host. If anything would make them show up, that should result in an error.\r\n\r\n# Issues\r\n\r\n - Late-binding configuration\r\n - Secrets in dcos-release.config.yaml vs. config.yaml for AWS Advanced templates\r\n - Enabling dcos-launch to use config.yaml\r\n\r\n# Checklist\r\n\r\n - [x] Included a test which will fail if code is reverted but test is not\r\n - [x] Followed relevant code rules [Rules for Packages and Systemd](https://github.com/dcos/dcos/tree/master/docs)\r\n","created_at":"2016-11-15T22:17:12Z","updated_at":"2016-11-22T05:49:47Z","closed_at":null,"merged_at":null,"merge_commit_sha":"84e4e88cfb2520808c8f2c46418fbf4c782b8b2d","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/dcos/dcos/pulls/958/commits","review_comments_url":"https://api.github.com/repos/dcos/dcos/pulls/958/comments","review_comment_url":"https://api.github.com/repos/dcos/dcos/pulls/comments{/number}","comments_url":"https://api.github.com/repos/dcos/dcos/issues/958/comments","statuses_url":"https://api.github.com/repos/dcos/dcos/statuses/5df5a1e1c432704b9757def4b60a02203d80e623","head":{"label":"cmaloney:gen-new-features","ref":"gen-new-features","sha":"5df5a1e1c432704b9757def4b60a02203d80e623","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-11-22T05:15:58Z","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":2230,"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":"af1d8f4c4858c1db631db7ff6eb9806cf14e714c","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-11-21T14:55:18Z","pushed_at":"2016-11-22T05:15:59Z","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":3442,"stargazers_count":1116,"watchers_count":1116,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":185,"mirror_url":null,"open_issues_count":53,"forks":185,"open_issues":53,"watchers":1116,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/dcos/dcos/pulls/958"},"html":{"href":"https://github.com/dcos/dcos/pull/958"},"issue":{"href":"https://api.github.com/repos/dcos/dcos/issues/958"},"comments":{"href":"https://api.github.com/repos/dcos/dcos/issues/958/comments"},"review_comments":{"href":"https://api.github.com/repos/dcos/dcos/pulls/958/comments"},"review_comment":{"href":"https://api.github.com/repos/dcos/dcos/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/dcos/dcos/pulls/958/commits"},"statuses":{"href":"https://api.github.com/repos/dcos/dcos/statuses/5df5a1e1c432704b9757def4b60a02203d80e623"}}}}
{"actor":{"display_login":"cmaloney"}}