{"action":"created","comment":{"url":"https://api.github.com/repos/open-craft/opencraft/pulls/comments/84534110","pull_request_review_id":5309397,"id":84534110,"diff_hunk":"@@ -0,0 +1,66 @@\n+#!/edx/bin/python.edxapp\n+# pylint: skip-file\n+\n+from ConfigParser import ConfigParser\n+import gzip\n+import os\n+import re\n+import sys\n+\n+import django\n+\n+\n+nginx_access_pattern = re.compile(r'- - (?P<ipaddress>\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})')\n+log_path = '/edx/var/log/nginx'\n+\n+\n+if __name__ == '__main__':\n+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lms.envs.aws')\n+    os.environ.setdefault('SERVICE_VARIANT', 'lms')\n+    django.setup()\n+\n+    unique_hits = set()\n+    files = sorted([\n+        f for f in os.listdir(log_path)\n+        if os.path.isfile(os.path.join(log_path, f)) and f.startswith('access.log')\n+    ])\n+\n+    for file in files:\n+        print >> sys.stderr, 'Parsing log file: {file}'.format(file=file)\n+        file_path = os.path.join(log_path, file)\n+        if file.endswith('.gz'):\n+            handle = gzip.open(file_path, 'rb')\n+        else:\n+            handle = open(file_path, 'r')\n+\n+        for line in handle:\n+            match = re.match(nginx_access_pattern, line)\n+            if match:\n+                unique_hits.add(match.group('ipaddress'))\n+\n+        handle.close()\n+\n+    from django.contrib.auth.models import User\n+    from xmodule.modulestore.django import modulestore\n+\n+    ms = modulestore()\n+\n+    stats = {\n+        'users': User.objects.count(),\n+        'courses': len(ms.get_courses()),\n+        'hits': len(unique_hits),\n+    }\n+\n+    section_name = sys.argv[1]\n+\n+    config = ConfigParser()\n+    config.add_section(section_name)\n+    for key, value in stats.items():\n+        config.set(section_name, key, value)\n+\n+    config.write(sys.stdout)\n+    if len(sys.argv) > 2:\n+        filename = sys.argv[2]\n+        print >> sys.stderr, 'Writing to file passed via parameter: {filename}'.format(filename=filename)\n+        with open(filename, 'w') as output_file:\n+            config.write(output_file)","path":"playbooks/collect_activity/stats.py","position":79,"original_position":66,"commit_id":"d748a0fd16f4068c630f0553733d6806798cd313","original_commit_id":"6a3036388fda87d41a3e19a2730bc24f727d079d","user":{"login":"bdero","id":919017,"avatar_url":"https://avatars.githubusercontent.com/u/919017?v=3","gravatar_id":"","url":"https://api.github.com/users/bdero","html_url":"https://github.com/bdero","followers_url":"https://api.github.com/users/bdero/followers","following_url":"https://api.github.com/users/bdero/following{/other_user}","gists_url":"https://api.github.com/users/bdero/gists{/gist_id}","starred_url":"https://api.github.com/users/bdero/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bdero/subscriptions","organizations_url":"https://api.github.com/users/bdero/orgs","repos_url":"https://api.github.com/users/bdero/repos","events_url":"https://api.github.com/users/bdero/events{/privacy}","received_events_url":"https://api.github.com/users/bdero/received_events","type":"User","site_admin":false},"body":"Yeah I agree, there are also more gotchas in this script relatively to the rest of the PR as well. I went ahead and annotated this file with comments to explain things. I also rearranged the execution order of some things to make it more understandable in context. Let me know if it seems good now!","created_at":"2016-10-21T19:53:56Z","updated_at":"2016-10-21T19:53:56Z","html_url":"https://github.com/open-craft/opencraft/pull/144#discussion_r84534110","pull_request_url":"https://api.github.com/repos/open-craft/opencraft/pulls/144","_links":{"self":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/comments/84534110"},"html":{"href":"https://github.com/open-craft/opencraft/pull/144#discussion_r84534110"},"pull_request":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/144"}}},"pull_request":{"url":"https://api.github.com/repos/open-craft/opencraft/pulls/144","id":90232620,"html_url":"https://github.com/open-craft/opencraft/pull/144","diff_url":"https://github.com/open-craft/opencraft/pull/144.diff","patch_url":"https://github.com/open-craft/opencraft/pull/144.patch","issue_url":"https://api.github.com/repos/open-craft/opencraft/issues/144","number":144,"state":"open","locked":false,"title":"Added management command for collecting activity stats.","user":{"login":"bdero","id":919017,"avatar_url":"https://avatars.githubusercontent.com/u/919017?v=3","gravatar_id":"","url":"https://api.github.com/users/bdero","html_url":"https://github.com/bdero","followers_url":"https://api.github.com/users/bdero/followers","following_url":"https://api.github.com/users/bdero/following{/other_user}","gists_url":"https://api.github.com/users/bdero/gists{/gist_id}","starred_url":"https://api.github.com/users/bdero/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bdero/subscriptions","organizations_url":"https://api.github.com/users/bdero/orgs","repos_url":"https://api.github.com/users/bdero/repos","events_url":"https://api.github.com/users/bdero/events{/privacy}","received_events_url":"https://api.github.com/users/bdero/received_events","type":"User","site_admin":false},"body":"This change implements a management command `activity_csv` that generates a CSV containing basic activity stats for each *active* app server.\r\n\r\nIt works like this:\r\n\r\n1. The management command first compiles an inventory consisting of the public IP of each *active* app server.\r\n1. The inventory is fed to the `collect_activity` playbook via `instance.ansible.run_playbook`, which sets up a new virtualenv and executes the playbook.\r\n1. The playbook places and executes a python script `stats.py` against the edX environment on each server. It grabs courses from the modulestore, counts the users, and parses all of the Nginx logs (including gzipped files) to collect a unique set of hits (by IPv4 address).\r\n1. `stats.py` outputs a temporary file containing the stats (in `ConfigParser` format).\r\n1. The playbook fetches the results from the remote host and aggregates them in a temporary directory. The playbook finishes here.\r\n1. After the playbook finishes executing, the aggregated `ConfigParser` files are loaded/merged.\r\n1. All of the information is loaded and available, so the CSV is pushed to the output stream. The output stream is either `stdout` or an output file, if one was specified using the `--out` flag.\r\n\r\n**JIRA tickets**: Implements [OC-1999](https://tasks.opencraft.com/browse/OC-1999).\r\n\r\n**Testing instructions**:\r\n\r\nGet OCIM setup to manage at least one instance (skip if you already have it setup):\r\n\r\n1. Setup an OpenCraft IM vagrant box.\r\n1. Configure the OpenCraft IM dev to either use the \"Dev\" or \"Integration\" account on OVH.\r\n1. Provision a new app server\r\n\r\nTest this feature:\r\n\r\n1. Checkout this branch:\r\n\r\n  ```\r\ngit remote update && git checkout origin/bdero/activity_csv\r\n  ```\r\n\r\n1. Ensure everything is up-to-date:\r\n\r\n  ```\r\nmake clean && make migration_check && make static_external\r\n  ```\r\n\r\n1. Run the new management command:\r\n\r\n  ```\r\nmake manage activity_csv\r\n  ```\r\n\r\n  **NOTE:** You will likely see build failures building \"cryptography\" during the virtualenv setup. This is expected and not an issue.\r\n\r\n  Example successful output of playbook:\r\n\r\n  ```\r\nPLAY [all] *********************************************************************\r\n\r\nTASK [setup] *******************************************************************\r\nok: [149.202.188.116]\r\n\r\nTASK [Copy stats script] *******************************************************\r\nok: [149.202.188.116]\r\n\r\nTASK [Run stats script] ********************************************************\r\nchanged: [149.202.188.116]\r\n\r\nTASK [debug] *******************************************************************\r\nok: [149.202.188.116] => {\r\n    \"stats_results\": {\r\n        \"changed\": true,\r\n        \"cmd\": [\r\n            \"/edx/bin/python.edxapp\",\r\n            \"/tmp/collect_activity.py\",\r\n            \"149.202.188.116\",\r\n            \"/tmp/activity_report\"\r\n        ],\r\n        \"delta\": \"0:00:03.881274\",\r\n        \"end\": \"2016-10-20 15:41:23.425372\",\r\n        \"rc\": 0,\r\n        \"start\": \"2016-10-20 15:41:19.544098\",\r\n        \"stderr\": \"2016-10-20 15:41:21,163 WARNING 5561 [py.warnings] base.py:116 - /edx/app/edxapp/edx-platform/openedx/core/djangoapps/site_configuration/models.py:19: RemovedInDjango19Warning: Model class openedx.core.djangoapps.site_configuration.models.SiteConfiguration doesn't declare an explicit app_label and either isn't in an application in INSTALLED_APPS or else was imported before its application was loaded. This will no longer be supported in Django 1.9.\\n  class SiteConfiguration(models.Model):\\n\\n2016-10-20 15:41:21,164 WARNING 5561 [py.warnings] base.py:116 - /edx/app/edxapp/edx-platform/openedx/core/djangoapps/site_configuration/models.py:113: RemovedInDjango19Warning: Model class openedx.core.djangoapps.site_configuration.models.SiteConfigurationHistory doesn't declare an explicit app_label and either isn't in an application in INSTALLED_APPS or else was imported before its application was loaded. This will no longer be supported in Django 1.9.\\n  class SiteConfigurationHistory(TimeStampedModel):\\n\\n2016-10-20 15:41:21,196 WARNING 5561 [py.warnings] importlib.py:9 - /edx/app/edxapp/venvs/edxapp/local/lib/python2.7/site-packages/require/helpers.py:4: RemovedInDjango19Warning: django.utils.importlib will be removed in Django 1.9.\\n  from django.utils.importlib import import_module\\n\\n2016-10-20 15:41:21,208 WARNING 5561 [py.warnings] base.py:116 - /edx/app/edxapp/edx-platform/openedx/core/djangoapps/self_paced/models.py:11: RemovedInDjango19Warning: Model class openedx.core.djangoapps.self_paced.models.SelfPacedConfiguration doesn't declare an explicit app_label and either isn't in an application in INSTALLED_APPS or else was imported before its application was loaded. This will no longer be supported in Django 1.9.\\n  class SelfPacedConfiguration(ConfigurationModel):\\n\\n2016-10-20 15:41:21,338 WARNING 5561 [py.warnings] base.py:116 - /edx/app/edxapp/edx-platform/openedx/core/djangoapps/content/course_overviews/models.py:618: RemovedInDjango19Warning: Model class openedx.core.djangoapps.content.course_overviews.models.CourseOverviewTab doesn't declare an explicit app_label and either isn't in an application in INSTALLED_APPS or else was imported before its application was loaded. This will no longer be supported in Django 1.9.\\n  class CourseOverviewTab(models.Model):\\n\\n2016-10-20 15:41:21,339 WARNING 5561 [py.warnings] base.py:116 - /edx/app/edxapp/edx-platform/openedx/core/djangoapps/content/course_overviews/models.py:626: RemovedInDjango19Warning: Model class openedx.core.djangoapps.content.course_overviews.models.CourseOverviewImageSet doesn't declare an explicit app_label and either isn't in an application in INSTALLED_APPS or else was imported before its application was loaded. This will no longer be supported in Django 1.9.\\n  class CourseOverviewImageSet(TimeStampedModel):\\n\\n2016-10-20 15:41:21,340 WARNING 5561 [py.warnings] base.py:116 - /edx/app/edxapp/edx-platform/openedx/core/djangoapps/content/course_overviews/models.py:764: RemovedInDjango19Warning: Model class openedx.core.djangoapps.content.course_overviews.models.CourseOverviewImageConfig doesn't declare an explicit app_label and either isn't in an application in INSTALLED_APPS or else was imported before its application was loaded. This will no longer be supported in Django 1.9.\\n  class CourseOverviewImageConfig(ConfigurationModel):\\n\\n2016-10-20 15:41:21,344 WARNING 5561 [py.warnings] base.py:116 - /edx/app/edxapp/edx-platform/common/djangoapps/course_modes/models.py:29: RemovedInDjango19Warning: Model class course_modes.models.CourseMode doesn't declare an explicit app_label and either isn't in an application in INSTALLED_APPS or else was imported before its application was loaded. This will no longer be supported in Django 1.9.\\n  class CourseMode(models.Model):\\n\\nParsing log file: access.log\\nWriting to file passed via parameter: /tmp/activity_report\",\r\n        \"stdout\": \"[149.202.188.116]\\ncourses = 1\\nhits = 4\\nusers = 7\",\r\n        \"stdout_lines\": [\r\n            \"[149.202.188.116]\",\r\n            \"courses = 1\",\r\n            \"hits = 4\",\r\n            \"users = 7\"\r\n        ],\r\n        \"warnings\": []\r\n    }\r\n}\r\n\r\nTASK [Fetch stats] *************************************************************\r\nchanged: [149.202.188.116]\r\n\r\nTASK [debug] *******************************************************************\r\nok: [149.202.188.116] => {\r\n    \"fetch_results\": {\r\n        \"changed\": true,\r\n        \"checksum\": \"932360475ce5553997c0c5848587113df8d66fae\",\r\n        \"dest\": \"/tmp/tmp5mdcxrap/149.202.188.116\",\r\n        \"md5sum\": \"22bee0d1341ccfe358eab3a86424b936\",\r\n        \"remote_checksum\": \"932360475ce5553997c0c5848587113df8d66fae\",\r\n        \"remote_md5sum\": null\r\n    }\r\n}\r\n\r\nPLAY RECAP *********************************************************************\r\n149.202.188.116            : ok=6    changed=2    unreachable=0    failed=0\r\n\r\nAppserver IP,Owner Emails,Unique Hits,Total Users,Total Courses,Age (Days)\r\n149.202.188.116,\"['brandon@opencraft.com', 'x@bdero.me']\",4,7,1,0\r\nDone generating CSV output.\r\n  ```\r\n\r\n  Note that the actual CSV output is written to stdout, while everything else is written to stderr. This makes the command directly machine readable.\r\n\r\n1. Run the command again, this time specifying an output file:\r\n\r\n  ```\r\nmake manage \"activity_csv --out out.csv\"\r\n  ```\r\n\r\n  **NOTE:** The double quotes are necessary here. Without them, the `--out` flag would be passed to make instead of `manage.py`.\r\n\r\n1. Verify that the results have been placed in `out.csv`:\r\n\r\n  ```\r\n$ cat out.csv \r\nAppserver IP,Owner Emails,Unique Hits,Total Users,Total Courses,Age (Days)\r\n149.202.188.116,\"['brandon@opencraft.com', 'x@bdero.me']\",4,7,1,0\r\n  ```","created_at":"2016-10-20T16:11:34Z","updated_at":"2016-10-21T19:53:56Z","closed_at":null,"merged_at":null,"merge_commit_sha":"19cb8f94fb30edb67dd24f4181e2a6c20036ad9e","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/open-craft/opencraft/pulls/144/commits","review_comments_url":"https://api.github.com/repos/open-craft/opencraft/pulls/144/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/144/comments","statuses_url":"https://api.github.com/repos/open-craft/opencraft/statuses/d748a0fd16f4068c630f0553733d6806798cd313","head":{"label":"open-craft:bdero/activity_csv","ref":"bdero/activity_csv","sha":"d748a0fd16f4068c630f0553733d6806798cd313","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-09-17T10:16:25Z","pushed_at":"2016-10-21T19:53:15Z","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":2411,"stargazers_count":13,"watchers_count":13,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":10,"mirror_url":null,"open_issues_count":12,"forks":10,"open_issues":12,"watchers":13,"default_branch":"master"}},"base":{"label":"open-craft:master","ref":"master","sha":"8506e884d2f429be877e276dbce5ca0595041cf5","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-09-17T10:16:25Z","pushed_at":"2016-10-21T19:53:15Z","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":2411,"stargazers_count":13,"watchers_count":13,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":10,"mirror_url":null,"open_issues_count":12,"forks":10,"open_issues":12,"watchers":13,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/144"},"html":{"href":"https://github.com/open-craft/opencraft/pull/144"},"issue":{"href":"https://api.github.com/repos/open-craft/opencraft/issues/144"},"comments":{"href":"https://api.github.com/repos/open-craft/opencraft/issues/144/comments"},"review_comments":{"href":"https://api.github.com/repos/open-craft/opencraft/pulls/144/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/144/commits"},"statuses":{"href":"https://api.github.com/repos/open-craft/opencraft/statuses/d748a0fd16f4068c630f0553733d6806798cd313"}}}}
{"actor":{"display_login":"bdero"}}