Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,6 @@ cython_debug/


# default location to write files
cubes/
views/
examples/


Expand Down
3 changes: 1 addition & 2 deletions lkml2cube/parser/explores.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import re
import traceback
import rich

from pprint import pformat

from lkml2cube.parser.views import parse_view
from lkml2cube.parser.types import console

snake_case = r"\{([a-zA-Z]+(?:_[a-zA-Z]+)*\.[a-zA-Z]+(?:_[a-zA-Z]+)*)\}"
console = rich.console.Console()


def snakify(s):
Expand Down
3 changes: 2 additions & 1 deletion lkml2cube/parser/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
from os.path import abspath, dirname, join
from pathlib import Path

from lkml2cube.parser.types import console

visited_path = {}
console = rich.console.Console()


def update_namespace(namespace, new_file):
Expand Down
11 changes: 11 additions & 0 deletions lkml2cube/parser/types.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import rich


# console = rich.console.Console()
class Console:
def print(self, s, *args):
print(s)


console = Console()

type_map = {
"zipcode": "string",
"string": "string",
Expand Down
29 changes: 22 additions & 7 deletions lkml2cube/parser/views.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import copy
import traceback
import rich

from pprint import pformat
from lkml2cube.parser.types import type_map, literal_unicode

console = rich.console.Console()
from lkml2cube.parser.types import type_map, literal_unicode, folded_unicode, console


def parse_view(lookml_model, raise_when_views_not_present=True):
cubes = []
cube_def = {"cubes": cubes}
rpl_table = lambda s: s.replace("${TABLE}", "{CUBE}").replace("${", "{")
convert_to_literal = lambda s: (
literal_unicode(rpl_table(s)) if "\n" in s else rpl_table(s)
)
sets = {}

if raise_when_views_not_present and "views" not in lookml_model:
Expand Down Expand Up @@ -102,10 +102,21 @@ def parse_view(lookml_model, raise_when_views_not_present=True):

cube_dimension = {
"name": dimension["name"],
"sql": rpl_table(dimension["sql"]),
"sql": convert_to_literal(dimension["sql"]),
"type": type_map[dimension["type"]],
}

if "primary_key" in dimension:
cube_dimension["primary_key"] = bool(
dimension["primary_key"] == "yes"
)

if "label" in dimension:
cube_dimension["title"] = dimension["label"]

if "description" in dimension:
cube_dimension["description"] = dimension["description"]

if "hidden" in dimension:
cube_dimension["public"] = not bool(dimension["hidden"] == "yes")

Expand All @@ -121,13 +132,17 @@ def parse_view(lookml_model, raise_when_views_not_present=True):
)
continue
if len(bins) < 2:
console.print(
f'Dimension type: {dimension["type"]} requires more than 1 tiers',
style="bold red",
)
pass
else:
tier_sql = f"CASE "
for i in range(0, len(bins) - 1):
tier_sql += f" WHEN {cube_dimension['sql']} >= {bins[i]} AND {cube_dimension['sql']} < {bins[i + 1]} THEN {bins[i]} "
tier_sql += "ELSE NULL END"
cube_dimension["sql"] = tier_sql
cube_dimension["sql"] = folded_unicode(tier_sql)
cube["dimensions"].append(cube_dimension)

for measure in view.get("measures", []):
Expand All @@ -145,7 +160,7 @@ def parse_view(lookml_model, raise_when_views_not_present=True):
cube_measure["public"] = not bool(measure["hidden"] == "yes")

if measure["type"] != "count":
cube_measure["sql"] = rpl_table(measure["sql"])
cube_measure["sql"] = convert_to_literal(measure["sql"])
elif "drill_fields" in measure:
drill_members = []
for drill_field in measure["drill_fields"]:
Expand Down
Empty file added lkml2cube/tests/__init__.py
Empty file.
45 changes: 45 additions & 0 deletions lkml2cube/tests/e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest
import yaml

from os.path import join, dirname

from lkml2cube.parser.loader import file_loader
from lkml2cube.parser.views import parse_view
from lkml2cube.parser.explores import parse_explores, generate_cube_joins

# Dynamically calculate the root directory
rootdir = join(dirname(__file__), "samples")


class TestExamples:
def test_simple_view(self):
file_path = "lkml/views/orders.view.lkml"
# print(join(rootdir, file_path))
lookml_model = file_loader(join(rootdir, file_path), rootdir)

# lookml_model can't be None
# if None it means file was not found or couldn't be parsed
assert lookml_model is not None

cube_def = parse_view(lookml_model)
cube_def = generate_cube_joins(cube_def, lookml_model)

# Convert the generated cube definition to a dictionary
generated_yaml = yaml.safe_load(yaml.dump(cube_def, allow_unicode=True))

# print("Expected yaml:")
# print(yaml.dump(generated_yaml, allow_unicode=True))

file_path = "cubeml/orders.yml"
# print(join(rootdir, file_path))
with open(join(rootdir, file_path)) as f:
cube_model = yaml.safe_load(f)

# print(cube_model)

# Compare the two dictionaries
assert (
generated_yaml == cube_model
), "Generated YAML does not match the expected YAML"

assert True
34 changes: 34 additions & 0 deletions lkml2cube/tests/samples/cubeml/orders.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
cubes:
- description: Orders
dimensions:
- name: id
primary_key: true
sql: '{CUBE}."ID"'
type: number
- description: My description
name: item_id
sql: '{CUBE}.item_id'
title: Item ID
type: number
- name: order_status
sql: '{CUBE}."STATUS"'
type: string
- name: is_cancelled
sql: case {CUBE}."STATUS" when "CANCELLED" then true else false end
title: Is Cancelled
type: boolean
- name: created_at
sql: '{CUBE}."CREATED_AT"'
type: time
- name: completed_at
sql: '{CUBE}."COMPLETED_AT"'
type: time
joins: []
measures:
- name: count
type: count
- name: order_count_distinct
sql: '{id}'
type: count_distinct_approx
name: orders
sql_table: '{{_user_attributes[''ecom_database'']}}.{{_user_attributes[''ecom_schema'']}}."ORDERS"'
39 changes: 39 additions & 0 deletions lkml2cube/tests/samples/lkml/explores/orders_summary.model.lkml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
connection: "my_connection"

include: "/views/*.view.lkml" # include all views in the views/ folder in this project
# include: "/**/*.view.lkml" # include all views in this project
# include: "/**/*.dashboard.lookml" # include a LookML dashboard called my_dashboard


explore: orders {
label: "Orders Summary"

join: line_items {
relationship: one_to_many
sql_on: ${orders.id} = ${line_items.order_id} ;;
type: left_outer
}

join: products {
relationship: many_to_one
sql_on: ${line_items.product_id} = ${products.id} ;;
type: left_outer
}

}

explore: line_items {
label: "Line Items Summary"

join: orders {
relationship: many_to_one
sql_on: ${line_items.order_id} = ${orders.id} ;;
type: left_outer
}

join: products {
relationship: many_to_one
sql_on: ${line_items.product_id} = ${products.id} ;;
type: left_outer
}
}
90 changes: 90 additions & 0 deletions lkml2cube/tests/samples/lkml/views/line_items.view.lkml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# The name of this view in Looker is "Line Items"
view: line_items {
view_label: "Line Items"
# The sql_table_name parameter indicates the underlying database table
# to be used for all fields in this view.
sql_table_name: {{_user_attributes['ecom_database']}}.{{_user_attributes['ecom_schema']}}."LINE_ITEMS"
;;
# In order to join this view in an Explore,
# define primary_key: yes on a dimension that has no repeated values.

dimension: id {
primary_key: yes
type: number
sql: ${TABLE}."ID" ;;
}

# This table contains a foreign key to other tables.
# Joins are defined in explores
dimension: order_id {
hidden: yes
type: number
sql: ${TABLE}."ORDER_ID" ;;
}

dimension: product_id {
hidden: yes
type: number
sql: ${TABLE}."PRODUCT_ID" ;;
}

dimension: price {
type: number
sql: ${TABLE}."PRICE" ;;
}

dimension: quantity {
label: "Quantity"
type: number
sql: ${TABLE}."QUANTITY" ;;
}

# You can reference other dimensions while defining a dimension.
dimension: line_amount {
type: number
sql: ${quantity} * ${price};;
}

dimension: quantity_bins {
type: tier
style: integer
bins: [0,10,50,100]
sql: ${quantity} ;;
}

# A measure is a field that uses a SQL aggregate function. Here are defined sum and count
# measures for this view, but you can also add measures of many different aggregates.

measure: total_quantity {
type: sum
sql: ${quantity} ;;
}

measure: total_amount {
type: sum
sql: ${line_amount} ;;
}

measure: count {
type: count
}


# Dates and timestamps can be represented in Looker using a dimension group of type: time.
# Looker converts dates and timestamps to the specified timeframes within the dimension group.

dimension_group: created_at {
type: time
timeframes: [
raw,
time,
date,
week,
month,
quarter,
year
]
sql: ${TABLE}."CREATED_AT" ;;
}

}
Loading