{"action":"created","comment":{"url":"https://api.github.com/repos/open-craft/opencraft/pulls/comments/61756007","id":61756007,"diff_hunk":"@@ -0,0 +1,254 @@\n+# -*- coding: utf-8 -*-\n+#\n+# OpenCraft -- tools to aid developing and hosting free software projects\n+# Copyright (C) 2015 OpenCraft <xavier@opencraft.com>\n+#\n+# This program is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU Affero General Public License as\n+# published by the Free Software Foundation, either version 3 of the\n+# License, or (at your option) any later version.\n+#\n+# This program is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU Affero General Public License for more details.\n+#\n+# You should have received a copy of the GNU Affero General Public License\n+# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n+#\n+\"\"\"\n+Instance app models - App Server\n+\"\"\"\n+\n+# Imports #####################################################################\n+\n+import logging\n+\n+from django.conf import settings\n+from django.contrib.contenttypes.models import ContentType\n+from django.db import models\n+from django.db.models import Q\n+from django_extensions.db.models import TimeStampedModel\n+\n+from instance.logger_adapter import AppServerLoggerAdapter\n+from .instance import InstanceReference\n+from .log_entry import LogEntry\n+from .server import OpenStackServer\n+from .utils import ModelResourceStateDescriptor, ResourceState, ValidateModelMixin\n+\n+\n+# Logging #####################################################################\n+\n+logger = logging.getLogger(__name__)\n+\n+\n+# States ######################################################################\n+\n+class AppServerState(ResourceState):\n+    \"\"\"\n+    A [finite state machine] state describing an instance.\n+    \"\"\"\n+    # A state is \"steady\" if we don't expect it to change.\n+    # This information can be used to:\n+    # - delay execution of an operation until the target server reaches a steady state\n+    # - raise an exception when trying to schedule an operation that depends on a state change\n+    #   while the target server is in a steady state\n+    # Steady states include:\n+    # - Status.New\n+    # - Status.Running\n+    # - Status.ConfigurationFailed\n+    # - Status.Error\n+    # - Status.Terminated\n+    is_steady_state = True\n+\n+    # An instance is healthy if it is part of a normal (expected) workflow.\n+    # This information can be used to detect problems and highlight them in the UI or notify users.\n+    # Healthy states include:\n+    # - Status.New\n+    # - Status.WaitingForServer\n+    # - Status.ConfiguringServer\n+    # - Status.Running\n+    # - Status.Terminated\n+    is_healthy_state = True\n+\n+\n+class Status(ResourceState.Enum):\n+    \"\"\"\n+    The states that an instance can be in.\n+    \"\"\"\n+\n+    class New(AppServerState):\n+        \"\"\" Newly created \"\"\"\n+        state_id = 'new'\n+\n+    class WaitingForServer(AppServerState):\n+        \"\"\" Server not yet accessible \"\"\"\n+        state_id = 'waiting'\n+        name = 'Waiting for server'\n+        is_steady_state = False\n+\n+    class ConfiguringServer(AppServerState):\n+        \"\"\" Running Ansible playbooks on server \"\"\"\n+        state_id = 'configuring'\n+        name = 'Configuring server'\n+        is_steady_state = False\n+\n+    class Running(AppServerState):\n+        \"\"\" Instance is up and running \"\"\"\n+        state_id = 'running'\n+\n+    class ConfigurationFailed(AppServerState):\n+        \"\"\" Instance was not configured successfully (but may be partially online) \"\"\"\n+        state_id = 'failed'\n+        name = 'Configuration failed'\n+        is_healthy_state = False\n+\n+    class Error(AppServerState):\n+        \"\"\" Instance never got up and running (something went wrong when trying to build new VM) \"\"\"\n+        state_id = 'error'\n+        is_healthy_state = False\n+\n+    class Terminated(AppServerState):\n+        \"\"\" Instance was running successfully and has been shut down \"\"\"\n+        state_id = 'terminated'\n+\n+\n+# Models ######################################################################\n+\n+\n+class AppServer(ValidateModelMixin, TimeStampedModel):\n+    \"\"\"\n+    AppServer - One or more distinct web applications running on a single VM.\n+\n+    Owned by an Instance.\n+\n+    Characteristics of an AppServer:\n+    * An AppServer instance's configuration fields are *immutable*. If you want to change\n+      configuration, change hte Instance's configuration and have it create a new AppServer.\n+    * An AppServer owns exactly one VM (Server), onto which it installs its applications.\n+    \"\"\"\n+    Status = Status\n+    status = ModelResourceStateDescriptor(\n+        state_classes=Status.states, default_state=Status.New, model_field_name='_status'\n+    )\n+    _status = models.CharField(\n+        max_length=20,\n+        default=status.default_state_class.state_id,\n+        choices=status.model_field_choices,\n+        db_index=True,\n+        db_column='status',\n+    )\n+    # State transitions:\n+    _status_to_waiting_for_server = status.transition(\n+        from_states=Status.New, to_state=Status.WaitingForServer\n+    )\n+    _status_to_configuring_server = status.transition(\n+        from_states=Status.WaitingForServer, to_state=Status.ConfiguringServer\n+    )\n+    _status_to_error = status.transition(\n+        from_states=Status.WaitingForServer, to_state=Status.Error\n+    )\n+    _status_to_running = status.transition(\n+        from_states=Status.ConfiguringServer, to_state=Status.Running\n+    )\n+    _status_to_configuration_failed = status.transition(\n+        from_states=Status.ConfiguringServer, to_state=Status.ConfigurationFailed\n+    )\n+    _status_to_terminated = status.transition(\n+        from_states=Status.Running, to_state=Status.Terminated\n+    )\n+\n+    name = models.CharField(max_length=250, blank=False)\n+    server = models.OneToOneField(OpenStackServer, on_delete=models.CASCADE, related_name='+')\n+    # The Instance that owns this. Instance will get related_name accessors like 'openedxappserver_set'\n+    owner = models.ForeignKey(InstanceReference, on_delete=models.CASCADE, related_name='%(class)s_set')\n+\n+    class Meta:\n+        abstract = True\n+\n+    def __init__(self, *args, **kwargs):\n+        super().__init__(*args, **kwargs)\n+\n+        self.logger = AppServerLoggerAdapter(logger, {'obj': self})\n+\n+    def __str__(self):\n+        return self.name\n+\n+    @property\n+    def instance(self):\n+        \"\"\"\n+        Get the Instance that owns this AppServer\n+        \"\"\"\n+        return self.owner.instance\n+\n+    @property\n+    def event_context(self):\n+        \"\"\"\n+        Context dictionary to include in events\n+        \"\"\"\n+        context = self.instance.event_context  # dict with instance_id\n+        context.update({'appserver_id': self.pk, 'appserver_type': self.__class__.__name__})\n+        return context\n+\n+    def set_field_defaults(self):\n+        \"\"\"\n+        Set default values.\n+\n+        We don't use django field 'defaults=callable' because changes to the field defaults\n+        affect automatically generated migrations, generating a new one when settings don't match\n+\n+        This method is called by clean_fields() before validation and save()\n+        \"\"\"\n+        pass\n+\n+    def clean_fields(self, exclude=None):\n+        \"\"\"\n+        Set default values before validation.\n+        \"\"\"\n+        if not self.pk:\n+            self.set_field_defaults()\n+        super().clean_fields(exclude=exclude)\n+\n+    def save(self, **kwargs):\n+        if self.pk:\n+            # We are changing an existing instance. But most AppServer fields are meant to be\n+            # immutable. Only 'status' can change.","path":"instance/models/appserver.py","position":215,"original_position":215,"commit_id":"3c3c7b24978fbc979ba533b6997d9b8cec5c367b","original_commit_id":"3c3c7b24978fbc979ba533b6997d9b8cec5c367b","user":{"login":"itsjeyd","id":961441,"avatar_url":"https://avatars.githubusercontent.com/u/961441?v=3","gravatar_id":"","url":"https://api.github.com/users/itsjeyd","html_url":"https://github.com/itsjeyd","followers_url":"https://api.github.com/users/itsjeyd/followers","following_url":"https://api.github.com/users/itsjeyd/following{/other_user}","gists_url":"https://api.github.com/users/itsjeyd/gists{/gist_id}","starred_url":"https://api.github.com/users/itsjeyd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/itsjeyd/subscriptions","organizations_url":"https://api.github.com/users/itsjeyd/orgs","repos_url":"https://api.github.com/users/itsjeyd/repos","events_url":"https://api.github.com/users/itsjeyd/events{/privacy}","received_events_url":"https://api.github.com/users/itsjeyd/received_events","type":"User","site_admin":false},"body":"@bradenmacdonald \"We are changing an existing app server\", perhaps? Also, based on the code below \"modified\" can change as well, not just \"status\".","created_at":"2016-05-02T15:39:17Z","updated_at":"2016-05-02T15:39:17Z","html_url":"https://github.com/open-craft/opencraft/pull/63#discussion_r61756007","pull_request_url":"https://api.github.com/repos/open-craft/opencraft/pulls/63","_links":{"self":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/comments/61756007"},"html":{"href":"https://github.com/open-craft/opencraft/pull/63#discussion_r61756007"},"pull_request":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/63"}}},"pull_request":{"url":"https://api.github.com/repos/open-craft/opencraft/pulls/63","id":68165566,"html_url":"https://github.com/open-craft/opencraft/pull/63","diff_url":"https://github.com/open-craft/opencraft/pull/63.diff","patch_url":"https://github.com/open-craft/opencraft/pull/63.patch","issue_url":"https://api.github.com/repos/open-craft/opencraft/issues/63","number":63,"state":"open","locked":false,"title":"OC-1541: Keep the old instance running while deploying a new one","user":{"login":"bradenmacdonald","id":945577,"avatar_url":"https://avatars.githubusercontent.com/u/945577?v=3","gravatar_id":"","url":"https://api.github.com/users/bradenmacdonald","html_url":"https://github.com/bradenmacdonald","followers_url":"https://api.github.com/users/bradenmacdonald/followers","following_url":"https://api.github.com/users/bradenmacdonald/following{/other_user}","gists_url":"https://api.github.com/users/bradenmacdonald/gists{/gist_id}","starred_url":"https://api.github.com/users/bradenmacdonald/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bradenmacdonald/subscriptions","organizations_url":"https://api.github.com/users/bradenmacdonald/orgs","repos_url":"https://api.github.com/users/bradenmacdonald/repos","events_url":"https://api.github.com/users/bradenmacdonald/events{/privacy}","received_events_url":"https://api.github.com/users/bradenmacdonald/received_events","type":"User","site_admin":false},"body":"**This is WIP**. Everything is broken (on purpose).\r\n\r\nChanges:\r\n- Fix occasional pylint warning about undocumented Meta classes\r\n- Combine LogEntry classes\r\n    * ^ These two commits have been copied to #67 so they can be reviewed more easily and merged sooner\r\n- Separated `[OpenEdX]Instance` into `Instance` (long-lived, mutable, creates AppServers and marks one as active), and `AppServer` (immutable, owns one VM)\r\n- Factored Open edX specific code into `OpenEdXInstance` and `OpenEdXAppServer` respectively\r\n- Factored Open edX specific code out of the ansible mixin\r\n- Change the ansible mixin so it can optionally run multiple playbooks\r\n- Removed `instance.last_provisioning_started` since you can compute that from the most recent `AppServer.created` field\r\n- Combined `forum_version`, `notifier_version`, `xqueue_version`, and `certs_version` into a single `openedx_release` setting, since these were always set to the same value, and if one needs to be set differently it can be done using ansible_extra_settings anyways.\r\n- Separated PR-watching code to a separate app (`OpenEdXInstance` should not need to know about pull requests)\r\n\r\nTODO:\r\n- Generate migrations & data migrations\r\n- Fix everything that broke\r\n   * Update API\r\n   * Update UI\r\n- Update tests\r\n\r\n\r\nSuggestions for future work:\r\n- Allow stopping an AppServer build\r\n- Allow use of OpenCraft IM without specifying GitHub orgs/repos to watch), i.e. with the new `pr_watch` app disabled\r\n- Add Terminated state to the AppServer classes\r\n- Allow provisioning a new AppServer at any time","created_at":"2016-04-28T05:23:57Z","updated_at":"2016-05-02T15:39:17Z","closed_at":null,"merged_at":null,"merge_commit_sha":"d38efff12640e7b76bd82397ed27a5ddf3deaa69","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/open-craft/opencraft/pulls/63/commits","review_comments_url":"https://api.github.com/repos/open-craft/opencraft/pulls/63/comments","review_comment_url":"https://api.github.com/repos/open-craft/opencraft/pulls/comments{/number}","comments_url":"https://api.github.com/repos/open-craft/opencraft/issues/63/comments","statuses_url":"https://api.github.com/repos/open-craft/opencraft/statuses/3c3c7b24978fbc979ba533b6997d9b8cec5c367b","head":{"label":"open-craft:provision-while-running","ref":"provision-while-running","sha":"3c3c7b24978fbc979ba533b6997d9b8cec5c367b","user":{"login":"open-craft","id":7414786,"avatar_url":"https://avatars.githubusercontent.com/u/7414786?v=3","gravatar_id":"","url":"https://api.github.com/users/open-craft","html_url":"https://github.com/open-craft","followers_url":"https://api.github.com/users/open-craft/followers","following_url":"https://api.github.com/users/open-craft/following{/other_user}","gists_url":"https://api.github.com/users/open-craft/gists{/gist_id}","starred_url":"https://api.github.com/users/open-craft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/open-craft/subscriptions","organizations_url":"https://api.github.com/users/open-craft/orgs","repos_url":"https://api.github.com/users/open-craft/repos","events_url":"https://api.github.com/users/open-craft/events{/privacy}","received_events_url":"https://api.github.com/users/open-craft/received_events","type":"Organization","site_admin":false},"repo":{"id":40447973,"name":"opencraft","full_name":"open-craft/opencraft","owner":{"login":"open-craft","id":7414786,"avatar_url":"https://avatars.githubusercontent.com/u/7414786?v=3","gravatar_id":"","url":"https://api.github.com/users/open-craft","html_url":"https://github.com/open-craft","followers_url":"https://api.github.com/users/open-craft/followers","following_url":"https://api.github.com/users/open-craft/following{/other_user}","gists_url":"https://api.github.com/users/open-craft/gists{/gist_id}","starred_url":"https://api.github.com/users/open-craft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/open-craft/subscriptions","organizations_url":"https://api.github.com/users/open-craft/orgs","repos_url":"https://api.github.com/users/open-craft/repos","events_url":"https://api.github.com/users/open-craft/events{/privacy}","received_events_url":"https://api.github.com/users/open-craft/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/open-craft/opencraft","description":"OpenCraft - Instance Manager","fork":false,"url":"https://api.github.com/repos/open-craft/opencraft","forks_url":"https://api.github.com/repos/open-craft/opencraft/forks","keys_url":"https://api.github.com/repos/open-craft/opencraft/keys{/key_id}","collaborators_url":"https://api.github.com/repos/open-craft/opencraft/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/open-craft/opencraft/teams","hooks_url":"https://api.github.com/repos/open-craft/opencraft/hooks","issue_events_url":"https://api.github.com/repos/open-craft/opencraft/issues/events{/number}","events_url":"https://api.github.com/repos/open-craft/opencraft/events","assignees_url":"https://api.github.com/repos/open-craft/opencraft/assignees{/user}","branches_url":"https://api.github.com/repos/open-craft/opencraft/branches{/branch}","tags_url":"https://api.github.com/repos/open-craft/opencraft/tags","blobs_url":"https://api.github.com/repos/open-craft/opencraft/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/open-craft/opencraft/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/open-craft/opencraft/git/refs{/sha}","trees_url":"https://api.github.com/repos/open-craft/opencraft/git/trees{/sha}","statuses_url":"https://api.github.com/repos/open-craft/opencraft/statuses/{sha}","languages_url":"https://api.github.com/repos/open-craft/opencraft/languages","stargazers_url":"https://api.github.com/repos/open-craft/opencraft/stargazers","contributors_url":"https://api.github.com/repos/open-craft/opencraft/contributors","subscribers_url":"https://api.github.com/repos/open-craft/opencraft/subscribers","subscription_url":"https://api.github.com/repos/open-craft/opencraft/subscription","commits_url":"https://api.github.com/repos/open-craft/opencraft/commits{/sha}","git_commits_url":"https://api.github.com/repos/open-craft/opencraft/git/commits{/sha}","comments_url":"https://api.github.com/repos/open-craft/opencraft/comments{/number}","issue_comment_url":"https://api.github.com/repos/open-craft/opencraft/issues/comments{/number}","contents_url":"https://api.github.com/repos/open-craft/opencraft/contents/{+path}","compare_url":"https://api.github.com/repos/open-craft/opencraft/compare/{base}...{head}","merges_url":"https://api.github.com/repos/open-craft/opencraft/merges","archive_url":"https://api.github.com/repos/open-craft/opencraft/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/open-craft/opencraft/downloads","issues_url":"https://api.github.com/repos/open-craft/opencraft/issues{/number}","pulls_url":"https://api.github.com/repos/open-craft/opencraft/pulls{/number}","milestones_url":"https://api.github.com/repos/open-craft/opencraft/milestones{/number}","notifications_url":"https://api.github.com/repos/open-craft/opencraft/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/open-craft/opencraft/labels{/name}","releases_url":"https://api.github.com/repos/open-craft/opencraft/releases{/id}","deployments_url":"https://api.github.com/repos/open-craft/opencraft/deployments","created_at":"2015-08-09T19:35:05Z","updated_at":"2016-02-17T16:53:19Z","pushed_at":"2016-05-02T15:38:44Z","git_url":"git://github.com/open-craft/opencraft.git","ssh_url":"git@github.com:open-craft/opencraft.git","clone_url":"https://github.com/open-craft/opencraft.git","svn_url":"https://github.com/open-craft/opencraft","homepage":null,"size":1634,"stargazers_count":6,"watchers_count":6,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":6,"mirror_url":null,"open_issues_count":6,"forks":6,"open_issues":6,"watchers":6,"default_branch":"master"}},"base":{"label":"open-craft:master","ref":"master","sha":"f95e9d47cbd0bc6e6d3ebaead4592b673cc2c351","user":{"login":"open-craft","id":7414786,"avatar_url":"https://avatars.githubusercontent.com/u/7414786?v=3","gravatar_id":"","url":"https://api.github.com/users/open-craft","html_url":"https://github.com/open-craft","followers_url":"https://api.github.com/users/open-craft/followers","following_url":"https://api.github.com/users/open-craft/following{/other_user}","gists_url":"https://api.github.com/users/open-craft/gists{/gist_id}","starred_url":"https://api.github.com/users/open-craft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/open-craft/subscriptions","organizations_url":"https://api.github.com/users/open-craft/orgs","repos_url":"https://api.github.com/users/open-craft/repos","events_url":"https://api.github.com/users/open-craft/events{/privacy}","received_events_url":"https://api.github.com/users/open-craft/received_events","type":"Organization","site_admin":false},"repo":{"id":40447973,"name":"opencraft","full_name":"open-craft/opencraft","owner":{"login":"open-craft","id":7414786,"avatar_url":"https://avatars.githubusercontent.com/u/7414786?v=3","gravatar_id":"","url":"https://api.github.com/users/open-craft","html_url":"https://github.com/open-craft","followers_url":"https://api.github.com/users/open-craft/followers","following_url":"https://api.github.com/users/open-craft/following{/other_user}","gists_url":"https://api.github.com/users/open-craft/gists{/gist_id}","starred_url":"https://api.github.com/users/open-craft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/open-craft/subscriptions","organizations_url":"https://api.github.com/users/open-craft/orgs","repos_url":"https://api.github.com/users/open-craft/repos","events_url":"https://api.github.com/users/open-craft/events{/privacy}","received_events_url":"https://api.github.com/users/open-craft/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/open-craft/opencraft","description":"OpenCraft - Instance Manager","fork":false,"url":"https://api.github.com/repos/open-craft/opencraft","forks_url":"https://api.github.com/repos/open-craft/opencraft/forks","keys_url":"https://api.github.com/repos/open-craft/opencraft/keys{/key_id}","collaborators_url":"https://api.github.com/repos/open-craft/opencraft/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/open-craft/opencraft/teams","hooks_url":"https://api.github.com/repos/open-craft/opencraft/hooks","issue_events_url":"https://api.github.com/repos/open-craft/opencraft/issues/events{/number}","events_url":"https://api.github.com/repos/open-craft/opencraft/events","assignees_url":"https://api.github.com/repos/open-craft/opencraft/assignees{/user}","branches_url":"https://api.github.com/repos/open-craft/opencraft/branches{/branch}","tags_url":"https://api.github.com/repos/open-craft/opencraft/tags","blobs_url":"https://api.github.com/repos/open-craft/opencraft/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/open-craft/opencraft/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/open-craft/opencraft/git/refs{/sha}","trees_url":"https://api.github.com/repos/open-craft/opencraft/git/trees{/sha}","statuses_url":"https://api.github.com/repos/open-craft/opencraft/statuses/{sha}","languages_url":"https://api.github.com/repos/open-craft/opencraft/languages","stargazers_url":"https://api.github.com/repos/open-craft/opencraft/stargazers","contributors_url":"https://api.github.com/repos/open-craft/opencraft/contributors","subscribers_url":"https://api.github.com/repos/open-craft/opencraft/subscribers","subscription_url":"https://api.github.com/repos/open-craft/opencraft/subscription","commits_url":"https://api.github.com/repos/open-craft/opencraft/commits{/sha}","git_commits_url":"https://api.github.com/repos/open-craft/opencraft/git/commits{/sha}","comments_url":"https://api.github.com/repos/open-craft/opencraft/comments{/number}","issue_comment_url":"https://api.github.com/repos/open-craft/opencraft/issues/comments{/number}","contents_url":"https://api.github.com/repos/open-craft/opencraft/contents/{+path}","compare_url":"https://api.github.com/repos/open-craft/opencraft/compare/{base}...{head}","merges_url":"https://api.github.com/repos/open-craft/opencraft/merges","archive_url":"https://api.github.com/repos/open-craft/opencraft/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/open-craft/opencraft/downloads","issues_url":"https://api.github.com/repos/open-craft/opencraft/issues{/number}","pulls_url":"https://api.github.com/repos/open-craft/opencraft/pulls{/number}","milestones_url":"https://api.github.com/repos/open-craft/opencraft/milestones{/number}","notifications_url":"https://api.github.com/repos/open-craft/opencraft/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/open-craft/opencraft/labels{/name}","releases_url":"https://api.github.com/repos/open-craft/opencraft/releases{/id}","deployments_url":"https://api.github.com/repos/open-craft/opencraft/deployments","created_at":"2015-08-09T19:35:05Z","updated_at":"2016-02-17T16:53:19Z","pushed_at":"2016-05-02T15:38:44Z","git_url":"git://github.com/open-craft/opencraft.git","ssh_url":"git@github.com:open-craft/opencraft.git","clone_url":"https://github.com/open-craft/opencraft.git","svn_url":"https://github.com/open-craft/opencraft","homepage":null,"size":1634,"stargazers_count":6,"watchers_count":6,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":6,"mirror_url":null,"open_issues_count":6,"forks":6,"open_issues":6,"watchers":6,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/63"},"html":{"href":"https://github.com/open-craft/opencraft/pull/63"},"issue":{"href":"https://api.github.com/repos/open-craft/opencraft/issues/63"},"comments":{"href":"https://api.github.com/repos/open-craft/opencraft/issues/63/comments"},"review_comments":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/63/comments"},"review_comment":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/63/commits"},"statuses":{"href":"https://api.github.com/repos/open-craft/opencraft/statuses/3c3c7b24978fbc979ba533b6997d9b8cec5c367b"}}}}