{"action":"created","comment":{"url":"https://api.github.com/repos/State/stacks/pulls/comments/32518576","id":32518576,"diff_hunk":"@@ -0,0 +1,333 @@\n+#!/usr/bin/env python3\n+\n+from __future__ import print_function\n+\n+import os\n+import sys\n+import yaml\n+import argparse\n+import json\n+import hashlib\n+import time\n+\n+import boto.ec2\n+import boto.vpc\n+import boto.route53\n+import boto.cloudformation\n+import boto.s3\n+import botocore.config\n+import jinja2\n+\n+from boto.exception import BotoServerError\n+from fnmatch import fnmatch\n+from tabulate import tabulate\n+\n+\n+# Resolve a basedir of this script\n+if os.path.islink(__file__):\n+    real_file = os.readlink(__file__)\n+    basedir = os.path.realpath(os.path.dirname(real_file))\n+else:\n+    basedir = os.path.realpath(os.path.dirname(__file__))\n+\n+\n+VERSION = '0.1'\n+CONFIG_FILE = basedir + '/config.yaml'\n+AWS_CONFIG_FILE = os.environ['HOME'] + '/.aws/credentials'\n+DEFAULT_PROFILE = 'default'\n+DEFAULT_REGION = 'us-east-1'\n+\n+\n+def _load_yaml(f):\n+    try:\n+        y = yaml.load(f)\n+        return y\n+    # TODO respond to exceptions\n+    except:\n+        return None\n+\n+\n+def _merge_config(config, env):\n+    '''Merges config common with environment specific section'''\n+    c = config['common'].copy()\n+    c.update(config[env])\n+    return c\n+\n+\n+# TODO(vaijab)\n+def _extract_error():\n+    '''Reads AWS API response and extracs the error message'''\n+    pass\n+\n+\n+# Decorator for retrying when AWS is throttling API calls\n+def throttling_retry(func):\n+    def retry_call(*args, **kwargs):\n+        retries = 0\n+        while True:\n+            try:\n+                retval = func(*args)\n+                return retval\n+            except BotoServerError as err:\n+                if err.code == 'Throttling' and retries <= 3:\n+                    sleep = 3 * (2**retries)\n+                    print('Being throttled. Retrying after {} seconds..'.format(sleep))\n+                    time.sleep(sleep)\n+                    retries += 1\n+                else:\n+                    raise err\n+    return retry_call\n+\n+\n+def _match_stack_name(name, pattern):\n+    return fnmatch(name, pattern)\n+\n+\n+def load_config(config_file, env):\n+    '''Loads stack configuration file'''\n+    with open(config_file) as f:\n+        c = _load_yaml(f)\n+        if c:\n+            return _merge_config(c, env)\n+        else:\n+            return {}\n+\n+\n+@throttling_retry\n+def get_region_name(profile, config_file=AWS_CONFIG_FILE):\n+    '''Return region name from AWS config file'''\n+    try:\n+        c = botocore.config.load_config(config_file)\n+        r = c.get(profile, {}).get('region', DEFAULT_REGION)\n+        return r\n+    except:\n+        raise RuntimeError('Failed loading config {}'.format(config_file))\n+\n+\n+@throttling_retry\n+def get_ami_id(conn, name):\n+    '''Returns the first AMI ID given its name'''\n+    images = conn.get_all_images(filters={'name': name})\n+    conn.close()\n+    if len(images) != 0:\n+        return images[0].id\n+    else:\n+        raise RuntimeError('{} AMI not found'.format(name))\n+\n+\n+@throttling_retry\n+def get_zone_id(conn, name):\n+    '''Returns the first Route53 zone ID given its name'''\n+    zone = conn.get_zone(name)\n+    conn.close()\n+    if zone:\n+        return zone.id\n+    else:\n+        raise RuntimeError('{} zone not found'.format(name))\n+\n+\n+@throttling_retry\n+def get_vpc_id(conn, name):\n+    '''Returns the first VPC ID given its name and region'''\n+    vpcs = conn.get_all_vpcs(filters={'tag:Name': name})\n+    conn.close()\n+    if len(vpcs) == 1:\n+        return vpcs[0].id\n+    else:\n+        raise RuntimeError('{} VPC not found'.format(name))\n+\n+\n+@throttling_retry\n+def get_stack_output(conn, name, key):\n+    '''Returns stack output key value'''\n+    result = conn.describe_stacks(name)\n+    if len(result) != 1:\n+        raise RuntimeError('{} stack not found'.format(name))\n+    outputs = [s.outputs for s in result][0]\n+    for output in outputs:\n+        if output.key == key:\n+            return output.value\n+    raise RuntimeError('{} output not found'.format(key))\n+\n+\n+@throttling_retry\n+def get_stack_tag(conn, name, tag):\n+    '''Returns stack tag'''\n+    result = conn.describe_stacks(name)\n+    if len(result) != 1:\n+        raise RuntimeError('{} stack not found'.format(name))\n+    tags = [s.tags for s in result][0]\n+    return tags.get(tag, '')\n+\n+\n+def gen_template(template, config, pretty=False):\n+    '''Return generated CloudFormation template string'''\n+    tpl = jinja2.Template(template.read())\n+    out = tpl.render(config)\n+    yaml_out = yaml.load(out)\n+    indent = 2 if pretty else None\n+    return json.dumps(yaml_out, indent=indent)\n+\n+\n+def list_stacks(conn, name_filter='*', verbose=False):\n+    '''Lists active stacks'''\n+    stacks_filters = [\n+        'CREATE_COMPLETE',\n+        'CREATE_IN_PROGRESS',\n+        'CREATE_FAILED',\n+        'DELETE_IN_PROGRESS',\n+        'DELETE_FAILED',\n+        'ROLLBACK_COMPLETE',\n+        'ROLLBACK_FAILED',\n+        'ROLLBACK_IN_PROGRESS',\n+        'UPDATE_COMPLETE',\n+        'UPDATE_IN_PROGRESS',\n+        'UPDATE_ROLLBACK_COMPLETE',\n+        'UPDATE_ROLLBACK_FAILED',\n+        'UPDATE_ROLLBACK_IN_PROGRESS',\n+    ]\n+\n+    s = conn.list_stacks(stacks_filters)\n+\n+    stacks = []\n+    for n in s:\n+        if name_filter and _match_stack_name(n.stack_name, name_filter):\n+            columns = [n.stack_name, n.stack_status]\n+            if verbose:\n+                env = get_stack_tag(conn, n.stack_name, 'Env')\n+                columns.append(env)\n+                columns.append(n.template_description)\n+            stacks.append(columns)\n+            columns = []\n+\n+    if len(stacks) >= 1:\n+        return tabulate(stacks, tablefmt='plain')\n+    else:\n+        return 'No stacks found'\n+\n+\n+def create_stack(conn, name, template, config, update=False, dry=False):\n+    '''Creates or updates CloudFormation stack from a jinja2 template'''\n+    tags = {\n+        'Env': config['env'],\n+    }\n+\n+    tpl = gen_template(template, config, dry)\n+    if dry:\n+        print(tpl)\n+        print(\"Template size: \" + str(len(tpl)), file=sys.stderr)\n+        return True\n+\n+    bucket_name = '{}-stacks-{}'.format(config['env'], config['region'])\n+    b = config['s3_conn'].get_bucket(bucket_name)\n+    h = hashlib.md5(tpl.encode('utf-8')).hexdigest()\n+    k = boto.s3.key.Key(b)\n+    k.key = '{}/{}/{}'.format(config['env'], name, h)\n+    k.set_contents_from_string(tpl)\n+    url = k.generate_url(expires_in=30)\n+\n+    try:\n+        if update:\n+            conn.update_stack(name, template_url=url, tags=tags, capabilities=['CAPABILITY_IAM'])\n+        else:\n+            conn.create_stack(name, template_url=url, tags=tags, capabilities=['CAPABILITY_IAM'])\n+    except BotoServerError as err:\n+        print(err)\n+\n+\n+def delete_stack(conn, name, region, profile):\n+    '''Deletes stack given its name'''\n+    msg = ('You are about to delete the following stack:\\n'\n+           'name: {}\\n'\n+           'region: {}\\n'\n+           'profile: {}\\n').format(name, region, profile)\n+    print(msg)\n+    response = input('Are you sure? [y/N] ')\n+    yes = ['y', 'Y', 'yes', 'YES', 'Yes']\n+    if response in yes:\n+        conn.delete_stack(name)\n+\n+\n+def main():\n+    parser = argparse.ArgumentParser()\n+    parser.add_argument('-p', '--profile', default=DEFAULT_PROFILE)\n+    parser.add_argument('-r', '--region')\n+    subparsers = parser.add_subparsers(title='available subcommands', dest='subcommand')\n+\n+    parser_list = subparsers.add_parser('list', help='List stacks')\n+    parser_list.add_argument('-v', '--verbose', action='store_true')\n+    parser_list.add_argument('name', default='*', nargs='?',\n+                             help='stack name or unix shell-style pattern')\n+\n+    parser_create = subparsers.add_parser('create', help='Create a new stack')\n+    parser_create.add_argument('-t', '--template', required=True, type=argparse.FileType())\n+    parser_create.add_argument('name')\n+    parser_create.add_argument('-e', '--env', required=True)\n+    parser_create.add_argument('-d', '--dry-run', action='store_true')\n+\n+    parser_update = subparsers.add_parser('update', help='Update an existing stack')\n+    parser_update.add_argument('-t', '--template', required=True, type=argparse.FileType())\n+    parser_update.add_argument('name')\n+    parser_update.add_argument('-e', '--env', required=True)\n+    parser_update.add_argument('-d', '--dry-run', action='store_true')\n+\n+    parser_delete = subparsers.add_parser('delete', help='Delete an existing stack')\n+    parser_delete.add_argument('name')\n+\n+    args = parser.parse_args()\n+\n+    if not args.subcommand:\n+        parser.print_help()\n+\n+    # Set region name from cli arg or aws config file\n+    if args.region:\n+        region = args.region\n+    elif get_region_name(args.profile):\n+        region = get_region_name(args.profile)\n+\n+    if args.subcommand == 'list':\n+        conn = boto.cloudformation.connect_to_region(region, profile_name=args.profile)\n+        print(list_stacks(conn, args.name, args.verbose))\n+        conn.close()\n+\n+    if args.subcommand == 'create' or args.subcommand == 'update':\n+        ec2_conn = boto.ec2.connect_to_region(region, profile_name=args.profile)\n+        vpc_conn = boto.vpc.connect_to_region(region, profile_name=args.profile)\n+        cf_conn = boto.cloudformation.connect_to_region(region, profile_name=args.profile)\n+        r53_conn = boto.route53.connect_to_region(region, profile_name=args.profile)\n+        s3_conn = boto.s3.connect_to_region(region, profile_name=args.profile)\n+        config = load_config('config.yaml', args.env)\n+        config['name'] = args.name\n+        config['env'] = args.env\n+        config['region'] = region\n+        config['profile'] = args.profile\n+        config['ec2_conn'] = ec2_conn\n+        config['vpc_conn'] = vpc_conn\n+        config['cf_conn'] = cf_conn\n+        config['r53_conn'] = r53_conn\n+        config['s3_conn'] = s3_conn\n+        config['get_ami_id'] = get_ami_id\n+        config['get_vpc_id'] = get_vpc_id\n+        config['get_zone_id'] = get_zone_id\n+        config['get_stack_output'] = get_stack_output\n+        if args.subcommand == 'create':\n+            if not args.name.startswith(args.env):\n+                msg = 'Stack name does not begin with env name. Are you sure?? [y/N] '\n+                response = input(msg)\n+                yes = ['y', 'Y', 'yes', 'YES', 'Yes']\n+                if response in yes:\n+                    create_stack(cf_conn, args.name, args.template, config, dry=args.dry_run)\n+            else:\n+                create_stack(cf_conn, args.name, args.template, config, dry=args.dry_run)","path":"bin/stacks","position":321,"original_position":321,"commit_id":"cd42ad08cf4db1553d896b065c3d1b1a827519fe","original_commit_id":"cd42ad08cf4db1553d896b065c3d1b1a827519fe","user":{"login":"danharvey","id":249298,"avatar_url":"https://avatars.githubusercontent.com/u/249298?v=3","gravatar_id":"","url":"https://api.github.com/users/danharvey","html_url":"https://github.com/danharvey","followers_url":"https://api.github.com/users/danharvey/followers","following_url":"https://api.github.com/users/danharvey/following{/other_user}","gists_url":"https://api.github.com/users/danharvey/gists{/gist_id}","starred_url":"https://api.github.com/users/danharvey/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/danharvey/subscriptions","organizations_url":"https://api.github.com/users/danharvey/orgs","repos_url":"https://api.github.com/users/danharvey/repos","events_url":"https://api.github.com/users/danharvey/events{/privacy}","received_events_url":"https://api.github.com/users/danharvey/received_events","type":"User","site_admin":false},"body":"just put this once, and return above if it's not satisfied?","created_at":"2015-06-16T13:11:20Z","updated_at":"2015-06-16T13:11:20Z","html_url":"https://github.com/State/stacks/pull/1#discussion_r32518576","pull_request_url":"https://api.github.com/repos/State/stacks/pulls/1","_links":{"self":{"href":"https://api.github.com/repos/State/stacks/pulls/comments/32518576"},"html":{"href":"https://github.com/State/stacks/pull/1#discussion_r32518576"},"pull_request":{"href":"https://api.github.com/repos/State/stacks/pulls/1"}}},"pull_request":{"url":"https://api.github.com/repos/State/stacks/pulls/1","id":37695699,"html_url":"https://github.com/State/stacks/pull/1","diff_url":"https://github.com/State/stacks/pull/1.diff","patch_url":"https://github.com/State/stacks/pull/1.patch","issue_url":"https://api.github.com/repos/State/stacks/issues/1","number":1,"state":"open","locked":false,"title":"Adding initial code","user":{"login":"vaijab","id":1308018,"avatar_url":"https://avatars.githubusercontent.com/u/1308018?v=3","gravatar_id":"","url":"https://api.github.com/users/vaijab","html_url":"https://github.com/vaijab","followers_url":"https://api.github.com/users/vaijab/followers","following_url":"https://api.github.com/users/vaijab/following{/other_user}","gists_url":"https://api.github.com/users/vaijab/gists{/gist_id}","starred_url":"https://api.github.com/users/vaijab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/vaijab/subscriptions","organizations_url":"https://api.github.com/users/vaijab/orgs","repos_url":"https://api.github.com/users/vaijab/repos","events_url":"https://api.github.com/users/vaijab/events{/privacy}","received_events_url":"https://api.github.com/users/vaijab/received_events","type":"User","site_admin":false},"body":"","created_at":"2015-06-15T14:16:10Z","updated_at":"2015-06-16T13:11:20Z","closed_at":null,"merged_at":null,"merge_commit_sha":"02b072913d7d0ef09bd3bc298287279dcbf54923","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/State/stacks/pulls/1/commits","review_comments_url":"https://api.github.com/repos/State/stacks/pulls/1/comments","review_comment_url":"https://api.github.com/repos/State/stacks/pulls/comments{/number}","comments_url":"https://api.github.com/repos/State/stacks/issues/1/comments","statuses_url":"https://api.github.com/repos/State/stacks/statuses/cd42ad08cf4db1553d896b065c3d1b1a827519fe","head":{"label":"vaijab:initial","ref":"initial","sha":"cd42ad08cf4db1553d896b065c3d1b1a827519fe","user":{"login":"vaijab","id":1308018,"avatar_url":"https://avatars.githubusercontent.com/u/1308018?v=3","gravatar_id":"","url":"https://api.github.com/users/vaijab","html_url":"https://github.com/vaijab","followers_url":"https://api.github.com/users/vaijab/followers","following_url":"https://api.github.com/users/vaijab/following{/other_user}","gists_url":"https://api.github.com/users/vaijab/gists{/gist_id}","starred_url":"https://api.github.com/users/vaijab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/vaijab/subscriptions","organizations_url":"https://api.github.com/users/vaijab/orgs","repos_url":"https://api.github.com/users/vaijab/repos","events_url":"https://api.github.com/users/vaijab/events{/privacy}","received_events_url":"https://api.github.com/users/vaijab/received_events","type":"User","site_admin":false},"repo":{"id":37316760,"name":"stacks","full_name":"vaijab/stacks","owner":{"login":"vaijab","id":1308018,"avatar_url":"https://avatars.githubusercontent.com/u/1308018?v=3","gravatar_id":"","url":"https://api.github.com/users/vaijab","html_url":"https://github.com/vaijab","followers_url":"https://api.github.com/users/vaijab/followers","following_url":"https://api.github.com/users/vaijab/following{/other_user}","gists_url":"https://api.github.com/users/vaijab/gists{/gist_id}","starred_url":"https://api.github.com/users/vaijab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/vaijab/subscriptions","organizations_url":"https://api.github.com/users/vaijab/orgs","repos_url":"https://api.github.com/users/vaijab/repos","events_url":"https://api.github.com/users/vaijab/events{/privacy}","received_events_url":"https://api.github.com/users/vaijab/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/vaijab/stacks","description":"Manage Cloud Formation sanely with templates written in YAML","fork":true,"url":"https://api.github.com/repos/vaijab/stacks","forks_url":"https://api.github.com/repos/vaijab/stacks/forks","keys_url":"https://api.github.com/repos/vaijab/stacks/keys{/key_id}","collaborators_url":"https://api.github.com/repos/vaijab/stacks/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/vaijab/stacks/teams","hooks_url":"https://api.github.com/repos/vaijab/stacks/hooks","issue_events_url":"https://api.github.com/repos/vaijab/stacks/issues/events{/number}","events_url":"https://api.github.com/repos/vaijab/stacks/events","assignees_url":"https://api.github.com/repos/vaijab/stacks/assignees{/user}","branches_url":"https://api.github.com/repos/vaijab/stacks/branches{/branch}","tags_url":"https://api.github.com/repos/vaijab/stacks/tags","blobs_url":"https://api.github.com/repos/vaijab/stacks/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/vaijab/stacks/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/vaijab/stacks/git/refs{/sha}","trees_url":"https://api.github.com/repos/vaijab/stacks/git/trees{/sha}","statuses_url":"https://api.github.com/repos/vaijab/stacks/statuses/{sha}","languages_url":"https://api.github.com/repos/vaijab/stacks/languages","stargazers_url":"https://api.github.com/repos/vaijab/stacks/stargazers","contributors_url":"https://api.github.com/repos/vaijab/stacks/contributors","subscribers_url":"https://api.github.com/repos/vaijab/stacks/subscribers","subscription_url":"https://api.github.com/repos/vaijab/stacks/subscription","commits_url":"https://api.github.com/repos/vaijab/stacks/commits{/sha}","git_commits_url":"https://api.github.com/repos/vaijab/stacks/git/commits{/sha}","comments_url":"https://api.github.com/repos/vaijab/stacks/comments{/number}","issue_comment_url":"https://api.github.com/repos/vaijab/stacks/issues/comments{/number}","contents_url":"https://api.github.com/repos/vaijab/stacks/contents/{+path}","compare_url":"https://api.github.com/repos/vaijab/stacks/compare/{base}...{head}","merges_url":"https://api.github.com/repos/vaijab/stacks/merges","archive_url":"https://api.github.com/repos/vaijab/stacks/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/vaijab/stacks/downloads","issues_url":"https://api.github.com/repos/vaijab/stacks/issues{/number}","pulls_url":"https://api.github.com/repos/vaijab/stacks/pulls{/number}","milestones_url":"https://api.github.com/repos/vaijab/stacks/milestones{/number}","notifications_url":"https://api.github.com/repos/vaijab/stacks/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/vaijab/stacks/labels{/name}","releases_url":"https://api.github.com/repos/vaijab/stacks/releases{/id}","created_at":"2015-06-12T10:46:13Z","updated_at":"2015-06-12T10:34:34Z","pushed_at":"2015-06-15T14:15:45Z","git_url":"git://github.com/vaijab/stacks.git","ssh_url":"git@github.com:vaijab/stacks.git","clone_url":"https://github.com/vaijab/stacks.git","svn_url":"https://github.com/vaijab/stacks","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"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":"State:master","ref":"master","sha":"92ae5a6f261c81276e702e8f9eadb8c0c208c98b","user":{"login":"State","id":696112,"avatar_url":"https://avatars.githubusercontent.com/u/696112?v=3","gravatar_id":"","url":"https://api.github.com/users/State","html_url":"https://github.com/State","followers_url":"https://api.github.com/users/State/followers","following_url":"https://api.github.com/users/State/following{/other_user}","gists_url":"https://api.github.com/users/State/gists{/gist_id}","starred_url":"https://api.github.com/users/State/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/State/subscriptions","organizations_url":"https://api.github.com/users/State/orgs","repos_url":"https://api.github.com/users/State/repos","events_url":"https://api.github.com/users/State/events{/privacy}","received_events_url":"https://api.github.com/users/State/received_events","type":"Organization","site_admin":false},"repo":{"id":37316252,"name":"stacks","full_name":"State/stacks","owner":{"login":"State","id":696112,"avatar_url":"https://avatars.githubusercontent.com/u/696112?v=3","gravatar_id":"","url":"https://api.github.com/users/State","html_url":"https://github.com/State","followers_url":"https://api.github.com/users/State/followers","following_url":"https://api.github.com/users/State/following{/other_user}","gists_url":"https://api.github.com/users/State/gists{/gist_id}","starred_url":"https://api.github.com/users/State/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/State/subscriptions","organizations_url":"https://api.github.com/users/State/orgs","repos_url":"https://api.github.com/users/State/repos","events_url":"https://api.github.com/users/State/events{/privacy}","received_events_url":"https://api.github.com/users/State/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/State/stacks","description":"Manage Cloud Formation sanely with templates written in YAML","fork":false,"url":"https://api.github.com/repos/State/stacks","forks_url":"https://api.github.com/repos/State/stacks/forks","keys_url":"https://api.github.com/repos/State/stacks/keys{/key_id}","collaborators_url":"https://api.github.com/repos/State/stacks/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/State/stacks/teams","hooks_url":"https://api.github.com/repos/State/stacks/hooks","issue_events_url":"https://api.github.com/repos/State/stacks/issues/events{/number}","events_url":"https://api.github.com/repos/State/stacks/events","assignees_url":"https://api.github.com/repos/State/stacks/assignees{/user}","branches_url":"https://api.github.com/repos/State/stacks/branches{/branch}","tags_url":"https://api.github.com/repos/State/stacks/tags","blobs_url":"https://api.github.com/repos/State/stacks/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/State/stacks/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/State/stacks/git/refs{/sha}","trees_url":"https://api.github.com/repos/State/stacks/git/trees{/sha}","statuses_url":"https://api.github.com/repos/State/stacks/statuses/{sha}","languages_url":"https://api.github.com/repos/State/stacks/languages","stargazers_url":"https://api.github.com/repos/State/stacks/stargazers","contributors_url":"https://api.github.com/repos/State/stacks/contributors","subscribers_url":"https://api.github.com/repos/State/stacks/subscribers","subscription_url":"https://api.github.com/repos/State/stacks/subscription","commits_url":"https://api.github.com/repos/State/stacks/commits{/sha}","git_commits_url":"https://api.github.com/repos/State/stacks/git/commits{/sha}","comments_url":"https://api.github.com/repos/State/stacks/comments{/number}","issue_comment_url":"https://api.github.com/repos/State/stacks/issues/comments{/number}","contents_url":"https://api.github.com/repos/State/stacks/contents/{+path}","compare_url":"https://api.github.com/repos/State/stacks/compare/{base}...{head}","merges_url":"https://api.github.com/repos/State/stacks/merges","archive_url":"https://api.github.com/repos/State/stacks/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/State/stacks/downloads","issues_url":"https://api.github.com/repos/State/stacks/issues{/number}","pulls_url":"https://api.github.com/repos/State/stacks/pulls{/number}","milestones_url":"https://api.github.com/repos/State/stacks/milestones{/number}","notifications_url":"https://api.github.com/repos/State/stacks/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/State/stacks/labels{/name}","releases_url":"https://api.github.com/repos/State/stacks/releases{/id}","created_at":"2015-06-12T10:32:23Z","updated_at":"2015-06-12T10:34:34Z","pushed_at":"2015-06-15T14:16:10Z","git_url":"git://github.com/State/stacks.git","ssh_url":"git@github.com:State/stacks.git","clone_url":"https://github.com/State/stacks.git","svn_url":"https://github.com/State/stacks","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":1,"forks":1,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/State/stacks/pulls/1"},"html":{"href":"https://github.com/State/stacks/pull/1"},"issue":{"href":"https://api.github.com/repos/State/stacks/issues/1"},"comments":{"href":"https://api.github.com/repos/State/stacks/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/State/stacks/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/State/stacks/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/State/stacks/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/State/stacks/statuses/cd42ad08cf4db1553d896b065c3d1b1a827519fe"}}}}