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

Skip building the project for the SQL validation if it isn't needed #831

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8598638
add callback logging to backoff
jsnb-devoted Nov 27, 2024
14eb8f0
skip lookml_models for incremental content validation
jsnb-devoted Nov 27, 2024
4813d2b
add empty models
jsnb-devoted Nov 27, 2024
52c2770
dont call lookml_models api for sql validator if explores are provided
jsnb-devoted Nov 27, 2024
0df4891
add some logging
jsnb-devoted Nov 27, 2024
3dacc2f
fix dict initialization
jsnb-devoted Nov 27, 2024
d8b3fb1
fix models type and dont return
jsnb-devoted Nov 27, 2024
7b5127a
dont filter for models/explores in content validation if the project …
jsnb-devoted Nov 27, 2024
385fd7c
adding log
jsnb-devoted Nov 27, 2024
8caccad
another log
jsnb-devoted Nov 27, 2024
ceddf54
fix log
jsnb-devoted Nov 27, 2024
4c7069d
check for default */*
jsnb-devoted Nov 27, 2024
7d4b973
fix conditional
jsnb-devoted Nov 27, 2024
c7f0417
ignore if */*
jsnb-devoted Nov 27, 2024
18da8ff
set is_complete_project better
jsnb-devoted Nov 27, 2024
881d184
revert content changes
jsnb-devoted Dec 2, 2024
9c6d31e
add callback logging to backoff
jsnb-devoted Nov 27, 2024
15b9873
skip lookml_models for incremental content validation
jsnb-devoted Nov 27, 2024
d7804ee
add empty models
jsnb-devoted Nov 27, 2024
237e4f4
dont call lookml_models api for sql validator if explores are provided
jsnb-devoted Nov 27, 2024
882af7a
add some logging
jsnb-devoted Nov 27, 2024
a9593b9
fix dict initialization
jsnb-devoted Nov 27, 2024
dd34557
fix models type and dont return
jsnb-devoted Nov 27, 2024
2caa8d3
dont filter for models/explores in content validation if the project …
jsnb-devoted Nov 27, 2024
e7b6271
adding log
jsnb-devoted Nov 27, 2024
0c238af
another log
jsnb-devoted Nov 27, 2024
a032a5c
fix log
jsnb-devoted Nov 27, 2024
ba25934
check for default */*
jsnb-devoted Nov 27, 2024
4688e7b
fix conditional
jsnb-devoted Nov 27, 2024
cb484e8
ignore if */*
jsnb-devoted Nov 27, 2024
a49c7e8
set is_complete_project better
jsnb-devoted Nov 27, 2024
f5c5b3c
revert content changes
jsnb-devoted Dec 2, 2024
73e1db6
add logging for error
jsnb-devoted Dec 4, 2024
f800ed3
Merge branch 'devoted-sql-validation-skip-build-project' of github.co…
jsnb-devoted Dec 4, 2024
3197eac
make it an info log
jsnb-devoted Dec 4, 2024
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: 9 additions & 0 deletions spectacles/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,26 @@ def expired(self) -> bool:
return False if time.time() < self.expires_at else True


def log_backoff(details: dict) -> None:
logger.debug(
f"Backing off {details['wait']:0.1f} seconds after {details['tries']} tries. "
f"Error: {details['exception'].__class__.__name__}"
)


def backoff_with_exceptions(func: Callable[..., Any]) -> Callable[..., Any]:
@backoff.on_exception(
backoff.expo,
STATUS_EXCEPTIONS,
giveup=giveup_unless_bad_gateway,
max_tries=DEFAULT_RETRIES,
on_backoff=log_backoff,
)
@backoff.on_exception(
backoff.expo,
NETWORK_EXCEPTIONS,
max_tries=DEFAULT_NETWORK_RETRIES,
on_backoff=log_backoff,
)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
if asyncio.iscoroutinefunction(func):
Expand Down
62 changes: 45 additions & 17 deletions spectacles/lookml.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ class Project(LookMlObject):
def __init__(self, name: str, models: Sequence[Model]) -> None:
self.name = name
self.models = models
self._is_complete = False

def __eq__(self, other: Any) -> bool:
if not isinstance(other, Project):
Expand Down Expand Up @@ -344,6 +345,14 @@ def iter_dimensions(self, errored: bool = False) -> Iterable[Dimension]:
else:
yield dimension

@property
def is_complete_project(self) -> bool:
return self._is_complete

@is_complete_project.setter
def is_complete_project(self, value: bool) -> None:
self._is_complete = value

@property
def errored(self) -> Optional[bool]:
if self.queried:
Expand Down Expand Up @@ -507,28 +516,45 @@ async def build_project(
include_dimensions: bool = False,
ignore_hidden_fields: bool = False,
include_all_explores: bool = False,
get_full_project: bool = True,
) -> Project:
"""Creates an object (tree) representation of a LookML project."""
if filters is None:
filters = ["*/*"]
is_complete_project = False

if get_full_project:
models = []
fields = ["name", "project_name", "explores"]
for lookmlmodel in await client.get_lookml_models(fields=fields):
model = Model.from_json(lookmlmodel)
if model.project_name == name:
models.append(model)

if not models:
raise LookMlNotFound(
name="project-models-not-found",
title="No configured models found for the specified project.",
detail=(
f"Go to {client.base_url}/projects and confirm "
"a) at least one model exists for the project and "
"b) it has an active configuration."
),
)
is_complete_project = True

models = []
fields = ["name", "project_name", "explores"]
for lookmlmodel in await client.get_lookml_models(fields=fields):
model = Model.from_json(lookmlmodel)
if model.project_name == name:
models.append(model)

if not models:
raise LookMlNotFound(
name="project-models-not-found",
title="No configured models found for the specified project.",
detail=(
f"Go to {client.base_url}/projects and confirm "
"a) at least one model exists for the project and "
"b) it has an active configuration."
),
)
else:
# Create a project with only the models specified in the filters
logger.debug("Building project with only the filtered models")
models: Dict[str, Model] = {}
for filter in filters:
model, explore = filter.split("/")
if model not in models:
models[model] = Model(name=model, project_name=name, explores=[])
if explore not in models[model].explores:
models[model].explores.append(Explore(name=explore, model_name=model))
project = Project(name=name, models=models.values())
models = project.models

# Prune to selected explores for non-content validators
if not include_all_explores:
Expand All @@ -555,4 +581,6 @@ async def build_project(
else:
project = Project(name, [m for m in models if len(m.explores) > 0])

# Indicates whether the project has all of the models/explores or just the selected ones
project.is_complete_project = is_complete_project
return project
19 changes: 16 additions & 3 deletions spectacles/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from spectacles.client import LOOKML_VALIDATION_TIMEOUT, LookerClient
from spectacles.exceptions import LookerApiError, SpectaclesException, SqlError
from spectacles.logger import GLOBAL_LOGGER as logger
from spectacles.lookml import CompiledSql, Explore, build_project
from spectacles.lookml import CompiledSql, Explore, build_project, Project
from spectacles.models import JsonDict, SkipReason
from spectacles.printer import print_header
from spectacles.utils import time_hash
Expand Down Expand Up @@ -355,6 +355,10 @@ async def validate_sql(
) -> JsonDict:
if filters is None:
filters = ["*/*"]
else:
# Only build the full project from the API if we're using a wildcard filter and not in incremental mode
get_full_project = any("*" in f for f in filters) or incremental

validator = SqlValidator(self.client, concurrency, runtime_threshold)
ephemeral = True if incremental else None
# Create explore-level tests for the desired ref
Expand All @@ -367,6 +371,7 @@ async def validate_sql(
filters=filters,
include_dimensions=True,
ignore_hidden_fields=ignore_hidden_fields,
get_full_project=get_full_project,
)
base_explores: Set[CompiledSql] = set()
if incremental:
Expand Down Expand Up @@ -532,8 +537,16 @@ async def validate_content(
exclude_personal: bool = False,
folders: Optional[List[str]] = None,
) -> JsonDict:
if filters is None:
filters = ["*/*"]
logger.debug(
f"Validating content. ref={ref}, filters={filters}, incremental={incremental}",
)
if filters is not None or filters == ["*/*"]:
# Only build the full project from the API if we're using a wildcard filter and not in incremental mode
get_full_project = any("*" in f for f in filters if f != "*/*") or (
not incremental
)
logger.debug(f"get_full_project = {get_full_project}")

if folders is None:
folders = []

Expand Down