Skip to content
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: 3 additions & 0 deletions .landscape.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ autodetect: yes
pylint:
disable:
- cyclic-import
- invalid-name
options:
docstring-min-length: 10
pep8:
full: true
ignore-paths:
- docs
- panoramix/migrations/env.py
Expand Down
2 changes: 2 additions & 0 deletions panoramix/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Package's main module!"""

import logging
import os
from flask import Flask, redirect
Expand Down
4 changes: 4 additions & 0 deletions panoramix/data/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Loads datasets, dashboards and slices in a new panoramix instance"""

import gzip
import json
import os
Expand Down Expand Up @@ -46,6 +48,7 @@ def get_slice_json(defaults, **kwargs):


def load_world_bank_health_n_pop():
"""Loads the world bank health dataset, slices and a dashboard"""
tbl_name = 'wb_health_population'
with gzip.open(os.path.join(DATA_FOLDER, 'countries.json.gz')) as f:
pdf = pd.read_json(f)
Expand Down Expand Up @@ -282,6 +285,7 @@ def load_world_bank_health_n_pop():


def load_css_templates():
"""Loads 2 css templates to demonstrate the feature"""
print('Creating default CSS templates')
CSS = models.CssTemplate

Expand Down
1 change: 1 addition & 0 deletions panoramix/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ def choicify(l):
return [("{}".format(obj), "{}".format(obj)) for obj in l]

def get_form(self):
"""Returns a form object based on the viz/datasource/context"""
viz = self.viz
field_css_classes = {}
for name, obj in self.field_dict.items():
Expand Down
59 changes: 53 additions & 6 deletions panoramix/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
A collection of ORM sqlalchemy models for Panoramix
"""

from copy import deepcopy, copy
from collections import namedtuple
from datetime import timedelta, datetime
Expand Down Expand Up @@ -36,6 +40,12 @@


class AuditMixinNullable(AuditMixin):

"""Altering the AuditMixin to use nullable fields

Allows creating objects programmatically outside of CRUD
"""

created_on = Column(DateTime, default=datetime.now, nullable=True)
changed_on = Column(
DateTime, default=datetime.now,
Expand Down Expand Up @@ -142,6 +152,7 @@ def json_data(self):

@property
def slice_url(self):
"""Defines the url to access the slice"""
try:
slice_params = json.loads(self.params)
except Exception as e:
Expand Down Expand Up @@ -175,7 +186,7 @@ def slice_link(self):

class Dashboard(Model, AuditMixinNullable):

"""A dash to slash"""
"""The dashboard object!"""

__tablename__ = 'dashboards'
id = Column(Integer, primary_key=True)
Expand Down Expand Up @@ -218,6 +229,7 @@ def json_data(self):


class Queryable(object):
"""A common interface to objects that are queryable (tables and datasources)"""
@property
def column_names(self):
return sorted([c.column_name for c in self.columns])
Expand All @@ -240,6 +252,9 @@ def dttm_cols(self):


class Database(Model, AuditMixinNullable):

"""An ORM object that stores Database related information"""

__tablename__ = 'dbs'
id = Column(Integer, primary_key=True)
database_name = Column(String(250), unique=True)
Expand All @@ -256,15 +271,14 @@ def safe_sqlalchemy_uri(self):
return self.sqlalchemy_uri

def grains(self):
"""Defines time granularity database-specific expressions.

"""Defines time granularity database-specific expressions. The idea
here is to make it easy for users to change the time grain form a
datetime (maybe the source grain is arbitrary timestamps, daily
The idea here is to make it easy for users to change the time grain
form a datetime (maybe the source grain is arbitrary timestamps, daily
or 5 minutes increments) to another, "truncated" datetime. Since
each database has slightly different but similar datetime functions,
this allows a mapping between database engines and actual functions.
"""

Grain = namedtuple('Grain', 'name function')
DB_TIME_GRAINS = {
'presto': (
Expand Down Expand Up @@ -314,6 +328,9 @@ def sql_link(self):


class SqlaTable(Model, Queryable, AuditMixinNullable):

"""An ORM object for SqlAlchemy table references"""

type = "table"

__tablename__ = 'tables'
Expand Down Expand Up @@ -554,6 +571,7 @@ def query(
df=df, duration=datetime.now() - qry_start_dttm, query=sql)

def fetch_metadata(self):
"""Fetches the metadata for the table and merges it in"""
table = self.database.get_table(self.table_name)
try:
table = self.database.get_table(self.table_name)
Expand Down Expand Up @@ -653,6 +671,9 @@ def fetch_metadata(self):


class SqlMetric(Model, AuditMixinNullable):

"""ORM object for metrics, each table can have multiple metrics"""

__tablename__ = 'sql_metrics'
id = Column(Integer, primary_key=True)
metric_name = Column(String(512))
Expand All @@ -666,6 +687,9 @@ class SqlMetric(Model, AuditMixinNullable):


class TableColumn(Model, AuditMixinNullable):

"""ORM object for table columns, each table can have multiple columns"""

__tablename__ = 'table_columns'
id = Column(Integer, primary_key=True)
table_id = Column(Integer, ForeignKey('tables.id'))
Expand Down Expand Up @@ -693,6 +717,9 @@ def isnum(self):


class DruidCluster(Model, AuditMixinNullable):

"""ORM object referencing the Druid clusters"""

__tablename__ = 'clusters'
id = Column(Integer, primary_key=True)
cluster_name = Column(String(250), unique=True)
Expand Down Expand Up @@ -726,6 +753,9 @@ def refresh_datasources(self):


class DruidDatasource(Model, AuditMixinNullable, Queryable):

"""ORM object referencing Druid datasources (tables)"""

type = "druid"

baselink = "datasourcemodelview"
Expand Down Expand Up @@ -793,6 +823,7 @@ def get_metric_obj(self, metric_name):
][0]

def latest_metadata(self):
"""Returns segment metadata from the latest segment"""
client = self.cluster.get_pydruid_client()
results = client.time_boundary(datasource=self.datasource_name)
if not results:
Expand All @@ -813,6 +844,7 @@ def generate_metrics(self):

@classmethod
def sync_to_db(cls, name, cluster):
"""Fetches metadata for that datasource and merges the Panoramix db"""
print("Syncing Druid datasource [{}]".format(name))
session = get_session()
datasource = session.query(cls).filter_by(datasource_name=name).first()
Expand Down Expand Up @@ -855,8 +887,13 @@ def query(
timeseries_limit=None,
row_limit=None,
inner_from_dttm=None, inner_to_dttm=None,
extras=None,
extras=None, # noqa
select=None):
"""Runs a query against Druid and returns a dataframe.

This query interface is common to SqlAlchemy and Druid
"""
# TODO refactor into using a TBD Query object
qry_start_dttm = datetime.now()

inner_from_dttm = inner_from_dttm or from_dttm
Expand Down Expand Up @@ -996,6 +1033,9 @@ def query(


class Log(Model):

"""ORM object used to log Panoramix actions to the database"""

__tablename__ = 'logs'

id = Column(Integer, primary_key=True)
Expand Down Expand Up @@ -1033,6 +1073,9 @@ def wrapper(*args, **kwargs):


class DruidMetric(Model):

"""ORM object referencing Druid metrics for a datasource"""

__tablename__ = 'metrics'
id = Column(Integer, primary_key=True)
metric_name = Column(String(512))
Expand All @@ -1055,6 +1098,9 @@ def json_obj(self):


class DruidColumn(Model):

"""ORM model for storing Druid datasource column metadata"""

__tablename__ = 'columns'
id = Column(Integer, primary_key=True)
datasource_name = Column(
Expand All @@ -1080,6 +1126,7 @@ def isnum(self):
return self.type in ('LONG', 'DOUBLE', 'FLOAT')

def generate_metrics(self):
"""Generate metrics based on the column metadata"""
M = DruidMetric
metrics = []
metrics.append(DruidMetric(
Expand Down
26 changes: 15 additions & 11 deletions panoramix/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Utility functions used across Panoramix"""

from datetime import datetime
import hashlib
import functools
Expand All @@ -12,10 +14,12 @@


class memoized(object):

"""Decorator that caches a function's return value each time it is called.
If called later with the same arguments, the cached value is returned, and
not re-evaluated.
"""

def __init__(self, func):
self.func = func
self.cache = {}
Expand Down Expand Up @@ -47,8 +51,7 @@ def list_minus(l, minus):

def parse_human_datetime(s):
"""
Use the parsedatetime lib to return ``datetime.datetime`` from human
generated strings
Returns ``datetime.datetime`` from human readable strings

>>> from datetime import date, timedelta
>>> from dateutil.relativedelta import relativedelta
Expand Down Expand Up @@ -92,8 +95,7 @@ def merge_perm(sm, permission_name, view_menu_name):

def parse_human_timedelta(s):
"""
Use the parsedatetime lib to return ``datetime.datetime`` from human
generated strings
Returns ``datetime.datetime`` from natural language time deltas

>>> parse_human_datetime("now") <= datetime.now()
True
Expand All @@ -107,7 +109,9 @@ def parse_human_timedelta(s):


class JSONEncodedDict(TypeDecorator):

"""Represents an immutable structure as a json-encoded string."""

impl = TEXT
def process_bind_param(self, value, dialect):
if value is not None:
Expand All @@ -122,6 +126,9 @@ def process_result_value(self, value, dialect):


class ColorFactory(object):

"""Used to generated arrays of colors server side"""

BNB_COLORS = [
#rausch hackb kazan babu lima beach barol
'#ff5a5f', '#7b0051', '#007A87', '#00d1c1', '#8ce071', '#ffb400', '#b4a76c',
Expand All @@ -134,7 +141,8 @@ def __init__(self, hash_based=False):
self.hash_based = hash_based

def get(self, s):
"""
"""Gets a color from a string and memoize the association

>>> cf = ColorFactory()
>>> cf.get('item_1')
'#ff5a5f'
Expand All @@ -155,9 +163,7 @@ def get(self, s):


def init(panoramix):
"""
Inits the Panoramix application with security roles and such
"""
"""Inits the Panoramix application with security roles and such"""
db = panoramix.db
models = panoramix.models
sm = panoramix.appbuilder.sm
Expand Down Expand Up @@ -204,9 +210,7 @@ def init(panoramix):


def datetime_f(dttm):
"""
Formats datetime to take less room is recent
"""
"""Formats datetime to take less room when it is recent"""
if dttm:
dttm = dttm.isoformat()
now_iso = datetime.now().isoformat()
Expand Down
Loading