{"action":"created","comment":{"url":"https://api.github.com/repos/EducationalTestingService/skll/pulls/comments/34941002","id":34941002,"diff_hunk":"@@ -0,0 +1,575 @@\n+# License: BSD 3 clause\n+\"\"\"\n+Functions related to parsing configuration files.\n+\n+:author: Nitin Madnani (nmadnani@ets.org)\n+:author: Dan Blanchard (dblanchard@ets.org)\n+:author: Michael Heilman (mheilman@ets.org)\n+:author: Chee Wee Leong (cleong@ets.org)\n+\"\"\"\n+\n+from __future__ import absolute_import, print_function, unicode_literals\n+\n+import csv\n+import errno\n+import itertools\n+import logging\n+import os\n+from io import open\n+from os.path import (abspath, basename,\n+                     dirname, exists,\n+                     isabs, join, normpath)\n+\n+import configparser  # Backported version from Python 3\n+import yaml\n+from six import string_types, iteritems  # Python 2/3\n+from sklearn.metrics import SCORERS\n+\n+\n+_VALID_TASKS = frozenset(['predict', 'train', 'evaluate', 'cross_validate'])\n+_VALID_SAMPLERS = frozenset(['Nystroem', 'RBFSampler', 'SkewedChi2Sampler',\n+                             'AdditiveChi2Sampler', ''])\n+\n+\n+class SKLLConfigParser(configparser.ConfigParser):\n+\n+    \"\"\"A custom configuration file parser for SKLL\"\"\"\n+\n+    def __init__(self):\n+\n+        # these are the three options that must be set in a config\n+        # file and no defaults are provided\n+        required = ['experiment_name', 'task', 'learners']\n+\n+        # these are the optional config options for which#\n+        # defaults are automatically provided\n+        defaults = {'class_map': '{}',\n+                    'custom_learner_path': '',\n+                    'cv_folds_file': '',\n+                    'feature_hasher': 'False',\n+                    'feature_scaling': 'none',\n+                    'featuresets': '[]',\n+                    'featureset_names': '[]',\n+                    'fixed_parameters': '[]',\n+                    'grid_search': 'False',\n+                    'grid_search_folds': '3',\n+                    'grid_search_jobs': '0',\n+                    'hasher_features': '0',\n+                    'id_col': 'id',\n+                    'ids_to_floats': 'False',\n+                    'label_col': 'y',\n+                    'log': '',\n+                    'min_feature_count': '1',\n+                    'models': '',\n+                    'num_cv_folds': '10',\n+                    'objective': 'f1_score_micro',\n+                    'param_grids': '[]',\n+                    'pos_label_str': '',\n+                    'predictions': '',\n+                    'probability': 'False',\n+                    'random_folds': 'False',\n+                    'results': '',\n+                    'sampler': '',\n+                    'sampler_parameters': '[]',\n+                    'shuffle': 'False',\n+                    'suffix': '',\n+                    'test_directory': '',\n+                    'test_file': '',\n+                    'train_directory': '',\n+                    'train_file': ''}\n+\n+        correct_section_mapping = {'class_map': 'Input',\n+                                   'custom_learner_path': 'Input',\n+                                   'cv_folds_file': 'Input',\n+                                   'feature_hasher': 'Input',\n+                                   'feature_scaling': 'Input',\n+                                   'featuresets': 'Input',\n+                                   'featureset_names': 'Input',\n+                                   'fixed_parameters': 'Input',\n+                                   'grid_search': 'Tuning',\n+                                   'grid_search_folds': 'Tuning',\n+                                   'grid_search_jobs': 'Tuning',\n+                                   'hasher_features': 'Input',\n+                                   'id_col': 'Input',\n+                                   'ids_to_floats': 'Input',\n+                                   'label_col': 'Input',\n+                                   'log': 'Output',\n+                                   'min_feature_count': 'Tuning',\n+                                   'models': 'Output',\n+                                   'num_cv_folds': 'Input',\n+                                   'objective': 'Tuning',\n+                                   'param_grids': 'Tuning',\n+                                   'pos_label_str': 'Tuning',\n+                                   'predictions': 'Output',\n+                                   'probability': 'Output',\n+                                   'random_folds': 'Input',\n+                                   'results': 'Output',\n+                                   'sampler': 'Input',\n+                                   'sampler_parameters': 'Input',\n+                                   'shuffle': 'Input',\n+                                   'suffix': 'Input',\n+                                   'test_directory': 'Input',\n+                                   'test_file': 'Input',\n+                                   'train_directory': 'Input',\n+                                   'train_file': 'Input'}\n+\n+        # make sure that the defaults dictionary and the\n+        # section mapping dictionary have the same keys\n+        assert defaults.keys() == correct_section_mapping.keys()\n+\n+        super(SKLLConfigParser, self).__init__(defaults=defaults)\n+        self._required_options = required\n+        self._section_mapping = correct_section_mapping\n+\n+    def _find_invalid_options(self):\n+        \"\"\"\n+        Returns the set of invalid options specified by the user\n+        \"\"\"\n+\n+        # compute a list of all the valid options\n+        valid_options = list(self._defaults.keys()) + self._required_options\n+\n+        # get a set of all of the specified options\n+        specified_options = set(itertools.chain(*[self.options(section)\n+                                                  for section in self.sections()]))\n+\n+        # find any invalid options and return\n+        invalid_options = set(specified_options).difference(valid_options)\n+        return invalid_options\n+\n+    def _find_ill_specified_options(self):\n+        \"\"\"\n+        Make sure that all the options are specified in the appropriate sections\n+        and are not specified in multiple spaces. One way to do this is to\n+        basically get the values for all the optional config options from each\n+        of the sections and then look at the section that has a non-default\n+        (specified) value and if that's not the right section as indicated by\n+        our section mapping, then we need to raise an exception.\n+\n+        .. note::\n+\n+            This will NOT work if the user specifies the default value for the\n+            option but puts it in the wrong section. However, since specifying\n+            the default value for the option  does not result in running an\n+            experiment with unexpected settings, this is not really a major\n+            problem.\n+        \"\"\"\n+        incorrectly_specified_options = []\n+        multiply_specified_options = []\n+        for option_name, default_value in self._defaults.items():\n+            used_sections = [section for section in self.sections()\n+                             if self.get(section, option_name) != default_value]\n+            # skip options that are not specified in any sections\n+            if len(used_sections) == 0:\n+                pass\n+            # find all options that are specified in multiple sections\n+            elif len(used_sections) > 1:\n+                multiply_specified_options.append((option_name, used_sections))\n+            # find all options that are specified in incorrect sections\n+            elif used_sections[0] != self._section_mapping[option_name]:\n+                incorrectly_specified_options.append((option_name,\n+                                                      used_sections[0]))\n+\n+        return (incorrectly_specified_options, multiply_specified_options)\n+\n+    def validate(self):\n+        \"\"\"Validate specified options to check:\n+             (a) no invalid options are specified\n+             (b) options are not specified in multiple sections\n+             (c) options are specified in the correct section\n+        \"\"\"\n+        invalid_options = self._find_invalid_options()\n+        if invalid_options:\n+            raise KeyError('Configuration file contains the following '\n+                           'unrecognized options: {}'\n+                           .format(list(invalid_options)))\n+\n+        incorrectly_specified_options, multiply_specified_options = self._find_ill_specified_options()\n+        if multiply_specified_options:\n+            raise KeyError('The following are defined in multiple sections: '\n+                           '{}'.format([t[0] for t in\n+                                        multiply_specified_options]))\n+        if incorrectly_specified_options:\n+            raise KeyError('The following are not defined in the appropriate '\n+                           'sections: {}'.format([t[0] for t in\n+                                                  incorrectly_specified_options]))\n+\n+\n+def _locate_file(file_path, config_path):\n+    if not file_path:\n+        return ''\n+    path_to_check = file_path if isabs(file_path) else normpath(join(dirname(config_path), file_path))\n+    ans = exists(path_to_check)\n+    if not ans:\n+        raise IOError(errno.ENOENT, \"File does not exist\",\n+                      path_to_check)\n+    else:\n+        return path_to_check\n+\n+def _setup_config_parser(config_path, validate=True):\n+    \"\"\"\n+    Returns a config parser at a given path. Only implemented as a separate\n+    function to simplify testing.\n+    \"\"\"\n+    # initialize config parser with the given defaults\n+    config = SKLLConfigParser()\n+\n+    # Read file if it exists\n+    if not exists(config_path):\n+        raise IOError(errno.ENOENT, \"Configuration file does not exist\",\n+                      config_path)\n+    config.read(config_path)\n+    if validate:\n+        config.validate()\n+\n+    return config\n+\n+\n+def _parse_config_file(config_path):\n+    \"\"\"\n+    Parses a SKLL experiment configuration file with the given path.\n+    \"\"\"\n+\n+    # Initialize logger\n+    logger = logging.getLogger(__name__)\n+\n+    # compute the absolute path for the config file\n+    config_path = abspath(config_path)\n+\n+    # set up a config parser with the above default values\n+    config = _setup_config_parser(config_path)\n+\n+    # extract parameters from the various sections in the config file\n+\n+    # 1. General\n+    if config.has_option(\"General\", \"experiment_name\"):\n+        experiment_name = config.get(\"General\", \"experiment_name\")\n+    else:\n+        raise ValueError(\"Configuration file does not contain experiment_name \"\n+                         \"in the [Input] section.\")\n+\n+    if config.has_option(\"General\", \"task\"):\n+        task = config.get(\"General\", \"task\")\n+    else:\n+        raise ValueError(\"Configuration file does not contain task in the \"\n+                         \"[Input] section.\")\n+    if task not in _VALID_TASKS:\n+        raise ValueError('An invalid task was specified: {}.  Valid tasks are:'\n+                         ' {}'.format(task, ', '.join(_VALID_TASKS)))\n+\n+    # 2. Input\n+    sampler = config.get(\"Input\", \"sampler\")\n+    if sampler not in _VALID_SAMPLERS:\n+        raise ValueError('An invalid sample was specified: {}.  Valid samplers'\n+                         ' are: {}'.format(sampler,\n+                                           ', '.join(_VALID_SAMPLERS)))\n+\n+    # produce warnings if feature_hasher is set but hasher_features\n+    # is less than or equal to zero.\n+    feature_hasher = config.getboolean(\"Input\", \"feature_hasher\")\n+    hasher_features = config.getint(\"Input\", \"hasher_features\")\n+    if feature_hasher:\n+        if hasher_features <= 0:\n+            raise ValueError(\"Configuration file must specify a non-zero value \"\n+                             \"for the option hasher_features when \"\n+                             \"feature_hasher is True.\")\n+\n+    # produce warnings if hasher_features is set but feature_hasher\n+    # is not set correctly\n+    if hasher_features > 0:\n+        logger.warning(\"Ignoring hasher_features since feature_hasher is either\"\n+                       \" missing or set to False.\")\n+\n+    if config.has_option(\"Input\", \"learners\"):\n+        learners_string = config.get(\"Input\", \"learners\")\n+    else:\n+        raise ValueError(\"Configuration file does not contain list of learners \"\n+                         \"in [Input] section.\")\n+    learners = yaml.load(_fix_json(learners_string))\n+\n+    if len(learners) == 0:\n+        raise ValueError(\"Configuration file contains an empty list of learners\"\n+                         \" in the [Input] section.\")\n+\n+    elif len(set(learners)) < len(learners):\n+        raise ValueError('Configuration file contains the same learner multiple'\n+                         ' times, which is not currently supported.  Please use'\n+                         ' param_grids with tuning to find the optimal settings'\n+                         ' for the learner.')\n+    custom_learner_path = config.get(\"Input\", \"custom_learner_path\")\n+\n+    # get the featuresets\n+    featuresets_string = config.get(\"Input\", \"featuresets\")\n+    featuresets = yaml.load(_fix_json(featuresets_string))\n+\n+    # ensure that featuresets is either a list of features or a list of lists\n+    # of features\n+    if not isinstance(featuresets, list) or not all(isinstance(fs, list) for fs\n+                                                    in featuresets):\n+        raise ValueError(\"The featuresets parameter should be a list of \"\n+                         \"features or a list of lists of features. You \"\n+                         \"specified: {}\".format(featuresets))\n+\n+    featureset_names = yaml.load(_fix_json(config.get(\"Input\",\n+                                                      \"featureset_names\")))\n+\n+    # ensure that featureset_names is a list of strings, if specified\n+    if featureset_names:\n+        if (not isinstance(featureset_names, list) or\n+                not all([isinstance(fs, string_types) for fs in\n+                         featureset_names])):\n+            raise ValueError(\"The featureset_names parameter should be a list \"\n+                             \"of strings. You specified: {}\"\n+                             .format(featureset_names))\n+\n+    # do we need to shuffle the training data\n+    do_shuffle = config.getboolean(\"Input\", \"shuffle\")\n+\n+    fixed_parameter_list = yaml.load(_fix_json(config.get(\"Input\",\n+                                                          \"fixed_parameters\")))\n+    fixed_sampler_parameters = _fix_json(config.get(\"Input\",\n+                                                    \"sampler_parameters\"))\n+    fixed_sampler_parameters = yaml.load(fixed_sampler_parameters)\n+    param_grid_list = yaml.load(_fix_json(config.get(\"Tuning\", \"param_grids\")))\n+    pos_label_str = config.get(\"Tuning\", \"pos_label_str\")\n+\n+    # ensure that feature_scaling is specified only as one of the\n+    # four available choices\n+    feature_scaling = config.get(\"Input\", \"feature_scaling\")\n+    if feature_scaling not in ['with_std', 'with_mean', 'both', 'none']:\n+        raise ValueError(\"Invalid value for feature_scaling parameter: {}\"\n+                         .format(feature_scaling))\n+\n+    suffix = config.get(\"Input\", \"suffix\")\n+    label_col = config.get(\"Input\", \"label_col\")\n+    id_col = config.get(\"Input\", \"id_col\")\n+    ids_to_floats = config.getboolean(\"Input\", \"ids_to_floats\")\n+\n+    # get the cv folds file and make a dictionary from it, if it exists\n+    cv_folds_file = config.get(\"Input\", \"cv_folds_file\")\n+    num_cv_folds = config.get(\"Input\", \"num_cv_folds\")\n+    if cv_folds_file:\n+        cv_folds_file = _locate_file(cv_folds_file, config_path)\n+        cv_folds = _load_cv_folds(cv_folds_file,\n+                                  ids_to_floats=ids_to_floats)\n+    else:\n+        # set the number of folds for cross-validation\n+        if num_cv_folds:\n+            try:\n+                cv_folds = int(num_cv_folds)\n+            except ValueError:\n+                raise ValueError(\"The value for cv_folds should be an integer. \"\n+                                 \"You specified {}\".format(num_cv_folds))\n+        else:\n+            # default number of cross-validation folds\n+            cv_folds = 10\n+\n+    # whether or not to do stratified cross validation\n+    random_folds = config.getboolean(\"Input\", \"random_folds\")\n+    if random_folds:\n+        if cv_folds_file:\n+            logger.warning('Specifying cv_folds_file overrides random_folds')\n+        do_stratified_folds = False\n+    else:\n+        do_stratified_folds = True\n+\n+    # get all the input paths and directories (without trailing slashes)\n+    train_path = config.get(\"Input\", \"train_directory\").rstrip('/')\n+    test_path = config.get(\"Input\", \"test_directory\").rstrip('/')","path":"skll/config.py","position":378,"original_position":378,"commit_id":"3a08303a8497d30b18a08bcbcef36688b0164b92","original_commit_id":"3a08303a8497d30b18a08bcbcef36688b0164b92","user":{"login":"desilinguist","id":161680,"avatar_url":"https://avatars.githubusercontent.com/u/161680?v=3","gravatar_id":"","url":"https://api.github.com/users/desilinguist","html_url":"https://github.com/desilinguist","followers_url":"https://api.github.com/users/desilinguist/followers","following_url":"https://api.github.com/users/desilinguist/following{/other_user}","gists_url":"https://api.github.com/users/desilinguist/gists{/gist_id}","starred_url":"https://api.github.com/users/desilinguist/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/desilinguist/subscriptions","organizations_url":"https://api.github.com/users/desilinguist/orgs","repos_url":"https://api.github.com/users/desilinguist/repos","events_url":"https://api.github.com/users/desilinguist/events{/privacy}","received_events_url":"https://api.github.com/users/desilinguist/received_events","type":"User","site_admin":false},"body":"This has been in there since the beginning so I guess it has never worked?","created_at":"2015-07-18T00:07:33Z","updated_at":"2015-07-18T00:07:33Z","html_url":"https://github.com/EducationalTestingService/skll/pull/246#discussion_r34941002","pull_request_url":"https://api.github.com/repos/EducationalTestingService/skll/pulls/246","_links":{"self":{"href":"https://api.github.com/repos/EducationalTestingService/skll/pulls/comments/34941002"},"html":{"href":"https://github.com/EducationalTestingService/skll/pull/246#discussion_r34941002"},"pull_request":{"href":"https://api.github.com/repos/EducationalTestingService/skll/pulls/246"}}},"pull_request":{"url":"https://api.github.com/repos/EducationalTestingService/skll/pulls/246","id":39955857,"html_url":"https://github.com/EducationalTestingService/skll/pull/246","diff_url":"https://github.com/EducationalTestingService/skll/pull/246.diff","patch_url":"https://github.com/EducationalTestingService/skll/pull/246.patch","issue_url":"https://api.github.com/repos/EducationalTestingService/skll/issues/246","number":246,"state":"open","locked":false,"title":"Overhaul configuration file parsing","user":{"login":"desilinguist","id":161680,"avatar_url":"https://avatars.githubusercontent.com/u/161680?v=3","gravatar_id":"","url":"https://api.github.com/users/desilinguist","html_url":"https://github.com/desilinguist","followers_url":"https://api.github.com/users/desilinguist/followers","following_url":"https://api.github.com/users/desilinguist/following{/other_user}","gists_url":"https://api.github.com/users/desilinguist/gists{/gist_id}","starred_url":"https://api.github.com/users/desilinguist/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/desilinguist/subscriptions","organizations_url":"https://api.github.com/users/desilinguist/orgs","repos_url":"https://api.github.com/users/desilinguist/repos","events_url":"https://api.github.com/users/desilinguist/events{/privacy}","received_events_url":"https://api.github.com/users/desilinguist/received_events","type":"User","site_admin":false},"body":"- Use a new custom parser class `SKLLConfigParser` based on `ConfigParser`.\r\n- Move all of the configuration parsing code to `config.py` and out of `experiments.py`.\r\n- Add a `validate()` method to the config parser that raises a `KeyError` if:\r\n  - unrecognized options are specified\r\n  - options are specified in more than one section\r\n  - options are specified in the wrong section (#223)\r\n- Add unit tests for the above validation checks\r\n- Update tests to deal with the above changes\r\n- Update `requirements_rtd.txt` to install the latest version of `ConfigParser` (v3.5.0b2)","created_at":"2015-07-14T20:00:32Z","updated_at":"2015-07-18T00:07:33Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4c1f598af76c16ecae78b0725b639caaae60a249","assignee":{"login":"aoifecahill","id":1999341,"avatar_url":"https://avatars.githubusercontent.com/u/1999341?v=3","gravatar_id":"","url":"https://api.github.com/users/aoifecahill","html_url":"https://github.com/aoifecahill","followers_url":"https://api.github.com/users/aoifecahill/followers","following_url":"https://api.github.com/users/aoifecahill/following{/other_user}","gists_url":"https://api.github.com/users/aoifecahill/gists{/gist_id}","starred_url":"https://api.github.com/users/aoifecahill/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aoifecahill/subscriptions","organizations_url":"https://api.github.com/users/aoifecahill/orgs","repos_url":"https://api.github.com/users/aoifecahill/repos","events_url":"https://api.github.com/users/aoifecahill/events{/privacy}","received_events_url":"https://api.github.com/users/aoifecahill/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/EducationalTestingService/skll/pulls/246/commits","review_comments_url":"https://api.github.com/repos/EducationalTestingService/skll/pulls/246/comments","review_comment_url":"https://api.github.com/repos/EducationalTestingService/skll/pulls/comments{/number}","comments_url":"https://api.github.com/repos/EducationalTestingService/skll/issues/246/comments","statuses_url":"https://api.github.com/repos/EducationalTestingService/skll/statuses/3a08303a8497d30b18a08bcbcef36688b0164b92","head":{"label":"EducationalTestingService:feature/overhaul-config-parsing","ref":"feature/overhaul-config-parsing","sha":"3a08303a8497d30b18a08bcbcef36688b0164b92","user":{"login":"EducationalTestingService","id":4822867,"avatar_url":"https://avatars.githubusercontent.com/u/4822867?v=3","gravatar_id":"","url":"https://api.github.com/users/EducationalTestingService","html_url":"https://github.com/EducationalTestingService","followers_url":"https://api.github.com/users/EducationalTestingService/followers","following_url":"https://api.github.com/users/EducationalTestingService/following{/other_user}","gists_url":"https://api.github.com/users/EducationalTestingService/gists{/gist_id}","starred_url":"https://api.github.com/users/EducationalTestingService/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/EducationalTestingService/subscriptions","organizations_url":"https://api.github.com/users/EducationalTestingService/orgs","repos_url":"https://api.github.com/users/EducationalTestingService/repos","events_url":"https://api.github.com/users/EducationalTestingService/events{/privacy}","received_events_url":"https://api.github.com/users/EducationalTestingService/received_events","type":"Organization","site_admin":false},"repo":{"id":11845170,"name":"skll","full_name":"EducationalTestingService/skll","owner":{"login":"EducationalTestingService","id":4822867,"avatar_url":"https://avatars.githubusercontent.com/u/4822867?v=3","gravatar_id":"","url":"https://api.github.com/users/EducationalTestingService","html_url":"https://github.com/EducationalTestingService","followers_url":"https://api.github.com/users/EducationalTestingService/followers","following_url":"https://api.github.com/users/EducationalTestingService/following{/other_user}","gists_url":"https://api.github.com/users/EducationalTestingService/gists{/gist_id}","starred_url":"https://api.github.com/users/EducationalTestingService/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/EducationalTestingService/subscriptions","organizations_url":"https://api.github.com/users/EducationalTestingService/orgs","repos_url":"https://api.github.com/users/EducationalTestingService/repos","events_url":"https://api.github.com/users/EducationalTestingService/events{/privacy}","received_events_url":"https://api.github.com/users/EducationalTestingService/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/EducationalTestingService/skll","description":"SciKit-Learn Laboratory (SKLL) makes it easy to run machine learning experiments.","fork":false,"url":"https://api.github.com/repos/EducationalTestingService/skll","forks_url":"https://api.github.com/repos/EducationalTestingService/skll/forks","keys_url":"https://api.github.com/repos/EducationalTestingService/skll/keys{/key_id}","collaborators_url":"https://api.github.com/repos/EducationalTestingService/skll/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/EducationalTestingService/skll/teams","hooks_url":"https://api.github.com/repos/EducationalTestingService/skll/hooks","issue_events_url":"https://api.github.com/repos/EducationalTestingService/skll/issues/events{/number}","events_url":"https://api.github.com/repos/EducationalTestingService/skll/events","assignees_url":"https://api.github.com/repos/EducationalTestingService/skll/assignees{/user}","branches_url":"https://api.github.com/repos/EducationalTestingService/skll/branches{/branch}","tags_url":"https://api.github.com/repos/EducationalTestingService/skll/tags","blobs_url":"https://api.github.com/repos/EducationalTestingService/skll/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/EducationalTestingService/skll/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/EducationalTestingService/skll/git/refs{/sha}","trees_url":"https://api.github.com/repos/EducationalTestingService/skll/git/trees{/sha}","statuses_url":"https://api.github.com/repos/EducationalTestingService/skll/statuses/{sha}","languages_url":"https://api.github.com/repos/EducationalTestingService/skll/languages","stargazers_url":"https://api.github.com/repos/EducationalTestingService/skll/stargazers","contributors_url":"https://api.github.com/repos/EducationalTestingService/skll/contributors","subscribers_url":"https://api.github.com/repos/EducationalTestingService/skll/subscribers","subscription_url":"https://api.github.com/repos/EducationalTestingService/skll/subscription","commits_url":"https://api.github.com/repos/EducationalTestingService/skll/commits{/sha}","git_commits_url":"https://api.github.com/repos/EducationalTestingService/skll/git/commits{/sha}","comments_url":"https://api.github.com/repos/EducationalTestingService/skll/comments{/number}","issue_comment_url":"https://api.github.com/repos/EducationalTestingService/skll/issues/comments{/number}","contents_url":"https://api.github.com/repos/EducationalTestingService/skll/contents/{+path}","compare_url":"https://api.github.com/repos/EducationalTestingService/skll/compare/{base}...{head}","merges_url":"https://api.github.com/repos/EducationalTestingService/skll/merges","archive_url":"https://api.github.com/repos/EducationalTestingService/skll/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/EducationalTestingService/skll/downloads","issues_url":"https://api.github.com/repos/EducationalTestingService/skll/issues{/number}","pulls_url":"https://api.github.com/repos/EducationalTestingService/skll/pulls{/number}","milestones_url":"https://api.github.com/repos/EducationalTestingService/skll/milestones{/number}","notifications_url":"https://api.github.com/repos/EducationalTestingService/skll/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/EducationalTestingService/skll/labels{/name}","releases_url":"https://api.github.com/repos/EducationalTestingService/skll/releases{/id}","created_at":"2013-08-02T14:31:46Z","updated_at":"2015-06-30T00:52:47Z","pushed_at":"2015-07-17T22:20:53Z","git_url":"git://github.com/EducationalTestingService/skll.git","ssh_url":"git@github.com:EducationalTestingService/skll.git","clone_url":"https://github.com/EducationalTestingService/skll.git","svn_url":"https://github.com/EducationalTestingService/skll","homepage":"http://skll.readthedocs.org","size":33458,"stargazers_count":292,"watchers_count":292,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":30,"mirror_url":null,"open_issues_count":20,"forks":30,"open_issues":20,"watchers":292,"default_branch":"master"}},"base":{"label":"EducationalTestingService:master","ref":"master","sha":"2ad3b657910944779527ab567583fb7dc7f8c9f9","user":{"login":"EducationalTestingService","id":4822867,"avatar_url":"https://avatars.githubusercontent.com/u/4822867?v=3","gravatar_id":"","url":"https://api.github.com/users/EducationalTestingService","html_url":"https://github.com/EducationalTestingService","followers_url":"https://api.github.com/users/EducationalTestingService/followers","following_url":"https://api.github.com/users/EducationalTestingService/following{/other_user}","gists_url":"https://api.github.com/users/EducationalTestingService/gists{/gist_id}","starred_url":"https://api.github.com/users/EducationalTestingService/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/EducationalTestingService/subscriptions","organizations_url":"https://api.github.com/users/EducationalTestingService/orgs","repos_url":"https://api.github.com/users/EducationalTestingService/repos","events_url":"https://api.github.com/users/EducationalTestingService/events{/privacy}","received_events_url":"https://api.github.com/users/EducationalTestingService/received_events","type":"Organization","site_admin":false},"repo":{"id":11845170,"name":"skll","full_name":"EducationalTestingService/skll","owner":{"login":"EducationalTestingService","id":4822867,"avatar_url":"https://avatars.githubusercontent.com/u/4822867?v=3","gravatar_id":"","url":"https://api.github.com/users/EducationalTestingService","html_url":"https://github.com/EducationalTestingService","followers_url":"https://api.github.com/users/EducationalTestingService/followers","following_url":"https://api.github.com/users/EducationalTestingService/following{/other_user}","gists_url":"https://api.github.com/users/EducationalTestingService/gists{/gist_id}","starred_url":"https://api.github.com/users/EducationalTestingService/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/EducationalTestingService/subscriptions","organizations_url":"https://api.github.com/users/EducationalTestingService/orgs","repos_url":"https://api.github.com/users/EducationalTestingService/repos","events_url":"https://api.github.com/users/EducationalTestingService/events{/privacy}","received_events_url":"https://api.github.com/users/EducationalTestingService/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/EducationalTestingService/skll","description":"SciKit-Learn Laboratory (SKLL) makes it easy to run machine learning experiments.","fork":false,"url":"https://api.github.com/repos/EducationalTestingService/skll","forks_url":"https://api.github.com/repos/EducationalTestingService/skll/forks","keys_url":"https://api.github.com/repos/EducationalTestingService/skll/keys{/key_id}","collaborators_url":"https://api.github.com/repos/EducationalTestingService/skll/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/EducationalTestingService/skll/teams","hooks_url":"https://api.github.com/repos/EducationalTestingService/skll/hooks","issue_events_url":"https://api.github.com/repos/EducationalTestingService/skll/issues/events{/number}","events_url":"https://api.github.com/repos/EducationalTestingService/skll/events","assignees_url":"https://api.github.com/repos/EducationalTestingService/skll/assignees{/user}","branches_url":"https://api.github.com/repos/EducationalTestingService/skll/branches{/branch}","tags_url":"https://api.github.com/repos/EducationalTestingService/skll/tags","blobs_url":"https://api.github.com/repos/EducationalTestingService/skll/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/EducationalTestingService/skll/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/EducationalTestingService/skll/git/refs{/sha}","trees_url":"https://api.github.com/repos/EducationalTestingService/skll/git/trees{/sha}","statuses_url":"https://api.github.com/repos/EducationalTestingService/skll/statuses/{sha}","languages_url":"https://api.github.com/repos/EducationalTestingService/skll/languages","stargazers_url":"https://api.github.com/repos/EducationalTestingService/skll/stargazers","contributors_url":"https://api.github.com/repos/EducationalTestingService/skll/contributors","subscribers_url":"https://api.github.com/repos/EducationalTestingService/skll/subscribers","subscription_url":"https://api.github.com/repos/EducationalTestingService/skll/subscription","commits_url":"https://api.github.com/repos/EducationalTestingService/skll/commits{/sha}","git_commits_url":"https://api.github.com/repos/EducationalTestingService/skll/git/commits{/sha}","comments_url":"https://api.github.com/repos/EducationalTestingService/skll/comments{/number}","issue_comment_url":"https://api.github.com/repos/EducationalTestingService/skll/issues/comments{/number}","contents_url":"https://api.github.com/repos/EducationalTestingService/skll/contents/{+path}","compare_url":"https://api.github.com/repos/EducationalTestingService/skll/compare/{base}...{head}","merges_url":"https://api.github.com/repos/EducationalTestingService/skll/merges","archive_url":"https://api.github.com/repos/EducationalTestingService/skll/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/EducationalTestingService/skll/downloads","issues_url":"https://api.github.com/repos/EducationalTestingService/skll/issues{/number}","pulls_url":"https://api.github.com/repos/EducationalTestingService/skll/pulls{/number}","milestones_url":"https://api.github.com/repos/EducationalTestingService/skll/milestones{/number}","notifications_url":"https://api.github.com/repos/EducationalTestingService/skll/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/EducationalTestingService/skll/labels{/name}","releases_url":"https://api.github.com/repos/EducationalTestingService/skll/releases{/id}","created_at":"2013-08-02T14:31:46Z","updated_at":"2015-06-30T00:52:47Z","pushed_at":"2015-07-17T22:20:53Z","git_url":"git://github.com/EducationalTestingService/skll.git","ssh_url":"git@github.com:EducationalTestingService/skll.git","clone_url":"https://github.com/EducationalTestingService/skll.git","svn_url":"https://github.com/EducationalTestingService/skll","homepage":"http://skll.readthedocs.org","size":33458,"stargazers_count":292,"watchers_count":292,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":30,"mirror_url":null,"open_issues_count":20,"forks":30,"open_issues":20,"watchers":292,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/EducationalTestingService/skll/pulls/246"},"html":{"href":"https://github.com/EducationalTestingService/skll/pull/246"},"issue":{"href":"https://api.github.com/repos/EducationalTestingService/skll/issues/246"},"comments":{"href":"https://api.github.com/repos/EducationalTestingService/skll/issues/246/comments"},"review_comments":{"href":"https://api.github.com/repos/EducationalTestingService/skll/pulls/246/comments"},"review_comment":{"href":"https://api.github.com/repos/EducationalTestingService/skll/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/EducationalTestingService/skll/pulls/246/commits"},"statuses":{"href":"https://api.github.com/repos/EducationalTestingService/skll/statuses/3a08303a8497d30b18a08bcbcef36688b0164b92"}}}}