{"action":"created","comment":{"url":"https://api.github.com/repos/biolab/orange3/pulls/comments/74391083","id":74391083,"diff_hunk":"@@ -0,0 +1,1292 @@\n+import os\n+import zlib\n+from collections import Sequence\n+from threading import Lock\n+from numbers import Number\n+\n+from itertools import chain\n+import numpy as np\n+import scipy.sparse as sp\n+import pandas as pd\n+import pandas.core.internals\n+\n+from Orange.data import Domain, StringVariable, ContinuousVariable, DiscreteVariable, Variable, TimeVariable\n+from Orange.util import flatten, deprecated\n+\n+\n+# noinspection PyPep8Naming\n+class TableBase:\n+    \"\"\"An abstract base class for data storage structures in Orange.\"\"\"\n+\n+    # a list of all pandas kwargs for use in the pandas compatibility sections\n+    # empty here, subclasses must override with the known kwargs of the\n+    # parent pandas class\n+    _KNOWN_PANDAS_KWARGS = {}\n+\n+    # the default name for weights columns\n+    # subclasses may override this to rename the column\n+    _WEIGHTS_COLUMN = \"__weights__\"\n+\n+    # a collection of all known internal column names\n+    # use case: subclasses add additional columns (not only weights) and we need to know\n+    # of all existing names to be able to process things without these internals\n+    _INTERNAL_COLUMN_NAMES = [_WEIGHTS_COLUMN]\n+\n+    # a counter for indexing rows, important for deterministically selecting rows\n+    # and keeping pandas indices sane\n+    _next_instance_id = 0\n+    _next_instance_lock = Lock()\n+\n+    # the conversion cache for converting tables to different domains\n+    _conversion_cache = None\n+\n+    # custom properties, preserved through pandas manipulations\n+    _metadata = ['name',  # the table name\n+                 'domain',  # the domain assigned to the table\n+                 'attributes',  # any custom attributes the table has\n+                 '__file__']  # if read from a file, the filename of that file\n+\n+    def __new__(cls, *args, **kwargs):\n+        \"\"\"Create a new Table, the exact result type depends on the data passed.\n+\n+        If called without arguments, create and initialize a blank Table, otherwise\n+        intelligently call one of the TableBase.from_X functions, depending on the arguments.\n+        Also passes through pandas.DataFrame constructor keyword arguments.\n+        Does not pass positional arguments through to pandas, unless called from pandas internals.\n+\n+        Parameters\n+        ----------\n+        args\n+            One of:\n+             - (str): create a Table from a file or URL\n+             - (TableBase): initialize from another TableBase,\n+             - (np.ndarray | scipy.sparse): a dense or sparse matrix, resulting\n+               Table data type depends on the type passed, infer a domain\n+                - Up to four may be passed to fit into X/Y/metas/weights respectively.\n+             - (Domain, np.ndarray | scipy.sparse): Same as above, without domain inference\n+             - (Domain, TableBase): convert another TableBase object to the passed domain\n+             - (Domain, list): create a Table based on a list of rows.\n+             - (Domain, pd.DataFrame): create a table based on an existing pandas DataFrame and Domain.\n+        kwargs\n+            Used exclusively for compatibility with pandas.\n+\n+        Returns\n+        -------\n+        Table | SparseTable\n+        A new Table or SparseTable, depending on the arguments passed and the calling class.\n+        \"\"\"\n+        # Needed because we have two construction paths: Table() or Table.from_X,\n+        # complications arise from having a completely different signature from pandas.\n+\n+        # is pandas is calling this as part of its transformations, pass it through\n+        all_kwargs_are_pandas = len(set(kwargs.keys()).difference(cls._KNOWN_PANDAS_KWARGS)) == 0\n+\n+        ##### START PANDAS SUBCLASS COMPATIBILITY SECTION #####\n+        # this compatibility hack must exist because we override __new__ in a way incompatible\n+        # with pandas' subclassing scheme---hacks are needed for compatibility, read on\n+\n+        # if we called the constructor without arguments, create empty\n+        # all possible pandas kwargs will be passed to __init__, where\n+        # pandas will be able to process them\n+        if not args and (not kwargs or all_kwargs_are_pandas):\n+            return super().__new__(cls)\n+\n+        # pandas can call this constructor (through Table._constructor) internally\n+        # when doing transformations: its signatures are either a BlockManager or dict as the sole arg,\n+        # or a ndarray as the sole arg, accompanied by at least one kwarg\n+        # this serves the purpose of transforming the first arg into its matching kwarg,\n+        # which will then be processed by the if clause a couple of lines above this\n+        if len(args) == 1 and (isinstance(args[0], (pd.core.internals.BlockManager, dict))\n+                               or (isinstance(args[0], np.ndarray) and len(kwargs) != 0 and all_kwargs_are_pandas)):\n+            return cls(data=args[0], **kwargs)\n+        ##### END PANDAS SUBCLASS COMPATIBILITY SECTION #####\n+\n+        if 'filename' in kwargs:\n+            args = [kwargs.pop('filename')]\n+\n+        if isinstance(args[0], str):\n+            if args[0].startswith('https://') or args[0].startswith('http://'):\n+                return cls.from_url(args[0], **kwargs)\n+            else:\n+                return cls.from_file(args[0], **kwargs)\n+        elif isinstance(args[0], TableBase):\n+            return cls.from_table(args[0].domain, args[0])\n+        elif isinstance(args[0], pd.DataFrame):\n+            return cls.from_dataframe(None, args[0], **kwargs)\n+        elif isinstance(args[0], Domain):\n+            domain, args = args[0], args[1:]\n+            if not args:\n+                return cls.from_domain(domain, **kwargs)\n+            if isinstance(args[0], TableBase):\n+                return cls.from_table(domain, *args)\n+            elif isinstance(args[0], list):\n+                return cls.from_list(domain, *args)\n+            elif isinstance(args[0], pd.DataFrame):\n+                return cls.from_dataframe(domain, args[0], **kwargs)\n+        else:\n+            domain = None\n+        return cls.from_numpy(domain, *args, **kwargs)\n+\n+    def __init__(self, *args, **kwargs):\n+        \"\"\"Initialize the object.\n+\n+        Sets the name and other attributes and calls pandas' initialization.\n+        Filters the domain: retains only the columns actually in the table.\n+        Sets the weights if they are not set already.\n+        \"\"\"\n+        # see the comment in __new__ for the rationale here\n+        # also, another tidbit is that pandas has some internals that need to be set up\n+        # and expects its arguments to be set appropriately\n+        # because we override the constructor arguments in a completely incompatible way,\n+        # we need to pass ourselves as the data object if we have already set things up\n+        # previously by e.g. creating an empty Table via the __new__ hack in from_X\n+        # functions, then filling up with columns.\n+        # to check for this, we check for domain existence because tables without domains\n+        # can't really be used in Orange in any meaningful way\n+        if hasattr(self, 'domain'):\n+            kwargs['data'] = self\n+\n+        # only pass through things known to pandas, e.g.\n+        # Table(..., weights=1) passes weights to from_numpy, but would error\n+        # when passing to pandas upstream\n+        super().__init__(**{k: v for k, v in kwargs.items() if k in self._KNOWN_PANDAS_KWARGS})\n+\n+        self.name = getattr(self, 'name', kwargs.get(\"name\", \"untitled\"))\n+        self.attributes = getattr(self, 'attributes', kwargs.get(\"attributes\", {}))\n+        self.__file__ = getattr(self, '__file__', kwargs.get(\"__file__\"))\n+\n+        # we need to filter the domain to only include the columns present in the table\n+        # but we still need to allow constructing an empty table (with no domain)\n+        # also, we only set the domain if it has changed (==number of variables),\n+        # so in those cases, id(domain_before) == id(domain_after)\n+        if hasattr(self, 'domain') and self.domain is not None:\n+            new_domain = Domain(\n+                [c for c in self.domain.attributes if c in self.columns],\n+                [c for c in self.domain.class_vars if c in self.columns],\n+                [c for c in self.domain.metas if c in self.columns]\n+            )\n+            if len(new_domain.variables) + len(new_domain.metas) != \\\n+                            len(self.domain.variables) + len(self.domain.metas):\n+                self.domain = new_domain\n+        else:\n+            self.domain = None\n+\n+        # only set the weights if they aren't set already\n+        if self._WEIGHTS_COLUMN not in self.columns:\n+            self.set_weights(1)\n+\n+    @classmethod\n+    def from_domain(cls, domain):\n+        \"\"\"Construct a new Table for the given Domain.\n+\n+        Parameters\n+        ----------\n+        domain : Domain\n+            The domain passed to the new Table object.\n+\n+        Returns\n+        -------\n+        TableBase\n+            The new Table with a Domain assigned.\n+        \"\"\"\n+        result = cls(columns=domain.attributes + domain.class_vars + domain.metas)\n+        result.domain = domain\n+        return result\n+\n+    @classmethod\n+    @deprecated(\"t.iloc[row_indices].copy()\")\n+    def from_table_rows(cls, source, row_indices):\n+        \"\"\"Construct a new Table as a copy of the selected rows (position indexing).\n+\n+        Parameters\n+        ----------\n+        source : TableBase\n+            The source Table.\n+        row_indices\n+            The locations of the selected rows in any of the standard formats.\n+        Returns\n+        -------\n+        TableBase\n+            A subset of the original Table as a copy.\n+        \"\"\"\n+        # don't just plain copy here: in case of subclasses of Table, a plain table is passed\n+        # through the constructor and from_table to here, and expects to be converted\n+        # into a proper subclass type\n+        result = cls(data=source.iloc[row_indices]).copy()\n+        result._transfer_properties(source, transfer_domain=True)  # because we manually copy data, not the whole table\n+        return result\n+\n+    @classmethod\n+    def from_table(cls, target_domain, source_table, row_indices=slice(None)):\n+        \"\"\"Create a new Table as a conversion from the source Table's domain to the new one.\n+\n+        Row indices may be given to select a subset at the same time.\n+        The new domain defines the columns of the new table - new columns are computed\n+        on the fly from source columns whenever appropriate.\n+\n+        Parameters\n+        ----------\n+        target_domain : Domain\n+            The new domain. Defines the resulting columns and the transformation\n+            from the original Table to the new one.\n+        source_table : TableBase\n+            The source table.\n+        row_indices : slice | list[int] | list[bool], optional\n+            The row indices to select a subset of the resulting table.\n+\n+        Returns\n+        -------\n+        TableBase\n+            A table converted from the source domain to the target domain.\n+        \"\"\"\n+        # if a series is passed, convert it to a frame\n+        if isinstance(source_table, SeriesBase):\n+            d = source_table.domain\n+            # to pandas df first to avoid adding a column/row of weights automatically\n+            source_table = cls(source_table.domain, pd.DataFrame(source_table).transpose())\n+\n+        new_cache = cls._conversion_cache is None\n+        try:\n+            if new_cache:\n+                cls._conversion_cache = {}\n+            else:\n+                cached = cls._conversion_cache.get((id(target_domain), id(source_table)))\n+                if cached is not None:\n+                    return cached\n+            if target_domain == source_table.domain:\n+                # intentional casting to subclass\n+                result = cls(data=source_table.iloc[row_indices]).copy()\n+                # because we manually copy data, not the whole table\n+                result._transfer_properties(source_table, transfer_domain=True)\n+                return result\n+\n+            result = cls()\n+            conversion = target_domain.get_conversion(source_table.domain)\n+\n+            for conversion, target_column in zip(chain(conversion.variables, conversion.metas),\n+                                                 chain(target_domain.variables, target_domain.metas)):\n+                if isinstance(conversion, Number):\n+                    # mandatory copy\n+                    result[target_column.name] = source_table[source_table.domain[conversion]].copy()\n+                else:\n+                    # when converting a single instance (row), this results in a single value\n+                    # (and not e.g. a Series), so assigning to a table fails:\n+                    # we ensure it's at least a 1D ndarray, and not a 0D ndarray\n+                    result[target_column.name] = np.atleast_1d(conversion(source_table))\n+            result.domain = target_domain\n+\n+            # if the new domain has 0 columns, the length of the table is also 0\n+            # and we can't set the previous weights or index (which are len(source_table))\n+            if len(result) != 0:\n+                result.set_weights(source_table.weights)\n+                result.index = source_table.index  # keep previous index\n+            result = result.iloc[row_indices]\n+\n+            cls._conversion_cache[(id(target_domain), id(source_table))] = result\n+\n+            # transform any values we believe are null into actual null values\n+            result.replace(to_replace=list(Variable.MISSING_VALUES), value=np.nan, inplace=True)\n+            result._transform_discrete_into_categorical()\n+            result._transform_timevariable_into_datetime()\n+\n+            return result\n+        finally:\n+            if new_cache:\n+                cls._conversion_cache = None\n+\n+    @classmethod\n+    def from_dataframe(cls, domain, df, reindex=False, weights=None):\n+        \"\"\"Create a new Table from a pandas DataFrame.\n+\n+        Parameters\n+        ----------\n+        domain : Domain | None\n+            The domain to use for the result. If None, the domain is inferred.\n+        df : pd.DataFrame\n+            The source pandas DataFrame.\n+        reindex : bool, default False\n+            Whether to override the original DataFrame's index with a new one.\n+        weights : anything TableBase.set_weights accepts, optional, default None\n+            The weights to use on the resulting DataFrame.\n+\n+        Returns\n+        -------\n+        TableBase\n+            A new Table generated from the source DataFrame.\n+        \"\"\"\n+\n+        \"\"\"\n+        Convert a pandas.DataFrame object to a Table.\n+        This can infer infers column variable types and roles, reindex and set weights.\n+        \"\"\"\n+        if domain is None:\n+            result = cls._from_data_inferred(df)\n+        else:\n+            result = cls(data=df)\n+            result.domain = domain\n+\n+        # transform any values we believe are null into actual null values\n+        result.replace(to_replace=list(Variable.MISSING_VALUES), value=np.nan, inplace=True)\n+        result._transform_discrete_into_categorical()\n+        result._transform_timevariable_into_datetime()\n+\n+        if reindex:\n+            result.index = cls._new_id(len(result), force_list=True)\n+        if weights is not None:\n+            result.set_weights(weights)\n+        return result\n+\n+    @classmethod\n+    def _from_data_inferred(cls, X_or_data, Y=None, meta=None, infer_roles=True):\n+        \"\"\"Create a Table and infer its domain.\n+\n+        Create a Table and infer its domain.\n+\n+        X_or_data, Y and meta can be instances of Table, DataFrame,\n+        np.ndarray or a list of rows.\n+\n+        If X_or_data is the sole argument and infer_roles == True,\n+        we will try to infer the column role (x/y/meta) from the data.\n+        If infer_roles = False or Y or meta are given, column roles will be\n+        set to what its container argument represents.\n+\n+        This only does shallow inference on data types. Example:\n+        if given a numpy matrix of dtype object (e.g. mixed numbers and strings),\n+        pandas will interpret all columns as objects, and so will we.\n+\n+        Where possible, column names are preserved form the input, otherwise they are named\n+        \"Feature <n>\", \"Class <n>\", \"Target <n>\" or \"Meta <n>\".\n+        The domain is marked as anonymous.\n+\n+        Parameters\n+        ----------\n+        X_or_data : np.ndarray | pd.DataFrame\n+            Either the X component of the data as a numpy ndarray or a complete pandas DataFrame.\n+            If this is the only data argument, the column roles are also inferred.\n+        Y : np.ndarray, optional, default None\n+            The Y component of the data. Always assigned to Y in the result.\n+        meta : np.ndarray, optional, default None\n+            The meta attributes of the data. Always assigned to metas in the result.\n+        infer_roles : bool, optional, default True\n+            Whether to enable column role inference.\n+\n+        Returns\n+        -------\n+        TableBase\n+            Return a new Table with the inferred domain.\n+        \"\"\"\n+        role_vars = {'x': [], 'y': [], 'meta': []}\n+        result = cls()\n+\n+        # used for data inference, shape checks, consolidated access\n+        X_df = pd.DataFrame(data=X_or_data)\n+        Y_df = pd.DataFrame(data=Y)\n+        meta_df = pd.DataFrame(data=meta)\n+\n+        # transform any values we believe are null into actual null values\n+        X_df.replace(to_replace=list(Variable.MISSING_VALUES), value=np.nan, inplace=True)\n+        Y_df.replace(to_replace=list(Variable.MISSING_VALUES), value=np.nan, inplace=True)\n+        meta_df.replace(to_replace=list(Variable.MISSING_VALUES), value=np.nan, inplace=True)\n+\n+        # override, because the user wishes to specify roles manually\n+        if Y is not None or meta is not None:\n+            infer_roles = False\n+\n+        # process every input segment with its intended role\n+        for df, initial_role in zip((X_df, Y_df, meta_df), ('x', 'y', 'meta')):\n+            for column_name, column in ((c, df[c]) for c in df.columns):\n+                t, r = Domain.infer_type_role(column, initial_role if not infer_roles else None)\n+                name = Domain.infer_name(t, r, column_name,\n+                                         role_vars['x'], role_vars['y'], role_vars['meta'])\n+                if t is DiscreteVariable:\n+                    var = t(name, values=DiscreteVariable.generate_unique_values(column))\n+                else:\n+                    var = t(name)\n+                result[var.name] = column\n+                role_vars[r].append(var)\n+        result.domain = Domain(role_vars['x'], role_vars['y'], role_vars['meta'])\n+        result.domain.anonymous = True","path":"Orange/data/table/base.py","position":408,"original_position":408,"commit_id":"bb6a60e1b2671ffac42251f8a001b17cd3b37dd2","original_commit_id":"bb6a60e1b2671ffac42251f8a001b17cd3b37dd2","user":{"login":"sstanovnik","id":8071628,"avatar_url":"https://avatars.githubusercontent.com/u/8071628?v=3","gravatar_id":"","url":"https://api.github.com/users/sstanovnik","html_url":"https://github.com/sstanovnik","followers_url":"https://api.github.com/users/sstanovnik/followers","following_url":"https://api.github.com/users/sstanovnik/following{/other_user}","gists_url":"https://api.github.com/users/sstanovnik/gists{/gist_id}","starred_url":"https://api.github.com/users/sstanovnik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sstanovnik/subscriptions","organizations_url":"https://api.github.com/users/sstanovnik/orgs","repos_url":"https://api.github.com/users/sstanovnik/repos","events_url":"https://api.github.com/users/sstanovnik/events{/privacy}","received_events_url":"https://api.github.com/users/sstanovnik/received_events","type":"User","site_admin":false},"body":"There are tests for that, but I didn't see any code usages, not even in pickling, which it supposedly tests. @janezd?","created_at":"2016-08-11T09:21:51Z","updated_at":"2016-08-11T09:21:51Z","html_url":"https://github.com/biolab/orange3/pull/1347#discussion_r74391083","pull_request_url":"https://api.github.com/repos/biolab/orange3/pulls/1347","_links":{"self":{"href":"https://api.github.com/repos/biolab/orange3/pulls/comments/74391083"},"html":{"href":"https://github.com/biolab/orange3/pull/1347#discussion_r74391083"},"pull_request":{"href":"https://api.github.com/repos/biolab/orange3/pulls/1347"}}},"pull_request":{"url":"https://api.github.com/repos/biolab/orange3/pulls/1347","id":74257124,"html_url":"https://github.com/biolab/orange3/pull/1347","diff_url":"https://github.com/biolab/orange3/pull/1347.diff","patch_url":"https://github.com/biolab/orange3/pull/1347.patch","issue_url":"https://api.github.com/repos/biolab/orange3/issues/1347","number":1347,"state":"open","locked":false,"title":"[WIP] pandas migration","user":{"login":"sstanovnik","id":8071628,"avatar_url":"https://avatars.githubusercontent.com/u/8071628?v=3","gravatar_id":"","url":"https://api.github.com/users/sstanovnik","html_url":"https://github.com/sstanovnik","followers_url":"https://api.github.com/users/sstanovnik/followers","following_url":"https://api.github.com/users/sstanovnik/following{/other_user}","gists_url":"https://api.github.com/users/sstanovnik/gists{/gist_id}","starred_url":"https://api.github.com/users/sstanovnik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sstanovnik/subscriptions","organizations_url":"https://api.github.com/users/sstanovnik/orgs","repos_url":"https://api.github.com/users/sstanovnik/repos","events_url":"https://api.github.com/users/sstanovnik/events{/privacy}","received_events_url":"https://api.github.com/users/sstanovnik/received_events","type":"User","site_admin":false},"body":"This is the giant ~~(well, WIP)~~ pull request for converting the existing architecture to `pandas`. \r\nSee [this wiki page](https://github.com/biolab/orange3/wiki/pandas-migration) and [this Google Sheet](https://docs.google.com/spreadsheets/d/1yAGeUYut71Imd1c0YGgqp-QgmwtCQZfm9fat_mHvGcQ/edit?usp=sharing) for comments on more general architectural decisions. \r\n\r\n~~So far, the majority of the additional new API is done, but few functions actually work and none have been tested. This will break until I start writing tests and modifying existing code. This currently still does not pass tests, but the Table by itself is completely functional, as well as some other learners. Now in progress: making sure everything works.~~\r\n\r\nOver 95 % of all base widgets now work! ~~Most unit tests pass, the ones that don't are tied to sparse matrices and SQL support. Up next: (re)adding sparse Table and SQL Table support.~~ All unit tests now pass ~~(not including widget tests)~~!\r\n\r\nMulti-type sparse support depends on pandas 0.19.0 (unreleased). That release will fix a lot of sparse issues (some were even fixed by me =D).\r\n\r\nTodos and progress, along with code examples, is monitored in the Gsheet. \r\n\r\nLinked pull requests: biolab/orange3-text#97","created_at":"2016-06-17T15:03:49Z","updated_at":"2016-08-11T09:21:51Z","closed_at":null,"merged_at":null,"merge_commit_sha":"75fdc4752c0615e957e720b67ef4881a8ebc21c0","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/biolab/orange3/pulls/1347/commits","review_comments_url":"https://api.github.com/repos/biolab/orange3/pulls/1347/comments","review_comment_url":"https://api.github.com/repos/biolab/orange3/pulls/comments{/number}","comments_url":"https://api.github.com/repos/biolab/orange3/issues/1347/comments","statuses_url":"https://api.github.com/repos/biolab/orange3/statuses/bb6a60e1b2671ffac42251f8a001b17cd3b37dd2","head":{"label":"sstanovnik:koalas","ref":"koalas","sha":"bb6a60e1b2671ffac42251f8a001b17cd3b37dd2","user":{"login":"sstanovnik","id":8071628,"avatar_url":"https://avatars.githubusercontent.com/u/8071628?v=3","gravatar_id":"","url":"https://api.github.com/users/sstanovnik","html_url":"https://github.com/sstanovnik","followers_url":"https://api.github.com/users/sstanovnik/followers","following_url":"https://api.github.com/users/sstanovnik/following{/other_user}","gists_url":"https://api.github.com/users/sstanovnik/gists{/gist_id}","starred_url":"https://api.github.com/users/sstanovnik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sstanovnik/subscriptions","organizations_url":"https://api.github.com/users/sstanovnik/orgs","repos_url":"https://api.github.com/users/sstanovnik/repos","events_url":"https://api.github.com/users/sstanovnik/events{/privacy}","received_events_url":"https://api.github.com/users/sstanovnik/received_events","type":"User","site_admin":false},"repo":{"id":58224040,"name":"orange3","full_name":"sstanovnik/orange3","owner":{"login":"sstanovnik","id":8071628,"avatar_url":"https://avatars.githubusercontent.com/u/8071628?v=3","gravatar_id":"","url":"https://api.github.com/users/sstanovnik","html_url":"https://github.com/sstanovnik","followers_url":"https://api.github.com/users/sstanovnik/followers","following_url":"https://api.github.com/users/sstanovnik/following{/other_user}","gists_url":"https://api.github.com/users/sstanovnik/gists{/gist_id}","starred_url":"https://api.github.com/users/sstanovnik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sstanovnik/subscriptions","organizations_url":"https://api.github.com/users/sstanovnik/orgs","repos_url":"https://api.github.com/users/sstanovnik/repos","events_url":"https://api.github.com/users/sstanovnik/events{/privacy}","received_events_url":"https://api.github.com/users/sstanovnik/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/sstanovnik/orange3","description":"Orange 3 data mining suite: http://orange.biolab.si ","fork":true,"url":"https://api.github.com/repos/sstanovnik/orange3","forks_url":"https://api.github.com/repos/sstanovnik/orange3/forks","keys_url":"https://api.github.com/repos/sstanovnik/orange3/keys{/key_id}","collaborators_url":"https://api.github.com/repos/sstanovnik/orange3/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/sstanovnik/orange3/teams","hooks_url":"https://api.github.com/repos/sstanovnik/orange3/hooks","issue_events_url":"https://api.github.com/repos/sstanovnik/orange3/issues/events{/number}","events_url":"https://api.github.com/repos/sstanovnik/orange3/events","assignees_url":"https://api.github.com/repos/sstanovnik/orange3/assignees{/user}","branches_url":"https://api.github.com/repos/sstanovnik/orange3/branches{/branch}","tags_url":"https://api.github.com/repos/sstanovnik/orange3/tags","blobs_url":"https://api.github.com/repos/sstanovnik/orange3/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/sstanovnik/orange3/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/sstanovnik/orange3/git/refs{/sha}","trees_url":"https://api.github.com/repos/sstanovnik/orange3/git/trees{/sha}","statuses_url":"https://api.github.com/repos/sstanovnik/orange3/statuses/{sha}","languages_url":"https://api.github.com/repos/sstanovnik/orange3/languages","stargazers_url":"https://api.github.com/repos/sstanovnik/orange3/stargazers","contributors_url":"https://api.github.com/repos/sstanovnik/orange3/contributors","subscribers_url":"https://api.github.com/repos/sstanovnik/orange3/subscribers","subscription_url":"https://api.github.com/repos/sstanovnik/orange3/subscription","commits_url":"https://api.github.com/repos/sstanovnik/orange3/commits{/sha}","git_commits_url":"https://api.github.com/repos/sstanovnik/orange3/git/commits{/sha}","comments_url":"https://api.github.com/repos/sstanovnik/orange3/comments{/number}","issue_comment_url":"https://api.github.com/repos/sstanovnik/orange3/issues/comments{/number}","contents_url":"https://api.github.com/repos/sstanovnik/orange3/contents/{+path}","compare_url":"https://api.github.com/repos/sstanovnik/orange3/compare/{base}...{head}","merges_url":"https://api.github.com/repos/sstanovnik/orange3/merges","archive_url":"https://api.github.com/repos/sstanovnik/orange3/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/sstanovnik/orange3/downloads","issues_url":"https://api.github.com/repos/sstanovnik/orange3/issues{/number}","pulls_url":"https://api.github.com/repos/sstanovnik/orange3/pulls{/number}","milestones_url":"https://api.github.com/repos/sstanovnik/orange3/milestones{/number}","notifications_url":"https://api.github.com/repos/sstanovnik/orange3/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/sstanovnik/orange3/labels{/name}","releases_url":"https://api.github.com/repos/sstanovnik/orange3/releases{/id}","deployments_url":"https://api.github.com/repos/sstanovnik/orange3/deployments","created_at":"2016-05-06T17:35:01Z","updated_at":"2016-05-06T17:35:04Z","pushed_at":"2016-08-10T10:07:33Z","git_url":"git://github.com/sstanovnik/orange3.git","ssh_url":"git@github.com:sstanovnik/orange3.git","clone_url":"https://github.com/sstanovnik/orange3.git","svn_url":"https://github.com/sstanovnik/orange3","homepage":"","size":38985,"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":"master"}},"base":{"label":"biolab:master","ref":"master","sha":"1ee248ab3693066dd07a43a55ea6b2f25a6d759a","user":{"login":"biolab","id":555189,"avatar_url":"https://avatars.githubusercontent.com/u/555189?v=3","gravatar_id":"","url":"https://api.github.com/users/biolab","html_url":"https://github.com/biolab","followers_url":"https://api.github.com/users/biolab/followers","following_url":"https://api.github.com/users/biolab/following{/other_user}","gists_url":"https://api.github.com/users/biolab/gists{/gist_id}","starred_url":"https://api.github.com/users/biolab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/biolab/subscriptions","organizations_url":"https://api.github.com/users/biolab/orgs","repos_url":"https://api.github.com/users/biolab/repos","events_url":"https://api.github.com/users/biolab/events{/privacy}","received_events_url":"https://api.github.com/users/biolab/received_events","type":"Organization","site_admin":false},"repo":{"id":8357227,"name":"orange3","full_name":"biolab/orange3","owner":{"login":"biolab","id":555189,"avatar_url":"https://avatars.githubusercontent.com/u/555189?v=3","gravatar_id":"","url":"https://api.github.com/users/biolab","html_url":"https://github.com/biolab","followers_url":"https://api.github.com/users/biolab/followers","following_url":"https://api.github.com/users/biolab/following{/other_user}","gists_url":"https://api.github.com/users/biolab/gists{/gist_id}","starred_url":"https://api.github.com/users/biolab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/biolab/subscriptions","organizations_url":"https://api.github.com/users/biolab/orgs","repos_url":"https://api.github.com/users/biolab/repos","events_url":"https://api.github.com/users/biolab/events{/privacy}","received_events_url":"https://api.github.com/users/biolab/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/biolab/orange3","description":"Orange 3 data mining suite: http://orange.biolab.si ","fork":false,"url":"https://api.github.com/repos/biolab/orange3","forks_url":"https://api.github.com/repos/biolab/orange3/forks","keys_url":"https://api.github.com/repos/biolab/orange3/keys{/key_id}","collaborators_url":"https://api.github.com/repos/biolab/orange3/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/biolab/orange3/teams","hooks_url":"https://api.github.com/repos/biolab/orange3/hooks","issue_events_url":"https://api.github.com/repos/biolab/orange3/issues/events{/number}","events_url":"https://api.github.com/repos/biolab/orange3/events","assignees_url":"https://api.github.com/repos/biolab/orange3/assignees{/user}","branches_url":"https://api.github.com/repos/biolab/orange3/branches{/branch}","tags_url":"https://api.github.com/repos/biolab/orange3/tags","blobs_url":"https://api.github.com/repos/biolab/orange3/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/biolab/orange3/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/biolab/orange3/git/refs{/sha}","trees_url":"https://api.github.com/repos/biolab/orange3/git/trees{/sha}","statuses_url":"https://api.github.com/repos/biolab/orange3/statuses/{sha}","languages_url":"https://api.github.com/repos/biolab/orange3/languages","stargazers_url":"https://api.github.com/repos/biolab/orange3/stargazers","contributors_url":"https://api.github.com/repos/biolab/orange3/contributors","subscribers_url":"https://api.github.com/repos/biolab/orange3/subscribers","subscription_url":"https://api.github.com/repos/biolab/orange3/subscription","commits_url":"https://api.github.com/repos/biolab/orange3/commits{/sha}","git_commits_url":"https://api.github.com/repos/biolab/orange3/git/commits{/sha}","comments_url":"https://api.github.com/repos/biolab/orange3/comments{/number}","issue_comment_url":"https://api.github.com/repos/biolab/orange3/issues/comments{/number}","contents_url":"https://api.github.com/repos/biolab/orange3/contents/{+path}","compare_url":"https://api.github.com/repos/biolab/orange3/compare/{base}...{head}","merges_url":"https://api.github.com/repos/biolab/orange3/merges","archive_url":"https://api.github.com/repos/biolab/orange3/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/biolab/orange3/downloads","issues_url":"https://api.github.com/repos/biolab/orange3/issues{/number}","pulls_url":"https://api.github.com/repos/biolab/orange3/pulls{/number}","milestones_url":"https://api.github.com/repos/biolab/orange3/milestones{/number}","notifications_url":"https://api.github.com/repos/biolab/orange3/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/biolab/orange3/labels{/name}","releases_url":"https://api.github.com/repos/biolab/orange3/releases{/id}","deployments_url":"https://api.github.com/repos/biolab/orange3/deployments","created_at":"2013-02-22T12:52:56Z","updated_at":"2016-08-11T08:02:26Z","pushed_at":"2016-08-10T11:31:40Z","git_url":"git://github.com/biolab/orange3.git","ssh_url":"git@github.com:biolab/orange3.git","clone_url":"https://github.com/biolab/orange3.git","svn_url":"https://github.com/biolab/orange3","homepage":"","size":94121,"stargazers_count":453,"watchers_count":453,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":150,"mirror_url":null,"open_issues_count":49,"forks":150,"open_issues":49,"watchers":453,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/biolab/orange3/pulls/1347"},"html":{"href":"https://github.com/biolab/orange3/pull/1347"},"issue":{"href":"https://api.github.com/repos/biolab/orange3/issues/1347"},"comments":{"href":"https://api.github.com/repos/biolab/orange3/issues/1347/comments"},"review_comments":{"href":"https://api.github.com/repos/biolab/orange3/pulls/1347/comments"},"review_comment":{"href":"https://api.github.com/repos/biolab/orange3/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/biolab/orange3/pulls/1347/commits"},"statuses":{"href":"https://api.github.com/repos/biolab/orange3/statuses/bb6a60e1b2671ffac42251f8a001b17cd3b37dd2"}}}}
{"actor":{"display_login":"sstanovnik"}}