{"action":"created","comment":{"url":"https://api.github.com/repos/boutiques/boutiques/pulls/comments/172022260","pull_request_review_id":100980469,"id":172022260,"diff_hunk":"@@ -277,6 +281,37 @@ def evaluate(*params):\n     return query_results[0] if len(query_results) == 1 else query_results\n \n \n+def test(*params):\n+\n+    parser = ArgumentParser(\"Perform all the tests defined within the given descriptor\")\n+    parser.add_argument(\"descriptor\", action=\"store\", help=\"The Boutiques descriptor.\")\n+    result = parser.parse_args(params)\n+\n+    # Generation of the invocation schema (and descriptor validation).\n+    invocation(result.descriptor)\n+    \n+    # Extraction of all the invocations defined for the test-cases.\n+    descriptor = json.loads(open(result.descriptor).read())\n+    \n+    if (not descriptor.get(\"tests\")):\n+        raise SystemExit(\"No test found in descriptor\")\n+    \n+    for test in descriptor[\"tests\"]:\n+        # Create temporary file for the invocation() function.\n+        invocation_JSON = test[\"invocation\"];\n+        temp_invocation_JSON = tempfile.NamedTemporaryFile(suffix=\".json\", delete=False)\n+        temp_invocation_JSON.write(json.dumps(invocation_JSON).encode())\n+        temp_invocation_JSON.seek(0)","path":"tools/python/boutiques/bosh.py","position":59,"original_position":59,"commit_id":"6181b64373fad189d19b1c2cd4b4ab941703535a","original_commit_id":"6181b64373fad189d19b1c2cd4b4ab941703535a","user":{"login":"gkiar","id":4883288,"avatar_url":"https://avatars3.githubusercontent.com/u/4883288?v=4","gravatar_id":"","url":"https://api.github.com/users/gkiar","html_url":"https://github.com/gkiar","followers_url":"https://api.github.com/users/gkiar/followers","following_url":"https://api.github.com/users/gkiar/following{/other_user}","gists_url":"https://api.github.com/users/gkiar/gists{/gist_id}","starred_url":"https://api.github.com/users/gkiar/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gkiar/subscriptions","organizations_url":"https://api.github.com/users/gkiar/orgs","repos_url":"https://api.github.com/users/gkiar/repos","events_url":"https://api.github.com/users/gkiar/events{/privacy}","received_events_url":"https://api.github.com/users/gkiar/received_events","type":"User","site_admin":false},"body":"@Trajan64 no problem - you don't need to implement this in this PR. I have done something similar to this before, so will take a crack at it myself later on. I suspect your assessment is correct, but that there are simplification we can make to still have this work for us (i.e. wrap it as a string as you suggest, etc.)..\r\n\r\nI mentioned to @glatard that I've made plans to overhaul the API organization a bit again with some recent lessons learned, and will do this then. So as @glatard suggested upstream here, please ignore this suggestion in the PR, and let us know when all other suggested changes are done.\r\n\r\nCheers!","created_at":"2018-03-03T17:01:11Z","updated_at":"2018-03-03T17:01:11Z","html_url":"https://github.com/boutiques/boutiques/pull/214#discussion_r172022260","pull_request_url":"https://api.github.com/repos/boutiques/boutiques/pulls/214","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/boutiques/boutiques/pulls/comments/172022260"},"html":{"href":"https://github.com/boutiques/boutiques/pull/214#discussion_r172022260"},"pull_request":{"href":"https://api.github.com/repos/boutiques/boutiques/pulls/214"}},"in_reply_to_id":170088269},"pull_request":{"url":"https://api.github.com/repos/boutiques/boutiques/pulls/214","id":168927888,"html_url":"https://github.com/boutiques/boutiques/pull/214","diff_url":"https://github.com/boutiques/boutiques/pull/214.diff","patch_url":"https://github.com/boutiques/boutiques/pull/214.patch","issue_url":"https://api.github.com/repos/boutiques/boutiques/issues/214","number":214,"state":"open","locked":false,"title":"boutiques with tests","user":{"login":"Trajan64","id":16823814,"avatar_url":"https://avatars3.githubusercontent.com/u/16823814?v=4","gravatar_id":"","url":"https://api.github.com/users/Trajan64","html_url":"https://github.com/Trajan64","followers_url":"https://api.github.com/users/Trajan64/followers","following_url":"https://api.github.com/users/Trajan64/following{/other_user}","gists_url":"https://api.github.com/users/Trajan64/gists{/gist_id}","starred_url":"https://api.github.com/users/Trajan64/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Trajan64/subscriptions","organizations_url":"https://api.github.com/users/Trajan64/orgs","repos_url":"https://api.github.com/users/Trajan64/repos","events_url":"https://api.github.com/users/Trajan64/events{/privacy}","received_events_url":"https://api.github.com/users/Trajan64/received_events","type":"User","site_admin":false},"body":"# Descriptions and usage #\r\n\r\nBoutiques will now be able to automatically perform a series of tests that are defined within a descriptor.\r\n\r\nTo try it out, simply type `bosh test <path to descriptor>` in a console.\r\n\r\nNote that the provided descriptor must be augmented with the definition of a new property `tests`. \r\nSee the following section to know how to define it\r\n\r\nAlso, note that:\r\n- Sample JSON descriptors can be fond under `boutiques/tools/python/boutiques/schema/examples/` with names starting with tests.\r\n- There is a test defined to automatically check the validity of this feature. It is located in `boutiques/tools/python/boutiques/test/ as test_test.py`\r\nTo perform those tests, a docker image must be mounted. It is the exact same image as the one needed for `test_exemple1.py`\r\nYou shall set it using the following command:\r\n`docker build -t boutiques/example1:test ./tools/python/boutiques/schema/examples/example1`\r\n\r\n## Structure of the `tests` property ##\r\n\r\nThe schema was augmented with the introduction of a new `tests` property. This property is an array which hold `test` objects that represent individual test that we wish to perform when using bosh test with the descriptor.\r\n\r\nA test may be defined with the following items:\r\n- A `name` (It has to be unique, no others tests may share the same name)\r\n- An `invocation` object which represents the arguments for the pipeline program.\r\n- An `assertion` object. It defines what pytest will have to check to figure out if the test passed or fail.\r\nThere are two items defined (at least one of those items has to be included in the descriptor) within an assertion object:\r\n    - An `exit-code`, which is an integer representing the expected returned value by the pipeline.\r\n    - An `output-files` item that represent an array of output-file objects, that have the following properties:\r\n        - An id that refer to an output object. An `output` object, in this context, is an object held by the `output-files` array _which in turns, is defined at the top-level of the descriptor_\r\n        - (Optional) A `reference` path that points to a file. The content of this file will be checked against the content of the file pointed to by the `id` property of the associated object.\r\n\r\nIn short, in regards to assertions, boutiques will do either, or both:\r\n- Assert on the exit-code, if one was defined.\r\n- Check each of the output-files defined in the array. More specifically, it will:\r\n (1) Check if the output file was indeed produced by the pipeline\r\n (2) If that is the case, and if a reference path was included, check if the output file's content matches the content of the file pointed by the reference path.\r\n\r\nAlso note that `name`, `invocation` and `assertion` are all required properties of a `test` object.\r\n\r\nThis is what the `tests` property looks like inside the JSON schema:\r\n\r\n```\r\n        [...]\r\n        \"tests\": {\r\n            \"id\": \"http://github.com/boutiques/boutiques-schema/tests\",\r\n            \"type\": \"array\",\r\n            \"minItems\": 1,\r\n            \"items\": {\r\n                \"id\": \"http://github.com/boutiques/boutiques-schema/tests/test-case\",\r\n                \"type\": \"object\",\r\n                \"properties\": {\r\n                    \"name\": {\r\n                        \"id\": \"http://github.com/boutiques/boutiques-schema/tests/test-case/name\",\r\n                        \"minLength\": 1,\r\n                        \"description\": \"Name of the test-case\",\r\n                        \"type\": \"string\"\r\n                    },\r\n                    \"invocation\": {\r\n                        \"id\": \"http://github.com/boutiques/boutiques-schema/tests/test-case/invocation\",\r\n                        \"type\": \"object\"\r\n                    },\r\n                    \"assertions\": {\r\n                        \"id\": \"http://github.com/boutiques/boutiques-schema/tests/test-case/assertions\",\r\n                        \"type\": \"object\",\r\n                        \"properties\": {\r\n                            \"exit-code\": {\r\n                                \"id\": \"http://github.com/boutiques/boutiques-schema/tests/test-case/assertions/exit-code\",\r\n                                \"description\": \"Expected code returned by the program.\",\r\n                                \"type\": \"integer\"\r\n                            },\r\n                            \"output-files\": {\r\n                                \"id\": \"http://github.com/boutiques/boutiques-schema/tests/test-case/assertions/out-files\",\r\n                                \"type\": \"array\",\r\n                                \"minItems\": 1,\r\n                                \"items\": {\r\n                                    \"id\": \"http://github.com/boutiques/boutiques-schema/test-case/assertions/out-files/output\",\r\n                                    \"type\": \"object\",\r\n                                    \"properties\": {\r\n                                        \"id\": {\r\n                                            \"id\": \"http://github.com/boutiques/boutiques-schema/tests/test-case/assertions/out-files/output/id\",\r\n                                            \"description\": \"Id refering to an output-file\",\r\n                                            \"minLength\": 1,\r\n                                            \"pattern\": \"^[0-9,_,a-z,A-Z]*$\",\r\n                                            \"type\": \"string\"\r\n                                        },\r\n                                        \"reference\": {\r\n                                            \"id\": \"http://github.com/boutiques/boutiques-schema/tests/test-case/assertions/out-files/output/reference\",\r\n                                            \"description\": \"Path to a file whose content will be compared against the outputed file matching the associated id\",\r\n                                            \"minLength\": 1,\r\n                                            \"type\": \"string\"\r\n                                        }\r\n                                    },\r\n                                \"required\": [\r\n                                    \"id\"\r\n                                ]\r\n                                }\r\n                          }\r\n                        },\r\n                        \"anyOf\": [\r\n                          {\r\n                            \"required\": [\r\n                              \"exit-code\"\r\n                            ]\r\n                          },\r\n                          {\r\n                            \"required\": [\r\n                              \"output-files\"\r\n                            ]\r\n                          }\r\n                        ]\r\n                    }\r\n                },\r\n                \"required\": [\r\n                  \"name\",\r\n                  \"assertions\",\r\n\t\t  \"invocation\"\r\n                ]\r\n            }\r\n        },\r\n        [...]\r\n```\r\n\r\n \r\n# Implementation details #\r\n\r\n## The test engine ##\r\n\r\nTo allow a variable number of tests to be carried out by pytest, the new feature introduce 2 new files: `conftest.py` and `test.py`\r\n`conftest.py` is a script that is meant to be executed by pytest before looking for tests to run (which in this case is just `test.py`).\r\nThe script forces pytest to call the test function within test.py, with different parameters every time.\r\nEach call will provide enough information as parameters for the function, so that it can execute the pipeline and know how the subsequent results are expected to be asserted.\r\n\r\n### `test.py` ###\r\n\r\n`test.py` defines a function which, as stated in the section above, is called by pytest for each of the tests that are defined in the descriptor.\r\nThe function takes 3 parameters: A descriptor, the related test item from the descriptor and an invocation file.\r\n\r\nThe function first runs the pipeline with `bosh exec`, using both the descriptor and the invocation provided as parameters.\r\nSubsequently, the function will then begin asserting.\r\nIt will first look if an exit-code was specified in the descriptor, and if such is the case, look at the captured exit code value to see if it matches that value inside the `test` JSON item provided as argument.\r\nIf `output-file` objects were provided in the descriptor, the function will iterate on each of them, and for each:\r\n- ~~It will use a `LocalExecutor` instance to obtain a dictionary of output file ids mapped to actual file paths. **(NOTE: This is meant to go. A subsequent PR will use `bosh evaluate` instead)**.~~\r\n- It will translate the ouput file id to an actual file path through a dictionary that was previously returned by a call to `bosh evaluate`.\r\n- Assert on that the `output-file` object's `id` from the descriptor is found among the dictionary mentioned above.\r\n- If a reference was specified, the function will:\r\n    -- Assert on the actual existence of the file pointed by it.\r\n    -- Compute the MD5 of both the output files and the reference files and assert on their equality.\r\n\r\n\r\n## Modifications ##\r\n\r\n### `bosh.py` ###\r\n\r\nA method `test` was introduced to `bosh.py`. The function checks the descriptor provided as argument and prepare the call to `pytest`. In more details, the function will:\r\n- Validate the descriptor and generate the invocation schema, all through one call to the invocation function.\r\n- Generate the tests's invocations into temporary files and verify their validity through calls to the invocation function.\r\n- Call `pytest` with the descriptor as parameter.\r\nNOTE: There is room for improvement on that point. bosh.execute and bosh.invocation should be able to take objects as arguments and not just files. That would remove the need to rely on temporary files. Also, the process of generating temporary files for those same reasons is carried out in `conftest.py` too.\r\n\r\nAdditionally, bosh exec had its return statement modified. The function now returns a tuple: `(exit-code, stdout, stderr)`. It also print each of those elements onto `stdout` before returning.\r\nNote also, that this applies for `bosh exec -launch`. `bosh exec -simulate` now only returns `(0, \"\", \"\")`.\r\nThose modifications were made because being able to isolate the exit-code is a needed feature for `test.py`.\r\n\r\n### `validation.py` ###\r\n`validation.py` has been modified so as to include validating operations on the 'tests' property.\r\nWith the modifications made, there are 3 new possible errors that may arise during (the thorough) descriptor validation:\r\n. Two or more tests share the same names.\r\n. Two or more output-file (within one defined test) share the same output id.\r\n. A test output-file refer to an invalid id (that is, there are no 'output' objects that have this id)\r\n\r\n### ~~`localExec.py`~~ ###\r\n~~Disregard the changes made to that file. I will use `bosh evaluate` instead.\r\nBasically I needed a way to obtain a dictionary mapping output ids to file paths and I didn't think bosh provided this exact feature.~~\r\nChanges applied; rolled back to original version.\r\n\r\n## The tests (for this feature) ##\r\nThis new feature introduce a new testing file inside the `boutiques/tools/python/boutiques/tests/` directory. This file is (unoriginally) called `test_test.py`, and define 5 tests of different nature.\r\nAll those tests use the `exampleTool1.py` script, used in other previously defined tests such as `test_example1.py`.\r\nIn addition to retaking this sample pipeline, the script also retake the descriptors found in `boutiques/tools/python/boutiques/schema/examples/example1/example1.json`.\r\nThe JSON file remains the same, except that it is augmented with the new property 'tests', for which its definition is different for each of the (actual) tests.\r\n\r\n- The first test is the only one that is meant to pass sucessfully. It defines 3 `test` objects, with various assertions strategies\r\n(found in `boutiques/tools/python/boutiques/schema/example/tests_good.json`):\r\n    - The first `test` object states that the assertions must be made on an exit-code of 0 and on the verification of the existence of a an output file. If it exists, the `test` object also state that an assertion must be made to check if its content matches the content of another file (`boutiques/tools/python/boutiques/schema/examples/ref_log.txt`)\r\n    - The second `test` object specifies only an assertion on the exit-code (which is 0, again).\r\n    - The third `test` object stipulates that the assertions must be made on the existence of two different output files (and no reference defined this time, so no content matching is required).\r\n\r\n- The second test checks how bosh behave when given a descriptor with an invalid `tests` property (`boutiques/tools/python/boutiques/schema/examples/tests_invalid.json`)\r\nIt verifies that `bosh` correctly raises a `ValidationError`, and that the expected error messages are found in the exception's associated message.\r\n\r\n- The third, fourth and fifth tests are all meant to fail during the assertions made by `test.py`.\r\nRespectively, they test that an assertion failure happen when there is:\r\n    - A mismatch between actual and expected exit-code (`boutiques/tools/python/boutiques/schema/examples/tests_failure_exitcode.json`)\r\n    - A missing output-file (`boutiques/tools/python/boutiques/schema/examples/tests_failure_output_id.json`)\r\n    - A MD5 mismatch between an output-file and its expected content (`boutiques/tools/python/boutiques/schema/examples/tests_failure_reference.json`)","created_at":"2018-02-13T18:57:23Z","updated_at":"2018-03-03T17:01:11Z","closed_at":null,"merged_at":null,"merge_commit_sha":"ffff83ad4f3513710febabafd6ed4df2228d8e0a","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/boutiques/boutiques/pulls/214/commits","review_comments_url":"https://api.github.com/repos/boutiques/boutiques/pulls/214/comments","review_comment_url":"https://api.github.com/repos/boutiques/boutiques/pulls/comments{/number}","comments_url":"https://api.github.com/repos/boutiques/boutiques/issues/214/comments","statuses_url":"https://api.github.com/repos/boutiques/boutiques/statuses/6181b64373fad189d19b1c2cd4b4ab941703535a","head":{"label":"Trajan64:master","ref":"master","sha":"6181b64373fad189d19b1c2cd4b4ab941703535a","user":{"login":"Trajan64","id":16823814,"avatar_url":"https://avatars3.githubusercontent.com/u/16823814?v=4","gravatar_id":"","url":"https://api.github.com/users/Trajan64","html_url":"https://github.com/Trajan64","followers_url":"https://api.github.com/users/Trajan64/followers","following_url":"https://api.github.com/users/Trajan64/following{/other_user}","gists_url":"https://api.github.com/users/Trajan64/gists{/gist_id}","starred_url":"https://api.github.com/users/Trajan64/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Trajan64/subscriptions","organizations_url":"https://api.github.com/users/Trajan64/orgs","repos_url":"https://api.github.com/users/Trajan64/repos","events_url":"https://api.github.com/users/Trajan64/events{/privacy}","received_events_url":"https://api.github.com/users/Trajan64/received_events","type":"User","site_admin":false},"repo":{"id":118676495,"name":"boutiques","full_name":"Trajan64/boutiques","owner":{"login":"Trajan64","id":16823814,"avatar_url":"https://avatars3.githubusercontent.com/u/16823814?v=4","gravatar_id":"","url":"https://api.github.com/users/Trajan64","html_url":"https://github.com/Trajan64","followers_url":"https://api.github.com/users/Trajan64/followers","following_url":"https://api.github.com/users/Trajan64/following{/other_user}","gists_url":"https://api.github.com/users/Trajan64/gists{/gist_id}","starred_url":"https://api.github.com/users/Trajan64/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Trajan64/subscriptions","organizations_url":"https://api.github.com/users/Trajan64/orgs","repos_url":"https://api.github.com/users/Trajan64/repos","events_url":"https://api.github.com/users/Trajan64/events{/privacy}","received_events_url":"https://api.github.com/users/Trajan64/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/Trajan64/boutiques","description":"JSON schema and tools for boutiques application descriptors.","fork":true,"url":"https://api.github.com/repos/Trajan64/boutiques","forks_url":"https://api.github.com/repos/Trajan64/boutiques/forks","keys_url":"https://api.github.com/repos/Trajan64/boutiques/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Trajan64/boutiques/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Trajan64/boutiques/teams","hooks_url":"https://api.github.com/repos/Trajan64/boutiques/hooks","issue_events_url":"https://api.github.com/repos/Trajan64/boutiques/issues/events{/number}","events_url":"https://api.github.com/repos/Trajan64/boutiques/events","assignees_url":"https://api.github.com/repos/Trajan64/boutiques/assignees{/user}","branches_url":"https://api.github.com/repos/Trajan64/boutiques/branches{/branch}","tags_url":"https://api.github.com/repos/Trajan64/boutiques/tags","blobs_url":"https://api.github.com/repos/Trajan64/boutiques/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Trajan64/boutiques/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Trajan64/boutiques/git/refs{/sha}","trees_url":"https://api.github.com/repos/Trajan64/boutiques/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Trajan64/boutiques/statuses/{sha}","languages_url":"https://api.github.com/repos/Trajan64/boutiques/languages","stargazers_url":"https://api.github.com/repos/Trajan64/boutiques/stargazers","contributors_url":"https://api.github.com/repos/Trajan64/boutiques/contributors","subscribers_url":"https://api.github.com/repos/Trajan64/boutiques/subscribers","subscription_url":"https://api.github.com/repos/Trajan64/boutiques/subscription","commits_url":"https://api.github.com/repos/Trajan64/boutiques/commits{/sha}","git_commits_url":"https://api.github.com/repos/Trajan64/boutiques/git/commits{/sha}","comments_url":"https://api.github.com/repos/Trajan64/boutiques/comments{/number}","issue_comment_url":"https://api.github.com/repos/Trajan64/boutiques/issues/comments{/number}","contents_url":"https://api.github.com/repos/Trajan64/boutiques/contents/{+path}","compare_url":"https://api.github.com/repos/Trajan64/boutiques/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Trajan64/boutiques/merges","archive_url":"https://api.github.com/repos/Trajan64/boutiques/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Trajan64/boutiques/downloads","issues_url":"https://api.github.com/repos/Trajan64/boutiques/issues{/number}","pulls_url":"https://api.github.com/repos/Trajan64/boutiques/pulls{/number}","milestones_url":"https://api.github.com/repos/Trajan64/boutiques/milestones{/number}","notifications_url":"https://api.github.com/repos/Trajan64/boutiques/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Trajan64/boutiques/labels{/name}","releases_url":"https://api.github.com/repos/Trajan64/boutiques/releases{/id}","deployments_url":"https://api.github.com/repos/Trajan64/boutiques/deployments","created_at":"2018-01-23T22:07:43Z","updated_at":"2018-02-06T19:09:41Z","pushed_at":"2018-02-20T07:17:08Z","git_url":"git://github.com/Trajan64/boutiques.git","ssh_url":"git@github.com:Trajan64/boutiques.git","clone_url":"https://github.com/Trajan64/boutiques.git","svn_url":"https://github.com/Trajan64/boutiques","homepage":"","size":628,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"boutiques:develop","ref":"develop","sha":"5c600798a6cb119352872d476b3995ae0e3db4e3","user":{"login":"boutiques","id":11424909,"avatar_url":"https://avatars3.githubusercontent.com/u/11424909?v=4","gravatar_id":"","url":"https://api.github.com/users/boutiques","html_url":"https://github.com/boutiques","followers_url":"https://api.github.com/users/boutiques/followers","following_url":"https://api.github.com/users/boutiques/following{/other_user}","gists_url":"https://api.github.com/users/boutiques/gists{/gist_id}","starred_url":"https://api.github.com/users/boutiques/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/boutiques/subscriptions","organizations_url":"https://api.github.com/users/boutiques/orgs","repos_url":"https://api.github.com/users/boutiques/repos","events_url":"https://api.github.com/users/boutiques/events{/privacy}","received_events_url":"https://api.github.com/users/boutiques/received_events","type":"Organization","site_admin":false},"repo":{"id":32616811,"name":"boutiques","full_name":"boutiques/boutiques","owner":{"login":"boutiques","id":11424909,"avatar_url":"https://avatars3.githubusercontent.com/u/11424909?v=4","gravatar_id":"","url":"https://api.github.com/users/boutiques","html_url":"https://github.com/boutiques","followers_url":"https://api.github.com/users/boutiques/followers","following_url":"https://api.github.com/users/boutiques/following{/other_user}","gists_url":"https://api.github.com/users/boutiques/gists{/gist_id}","starred_url":"https://api.github.com/users/boutiques/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/boutiques/subscriptions","organizations_url":"https://api.github.com/users/boutiques/orgs","repos_url":"https://api.github.com/users/boutiques/repos","events_url":"https://api.github.com/users/boutiques/events{/privacy}","received_events_url":"https://api.github.com/users/boutiques/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/boutiques/boutiques","description":"JSON schema and tools for boutiques application descriptors.","fork":false,"url":"https://api.github.com/repos/boutiques/boutiques","forks_url":"https://api.github.com/repos/boutiques/boutiques/forks","keys_url":"https://api.github.com/repos/boutiques/boutiques/keys{/key_id}","collaborators_url":"https://api.github.com/repos/boutiques/boutiques/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/boutiques/boutiques/teams","hooks_url":"https://api.github.com/repos/boutiques/boutiques/hooks","issue_events_url":"https://api.github.com/repos/boutiques/boutiques/issues/events{/number}","events_url":"https://api.github.com/repos/boutiques/boutiques/events","assignees_url":"https://api.github.com/repos/boutiques/boutiques/assignees{/user}","branches_url":"https://api.github.com/repos/boutiques/boutiques/branches{/branch}","tags_url":"https://api.github.com/repos/boutiques/boutiques/tags","blobs_url":"https://api.github.com/repos/boutiques/boutiques/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/boutiques/boutiques/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/boutiques/boutiques/git/refs{/sha}","trees_url":"https://api.github.com/repos/boutiques/boutiques/git/trees{/sha}","statuses_url":"https://api.github.com/repos/boutiques/boutiques/statuses/{sha}","languages_url":"https://api.github.com/repos/boutiques/boutiques/languages","stargazers_url":"https://api.github.com/repos/boutiques/boutiques/stargazers","contributors_url":"https://api.github.com/repos/boutiques/boutiques/contributors","subscribers_url":"https://api.github.com/repos/boutiques/boutiques/subscribers","subscription_url":"https://api.github.com/repos/boutiques/boutiques/subscription","commits_url":"https://api.github.com/repos/boutiques/boutiques/commits{/sha}","git_commits_url":"https://api.github.com/repos/boutiques/boutiques/git/commits{/sha}","comments_url":"https://api.github.com/repos/boutiques/boutiques/comments{/number}","issue_comment_url":"https://api.github.com/repos/boutiques/boutiques/issues/comments{/number}","contents_url":"https://api.github.com/repos/boutiques/boutiques/contents/{+path}","compare_url":"https://api.github.com/repos/boutiques/boutiques/compare/{base}...{head}","merges_url":"https://api.github.com/repos/boutiques/boutiques/merges","archive_url":"https://api.github.com/repos/boutiques/boutiques/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/boutiques/boutiques/downloads","issues_url":"https://api.github.com/repos/boutiques/boutiques/issues{/number}","pulls_url":"https://api.github.com/repos/boutiques/boutiques/pulls{/number}","milestones_url":"https://api.github.com/repos/boutiques/boutiques/milestones{/number}","notifications_url":"https://api.github.com/repos/boutiques/boutiques/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/boutiques/boutiques/labels{/name}","releases_url":"https://api.github.com/repos/boutiques/boutiques/releases{/id}","deployments_url":"https://api.github.com/repos/boutiques/boutiques/deployments","created_at":"2015-03-21T02:23:09Z","updated_at":"2018-02-13T21:05:56Z","pushed_at":"2018-02-20T07:17:10Z","git_url":"git://github.com/boutiques/boutiques.git","ssh_url":"git@github.com:boutiques/boutiques.git","clone_url":"https://github.com/boutiques/boutiques.git","svn_url":"https://github.com/boutiques/boutiques","homepage":"","size":584,"stargazers_count":16,"watchers_count":16,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":17,"mirror_url":null,"archived":false,"open_issues_count":62,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":17,"open_issues":62,"watchers":16,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/boutiques/boutiques/pulls/214"},"html":{"href":"https://github.com/boutiques/boutiques/pull/214"},"issue":{"href":"https://api.github.com/repos/boutiques/boutiques/issues/214"},"comments":{"href":"https://api.github.com/repos/boutiques/boutiques/issues/214/comments"},"review_comments":{"href":"https://api.github.com/repos/boutiques/boutiques/pulls/214/comments"},"review_comment":{"href":"https://api.github.com/repos/boutiques/boutiques/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/boutiques/boutiques/pulls/214/commits"},"statuses":{"href":"https://api.github.com/repos/boutiques/boutiques/statuses/6181b64373fad189d19b1c2cd4b4ab941703535a"}},"author_association":"NONE"}}
{"actor":{"display_login":"gkiar"}}