{"action":"created","comment":{"url":"https://api.github.com/repos/astropy/astropy/pulls/comments/36884399","id":36884399,"diff_hunk":"@@ -0,0 +1,781 @@\n+# Licensed under a 3-clause BSD style license - see LICENSE.rst\n+from __future__ import (absolute_import, division, print_function,\n+                        unicode_literals)\n+from copy import deepcopy\n+import numpy as np\n+\n+from ..extern import six\n+from .bst import BST, FastBST, FastRBT, MinValue, MaxValue\n+from .sorted_array import SortedArray\n+from ..time import Time\n+\n+'''\n+The Index class can use several implementations as its\n+engine. Any implementation should implement the following:\n+\n+__init__(data, row_index) : initialize index based on key/row list pairs\n+add(key, row) -> None : add (key, row) to existing data\n+remove(key, data=None) -> boolean : remove data from self[key], or all of\n+                                    self[key] if data is None\n+shift_left(row) -> None : decrement row numbers after row\n+shift_right(row) -> None : increase row numbers >= row\n+find(key) -> list : list of rows corresponding to key\n+range(lower, upper, bounds) -> list : rows in self[k] where k is between\n+                               lower and upper (<= or < based on bounds)\n+sort() -> list of rows in sorted order (by key)\n+replace_rows(row_map) -> None : replace row numbers based on slice\n+items() -> list of tuples of the form (key, data)\n+\n+Notes\n+-----\n+    When a Table is initialized from another Table, indices are\n+    (deep) copied and their columns are set to the columns of the new Table.\n+\n+    Column creation:\n+    Column(c) -> deep copy of indices\n+    c[[1, 2]] -> deep copy and reordering of indices\n+    c[1:2] -> reference\n+    array.view(Column) -> no indices\n+'''\n+\n+class QueryError(ValueError):\n+    '''\n+    Indicates that a given index cannot handle the supplied query.\n+    '''\n+    pass\n+\n+class Index(object):\n+    '''\n+    The Index class makes it possible to maintain indices\n+    on columns of a Table, so that column values can be queried\n+    quickly and efficiently. Column values are stored in lexicographic\n+    sorted order, which allows for binary searching in O(log n).\n+\n+    Parameters\n+    ----------\n+    columns : list or None\n+        List of columns on which to create an index. If None,\n+        create an empty index for purposes of deep copying.\n+    engine : type or None\n+        Indexing engine class to use, from among SortedArray, BST,\n+        FastBST, and FastRBT. If the supplied argument is None (by\n+        default), use SortedArray.\n+    data : SortedArray, BST, FastBST, FastRBT, or None\n+        Engine data to copy\n+    '''\n+    def __new__(cls, *args, **kwargs):\n+        self = super(Index, cls).__new__(cls)\n+        self.__init__(*args, **kwargs)\n+        return SlicedIndex(self, slice(0, 0, None), original=True)\n+\n+    def __init__(self, columns, engine=None, data=None):\n+        from .table import Table\n+\n+        if data is not None: # create from data\n+            self.engine = data.__class__\n+            self.data = data\n+            self.columns = columns\n+            return\n+\n+        # by default, use SortedArray\n+        self.engine = engine or SortedArray\n+\n+        if columns is None: # this creates a special exception for deep copying\n+            columns = []\n+            data = []\n+            row_index = []\n+        elif len(columns) == 0:\n+            raise ValueError(\"Cannot create index without at least one column\")\n+        else:\n+            num_rows = len(columns[0])\n+\n+            # replace Time columns with approximate form and remainder\n+            new_columns = []\n+            for col in columns:\n+                if isinstance(col, Time):\n+                    new_columns.append(col.jd)\n+                    remainder = col - col.__class__(col.jd, format='jd')\n+                    new_columns.append(remainder.jd)\n+                else:\n+                    new_columns.append(col)\n+\n+            # sort the table lexicographically and keep row numbers\n+            table = Table(columns + [np.arange(num_rows)])\n+            sort_columns = new_columns[::-1]\n+            try:\n+                lines = table[np.lexsort(sort_columns)]\n+            except TypeError: # arbitrary mixins might not work with lexsort\n+                lines = table[table.argsort()]\n+            data = lines[lines.colnames[:-1]]\n+            row_index = lines[lines.colnames[-1]]\n+\n+        self.data = self.engine(data, row_index)\n+        self.columns = columns\n+\n+    def __len__(self):\n+        '''\n+        Number of rows in index.\n+        '''\n+        return len(self.columns[0])\n+\n+    def refresh(self, columns):\n+        '''\n+        Update index to include correct column references.\n+\n+        Parameters\n+        ----------\n+        columns : list\n+            List of column references to use for updating\n+        '''\n+        prev_names = [x.info.name for x in self.columns]\n+        new_names = [x.info.name for x in columns.values()]\n+        name_dict = dict(zip(prev_names, new_names))\n+        self.columns = [columns[name_dict[x.info.name]] for x in self.columns]\n+\n+    def reload(self):\n+        '''\n+        Recreate the index based on data in self.columns.\n+        '''\n+        from .table import Table\n+        num_rows = len(self.columns[0])\n+        table = Table([np.array(x) for x in self.columns] + [np.arange(num_rows)])\n+        lines = table[np.lexsort(self.columns[::-1])]\n+        self.data = self.engine(lines[lines.colnames[:-1]],\n+                                lines[lines.colnames[-1]])\n+\n+    def col_position(self, col_name):\n+        '''\n+        Return the position of col_name in self.columns.\n+\n+        Parameters\n+        ----------\n+        col_name : str\n+            Name of column to look up\n+        '''\n+        for i, c in enumerate(self.columns):\n+            if c.info.name == col_name:\n+                return i\n+        raise ValueError(\"Column does not belong to index: {0}\".format(col_name))\n+\n+    def insert_row(self, pos, vals, columns):\n+        '''\n+        Insert a new row from the given values.\n+\n+        Parameters\n+        ----------\n+        pos : int\n+            Position at which to insert row\n+        vals : list or tuple\n+            List of values to insert into a new row\n+        columns : list\n+            Table column references\n+        '''\n+        key = [None] * len(self.columns)\n+        for i, col in enumerate(columns):\n+            try:\n+                key[i] = vals[self.col_position(col.info.name)]\n+            except ValueError: # not a member of index\n+                continue\n+        num_rows = len(self.columns[0])\n+        if pos < num_rows:\n+            # shift all rows >= pos to the right\n+            self.data.shift_right(pos)\n+        self.data.add(tuple(key), pos)\n+\n+    def get_row_specifier(self, row_specifier):\n+        '''\n+        Return an interable corresponding to the\n+        input row specifier.\n+\n+        Parameters\n+        ----------\n+        row_specifier : int, list, ndarray, or slice\n+        '''\n+        if isinstance(row_specifier, int):\n+            # single row\n+            return (row_specifier,)\n+        elif isinstance(row_specifier, (list, np.ndarray)):\n+            return row_specifier\n+        elif isinstance(row_specifier, slice):\n+            col_len = len(self.columns[0])\n+            return range(*row_specifier.indices(col_len))\n+        raise ValueError(\"Expected int, array of ints, or slice but \"\n+                         \"got {0} in remove_rows\".format(row_specifier))\n+\n+    def remove_rows(self, row_specifier):\n+        '''\n+        Remove the given rows from the index.\n+\n+        Parameters\n+        ----------\n+        row_specifier : int, list, ndarray, or slice\n+            Indicates which row(s) to remove\n+        '''\n+        rows = []\n+\n+        # To maintain the correct row order, we loop twice,\n+        # deleting rows first and then reordering the remaining rows\n+        for row in self.get_row_specifier(row_specifier):\n+            self.remove_row(row, reorder=False)\n+            rows.append(row)\n+        # second pass - row order is reversed to maintain\n+        # correct row numbers\n+        for row in reversed(sorted(rows)):\n+            self.data.shift_left(row)\n+\n+    def remove_row(self, row, reorder=True):\n+        '''\n+        Remove the given row from the index.\n+\n+        Parameters\n+        ----------\n+        row : int\n+            Position of row to remove\n+        reorder : bool\n+            Whether to reorder indices after removal\n+        '''\n+        # for removal, form a key consisting of column values in this row\n+        if not self.data.remove(tuple([col[row] for col in self.columns]), row):\n+            raise ValueError(\"Could not remove row {0} from index\".format(row))\n+        # decrement the row number of all later rows\n+        if reorder:\n+            self.data.shift_left(row)\n+\n+    def find(self, key):\n+        '''\n+        Return the row values corresponding to key, in sorted order.\n+\n+        Parameters\n+        ----------\n+        key : tuple\n+            Values to search for in each column\n+        '''\n+        return self.data.find(key)\n+\n+    def same_prefix(self, key):\n+        '''\n+        Return rows whose keys contain the supplied key as a prefix.\n+\n+        Parameters\n+        ----------\n+        key : tuple\n+            Prefix for which to search\n+        '''\n+        return self.same_prefix_range(key, key, (True, True))\n+\n+    def same_prefix_range(self, lower, upper, bounds=(True, True)):\n+        '''\n+        Return rows whose keys have a prefix in the given range.\n+\n+        Parameters\n+        ----------\n+        lower : tuple\n+            Lower prefix bound\n+        upper : tuple\n+            Upper prefix bound\n+        bounds : tuple (x, y) of bools\n+            Indicates whether the search should be inclusive or\n+            exclusive with respect to the endpoints. The first\n+            argument x corresponds to an inclusive lower bound,\n+            and the second argument y to an inclusive upper bound.\n+        '''\n+        n = len(lower)\n+        ncols = len(self.columns)\n+        a = MinValue() if bounds[0] else MaxValue()\n+        b = MaxValue() if bounds[1] else MinValue()\n+        # [x, y] search corresponds to [(x, min), (y, max)]\n+        # (x, y) search corresponds to ((x, max), (x, min))\n+        lower = lower + tuple((ncols - n) * [a])\n+        upper = upper + tuple((ncols - n) * [b])\n+        return self.data.range(lower, upper, bounds)\n+\n+    def range(self, lower, upper, bounds=(True, True)):\n+        '''\n+        Return rows within the given range.\n+\n+        Parameters\n+        ----------\n+        lower : tuple\n+            Lower prefix bound\n+        upper : tuple\n+            Upper prefix bound\n+        bounds : tuple (x, y) of bools\n+            Indicates whether the search should be inclusive or\n+            exclusive with respect to the endpoints. The first\n+            argument x corresponds to an inclusive lower bound,\n+            and the second argument y to an inclusive upper bound.\n+        '''\n+        return self.data.range(lower, upper, bounds)\n+\n+    def replace(self, row, col_name, val):\n+        '''\n+        Replace the value of a column at a given position.\n+\n+        Parameters\n+        ----------\n+        row : int\n+            Row number to modify\n+        col_name : str\n+            Name of the Column to modify\n+        val : col.info.dtype\n+            Value to insert at specified row of col\n+        '''\n+        self.remove_row(row, reorder=False)\n+        key = [c[row] for c in self.columns]\n+        key[self.col_position(col_name)] = val\n+        self.data.add(tuple(key), row)\n+\n+    def replace_rows(self, col_slice):\n+        '''\n+        Modify rows in this index to agree with the specified\n+        slice. For example, given an index\n+        {'5': 1, '2': 0, '3': 2} on a column ['2', '5', '3'],\n+        an input col_slice of [2, 0] will result in the relabeling\n+        {'3': 0, '2': 1} on the sliced column ['3', '2'].\n+\n+        Parameters\n+        ----------\n+        col_slice : list\n+            Indices to slice\n+        '''\n+        row_map = dict((row, i) for i, row in enumerate(col_slice))\n+        self.data.replace_rows(row_map)\n+\n+    def sorted_data(self):\n+        '''\n+        Returns a list of rows in sorted order based on keys;\n+        essentially acts as an argsort() on columns.\n+        '''\n+        return self.data.sort()\n+\n+    def __getitem__(self, item):\n+        '''\n+        Returns a sliced version of this index.\n+\n+        Parameters\n+        ----------\n+        item : slice\n+            Input slice\n+\n+        Returns\n+        -------\n+        SlicedIndex\n+            A sliced reference to this index.\n+        '''\n+        return SlicedIndex(self, item)\n+\n+    def __str__(self):\n+        return str(self.data)\n+\n+    def __repr__(self):\n+        return str(self)\n+\n+    def __deepcopy__(self, memo):\n+        '''\n+        Return a deep copy of this index.\n+\n+        Notes\n+        -----\n+        The default deep copy must be overridden to perform\n+        a shallow copy of the index columns, avoiding infinite recursion.\n+\n+        Parameters\n+        ----------\n+        memo : dict\n+        '''\n+        num_cols = self.data.num_cols if self.engine == SortedArray else None\n+        # create an actual Index, not a SlicedIndex\n+        index = super(Index, Index).__new__(Index)\n+        index.__init__(None, engine=self.data.__class__)\n+        index.data = deepcopy(self.data, memo)\n+        index.columns = self.columns[:] # new list, same columns\n+        memo[id(self)] = index\n+        return index\n+\n+class SlicedIndex(object):\n+    '''\n+    This class provides a wrapper around an actual Index object\n+    to make index slicing function correctly. Since numpy expects\n+    array slices to provide an actual data view, a SlicedIndex should\n+    retrieve data directly from the original index and then adapt\n+    it to the sliced coordinate system as appropriate.\n+\n+    Parameters\n+    ----------\n+    index : Index\n+        The original Index reference\n+    index_slice : slice\n+        The slice to which this SlicedIndex corresponds\n+    original : bool\n+        Whether this SlicedIndex represents the original index itself.\n+        For the most part this is similar to index[:] but certain\n+        copying operations are avoided, and the slice retains the\n+        length of the actual index despite modification.\n+    '''\n+    def __init__(self, index, index_slice, original=False):\n+        self.index = index\n+        self.original = original\n+        self._frozen = False\n+\n+        if isinstance(index_slice, tuple):\n+            self.start, self._stop, self.step = index_slice\n+        else: # index_slice is an actual slice\n+            num_rows = len(index.columns[0])\n+            self.start, self._stop, self.step = index_slice.indices(num_rows)\n+\n+    @property\n+    def length(self):\n+        return 1 + (self.stop - self.start - 1) // self.step\n+\n+    @property\n+    def stop(self):\n+        '''\n+        The stopping position of the slice, or the end of the\n+        index if this is an original slice.\n+        '''\n+        return len(self.index) if self.original else self._stop\n+\n+    def __getitem__(self, item):\n+        '''\n+        Returns another slice of this Index slice.\n+\n+        Parameters\n+        ----------\n+        item : slice\n+            Index slice\n+        '''\n+        if self.length <= 0:\n+            # empty slice\n+            return SlicedIndex(self.index, slice(1, 0))\n+        start, stop, step = item.indices(self.length)\n+        new_start = self.orig_coords(start)\n+        new_stop = self.orig_coords(stop)\n+        new_step = self.step * step\n+        return SlicedIndex(self.index, (new_start, new_stop, new_step))\n+\n+    def sliced_coords(self, rows):\n+        '''\n+        Convert the input rows to the sliced coordinate system.\n+\n+        Parameters\n+        ----------\n+        rows : list\n+            Rows in the original coordinate system\n+\n+        Returns\n+        -------\n+        sliced_rows : list\n+            Rows in the sliced coordinate system\n+        '''\n+        if self.original:\n+            return rows\n+        else:\n+            rows = np.array(rows)\n+            row0 = rows - self.start\n+            if self.step != 1:\n+                correct_mod = np.mod(row0, self.step) == 0\n+                row0 = row0[correct_mod]\n+            if self.step > 0:\n+                ok = (row0 >= 0) & (row0 < self.stop - self.start)\n+            else:\n+                ok = (row0 <= 0) & (row0 > self.stop - self.start)\n+            return row0[ok] // self.step\n+\n+    def orig_coords(self, row):\n+        '''\n+        Convert the input row from sliced coordinates back\n+        to original coordinates.\n+\n+        Parameters\n+        ----------\n+        row : int\n+            Row in the sliced coordinate system\n+\n+        Returns\n+        -------\n+        orig_row : int\n+            Row in the original coordinate system\n+        '''\n+        return row if self.original else self.start + row * self.step\n+\n+    def find(self, key):\n+        return self.sliced_coords(self.index.find(key))\n+\n+    def where(self, col_map):\n+        return self.sliced_coords(self.index.where(col_map))\n+\n+    def range(self, lower, upper):\n+        return self.sliced_coords(self.index.range(lower, upper))\n+\n+    def same_prefix(self, key):\n+        return self.sliced_coords(self.index.same_prefix(key))\n+\n+    def sorted_data(self):\n+        return self.sliced_coords(self.index.sorted_data())\n+\n+    def replace(self, row, col, val):\n+        if not self._frozen:\n+            self.index.replace(self.orig_coords(row), col, val)\n+\n+    def copy(self):\n+        if not self.original:\n+            # replace self.index with a new object reference\n+            self.index = deepcopy(self.index)\n+        return self.index\n+\n+    def insert_row(self, pos, vals, columns):\n+        if not self._frozen:\n+            self.copy().insert_row(self.orig_coords(pos), vals,\n+                                   columns)\n+\n+    def get_row_specifier(self, row_specifier):\n+        return [self.orig_coords(x) for x in\n+                self.index.get_row_specifier(row_specifier)]\n+\n+    def remove_rows(self, row_specifier):\n+        if not self._frozen:\n+            self.copy().remove_rows(row_specifier)\n+\n+    def replace_rows(self, col_slice):\n+        if not self._frozen:\n+            self.index.replace_rows([self.orig_coords(x) for x in col_slice])\n+\n+    def __repr__(self):\n+        if self.original:\n+            return repr(self.index)\n+        return 'Index slice {0} of {1}'.format(\n+            (self.start, self.stop, self.step), self.index)\n+\n+    def __str__(self):\n+        return repr(self)\n+\n+    def refresh(self, columns):\n+        self.index.refresh(columns)\n+\n+    def reload(self):\n+        self.index.reload()\n+\n+    def col_position(self, col_name):\n+        return self.index.col_position(col_name)\n+\n+    @property\n+    def columns(self):\n+        return self.index.columns\n+\n+    @property\n+    def data(self):\n+        return self.index.data\n+\n+\n+def get_index(table, table_copy):\n+    '''\n+    Inputs a table and some subset of its columns, and\n+    returns an index corresponding to this subset or None\n+    if no such index exists.\n+\n+    Parameters\n+    ----------\n+    table : `Table`\n+        Input table\n+    table_copy : `Table`\n+        Subset of the columns in the table argument\n+    '''\n+    cols = set(table_copy.columns)\n+    indices = set()\n+    for column in cols:\n+        for index in table[column].indices:\n+            if set([x.info.name for x in index.columns]) == cols:\n+                return index\n+    return None\n+\n+class index_mode(object):","path":"astropy/table/index.py","position":591,"original_position":591,"commit_id":"1776150342cc833bb92bb38376319a25ed8aa25f","original_commit_id":"1776150342cc833bb92bb38376319a25ed8aa25f","user":{"login":"mdmueller","id":6729036,"avatar_url":"https://avatars.githubusercontent.com/u/6729036?v=3","gravatar_id":"","url":"https://api.github.com/users/mdmueller","html_url":"https://github.com/mdmueller","followers_url":"https://api.github.com/users/mdmueller/followers","following_url":"https://api.github.com/users/mdmueller/following{/other_user}","gists_url":"https://api.github.com/users/mdmueller/gists{/gist_id}","starred_url":"https://api.github.com/users/mdmueller/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mdmueller/subscriptions","organizations_url":"https://api.github.com/users/mdmueller/orgs","repos_url":"https://api.github.com/users/mdmueller/repos","events_url":"https://api.github.com/users/mdmueller/events{/privacy}","received_events_url":"https://api.github.com/users/mdmueller/received_events","type":"User","site_admin":false},"body":"Ah, thanks for pointing this out--we actually decided on this previously, but I forgot to implement it. I'll add a commit now.","created_at":"2015-08-12T16:56:43Z","updated_at":"2015-08-12T16:56:43Z","html_url":"https://github.com/astropy/astropy/pull/3915#discussion_r36884399","pull_request_url":"https://api.github.com/repos/astropy/astropy/pulls/3915","_links":{"self":{"href":"https://api.github.com/repos/astropy/astropy/pulls/comments/36884399"},"html":{"href":"https://github.com/astropy/astropy/pull/3915#discussion_r36884399"},"pull_request":{"href":"https://api.github.com/repos/astropy/astropy/pulls/3915"}}},"pull_request":{"url":"https://api.github.com/repos/astropy/astropy/pulls/3915","id":39188190,"html_url":"https://github.com/astropy/astropy/pull/3915","diff_url":"https://github.com/astropy/astropy/pull/3915.diff","patch_url":"https://github.com/astropy/astropy/pull/3915.patch","issue_url":"https://api.github.com/repos/astropy/astropy/issues/3915","number":3915,"state":"open","locked":false,"title":"Indexing system for Table","user":{"login":"mdmueller","id":6729036,"avatar_url":"https://avatars.githubusercontent.com/u/6729036?v=3","gravatar_id":"","url":"https://api.github.com/users/mdmueller","html_url":"https://github.com/mdmueller","followers_url":"https://api.github.com/users/mdmueller/followers","following_url":"https://api.github.com/users/mdmueller/following{/other_user}","gists_url":"https://api.github.com/users/mdmueller/gists{/gist_id}","starred_url":"https://api.github.com/users/mdmueller/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mdmueller/subscriptions","organizations_url":"https://api.github.com/users/mdmueller/orgs","repos_url":"https://api.github.com/users/mdmueller/repos","events_url":"https://api.github.com/users/mdmueller/events{/privacy}","received_events_url":"https://api.github.com/users/mdmueller/received_events","type":"User","site_admin":false},"body":"This is a first pass at a new system allowing the `Table` class to hold indices, both for improved grouping performance and for searching by column values. Indices are stored in columns rather than the full table, and indices in turn store a reference to the column(s) they act on, so that composite indices work correctly. Some parts of this PR are final and some are still unfinished, so any comments/suggestions would be great.\r\n\r\n### Complete\r\n* The `Index` class, which provides a wrapper around the sorting engine\r\n* Modifications to `Column`, which ensure that indices are modified as appropriate when a `Column` is modified\r\n* Modifications to `Table`\r\n\r\n### Unfinished\r\n* ~~The `Table.where()` method, particularly finishing up the searching syntax~~\r\n* ~~The actual sorting engine. I have bintrees (which provides fast binary search tree and red-black tree implementations) as an optional dependency, which is replaced by a pure-Python binary search tree class if bintrees isn't present. I also began work on a pure-Python red-black tree and a sorted array class, both unfinished.~~\r\nExperimental parts of this PR have been removed.\r\n\r\n@mdboom @taldcroft ","created_at":"2015-07-03T16:52:28Z","updated_at":"2015-08-12T16:56:43Z","closed_at":null,"merged_at":null,"merge_commit_sha":"ecb1559a7b57c8bcaef1d47c8d417f270ca4c8d5","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/astropy/astropy/pulls/3915/commits","review_comments_url":"https://api.github.com/repos/astropy/astropy/pulls/3915/comments","review_comment_url":"https://api.github.com/repos/astropy/astropy/pulls/comments{/number}","comments_url":"https://api.github.com/repos/astropy/astropy/issues/3915/comments","statuses_url":"https://api.github.com/repos/astropy/astropy/statuses/1776150342cc833bb92bb38376319a25ed8aa25f","head":{"label":"mdmueller:table-indexing","ref":"table-indexing","sha":"1776150342cc833bb92bb38376319a25ed8aa25f","user":{"login":"mdmueller","id":6729036,"avatar_url":"https://avatars.githubusercontent.com/u/6729036?v=3","gravatar_id":"","url":"https://api.github.com/users/mdmueller","html_url":"https://github.com/mdmueller","followers_url":"https://api.github.com/users/mdmueller/followers","following_url":"https://api.github.com/users/mdmueller/following{/other_user}","gists_url":"https://api.github.com/users/mdmueller/gists{/gist_id}","starred_url":"https://api.github.com/users/mdmueller/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mdmueller/subscriptions","organizations_url":"https://api.github.com/users/mdmueller/orgs","repos_url":"https://api.github.com/users/mdmueller/repos","events_url":"https://api.github.com/users/mdmueller/events{/privacy}","received_events_url":"https://api.github.com/users/mdmueller/received_events","type":"User","site_admin":false},"repo":{"id":16990852,"name":"astropy","full_name":"mdmueller/astropy","owner":{"login":"mdmueller","id":6729036,"avatar_url":"https://avatars.githubusercontent.com/u/6729036?v=3","gravatar_id":"","url":"https://api.github.com/users/mdmueller","html_url":"https://github.com/mdmueller","followers_url":"https://api.github.com/users/mdmueller/followers","following_url":"https://api.github.com/users/mdmueller/following{/other_user}","gists_url":"https://api.github.com/users/mdmueller/gists{/gist_id}","starred_url":"https://api.github.com/users/mdmueller/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mdmueller/subscriptions","organizations_url":"https://api.github.com/users/mdmueller/orgs","repos_url":"https://api.github.com/users/mdmueller/repos","events_url":"https://api.github.com/users/mdmueller/events{/privacy}","received_events_url":"https://api.github.com/users/mdmueller/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/mdmueller/astropy","description":"Main Astropy repository","fork":true,"url":"https://api.github.com/repos/mdmueller/astropy","forks_url":"https://api.github.com/repos/mdmueller/astropy/forks","keys_url":"https://api.github.com/repos/mdmueller/astropy/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mdmueller/astropy/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mdmueller/astropy/teams","hooks_url":"https://api.github.com/repos/mdmueller/astropy/hooks","issue_events_url":"https://api.github.com/repos/mdmueller/astropy/issues/events{/number}","events_url":"https://api.github.com/repos/mdmueller/astropy/events","assignees_url":"https://api.github.com/repos/mdmueller/astropy/assignees{/user}","branches_url":"https://api.github.com/repos/mdmueller/astropy/branches{/branch}","tags_url":"https://api.github.com/repos/mdmueller/astropy/tags","blobs_url":"https://api.github.com/repos/mdmueller/astropy/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mdmueller/astropy/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mdmueller/astropy/git/refs{/sha}","trees_url":"https://api.github.com/repos/mdmueller/astropy/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mdmueller/astropy/statuses/{sha}","languages_url":"https://api.github.com/repos/mdmueller/astropy/languages","stargazers_url":"https://api.github.com/repos/mdmueller/astropy/stargazers","contributors_url":"https://api.github.com/repos/mdmueller/astropy/contributors","subscribers_url":"https://api.github.com/repos/mdmueller/astropy/subscribers","subscription_url":"https://api.github.com/repos/mdmueller/astropy/subscription","commits_url":"https://api.github.com/repos/mdmueller/astropy/commits{/sha}","git_commits_url":"https://api.github.com/repos/mdmueller/astropy/git/commits{/sha}","comments_url":"https://api.github.com/repos/mdmueller/astropy/comments{/number}","issue_comment_url":"https://api.github.com/repos/mdmueller/astropy/issues/comments{/number}","contents_url":"https://api.github.com/repos/mdmueller/astropy/contents/{+path}","compare_url":"https://api.github.com/repos/mdmueller/astropy/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mdmueller/astropy/merges","archive_url":"https://api.github.com/repos/mdmueller/astropy/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mdmueller/astropy/downloads","issues_url":"https://api.github.com/repos/mdmueller/astropy/issues{/number}","pulls_url":"https://api.github.com/repos/mdmueller/astropy/pulls{/number}","milestones_url":"https://api.github.com/repos/mdmueller/astropy/milestones{/number}","notifications_url":"https://api.github.com/repos/mdmueller/astropy/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mdmueller/astropy/labels{/name}","releases_url":"https://api.github.com/repos/mdmueller/astropy/releases{/id}","created_at":"2014-02-19T16:35:18Z","updated_at":"2014-10-20T17:13:11Z","pushed_at":"2015-08-12T00:21:35Z","git_url":"git://github.com/mdmueller/astropy.git","ssh_url":"git@github.com:mdmueller/astropy.git","clone_url":"https://github.com/mdmueller/astropy.git","svn_url":"https://github.com/mdmueller/astropy","homepage":"astropy.org","size":39115,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"fast-c-reader"}},"base":{"label":"astropy:master","ref":"master","sha":"7b753b7a81d5af597a4b544e982a158f799da10f","user":{"login":"astropy","id":847984,"avatar_url":"https://avatars.githubusercontent.com/u/847984?v=3","gravatar_id":"","url":"https://api.github.com/users/astropy","html_url":"https://github.com/astropy","followers_url":"https://api.github.com/users/astropy/followers","following_url":"https://api.github.com/users/astropy/following{/other_user}","gists_url":"https://api.github.com/users/astropy/gists{/gist_id}","starred_url":"https://api.github.com/users/astropy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/astropy/subscriptions","organizations_url":"https://api.github.com/users/astropy/orgs","repos_url":"https://api.github.com/users/astropy/repos","events_url":"https://api.github.com/users/astropy/events{/privacy}","received_events_url":"https://api.github.com/users/astropy/received_events","type":"Organization","site_admin":false},"repo":{"id":2081289,"name":"astropy","full_name":"astropy/astropy","owner":{"login":"astropy","id":847984,"avatar_url":"https://avatars.githubusercontent.com/u/847984?v=3","gravatar_id":"","url":"https://api.github.com/users/astropy","html_url":"https://github.com/astropy","followers_url":"https://api.github.com/users/astropy/followers","following_url":"https://api.github.com/users/astropy/following{/other_user}","gists_url":"https://api.github.com/users/astropy/gists{/gist_id}","starred_url":"https://api.github.com/users/astropy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/astropy/subscriptions","organizations_url":"https://api.github.com/users/astropy/orgs","repos_url":"https://api.github.com/users/astropy/repos","events_url":"https://api.github.com/users/astropy/events{/privacy}","received_events_url":"https://api.github.com/users/astropy/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/astropy/astropy","description":"Repository for the Astropy core package","fork":false,"url":"https://api.github.com/repos/astropy/astropy","forks_url":"https://api.github.com/repos/astropy/astropy/forks","keys_url":"https://api.github.com/repos/astropy/astropy/keys{/key_id}","collaborators_url":"https://api.github.com/repos/astropy/astropy/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/astropy/astropy/teams","hooks_url":"https://api.github.com/repos/astropy/astropy/hooks","issue_events_url":"https://api.github.com/repos/astropy/astropy/issues/events{/number}","events_url":"https://api.github.com/repos/astropy/astropy/events","assignees_url":"https://api.github.com/repos/astropy/astropy/assignees{/user}","branches_url":"https://api.github.com/repos/astropy/astropy/branches{/branch}","tags_url":"https://api.github.com/repos/astropy/astropy/tags","blobs_url":"https://api.github.com/repos/astropy/astropy/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/astropy/astropy/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/astropy/astropy/git/refs{/sha}","trees_url":"https://api.github.com/repos/astropy/astropy/git/trees{/sha}","statuses_url":"https://api.github.com/repos/astropy/astropy/statuses/{sha}","languages_url":"https://api.github.com/repos/astropy/astropy/languages","stargazers_url":"https://api.github.com/repos/astropy/astropy/stargazers","contributors_url":"https://api.github.com/repos/astropy/astropy/contributors","subscribers_url":"https://api.github.com/repos/astropy/astropy/subscribers","subscription_url":"https://api.github.com/repos/astropy/astropy/subscription","commits_url":"https://api.github.com/repos/astropy/astropy/commits{/sha}","git_commits_url":"https://api.github.com/repos/astropy/astropy/git/commits{/sha}","comments_url":"https://api.github.com/repos/astropy/astropy/comments{/number}","issue_comment_url":"https://api.github.com/repos/astropy/astropy/issues/comments{/number}","contents_url":"https://api.github.com/repos/astropy/astropy/contents/{+path}","compare_url":"https://api.github.com/repos/astropy/astropy/compare/{base}...{head}","merges_url":"https://api.github.com/repos/astropy/astropy/merges","archive_url":"https://api.github.com/repos/astropy/astropy/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/astropy/astropy/downloads","issues_url":"https://api.github.com/repos/astropy/astropy/issues{/number}","pulls_url":"https://api.github.com/repos/astropy/astropy/pulls{/number}","milestones_url":"https://api.github.com/repos/astropy/astropy/milestones{/number}","notifications_url":"https://api.github.com/repos/astropy/astropy/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/astropy/astropy/labels{/name}","releases_url":"https://api.github.com/repos/astropy/astropy/releases{/id}","created_at":"2011-07-21T01:33:49Z","updated_at":"2015-08-11T09:46:10Z","pushed_at":"2015-08-12T15:38:17Z","git_url":"git://github.com/astropy/astropy.git","ssh_url":"git@github.com:astropy/astropy.git","clone_url":"https://github.com/astropy/astropy.git","svn_url":"https://github.com/astropy/astropy","homepage":"www.astropy.org","size":65315,"stargazers_count":626,"watchers_count":626,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":415,"mirror_url":null,"open_issues_count":605,"forks":415,"open_issues":605,"watchers":626,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/astropy/astropy/pulls/3915"},"html":{"href":"https://github.com/astropy/astropy/pull/3915"},"issue":{"href":"https://api.github.com/repos/astropy/astropy/issues/3915"},"comments":{"href":"https://api.github.com/repos/astropy/astropy/issues/3915/comments"},"review_comments":{"href":"https://api.github.com/repos/astropy/astropy/pulls/3915/comments"},"review_comment":{"href":"https://api.github.com/repos/astropy/astropy/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/astropy/astropy/pulls/3915/commits"},"statuses":{"href":"https://api.github.com/repos/astropy/astropy/statuses/1776150342cc833bb92bb38376319a25ed8aa25f"}}}}