{"action":"created","comment":{"url":"https://api.github.com/repos/datalad/datalad/pulls/comments/171066459","pull_request_review_id":99849429,"id":171066459,"diff_hunk":"@@ -0,0 +1,654 @@\n+# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-\n+# ex: set sts=4 ts=4 sw=4 noet:\n+# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n+#\n+#   See COPYING file distributed along with the datalad package for the\n+#   copyright and license terms.\n+#\n+# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##\n+\"\"\"Create and update a dataset from a list of URLs.\n+\"\"\"\n+\n+from collections import Mapping\n+from functools import partial\n+import logging\n+import os\n+import re\n+import string\n+\n+from six import string_types\n+from six.moves.urllib.parse import urlparse\n+\n+lgr = logging.getLogger(\"datalad.plugin.addurls\")\n+\n+__docformat__ = \"restructuredtext\"\n+\n+\n+class Formatter(string.Formatter):\n+    \"\"\"Formatter that gives precedence to custom keys.\n+\n+    The first positional argument to the `format` call should be a\n+    mapping whose keys are exposed as placeholders (e.g.,\n+    \"{key1}.py\").\n+\n+    Parameters\n+    ----------\n+    idx_to_name : dict\n+        A mapping from a positional index to a key.  If not provided,\n+        \"{N}\" elements are not supported.\n+    missing : str, optional\n+        When column lookup results in an empty string, use this value in\n+        its place.\n+    \"\"\"\n+\n+    def __init__(self, idx_to_name, missing_value=None, *args, **kwargs):\n+        self.idx_to_name = idx_to_name or {}\n+        self.missing = missing_value\n+        super(Formatter, self).__init__(*args, **kwargs)\n+\n+    def format(self, format_string, *args, **kwargs):\n+        if not isinstance(args[0], Mapping):\n+            raise ValueError(\"First positional argument should be mapping\")\n+        return super(Formatter, self).format(format_string, *args, **kwargs)\n+\n+    def get_value(self, key, args, kwargs):\n+        \"\"\"Look for key's value in `args[0]` mapping first.\n+\n+\n+        \"\"\"\n+        # FIXME: This approach will fail for keys that contain \"!\" and\n+        # \":\" because they'll be interpreted as formatting flags.\n+        data = args[0]\n+\n+        name = key\n+        try:\n+            key_int = int(key)\n+        except ValueError:\n+            pass\n+        else:\n+            name = self.idx_to_name[key_int]\n+\n+        try:\n+            value = data[name]\n+        except KeyError:\n+            return super(Formatter, self).get_value(\n+                key, args, kwargs)\n+\n+        if self.missing is not None and isinstance(value, string_types):\n+            return value or self.missing\n+        return value\n+\n+    def convert_field(self, value, conversion):\n+        if conversion == 'l':\n+            return str(value).lower()\n+        return super(Formatter, self).convert_field(value, conversion)\n+\n+\n+class RepFormatter(Formatter):\n+    \"\"\"Extend Formatter to support a {_repindex} placeholder.\n+    \"\"\"\n+\n+    def __init__(self, *args, **kwargs):\n+        super(RepFormatter, self).__init__(*args, **kwargs)\n+        self.repeats = {}\n+        self.repindex = 0\n+\n+    def format(self, *args, **kwargs):\n+        self.repindex = 0\n+        result = super(RepFormatter, self).format(*args, **kwargs)\n+        if result in self.repeats:\n+            self.repindex = self.repeats[result] + 1\n+            self.repeats[result] = self.repindex\n+            result = super(RepFormatter, self).format(*args, **kwargs)\n+        else:\n+            self.repeats[result] = 0\n+        return result\n+\n+    def get_value(self, key, args, kwargs):\n+        args[0][\"_repindex\"] = self.repindex\n+        return super(RepFormatter, self).get_value(key, args, kwargs)\n+\n+\n+def clean_meta_args(args):\n+    \"\"\"Prepare formatted metadata arguments to be passed to git-annex.\n+\n+    Parameters\n+    ----------\n+    args : iterable of str\n+        Formatted metadata arguments for 'git-annex metadata --set'.\n+\n+    Returns\n+    -------\n+    Generator that yields processed arguments (str).\n+    \"\"\"\n+    for arg in args:\n+        parts = [x.strip() for x in arg.split(\"=\", 1)]\n+        if len(parts) == 2:\n+            if not parts[0]:\n+                raise ValueError(\"Empty field name\")\n+            field, value = parts\n+        else:\n+            raise ValueError(\"meta argument isn't in 'field=value' format\")\n+\n+        if not value:\n+            # The `url_file` may have an empty value.\n+            continue\n+\n+        yield field + \"=\" + value\n+\n+\n+def get_subpaths(filename):\n+    \"\"\"Convert \"//\" marker in `filename` to a list of subpaths.\n+\n+    >>> get_subpaths(\"p1/p2//p3/p4//file\")\n+    ('p1/p2/p3/p4/file', ['p1/p2', 'p1/p2/p3/p4'])\n+\n+    Note: With Python 3, the subpaths could be generated with\n+\n+        itertools.accumulate(filename.split(\"//\")[:-1], os.path.join)\n+\n+    Parameters\n+    ----------\n+    filename : str\n+        File name with \"//\" marking subpaths.\n+\n+    Returns\n+    -------\n+    A tuple of the filename with any \"//\" collapsed to a single\n+    separator and a list of subpaths (str).\n+    \"\"\"\n+    if \"//\" not in filename:\n+        return filename, []\n+\n+    spaths = []\n+    for part in filename.split(\"//\")[:-1]:\n+        path = os.path.join(*(spaths + [part]))\n+        spaths.append(path)\n+    return filename.replace(\"//\", os.path.sep), spaths\n+\n+\n+def is_legal_metafield(name):\n+    \"\"\"Test whether `name` is a valid metadata field.\n+\n+    The set of permitted characters is taken from git-annex's\n+    MetaData.hs:legalField.\n+    \"\"\"\n+    legal = re.compile(r\"[a-zA-Z0-9][a-zA-Z0-9_.-]*\\Z\")\n+    return True if legal.match(name) else False\n+\n+\n+def filter_legal_metafield(fields):\n+    \"\"\"Remove illegal names from `fields`.\n+    \"\"\"\n+    legal = []\n+    for field in fields:\n+        if is_legal_metafield(field):\n+            legal.append(field)\n+        else:\n+            lgr.debug(\"%s is not a valid metadata field name; dropping\",\n+                      field)\n+    return legal\n+\n+\n+def get_fmt_names(format_string):\n+    \"\"\"Yield field names in `format_string`.\n+    \"\"\"\n+    for _, name, _, _ in string.Formatter().parse(format_string):\n+        if name:\n+            yield name\n+\n+\n+def fmt_to_name(format_string, num_to_name):\n+    \"\"\"Try to map a format string to a single name.\n+\n+    Parameters\n+    ----------\n+    format_string : string\n+    num_to_name : dict\n+        A dictionary that maps from an integer to a column name.  This\n+        enables mapping the format string to an integer to a name.\n+\n+    Returns\n+    -------\n+    A placeholder name if `format_string` consists of a single\n+    placeholder and no other text.  Otherwise, None is returned.\n+    \"\"\"\n+    parsed = list(string.Formatter().parse(format_string))\n+    if len(parsed) != 1:\n+        # It's an empty string or there's more than one placeholder.\n+        return\n+    if parsed[0][0]:\n+        # Format string contains text before the placeholder.\n+        return\n+\n+    name = parsed[0][1]\n+    if not name:\n+        # The field name is empty.\n+        return\n+\n+    try:\n+        return num_to_name[int(name)]\n+    except (KeyError, ValueError):\n+        return name\n+\n+\n+def _read(stream, input_type):\n+    if input_type == \"csv\":\n+        import csv\n+        csvrows = csv.reader(stream)\n+        headers = next(csvrows)\n+        lgr.debug(\"Taking %s fields from first line as headers: %s\",\n+                  len(headers), headers)\n+        idx_map = dict(enumerate(headers))\n+        rows = [dict(zip(headers, r)) for r in csvrows]\n+    elif input_type == \"json\":\n+        import json\n+        rows = json.load(stream)\n+        # For json input, we do not support indexing by position,\n+        # only names.\n+        idx_map = {}\n+    else:\n+        raise ValueError(\"input_type must be 'csv', 'json', or 'ext'\")\n+    return rows, idx_map\n+\n+\n+def _format_filenames(format_fn, rows, row_infos):\n+    subpaths = set()\n+    for row, info in zip(rows, row_infos):\n+        filename = format_fn(row)\n+        filename, spaths = get_subpaths(filename)\n+        subpaths |= set(spaths)\n+        info[\"filename\"] = filename\n+        info[\"subpath\"] = spaths[-1] if spaths else None\n+    return subpaths\n+\n+\n+def get_url_names(url):\n+    \"\"\"Assign a name to each part of a URL's path.\n+\n+    Parameters\n+    ----------\n+    url : str\n+\n+    Returns\n+    -------\n+    A dict with keys '_url0' through '_urlN' for a path with N+1\n+    components.  There is also a `_url_basename` for the rightmost\n+    entry.\n+    \"\"\"\n+    path = urlparse(url).path.strip(\"/\")\n+    if not path:\n+        return {}\n+\n+    url_parts = path.split(\"/\")\n+    names = {}\n+    for pidx, part in enumerate(url_parts):\n+        names[\"_url{}\".format(pidx)] = part\n+    names[\"_url_basename\"] = url_parts[-1]\n+    return names\n+\n+\n+def extract(stream, input_type, filename_format, url_format,\n+            exclude_autometa, meta, missing_value):\n+    \"\"\"Extract and format information from `url_file`.\n+\n+    Parameters\n+    ----------\n+    stream : file object\n+        Items used to construct the file names and URLs.\n+\n+    All other parameters match those described in `dlplugin`.\n+\n+    Returns\n+    -------\n+    A tuple where the first item is a list with a dict of extracted information\n+    for each row in `stream` and the second item is a set that contains all the\n+    subdataset paths.\n+    \"\"\"\n+    rows, colidx_to_name = _read(stream, input_type)\n+\n+    fmt = Formatter(colidx_to_name, missing_value)  # For URL and meta\n+    format_url = partial(fmt.format, url_format)\n+\n+    auto_meta_args = []\n+    if exclude_autometa not in [\"*\", \"\"]:\n+        urlcol = fmt_to_name(url_format, colidx_to_name)\n+        # TODO: Try to normalize invalid fields, checking for any\n+        # collisions.\n+        metacols = (c for c in sorted(rows[0].keys()) if c != urlcol)\n+        if exclude_autometa:\n+            metacols = (c for c in metacols\n+                        if not re.search(exclude_autometa, c))\n+        metacols = filter_legal_metafield(metacols)\n+        auto_meta_args = [c + \"=\" + \"{\" + c + \"}\" for c in metacols]\n+\n+    # Unlike `filename_format` and `url_format`, `meta` is a list\n+    # because meta may be given multiple times on the command line.\n+    formats_meta = [partial(fmt.format, m) for m in meta + auto_meta_args]\n+\n+    infos = []\n+    for row in rows:\n+        meta_args = list(clean_meta_args(fmt(row) for fmt in formats_meta))\n+        infos.append({\"url\": format_url(row),\n+                      \"meta_args\": meta_args})\n+\n+    # Format the filename in a second pass so that we can provide\n+    # information about the formatted URLs.\n+    if any(i.startswith(\"_url\") for i in get_fmt_names(filename_format)):\n+        for row, info in zip(rows, infos):\n+            row.update(get_url_names(info[\"url\"]))\n+\n+    # For the file name, we allow the _repindex special key.\n+    format_filename = partial(\n+        RepFormatter(colidx_to_name, missing_value).format,\n+        filename_format)\n+    subpaths = _format_filenames(format_filename, rows, infos)\n+    return infos, subpaths\n+\n+\n+def dlplugin(dataset=None, url_file=None, input_type=\"ext\",\n+             url_format=\"{0}\", filename_format=\"{1}\",\n+             exclude_autometa=None, meta=None,\n+             message=None, dry_run=False, fast=False,\n+             ifexists=None, missing_value=None):\n+    \"\"\"Create and update a dataset from a list of URLs.\n+\n+    Parameters\n+    ----------\n+    dataset : Dataset\n+        Add the URLs to this dataset (or possibly subdatasets of this\n+        dataset).  An empty or non-existent directory is passed to\n+        create a new dataset.  New subdatasets can be specified with\n+        `filename_format`.\n+    url_file : str\n+        A file that contains URLs or information that can be used to\n+        construct URLs.  Depending on the value of `input_type`, this\n+        should be a CSV file (with a header as the first row) or a\n+        JSON file (structured as a list of objects with string\n+        values).\n+    input_type : {\"ext\", \"csv\", \"json\"}, optional\n+        Whether `url_file` should be considered a CSV file or a JSON\n+        file.  The default value, \"ext\", means to consider `url_file`\n+        as a JSON file if it ends with \".json\".  Otherwise, treat it\n+        as a CSV file.\n+    url_format : str, optional\n+        A format string that specifies the URL for each entry.  This\n+        value is similar to a normal Python format string where the\n+        names from `url_file` (column names for a CSV or properties\n+        for JSON) are available as placeholders.  If `url_file` is a\n+        CSV file, a positional index can also be used (i.e., \"{0}\" for\n+        the first column).  Note that a placeholder cannot contain a\n+        ':' or '!'.\n+    filename_format : str, optional\n+        Like `url_format`, but this format string specifies the file to\n+        which the URL's content will be downloaded.  The file name may\n+        contain directories.  The separator \"//\" can be used to indicate\n+        that the left-side directory should be created as a new\n+        subdataset.\n+\n+        In addition to the placeholders described in `url_format`, there\n+        are a few special placeholder.\n+\n+          - \"_repindex\"\n+\n+            The constructed file names must be unique across all fields\n+            rows.  To avoid collisions, the special placeholder\n+            \"_repindex\" can added to the formatter.  It value will start\n+            at 0 and increment every time a file name repeats.\n+\n+          - \"_urlN\"\n+\n+            Each part of the formatted URL is available.  For example,\n+            in \"http://datalad.org/for/git-users\", \"_url0\" and \"_url1\"\n+            would map to \"for\" and \"git-users\", respectively.","path":"datalad/plugin/addurls.py","position":403,"original_position":403,"commit_id":"be3111bc8203e443f0b02ebad2661ddd8a14df9c","original_commit_id":"be3111bc8203e443f0b02ebad2661ddd8a14df9c","user":{"login":"yarikoptic","id":39889,"avatar_url":"https://avatars3.githubusercontent.com/u/39889?v=4","gravatar_id":"","url":"https://api.github.com/users/yarikoptic","html_url":"https://github.com/yarikoptic","followers_url":"https://api.github.com/users/yarikoptic/followers","following_url":"https://api.github.com/users/yarikoptic/following{/other_user}","gists_url":"https://api.github.com/users/yarikoptic/gists{/gist_id}","starred_url":"https://api.github.com/users/yarikoptic/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yarikoptic/subscriptions","organizations_url":"https://api.github.com/users/yarikoptic/orgs","repos_url":"https://api.github.com/users/yarikoptic/repos","events_url":"https://api.github.com/users/yarikoptic/events{/privacy}","received_events_url":"https://api.github.com/users/yarikoptic/received_events","type":"User","site_admin":false},"body":"lovely! but then what about hostname? `_url_hostname`?","created_at":"2018-02-27T21:08:09Z","updated_at":"2018-02-27T21:59:44Z","html_url":"https://github.com/datalad/datalad/pull/2121#discussion_r171066459","pull_request_url":"https://api.github.com/repos/datalad/datalad/pulls/2121","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/datalad/datalad/pulls/comments/171066459"},"html":{"href":"https://github.com/datalad/datalad/pull/2121#discussion_r171066459"},"pull_request":{"href":"https://api.github.com/repos/datalad/datalad/pulls/2121"}}},"pull_request":{"url":"https://api.github.com/repos/datalad/datalad/pulls/2121","id":167714732,"html_url":"https://github.com/datalad/datalad/pull/2121","diff_url":"https://github.com/datalad/datalad/pull/2121.diff","patch_url":"https://github.com/datalad/datalad/pull/2121.patch","issue_url":"https://api.github.com/repos/datalad/datalad/issues/2121","number":2121,"state":"open","locked":false,"title":"[WIP] ENH: Add initial implementation of addurls plugin","user":{"login":"kyleam","id":1297788,"avatar_url":"https://avatars2.githubusercontent.com/u/1297788?v=4","gravatar_id":"","url":"https://api.github.com/users/kyleam","html_url":"https://github.com/kyleam","followers_url":"https://api.github.com/users/kyleam/followers","following_url":"https://api.github.com/users/kyleam/following{/other_user}","gists_url":"https://api.github.com/users/kyleam/gists{/gist_id}","starred_url":"https://api.github.com/users/kyleam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kyleam/subscriptions","organizations_url":"https://api.github.com/users/kyleam/orgs","repos_url":"https://api.github.com/users/kyleam/repos","events_url":"https://api.github.com/users/kyleam/events{/privacy}","received_events_url":"https://api.github.com/users/kyleam/received_events","type":"User","site_admin":false},"body":"This covers some basic features mentioned at gh-1665.\r\n\r\nThe few things it can do:\r\n\r\n  * Data from a CSV or JSON file can be used to construct URLs and file names to be passed to 'git annex addurl'.\r\n\r\n    The field names from the data file (header names for CSV or properties for JSON) can be used as placeholders like \"{participant_id}.mgz\".  However, names with \"!\" or \":\" aren't currently supported because these are interpreted as formatting flags.\r\n\r\n  * Subdatasets can be created by using \"//\" in the filename format string.\r\n\r\n  * fast=True will pass --fast to 'git annex addurl'.\r\n\r\nSome of what it can't do:\r\n\r\n  * ~It doesn't yet have an option for specifying which columns should be used as metadata.~\r\n\r\n  * There's no support for the Content-Disposition filename.\r\n\r\n  * There isn't a jobs flag that's passed --jobs=N to 'git annex addurl'.\r\n\r\n  * There aren't any reserved formatting keys like \"url_filename\" or \"disposition_filename\".\r\n\r\n    Update: Parts of the URL path are now exposed.\r\n\r\n  * There isn't a non-'git annex addurl' download option (e.g., providers.download).\r\n\r\n    And if we want to provide a non-addurl option, perhaps my choice to call this plugin `addurls` wasn't a good one.\r\n\r\n\r\nAnd it of course needs tests.\r\n\r\n### To add\r\n\r\nHere's my current plan for what to add to this PR.\r\n\r\n- [x] tests\r\n\r\n- [x] support for setting metadata based on information in the file\r\n\r\n- [x] add support for metadata\r\n\r\n- [x] ability to provide replace empty strings with another missing value\r\n\r\n- [x] parts of the URL path are now accessible in `filename_format` as `_url_0`, ..., `_url_N`.  There's also `_url_basename` for the rightmost url part.\r\n\r\n- [x] progress bars for `addurl` and metadata actions\r\n\r\n- [ ] batch `git annex metadata --set` calls?\r\n\r\n  ~At the least, I should group the calls so that it calls once per file rather than once per file per metadata field.  (I hadn't realized you could use `--set` multiple times.)~\r\n\r\n  Update: The call is now made once per file.\r\n\r\n\r\n### Maybe later\r\n\r\nThings that I don't plan to add to this PR\r\n\r\n- [ ] better way to handle the placeholder formatting?\r\n\r\n  Nothing better has occurred to me yet.\r\n\r\n- [ ] jobs flag\r\n\r\n  I'm not sure if this is possible with batch commands.\r\n\r\n- [ ] add a `filename_format` field for the Content-Disposition filename.\r\n\r\n\r\n### Changes\r\n\r\n- [X] This is in progress\r\n\r\nPlease have a look @datalad/developers\r\n","created_at":"2018-02-07T14:55:26Z","updated_at":"2018-02-27T21:59:44Z","closed_at":null,"merged_at":null,"merge_commit_sha":"1c6ecf2040e764bc5d0ab944458606cc2f3ec127","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":449733138,"url":"https://api.github.com/repos/datalad/datalad/labels/WIP","name":"WIP","color":"5319e7","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/datalad/datalad/pulls/2121/commits","review_comments_url":"https://api.github.com/repos/datalad/datalad/pulls/2121/comments","review_comment_url":"https://api.github.com/repos/datalad/datalad/pulls/comments{/number}","comments_url":"https://api.github.com/repos/datalad/datalad/issues/2121/comments","statuses_url":"https://api.github.com/repos/datalad/datalad/statuses/be3111bc8203e443f0b02ebad2661ddd8a14df9c","head":{"label":"kyleam:enh-import-urls","ref":"enh-import-urls","sha":"be3111bc8203e443f0b02ebad2661ddd8a14df9c","user":{"login":"kyleam","id":1297788,"avatar_url":"https://avatars2.githubusercontent.com/u/1297788?v=4","gravatar_id":"","url":"https://api.github.com/users/kyleam","html_url":"https://github.com/kyleam","followers_url":"https://api.github.com/users/kyleam/followers","following_url":"https://api.github.com/users/kyleam/following{/other_user}","gists_url":"https://api.github.com/users/kyleam/gists{/gist_id}","starred_url":"https://api.github.com/users/kyleam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kyleam/subscriptions","organizations_url":"https://api.github.com/users/kyleam/orgs","repos_url":"https://api.github.com/users/kyleam/repos","events_url":"https://api.github.com/users/kyleam/events{/privacy}","received_events_url":"https://api.github.com/users/kyleam/received_events","type":"User","site_admin":false},"repo":{"id":113788453,"name":"datalad","full_name":"kyleam/datalad","owner":{"login":"kyleam","id":1297788,"avatar_url":"https://avatars2.githubusercontent.com/u/1297788?v=4","gravatar_id":"","url":"https://api.github.com/users/kyleam","html_url":"https://github.com/kyleam","followers_url":"https://api.github.com/users/kyleam/followers","following_url":"https://api.github.com/users/kyleam/following{/other_user}","gists_url":"https://api.github.com/users/kyleam/gists{/gist_id}","starred_url":"https://api.github.com/users/kyleam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kyleam/subscriptions","organizations_url":"https://api.github.com/users/kyleam/orgs","repos_url":"https://api.github.com/users/kyleam/repos","events_url":"https://api.github.com/users/kyleam/events{/privacy}","received_events_url":"https://api.github.com/users/kyleam/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/kyleam/datalad","description":"Keep scientific data under control with git  and git-annex","fork":true,"url":"https://api.github.com/repos/kyleam/datalad","forks_url":"https://api.github.com/repos/kyleam/datalad/forks","keys_url":"https://api.github.com/repos/kyleam/datalad/keys{/key_id}","collaborators_url":"https://api.github.com/repos/kyleam/datalad/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/kyleam/datalad/teams","hooks_url":"https://api.github.com/repos/kyleam/datalad/hooks","issue_events_url":"https://api.github.com/repos/kyleam/datalad/issues/events{/number}","events_url":"https://api.github.com/repos/kyleam/datalad/events","assignees_url":"https://api.github.com/repos/kyleam/datalad/assignees{/user}","branches_url":"https://api.github.com/repos/kyleam/datalad/branches{/branch}","tags_url":"https://api.github.com/repos/kyleam/datalad/tags","blobs_url":"https://api.github.com/repos/kyleam/datalad/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/kyleam/datalad/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/kyleam/datalad/git/refs{/sha}","trees_url":"https://api.github.com/repos/kyleam/datalad/git/trees{/sha}","statuses_url":"https://api.github.com/repos/kyleam/datalad/statuses/{sha}","languages_url":"https://api.github.com/repos/kyleam/datalad/languages","stargazers_url":"https://api.github.com/repos/kyleam/datalad/stargazers","contributors_url":"https://api.github.com/repos/kyleam/datalad/contributors","subscribers_url":"https://api.github.com/repos/kyleam/datalad/subscribers","subscription_url":"https://api.github.com/repos/kyleam/datalad/subscription","commits_url":"https://api.github.com/repos/kyleam/datalad/commits{/sha}","git_commits_url":"https://api.github.com/repos/kyleam/datalad/git/commits{/sha}","comments_url":"https://api.github.com/repos/kyleam/datalad/comments{/number}","issue_comment_url":"https://api.github.com/repos/kyleam/datalad/issues/comments{/number}","contents_url":"https://api.github.com/repos/kyleam/datalad/contents/{+path}","compare_url":"https://api.github.com/repos/kyleam/datalad/compare/{base}...{head}","merges_url":"https://api.github.com/repos/kyleam/datalad/merges","archive_url":"https://api.github.com/repos/kyleam/datalad/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/kyleam/datalad/downloads","issues_url":"https://api.github.com/repos/kyleam/datalad/issues{/number}","pulls_url":"https://api.github.com/repos/kyleam/datalad/pulls{/number}","milestones_url":"https://api.github.com/repos/kyleam/datalad/milestones{/number}","notifications_url":"https://api.github.com/repos/kyleam/datalad/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/kyleam/datalad/labels{/name}","releases_url":"https://api.github.com/repos/kyleam/datalad/releases{/id}","deployments_url":"https://api.github.com/repos/kyleam/datalad/deployments","created_at":"2017-12-10T22:28:28Z","updated_at":"2017-12-10T22:28:31Z","pushed_at":"2018-02-27T14:08:25Z","git_url":"git://github.com/kyleam/datalad.git","ssh_url":"git@github.com:kyleam/datalad.git","clone_url":"https://github.com/kyleam/datalad.git","svn_url":"https://github.com/kyleam/datalad","homepage":"http://datalad.org","size":11729,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"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":"datalad:master","ref":"master","sha":"2cac759afec7d320ff8c53e8892a16f191d949d6","user":{"login":"datalad","id":8927200,"avatar_url":"https://avatars2.githubusercontent.com/u/8927200?v=4","gravatar_id":"","url":"https://api.github.com/users/datalad","html_url":"https://github.com/datalad","followers_url":"https://api.github.com/users/datalad/followers","following_url":"https://api.github.com/users/datalad/following{/other_user}","gists_url":"https://api.github.com/users/datalad/gists{/gist_id}","starred_url":"https://api.github.com/users/datalad/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/datalad/subscriptions","organizations_url":"https://api.github.com/users/datalad/orgs","repos_url":"https://api.github.com/users/datalad/repos","events_url":"https://api.github.com/users/datalad/events{/privacy}","received_events_url":"https://api.github.com/users/datalad/received_events","type":"Organization","site_admin":false},"repo":{"id":14052034,"name":"datalad","full_name":"datalad/datalad","owner":{"login":"datalad","id":8927200,"avatar_url":"https://avatars2.githubusercontent.com/u/8927200?v=4","gravatar_id":"","url":"https://api.github.com/users/datalad","html_url":"https://github.com/datalad","followers_url":"https://api.github.com/users/datalad/followers","following_url":"https://api.github.com/users/datalad/following{/other_user}","gists_url":"https://api.github.com/users/datalad/gists{/gist_id}","starred_url":"https://api.github.com/users/datalad/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/datalad/subscriptions","organizations_url":"https://api.github.com/users/datalad/orgs","repos_url":"https://api.github.com/users/datalad/repos","events_url":"https://api.github.com/users/datalad/events{/privacy}","received_events_url":"https://api.github.com/users/datalad/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/datalad/datalad","description":"Keep scientific data under control with git  and git-annex","fork":false,"url":"https://api.github.com/repos/datalad/datalad","forks_url":"https://api.github.com/repos/datalad/datalad/forks","keys_url":"https://api.github.com/repos/datalad/datalad/keys{/key_id}","collaborators_url":"https://api.github.com/repos/datalad/datalad/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/datalad/datalad/teams","hooks_url":"https://api.github.com/repos/datalad/datalad/hooks","issue_events_url":"https://api.github.com/repos/datalad/datalad/issues/events{/number}","events_url":"https://api.github.com/repos/datalad/datalad/events","assignees_url":"https://api.github.com/repos/datalad/datalad/assignees{/user}","branches_url":"https://api.github.com/repos/datalad/datalad/branches{/branch}","tags_url":"https://api.github.com/repos/datalad/datalad/tags","blobs_url":"https://api.github.com/repos/datalad/datalad/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/datalad/datalad/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/datalad/datalad/git/refs{/sha}","trees_url":"https://api.github.com/repos/datalad/datalad/git/trees{/sha}","statuses_url":"https://api.github.com/repos/datalad/datalad/statuses/{sha}","languages_url":"https://api.github.com/repos/datalad/datalad/languages","stargazers_url":"https://api.github.com/repos/datalad/datalad/stargazers","contributors_url":"https://api.github.com/repos/datalad/datalad/contributors","subscribers_url":"https://api.github.com/repos/datalad/datalad/subscribers","subscription_url":"https://api.github.com/repos/datalad/datalad/subscription","commits_url":"https://api.github.com/repos/datalad/datalad/commits{/sha}","git_commits_url":"https://api.github.com/repos/datalad/datalad/git/commits{/sha}","comments_url":"https://api.github.com/repos/datalad/datalad/comments{/number}","issue_comment_url":"https://api.github.com/repos/datalad/datalad/issues/comments{/number}","contents_url":"https://api.github.com/repos/datalad/datalad/contents/{+path}","compare_url":"https://api.github.com/repos/datalad/datalad/compare/{base}...{head}","merges_url":"https://api.github.com/repos/datalad/datalad/merges","archive_url":"https://api.github.com/repos/datalad/datalad/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/datalad/datalad/downloads","issues_url":"https://api.github.com/repos/datalad/datalad/issues{/number}","pulls_url":"https://api.github.com/repos/datalad/datalad/pulls{/number}","milestones_url":"https://api.github.com/repos/datalad/datalad/milestones{/number}","notifications_url":"https://api.github.com/repos/datalad/datalad/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/datalad/datalad/labels{/name}","releases_url":"https://api.github.com/repos/datalad/datalad/releases{/id}","deployments_url":"https://api.github.com/repos/datalad/datalad/deployments","created_at":"2013-11-01T19:40:08Z","updated_at":"2018-02-25T19:37:40Z","pushed_at":"2018-02-27T21:34:16Z","git_url":"git://github.com/datalad/datalad.git","ssh_url":"git@github.com:datalad/datalad.git","clone_url":"https://github.com/datalad/datalad.git","svn_url":"https://github.com/datalad/datalad","homepage":"http://datalad.org","size":12133,"stargazers_count":87,"watchers_count":87,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":26,"mirror_url":null,"archived":false,"open_issues_count":402,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":26,"open_issues":402,"watchers":87,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/datalad/datalad/pulls/2121"},"html":{"href":"https://github.com/datalad/datalad/pull/2121"},"issue":{"href":"https://api.github.com/repos/datalad/datalad/issues/2121"},"comments":{"href":"https://api.github.com/repos/datalad/datalad/issues/2121/comments"},"review_comments":{"href":"https://api.github.com/repos/datalad/datalad/pulls/2121/comments"},"review_comment":{"href":"https://api.github.com/repos/datalad/datalad/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/datalad/datalad/pulls/2121/commits"},"statuses":{"href":"https://api.github.com/repos/datalad/datalad/statuses/be3111bc8203e443f0b02ebad2661ddd8a14df9c"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"yarikoptic"}}