Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #9 Add data source version extension. #10

Merged
merged 2 commits into from
Sep 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
'redash.extensions': [
'dockerflow = redash_stmo.dockerflow:dockerflow',
'datasource_health = redash_stmo.health:datasource_health',
'datasource_link = redash_stmo.datasource_link:datasource_link'
'datasource_link = redash_stmo.datasource_link:datasource_link',
'datasource_version = redash_stmo.datasource_version:datasource_version',
],
},
classifiers=[
Expand Down
10 changes: 4 additions & 6 deletions src/redash_stmo/datasource_link.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from redash_stmo.resources import add_resource

from redash.models import DataSource
from redash.handlers.api import api
from redash.handlers.base import BaseResource, get_object_or_404
from redash.permissions import require_access, view_only
from redash.query_runner import BaseQueryRunner, query_runners
from redash.query_runner import query_runners

DATASOURCE_URLS = {
"bigquery": "https://cloud.google.com/bigquery/docs/reference/legacy-sql",
Expand Down Expand Up @@ -62,7 +63,4 @@ def datasource_link(app=None):
"title": "Documentation URL",
"default": DATASOURCE_URLS[runner_type]})

# After api.init_app() is called, api.app should be set by Flask (but it's not) so that
# further calls to add_resource() are handled immediately for the given app.
api.app = app
api.add_org_resource(DataSourceLinkResource, '/api/data_sources/<data_source_id>/link')
add_resource(app, DataSourceLinkResource, '/api/data_sources/<data_source_id>/link')
63 changes: 63 additions & 0 deletions src/redash_stmo/datasource_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import json
import logging

from redash_stmo.resources import add_resource

from redash.models import DataSource
from redash.handlers.base import BaseResource, get_object_or_404
from redash.permissions import require_access, view_only

logger = logging.getLogger(__name__)


DATASOURCE_VERSION_PARSE_INFO = {
"pg": {
"version_query": "select version()",
"delimiter": " ",
"index": 1
},
"redshift": {
"version_query": "select version()",
"delimiter": " ",
"index": -1
},
"mysql": {
"version_query": "select version()",
"delimiter": "-",
"index": 0
}
}

class DataSourceVersionResource(BaseResource):
def get(self, data_source_id):
data_source = get_object_or_404(
DataSource.get_by_id_and_org,
data_source_id,
self.current_org
)
require_access(data_source.groups, self.current_user, view_only)
version_info = get_data_source_version(data_source.query_runner)
return {"version": version_info}

def get_data_source_version(query_runner):
parse_info = DATASOURCE_VERSION_PARSE_INFO.get(query_runner.type())
if parse_info is None:
return None

data, error = query_runner.run_query(parse_info["version_query"], None)
if error is not None:
logger.error(
"Unable to run version query for %s: %s", query_runner.type(), error)
return None
try:
version = json.loads(data)['rows'][0]['version']
except (KeyError, IndexError) as err:
logger.exception(
"Unable to parse data source version for %s: %s", query_runner.type(), err)
return None

version = version.split(parse_info["delimiter"])[parse_info["index"]]
return version

def datasource_version(app=None):
add_resource(app, DataSourceVersionResource, '/api/data_sources/<data_source_id>/version')
57 changes: 57 additions & 0 deletions src/redash_stmo/datasource_version/bundle/datasource_version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';

class DatasourceVersion extends React.Component {
static propTypes = {
clientConfig: PropTypes.object.isRequired,
datasourceId: PropTypes.number.isRequired,
}

constructor(props) {
super(props);
this.state = {
version: '',
};
}

loadURLData() {
fetch(`${this.props.clientConfig.basePath}api/data_sources/${this.props.datasourceId}/version`)
.then((response) => {
if (response.status === 200) {
return response.json();
}
return {};
})
.catch(error => {
console.error(`Error loading data source version: ${error}`);
return {};
})
.then((json) => {
this.setState({ version: json.version });
});
}

componentDidMount() {
this.loadURLData();
}

componentDidUpdate(prevProps) {
if (this.props.datasourceId !== prevProps.datasourceId) {
this.loadURLData();
}
}

render() {
if (!this.state.version) {
return null;
}
return (
<span>{this.state.version}</span>
);
}
}

export default function init(ngModule) {
ngModule.component('datasourceVersion', react2angular(DatasourceVersion, ['datasourceId'], ['clientConfig']));
}
9 changes: 9 additions & 0 deletions src/redash_stmo/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from redash.handlers import api

def add_resource(app, resource, endpoint):
"""
After api.init_app() is called, api.app should be set by Flask (but it's not) so that
further calls to add_resource() are handled immediately for the given app.
"""
api.app = app
api.add_org_resource(resource, endpoint)
2 changes: 1 addition & 1 deletion tests/test_datasource_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from redash.models import DataSource
from redash.query_runner.pg import PostgreSQL
from redash_stmo.datasource_link import datasource_link, BaseQueryRunner
from redash_stmo.datasource_link import datasource_link


class TestDatasourceLink(BaseTestCase):
Expand Down
70 changes: 70 additions & 0 deletions tests/test_datasource_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import json
import mock

from tests import BaseTestCase
from flask import Flask

from redash.models import DataSource
from redash.query_runner.pg import PostgreSQL
from redash_stmo.datasource_version import datasource_version


class TestDatasourceVersion(BaseTestCase):
EXPECTED_DOC_URL = "www.example.com"
def setUp(self):
super(TestDatasourceVersion, self).setUp()
self.admin = self.factory.create_admin()
self.data_source = self.factory.create_data_source()
self.patched_run_query = self._setup_mock('redash.query_runner.pg.PostgreSQL.run_query')
self.patched_runner_type = self._setup_mock('redash.query_runner.pg.PostgreSQL.type')
datasource_version(self.app)

def _setup_mock(self, function_to_patch):
patcher = mock.patch(function_to_patch)
patched_function = patcher.start()
self.addCleanup(patcher.stop)
return patched_function

def _test_expected_version_returned(self, expected_version, version_string, runner_type):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this method picked up by the test runner with that underscore in the name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. it's a helper method called by the other test methods and the runner doesn't pick it up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D'oh!

self.patched_runner_type.return_value = runner_type
self.patched_run_query.return_value = (json.dumps({
"rows":
[{ "version": version_string.format(version=expected_version) }]
}), None)
rv = self.make_request('get', '/api/data_sources/{}/version'.format(self.data_source.id), user=self.admin)
self.assertEqual(200, rv.status_code)
self.assertEqual(rv.json['version'], expected_version)

def test_gets_postgres_version(self):
RUNNER_TYPE = "pg"
DATASOURCE_VERSION = "9.5.10"
VERSION_STRING = (
"PostgreSQL {version} on x86_64-pc-linux-gnu, compiled by gcc "
"(GCC) 4.8.3 20140911 (Red Hat 4.8.3-9), 64-bit"
)
self._test_expected_version_returned(DATASOURCE_VERSION, VERSION_STRING, RUNNER_TYPE)

def test_gets_redshift_version(self):
RUNNER_TYPE = "redshift"
DATASOURCE_VERSION = "1.0.3688"
VERSION_STRING = (
"PostgreSQL 8.0.2 on i686-pc-linux-gnu, compiled by GCC "
"gcc (GCC) 3.4.2 20041017 (Red Hat 3.4.2-6.fc3), Redshift {version}"
)
self._test_expected_version_returned(DATASOURCE_VERSION, VERSION_STRING, RUNNER_TYPE)

def test_gets_mysql_version(self):
RUNNER_TYPE = "mysql"
DATASOURCE_VERSION = "5.7.16"
VERSION_STRING = "{version}-log"
self._test_expected_version_returned(DATASOURCE_VERSION, VERSION_STRING, RUNNER_TYPE)

def test_unexpected_json(self):
self.patched_runner_type.return_value = "pg"
self.patched_run_query.return_value = (json.dumps({
"rows":
[{ "bad_json": "foo" }]
}), None)
rv = self.make_request('get', '/api/data_sources/{}/version'.format(self.data_source.id), user=self.admin)
self.assertEqual(200, rv.status_code)
self.assertEqual(rv.json['version'], None)