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

[CT-920][CT-1900] Create Click CLI runner and use it to fix dbt docs … #6723

Merged
merged 8 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
6 changes: 6 additions & 0 deletions .changes/unreleased/Under the Hood-20230125-041136.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Under the Hood
body: '[CT-920][CT-1900] Create Click CLI runner and use it to fix dbt docs commands'
time: 2023-01-25T04:11:36.57506-08:00
custom:
Author: aranke
Issue: 5544 6722
23 changes: 20 additions & 3 deletions core/dbt/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from dbt.task.compile import CompileTask
from dbt.task.deps import DepsTask
from dbt.task.run import RunTask
from dbt.task.serve import ServeTask
from dbt.task.test import TestTask
from dbt.task.snapshot import SnapshotTask
from dbt.task.seed import SeedTask
Expand Down Expand Up @@ -170,6 +171,7 @@ def docs(ctx, **kwargs):
@p.models
@p.profile
@p.profiles_dir
@p.project_dir
@p.select
@p.selector
@p.state
Expand All @@ -185,7 +187,11 @@ def docs(ctx, **kwargs):
@requires.manifest
def docs_generate(ctx, **kwargs):
"""Generate the documentation website for your project"""
task = GenerateTask(ctx.obj["flags"], ctx.obj["runtime_config"])
task = GenerateTask(
ctx.obj["flags"],
ctx.obj["runtime_config"],
ctx.obj["manifest"],
)

results = task.run()
success = task.interpret_results(results)
Expand All @@ -203,10 +209,21 @@ def docs_generate(ctx, **kwargs):
@p.target
@p.vars
@requires.preflight
@requires.profile
@requires.project
@requires.runtime_config
@requires.manifest
def docs_serve(ctx, **kwargs):
"""Serve the documentation website for your project"""
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {ctx.obj['flags']}")
return None, True
task = ServeTask(
ctx.obj["flags"],
ctx.obj["runtime_config"],
ctx.obj["manifest"],
)

results = task.run()
success = task.interpret_results(results)
return results, success


# dbt compile
Expand Down
24 changes: 0 additions & 24 deletions core/dbt/events/proto_types.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 0 additions & 27 deletions core/dbt/events/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2463,33 +2463,6 @@ def message(self) -> str:
return ""


@dataclass
class ServingDocsPort(InfoLevel, pt.ServingDocsPort):
def code(self):
return "Z018"

def message(self) -> str:
return f"Serving docs at {self.address}:{self.port}"


@dataclass
class ServingDocsAccessInfo(InfoLevel, pt.ServingDocsAccessInfo):
def code(self):
return "Z019"

def message(self) -> str:
return f"To access from your browser, navigate to: http://localhost:{self.port}"


@dataclass
class ServingDocsExitInfo(InfoLevel, pt.ServingDocsExitInfo):
def code(self):
return "Z020"

def message(self) -> str:
return "Press Ctrl+C to exit."


@dataclass
class RunResultWarning(WarnLevel, pt.RunResultWarning):
def code(self):
Expand Down
46 changes: 14 additions & 32 deletions core/dbt/task/serve.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,28 @@
import shutil
import os
import shutil
import socketserver
import webbrowser

from dbt.include.global_project import DOCS_INDEX_FILE_PATH
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from dbt.events.functions import fire_event
from dbt.events.types import ServingDocsPort, ServingDocsAccessInfo, ServingDocsExitInfo, EmptyLine

import click

from dbt.include.global_project import DOCS_INDEX_FILE_PATH
from dbt.task.base import ConfiguredTask


class ServeTask(ConfiguredTask):
def run(self):
os.chdir(self.config.target_path)

port = self.args.port
address = "0.0.0.0"

shutil.copyfile(DOCS_INDEX_FILE_PATH, "index.html")

fire_event(ServingDocsPort(address=address, port=port))
fire_event(ServingDocsAccessInfo(port=port))
fire_event(EmptyLine())
fire_event(EmptyLine())
fire_event(ServingDocsExitInfo())

# mypy doesn't think SimpleHTTPRequestHandler is ok here, but it is
httpd = TCPServer( # type: ignore
(address, port), SimpleHTTPRequestHandler # type: ignore
) # type: ignore

if self.args.open_browser:
try:
webbrowser.open_new_tab(f"http://127.0.0.1:{port}")
except webbrowser.Error:
pass
port = self.args.port

try:
httpd.serve_forever() # blocks
finally:
httpd.shutdown()
httpd.server_close()
if self.args.browser:
webbrowser.open_new_tab(f"http://localhost:{port}")

return None
with socketserver.TCPServer(("", port), SimpleHTTPRequestHandler) as httpd:
click.echo(f"Serving docs at {port}")
click.echo(f"To access from your browser, navigate to: http://localhost:{port}")
click.echo("\n\n")
click.echo("Press Ctrl+C to exit.")
Comment on lines +24 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change makes sense to me. docs serve is CLI-only functionality; it might make more sense to just click.echo, rather than firing a real event.

If we did want these to keep being "real" events / log messages, we could use the Formatting ("FYI") message type that @peterallenwebb added in #6691. I leave it up to your discretion.

In any case, I think we could also just remove the one-off event types that are no longer being used anywhere else in the codebase: ServingDocsPort, ServingDocsAccessInfo, ServingDocsExitInfo

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep these click.echo for now, have removed the one-off event types.

httpd.serve_forever()
120 changes: 120 additions & 0 deletions tests/functional/minimal_cli/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import pytest

models__schema_yml = """
version: 2
models:
- name: sample_model
columns:
- name: sample_num
tests:
- accepted_values:
values: [1, 2]
- not_null
- name: sample_bool
tests:
- not_null
- unique
"""

models__sample_model = """
select * from {{ ref('sample_seed') }}
"""

snapshots__sample_snapshot = """
{% snapshot orders_snapshot %}

{{
config(
target_database='postgres',
target_schema='snapshots',
unique_key='sample_num',
strategy='timestamp',
updated_at='updated_at',
)
}}

select * from {{ ref('sample_model') }}

{% endsnapshot %}
"""

seeds__sample_seed = """sample_num,sample_bool
1,true
2,false
,true
"""

tests__failing_sql = """
{{ config(severity = 'warn') }}
select 1
"""


class BaseConfigProject:

@pytest.fixture(scope="class")
def project_config_update(self):
return {
"name": "jaffle_shop",
"profile": "jaffle_shop",
"version": "0.1.0",
"config-version": 2,
"clean-targets": [
"target",
"dbt_packages",
"logs"
]
}

@pytest.fixture(scope="class")
def profiles_config_update(self):
return {
"jaffle_shop": {
"outputs": {
"dev": {
"type": "postgres",
"database": "postgres",
"schema": "jaffle_shop",
"host": "localhost",
"user": "root",
"port": 5432,
"password": "password"
}
},
"target": "dev"
}
}

@pytest.fixture(scope="class")
def packages(self):
return {
"packages": [
{
"package": "dbt-labs/dbt_utils",
"version": "1.0.0"
}
]
}

@pytest.fixture(scope="class")
def models(self):
return {
"schema.yml": models__schema_yml,
"sample_model.sql": models__sample_model,
}

@pytest.fixture(scope="class")
def snapshots(self):
return {
"sample_snapshot.sql": snapshots__sample_snapshot
}

@pytest.fixture(scope="class")
def seeds(self):
return {"sample_seed.csv": seeds__sample_seed}

@pytest.fixture(scope="class")
def tests(self):
return {
"failing.sql": tests__failing_sql,
}
49 changes: 49 additions & 0 deletions tests/functional/minimal_cli/test_minimal_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import pytest
from click.testing import CliRunner

from dbt.cli.main import cli
from tests.functional.minimal_cli.fixtures import BaseConfigProject


class TestMinimalCli(BaseConfigProject):
"""Test the minimal/happy-path for the CLI using the Click CliRunner"""
@pytest.fixture(scope="class")
def runner(self):
return CliRunner()

def test_clean(self, runner, project):
result = runner.invoke(cli, ['clean'])
assert 'target' in result.output
assert 'dbt_packages' in result.output
assert 'logs' in result.output

def test_deps(self, runner, project):
result = runner.invoke(cli, ['deps'])
assert 'dbt-labs/dbt_utils' in result.output
assert '1.0.0' in result.output

def test_ls(self, runner, project):
runner.invoke(cli, ['deps'])
ls_result = runner.invoke(cli, ['ls'])
assert '1 seed' in ls_result.output
assert '1 model' in ls_result.output
assert '5 tests' in ls_result.output
assert '1 snapshot' in ls_result.output

def test_build(self, runner, project):
runner.invoke(cli, ['deps'])
result = runner.invoke(cli, ['build'])
# 1 seed, 1 model, 2 tests
assert 'PASS=4' in result.output
# 2 tests
assert 'ERROR=2' in result.output
# Singular test
assert 'WARN=1' in result.output
# 1 snapshot
assert 'SKIP=1' in result.output

def test_docs_generate(self, runner, project):
runner.invoke(cli, ['deps'])
result = runner.invoke(cli, ['docs', 'generate'])
assert 'Building catalog' in result.output
assert 'Catalog written' in result.output