lookml-generator/generator/views/view.py

130 строки
4.1 KiB
Python

"""Generic class to describe Looker views."""
from __future__ import annotations
from typing import Any, Dict, Iterator, List, Optional, Set, TypedDict
from click import ClickException
OMIT_VIEWS: Set[str] = set()
# TODO: Once we upgrade to Python 3.11 mark just `measures` as non-required, not all keys.
class ViewDict(TypedDict, total=False):
"""Represent a view definition."""
type: str
tables: List[Dict[str, str]]
measures: Dict[str, Dict[str, Any]]
class View(object):
"""A generic Looker View."""
name: str
view_type: str
tables: List[Dict[str, Any]]
namespace: str
def __init__(
self,
namespace: str,
name: str,
view_type: str,
tables: List[Dict[str, Any]],
**kwargs,
):
"""Create an instance of a view."""
self.namespace = namespace
self.tables = tables
self.name = name
self.view_type = view_type
@classmethod
def from_db_views(
klass,
namespace: str,
is_glean: bool,
channels: List[Dict[str, str]],
db_views: dict,
) -> Iterator[View]:
"""Get Looker views from app."""
raise NotImplementedError("Only implemented in subclass.")
@classmethod
def from_dict(klass, namespace: str, name: str, _dict: ViewDict) -> View:
"""Get a view from a name and dict definition."""
raise NotImplementedError("Only implemented in subclass.")
def get_type(self) -> str:
"""Get the type of this view."""
return self.view_type
def as_dict(self) -> dict:
"""Get this view as a dictionary."""
return {
"type": self.view_type,
"tables": self.tables,
}
def __str__(self):
"""Stringify."""
return f"name: {self.name}, type: {self.type}, table: {self.tables}, namespace: {self.namespace}"
def __eq__(self, other) -> bool:
"""Check for equality with other View."""
def comparable_dict(d):
return {tuple(sorted([(k, str(v)) for k, v in t.items()])) for t in d}
if isinstance(other, View):
return (
self.name == other.name
and self.view_type == other.view_type
and comparable_dict(self.tables) == comparable_dict(other.tables)
and self.namespace == other.namespace
)
return False
def get_dimensions(
self, bq_client, table, v1_name: Optional[str]
) -> List[Dict[str, Any]]:
"""Get the set of dimensions for this view."""
raise NotImplementedError("Only implemented in subclass.")
def to_lookml(self, bq_client, v1_name: Optional[str]) -> Dict[str, Any]:
"""
Generate Lookml for this view.
View instances can generate more than one Looker view,
for e.g. nested fields and joins, so this returns
a list.
"""
raise NotImplementedError("Only implemented in subclass.")
def get_client_id(self, dimensions: List[dict], table: str) -> Optional[str]:
"""Return the first field that looks like a client identifier."""
client_id_fields = [
d["name"]
for d in dimensions
if d["name"] in {"client_id", "client_info__client_id", "context_id"}
]
if not client_id_fields:
# Some pings purposely disinclude client_ids, e.g. firefox installer
return None
if len(client_id_fields) > 1:
raise ClickException(f"Duplicate client_id dimension in {table!r}")
return client_id_fields[0]
def get_document_id(self, dimensions: List[dict], table: str) -> Optional[str]:
"""Return the first field that looks like a document_id."""
document_id_fields = [
d["name"] for d in dimensions if d["name"] in {"document_id"}
]
if not document_id_fields:
# Some pings purposely disinclude client_ids, e.g. firefox installer
return None
if len(document_id_fields) > 1:
raise ClickException(f"Duplicate document_id dimension in {table!r}")
return document_id_fields[0]