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

Migrate the application to Python 3 #4251

Merged
merged 36 commits into from
Oct 24, 2019
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4953ca3
Make core app compatible with Python 3
NicolasLM Oct 3, 2019
c34027a
Use Python 3.7 as base docker image
NicolasLM Oct 3, 2019
8fa5cdb
Upgrade some requirements to newest versions
NicolasLM Oct 3, 2019
f0bdd2d
Migrate tests to Python 3
NicolasLM Oct 3, 2019
11ad47a
Build frontend on Python 3
NicolasLM Oct 4, 2019
52e6121
Make the HMAC sign function compatible with Python 3
NicolasLM Oct 4, 2019
5dfcb4b
Use assertCountEqual instead of assertItemsEqual
NicolasLM Oct 4, 2019
0f21f82
Remove redundant encoding header for Python 3 modules
NicolasLM Oct 4, 2019
356f0fa
Remove redundant string encoding in CLI
NicolasLM Oct 4, 2019
b984600
Rename list() functions in CLI
NicolasLM Oct 4, 2019
5d10242
Replace usage of Exception.message in CLI
NicolasLM Oct 4, 2019
78665b0
Adapt test handlers to Python 3
NicolasLM Oct 4, 2019
e1e539b
Fix test that relied on dict ordering
NicolasLM Oct 4, 2019
c5b38a4
Make sure test results are always uploaded (#4215)
arikfr Oct 7, 2019
0c32f5e
Support encoding memoryview to JSON
NicolasLM Oct 7, 2019
f78c8fd
Fix test relying on object address ordering
NicolasLM Oct 8, 2019
7c736b6
Decode bytes returned from Redis
NicolasLM Oct 8, 2019
6ead505
Stop using e.message for most exceptions
NicolasLM Oct 8, 2019
4db01c5
Fix writing XLSX files in Python 3
NicolasLM Oct 8, 2019
1774146
Fix test by comparing strings to strings
NicolasLM Oct 8, 2019
9bc15fa
Fix another exception message unavailable in Python 3
NicolasLM Oct 8, 2019
bdf772d
Fix export to CSV in Python 3
NicolasLM Oct 9, 2019
d31da96
(Python 3) Use Redis' decode_responses=True option (#4232)
arikfr Oct 11, 2019
ad3ec88
Fix displaying error while connecting to SQLite
NicolasLM Oct 14, 2019
a8da4e5
Fix another missing exception message
NicolasLM Oct 14, 2019
d2fad33
Handle JSON encoding for datasources returning bytes
NicolasLM Oct 14, 2019
b3e11ed
Fix Python 3 compatibility with RQ
NicolasLM Oct 16, 2019
1827e48
Revert some changes 2to3 tends to do (#4261)
jezdez Oct 18, 2019
2f2a280
Upgrade dependencies
NicolasLM Oct 22, 2019
7fca0ef
Remove useless `iter` added by 2to3
NicolasLM Oct 22, 2019
fb85759
Fix get_next_path tests (#4280)
arikfr Oct 23, 2019
0103901
Switched pytz and certifi to unbinded versions.
arikfr Oct 23, 2019
7f09da6
Switch to new library for getting country from IP
NicolasLM Oct 23, 2019
be32197
Python 3 RQ modifications (#4281)
Oct 24, 2019
77bead0
Remove legacy Python flake8 tests
arikfr Oct 24, 2019
b08aa0b
Merge branch 'master' into python-3-rq
arikfr Oct 24, 2019
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
9 changes: 5 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
mkdir -p /tmp/test-results/unit-tests
docker cp tests:/app/coverage.xml ./coverage.xml
docker cp tests:/app/junit.xml /tmp/test-results/unit-tests/results.xml
when: always
- store_test_results:
path: /tmp/test-results
- store_artifacts:
Expand All @@ -63,8 +64,8 @@ jobs:
- image: circleci/node:8
steps:
- checkout
- run: sudo apt install python-pip
- run: sudo pip install -r requirements_bundles.txt
- run: sudo apt install python3-pip
- run: sudo pip3 install -r requirements_bundles.txt
- run: npm install
- run: npm run bundle
- run: npm test
Expand Down Expand Up @@ -99,8 +100,8 @@ jobs:
steps:
- setup_remote_docker
- checkout
- run: sudo apt install python-pip
- run: sudo pip install -r requirements_bundles.txt
- run: sudo apt install python3-pip
- run: sudo pip3 install -r requirements_bundles.txt
- run: .circleci/update_version
- run: npm run bundle
- run: .circleci/docker_build
Expand Down
31 changes: 30 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,40 @@ COPY client /frontend/client
COPY webpack.config.js /frontend/
RUN npm run build

FROM redash/base:debian
FROM python:3.7-slim

EXPOSE 5000

# Controls whether to install extra dependencies needed for all data sources.
ARG skip_ds_deps

RUN useradd --create-home redash

# Ubuntu packages
RUN apt-get update && \
apt-get install -y \
curl \
gnupg \
build-essential \
pwgen \
libffi-dev \
sudo \
git-core \
wget \
# Postgres client
libpq-dev \
# for SAML
xmlsec1 \
# Additional packages required for data sources:
libssl-dev \
default-libmysqlclient-dev \
freetds-dev \
libsasl2-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

WORKDIR /app

# We first copy only the requirements file, to avoid rebuilding on every file
# change.
COPY requirements.txt requirements_bundles.txt requirements_dev.txt requirements_all_ds.txt ./
Expand Down
5 changes: 2 additions & 3 deletions bin/bundle-extensions
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#!/usr/bin/env python3
"""Copy bundle extension files to the client/app/extension directory"""
import logging
import os
from pathlib2 import Path
from pathlib import Path
from shutil import copy
from collections import OrderedDict as odict

Expand Down
7 changes: 4 additions & 3 deletions bin/get_changes.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#!/bin/env python
from __future__ import print_function
#!/bin/env python3

import sys
import re
import subprocess


def get_change_log(previous_sha):
args = ['git', '--no-pager', 'log', '--merges', '--grep', 'Merge pull request', '--pretty=format:"%h|%s|%b|%p"', 'master...{}'.format(previous_sha)]
log = subprocess.check_output(args)
Expand Down Expand Up @@ -33,4 +34,4 @@ def get_change_log(previous_sha):
changes = get_change_log(previous_sha)

for change in changes:
print(change)
print(change)
2 changes: 1 addition & 1 deletion bin/release_manager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from __future__ import print_function
#!/usr/bin/env python3
import os
import sys
import re
Expand Down
4 changes: 2 additions & 2 deletions bin/upgrade
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import urllib
import argparse
import os
Expand Down Expand Up @@ -27,7 +27,7 @@ def run(cmd, cwd=None):


def confirm(question):
reply = str(raw_input(question + ' (y/n): ')).lower().strip()
reply = str(input(question + ' (y/n): ')).lower().strip()

if reply[0] == 'y':
return True
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Add is_draft status to queries and dashboards

Revision ID: 65fc9ede4746
Revises:
Revises:
Create Date: 2016-12-07 18:08:13.395586

"""
from __future__ import print_function

from alembic import op
import sqlalchemy as sa

Expand All @@ -26,7 +26,7 @@ def upgrade():
op.execute("UPDATE dashboards SET is_draft = false")
except ProgrammingError as e:
# The columns might exist if you ran the old migrations.
if 'column "is_draft" of relation "queries" already exists' in e.message:
if 'column "is_draft" of relation "queries" already exists' in str(e):
print("Can't run this migration as you already have is_draft columns, please run:")
print("./manage.py db stamp {} # you might need to alter the command to match your environment.".format(revision))
exit()
Expand Down
2 changes: 1 addition & 1 deletion migrations/versions/969126bd800f_.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Create Date: 2018-01-31 15:20:30.396533

"""
from __future__ import print_function

import simplejson
from alembic import op
import sqlalchemy as sa
Expand Down
3 changes: 1 addition & 2 deletions redash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import logging
import os
import sys
import urllib
import urlparse

import redis
from flask_mail import Mail
Expand Down Expand Up @@ -41,6 +39,7 @@ def setup_logging():
setup_logging()

redis_connection = redis.from_url(settings.REDIS_URL)
rq_redis_connection = redis.from_url(settings.RQ_REDIS_URL)
mail = Mail()
migrate = Migrate()
statsd_client = StatsClient(host=settings.STATSD_HOST, port=settings.STATSD_PORT, prefix=settings.STATSD_PREFIX)
Expand Down
2 changes: 1 addition & 1 deletion redash/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def __init__(self, *args, **kwargs):
kwargs.update({
'template_folder': settings.STATIC_ASSETS_PATH,
'static_folder': settings.STATIC_ASSETS_PATH,
'static_path': '/static',
'static_url_path': '/static',
})
super(Redash, self).__init__(__name__, *args, **kwargs)
# Make sure we get the right referral address even behind proxies like nginx.
Expand Down
10 changes: 5 additions & 5 deletions redash/authentication/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import hmac
import logging
import time
from urlparse import urlsplit, urlunsplit
from urllib.parse import urlsplit, urlunsplit

from flask import jsonify, redirect, request, url_for
from flask_login import LoginManager, login_user, logout_user, user_logged_in
Expand Down Expand Up @@ -33,8 +33,8 @@ def sign(key, path, expires):
if not key:
return None

h = hmac.new(str(key), msg=path, digestmod=hashlib.sha1)
h.update(str(expires))
h = hmac.new(key.encode(), msg=path.encode(), digestmod=hashlib.sha1)
h.update(str(expires).encode())

return h.hexdigest()

Expand Down Expand Up @@ -93,7 +93,7 @@ def hmac_load_user_from_request(request):
calculated_signature = sign(query.api_key, request.path, expires)

if query.api_key and signature == calculated_signature:
return models.ApiUser(query.api_key, query.org, query.groups.keys(), name="ApiKey: Query {}".format(query.id))
return models.ApiUser(query.api_key, query.org, list(query.groups.keys()), name="ApiKey: Query {}".format(query.id))

return None

Expand All @@ -118,7 +118,7 @@ def get_user_from_api_key(api_key, query_id):
if query_id:
query = models.Query.get_by_id_and_org(query_id, org)
if query and query.api_key == api_key:
user = models.ApiUser(api_key, query.org, query.groups.keys(), name="ApiKey: Query {}".format(query.id))
user = models.ApiUser(api_key, query.org, list(query.groups.keys()), name="ApiKey: Query {}".format(query.id))

return user

Expand Down
8 changes: 4 additions & 4 deletions redash/authentication/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def send_verify_email(user, org):
}
html_content = render_template('emails/verify.html', **context)
text_content = render_template('emails/verify.txt', **context)
subject = u"{}, please verify your email address".format(user.name)
subject = "{}, please verify your email address".format(user.name)

send_mail.delay([user.email], subject, html_content, text_content)

Expand All @@ -57,7 +57,7 @@ def send_invite_email(inviter, invited, invite_url, org):
context = dict(inviter=inviter, invited=invited, org=org, invite_url=invite_url)
html_content = render_template('emails/invite.html', **context)
text_content = render_template('emails/invite.txt', **context)
subject = u"{} invited you to join Redash".format(inviter.name)
subject = "{} invited you to join Redash".format(inviter.name)

send_mail.delay([invited.email], subject, html_content, text_content)

Expand All @@ -67,7 +67,7 @@ def send_password_reset_email(user):
context = dict(user=user, reset_link=reset_link)
html_content = render_template('emails/reset.html', **context)
text_content = render_template('emails/reset.txt', **context)
subject = u"Reset your password"
subject = "Reset your password"

send_mail.delay([user.email], subject, html_content, text_content)
return reset_link
Expand All @@ -76,6 +76,6 @@ def send_password_reset_email(user):
def send_user_disabled_email(user):
html_content = render_template('emails/reset_disabled.html', user=user)
text_content = render_template('emails/reset_disabled.txt', user=user)
subject = u"Your Redash account is disabled"
subject = "Your Redash account is disabled"

send_mail.delay([user.email], subject, html_content, text_content)
4 changes: 2 additions & 2 deletions redash/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from __future__ import print_function


import click
import simplejson
Expand Down Expand Up @@ -53,7 +53,7 @@ def status():
@manager.command()
def check_settings():
"""Show the settings as Redash sees them (useful for debugging)."""
for name, item in current_app.config.iteritems():
for name, item in current_app.config.items():
print("{} = {}".format(name, item))


Expand Down
12 changes: 6 additions & 6 deletions redash/cli/data_sources.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from __future__ import print_function

from sys import exit

import click
Expand All @@ -15,11 +15,11 @@
manager = AppGroup(help="Data sources management commands.")


@manager.command()
@manager.command(name='list')
@click.option('--org', 'organization', default=None,
help="The organization the user belongs to (leave blank for "
"all organizations).")
def list(organization=None):
def list_command(organization=None):
"""List currently configured data sources."""
if organization:
org = models.Organization.get_by_slug(organization)
Expand Down Expand Up @@ -99,11 +99,11 @@ def new(name=None, type=None, options=None, organization='default'):
print("{}. {}".format(i + 1, query_runner_name))

idx = 0
while idx < 1 or idx > len(query_runners.keys()):
while idx < 1 or idx > len(list(query_runners.keys())):
idx = click.prompt("[{}-{}]".format(1, len(query_runners.keys())),
type=int)

type = query_runners.keys()[idx - 1]
type = list(query_runners.keys())[idx - 1]
else:
validate_data_source_type(type)

Expand All @@ -119,7 +119,7 @@ def new(name=None, type=None, options=None, organization='default'):

options_obj = {}

for k, prop in schema['properties'].iteritems():
for k, prop in schema['properties'].items():
required = k in schema.get('required', [])
default_value = "<<DEFAULT_VALUE>>"
if required:
Expand Down
10 changes: 5 additions & 5 deletions redash/cli/groups.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from __future__ import print_function

from sys import exit

from sqlalchemy.orm.exc import NoResultFound
Expand Down Expand Up @@ -36,7 +36,7 @@ def create(name, permissions=None, organization='default'):
permissions=permissions))
models.db.session.commit()
except Exception as e:
print("Failed create group: %s" % e.message)
print("Failed create group: %s" % e)
exit(1)


Expand Down Expand Up @@ -67,7 +67,7 @@ def change_permissions(group_id, permissions=None):
models.db.session.add(group)
models.db.session.commit()
except Exception as e:
print("Failed change permission: %s" % e.message)
print("Failed change permission: %s" % e)
exit(1)


Expand All @@ -80,10 +80,10 @@ def extract_permissions_string(permissions):
return permissions


@manager.command()
@manager.command(name='list')
@option('--org', 'organization', default=None,
help="The organization to limit to (leave blank for all).")
def list(organization=None):
def list_command(organization=None):
"""List all groups"""
if organization:
org = models.Organization.get_by_slug(organization)
Expand Down
6 changes: 3 additions & 3 deletions redash/cli/organization.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from __future__ import print_function

from click import argument
from flask.cli import AppGroup

Expand Down Expand Up @@ -29,8 +29,8 @@ def show_google_apps_domains():
', '.join(organization.google_apps_domains)))


@manager.command()
def list():
@manager.command(name='list')
def list_command():
"""List all organizations"""
orgs = models.Organization.query
for i, org in enumerate(orgs.order_by(models.Organization.name)):
Expand Down
8 changes: 5 additions & 3 deletions redash/cli/rq.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from flask.cli import AppGroup
from rq import Connection, Worker

from redash import redis_connection
from redash import rq_redis_connection
from redash.schedule import rq_scheduler, schedule_periodic_jobs

manager = AppGroup(help="RQ management commands.")
Expand All @@ -22,15 +22,17 @@ def scheduler():
@manager.command()
@argument('queues', nargs=-1)
def worker(queues='default'):
with Connection(redis_connection):
if not queues:
queues = ('default',)
with Connection(rq_redis_connection):
w = Worker(queues)
w.work()


@manager.command()
def healthcheck():
hostname = socket.gethostname()
with Connection(redis_connection):
with Connection(rq_redis_connection):
all_workers = Worker.all()

local_workers = [w for w in all_workers if w.hostname == hostname]
Expand Down
Loading