From 4780c4bb182a74d4506066aed2e3c280aa30cd1b Mon Sep 17 00:00:00 2001 From: Jacob Beck Date: Fri, 14 Dec 2018 10:40:10 -0700 Subject: [PATCH] Split dbt into core and plugins --- .bumpversion.cfg | 14 +- .coveragerc | 3 +- CHANGELOG.md => core/CHANGELOG.md | 0 CONTRIBUTING.md => core/CONTRIBUTING.md | 0 License.md => core/License.md | 0 MANIFEST.in => core/MANIFEST.in | 0 README.md => core/README.md | 0 core/dbt/__init__.py | 1 + core/dbt/adapters/__init__.py | 1 + {dbt => core/dbt}/adapters/base/__init__.py | 1 + .../dbt}/adapters/base/connections.py | 0 {dbt => core/dbt}/adapters/base/impl.py | 0 {dbt => core/dbt}/adapters/base/meta.py | 0 core/dbt/adapters/base/plugin.py | 26 + {dbt => core/dbt}/adapters/base/relation.py | 0 {dbt => core/dbt}/adapters/cache.py | 0 {dbt => core/dbt}/adapters/factory.py | 24 +- {dbt => core/dbt}/adapters/sql/__init__.py | 0 {dbt => core/dbt}/adapters/sql/connections.py | 0 {dbt => core/dbt}/adapters/sql/impl.py | 0 {dbt => core/dbt}/api/__init__.py | 0 {dbt => core/dbt}/api/object.py | 0 {dbt => core/dbt/clients}/__init__.py | 0 {dbt => core/dbt}/clients/agate_helper.py | 0 {dbt => core/dbt}/clients/gcloud.py | 0 {dbt => core/dbt}/clients/git.py | 0 {dbt => core/dbt}/clients/jinja.py | 0 {dbt => core/dbt}/clients/registry.py | 0 {dbt => core/dbt}/clients/system.py | 0 {dbt => core/dbt}/clients/yaml_helper.py | 0 {dbt => core/dbt}/compat.py | 0 {dbt => core/dbt}/compilation.py | 0 core/dbt/config/__init__.py | 22 + core/dbt/config/profile.py | 370 ++++++ core/dbt/config/project.py | 442 +++++++ core/dbt/config/renderer.py | 95 ++ core/dbt/config/runtime.py | 190 +++ .../adapters => core/dbt/context}/__init__.py | 0 {dbt => core/dbt}/context/common.py | 25 +- {dbt => core/dbt}/context/parser.py | 0 {dbt => core/dbt}/context/runtime.py | 0 .../dbt/contracts}/__init__.py | 0 {dbt => core/dbt}/contracts/common.py | 0 {dbt => core/dbt}/contracts/connection.py | 0 .../dbt/contracts/graph}/__init__.py | 0 {dbt => core/dbt}/contracts/graph/compiled.py | 0 {dbt => core/dbt}/contracts/graph/manifest.py | 0 {dbt => core/dbt}/contracts/graph/parsed.py | 0 {dbt => core/dbt}/contracts/graph/unparsed.py | 0 {dbt => core/dbt}/contracts/project.py | 0 {dbt => core/dbt}/contracts/results.py | 0 {dbt => core/dbt}/deprecations.py | 0 {dbt => core/dbt}/exceptions.py | 0 {dbt => core/dbt}/flags.py | 0 {dbt/contracts => core/dbt/graph}/__init__.py | 0 {dbt => core/dbt}/graph/selector.py | 0 {dbt => core/dbt}/hooks.py | 0 core/dbt/include/__init__.py | 1 + core/dbt/include/global_project/__init__.py | 11 + .../include/global_project/dbt_project.yml | 0 .../include/global_project/docs/overview.md | 0 .../global_project/macros/adapters/common.sql | 0 .../include/global_project/macros/core.sql | 0 .../global_project/macros/etc/datetime.sql | 0 .../macros/etc/get_custom_schema.sql | 0 .../macros/etc/is_incremental.sql | 0 .../materializations/archive/archive.sql | 22 - .../macros/materializations/common/merge.sql | 15 +- .../macros/materializations/helpers.sql | 0 .../incremental/incremental.sql | 0 .../macros/materializations/seed/seed.sql | 0 .../macros/materializations/table/table.sql | 0 .../view/create_or_replace_view.sql | 19 +- .../macros/materializations/view/view.sql | 0 .../macros/operations/catalog/get_catalog.sql | 0 .../operations/relations/get_relations.sql | 0 .../macros/schema_tests/accepted_values.sql | 0 .../macros/schema_tests/not_null.sql | 0 .../macros/schema_tests/relationships.sql | 0 .../macros/schema_tests/unique.sql | 0 {dbt => core/dbt}/include/index.html | 0 {dbt => core/dbt}/linker.py | 0 {dbt => core/dbt}/links.py | 0 {dbt => core/dbt}/loader.py | 0 {dbt => core/dbt}/logger.py | 0 {dbt => core/dbt}/main.py | 0 {dbt => core/dbt}/node_runners.py | 1 - {dbt => core/dbt}/node_types.py | 0 {dbt => core/dbt}/parser/__init__.py | 0 {dbt => core/dbt}/parser/analysis.py | 0 {dbt => core/dbt}/parser/archives.py | 0 {dbt => core/dbt}/parser/base.py | 3 +- {dbt => core/dbt}/parser/base_sql.py | 0 {dbt => core/dbt}/parser/data_test.py | 0 {dbt => core/dbt}/parser/docs.py | 0 {dbt => core/dbt}/parser/hooks.py | 0 {dbt => core/dbt}/parser/macros.py | 0 {dbt => core/dbt}/parser/models.py | 0 {dbt => core/dbt}/parser/schemas.py | 0 {dbt => core/dbt}/parser/seeds.py | 0 {dbt => core/dbt}/parser/source_config.py | 0 {dbt => core/dbt}/parser/util.py | 0 {dbt => core/dbt}/profiler.py | 0 {dbt => core/dbt}/runner.py | 0 {dbt => core/dbt}/schema.py | 0 {dbt => core/dbt}/semver.py | 0 {dbt => core/dbt}/ssh_forward.py | 0 .../graph => core/dbt/task}/__init__.py | 0 {dbt => core/dbt}/task/archive.py | 0 {dbt => core/dbt}/task/base_task.py | 0 {dbt => core/dbt}/task/clean.py | 0 {dbt => core/dbt}/task/compile.py | 0 {dbt => core/dbt}/task/debug.py | 0 {dbt => core/dbt}/task/deps.py | 0 {dbt => core/dbt}/task/generate.py | 2 +- {dbt => core/dbt}/task/init.py | 0 {dbt => core/dbt}/task/run.py | 0 {dbt => core/dbt}/task/seed.py | 0 {dbt => core/dbt}/task/serve.py | 2 +- {dbt => core/dbt}/task/test.py | 0 {dbt => core/dbt}/tracking.py | 0 {dbt/graph => core/dbt/ui}/__init__.py | 0 {dbt => core/dbt}/ui/colors.py | 0 {dbt => core/dbt}/ui/printer.py | 0 {dbt => core/dbt}/utils.py | 8 +- {dbt => core/dbt}/version.py | 2 +- {dbt => core/dbt}/writer.py | 0 core/etc/dag.png | Bin 0 -> 161481 bytes .../invocation_env_context.json | 0 .../invocation_event.json | 0 .../platform_context.json | 0 .../run_model_context.json | 0 core/scripts/create_adapter_plugins.py | 162 +++ {scripts => core/scripts}/dbt | 0 .../upgrade_dbt_schema_tests_v1_to_v2.py | 0 setup.py => core/setup.py | 23 +- dbt/adapters/postgres/__init__.py | 6 - dbt/adapters/redshift/__init__.py | 6 - dbt/config.py | 1077 ----------------- dbt/include/__init__.py | 8 - dbt/task/__init__.py | 0 dbt/templates.py | 0 dbt/ui/__init__.py | 0 plugins/bigquery/dbt/__init__.py | 1 + plugins/bigquery/dbt/adapters/__init__.py | 1 + .../dbt}/adapters/bigquery/__init__.py | 9 +- .../dbt}/adapters/bigquery/connections.py | 0 .../bigquery/dbt}/adapters/bigquery/impl.py | 0 .../dbt}/adapters/bigquery/relation.py | 0 plugins/bigquery/dbt/include/__init__.py | 1 + .../bigquery/dbt/include/bigquery/__init__.py | 2 + .../dbt/include/bigquery/dbt_project.yml | 5 + .../dbt/include/bigquery/macros/adapters.sql | 0 .../dbt/include/bigquery/macros/etc.sql | 0 .../macros/materializations/archive.sql | 23 + .../macros/materializations/incremental.sql | 0 .../macros/materializations/merge.sql | 3 + .../bigquery/macros/materializations/seed.sql | 0 .../macros/materializations/table.sql | 0 .../bigquery/macros/materializations/view.sql | 13 + plugins/bigquery/setup.py | 29 + plugins/postgres/dbt/__init__.py | 1 + plugins/postgres/dbt/adapters/__init__.py | 1 + .../dbt/adapters/postgres/__init__.py | 11 + .../dbt}/adapters/postgres/connections.py | 0 .../postgres/dbt}/adapters/postgres/impl.py | 0 plugins/postgres/dbt/include/__init__.py | 1 + .../postgres/dbt/include/postgres/__init__.py | 2 + .../dbt/include/postgres/dbt_project.yml | 5 + .../dbt/include/postgres/macros/catalog.sql | 0 .../dbt/include/postgres/macros/relations.sql | 0 plugins/postgres/setup.py | 27 + plugins/redshift/dbt/__init__.py | 1 + plugins/redshift/dbt/adapters/__init__.py | 1 + .../dbt/adapters/redshift/__init__.py | 13 + .../dbt}/adapters/redshift/connections.py | 0 .../redshift/dbt}/adapters/redshift/impl.py | 0 plugins/redshift/dbt/include/__init__.py | 1 + .../redshift/dbt/include/redshift/__init__.py | 3 + .../dbt/include/redshift/dbt_project.yml | 5 + .../dbt/include/redshift/macros/adapters.sql | 0 .../dbt/include/redshift/macros/catalog.sql | 0 .../dbt/include/redshift/macros/relations.sql | 0 plugins/redshift/setup.py | 31 + plugins/snowflake/dbt/__init__.py | 1 + plugins/snowflake/dbt/adapters/__init__.py | 1 + .../dbt}/adapters/snowflake/__init__.py | 8 +- .../dbt}/adapters/snowflake/connections.py | 0 .../snowflake/dbt}/adapters/snowflake/impl.py | 0 .../dbt}/adapters/snowflake/relation.py | 0 plugins/snowflake/dbt/include/__init__.py | 1 + .../dbt/include/snowflake/__init__.py | 2 + .../dbt/include/snowflake/dbt_project.yml | 5 + .../dbt/include/snowflake/macros/adapters.sql | 0 .../dbt/include/snowflake/macros/catalog.sql | 0 .../macros/materializations/merge.sql | 3 + .../macros/materializations/table.sql | 0 .../macros/materializations/view.sql | 3 + plugins/snowflake/setup.py | 31 + requirements.txt | 6 +- .../test_local_dependency.py | 4 +- .../test_docs_generate.py | 57 +- test/unit/test_config.py | 8 +- test/unit/test_graph.py | 1 - tox.ini | 60 +- 205 files changed, 1722 insertions(+), 1241 deletions(-) rename CHANGELOG.md => core/CHANGELOG.md (100%) rename CONTRIBUTING.md => core/CONTRIBUTING.md (100%) rename License.md => core/License.md (100%) rename MANIFEST.in => core/MANIFEST.in (100%) rename README.md => core/README.md (100%) create mode 100644 core/dbt/__init__.py create mode 100644 core/dbt/adapters/__init__.py rename {dbt => core/dbt}/adapters/base/__init__.py (81%) rename {dbt => core/dbt}/adapters/base/connections.py (100%) rename {dbt => core/dbt}/adapters/base/impl.py (100%) rename {dbt => core/dbt}/adapters/base/meta.py (100%) create mode 100644 core/dbt/adapters/base/plugin.py rename {dbt => core/dbt}/adapters/base/relation.py (100%) rename {dbt => core/dbt}/adapters/cache.py (100%) rename {dbt => core/dbt}/adapters/factory.py (81%) rename {dbt => core/dbt}/adapters/sql/__init__.py (100%) rename {dbt => core/dbt}/adapters/sql/connections.py (100%) rename {dbt => core/dbt}/adapters/sql/impl.py (100%) rename {dbt => core/dbt}/api/__init__.py (100%) rename {dbt => core/dbt}/api/object.py (100%) rename {dbt => core/dbt/clients}/__init__.py (100%) rename {dbt => core/dbt}/clients/agate_helper.py (100%) rename {dbt => core/dbt}/clients/gcloud.py (100%) rename {dbt => core/dbt}/clients/git.py (100%) rename {dbt => core/dbt}/clients/jinja.py (100%) rename {dbt => core/dbt}/clients/registry.py (100%) rename {dbt => core/dbt}/clients/system.py (100%) rename {dbt => core/dbt}/clients/yaml_helper.py (100%) rename {dbt => core/dbt}/compat.py (100%) rename {dbt => core/dbt}/compilation.py (100%) create mode 100644 core/dbt/config/__init__.py create mode 100644 core/dbt/config/profile.py create mode 100644 core/dbt/config/project.py create mode 100644 core/dbt/config/renderer.py create mode 100644 core/dbt/config/runtime.py rename {dbt/adapters => core/dbt/context}/__init__.py (100%) rename {dbt => core/dbt}/context/common.py (95%) rename {dbt => core/dbt}/context/parser.py (100%) rename {dbt => core/dbt}/context/runtime.py (100%) rename {dbt/clients => core/dbt/contracts}/__init__.py (100%) rename {dbt => core/dbt}/contracts/common.py (100%) rename {dbt => core/dbt}/contracts/connection.py (100%) rename {dbt/context => core/dbt/contracts/graph}/__init__.py (100%) rename {dbt => core/dbt}/contracts/graph/compiled.py (100%) rename {dbt => core/dbt}/contracts/graph/manifest.py (100%) rename {dbt => core/dbt}/contracts/graph/parsed.py (100%) rename {dbt => core/dbt}/contracts/graph/unparsed.py (100%) rename {dbt => core/dbt}/contracts/project.py (100%) rename {dbt => core/dbt}/contracts/results.py (100%) rename {dbt => core/dbt}/deprecations.py (100%) rename {dbt => core/dbt}/exceptions.py (100%) rename {dbt => core/dbt}/flags.py (100%) rename {dbt/contracts => core/dbt/graph}/__init__.py (100%) rename {dbt => core/dbt}/graph/selector.py (100%) rename {dbt => core/dbt}/hooks.py (100%) create mode 100644 core/dbt/include/__init__.py create mode 100644 core/dbt/include/global_project/__init__.py rename {dbt => core/dbt}/include/global_project/dbt_project.yml (100%) rename {dbt => core/dbt}/include/global_project/docs/overview.md (100%) rename {dbt => core/dbt}/include/global_project/macros/adapters/common.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/core.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/etc/datetime.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/etc/get_custom_schema.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/etc/is_incremental.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/materializations/archive/archive.sql (89%) rename {dbt => core/dbt}/include/global_project/macros/materializations/common/merge.sql (73%) rename {dbt => core/dbt}/include/global_project/macros/materializations/helpers.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/materializations/incremental/incremental.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/materializations/seed/seed.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/materializations/table/table.sql (100%) rename dbt/include/global_project/macros/materializations/view/bq_snowflake_view.sql => core/dbt/include/global_project/macros/materializations/view/create_or_replace_view.sql (82%) rename {dbt => core/dbt}/include/global_project/macros/materializations/view/view.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/operations/catalog/get_catalog.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/operations/relations/get_relations.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/schema_tests/accepted_values.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/schema_tests/not_null.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/schema_tests/relationships.sql (100%) rename {dbt => core/dbt}/include/global_project/macros/schema_tests/unique.sql (100%) rename {dbt => core/dbt}/include/index.html (100%) rename {dbt => core/dbt}/linker.py (100%) rename {dbt => core/dbt}/links.py (100%) rename {dbt => core/dbt}/loader.py (100%) rename {dbt => core/dbt}/logger.py (100%) rename {dbt => core/dbt}/main.py (100%) rename {dbt => core/dbt}/node_runners.py (99%) rename {dbt => core/dbt}/node_types.py (100%) rename {dbt => core/dbt}/parser/__init__.py (100%) rename {dbt => core/dbt}/parser/analysis.py (100%) rename {dbt => core/dbt}/parser/archives.py (100%) rename {dbt => core/dbt}/parser/base.py (98%) rename {dbt => core/dbt}/parser/base_sql.py (100%) rename {dbt => core/dbt}/parser/data_test.py (100%) rename {dbt => core/dbt}/parser/docs.py (100%) rename {dbt => core/dbt}/parser/hooks.py (100%) rename {dbt => core/dbt}/parser/macros.py (100%) rename {dbt => core/dbt}/parser/models.py (100%) rename {dbt => core/dbt}/parser/schemas.py (100%) rename {dbt => core/dbt}/parser/seeds.py (100%) rename {dbt => core/dbt}/parser/source_config.py (100%) rename {dbt => core/dbt}/parser/util.py (100%) rename {dbt => core/dbt}/profiler.py (100%) rename {dbt => core/dbt}/runner.py (100%) rename {dbt => core/dbt}/schema.py (100%) rename {dbt => core/dbt}/semver.py (100%) rename {dbt => core/dbt}/ssh_forward.py (100%) rename {dbt/contracts/graph => core/dbt/task}/__init__.py (100%) rename {dbt => core/dbt}/task/archive.py (100%) rename {dbt => core/dbt}/task/base_task.py (100%) rename {dbt => core/dbt}/task/clean.py (100%) rename {dbt => core/dbt}/task/compile.py (100%) rename {dbt => core/dbt}/task/debug.py (100%) rename {dbt => core/dbt}/task/deps.py (100%) rename {dbt => core/dbt}/task/generate.py (99%) rename {dbt => core/dbt}/task/init.py (100%) rename {dbt => core/dbt}/task/run.py (100%) rename {dbt => core/dbt}/task/seed.py (100%) rename {dbt => core/dbt}/task/serve.py (94%) rename {dbt => core/dbt}/task/test.py (100%) rename {dbt => core/dbt}/tracking.py (100%) rename {dbt/graph => core/dbt/ui}/__init__.py (100%) rename {dbt => core/dbt}/ui/colors.py (100%) rename {dbt => core/dbt}/ui/printer.py (100%) rename {dbt => core/dbt}/utils.py (98%) rename {dbt => core/dbt}/version.py (98%) rename {dbt => core/dbt}/writer.py (100%) create mode 100644 core/etc/dag.png rename {events => core/events}/schemas/com.fishtownanalytics/invocation_env_context.json (100%) rename {events => core/events}/schemas/com.fishtownanalytics/invocation_event.json (100%) rename {events => core/events}/schemas/com.fishtownanalytics/platform_context.json (100%) rename {events => core/events}/schemas/com.fishtownanalytics/run_model_context.json (100%) create mode 100644 core/scripts/create_adapter_plugins.py rename {scripts => core/scripts}/dbt (100%) rename {scripts => core/scripts}/upgrade_dbt_schema_tests_v1_to_v2.py (100%) rename setup.py => core/setup.py (69%) delete mode 100644 dbt/adapters/postgres/__init__.py delete mode 100644 dbt/adapters/redshift/__init__.py delete mode 100644 dbt/config.py delete mode 100644 dbt/include/__init__.py delete mode 100644 dbt/task/__init__.py delete mode 100644 dbt/templates.py delete mode 100644 dbt/ui/__init__.py create mode 100644 plugins/bigquery/dbt/__init__.py create mode 100644 plugins/bigquery/dbt/adapters/__init__.py rename {dbt => plugins/bigquery/dbt}/adapters/bigquery/__init__.py (54%) rename {dbt => plugins/bigquery/dbt}/adapters/bigquery/connections.py (100%) rename {dbt => plugins/bigquery/dbt}/adapters/bigquery/impl.py (100%) rename {dbt => plugins/bigquery/dbt}/adapters/bigquery/relation.py (100%) create mode 100644 plugins/bigquery/dbt/include/__init__.py create mode 100644 plugins/bigquery/dbt/include/bigquery/__init__.py create mode 100644 plugins/bigquery/dbt/include/bigquery/dbt_project.yml rename dbt/include/global_project/macros/adapters/bigquery.sql => plugins/bigquery/dbt/include/bigquery/macros/adapters.sql (100%) rename dbt/include/global_project/macros/etc/bigquery.sql => plugins/bigquery/dbt/include/bigquery/macros/etc.sql (100%) create mode 100644 plugins/bigquery/dbt/include/bigquery/macros/materializations/archive.sql rename dbt/include/global_project/macros/materializations/incremental/bigquery_incremental.sql => plugins/bigquery/dbt/include/bigquery/macros/materializations/incremental.sql (100%) create mode 100644 plugins/bigquery/dbt/include/bigquery/macros/materializations/merge.sql rename dbt/include/global_project/macros/materializations/seed/bigquery.sql => plugins/bigquery/dbt/include/bigquery/macros/materializations/seed.sql (100%) rename dbt/include/global_project/macros/materializations/table/bigquery_table.sql => plugins/bigquery/dbt/include/bigquery/macros/materializations/table.sql (100%) create mode 100644 plugins/bigquery/dbt/include/bigquery/macros/materializations/view.sql create mode 100644 plugins/bigquery/setup.py create mode 100644 plugins/postgres/dbt/__init__.py create mode 100644 plugins/postgres/dbt/adapters/__init__.py create mode 100644 plugins/postgres/dbt/adapters/postgres/__init__.py rename {dbt => plugins/postgres/dbt}/adapters/postgres/connections.py (100%) rename {dbt => plugins/postgres/dbt}/adapters/postgres/impl.py (100%) create mode 100644 plugins/postgres/dbt/include/__init__.py create mode 100644 plugins/postgres/dbt/include/postgres/__init__.py create mode 100644 plugins/postgres/dbt/include/postgres/dbt_project.yml rename dbt/include/global_project/macros/catalog/postgres_catalog.sql => plugins/postgres/dbt/include/postgres/macros/catalog.sql (100%) rename dbt/include/global_project/macros/relations/postgres_relations.sql => plugins/postgres/dbt/include/postgres/macros/relations.sql (100%) create mode 100644 plugins/postgres/setup.py create mode 100644 plugins/redshift/dbt/__init__.py create mode 100644 plugins/redshift/dbt/adapters/__init__.py create mode 100644 plugins/redshift/dbt/adapters/redshift/__init__.py rename {dbt => plugins/redshift/dbt}/adapters/redshift/connections.py (100%) rename {dbt => plugins/redshift/dbt}/adapters/redshift/impl.py (100%) create mode 100644 plugins/redshift/dbt/include/__init__.py create mode 100644 plugins/redshift/dbt/include/redshift/__init__.py create mode 100644 plugins/redshift/dbt/include/redshift/dbt_project.yml rename dbt/include/global_project/macros/adapters/redshift.sql => plugins/redshift/dbt/include/redshift/macros/adapters.sql (100%) rename dbt/include/global_project/macros/catalog/redshift_catalog.sql => plugins/redshift/dbt/include/redshift/macros/catalog.sql (100%) rename dbt/include/global_project/macros/relations/redshift_relations.sql => plugins/redshift/dbt/include/redshift/macros/relations.sql (100%) create mode 100644 plugins/redshift/setup.py create mode 100644 plugins/snowflake/dbt/__init__.py create mode 100644 plugins/snowflake/dbt/adapters/__init__.py rename {dbt => plugins/snowflake/dbt}/adapters/snowflake/__init__.py (55%) rename {dbt => plugins/snowflake/dbt}/adapters/snowflake/connections.py (100%) rename {dbt => plugins/snowflake/dbt}/adapters/snowflake/impl.py (100%) rename {dbt => plugins/snowflake/dbt}/adapters/snowflake/relation.py (100%) create mode 100644 plugins/snowflake/dbt/include/__init__.py create mode 100644 plugins/snowflake/dbt/include/snowflake/__init__.py create mode 100644 plugins/snowflake/dbt/include/snowflake/dbt_project.yml rename dbt/include/global_project/macros/adapters/snowflake.sql => plugins/snowflake/dbt/include/snowflake/macros/adapters.sql (100%) rename dbt/include/global_project/macros/catalog/snowflake_catalog.sql => plugins/snowflake/dbt/include/snowflake/macros/catalog.sql (100%) create mode 100644 plugins/snowflake/dbt/include/snowflake/macros/materializations/merge.sql rename dbt/include/global_project/macros/materializations/table/snowflake_table.sql => plugins/snowflake/dbt/include/snowflake/macros/materializations/table.sql (100%) create mode 100644 plugins/snowflake/dbt/include/snowflake/macros/materializations/view.sql create mode 100644 plugins/snowflake/setup.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f04d1e92cfa..00c2666d1f2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.12.2rc1 +current_version = 0.13.0a1 parse = (?P\d+) \.(?P\d+) \.(?P\d+) @@ -20,7 +20,15 @@ values = [bumpversion:part:num] first_value = 1 -[bumpversion:file:setup.py] +[bumpversion:file:core/setup.py] -[bumpversion:file:dbt/version.py] +[bumpversion:file:core/dbt/version.py] + +[bumpversion:file:plugins/postgres/setup.py] + +[bumpversion:file:plugins/redshift/setup.py] + +[bumpversion:file:plugins/snowflake/setup.py] + +[bumpversion:file:plugins/bigquery/setup.py] diff --git a/.coveragerc b/.coveragerc index 23cf94ed431..18244411816 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,4 @@ [report] include = - dbt/* + core/dbt/* + plugins/adapters/dbt/* diff --git a/CHANGELOG.md b/core/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to core/CHANGELOG.md diff --git a/CONTRIBUTING.md b/core/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to core/CONTRIBUTING.md diff --git a/License.md b/core/License.md similarity index 100% rename from License.md rename to core/License.md diff --git a/MANIFEST.in b/core/MANIFEST.in similarity index 100% rename from MANIFEST.in rename to core/MANIFEST.in diff --git a/README.md b/core/README.md similarity index 100% rename from README.md rename to core/README.md diff --git a/core/dbt/__init__.py b/core/dbt/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/core/dbt/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/core/dbt/adapters/__init__.py b/core/dbt/adapters/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/core/dbt/adapters/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/dbt/adapters/base/__init__.py b/core/dbt/adapters/base/__init__.py similarity index 81% rename from dbt/adapters/base/__init__.py rename to core/dbt/adapters/base/__init__.py index a509f6b0ed9..98f96abe1b4 100644 --- a/dbt/adapters/base/__init__.py +++ b/core/dbt/adapters/base/__init__.py @@ -2,3 +2,4 @@ from dbt.adapters.base.relation import BaseRelation from dbt.adapters.base.connections import BaseConnectionManager, Credentials from dbt.adapters.base.impl import BaseAdapter +from dbt.adapters.base.plugin import AdapterPlugin diff --git a/dbt/adapters/base/connections.py b/core/dbt/adapters/base/connections.py similarity index 100% rename from dbt/adapters/base/connections.py rename to core/dbt/adapters/base/connections.py diff --git a/dbt/adapters/base/impl.py b/core/dbt/adapters/base/impl.py similarity index 100% rename from dbt/adapters/base/impl.py rename to core/dbt/adapters/base/impl.py diff --git a/dbt/adapters/base/meta.py b/core/dbt/adapters/base/meta.py similarity index 100% rename from dbt/adapters/base/meta.py rename to core/dbt/adapters/base/meta.py diff --git a/core/dbt/adapters/base/plugin.py b/core/dbt/adapters/base/plugin.py new file mode 100644 index 00000000000..242bc681b8a --- /dev/null +++ b/core/dbt/adapters/base/plugin.py @@ -0,0 +1,26 @@ +import os + +from dbt.config.project import Project + + +class AdapterPlugin(object): + """Defines the basic requirements for a dbt adapter plugin. + + :param type adapter: An adapter class, derived from BaseAdapter + :param type credentials: A credentials object, derived from Credentials + :param str project_name: The name of this adapter plugin's associated dbt + project. + :param str include_path: The path to this adapter plugin's root + :param Optional[List[str]] dependencies: A list of adapter names that this\ + adapter depends upon. + """ + def __init__(self, adapter, credentials, include_path, dependencies=None): + self.adapter = adapter + self.credentials = credentials + self.include_path = include_path + project_path = os.path.join(self.include_path, adapter.type()) + project = Project.from_project_root(project_path, {}) + self.project_name = project.project_name + if dependencies is None: + dependencies = [] + self.dependencies = dependencies diff --git a/dbt/adapters/base/relation.py b/core/dbt/adapters/base/relation.py similarity index 100% rename from dbt/adapters/base/relation.py rename to core/dbt/adapters/base/relation.py diff --git a/dbt/adapters/cache.py b/core/dbt/adapters/cache.py similarity index 100% rename from dbt/adapters/cache.py rename to core/dbt/adapters/cache.py diff --git a/dbt/adapters/factory.py b/core/dbt/adapters/factory.py similarity index 81% rename from dbt/adapters/factory.py rename to core/dbt/adapters/factory.py index bef5e66dfa3..f3145c690f1 100644 --- a/dbt/adapters/factory.py +++ b/core/dbt/adapters/factory.py @@ -2,10 +2,10 @@ import dbt.exceptions from importlib import import_module +from dbt.include.global_project import PACKAGES import threading - ADAPTER_TYPES = {} _ADAPTERS = {} @@ -28,25 +28,31 @@ def get_relation_class_by_name(adapter_name): return adapter.Relation -def load_adapter(adapter_name): - """Load an adapter package with the class of adapter_name, put it in the - ADAPTER_TYPES dict, and return its associated Credentials - """ +def load_plugin(adapter_name): try: mod = import_module('.'+adapter_name, 'dbt.adapters') except ImportError: raise dbt.exceptions.RuntimeException( "Could not find adapter type {}!".format(adapter_name) ) - if mod.Adapter.type() != adapter_name: + plugin = mod.Plugin + + if plugin.adapter.type() != adapter_name: raise dbt.exceptions.RuntimeException( 'Expected to find adapter with type named {}, got adapter with ' 'type {}' - .format(adapter_name, mod.Adapter.type()) + .format(adapter_name, plugin.adapter.type()) ) - ADAPTER_TYPES[adapter_name] = mod.Adapter - return mod.Credentials + with _ADAPTER_LOCK: + ADAPTER_TYPES[adapter_name] = plugin.adapter + + PACKAGES[plugin.project_name] = plugin.include_path + + for dep in plugin.dependencies: + load_plugin(dep) + + return plugin.credentials def get_adapter(config): diff --git a/dbt/adapters/sql/__init__.py b/core/dbt/adapters/sql/__init__.py similarity index 100% rename from dbt/adapters/sql/__init__.py rename to core/dbt/adapters/sql/__init__.py diff --git a/dbt/adapters/sql/connections.py b/core/dbt/adapters/sql/connections.py similarity index 100% rename from dbt/adapters/sql/connections.py rename to core/dbt/adapters/sql/connections.py diff --git a/dbt/adapters/sql/impl.py b/core/dbt/adapters/sql/impl.py similarity index 100% rename from dbt/adapters/sql/impl.py rename to core/dbt/adapters/sql/impl.py diff --git a/dbt/api/__init__.py b/core/dbt/api/__init__.py similarity index 100% rename from dbt/api/__init__.py rename to core/dbt/api/__init__.py diff --git a/dbt/api/object.py b/core/dbt/api/object.py similarity index 100% rename from dbt/api/object.py rename to core/dbt/api/object.py diff --git a/dbt/__init__.py b/core/dbt/clients/__init__.py similarity index 100% rename from dbt/__init__.py rename to core/dbt/clients/__init__.py diff --git a/dbt/clients/agate_helper.py b/core/dbt/clients/agate_helper.py similarity index 100% rename from dbt/clients/agate_helper.py rename to core/dbt/clients/agate_helper.py diff --git a/dbt/clients/gcloud.py b/core/dbt/clients/gcloud.py similarity index 100% rename from dbt/clients/gcloud.py rename to core/dbt/clients/gcloud.py diff --git a/dbt/clients/git.py b/core/dbt/clients/git.py similarity index 100% rename from dbt/clients/git.py rename to core/dbt/clients/git.py diff --git a/dbt/clients/jinja.py b/core/dbt/clients/jinja.py similarity index 100% rename from dbt/clients/jinja.py rename to core/dbt/clients/jinja.py diff --git a/dbt/clients/registry.py b/core/dbt/clients/registry.py similarity index 100% rename from dbt/clients/registry.py rename to core/dbt/clients/registry.py diff --git a/dbt/clients/system.py b/core/dbt/clients/system.py similarity index 100% rename from dbt/clients/system.py rename to core/dbt/clients/system.py diff --git a/dbt/clients/yaml_helper.py b/core/dbt/clients/yaml_helper.py similarity index 100% rename from dbt/clients/yaml_helper.py rename to core/dbt/clients/yaml_helper.py diff --git a/dbt/compat.py b/core/dbt/compat.py similarity index 100% rename from dbt/compat.py rename to core/dbt/compat.py diff --git a/dbt/compilation.py b/core/dbt/compilation.py similarity index 100% rename from dbt/compilation.py rename to core/dbt/compilation.py diff --git a/core/dbt/config/__init__.py b/core/dbt/config/__init__.py new file mode 100644 index 00000000000..b5280511ef7 --- /dev/null +++ b/core/dbt/config/__init__.py @@ -0,0 +1,22 @@ + +from .renderer import ConfigRenderer +from .profile import Profile, UserConfig +from .project import Project +from .profile import read_profile +from .profile import PROFILES_DIR +from .runtime import RuntimeConfig + + +def read_profiles(profiles_dir=None): + """This is only used in main, for some error handling""" + if profiles_dir is None: + profiles_dir = PROFILES_DIR + + raw_profiles = read_profile(profiles_dir) + + if raw_profiles is None: + profiles = {} + else: + profiles = {k: v for (k, v) in raw_profiles.items() if k != 'config'} + + return profiles diff --git a/core/dbt/config/profile.py b/core/dbt/config/profile.py new file mode 100644 index 00000000000..bb5c91cc246 --- /dev/null +++ b/core/dbt/config/profile.py @@ -0,0 +1,370 @@ +import os +import pprint + +from dbt.adapters.factory import load_plugin +from dbt.clients.system import load_file_contents +from dbt.clients.yaml_helper import load_yaml_text +from dbt.contracts.project import ProfileConfig +from dbt.exceptions import DbtProfileError +from dbt.exceptions import DbtProjectError +from dbt.exceptions import ValidationException +from dbt.exceptions import RuntimeException +from dbt.logger import GLOBAL_LOGGER as logger +from dbt.utils import parse_cli_vars + +from .renderer import ConfigRenderer + +DEFAULT_THREADS = 1 +DEFAULT_SEND_ANONYMOUS_USAGE_STATS = True +DEFAULT_USE_COLORS = True +DEFAULT_PROFILES_DIR = os.path.join(os.path.expanduser('~'), '.dbt') +PROFILES_DIR = os.path.expanduser( + os.environ.get('DBT_PROFILES_DIR', DEFAULT_PROFILES_DIR) +) + +INVALID_PROFILE_MESSAGE = """ +dbt encountered an error while trying to read your profiles.yml file. + +{error_string} +""" + + +NO_SUPPLIED_PROFILE_ERROR = """\ +dbt cannot run because no profile was specified for this dbt project. +To specify a profile for this project, add a line like the this to +your dbt_project.yml file: + +profile: [profile name] + +Here, [profile name] should be replaced with a profile name +defined in your profiles.yml file. You can find profiles.yml here: + +{profiles_file}/profiles.yml +""".format(profiles_file=PROFILES_DIR) + + +def read_profile(profiles_dir): + path = os.path.join(profiles_dir, 'profiles.yml') + + contents = None + if os.path.isfile(path): + try: + contents = load_file_contents(path, strip=False) + return load_yaml_text(contents) + except ValidationException as e: + msg = INVALID_PROFILE_MESSAGE.format(error_string=e) + raise ValidationException(msg) + + return {} + + +class UserConfig(object): + def __init__(self, send_anonymous_usage_stats, use_colors): + self.send_anonymous_usage_stats = send_anonymous_usage_stats + self.use_colors = use_colors + + @classmethod + def from_dict(cls, cfg=None): + if cfg is None: + cfg = {} + send_anonymous_usage_stats = cfg.get( + 'send_anonymous_usage_stats', + DEFAULT_SEND_ANONYMOUS_USAGE_STATS + ) + use_colors = cfg.get( + 'use_colors', + DEFAULT_USE_COLORS + ) + return cls(send_anonymous_usage_stats, use_colors) + + def to_dict(self): + return { + 'send_anonymous_usage_stats': self.send_anonymous_usage_stats, + 'use_colors': self.use_colors, + } + + @classmethod + def from_directory(cls, directory): + user_cfg = None + profile = read_profile(directory) + if profile: + user_cfg = profile.get('config', {}) + return cls.from_dict(user_cfg) + + +class Profile(object): + def __init__(self, profile_name, target_name, config, threads, + credentials): + self.profile_name = profile_name + self.target_name = target_name + if isinstance(config, dict): + config = UserConfig.from_dict(config) + self.config = config + self.threads = threads + self.credentials = credentials + + def to_profile_info(self, serialize_credentials=False): + """Unlike to_project_config, this dict is not a mirror of any existing + on-disk data structure. It's used when creating a new profile from an + existing one. + + :param serialize_credentials bool: If True, serialize the credentials. + Otherwise, the Credentials object will be copied. + :returns dict: The serialized profile. + """ + result = { + 'profile_name': self.profile_name, + 'target_name': self.target_name, + 'config': self.config.to_dict(), + 'threads': self.threads, + 'credentials': self.credentials.incorporate(), + } + if serialize_credentials: + result['credentials'] = result['credentials'].serialize() + return result + + def __str__(self): + return pprint.pformat(self.to_profile_info()) + + def __eq__(self, other): + if not (isinstance(other, self.__class__) and + isinstance(self, other.__class__)): + return False + return False + return self.to_profile_info() == other.to_profile_info() + + def validate(self): + if self.credentials: + self.credentials.validate() + try: + ProfileConfig(**self.to_profile_info(serialize_credentials=True)) + except ValidationException as exc: + raise DbtProfileError(str(exc)) + + @staticmethod + def _credentials_from_profile(profile, profile_name, target_name): + # credentials carry their 'type' in their actual type, not their + # attributes. We do want this in order to pick our Credentials class. + if 'type' not in profile: + raise DbtProfileError( + 'required field "type" not found in profile {} and target {}' + .format(profile_name, target_name)) + + typename = profile.pop('type') + + try: + cls = load_plugin(typename) + credentials = cls(**profile) + except RuntimeException as e: + raise DbtProfileError( + 'Credentials in profile "{}", target "{}" invalid: {}' + .format(profile_name, target_name, str(e)) + ) + return credentials + + @staticmethod + def pick_profile_name(args_profile_name, project_profile_name=None): + profile_name = project_profile_name + if args_profile_name is not None: + profile_name = args_profile_name + if profile_name is None: + raise DbtProjectError(NO_SUPPLIED_PROFILE_ERROR) + return profile_name + + @staticmethod + def _get_profile_data(profile, profile_name, target_name): + if 'outputs' not in profile: + raise DbtProfileError( + "outputs not specified in profile '{}'".format(profile_name) + ) + outputs = profile['outputs'] + + if target_name not in outputs: + outputs = '\n'.join(' - {}'.format(output) + for output in outputs) + msg = ("The profile '{}' does not have a target named '{}'. The " + "valid target names for this profile are:\n{}" + .format(profile_name, target_name, outputs)) + raise DbtProfileError(msg, result_type='invalid_target') + profile_data = outputs[target_name] + return profile_data + + @classmethod + def from_credentials(cls, credentials, threads, profile_name, target_name, + user_cfg=None): + """Create a profile from an existing set of Credentials and the + remaining information. + + :param credentials dict: The credentials dict for this profile. + :param threads int: The number of threads to use for connections. + :param profile_name str: The profile name used for this profile. + :param target_name str: The target name used for this profile. + :param user_cfg Optional[dict]: The user-level config block from the + raw profiles, if specified. + :raises DbtProfileError: If the profile is invalid. + :returns Profile: The new Profile object. + """ + config = UserConfig.from_dict(user_cfg) + profile = cls( + profile_name=profile_name, + target_name=target_name, + config=config, + threads=threads, + credentials=credentials + ) + profile.validate() + return profile + + @classmethod + def render_profile(cls, raw_profile, profile_name, target_override, + cli_vars): + """This is a containment zone for the hateful way we're rendering + profiles. + """ + renderer = ConfigRenderer(cli_vars=cli_vars) + + # rendering profiles is a bit complex. Two constraints cause trouble: + # 1) users should be able to use environment/cli variables to specify + # the target in their profile. + # 2) Missing environment/cli variables in profiles/targets that don't + # end up getting selected should not cause errors. + # so first we'll just render the target name, then we use that rendered + # name to extract a profile that we can render. + if target_override is not None: + target_name = target_override + elif 'target' in raw_profile: + # render the target if it was parsed from yaml + target_name = renderer.render_value(raw_profile['target']) + else: + target_name = 'default' + logger.debug( + "target not specified in profile '{}', using '{}'" + .format(profile_name, target_name) + ) + + raw_profile_data = cls._get_profile_data( + raw_profile, profile_name, target_name + ) + + profile_data = renderer.render_profile_data(raw_profile_data) + return target_name, profile_data + + @classmethod + def from_raw_profile_info(cls, raw_profile, profile_name, cli_vars, + user_cfg=None, target_override=None, + threads_override=None): + """Create a profile from its raw profile information. + + (this is an intermediate step, mostly useful for unit testing) + + :param raw_profile dict: The profile data for a single profile, from + disk as yaml and its values rendered with jinja. + :param profile_name str: The profile name used. + :param cli_vars dict: The command-line variables passed as arguments, + as a dict. + :param user_cfg Optional[dict]: The global config for the user, if it + was present. + :param target_override Optional[str]: The target to use, if provided on + the command line. + :param threads_override Optional[str]: The thread count to use, if + provided on the command line. + :raises DbtProfileError: If the profile is invalid or missing, or the + target could not be found + :returns Profile: The new Profile object. + """ + # user_cfg is not rendered since it only contains booleans. + # TODO: should it be, and the values coerced to bool? + target_name, profile_data = cls.render_profile( + raw_profile, profile_name, target_override, cli_vars + ) + + # valid connections never include the number of threads, but it's + # stored on a per-connection level in the raw configs + threads = profile_data.pop('threads', DEFAULT_THREADS) + if threads_override is not None: + threads = threads_override + + credentials = cls._credentials_from_profile( + profile_data, profile_name, target_name + ) + + return cls.from_credentials( + credentials=credentials, + profile_name=profile_name, + target_name=target_name, + threads=threads, + user_cfg=user_cfg + ) + + @classmethod + def from_raw_profiles(cls, raw_profiles, profile_name, cli_vars, + target_override=None, threads_override=None): + """ + :param raw_profiles dict: The profile data, from disk as yaml. + :param profile_name str: The profile name to use. + :param cli_vars dict: The command-line variables passed as arguments, + as a dict. + :param target_override Optional[str]: The target to use, if provided on + the command line. + :param threads_override Optional[str]: The thread count to use, if + provided on the command line. + :raises DbtProjectError: If there is no profile name specified in the + project or the command line arguments + :raises DbtProfileError: If the profile is invalid or missing, or the + target could not be found + :returns Profile: The new Profile object. + """ + if profile_name not in raw_profiles: + raise DbtProjectError( + "Could not find profile named '{}'".format(profile_name) + ) + + # First, we've already got our final decision on profile name, and we + # don't render keys, so we can pluck that out + raw_profile = raw_profiles[profile_name] + + user_cfg = raw_profiles.get('config') + + return cls.from_raw_profile_info( + raw_profile=raw_profile, + profile_name=profile_name, + cli_vars=cli_vars, + user_cfg=user_cfg, + target_override=target_override, + threads_override=threads_override, + ) + + @classmethod + def from_args(cls, args, project_profile_name=None, cli_vars=None): + """Given the raw profiles as read from disk and the name of the desired + profile if specified, return the profile component of the runtime + config. + + :param args argparse.Namespace: The arguments as parsed from the cli. + :param cli_vars dict: The command-line variables passed as arguments, + as a dict. + :param project_profile_name Optional[str]: The profile name, if + specified in a project. + :raises DbtProjectError: If there is no profile name specified in the + project or the command line arguments, or if the specified profile + is not found + :raises DbtProfileError: If the profile is invalid or missing, or the + target could not be found. + :returns Profile: The new Profile object. + """ + if cli_vars is None: + cli_vars = parse_cli_vars(getattr(args, 'vars', '{}')) + + threads_override = getattr(args, 'threads', None) + target_override = getattr(args, 'target', None) + raw_profiles = read_profile(args.profiles_dir) + profile_name = cls.pick_profile_name(args.profile, + project_profile_name) + + return cls.from_raw_profiles( + raw_profiles=raw_profiles, + profile_name=profile_name, + cli_vars=cli_vars, + target_override=target_override, + threads_override=threads_override + ) diff --git a/core/dbt/config/project.py b/core/dbt/config/project.py new file mode 100644 index 00000000000..8366755ef89 --- /dev/null +++ b/core/dbt/config/project.py @@ -0,0 +1,442 @@ + +from copy import deepcopy +import hashlib +import os +import pprint + +from dbt import compat +from dbt.clients.system import resolve_path_from_base +from dbt.clients.system import path_exists +from dbt.clients.system import load_file_contents +from dbt.clients.yaml_helper import load_yaml_text +from dbt.exceptions import DbtProjectError +from dbt.exceptions import RecursionException +from dbt.exceptions import SemverException +from dbt.exceptions import ValidationException +from dbt.logger import GLOBAL_LOGGER as logger +from dbt.semver import VersionSpecifier +from dbt.semver import versions_compatible +from dbt.version import get_installed_version +from dbt.ui import printer +from dbt.utils import deep_map +from dbt.utils import parse_cli_vars +from dbt.utils import DBTConfigKeys + +from dbt.contracts.project import Project as ProjectContract +from dbt.contracts.project import PackageConfig + +from .renderer import ConfigRenderer + + +UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE = """\ +WARNING: Configuration paths exist in your dbt_project.yml file which do not \ +apply to any resources. +There are {} unused configuration paths:\n{} +""" + + +INVALID_VERSION_ERROR = """\ +This version of dbt is not supported with the '{package}' package. + Installed version of dbt: {installed} + Required version of dbt for '{package}': {version_spec} +Check the requirements for the '{package}' package, or run dbt again with \ +--no-version-check +""" + + +IMPOSSIBLE_VERSION_ERROR = """\ +The package version requirement can never be satisfied for the '{package} +package. + Required versions of dbt for '{package}': {version_spec} +Check the requirements for the '{package}' package, or run dbt again with \ +--no-version-check +""" + + +def _list_if_none(value): + if value is None: + value = [] + return value + + +def _dict_if_none(value): + if value is None: + value = {} + return value + + +def _list_if_none_or_string(value): + value = _list_if_none(value) + if isinstance(value, compat.basestring): + return [value] + return value + + +def _load_yaml(path): + contents = load_file_contents(path) + return load_yaml_text(contents) + + +def _get_config_paths(config, path=(), paths=None): + if paths is None: + paths = set() + + for key, value in config.items(): + if isinstance(value, dict): + if key in DBTConfigKeys: + if path not in paths: + paths.add(path) + else: + _get_config_paths(value, path + (key,), paths) + else: + if path not in paths: + paths.add(path) + + return frozenset(paths) + + +def _is_config_used(path, fqns): + if fqns: + for fqn in fqns: + if len(path) <= len(fqn) and fqn[:len(path)] == path: + return True + return False + + +def package_data_from_root(project_root): + package_filepath = resolve_path_from_base( + 'packages.yml', project_root + ) + + if path_exists(package_filepath): + packages_dict = _load_yaml(package_filepath) + else: + packages_dict = None + return packages_dict + + +def package_config_from_data(packages_data): + if packages_data is None: + packages_data = {'packages': []} + + try: + packages = PackageConfig(**packages_data) + except ValidationException as e: + raise DbtProjectError('Invalid package config: {}'.format(str(e))) + return packages + + +def _parse_versions(versions): + """Parse multiple versions as read from disk. The versions value may be any + one of: + - a single version string ('>0.12.1') + - a single string specifying multiple comma-separated versions + ('>0.11.1,<=0.12.2') + - an array of single-version strings (['>0.11.1', '<=0.12.2']) + + Regardless, this will return a list of VersionSpecifiers + """ + if isinstance(versions, compat.basestring): + versions = versions.split(',') + return [VersionSpecifier.from_version_string(v) for v in versions] + + +class Project(object): + def __init__(self, project_name, version, project_root, profile_name, + source_paths, macro_paths, data_paths, test_paths, + analysis_paths, docs_paths, target_path, clean_targets, + log_path, modules_path, quoting, models, on_run_start, + on_run_end, archive, seeds, dbt_version, packages): + self.project_name = project_name + self.version = version + self.project_root = project_root + self.profile_name = profile_name + self.source_paths = source_paths + self.macro_paths = macro_paths + self.data_paths = data_paths + self.test_paths = test_paths + self.analysis_paths = analysis_paths + self.docs_paths = docs_paths + self.target_path = target_path + self.clean_targets = clean_targets + self.log_path = log_path + self.modules_path = modules_path + self.quoting = quoting + self.models = models + self.on_run_start = on_run_start + self.on_run_end = on_run_end + self.archive = archive + self.seeds = seeds + self.dbt_version = dbt_version + self.packages = packages + + @staticmethod + def _preprocess(project_dict): + """Pre-process certain special keys to convert them from None values + into empty containers, and to turn strings into arrays of strings. + """ + handlers = { + ('archive',): _list_if_none, + ('on-run-start',): _list_if_none_or_string, + ('on-run-end',): _list_if_none_or_string, + } + + for k in ('models', 'seeds'): + handlers[(k,)] = _dict_if_none + handlers[(k, 'vars')] = _dict_if_none + handlers[(k, 'pre-hook')] = _list_if_none_or_string + handlers[(k, 'post-hook')] = _list_if_none_or_string + handlers[('seeds', 'column_types')] = _dict_if_none + + def converter(value, keypath): + if keypath in handlers: + handler = handlers[keypath] + return handler(value) + else: + return value + + return deep_map(converter, project_dict) + + @classmethod + def from_project_config(cls, project_dict, packages_dict=None): + """Create a project from its project and package configuration, as read + by yaml.safe_load(). + + :param project_dict dict: The dictionary as read from disk + :param packages_dict Optional[dict]: If it exists, the packages file as + read from disk. + :raises DbtProjectError: If the project is missing or invalid, or if + the packages file exists and is invalid. + :returns Project: The project, with defaults populated. + """ + try: + project_dict = cls._preprocess(project_dict) + except RecursionException: + raise DbtProjectError( + 'Cycle detected: Project input has a reference to itself', + project=project_dict + ) + # just for validation. + try: + ProjectContract(**project_dict) + except ValidationException as e: + raise DbtProjectError(str(e)) + + # name/version are required in the Project definition, so we can assume + # they are present + name = project_dict['name'] + version = project_dict['version'] + # this is added at project_dict parse time and should always be here + # once we see it. + project_root = project_dict['project-root'] + # this is only optional in the sense that if it's not present, it needs + # to have been a cli argument. + profile_name = project_dict.get('profile') + # these are optional + source_paths = project_dict.get('source-paths', ['models']) + macro_paths = project_dict.get('macro-paths', ['macros']) + data_paths = project_dict.get('data-paths', ['data']) + test_paths = project_dict.get('test-paths', ['test']) + analysis_paths = project_dict.get('analysis-paths', []) + docs_paths = project_dict.get('docs-paths', source_paths[:]) + target_path = project_dict.get('target-path', 'target') + # should this also include the modules path by default? + clean_targets = project_dict.get('clean-targets', [target_path]) + log_path = project_dict.get('log-path', 'logs') + modules_path = project_dict.get('modules-path', 'dbt_modules') + # in the default case we'll populate this once we know the adapter type + quoting = project_dict.get('quoting', {}) + + models = project_dict.get('models', {}) + on_run_start = project_dict.get('on-run-start', []) + on_run_end = project_dict.get('on-run-end', []) + archive = project_dict.get('archive', []) + seeds = project_dict.get('seeds', {}) + dbt_raw_version = project_dict.get('require-dbt-version', '>=0.0.0') + + try: + dbt_version = _parse_versions(dbt_raw_version) + except SemverException as e: + raise DbtProjectError(str(e)) + + packages = package_config_from_data(packages_dict) + + project = cls( + project_name=name, + version=version, + project_root=project_root, + profile_name=profile_name, + source_paths=source_paths, + macro_paths=macro_paths, + data_paths=data_paths, + test_paths=test_paths, + analysis_paths=analysis_paths, + docs_paths=docs_paths, + target_path=target_path, + clean_targets=clean_targets, + log_path=log_path, + modules_path=modules_path, + quoting=quoting, + models=models, + on_run_start=on_run_start, + on_run_end=on_run_end, + archive=archive, + seeds=seeds, + dbt_version=dbt_version, + packages=packages + ) + # sanity check - this means an internal issue + project.validate() + return project + + def __str__(self): + cfg = self.to_project_config(with_packages=True) + return pprint.pformat(cfg) + + def __eq__(self, other): + if not (isinstance(other, self.__class__) and + isinstance(self, other.__class__)): + return False + return self.to_project_config(with_packages=True) == \ + other.to_project_config(with_packages=True) + + def to_project_config(self, with_packages=False): + """Return a dict representation of the config that could be written to + disk with `yaml.safe_dump` to get this configuration. + + :param with_packages bool: If True, include the serialized packages + file in the root. + :returns dict: The serialized profile. + """ + result = deepcopy({ + 'name': self.project_name, + 'version': self.version, + 'project-root': self.project_root, + 'profile': self.profile_name, + 'source-paths': self.source_paths, + 'macro-paths': self.macro_paths, + 'data-paths': self.data_paths, + 'test-paths': self.test_paths, + 'analysis-paths': self.analysis_paths, + 'docs-paths': self.docs_paths, + 'target-path': self.target_path, + 'clean-targets': self.clean_targets, + 'log-path': self.log_path, + 'quoting': self.quoting, + 'models': self.models, + 'on-run-start': self.on_run_start, + 'on-run-end': self.on_run_end, + 'archive': self.archive, + 'seeds': self.seeds, + 'require-dbt-version': [ + v.to_version_string() for v in self.dbt_version + ], + }) + if with_packages: + result.update(self.packages.serialize()) + return result + + def validate(self): + try: + ProjectContract(**self.to_project_config()) + except ValidationException as exc: + raise DbtProjectError(str(exc)) + + @classmethod + def from_project_root(cls, project_root, cli_vars): + """Create a project from a root directory. Reads in dbt_project.yml and + packages.yml, if it exists. + + :param project_root str: The path to the project root to load. + :raises DbtProjectError: If the project is missing or invalid, or if + the packages file exists and is invalid. + :returns Project: The project, with defaults populated. + """ + project_root = os.path.normpath(project_root) + project_yaml_filepath = os.path.join(project_root, 'dbt_project.yml') + + # get the project.yml contents + if not path_exists(project_yaml_filepath): + raise DbtProjectError( + 'no dbt_project.yml found at expected path {}' + .format(project_yaml_filepath) + ) + + if isinstance(cli_vars, compat.basestring): + cli_vars = parse_cli_vars(cli_vars) + renderer = ConfigRenderer(cli_vars) + + project_dict = _load_yaml(project_yaml_filepath) + rendered_project = renderer.render_project(project_dict) + rendered_project['project-root'] = project_root + packages_dict = package_data_from_root(project_root) + return cls.from_project_config(rendered_project, packages_dict) + + @classmethod + def from_current_directory(cls, cli_vars): + return cls.from_project_root(os.getcwd(), cli_vars) + + def hashed_name(self): + return hashlib.md5(self.project_name.encode('utf-8')).hexdigest() + + def get_resource_config_paths(self): + """Return a dictionary with 'seeds' and 'models' keys whose values are + lists of lists of strings, where each inner list of strings represents + a configured path in the resource. + """ + return { + 'models': _get_config_paths(self.models), + 'seeds': _get_config_paths(self.seeds), + } + + def get_unused_resource_config_paths(self, resource_fqns, disabled): + """Return a list of lists of strings, where each inner list of strings + represents a type + FQN path of a resource configuration that is not + used. + """ + disabled_fqns = frozenset(tuple(fqn) for fqn in disabled) + resource_config_paths = self.get_resource_config_paths() + unused_resource_config_paths = [] + for resource_type, config_paths in resource_config_paths.items(): + used_fqns = resource_fqns.get(resource_type, frozenset()) + fqns = used_fqns | disabled_fqns + + for config_path in config_paths: + if not _is_config_used(config_path, fqns): + unused_resource_config_paths.append( + (resource_type,) + config_path + ) + return unused_resource_config_paths + + def warn_for_unused_resource_config_paths(self, resource_fqns, disabled): + unused = self.get_unused_resource_config_paths(resource_fqns, disabled) + if len(unused) == 0: + return + + msg = UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE.format( + len(unused), + '\n'.join('- {}'.format('.'.join(u)) for u in unused) + ) + logger.info(printer.yellow(msg)) + + def validate_version(self): + """Ensure this package works with the installed version of dbt.""" + installed = get_installed_version() + if not versions_compatible(*self.dbt_version): + msg = IMPOSSIBLE_VERSION_ERROR.format( + package=self.project_name, + version_spec=[ + x.to_version_string() for x in self.dbt_version + ] + ) + raise DbtProjectError(msg) + + if not versions_compatible(installed, *self.dbt_version): + msg = INVALID_VERSION_ERROR.format( + package=self.project_name, + installed=installed.to_version_string(), + version_spec=[ + x.to_version_string() for x in self.dbt_version + ] + ) + raise DbtProjectError(msg) diff --git a/core/dbt/config/renderer.py b/core/dbt/config/renderer.py new file mode 100644 index 00000000000..35c66ae880a --- /dev/null +++ b/core/dbt/config/renderer.py @@ -0,0 +1,95 @@ +from dbt import compat +from dbt.clients.jinja import get_rendered +from dbt.context.common import env_var +from dbt.context.common import Var +from dbt.exceptions import DbtProfileError +from dbt.exceptions import DbtProjectError +from dbt.exceptions import RecursionException +from dbt.utils import deep_map + + +class ConfigRenderer(object): + """A renderer provides configuration rendering for a given set of cli + variables and a render type. + """ + def __init__(self, cli_vars): + self.context = {'env_var': env_var} + self.context['var'] = Var(None, self.context, cli_vars) + + @staticmethod + def _is_hook_or_model_vars_path(keypath): + if not keypath: + return False + + first = keypath[0] + # run hooks + if first in {'on-run-start', 'on-run-end'}: + return True + # models have two things to avoid + if first in {'seeds', 'models'}: + # model-level hooks + if 'pre-hook' in keypath or 'post-hook' in keypath: + return True + # model-level 'vars' declarations + if 'vars' in keypath: + return True + + return False + + def _render_project_entry(self, value, keypath): + """Render an entry, in case it's jinja. This is meant to be passed to + deep_map. + + If the parsed entry is a string and has the name 'port', this will + attempt to cast it to an int, and on failure will return the parsed + string. + + :param value Any: The value to potentially render + :param key str: The key to convert on. + :return Any: The rendered entry. + """ + # hooks should be treated as raw sql, they'll get rendered later. + # Same goes for 'vars' declarations inside 'models'/'seeds'. + if self._is_hook_or_model_vars_path(keypath): + return value + + return self.render_value(value) + + def render_value(self, value, keypath=None): + # keypath is ignored. + # if it wasn't read as a string, ignore it + if not isinstance(value, compat.basestring): + return value + + return get_rendered(value, self.context) + + def _render_profile_data(self, value, keypath): + result = self.render_value(value) + if len(keypath) == 1 and keypath[-1] == 'port': + try: + result = int(result) + except ValueError: + # let the validator or connection handle this + pass + return result + + def render_project(self, as_parsed): + """Render the parsed data, returning a new dict (or whatever was read). + """ + try: + return deep_map(self._render_project_entry, as_parsed) + except RecursionException: + raise DbtProjectError( + 'Cycle detected: Project input has a reference to itself', + project=as_parsed + ) + + def render_profile_data(self, as_parsed): + """Render the chosen profile entry, as it was parsed.""" + try: + return deep_map(self._render_profile_data, as_parsed) + except RecursionException: + raise DbtProfileError( + 'Cycle detected: Profile input has a reference to itself', + project=as_parsed + ) diff --git a/core/dbt/config/runtime.py b/core/dbt/config/runtime.py new file mode 100644 index 00000000000..ee654474a5b --- /dev/null +++ b/core/dbt/config/runtime.py @@ -0,0 +1,190 @@ + +from copy import deepcopy +import pprint + +from dbt.utils import parse_cli_vars +from dbt.contracts.project import Configuration +from dbt.exceptions import DbtProjectError +from dbt.exceptions import ValidationException +from dbt.adapters.factory import get_relation_class_by_name + +from .profile import Profile +from .project import Project + + +class RuntimeConfig(Project, Profile): + """The runtime configuration, as constructed from its components. There's a + lot because there is a lot of stuff! + """ + def __init__(self, project_name, version, project_root, source_paths, + macro_paths, data_paths, test_paths, analysis_paths, + docs_paths, target_path, clean_targets, log_path, + modules_path, quoting, models, on_run_start, on_run_end, + archive, seeds, dbt_version, profile_name, target_name, + config, threads, credentials, packages, args): + # 'vars' + self.args = args + self.cli_vars = parse_cli_vars(getattr(args, 'vars', '{}')) + # 'project' + Project.__init__( + self, + project_name=project_name, + version=version, + project_root=project_root, + profile_name=profile_name, + source_paths=source_paths, + macro_paths=macro_paths, + data_paths=data_paths, + test_paths=test_paths, + analysis_paths=analysis_paths, + docs_paths=docs_paths, + target_path=target_path, + clean_targets=clean_targets, + log_path=log_path, + modules_path=modules_path, + quoting=quoting, + models=models, + on_run_start=on_run_start, + on_run_end=on_run_end, + archive=archive, + seeds=seeds, + dbt_version=dbt_version, + packages=packages + ) + # 'profile' + Profile.__init__( + self, + profile_name=profile_name, + target_name=target_name, + config=config, + threads=threads, + credentials=credentials + ) + self.validate() + + @classmethod + def from_parts(cls, project, profile, args): + """Instantiate a RuntimeConfig from its components. + + :param profile Profile: A parsed dbt Profile. + :param project Project: A parsed dbt Project. + :param args argparse.Namespace: The parsed command-line arguments. + :returns RuntimeConfig: The new configuration. + """ + quoting = deepcopy( + get_relation_class_by_name(profile.credentials.type) + .DEFAULTS['quote_policy'] + ) + quoting.update(project.quoting) + return cls( + project_name=project.project_name, + version=project.version, + project_root=project.project_root, + source_paths=project.source_paths, + macro_paths=project.macro_paths, + data_paths=project.data_paths, + test_paths=project.test_paths, + analysis_paths=project.analysis_paths, + docs_paths=project.docs_paths, + target_path=project.target_path, + clean_targets=project.clean_targets, + log_path=project.log_path, + modules_path=project.modules_path, + quoting=quoting, + models=project.models, + on_run_start=project.on_run_start, + on_run_end=project.on_run_end, + archive=project.archive, + seeds=project.seeds, + dbt_version=project.dbt_version, + packages=project.packages, + profile_name=profile.profile_name, + target_name=profile.target_name, + config=profile.config, + threads=profile.threads, + credentials=profile.credentials, + args=args + ) + + def new_project(self, project_root): + """Given a new project root, read in its project dictionary, supply the + existing project's profile info, and create a new project file. + + :param project_root str: A filepath to a dbt project. + :raises DbtProfileError: If the profile is invalid. + :raises DbtProjectError: If project is missing or invalid. + :returns RuntimeConfig: The new configuration. + """ + # copy profile + profile = Profile(**self.to_profile_info()) + profile.validate() + # load the new project and its packages. Don't pass cli variables. + project = Project.from_project_root(project_root, {}) + + cfg = self.from_parts( + project=project, + profile=profile, + args=deepcopy(self.args), + ) + # force our quoting back onto the new project. + cfg.quoting = deepcopy(self.quoting) + return cfg + + def serialize(self): + """Serialize the full configuration to a single dictionary. For any + instance that has passed validate() (which happens in __init__), it + matches the Configuration contract. + + Note that args are not serialized. + + :returns dict: The serialized configuration. + """ + result = self.to_project_config(with_packages=True) + result.update(self.to_profile_info(serialize_credentials=True)) + result['cli_vars'] = deepcopy(self.cli_vars) + return result + + def __str__(self): + return pprint.pformat(self.serialize()) + + def validate(self): + """Validate the configuration against its contract. + + :raises DbtProjectError: If the configuration fails validation. + """ + try: + Configuration(**self.serialize()) + except ValidationException as e: + raise DbtProjectError(str(e)) + + if getattr(self.args, 'version_check', False): + self.validate_version() + + @classmethod + def from_args(cls, args): + """Given arguments, read in dbt_project.yml from the current directory, + read in packages.yml if it exists, and use them to find the profile to + load. + + :param args argparse.Namespace: The arguments as parsed from the cli. + :raises DbtProjectError: If the project is invalid or missing. + :raises DbtProfileError: If the profile is invalid or missing. + :raises ValidationException: If the cli variables are invalid. + """ + cli_vars = parse_cli_vars(getattr(args, 'vars', '{}')) + + # build the project and read in packages.yml + project = Project.from_current_directory(cli_vars) + + # build the profile + profile = Profile.from_args( + args=args, + project_profile_name=project.profile_name, + cli_vars=cli_vars + ) + + return cls.from_parts( + project=project, + profile=profile, + args=args + ) diff --git a/dbt/adapters/__init__.py b/core/dbt/context/__init__.py similarity index 100% rename from dbt/adapters/__init__.py rename to core/dbt/context/__init__.py diff --git a/dbt/context/common.py b/core/dbt/context/common.py similarity index 95% rename from dbt/context/common.py rename to core/dbt/context/common.py index d55e43d049d..61b7aeab66d 100644 --- a/dbt/context/common.py +++ b/core/dbt/context/common.py @@ -7,6 +7,8 @@ from dbt.compat import basestring from dbt.node_types import NodeType from dbt.contracts.graph.parsed import ParsedMacro, ParsedNode +from dbt.include.global_project import PACKAGES +from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME import dbt.clients.jinja import dbt.clients.agate_helper @@ -84,6 +86,20 @@ def commit(self): return self.adapter.commit_if_has_connection(self.model.get('name')) +def _add_macro_map(context, package_name, macro_map): + """Update an existing context in-place, adding the given macro map to the + appropriate package namespace. Adapter packages get inserted into the + global namespace. + """ + key = package_name + if package_name in PACKAGES: + key = GLOBAL_PROJECT_NAME + if key not in context: + context[key] = {} + + context[key].update(macro_map) + + def _add_macros(context, model, manifest): macros_to_add = {'global': [], 'local': []} @@ -96,15 +112,12 @@ def _add_macros(context, model, manifest): macro.name: macro.generator(context) } - if context.get(package_name) is None: - context[package_name] = {} - - context.get(package_name, {}) \ - .update(macro_map) + # adapter packages are part of the global project space + _add_macro_map(context, package_name, macro_map) if package_name == model.package_name: macros_to_add['local'].append(macro_map) - elif package_name == dbt.include.GLOBAL_PROJECT_NAME: + elif package_name in PACKAGES: macros_to_add['global'].append(macro_map) # Load global macros before local macros -- local takes precedence diff --git a/dbt/context/parser.py b/core/dbt/context/parser.py similarity index 100% rename from dbt/context/parser.py rename to core/dbt/context/parser.py diff --git a/dbt/context/runtime.py b/core/dbt/context/runtime.py similarity index 100% rename from dbt/context/runtime.py rename to core/dbt/context/runtime.py diff --git a/dbt/clients/__init__.py b/core/dbt/contracts/__init__.py similarity index 100% rename from dbt/clients/__init__.py rename to core/dbt/contracts/__init__.py diff --git a/dbt/contracts/common.py b/core/dbt/contracts/common.py similarity index 100% rename from dbt/contracts/common.py rename to core/dbt/contracts/common.py diff --git a/dbt/contracts/connection.py b/core/dbt/contracts/connection.py similarity index 100% rename from dbt/contracts/connection.py rename to core/dbt/contracts/connection.py diff --git a/dbt/context/__init__.py b/core/dbt/contracts/graph/__init__.py similarity index 100% rename from dbt/context/__init__.py rename to core/dbt/contracts/graph/__init__.py diff --git a/dbt/contracts/graph/compiled.py b/core/dbt/contracts/graph/compiled.py similarity index 100% rename from dbt/contracts/graph/compiled.py rename to core/dbt/contracts/graph/compiled.py diff --git a/dbt/contracts/graph/manifest.py b/core/dbt/contracts/graph/manifest.py similarity index 100% rename from dbt/contracts/graph/manifest.py rename to core/dbt/contracts/graph/manifest.py diff --git a/dbt/contracts/graph/parsed.py b/core/dbt/contracts/graph/parsed.py similarity index 100% rename from dbt/contracts/graph/parsed.py rename to core/dbt/contracts/graph/parsed.py diff --git a/dbt/contracts/graph/unparsed.py b/core/dbt/contracts/graph/unparsed.py similarity index 100% rename from dbt/contracts/graph/unparsed.py rename to core/dbt/contracts/graph/unparsed.py diff --git a/dbt/contracts/project.py b/core/dbt/contracts/project.py similarity index 100% rename from dbt/contracts/project.py rename to core/dbt/contracts/project.py diff --git a/dbt/contracts/results.py b/core/dbt/contracts/results.py similarity index 100% rename from dbt/contracts/results.py rename to core/dbt/contracts/results.py diff --git a/dbt/deprecations.py b/core/dbt/deprecations.py similarity index 100% rename from dbt/deprecations.py rename to core/dbt/deprecations.py diff --git a/dbt/exceptions.py b/core/dbt/exceptions.py similarity index 100% rename from dbt/exceptions.py rename to core/dbt/exceptions.py diff --git a/dbt/flags.py b/core/dbt/flags.py similarity index 100% rename from dbt/flags.py rename to core/dbt/flags.py diff --git a/dbt/contracts/__init__.py b/core/dbt/graph/__init__.py similarity index 100% rename from dbt/contracts/__init__.py rename to core/dbt/graph/__init__.py diff --git a/dbt/graph/selector.py b/core/dbt/graph/selector.py similarity index 100% rename from dbt/graph/selector.py rename to core/dbt/graph/selector.py diff --git a/dbt/hooks.py b/core/dbt/hooks.py similarity index 100% rename from dbt/hooks.py rename to core/dbt/hooks.py diff --git a/core/dbt/include/__init__.py b/core/dbt/include/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/core/dbt/include/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/core/dbt/include/global_project/__init__.py b/core/dbt/include/global_project/__init__.py new file mode 100644 index 00000000000..d0415108083 --- /dev/null +++ b/core/dbt/include/global_project/__init__.py @@ -0,0 +1,11 @@ +import os + +PACKAGE_PATH = os.path.dirname(os.path.dirname(__file__)) +PROJECT_NAME = 'dbt' + +DOCS_INDEX_FILE_PATH = os.path.normpath( + os.path.join(PACKAGE_PATH, "index.html")) + + +# Adapter registration will add to this +PACKAGES = {PROJECT_NAME: PACKAGE_PATH} diff --git a/dbt/include/global_project/dbt_project.yml b/core/dbt/include/global_project/dbt_project.yml similarity index 100% rename from dbt/include/global_project/dbt_project.yml rename to core/dbt/include/global_project/dbt_project.yml diff --git a/dbt/include/global_project/docs/overview.md b/core/dbt/include/global_project/docs/overview.md similarity index 100% rename from dbt/include/global_project/docs/overview.md rename to core/dbt/include/global_project/docs/overview.md diff --git a/dbt/include/global_project/macros/adapters/common.sql b/core/dbt/include/global_project/macros/adapters/common.sql similarity index 100% rename from dbt/include/global_project/macros/adapters/common.sql rename to core/dbt/include/global_project/macros/adapters/common.sql diff --git a/dbt/include/global_project/macros/core.sql b/core/dbt/include/global_project/macros/core.sql similarity index 100% rename from dbt/include/global_project/macros/core.sql rename to core/dbt/include/global_project/macros/core.sql diff --git a/dbt/include/global_project/macros/etc/datetime.sql b/core/dbt/include/global_project/macros/etc/datetime.sql similarity index 100% rename from dbt/include/global_project/macros/etc/datetime.sql rename to core/dbt/include/global_project/macros/etc/datetime.sql diff --git a/dbt/include/global_project/macros/etc/get_custom_schema.sql b/core/dbt/include/global_project/macros/etc/get_custom_schema.sql similarity index 100% rename from dbt/include/global_project/macros/etc/get_custom_schema.sql rename to core/dbt/include/global_project/macros/etc/get_custom_schema.sql diff --git a/dbt/include/global_project/macros/etc/is_incremental.sql b/core/dbt/include/global_project/macros/etc/is_incremental.sql similarity index 100% rename from dbt/include/global_project/macros/etc/is_incremental.sql rename to core/dbt/include/global_project/macros/etc/is_incremental.sql diff --git a/dbt/include/global_project/macros/materializations/archive/archive.sql b/core/dbt/include/global_project/macros/materializations/archive/archive.sql similarity index 89% rename from dbt/include/global_project/macros/materializations/archive/archive.sql rename to core/dbt/include/global_project/macros/materializations/archive/archive.sql index 06eabcade2e..1fb266535de 100644 --- a/dbt/include/global_project/macros/materializations/archive/archive.sql +++ b/core/dbt/include/global_project/macros/materializations/archive/archive.sql @@ -10,10 +10,6 @@ md5("dbt_pk" || '|' || "dbt_updated_at") {% endmacro %} -{% macro bigquery__archive_scd_hash() %} - to_hex(md5(concat(cast(`dbt_pk` as string), '|', cast(`dbt_updated_at` as string)))) -{% endmacro %} - {% macro create_temporary_table(sql, relation) %} {{ return(adapter_macro('create_temporary_table', sql, relation)) }} {% endmacro %} @@ -25,12 +21,6 @@ {{ return(relation) }} {% endmacro %} -{% macro bigquery__create_temporary_table(sql, relation) %} - {% set tmp_relation = adapter.create_temporary_table(sql) %} - {{ return(tmp_relation) }} -{% endmacro %} - - {# Add new columns to the table if applicable #} @@ -46,10 +36,6 @@ {% endfor %} {% endmacro %} -{% macro bigquery__create_columns(relation, columns) %} - {{ adapter.alter_table_add_columns(relation, columns) }} -{% endmacro %} - {# Run the update part of an archive query. Different databases have tricky differences in their `update` semantics. Table projection is @@ -68,14 +54,6 @@ and {{ adapter.quote('change_type') }} = 'update'; {% endmacro %} -{% macro bigquery__archive_update(target_relation, tmp_relation) %} - update {{ target_relation }} as dest - set dest.{{ adapter.quote('valid_to') }} = tmp.{{ adapter.quote('valid_to') }} - from {{ tmp_relation }} as tmp - where tmp.{{ adapter.quote('scd_id') }} = dest.{{ adapter.quote('scd_id') }} - and {{ adapter.quote('change_type') }} = 'update'; -{% endmacro %} - {# Cross-db compatible archival implementation diff --git a/dbt/include/global_project/macros/materializations/common/merge.sql b/core/dbt/include/global_project/macros/materializations/common/merge.sql similarity index 73% rename from dbt/include/global_project/macros/materializations/common/merge.sql rename to core/dbt/include/global_project/macros/materializations/common/merge.sql index bc537925abd..9903f7afd4e 100644 --- a/dbt/include/global_project/macros/materializations/common/merge.sql +++ b/core/dbt/include/global_project/macros/materializations/common/merge.sql @@ -4,7 +4,8 @@ {{ adapter_macro('get_merge_sql', target, source, unique_key, dest_columns) }} {%- endmacro %} -{% macro default__get_merge_sql(target, source, unique_key, dest_columns) -%} + +{% macro common_get_merge_sql(target, source, unique_key, dest_columns) -%} {%- set dest_cols_csv = dest_columns | map(attribute="name") | join(', ') -%} merge into {{ target }} as DBT_INTERNAL_DEST @@ -31,10 +32,12 @@ {% endmacro %} -{% macro redshift__get_merge_sql(target, source, unique_key, dest_columns) -%} - {{ exceptions.raise_compiler_error('get_merge_sql is not implemented for Redshift') }} -{% endmacro %} +{% macro default__get_merge_sql(target, source, unique_key, dest_columns) -%} + {% set typename = adapter.type() %} + + {{ exceptions.raise_compiler_error( + 'get_merge_sql is not implemented for {}'.format(typename) + ) + }} -{% macro postgres__get_merge_sql(target, source, unique_key, dest_columns) -%} - {{ exceptions.raise_compiler_error('get_merge_sql is not implemented for Postgres') }} {% endmacro %} diff --git a/dbt/include/global_project/macros/materializations/helpers.sql b/core/dbt/include/global_project/macros/materializations/helpers.sql similarity index 100% rename from dbt/include/global_project/macros/materializations/helpers.sql rename to core/dbt/include/global_project/macros/materializations/helpers.sql diff --git a/dbt/include/global_project/macros/materializations/incremental/incremental.sql b/core/dbt/include/global_project/macros/materializations/incremental/incremental.sql similarity index 100% rename from dbt/include/global_project/macros/materializations/incremental/incremental.sql rename to core/dbt/include/global_project/macros/materializations/incremental/incremental.sql diff --git a/dbt/include/global_project/macros/materializations/seed/seed.sql b/core/dbt/include/global_project/macros/materializations/seed/seed.sql similarity index 100% rename from dbt/include/global_project/macros/materializations/seed/seed.sql rename to core/dbt/include/global_project/macros/materializations/seed/seed.sql diff --git a/dbt/include/global_project/macros/materializations/table/table.sql b/core/dbt/include/global_project/macros/materializations/table/table.sql similarity index 100% rename from dbt/include/global_project/macros/materializations/table/table.sql rename to core/dbt/include/global_project/macros/materializations/table/table.sql diff --git a/dbt/include/global_project/macros/materializations/view/bq_snowflake_view.sql b/core/dbt/include/global_project/macros/materializations/view/create_or_replace_view.sql similarity index 82% rename from dbt/include/global_project/macros/materializations/view/bq_snowflake_view.sql rename to core/dbt/include/global_project/macros/materializations/view/create_or_replace_view.sql index d5ceafec383..20b2d5df9db 100644 --- a/dbt/include/global_project/macros/materializations/view/bq_snowflake_view.sql +++ b/core/dbt/include/global_project/macros/materializations/view/create_or_replace_view.sql @@ -9,15 +9,6 @@ {%- endif -%} {% endmacro %} -{% macro bigquery__handle_existing_table(full_refresh, non_destructive_mode, old_relation) %} - {%- if full_refresh and not non_destructive_mode -%} - {{ adapter.drop_relation(old_relation) }} - {%- else -%} - {{ exceptions.relation_wrong_type(old_relation, 'view') }} - {%- endif -%} -{% endmacro %} - - {# /* Core materialization implementation. BigQuery and Snowflake are similar because both can use `create or replace view` where the resulting view schema @@ -30,7 +21,7 @@ */ #} -{% macro impl_view_materialization(run_outside_transaction_hooks=True) %} +{% macro create_or_replace_view(run_outside_transaction_hooks=True) %} {%- set identifier = model['alias'] -%} {%- set non_destructive_mode = (flags.NON_DESTRUCTIVE == True) -%} @@ -88,11 +79,3 @@ {{ run_hooks(post_hooks, inside_transaction=False) }} {% endif %} {% endmacro %} - -{% materialization view, adapter='bigquery' -%} - {{ impl_view_materialization(run_outside_transaction_hooks=False) }} -{%- endmaterialization %} - -{% materialization view, adapter='snowflake' -%} - {{ impl_view_materialization() }} -{%- endmaterialization %} diff --git a/dbt/include/global_project/macros/materializations/view/view.sql b/core/dbt/include/global_project/macros/materializations/view/view.sql similarity index 100% rename from dbt/include/global_project/macros/materializations/view/view.sql rename to core/dbt/include/global_project/macros/materializations/view/view.sql diff --git a/dbt/include/global_project/macros/operations/catalog/get_catalog.sql b/core/dbt/include/global_project/macros/operations/catalog/get_catalog.sql similarity index 100% rename from dbt/include/global_project/macros/operations/catalog/get_catalog.sql rename to core/dbt/include/global_project/macros/operations/catalog/get_catalog.sql diff --git a/dbt/include/global_project/macros/operations/relations/get_relations.sql b/core/dbt/include/global_project/macros/operations/relations/get_relations.sql similarity index 100% rename from dbt/include/global_project/macros/operations/relations/get_relations.sql rename to core/dbt/include/global_project/macros/operations/relations/get_relations.sql diff --git a/dbt/include/global_project/macros/schema_tests/accepted_values.sql b/core/dbt/include/global_project/macros/schema_tests/accepted_values.sql similarity index 100% rename from dbt/include/global_project/macros/schema_tests/accepted_values.sql rename to core/dbt/include/global_project/macros/schema_tests/accepted_values.sql diff --git a/dbt/include/global_project/macros/schema_tests/not_null.sql b/core/dbt/include/global_project/macros/schema_tests/not_null.sql similarity index 100% rename from dbt/include/global_project/macros/schema_tests/not_null.sql rename to core/dbt/include/global_project/macros/schema_tests/not_null.sql diff --git a/dbt/include/global_project/macros/schema_tests/relationships.sql b/core/dbt/include/global_project/macros/schema_tests/relationships.sql similarity index 100% rename from dbt/include/global_project/macros/schema_tests/relationships.sql rename to core/dbt/include/global_project/macros/schema_tests/relationships.sql diff --git a/dbt/include/global_project/macros/schema_tests/unique.sql b/core/dbt/include/global_project/macros/schema_tests/unique.sql similarity index 100% rename from dbt/include/global_project/macros/schema_tests/unique.sql rename to core/dbt/include/global_project/macros/schema_tests/unique.sql diff --git a/dbt/include/index.html b/core/dbt/include/index.html similarity index 100% rename from dbt/include/index.html rename to core/dbt/include/index.html diff --git a/dbt/linker.py b/core/dbt/linker.py similarity index 100% rename from dbt/linker.py rename to core/dbt/linker.py diff --git a/dbt/links.py b/core/dbt/links.py similarity index 100% rename from dbt/links.py rename to core/dbt/links.py diff --git a/dbt/loader.py b/core/dbt/loader.py similarity index 100% rename from dbt/loader.py rename to core/dbt/loader.py diff --git a/dbt/logger.py b/core/dbt/logger.py similarity index 100% rename from dbt/logger.py rename to core/dbt/logger.py diff --git a/dbt/main.py b/core/dbt/main.py similarity index 100% rename from dbt/main.py rename to core/dbt/main.py diff --git a/dbt/node_runners.py b/core/dbt/node_runners.py similarity index 99% rename from dbt/node_runners.py rename to core/dbt/node_runners.py index 27f23c6347e..93224161809 100644 --- a/dbt/node_runners.py +++ b/core/dbt/node_runners.py @@ -13,7 +13,6 @@ import dbt.ui.printer import dbt.flags import dbt.schema -import dbt.templates import dbt.writer import six diff --git a/dbt/node_types.py b/core/dbt/node_types.py similarity index 100% rename from dbt/node_types.py rename to core/dbt/node_types.py diff --git a/dbt/parser/__init__.py b/core/dbt/parser/__init__.py similarity index 100% rename from dbt/parser/__init__.py rename to core/dbt/parser/__init__.py diff --git a/dbt/parser/analysis.py b/core/dbt/parser/analysis.py similarity index 100% rename from dbt/parser/analysis.py rename to core/dbt/parser/analysis.py diff --git a/dbt/parser/archives.py b/core/dbt/parser/archives.py similarity index 100% rename from dbt/parser/archives.py rename to core/dbt/parser/archives.py diff --git a/dbt/parser/base.py b/core/dbt/parser/base.py similarity index 98% rename from dbt/parser/base.py rename to core/dbt/parser/base.py index fbcd3050c98..0bf18a7df5d 100644 --- a/dbt/parser/base.py +++ b/core/dbt/parser/base.py @@ -8,6 +8,7 @@ import dbt.clients.jinja import dbt.context.parser +from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME from dbt.utils import coalesce from dbt.logger import GLOBAL_LOGGER as logger from dbt.contracts.graph.parsed import ParsedNode @@ -76,7 +77,7 @@ def get_schema_func(self): if get_schema_macro is None: get_schema_macro = self.macro_manifest.find_macro_by_name( 'generate_schema_name', - dbt.include.GLOBAL_PROJECT_NAME + GLOBAL_PROJECT_NAME ) if get_schema_macro is None: def get_schema(_): diff --git a/dbt/parser/base_sql.py b/core/dbt/parser/base_sql.py similarity index 100% rename from dbt/parser/base_sql.py rename to core/dbt/parser/base_sql.py diff --git a/dbt/parser/data_test.py b/core/dbt/parser/data_test.py similarity index 100% rename from dbt/parser/data_test.py rename to core/dbt/parser/data_test.py diff --git a/dbt/parser/docs.py b/core/dbt/parser/docs.py similarity index 100% rename from dbt/parser/docs.py rename to core/dbt/parser/docs.py diff --git a/dbt/parser/hooks.py b/core/dbt/parser/hooks.py similarity index 100% rename from dbt/parser/hooks.py rename to core/dbt/parser/hooks.py diff --git a/dbt/parser/macros.py b/core/dbt/parser/macros.py similarity index 100% rename from dbt/parser/macros.py rename to core/dbt/parser/macros.py diff --git a/dbt/parser/models.py b/core/dbt/parser/models.py similarity index 100% rename from dbt/parser/models.py rename to core/dbt/parser/models.py diff --git a/dbt/parser/schemas.py b/core/dbt/parser/schemas.py similarity index 100% rename from dbt/parser/schemas.py rename to core/dbt/parser/schemas.py diff --git a/dbt/parser/seeds.py b/core/dbt/parser/seeds.py similarity index 100% rename from dbt/parser/seeds.py rename to core/dbt/parser/seeds.py diff --git a/dbt/parser/source_config.py b/core/dbt/parser/source_config.py similarity index 100% rename from dbt/parser/source_config.py rename to core/dbt/parser/source_config.py diff --git a/dbt/parser/util.py b/core/dbt/parser/util.py similarity index 100% rename from dbt/parser/util.py rename to core/dbt/parser/util.py diff --git a/dbt/profiler.py b/core/dbt/profiler.py similarity index 100% rename from dbt/profiler.py rename to core/dbt/profiler.py diff --git a/dbt/runner.py b/core/dbt/runner.py similarity index 100% rename from dbt/runner.py rename to core/dbt/runner.py diff --git a/dbt/schema.py b/core/dbt/schema.py similarity index 100% rename from dbt/schema.py rename to core/dbt/schema.py diff --git a/dbt/semver.py b/core/dbt/semver.py similarity index 100% rename from dbt/semver.py rename to core/dbt/semver.py diff --git a/dbt/ssh_forward.py b/core/dbt/ssh_forward.py similarity index 100% rename from dbt/ssh_forward.py rename to core/dbt/ssh_forward.py diff --git a/dbt/contracts/graph/__init__.py b/core/dbt/task/__init__.py similarity index 100% rename from dbt/contracts/graph/__init__.py rename to core/dbt/task/__init__.py diff --git a/dbt/task/archive.py b/core/dbt/task/archive.py similarity index 100% rename from dbt/task/archive.py rename to core/dbt/task/archive.py diff --git a/dbt/task/base_task.py b/core/dbt/task/base_task.py similarity index 100% rename from dbt/task/base_task.py rename to core/dbt/task/base_task.py diff --git a/dbt/task/clean.py b/core/dbt/task/clean.py similarity index 100% rename from dbt/task/clean.py rename to core/dbt/task/clean.py diff --git a/dbt/task/compile.py b/core/dbt/task/compile.py similarity index 100% rename from dbt/task/compile.py rename to core/dbt/task/compile.py diff --git a/dbt/task/debug.py b/core/dbt/task/debug.py similarity index 100% rename from dbt/task/debug.py rename to core/dbt/task/debug.py diff --git a/dbt/task/deps.py b/core/dbt/task/deps.py similarity index 100% rename from dbt/task/deps.py rename to core/dbt/task/deps.py diff --git a/dbt/task/generate.py b/core/dbt/task/generate.py similarity index 99% rename from dbt/task/generate.py rename to core/dbt/task/generate.py index 6cc1df5e02c..96d5789eb10 100644 --- a/dbt/task/generate.py +++ b/core/dbt/task/generate.py @@ -5,7 +5,7 @@ from dbt.adapters.factory import get_adapter from dbt.clients.system import write_json from dbt.compat import bigint -from dbt.include import DOCS_INDEX_FILE_PATH +from dbt.include.global_project import DOCS_INDEX_FILE_PATH import dbt.ui.printer import dbt.utils import dbt.compilation diff --git a/dbt/task/init.py b/core/dbt/task/init.py similarity index 100% rename from dbt/task/init.py rename to core/dbt/task/init.py diff --git a/dbt/task/run.py b/core/dbt/task/run.py similarity index 100% rename from dbt/task/run.py rename to core/dbt/task/run.py diff --git a/dbt/task/seed.py b/core/dbt/task/seed.py similarity index 100% rename from dbt/task/seed.py rename to core/dbt/task/seed.py diff --git a/dbt/task/serve.py b/core/dbt/task/serve.py similarity index 94% rename from dbt/task/serve.py rename to core/dbt/task/serve.py index 3a94ffbafed..d5e58762466 100644 --- a/dbt/task/serve.py +++ b/core/dbt/task/serve.py @@ -2,7 +2,7 @@ import os import webbrowser -from dbt.include import DOCS_INDEX_FILE_PATH +from dbt.include.global_project import DOCS_INDEX_FILE_PATH from dbt.compat import SimpleHTTPRequestHandler, TCPServer from dbt.logger import GLOBAL_LOGGER as logger diff --git a/dbt/task/test.py b/core/dbt/task/test.py similarity index 100% rename from dbt/task/test.py rename to core/dbt/task/test.py diff --git a/dbt/tracking.py b/core/dbt/tracking.py similarity index 100% rename from dbt/tracking.py rename to core/dbt/tracking.py diff --git a/dbt/graph/__init__.py b/core/dbt/ui/__init__.py similarity index 100% rename from dbt/graph/__init__.py rename to core/dbt/ui/__init__.py diff --git a/dbt/ui/colors.py b/core/dbt/ui/colors.py similarity index 100% rename from dbt/ui/colors.py rename to core/dbt/ui/colors.py diff --git a/dbt/ui/printer.py b/core/dbt/ui/printer.py similarity index 100% rename from dbt/ui/printer.py rename to core/dbt/ui/printer.py diff --git a/dbt/utils.py b/core/dbt/utils.py similarity index 98% rename from dbt/utils.py rename to core/dbt/utils.py index 788c73f760f..b5c2001edaf 100644 --- a/dbt/utils.py +++ b/core/dbt/utils.py @@ -13,7 +13,7 @@ import dbt.exceptions import dbt.flags -from dbt.include import GLOBAL_DBT_MODULES_PATH +from dbt.include.global_project import PACKAGES from dbt.compat import basestring, DECIMALS from dbt.logger import GLOBAL_LOGGER as logger from dbt.node_types import NodeType @@ -216,10 +216,8 @@ def dependencies_for_path(config, module_path): def dependency_projects(config): - module_paths = [ - GLOBAL_DBT_MODULES_PATH, - os.path.join(config.project_root, config.modules_path) - ] + module_paths = list(PACKAGES.values()) + module_paths.append(os.path.join(config.project_root, config.modules_path)) for module_path in module_paths: for entry in dependencies_for_path(config, module_path): diff --git a/dbt/version.py b/core/dbt/version.py similarity index 98% rename from dbt/version.py rename to core/dbt/version.py index a990b6614f3..752926a2f59 100644 --- a/dbt/version.py +++ b/core/dbt/version.py @@ -57,5 +57,5 @@ def get_version_information(): .format(version_msg)) -__version__ = '0.12.2rc1' +__version__ = '0.13.0a1' installed = get_installed_version() diff --git a/dbt/writer.py b/core/dbt/writer.py similarity index 100% rename from dbt/writer.py rename to core/dbt/writer.py diff --git a/core/etc/dag.png b/core/etc/dag.png new file mode 100644 index 0000000000000000000000000000000000000000..ad76924a43f78d4e64c2220fa53273ac08663c8f GIT binary patch literal 161481 zcmeFZcT`i`x;G4hh{&dhH0dDHK|q911r(Hy^xi>ggwRWnB1nxQy%z-xP3avJsi7$) zK!DIg54{DFFMIEM&OP^j``-7w|Gr~qjEv08wPt_H?so*PjBhh(!TfM+rgG8jXC`mK@jwFc_&6CXkCsc~+Wz_V|g zqtgs#ac?+9d)gUVZoN2Hiof+BH#q|UOsBMQ%RJNed4V+)*VX+nqOH5P-WLCMTVNp7 zevclT2ixyR$>3mWiIS%~(s#C&dIYs|HB6BgG5)snW23-M@OI)L+oHaS^*F<=>X4;g z`9pO#UvUjFax1as&sOGSp$Sqn?^e(1e=QE&BwfhX=Vz%D!OE~9u) zQ{noCU3LyDwXA%v>ie{T@3e9}>Q}U<=^1kKS+5nq71`r`o%^h|v{$4bg0JobcqZGn z-|IEg)k~s&G#{$+nlmqlsX$8dM$oq&sRXt=k1TmKzRFWwBkX*`+xMFJ-8HsLMPl1$ z1J6w#Txvy($o4CJ=H-@%-g>i>c~4kd@Jr^8y1IH#VutyMBFGtwlRc0lTVS~vp3ubQY>Gq^IR8m-hD}-aH&kd&r8Jg@bL}u&O3tN=$OB> z?cM}a5f@P1^n7rG_#Ih>G3wApwEVRaZ`AP@#sM3i?I?aJQq_EFt?wp{q(+M*vtg>E zH|6$jlPnOCbC4=2F_7N+`PiG9>&LaPPqV4~eo)?2{Qh3*9^rGvA8*M`;>(}JzQ6sE zdHRh$SM>YkK9d!(UtzM3o8N&~gbuEE7kDQpxD@rQ+mGKoR*Lxf%==MhxMM-UOTuP4 zA!UJ|L@$W~>DprF6=i;=t+P|SddPgEB)aAYU@`brL8HI9m{zP=tibn_C4SGkMn`j| zUAjt|Q?gS=?H@IJ=B?cGv}|u7-*4@ioP9czIP(goZ6^$64ZVeY!`2zf^MKTp_*VF> zFx9u*AF0-8$Zmd$Y>N#1e*HU(66GS%`)4aunr{<-Jho*PO=x`E7}c0#tm?+$OVtt+ zaqsr?2zx~@t~W_%ADLfpI16kLY>;o<{~Uc3qrtV6f`@vQdV##k`u8UyH&qfo7V8@6z)I`JZ;khk`#rp?nYX5q zrAYHLb!^~e^vkrD7PTYZ2F-xEX!$tdqN-2mqJ~knQKTpXxm^?wSMK{RV_2zUwwwTU?>O25TKDwPlx&23#_zhh%zef%ZH zjph*~r$^Y4&Ay@L#<>1Oap}`CA)PWEnNO4b-#p_1vRWigEGSZxFbYPzbJLP0>3(^x zj?Cg(BxI9jLT=*2c-;8bIJz|S=Yq?b^G8>=(U;@?g)#7l@azJB6)1Y``j8RLG>ruh z*?l@qS+V^BI;cY>ZDrjv*M>=n3PpF89Rl~56T=s09RUA$lXvuJ?Zz=PulLt+&V z+uT(ig2dD%5gxu>%n}Wf+7eX{Iy}NmrHtPS)|+{2aB6tv-pkboY~*s!{ep?Xt9z

iA0U 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

G}*@!sN6a% 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⪖Ar7jM_pu~iOk@_Chlb1wKx~yev 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 Yj=4sy6{;cXw+no3Dyd(~Rk;1&UnWwQng9R* literal 0 HcmV?d00001 diff --git a/events/schemas/com.fishtownanalytics/invocation_env_context.json b/core/events/schemas/com.fishtownanalytics/invocation_env_context.json similarity index 100% rename from events/schemas/com.fishtownanalytics/invocation_env_context.json rename to core/events/schemas/com.fishtownanalytics/invocation_env_context.json diff --git a/events/schemas/com.fishtownanalytics/invocation_event.json b/core/events/schemas/com.fishtownanalytics/invocation_event.json similarity index 100% rename from events/schemas/com.fishtownanalytics/invocation_event.json rename to core/events/schemas/com.fishtownanalytics/invocation_event.json diff --git a/events/schemas/com.fishtownanalytics/platform_context.json b/core/events/schemas/com.fishtownanalytics/platform_context.json similarity index 100% rename from events/schemas/com.fishtownanalytics/platform_context.json rename to core/events/schemas/com.fishtownanalytics/platform_context.json diff --git a/events/schemas/com.fishtownanalytics/run_model_context.json b/core/events/schemas/com.fishtownanalytics/run_model_context.json similarity index 100% rename from events/schemas/com.fishtownanalytics/run_model_context.json rename to core/events/schemas/com.fishtownanalytics/run_model_context.json diff --git a/core/scripts/create_adapter_plugins.py b/core/scripts/create_adapter_plugins.py new file mode 100644 index 00000000000..9b0349f1876 --- /dev/null +++ b/core/scripts/create_adapter_plugins.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +import argparse +import os +import sys + +pj = os.path.join + +PROJECT_TEMPLATE = ''' +name: dbt_{adapter} +version: {version} + +macro-paths: ["macros"] +''' + +NAMESPACE_INIT_TEMPLATE = ''' +__path__ = __import__('pkgutil').extend_path(__path__, __name__) +'''.lstrip() + + +# TODO: make this not default to fishtown for everything! +SETUP_PY_TEMPLATE = ''' +#!/usr/bin/env python +from setuptools import find_packages +from distutils.core import setup + +package_name = "dbt-{adapter}" +package_version = "{version}" +description = """The {adapter} adpter plugin for dbt (data build tool)""" + +setup( + name=package_name, + version=package_version, + description=description, + long_description_content_type=description, + author={author_name}, + author_email={author_email}, + url={url}, + packages=find_packages(), + install_requires=[ + 'dbt-core=={dbt_core_version}', + {dependencies} + ] +) +'''.lstrip() + + + +ADAPTER_INIT_TEMPLATE = ''' +from dbt.adapters.{adapter}.connections import {title_adapter}ConnectionManager +from dbt.adapters.{adapter}.connections import {title_adapter}Credentials +from dbt.adapters.{adapter}.impl import {title_adapter}Adapter + +from dbt.adapters.base import AdapterPlugin +from dbt.include import {adapter} + +Plugin = AdapterPlugin( + adapter={title_adapter}Adapter, + credentials={title_adapter}Credentials, + include_path={adapter}.PACKAGE_PATH) +'''.lstrip() + + +INCLUDE_INIT_TEMPLATE = ''' +import os +PACKAGE_PATH = os.path.dirname(os.path.dirname(__file__)) +'''.lstrip() + + +def parse_args(argv=None): + if argv is None: + argv = sys.argv[1:] + parser = argparse.ArgumentParser() + parser.add_argument('root') + parser.add_argument('adapter') + parser.add_argument('--title-case', '-t', default=None) + parser.add_argument('--dependency', action='append') + parser.add_argument('--dbt-core-version', default='0.13.0') + parser.add_argument('--email') + parser.add_argument('--author') + parser.add_argument('--url') + parser.add_argument('--package-version', default='0.0.1') + parser.add_argument('--project-version', default='1.0') + parsed = parser.parse_args() + + if parsed.title_case is None: + parsed.title_case = parsed.adapter.title() + + if parsed.dependency: + #['a', 'b'] => "'a',\n 'b'"; ['a'] -> "'a'," + parsed.dependency = '\n '.join( + "'{}',".format(d) for d in parsed.dependency + ) + else: + parsed.dependency = '' + + if parsed.email is not None: + parsed.email = "'{}'".format(parsed.email) + else: + parsed.email = '' + if parsed.author is not None: + parsed.author = "'{}'".format(parsed.author) + else: + parsed.author = '' + if parsed.url is not None: + parsed.url = "'{}'".format(parsed.url) + else: + parsed.url = '' + return parsed + + + +def main(): + parsed = parse_args() + dest = pj(parsed.root, parsed.adapter) + if os.path.exists(dest): + raise Exception('path exists') + + adapters_path = pj(dest, 'dbt', 'adapters', parsed.adapter) + include_path = pj(dest, 'dbt', 'include', parsed.adapter) + os.makedirs(adapters_path) + os.makedirs(pj(include_path, 'macros')) + + # namespaces! + with open(pj(dest, 'dbt', '__init__.py'), 'w') as fp: + fp.write(NAMESPACE_INIT_TEMPLATE) + with open(pj(dest, 'dbt', 'adapters', '__init__.py'), 'w') as fp: + fp.write(NAMESPACE_INIT_TEMPLATE) + with open(pj(dest, 'dbt', 'include', '__init__.py'), 'w') as fp: + fp.write(NAMESPACE_INIT_TEMPLATE) + + # setup script! + with open(pj(dest, 'setup.py'), 'w') as fp: + fp.write(SETUP_PY_TEMPLATE.format(adapter=parsed.adapter, + version=parsed.package_version, + author_name=parsed.author, + author_email=parsed.email, + url=parsed.url, + dbt_core_version=parsed.dbt_core_version, + dependencies=parsed.dependency)) + + + # adapter stuff! + with open(pj(adapters_path, '__init__.py'), 'w') as fp: + fp.write(ADAPTER_INIT_TEMPLATE.format(adapter=parsed.adapter, + title_adapter=parsed.title_case)) + + # macro/project stuff! + with open(pj(include_path, '__init__.py'), 'w') as fp: + fp.write(INCLUDE_INIT_TEMPLATE) + + with open(pj(include_path, 'dbt_project.yml'), 'w') as fp: + fp.write(PROJECT_TEMPLATE.format(adapter=parsed.adapter, + version=parsed.project_version)) + + # TODO: + # - bare class impls for mandatory subclasses + # (ConnectionManager, Credentials, Adapter) + # - impls of mandatory abstract methods w/explicit NotImplementedErrors + + +if __name__ == '__main__': + main() diff --git a/scripts/dbt b/core/scripts/dbt similarity index 100% rename from scripts/dbt rename to core/scripts/dbt diff --git a/scripts/upgrade_dbt_schema_tests_v1_to_v2.py b/core/scripts/upgrade_dbt_schema_tests_v1_to_v2.py similarity index 100% rename from scripts/upgrade_dbt_schema_tests_v1_to_v2.py rename to core/scripts/upgrade_dbt_schema_tests_v1_to_v2.py diff --git a/setup.py b/core/setup.py similarity index 69% rename from setup.py rename to core/setup.py index 95e3a7d1593..68c5495e8fd 100644 --- a/setup.py +++ b/core/setup.py @@ -6,22 +6,12 @@ def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() -package_name = "dbt" -package_version = "0.12.2rc1" +package_name = "dbt-core" +package_version = "0.13.0a1" description = """dbt (data build tool) is a command line tool that helps \ analysts and engineers transform data in their warehouse more effectively""" -boto_requirements = [ - 'boto3>=1.6.23,<1.8.0', - 'botocore>=1.9.23,<1.11.0', -] - -postgres_requirements = [ - 'psycopg2>=2.7.5,<2.8', -] - - setup( name=package_name, version=package_version, @@ -50,15 +40,6 @@ def read(fname): scripts=[ 'scripts/dbt', ], - extras_require={ - # when we split out adapters into their own python packages, these - # extras requirements lists will just be the appropriate adapter - # packages - 'snowflake': ['snowflake-connector-python>=1.4.9'] + boto_requirements, - 'bigquery': ['google-cloud-bigquery>=1.0.0,<2'], - 'redshift': boto_requirements[:] + postgres_requirements[:], - 'postgres': postgres_requirements[:], - }, install_requires=[ 'Jinja2>=2.10', 'PyYAML>=3.11', diff --git a/dbt/adapters/postgres/__init__.py b/dbt/adapters/postgres/__init__.py deleted file mode 100644 index d63b2d32b58..00000000000 --- a/dbt/adapters/postgres/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from dbt.adapters.postgres.connections import PostgresConnectionManager -from dbt.adapters.postgres.connections import PostgresCredentials -from dbt.adapters.postgres.impl import PostgresAdapter - -Adapter = PostgresAdapter -Credentials = PostgresCredentials diff --git a/dbt/adapters/redshift/__init__.py b/dbt/adapters/redshift/__init__.py deleted file mode 100644 index aa47613a429..00000000000 --- a/dbt/adapters/redshift/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from dbt.adapters.redshift.connections import RedshiftConnectionManager -from dbt.adapters.redshift.connections import RedshiftCredentials -from dbt.adapters.redshift.impl import RedshiftAdapter - -Adapter = RedshiftAdapter -Credentials = RedshiftCredentials diff --git a/dbt/config.py b/dbt/config.py deleted file mode 100644 index 65365aeec61..00000000000 --- a/dbt/config.py +++ /dev/null @@ -1,1077 +0,0 @@ -import os.path -import os -from copy import deepcopy -import hashlib -import pprint - -import dbt.exceptions -import dbt.clients.yaml_helper -import dbt.clients.system -import dbt.utils -from dbt.compat import basestring -from dbt.contracts.project import Project as ProjectContract, Configuration, \ - PackageConfig, ProfileConfig -from dbt.exceptions import DbtProjectError, DbtProfileError, RecursionException -from dbt.context.common import env_var, Var -from dbt import compat -from dbt.adapters.factory import load_adapter, get_relation_class_by_name - -from dbt.logger import GLOBAL_LOGGER as logger -from dbt.utils import DBTConfigKeys -from dbt.version import get_installed_version -from dbt.semver import VersionSpecifier, versions_compatible -import dbt.ui.printer - -DEFAULT_THREADS = 1 -DEFAULT_SEND_ANONYMOUS_USAGE_STATS = True -DEFAULT_USE_COLORS = True -DEFAULT_PROFILES_DIR = os.path.join(os.path.expanduser('~'), '.dbt') -PROFILES_DIR = os.path.expanduser( - os.environ.get('DBT_PROFILES_DIR', DEFAULT_PROFILES_DIR) - ) - -INVALID_PROFILE_MESSAGE = """ -dbt encountered an error while trying to read your profiles.yml file. - -{error_string} -""" - - -NO_SUPPLIED_PROFILE_ERROR = """\ -dbt cannot run because no profile was specified for this dbt project. -To specify a profile for this project, add a line like the this to -your dbt_project.yml file: - -profile: [profile name] - -Here, [profile name] should be replaced with a profile name -defined in your profiles.yml file. You can find profiles.yml here: - -{profiles_file}/profiles.yml -""".format(profiles_file=PROFILES_DIR) - - -UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE = """\ -WARNING: Configuration paths exist in your dbt_project.yml file which do not \ -apply to any resources. -There are {} unused configuration paths:\n{} -""" - -INVALID_VERSION_ERROR = """\ -This version of dbt is not supported with the '{package}' package. - Installed version of dbt: {installed} - Required version of dbt for '{package}': {version_spec} - -Check the requirements for the '{package}' package, or run dbt again with \ ---no-version-check -""" - -IMPOSSIBLE_VERSION_ERROR = """\ -The package version requirement can never be satisfied for the '{package} -package. - Required versions of dbt for '{package}': {version_spec} - -Check the requirements for the '{package}' package, or run dbt again with \ ---no-version-check -""" - - -def read_profile(profiles_dir): - path = os.path.join(profiles_dir, 'profiles.yml') - - contents = None - if os.path.isfile(path): - try: - contents = dbt.clients.system.load_file_contents(path, strip=False) - return dbt.clients.yaml_helper.load_yaml_text(contents) - except dbt.exceptions.ValidationException as e: - msg = INVALID_PROFILE_MESSAGE.format(error_string=e) - raise dbt.exceptions.ValidationException(msg) - - return {} - - -def read_profiles(profiles_dir=None): - """This is only used in main, for some error handling""" - if profiles_dir is None: - profiles_dir = PROFILES_DIR - - raw_profiles = read_profile(profiles_dir) - - if raw_profiles is None: - profiles = {} - else: - profiles = {k: v for (k, v) in raw_profiles.items() if k != 'config'} - - return profiles - - -class ConfigRenderer(object): - """A renderer provides configuration rendering for a given set of cli - variables and a render type. - """ - def __init__(self, cli_vars): - self.context = {'env_var': env_var} - self.context['var'] = Var(None, self.context, cli_vars) - - @staticmethod - def _is_hook_or_model_vars_path(keypath): - if not keypath: - return False - - first = keypath[0] - # run hooks - if first in {'on-run-start', 'on-run-end'}: - return True - # models have two things to avoid - if first in {'seeds', 'models'}: - # model-level hooks - if 'pre-hook' in keypath or 'post-hook' in keypath: - return True - # model-level 'vars' declarations - if 'vars' in keypath: - return True - - return False - - def _render_project_entry(self, value, keypath): - """Render an entry, in case it's jinja. This is meant to be passed to - dbt.utils.deep_map. - - If the parsed entry is a string and has the name 'port', this will - attempt to cast it to an int, and on failure will return the parsed - string. - - :param value Any: The value to potentially render - :param key str: The key to convert on. - :return Any: The rendered entry. - """ - # hooks should be treated as raw sql, they'll get rendered later. - # Same goes for 'vars' declarations inside 'models'/'seeds'. - if self._is_hook_or_model_vars_path(keypath): - return value - - return self.render_value(value) - - def render_value(self, value, keypath=None): - # keypath is ignored. - # if it wasn't read as a string, ignore it - if not isinstance(value, compat.basestring): - return value - - return dbt.clients.jinja.get_rendered(value, self.context) - - def _render_profile_data(self, value, keypath): - result = self.render_value(value) - if len(keypath) == 1 and keypath[-1] == 'port': - try: - result = int(result) - except ValueError: - # let the validator or connection handle this - pass - return result - - def render_project(self, as_parsed): - """Render the parsed data, returning a new dict (or whatever was read). - """ - try: - return dbt.utils.deep_map(self._render_project_entry, as_parsed) - except RecursionException: - raise DbtProjectError( - 'Cycle detected: Project input has a reference to itself', - project=project_dict - ) - - def render_profile_data(self, as_parsed): - """Render the chosen profile entry, as it was parsed.""" - try: - return dbt.utils.deep_map(self._render_profile_data, as_parsed) - except RecursionException: - raise DbtProfileError( - 'Cycle detected: Profile input has a reference to itself', - project=as_parsed - ) - - -def _list_if_none(value): - if value is None: - value = [] - return value - - -def _dict_if_none(value): - if value is None: - value = {} - return value - - -def _list_if_none_or_string(value): - value = _list_if_none(value) - if isinstance(value, compat.basestring): - return [value] - return value - - -def _parse_versions(versions): - """Parse multiple versions as read from disk. The versions value may be any - one of: - - a single version string ('>0.12.1') - - a single string specifying multiple comma-separated versions - ('>0.11.1,<=0.12.2') - - an array of single-version strings (['>0.11.1', '<=0.12.2']) - - Regardless, this will return a list of VersionSpecifiers - """ - if isinstance(versions, basestring): - versions = versions.split(',') - return [VersionSpecifier.from_version_string(v) for v in versions] - - -class Project(object): - def __init__(self, project_name, version, project_root, profile_name, - source_paths, macro_paths, data_paths, test_paths, - analysis_paths, docs_paths, target_path, clean_targets, - log_path, modules_path, quoting, models, on_run_start, - on_run_end, archive, seeds, dbt_version, packages): - self.project_name = project_name - self.version = version - self.project_root = project_root - self.profile_name = profile_name - self.source_paths = source_paths - self.macro_paths = macro_paths - self.data_paths = data_paths - self.test_paths = test_paths - self.analysis_paths = analysis_paths - self.docs_paths = docs_paths - self.target_path = target_path - self.clean_targets = clean_targets - self.log_path = log_path - self.modules_path = modules_path - self.quoting = quoting - self.models = models - self.on_run_start = on_run_start - self.on_run_end = on_run_end - self.archive = archive - self.seeds = seeds - self.dbt_version = dbt_version - self.packages = packages - - @staticmethod - def _preprocess(project_dict): - """Pre-process certain special keys to convert them from None values - into empty containers, and to turn strings into arrays of strings. - """ - handlers = { - ('archive',): _list_if_none, - ('on-run-start',): _list_if_none_or_string, - ('on-run-end',): _list_if_none_or_string, - } - - for k in ('models', 'seeds'): - handlers[(k,)] = _dict_if_none - handlers[(k, 'vars')] = _dict_if_none - handlers[(k, 'pre-hook')] = _list_if_none_or_string - handlers[(k, 'post-hook')] = _list_if_none_or_string - handlers[('seeds', 'column_types')] = _dict_if_none - - def converter(value, keypath): - if keypath in handlers: - handler = handlers[keypath] - return handler(value) - else: - return value - - return dbt.utils.deep_map(converter, project_dict) - - @classmethod - def from_project_config(cls, project_dict, packages_dict=None): - """Create a project from its project and package configuration, as read - by yaml.safe_load(). - - :param project_dict dict: The dictionary as read from disk - :param packages_dict Optional[dict]: If it exists, the packages file as - read from disk. - :raises DbtProjectError: If the project is missing or invalid, or if - the packages file exists and is invalid. - :returns Project: The project, with defaults populated. - """ - try: - project_dict = cls._preprocess(project_dict) - except RecursionException: - raise DbtProjectError( - 'Cycle detected: Project input has a reference to itself', - project=project_dict - ) - # just for validation. - try: - ProjectContract(**project_dict) - except dbt.exceptions.ValidationException as e: - raise DbtProjectError(str(e)) - - # name/version are required in the Project definition, so we can assume - # they are present - name = project_dict['name'] - version = project_dict['version'] - # this is added at project_dict parse time and should always be here - # once we see it. - project_root = project_dict['project-root'] - # this is only optional in the sense that if it's not present, it needs - # to have been a cli argument. - profile_name = project_dict.get('profile') - # these are optional - source_paths = project_dict.get('source-paths', ['models']) - macro_paths = project_dict.get('macro-paths', ['macros']) - data_paths = project_dict.get('data-paths', ['data']) - test_paths = project_dict.get('test-paths', ['test']) - analysis_paths = project_dict.get('analysis-paths', []) - docs_paths = project_dict.get('docs-paths', source_paths[:]) - target_path = project_dict.get('target-path', 'target') - # should this also include the modules path by default? - clean_targets = project_dict.get('clean-targets', [target_path]) - log_path = project_dict.get('log-path', 'logs') - modules_path = project_dict.get('modules-path', 'dbt_modules') - # in the default case we'll populate this once we know the adapter type - quoting = project_dict.get('quoting', {}) - - models = project_dict.get('models', {}) - on_run_start = project_dict.get('on-run-start', []) - on_run_end = project_dict.get('on-run-end', []) - archive = project_dict.get('archive', []) - seeds = project_dict.get('seeds', {}) - dbt_raw_version = project_dict.get('require-dbt-version', '>=0.0.0') - - try: - dbt_version = _parse_versions(dbt_raw_version) - except dbt.exceptions.SemverException as e: - raise DbtProjectError(str(e)) - - packages = package_config_from_data(packages_dict) - - project = cls( - project_name=name, - version=version, - project_root=project_root, - profile_name=profile_name, - source_paths=source_paths, - macro_paths=macro_paths, - data_paths=data_paths, - test_paths=test_paths, - analysis_paths=analysis_paths, - docs_paths=docs_paths, - target_path=target_path, - clean_targets=clean_targets, - log_path=log_path, - modules_path=modules_path, - quoting=quoting, - models=models, - on_run_start=on_run_start, - on_run_end=on_run_end, - archive=archive, - seeds=seeds, - dbt_version=dbt_version, - packages=packages - ) - # sanity check - this means an internal issue - project.validate() - return project - - def __str__(self): - cfg = self.to_project_config(with_packages=True) - return pprint.pformat(cfg) - - def __eq__(self, other): - if not (isinstance(other, self.__class__) and - isinstance(self, other.__class__)): - return False - return self.to_project_config(with_packages=True) == \ - other.to_project_config(with_packages=True) - - def to_project_config(self, with_packages=False): - """Return a dict representation of the config that could be written to - disk with `yaml.safe_dump` to get this configuration. - - :param with_packages bool: If True, include the serialized packages - file in the root. - :returns dict: The serialized profile. - """ - result = deepcopy({ - 'name': self.project_name, - 'version': self.version, - 'project-root': self.project_root, - 'profile': self.profile_name, - 'source-paths': self.source_paths, - 'macro-paths': self.macro_paths, - 'data-paths': self.data_paths, - 'test-paths': self.test_paths, - 'analysis-paths': self.analysis_paths, - 'docs-paths': self.docs_paths, - 'target-path': self.target_path, - 'clean-targets': self.clean_targets, - 'log-path': self.log_path, - 'quoting': self.quoting, - 'models': self.models, - 'on-run-start': self.on_run_start, - 'on-run-end': self.on_run_end, - 'archive': self.archive, - 'seeds': self.seeds, - 'require-dbt-version': [ - v.to_version_string() for v in self.dbt_version - ], - }) - if with_packages: - result.update(self.packages.serialize()) - return result - - def validate(self): - try: - ProjectContract(**self.to_project_config()) - except dbt.exceptions.ValidationException as exc: - raise DbtProjectError(str(exc)) - - @classmethod - def from_project_root(cls, project_root, cli_vars): - """Create a project from a root directory. Reads in dbt_project.yml and - packages.yml, if it exists. - - :param project_root str: The path to the project root to load. - :raises DbtProjectError: If the project is missing or invalid, or if - the packages file exists and is invalid. - :returns Project: The project, with defaults populated. - """ - project_root = os.path.normpath(project_root) - project_yaml_filepath = os.path.join(project_root, 'dbt_project.yml') - - # get the project.yml contents - if not dbt.clients.system.path_exists(project_yaml_filepath): - raise DbtProjectError( - 'no dbt_project.yml found at expected path {}' - .format(project_yaml_filepath) - ) - - if isinstance(cli_vars, compat.basestring): - cli_vars = dbt.utils.parse_cli_vars(cli_vars) - renderer = ConfigRenderer(cli_vars) - - project_dict = _load_yaml(project_yaml_filepath) - rendered_project = renderer.render_project(project_dict) - rendered_project['project-root'] = project_root - packages_dict = package_data_from_root(project_root) - return cls.from_project_config(rendered_project, packages_dict) - - @classmethod - def from_current_directory(cls, cli_vars): - return cls.from_project_root(os.getcwd(), cli_vars) - - def hashed_name(self): - return hashlib.md5(self.project_name.encode('utf-8')).hexdigest() - - def get_resource_config_paths(self): - """Return a dictionary with 'seeds' and 'models' keys whose values are - lists of lists of strings, where each inner list of strings represents - a configured path in the resource. - """ - return { - 'models': _get_config_paths(self.models), - 'seeds': _get_config_paths(self.seeds), - } - - def get_unused_resource_config_paths(self, resource_fqns, disabled): - """Return a list of lists of strings, where each inner list of strings - represents a type + FQN path of a resource configuration that is not - used. - """ - disabled_fqns = frozenset(tuple(fqn) for fqn in disabled) - resource_config_paths = self.get_resource_config_paths() - unused_resource_config_paths = [] - for resource_type, config_paths in resource_config_paths.items(): - used_fqns = resource_fqns.get(resource_type, frozenset()) - fqns = used_fqns | disabled_fqns - - for config_path in config_paths: - if not _is_config_used(config_path, fqns): - unused_resource_config_paths.append( - (resource_type,) + config_path - ) - return unused_resource_config_paths - - def warn_for_unused_resource_config_paths(self, resource_fqns, disabled): - unused = self.get_unused_resource_config_paths(resource_fqns, disabled) - if len(unused) == 0: - return - - msg = UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE.format( - len(unused), - '\n'.join('- {}'.format('.'.join(u)) for u in unused) - ) - logger.info(dbt.ui.printer.yellow(msg)) - - def validate_version(self): - """Ensure this package works with the installed version of dbt.""" - installed = get_installed_version() - if not versions_compatible(*self.dbt_version): - msg = IMPOSSIBLE_VERSION_ERROR.format( - package=self.project_name, - version_spec=[ - x.to_version_string() for x in self.dbt_version - ] - ) - raise DbtProjectError(msg) - - if not versions_compatible(installed, *self.dbt_version): - msg = INVALID_VERSION_ERROR.format( - package=self.project_name, - installed=installed.to_version_string(), - version_spec=[ - x.to_version_string() for x in self.dbt_version - ] - ) - raise DbtProjectError(msg) - - -class UserConfig(object): - def __init__(self, send_anonymous_usage_stats, use_colors): - self.send_anonymous_usage_stats = send_anonymous_usage_stats - self.use_colors = use_colors - - @classmethod - def from_dict(cls, cfg=None): - if cfg is None: - cfg = {} - send_anonymous_usage_stats = cfg.get( - 'send_anonymous_usage_stats', - DEFAULT_SEND_ANONYMOUS_USAGE_STATS - ) - use_colors = cfg.get( - 'use_colors', - DEFAULT_USE_COLORS - ) - return cls(send_anonymous_usage_stats, use_colors) - - def to_dict(self): - return { - 'send_anonymous_usage_stats': self.send_anonymous_usage_stats, - 'use_colors': self.use_colors, - } - - @classmethod - def from_directory(cls, directory): - user_cfg = None - profile = read_profile(directory) - if profile: - user_cfg = profile.get('config', {}) - return cls.from_dict(user_cfg) - - -class Profile(object): - def __init__(self, profile_name, target_name, config, threads, - credentials): - self.profile_name = profile_name - self.target_name = target_name - if isinstance(config, dict): - config = UserConfig.from_dict(config) - self.config = config - self.threads = threads - self.credentials = credentials - - def to_profile_info(self, serialize_credentials=False): - """Unlike to_project_config, this dict is not a mirror of any existing - on-disk data structure. It's used when creating a new profile from an - existing one. - - :param serialize_credentials bool: If True, serialize the credentials. - Otherwise, the Credentials object will be copied. - :returns dict: The serialized profile. - """ - result = { - 'profile_name': self.profile_name, - 'target_name': self.target_name, - 'config': self.config.to_dict(), - 'threads': self.threads, - 'credentials': self.credentials.incorporate(), - } - if serialize_credentials: - result['credentials'] = result['credentials'].serialize() - return result - - def __str__(self): - return pprint.pformat(self.to_profile_info()) - - def __eq__(self, other): - if not (isinstance(other, self.__class__) and - isinstance(self, other.__class__)): - return False - return False - return self.to_profile_info() == other.to_profile_info() - - def validate(self): - if self.credentials: - self.credentials.validate() - try: - ProfileConfig(**self.to_profile_info(serialize_credentials=True)) - except dbt.exceptions.ValidationException as exc: - raise DbtProfileError(str(exc)) - - @staticmethod - def _credentials_from_profile(profile, profile_name, target_name): - # credentials carry their 'type' in their actual type, not their - # attributes. We do want this in order to pick our Credentials class. - if 'type' not in profile: - raise DbtProfileError( - 'required field "type" not found in profile {} and target {}' - .format(profile_name, target_name)) - - typename = profile.pop('type') - - try: - cls = load_adapter(typename) - credentials = cls(**profile) - except dbt.exceptions.RuntimeException as e: - raise DbtProfileError( - 'Credentials in profile "{}", target "{}" invalid: {}' - .format(profile_name, target_name, str(e)) - ) - return credentials - - @staticmethod - def pick_profile_name(args_profile_name, project_profile_name=None): - profile_name = project_profile_name - if args_profile_name is not None: - profile_name = args_profile_name - if profile_name is None: - raise DbtProjectError(NO_SUPPLIED_PROFILE_ERROR) - return profile_name - - @staticmethod - def _get_profile_data(profile, profile_name, target_name): - if 'outputs' not in profile: - raise DbtProfileError( - "outputs not specified in profile '{}'".format(profile_name) - ) - outputs = profile['outputs'] - - if target_name not in outputs: - outputs = '\n'.join(' - {}'.format(output) - for output in outputs) - msg = ("The profile '{}' does not have a target named '{}'. The " - "valid target names for this profile are:\n{}" - .format(profile_name, target_name, outputs)) - raise DbtProfileError(msg, result_type='invalid_target') - profile_data = outputs[target_name] - return profile_data - - @classmethod - def from_credentials(cls, credentials, threads, profile_name, target_name, - user_cfg=None): - """Create a profile from an existing set of Credentials and the - remaining information. - - :param credentials dict: The credentials dict for this profile. - :param threads int: The number of threads to use for connections. - :param profile_name str: The profile name used for this profile. - :param target_name str: The target name used for this profile. - :param user_cfg Optional[dict]: The user-level config block from the - raw profiles, if specified. - :raises DbtProfileError: If the profile is invalid. - :returns Profile: The new Profile object. - """ - config = UserConfig.from_dict(user_cfg) - profile = cls( - profile_name=profile_name, - target_name=target_name, - config=config, - threads=threads, - credentials=credentials - ) - profile.validate() - return profile - - @classmethod - def render_profile(cls, raw_profile, profile_name, target_override, - cli_vars): - """This is a containment zone for the hateful way we're rendering - profiles. - """ - renderer = ConfigRenderer(cli_vars=cli_vars) - - # rendering profiles is a bit complex. Two constraints cause trouble: - # 1) users should be able to use environment/cli variables to specify - # the target in their profile. - # 2) Missing environment/cli variables in profiles/targets that don't - # end up getting selected should not cause errors. - # so first we'll just render the target name, then we use that rendered - # name to extract a profile that we can render. - if target_override is not None: - target_name = target_override - elif 'target' in raw_profile: - # render the target if it was parsed from yaml - target_name = renderer.render_value(raw_profile['target']) - else: - target_name = 'default' - logger.debug( - "target not specified in profile '{}', using '{}'" - .format(profile_name, target_name) - ) - - raw_profile_data = cls._get_profile_data( - raw_profile, profile_name, target_name - ) - - profile_data = renderer.render_profile_data(raw_profile_data) - return target_name, profile_data - - @classmethod - def from_raw_profile_info(cls, raw_profile, profile_name, cli_vars, - user_cfg=None, target_override=None, - threads_override=None): - """Create a profile from its raw profile information. - - (this is an intermediate step, mostly useful for unit testing) - - :param raw_profile dict: The profile data for a single profile, from - disk as yaml and its values rendered with jinja. - :param profile_name str: The profile name used. - :param cli_vars dict: The command-line variables passed as arguments, - as a dict. - :param user_cfg Optional[dict]: The global config for the user, if it - was present. - :param target_override Optional[str]: The target to use, if provided on - the command line. - :param threads_override Optional[str]: The thread count to use, if - provided on the command line. - :raises DbtProfileError: If the profile is invalid or missing, or the - target could not be found - :returns Profile: The new Profile object. - """ - # user_cfg is not rendered since it only contains booleans. - # TODO: should it be, and the values coerced to bool? - target_name, profile_data = cls.render_profile( - raw_profile, profile_name, target_override, cli_vars - ) - - # valid connections never include the number of threads, but it's - # stored on a per-connection level in the raw configs - threads = profile_data.pop('threads', DEFAULT_THREADS) - if threads_override is not None: - threads = threads_override - - credentials = cls._credentials_from_profile( - profile_data, profile_name, target_name - ) - - return cls.from_credentials( - credentials=credentials, - profile_name=profile_name, - target_name=target_name, - threads=threads, - user_cfg=user_cfg - ) - - @classmethod - def from_raw_profiles(cls, raw_profiles, profile_name, cli_vars, - target_override=None, threads_override=None): - """ - :param raw_profiles dict: The profile data, from disk as yaml. - :param profile_name str: The profile name to use. - :param cli_vars dict: The command-line variables passed as arguments, - as a dict. - :param target_override Optional[str]: The target to use, if provided on - the command line. - :param threads_override Optional[str]: The thread count to use, if - provided on the command line. - :raises DbtProjectError: If there is no profile name specified in the - project or the command line arguments - :raises DbtProfileError: If the profile is invalid or missing, or the - target could not be found - :returns Profile: The new Profile object. - """ - if profile_name not in raw_profiles: - raise DbtProjectError( - "Could not find profile named '{}'".format(profile_name) - ) - - # First, we've already got our final decision on profile name, and we - # don't render keys, so we can pluck that out - raw_profile = raw_profiles[profile_name] - - user_cfg = raw_profiles.get('config') - - return cls.from_raw_profile_info( - raw_profile=raw_profile, - profile_name=profile_name, - cli_vars=cli_vars, - user_cfg=user_cfg, - target_override=target_override, - threads_override=threads_override, - ) - - @classmethod - def from_args(cls, args, project_profile_name=None, cli_vars=None): - """Given the raw profiles as read from disk and the name of the desired - profile if specified, return the profile component of the runtime - config. - - :param args argparse.Namespace: The arguments as parsed from the cli. - :param cli_vars dict: The command-line variables passed as arguments, - as a dict. - :param project_profile_name Optional[str]: The profile name, if - specified in a project. - :raises DbtProjectError: If there is no profile name specified in the - project or the command line arguments, or if the specified profile - is not found - :raises DbtProfileError: If the profile is invalid or missing, or the - target could not be found. - :returns Profile: The new Profile object. - """ - if cli_vars is None: - cli_vars = dbt.utils.parse_cli_vars(getattr(args, 'vars', '{}')) - - threads_override = getattr(args, 'threads', None) - target_override = getattr(args, 'target', None) - raw_profiles = read_profile(args.profiles_dir) - profile_name = cls.pick_profile_name(args.profile, - project_profile_name) - - return cls.from_raw_profiles( - raw_profiles=raw_profiles, - profile_name=profile_name, - cli_vars=cli_vars, - target_override=target_override, - threads_override=threads_override - ) - - -def package_config_from_data(packages_data): - if packages_data is None: - packages_data = {'packages': []} - - try: - packages = PackageConfig(**packages_data) - except dbt.exceptions.ValidationException as e: - raise DbtProjectError('Invalid package config: {}'.format(str(e))) - return packages - - -def package_data_from_root(project_root): - package_filepath = dbt.clients.system.resolve_path_from_base( - 'packages.yml', project_root - ) - - if dbt.clients.system.path_exists(package_filepath): - packages_dict = _load_yaml(package_filepath) - else: - packages_dict = None - return packages_dict - - -def package_config_from_root(project_root): - packages_dict = package_data_from_root(project_root) - return package_config_from_data(packages_dict) - - -class RuntimeConfig(Project, Profile): - """The runtime configuration, as constructed from its components. There's a - lot because there is a lot of stuff! - """ - def __init__(self, project_name, version, project_root, source_paths, - macro_paths, data_paths, test_paths, analysis_paths, - docs_paths, target_path, clean_targets, log_path, - modules_path, quoting, models, on_run_start, on_run_end, - archive, seeds, dbt_version, profile_name, target_name, - config, threads, credentials, packages, args): - # 'vars' - self.args = args - self.cli_vars = dbt.utils.parse_cli_vars(getattr(args, 'vars', '{}')) - # 'project' - Project.__init__( - self, - project_name=project_name, - version=version, - project_root=project_root, - profile_name=profile_name, - source_paths=source_paths, - macro_paths=macro_paths, - data_paths=data_paths, - test_paths=test_paths, - analysis_paths=analysis_paths, - docs_paths=docs_paths, - target_path=target_path, - clean_targets=clean_targets, - log_path=log_path, - modules_path=modules_path, - quoting=quoting, - models=models, - on_run_start=on_run_start, - on_run_end=on_run_end, - archive=archive, - seeds=seeds, - dbt_version=dbt_version, - packages=packages - ) - # 'profile' - Profile.__init__( - self, - profile_name=profile_name, - target_name=target_name, - config=config, - threads=threads, - credentials=credentials - ) - self.validate() - - @classmethod - def from_parts(cls, project, profile, args): - """Instantiate a RuntimeConfig from its components. - - :param profile Profile: A parsed dbt Profile. - :param project Project: A parsed dbt Project. - :param args argparse.Namespace: The parsed command-line arguments. - :returns RuntimeConfig: The new configuration. - """ - quoting = deepcopy( - get_relation_class_by_name(profile.credentials.type) - .DEFAULTS['quote_policy'] - ) - quoting.update(project.quoting) - return cls( - project_name=project.project_name, - version=project.version, - project_root=project.project_root, - source_paths=project.source_paths, - macro_paths=project.macro_paths, - data_paths=project.data_paths, - test_paths=project.test_paths, - analysis_paths=project.analysis_paths, - docs_paths=project.docs_paths, - target_path=project.target_path, - clean_targets=project.clean_targets, - log_path=project.log_path, - modules_path=project.modules_path, - quoting=quoting, - models=project.models, - on_run_start=project.on_run_start, - on_run_end=project.on_run_end, - archive=project.archive, - seeds=project.seeds, - dbt_version=project.dbt_version, - packages=project.packages, - profile_name=profile.profile_name, - target_name=profile.target_name, - config=profile.config, - threads=profile.threads, - credentials=profile.credentials, - args=args - ) - - def new_project(self, project_root): - """Given a new project root, read in its project dictionary, supply the - existing project's profile info, and create a new project file. - - :param project_root str: A filepath to a dbt project. - :raises DbtProfileError: If the profile is invalid. - :raises DbtProjectError: If project is missing or invalid. - :returns RuntimeConfig: The new configuration. - """ - # copy profile - profile = Profile(**self.to_profile_info()) - profile.validate() - # load the new project and its packages. Don't pass cli variables. - project = Project.from_project_root(project_root, {}) - - cfg = self.from_parts( - project=project, - profile=profile, - args=deepcopy(self.args), - ) - # force our quoting back onto the new project. - cfg.quoting = deepcopy(self.quoting) - return cfg - - def serialize(self): - """Serialize the full configuration to a single dictionary. For any - instance that has passed validate() (which happens in __init__), it - matches the Configuration contract. - - Note that args are not serialized. - - :returns dict: The serialized configuration. - """ - result = self.to_project_config(with_packages=True) - result.update(self.to_profile_info(serialize_credentials=True)) - result['cli_vars'] = deepcopy(self.cli_vars) - return result - - def __str__(self): - return pprint.pformat(self.serialize()) - - def validate(self): - """Validate the configuration against its contract. - - :raises DbtProjectError: If the configuration fails validation. - """ - try: - Configuration(**self.serialize()) - except dbt.exceptions.ValidationException as e: - raise DbtProjectError(str(e)) - - if getattr(self.args, 'version_check', False): - self.validate_version() - - @classmethod - def from_args(cls, args): - """Given arguments, read in dbt_project.yml from the current directory, - read in packages.yml if it exists, and use them to find the profile to - load. - - :param args argparse.Namespace: The arguments as parsed from the cli. - :raises DbtProjectError: If the project is invalid or missing. - :raises DbtProfileError: If the profile is invalid or missing. - :raises ValidationException: If the cli variables are invalid. - """ - cli_vars = dbt.utils.parse_cli_vars(getattr(args, 'vars', '{}')) - - # build the project and read in packages.yml - project = Project.from_current_directory(cli_vars) - - # build the profile - profile = Profile.from_args( - args=args, - project_profile_name=project.profile_name, - cli_vars=cli_vars - ) - - return cls.from_parts( - project=project, - profile=profile, - args=args - ) - - -def _load_yaml(path): - contents = dbt.clients.system.load_file_contents(path) - return dbt.clients.yaml_helper.load_yaml_text(contents) - - -def _get_config_paths(config, path=(), paths=None): - if paths is None: - paths = set() - - for key, value in config.items(): - if isinstance(value, dict): - if key in DBTConfigKeys: - if path not in paths: - paths.add(path) - else: - _get_config_paths(value, path + (key,), paths) - else: - if path not in paths: - paths.add(path) - - return frozenset(paths) - - -def _is_config_used(path, fqns): - if fqns: - for fqn in fqns: - if len(path) <= len(fqn) and fqn[:len(path)] == path: - return True - return False diff --git a/dbt/include/__init__.py b/dbt/include/__init__.py deleted file mode 100644 index 4a940f8a518..00000000000 --- a/dbt/include/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ - -import os - -GLOBAL_DBT_MODULES_PATH = os.path.dirname(__file__) -GLOBAL_PROJECT_NAME = 'dbt' - -DOCS_INDEX_FILE_PATH = os.path.normpath( - os.path.join(GLOBAL_DBT_MODULES_PATH, "index.html")) diff --git a/dbt/task/__init__.py b/dbt/task/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/dbt/templates.py b/dbt/templates.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/dbt/ui/__init__.py b/dbt/ui/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/plugins/bigquery/dbt/__init__.py b/plugins/bigquery/dbt/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/bigquery/dbt/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/plugins/bigquery/dbt/adapters/__init__.py b/plugins/bigquery/dbt/adapters/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/bigquery/dbt/adapters/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/dbt/adapters/bigquery/__init__.py b/plugins/bigquery/dbt/adapters/bigquery/__init__.py similarity index 54% rename from dbt/adapters/bigquery/__init__.py rename to plugins/bigquery/dbt/adapters/bigquery/__init__.py index c9822036cf6..5707c1690e9 100644 --- a/dbt/adapters/bigquery/__init__.py +++ b/plugins/bigquery/dbt/adapters/bigquery/__init__.py @@ -3,5 +3,10 @@ from dbt.adapters.bigquery.relation import BigQueryRelation from dbt.adapters.bigquery.impl import BigQueryAdapter -Adapter = BigQueryAdapter -Credentials = BigQueryCredentials +from dbt.adapters.base import AdapterPlugin +from dbt.include import bigquery + +Plugin = AdapterPlugin( + adapter=BigQueryAdapter, + credentials=BigQueryCredentials, + include_path=bigquery.PACKAGE_PATH) diff --git a/dbt/adapters/bigquery/connections.py b/plugins/bigquery/dbt/adapters/bigquery/connections.py similarity index 100% rename from dbt/adapters/bigquery/connections.py rename to plugins/bigquery/dbt/adapters/bigquery/connections.py diff --git a/dbt/adapters/bigquery/impl.py b/plugins/bigquery/dbt/adapters/bigquery/impl.py similarity index 100% rename from dbt/adapters/bigquery/impl.py rename to plugins/bigquery/dbt/adapters/bigquery/impl.py diff --git a/dbt/adapters/bigquery/relation.py b/plugins/bigquery/dbt/adapters/bigquery/relation.py similarity index 100% rename from dbt/adapters/bigquery/relation.py rename to plugins/bigquery/dbt/adapters/bigquery/relation.py diff --git a/plugins/bigquery/dbt/include/__init__.py b/plugins/bigquery/dbt/include/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/bigquery/dbt/include/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/plugins/bigquery/dbt/include/bigquery/__init__.py b/plugins/bigquery/dbt/include/bigquery/__init__.py new file mode 100644 index 00000000000..87098354afd --- /dev/null +++ b/plugins/bigquery/dbt/include/bigquery/__init__.py @@ -0,0 +1,2 @@ +import os +PACKAGE_PATH = os.path.dirname(os.path.dirname(__file__)) diff --git a/plugins/bigquery/dbt/include/bigquery/dbt_project.yml b/plugins/bigquery/dbt/include/bigquery/dbt_project.yml new file mode 100644 index 00000000000..edae5386994 --- /dev/null +++ b/plugins/bigquery/dbt/include/bigquery/dbt_project.yml @@ -0,0 +1,5 @@ + +name: dbt_bigquery +version: 1.0 + +macro-paths: ["macros"] diff --git a/dbt/include/global_project/macros/adapters/bigquery.sql b/plugins/bigquery/dbt/include/bigquery/macros/adapters.sql similarity index 100% rename from dbt/include/global_project/macros/adapters/bigquery.sql rename to plugins/bigquery/dbt/include/bigquery/macros/adapters.sql diff --git a/dbt/include/global_project/macros/etc/bigquery.sql b/plugins/bigquery/dbt/include/bigquery/macros/etc.sql similarity index 100% rename from dbt/include/global_project/macros/etc/bigquery.sql rename to plugins/bigquery/dbt/include/bigquery/macros/etc.sql diff --git a/plugins/bigquery/dbt/include/bigquery/macros/materializations/archive.sql b/plugins/bigquery/dbt/include/bigquery/macros/materializations/archive.sql new file mode 100644 index 00000000000..5548b71a7e8 --- /dev/null +++ b/plugins/bigquery/dbt/include/bigquery/macros/materializations/archive.sql @@ -0,0 +1,23 @@ +{% macro bigquery__create_temporary_table(sql, relation) %} + {% set tmp_relation = adapter.create_temporary_table(sql) %} + {{ return(tmp_relation) }} +{% endmacro %} + + +{% macro bigquery__archive_scd_hash() %} + to_hex(md5(concat(cast(`dbt_pk` as string), '|', cast(`dbt_updated_at` as string)))) +{% endmacro %} + + +{% macro bigquery__create_columns(relation, columns) %} + {{ adapter.alter_table_add_columns(relation, columns) }} +{% endmacro %} + + +{% macro bigquery__archive_update(target_relation, tmp_relation) %} + update {{ target_relation }} as dest + set dest.{{ adapter.quote('valid_to') }} = tmp.{{ adapter.quote('valid_to') }} + from {{ tmp_relation }} as tmp + where tmp.{{ adapter.quote('scd_id') }} = dest.{{ adapter.quote('scd_id') }} + and {{ adapter.quote('change_type') }} = 'update'; +{% endmacro %} diff --git a/dbt/include/global_project/macros/materializations/incremental/bigquery_incremental.sql b/plugins/bigquery/dbt/include/bigquery/macros/materializations/incremental.sql similarity index 100% rename from dbt/include/global_project/macros/materializations/incremental/bigquery_incremental.sql rename to plugins/bigquery/dbt/include/bigquery/macros/materializations/incremental.sql diff --git a/plugins/bigquery/dbt/include/bigquery/macros/materializations/merge.sql b/plugins/bigquery/dbt/include/bigquery/macros/materializations/merge.sql new file mode 100644 index 00000000000..8e8f42a3563 --- /dev/null +++ b/plugins/bigquery/dbt/include/bigquery/macros/materializations/merge.sql @@ -0,0 +1,3 @@ +{% macro bigquery__get_merge_sql(target, source, unique_key, dest_columns) %} + {{ common_get_merge_sql(target, source, unique_key, dest_columns) }} +{% endmacro %} diff --git a/dbt/include/global_project/macros/materializations/seed/bigquery.sql b/plugins/bigquery/dbt/include/bigquery/macros/materializations/seed.sql similarity index 100% rename from dbt/include/global_project/macros/materializations/seed/bigquery.sql rename to plugins/bigquery/dbt/include/bigquery/macros/materializations/seed.sql diff --git a/dbt/include/global_project/macros/materializations/table/bigquery_table.sql b/plugins/bigquery/dbt/include/bigquery/macros/materializations/table.sql similarity index 100% rename from dbt/include/global_project/macros/materializations/table/bigquery_table.sql rename to plugins/bigquery/dbt/include/bigquery/macros/materializations/table.sql diff --git a/plugins/bigquery/dbt/include/bigquery/macros/materializations/view.sql b/plugins/bigquery/dbt/include/bigquery/macros/materializations/view.sql new file mode 100644 index 00000000000..561c38bb5c3 --- /dev/null +++ b/plugins/bigquery/dbt/include/bigquery/macros/materializations/view.sql @@ -0,0 +1,13 @@ + +{% macro bigquery__handle_existing_table(full_refresh, non_destructive_mode, old_relation) %} + {%- if full_refresh and not non_destructive_mode -%} + {{ adapter.drop_relation(old_relation) }} + {%- else -%} + {{ exceptions.relation_wrong_type(old_relation, 'view') }} + {%- endif -%} +{% endmacro %} + + +{% materialization view, adapter='bigquery' -%} + {{ create_or_replace_view(run_outside_transaction_hooks=False) }} +{%- endmaterialization %} diff --git a/plugins/bigquery/setup.py b/plugins/bigquery/setup.py new file mode 100644 index 00000000000..5c743575d46 --- /dev/null +++ b/plugins/bigquery/setup.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +from setuptools import find_packages +from distutils.core import setup + +package_name = "dbt-bigquery" +package_version = "0.13.0a1" +description = """The bigquery adapter plugin for dbt (data build tool)""" + + +setup( + name=package_name, + version=package_version, + description=description, + long_description_content_type=description, + author="Fishtown Analytics", + author_email="info@fishtownanalytics.com", + url="https://github.com/fishtown-analytics/dbt", + packages=find_packages(), + package_data={ + 'dbt': [ + 'include/bigquery/macros/*.sql', + 'include/bigquery/macros/**/*.sql', + ] + }, + install_requires=[ + 'dbt-core=={}'.format(package_version), + 'google-cloud-bigquery>=1.0.0,<2', + ] +) diff --git a/plugins/postgres/dbt/__init__.py b/plugins/postgres/dbt/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/postgres/dbt/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/plugins/postgres/dbt/adapters/__init__.py b/plugins/postgres/dbt/adapters/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/postgres/dbt/adapters/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/plugins/postgres/dbt/adapters/postgres/__init__.py b/plugins/postgres/dbt/adapters/postgres/__init__.py new file mode 100644 index 00000000000..f139484e807 --- /dev/null +++ b/plugins/postgres/dbt/adapters/postgres/__init__.py @@ -0,0 +1,11 @@ +from dbt.adapters.postgres.connections import PostgresConnectionManager +from dbt.adapters.postgres.connections import PostgresCredentials +from dbt.adapters.postgres.impl import PostgresAdapter + +from dbt.adapters.base import AdapterPlugin +from dbt.include import postgres + +Plugin = AdapterPlugin( + adapter=PostgresAdapter, + credentials=PostgresCredentials, + include_path=postgres.PACKAGE_PATH) diff --git a/dbt/adapters/postgres/connections.py b/plugins/postgres/dbt/adapters/postgres/connections.py similarity index 100% rename from dbt/adapters/postgres/connections.py rename to plugins/postgres/dbt/adapters/postgres/connections.py diff --git a/dbt/adapters/postgres/impl.py b/plugins/postgres/dbt/adapters/postgres/impl.py similarity index 100% rename from dbt/adapters/postgres/impl.py rename to plugins/postgres/dbt/adapters/postgres/impl.py diff --git a/plugins/postgres/dbt/include/__init__.py b/plugins/postgres/dbt/include/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/postgres/dbt/include/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/plugins/postgres/dbt/include/postgres/__init__.py b/plugins/postgres/dbt/include/postgres/__init__.py new file mode 100644 index 00000000000..87098354afd --- /dev/null +++ b/plugins/postgres/dbt/include/postgres/__init__.py @@ -0,0 +1,2 @@ +import os +PACKAGE_PATH = os.path.dirname(os.path.dirname(__file__)) diff --git a/plugins/postgres/dbt/include/postgres/dbt_project.yml b/plugins/postgres/dbt/include/postgres/dbt_project.yml new file mode 100644 index 00000000000..266eba33db9 --- /dev/null +++ b/plugins/postgres/dbt/include/postgres/dbt_project.yml @@ -0,0 +1,5 @@ + +name: dbt_postgres +version: 1.0 + +macro-paths: ["macros"] diff --git a/dbt/include/global_project/macros/catalog/postgres_catalog.sql b/plugins/postgres/dbt/include/postgres/macros/catalog.sql similarity index 100% rename from dbt/include/global_project/macros/catalog/postgres_catalog.sql rename to plugins/postgres/dbt/include/postgres/macros/catalog.sql diff --git a/dbt/include/global_project/macros/relations/postgres_relations.sql b/plugins/postgres/dbt/include/postgres/macros/relations.sql similarity index 100% rename from dbt/include/global_project/macros/relations/postgres_relations.sql rename to plugins/postgres/dbt/include/postgres/macros/relations.sql diff --git a/plugins/postgres/setup.py b/plugins/postgres/setup.py new file mode 100644 index 00000000000..c91de0f680a --- /dev/null +++ b/plugins/postgres/setup.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +from setuptools import find_packages +from distutils.core import setup + +package_name = "dbt-postgres" +package_version = "0.13.0a1" +description = """The postgres adpter plugin for dbt (data build tool)""" + +setup( + name=package_name, + version=package_version, + description=description, + long_description_content_type=description, + author="Fishtown Analytics", + author_email="info@fishtownanalytics.com", + url="https://github.com/fishtown-analytics/dbt", + packages=find_packages(), + package_data={ + 'dbt': [ + 'include/postgres/macros/*.sql', + ] + }, + install_requires=[ + 'dbt-core=={}'.format(package_version), + 'psycopg2>=2.7.5,<2.8', + ] +) diff --git a/plugins/redshift/dbt/__init__.py b/plugins/redshift/dbt/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/redshift/dbt/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/plugins/redshift/dbt/adapters/__init__.py b/plugins/redshift/dbt/adapters/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/redshift/dbt/adapters/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/plugins/redshift/dbt/adapters/redshift/__init__.py b/plugins/redshift/dbt/adapters/redshift/__init__.py new file mode 100644 index 00000000000..336eb55d17d --- /dev/null +++ b/plugins/redshift/dbt/adapters/redshift/__init__.py @@ -0,0 +1,13 @@ +from dbt.adapters.redshift.connections import RedshiftConnectionManager +from dbt.adapters.redshift.connections import RedshiftCredentials +from dbt.adapters.redshift.impl import RedshiftAdapter + + +from dbt.adapters.base import AdapterPlugin +from dbt.include import redshift + +Plugin = AdapterPlugin( + adapter=RedshiftAdapter, + credentials=RedshiftCredentials, + include_path=redshift.PACKAGE_PATH, + dependencies=['postgres']) diff --git a/dbt/adapters/redshift/connections.py b/plugins/redshift/dbt/adapters/redshift/connections.py similarity index 100% rename from dbt/adapters/redshift/connections.py rename to plugins/redshift/dbt/adapters/redshift/connections.py diff --git a/dbt/adapters/redshift/impl.py b/plugins/redshift/dbt/adapters/redshift/impl.py similarity index 100% rename from dbt/adapters/redshift/impl.py rename to plugins/redshift/dbt/adapters/redshift/impl.py diff --git a/plugins/redshift/dbt/include/__init__.py b/plugins/redshift/dbt/include/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/redshift/dbt/include/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/plugins/redshift/dbt/include/redshift/__init__.py b/plugins/redshift/dbt/include/redshift/__init__.py new file mode 100644 index 00000000000..ef10d7896ca --- /dev/null +++ b/plugins/redshift/dbt/include/redshift/__init__.py @@ -0,0 +1,3 @@ +import os +from dbt.include.postgres import PACKAGE_PATH as POSTGRES_PACKAGE_PATH +PACKAGE_PATH = os.path.dirname(os.path.dirname(__file__)) diff --git a/plugins/redshift/dbt/include/redshift/dbt_project.yml b/plugins/redshift/dbt/include/redshift/dbt_project.yml new file mode 100644 index 00000000000..edcd805ab7a --- /dev/null +++ b/plugins/redshift/dbt/include/redshift/dbt_project.yml @@ -0,0 +1,5 @@ + +name: dbt_redshift +version: 1.0 + +macro-paths: ["macros"] diff --git a/dbt/include/global_project/macros/adapters/redshift.sql b/plugins/redshift/dbt/include/redshift/macros/adapters.sql similarity index 100% rename from dbt/include/global_project/macros/adapters/redshift.sql rename to plugins/redshift/dbt/include/redshift/macros/adapters.sql diff --git a/dbt/include/global_project/macros/catalog/redshift_catalog.sql b/plugins/redshift/dbt/include/redshift/macros/catalog.sql similarity index 100% rename from dbt/include/global_project/macros/catalog/redshift_catalog.sql rename to plugins/redshift/dbt/include/redshift/macros/catalog.sql diff --git a/dbt/include/global_project/macros/relations/redshift_relations.sql b/plugins/redshift/dbt/include/redshift/macros/relations.sql similarity index 100% rename from dbt/include/global_project/macros/relations/redshift_relations.sql rename to plugins/redshift/dbt/include/redshift/macros/relations.sql diff --git a/plugins/redshift/setup.py b/plugins/redshift/setup.py new file mode 100644 index 00000000000..a62328edf0d --- /dev/null +++ b/plugins/redshift/setup.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +from setuptools import find_packages +from distutils.core import setup + +package_name = "dbt-redshift" +package_version = "0.13.0a1" +description = """The redshift adapter plugin for dbt (data build tool)""" + + +setup( + name=package_name, + version=package_version, + description=description, + long_description_content_type=description, + author="Fishtown Analytics", + author_email="info@fishtownanalytics.com", + url="https://github.com/fishtown-analytics/dbt", + packages=find_packages(), + package_data={ + 'dbt': [ + 'include/redshift/macros/*.sql', + ] + }, + install_requires=[ + 'dbt-core=={}'.format(package_version), + 'dbt-postgres=={}'.format(package_version), + 'boto3>=1.6.23,<1.8.0', + 'botocore>=1.9.23,<1.11.0', + 'psycopg2>=2.7.5,<2.8', + ] +) diff --git a/plugins/snowflake/dbt/__init__.py b/plugins/snowflake/dbt/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/snowflake/dbt/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/plugins/snowflake/dbt/adapters/__init__.py b/plugins/snowflake/dbt/adapters/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/snowflake/dbt/adapters/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/dbt/adapters/snowflake/__init__.py b/plugins/snowflake/dbt/adapters/snowflake/__init__.py similarity index 55% rename from dbt/adapters/snowflake/__init__.py rename to plugins/snowflake/dbt/adapters/snowflake/__init__.py index c2b982df79a..1ac7dcbdf2f 100644 --- a/dbt/adapters/snowflake/__init__.py +++ b/plugins/snowflake/dbt/adapters/snowflake/__init__.py @@ -3,6 +3,10 @@ from dbt.adapters.snowflake.relation import SnowflakeRelation from dbt.adapters.snowflake.impl import SnowflakeAdapter +from dbt.adapters.base import AdapterPlugin +from dbt.include import snowflake -Adapter = SnowflakeAdapter -Credentials = SnowflakeCredentials +Plugin = AdapterPlugin( + adapter=SnowflakeAdapter, + credentials=SnowflakeCredentials, + include_path=snowflake.PACKAGE_PATH) diff --git a/dbt/adapters/snowflake/connections.py b/plugins/snowflake/dbt/adapters/snowflake/connections.py similarity index 100% rename from dbt/adapters/snowflake/connections.py rename to plugins/snowflake/dbt/adapters/snowflake/connections.py diff --git a/dbt/adapters/snowflake/impl.py b/plugins/snowflake/dbt/adapters/snowflake/impl.py similarity index 100% rename from dbt/adapters/snowflake/impl.py rename to plugins/snowflake/dbt/adapters/snowflake/impl.py diff --git a/dbt/adapters/snowflake/relation.py b/plugins/snowflake/dbt/adapters/snowflake/relation.py similarity index 100% rename from dbt/adapters/snowflake/relation.py rename to plugins/snowflake/dbt/adapters/snowflake/relation.py diff --git a/plugins/snowflake/dbt/include/__init__.py b/plugins/snowflake/dbt/include/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/plugins/snowflake/dbt/include/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/plugins/snowflake/dbt/include/snowflake/__init__.py b/plugins/snowflake/dbt/include/snowflake/__init__.py new file mode 100644 index 00000000000..87098354afd --- /dev/null +++ b/plugins/snowflake/dbt/include/snowflake/__init__.py @@ -0,0 +1,2 @@ +import os +PACKAGE_PATH = os.path.dirname(os.path.dirname(__file__)) diff --git a/plugins/snowflake/dbt/include/snowflake/dbt_project.yml b/plugins/snowflake/dbt/include/snowflake/dbt_project.yml new file mode 100644 index 00000000000..587a22b5232 --- /dev/null +++ b/plugins/snowflake/dbt/include/snowflake/dbt_project.yml @@ -0,0 +1,5 @@ + +name: dbt_snowflake +version: 1.0 + +macro-paths: ["macros"] diff --git a/dbt/include/global_project/macros/adapters/snowflake.sql b/plugins/snowflake/dbt/include/snowflake/macros/adapters.sql similarity index 100% rename from dbt/include/global_project/macros/adapters/snowflake.sql rename to plugins/snowflake/dbt/include/snowflake/macros/adapters.sql diff --git a/dbt/include/global_project/macros/catalog/snowflake_catalog.sql b/plugins/snowflake/dbt/include/snowflake/macros/catalog.sql similarity index 100% rename from dbt/include/global_project/macros/catalog/snowflake_catalog.sql rename to plugins/snowflake/dbt/include/snowflake/macros/catalog.sql diff --git a/plugins/snowflake/dbt/include/snowflake/macros/materializations/merge.sql b/plugins/snowflake/dbt/include/snowflake/macros/materializations/merge.sql new file mode 100644 index 00000000000..ac92f2ef26e --- /dev/null +++ b/plugins/snowflake/dbt/include/snowflake/macros/materializations/merge.sql @@ -0,0 +1,3 @@ +{% macro snowflake__get_merge_sql(target, source, unique_key, dest_columns) %} + {{ common_get_merge_sql(target, source, unique_key, dest_columns) }} +{% endmacro %} diff --git a/dbt/include/global_project/macros/materializations/table/snowflake_table.sql b/plugins/snowflake/dbt/include/snowflake/macros/materializations/table.sql similarity index 100% rename from dbt/include/global_project/macros/materializations/table/snowflake_table.sql rename to plugins/snowflake/dbt/include/snowflake/macros/materializations/table.sql diff --git a/plugins/snowflake/dbt/include/snowflake/macros/materializations/view.sql b/plugins/snowflake/dbt/include/snowflake/macros/materializations/view.sql new file mode 100644 index 00000000000..d1dde6e052a --- /dev/null +++ b/plugins/snowflake/dbt/include/snowflake/macros/materializations/view.sql @@ -0,0 +1,3 @@ +{% materialization view, adapter='snowflake' -%} + {{ create_or_replace_view() }} +{%- endmaterialization %} diff --git a/plugins/snowflake/setup.py b/plugins/snowflake/setup.py new file mode 100644 index 00000000000..24be4e60ee1 --- /dev/null +++ b/plugins/snowflake/setup.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +from setuptools import find_packages +from distutils.core import setup + +package_name = "dbt-snowflake" +package_version = "0.13.0a1" +description = """The snowflake adapter plugin for dbt (data build tool)""" + + +setup( + name=package_name, + version=package_version, + description=description, + long_description_content_type=description, + author="Fishtown Analytics", + author_email="info@fishtownanalytics.com", + url="https://github.com/fishtown-analytics/dbt", + packages=find_packages(), + package_data={ + 'dbt': [ + 'include/snowflake/macros/*.sql', + 'include/snowflake/macros/**/*.sql', + ] + }, + install_requires=[ + 'dbt-core=={}'.format(package_version), + 'snowflake-connector-python>=1.4.9', + 'boto3>=1.6.23,<1.8.0', + 'botocore>=1.9.23,<1.11.0', + ] +) diff --git a/requirements.txt b/requirements.txt index c84b33a5a62..1c185b37d46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,5 @@ --e .[postgres,snowflake,redshift,bigquery] +-e ./core +-e ./plugins/postgres +-e ./plugins/redshift +-e ./plugins/snowflake +-e ./plugins/bigquery diff --git a/test/integration/006_simple_dependency_test/test_local_dependency.py b/test/integration/006_simple_dependency_test/test_local_dependency.py index d09aa9de848..17c4c50e16f 100644 --- a/test/integration/006_simple_dependency_test/test_local_dependency.py +++ b/test/integration/006_simple_dependency_test/test_local_dependency.py @@ -63,7 +63,7 @@ def configured_schema(self): return 'configured_{}_macro'.format(self.unique_schema()) @attr(type='postgres') - @mock.patch('dbt.config.get_installed_version') + @mock.patch('dbt.config.project.get_installed_version') def test_postgres_local_dependency_out_of_date(self, mock_get): mock_get.return_value = dbt.semver.VersionSpecifier.from_version_string('0.0.1') self.run_dbt(['deps']) @@ -72,7 +72,7 @@ def test_postgres_local_dependency_out_of_date(self, mock_get): self.assertIn('--no-version-check', str(e.exception)) @attr(type='postgres') - @mock.patch('dbt.config.get_installed_version') + @mock.patch('dbt.config.project.get_installed_version') def test_postgres_local_dependency_out_of_date_no_check(self, mock_get): mock_get.return_value = dbt.semver.VersionSpecifier.from_version_string('0.0.1') self.run_dbt(['deps']) diff --git a/test/integration/029_docs_generate_tests/test_docs_generate.py b/test/integration/029_docs_generate_tests/test_docs_generate.py index 3e85e3122ce..979cf12f7b3 100644 --- a/test/integration/029_docs_generate_tests/test_docs_generate.py +++ b/test/integration/029_docs_generate_tests/test_docs_generate.py @@ -30,6 +30,19 @@ def _read_file(path): return fp.read() +def _normalize(path): + """On windows, neither is enough on its own: + + >>> normcase('C:\\documents/ALL CAPS/subdir\\..') + 'c:\\documents\\all caps\\subdir\\..' + >>> normpath('C:\\documents/ALL CAPS/subdir\\..') + 'C:\\documents\\ALL CAPS' + >>> normpath(normcase('C:\\documents/ALL CAPS/subdir\\..')) + 'c:\\documents\\all caps' + """ + return os.path.normcase(os.path.normpath(path)) + + class TestDocsGenerate(DBTIntegrationTest): def setUp(self): super(TestDocsGenerate,self).setUp() @@ -41,8 +54,9 @@ def schema(self): @staticmethod def dir(path): - return os.path.normpath( - os.path.join('test/integration/029_docs_generate_tests', path)) + return _normalize( + os.path.join('test/integration/029_docs_generate_tests', path) + ) @property def models(self): @@ -77,8 +91,8 @@ def run_and_generate(self, extra=None, seed_count=1, model_count=1): self.assertEqual(len(self.run_dbt(["seed"])), seed_count) self.assertEqual(len(self.run_dbt()), model_count) - os.remove(os.path.normpath('target/manifest.json')) - os.remove(os.path.normpath('target/run_results.json')) + os.remove(_normalize('target/manifest.json')) + os.remove(_normalize('target/run_results.json')) self.generate_start_time = datetime.utcnow() self.run_dbt(['docs', 'generate']) @@ -717,15 +731,16 @@ def verify_manifest_macros(self, manifest): self.assertTrue(len(macro['raw_sql']) > 10) without_sql = {k: v for k, v in macro.items() if k != 'raw_sql'} # Windows means we can't hard-code this. - helpers_path = os.path.normpath('macros/materializations/helpers.sql') + helpers_path = _normalize('macros/materializations/helpers.sql') self.assertEqual( without_sql, { 'path': helpers_path, 'original_file_path': helpers_path, 'package_name': 'dbt', - 'root_path': os.path.join(os.getcwd(), 'dbt','include', - 'global_project'), + 'root_path': _normalize(os.path.join( + os.getcwd(), 'core', 'dbt','include', 'global_project' + )), 'name': 'column_list', 'unique_id': 'macro.dbt.column_list', 'tags': [], @@ -843,7 +858,7 @@ def expected_seeded_manifest(self): 'name': 'not_null_model_id', 'original_file_path': self.dir('models/schema.yml'), 'package_name': 'test', - 'path': os.path.normpath('schema_test/not_null_model_id.sql'), + 'path': _normalize('schema_test/not_null_model_id.sql'), 'raw_sql': "{{ test_not_null(model=ref('model'), column_name='id') }}", 'refs': [['model']], 'resource_type': 'test', @@ -872,7 +887,7 @@ def expected_seeded_manifest(self): 'name': 'nothing_model_', 'original_file_path': self.dir('models/schema.yml'), 'package_name': 'test', - 'path': os.path.normpath('schema_test/nothing_model_.sql'), + 'path': _normalize('schema_test/nothing_model_.sql'), 'raw_sql': "{{ test_nothing(model=ref('model'), ) }}", 'refs': [['model']], 'resource_type': 'test', @@ -902,7 +917,7 @@ def expected_seeded_manifest(self): 'name': 'unique_model_id', 'original_file_path': self.dir('models/schema.yml'), 'package_name': 'test', - 'path': os.path.normpath('schema_test/unique_model_id.sql'), + 'path': _normalize('schema_test/unique_model_id.sql'), 'raw_sql': "{{ test_unique(model=ref('model'), column_name='id') }}", 'refs': [['model']], 'resource_type': 'test', @@ -1633,7 +1648,7 @@ def expected_run_results(self, quote_schema=True, quote_model=False): 'fail': None, 'node': { 'alias': 'model', - 'build_path': os.path.normpath( + 'build_path': _normalize( 'target/compiled/test/model.sql' ), 'columns': { @@ -1689,7 +1704,7 @@ def expected_run_results(self, quote_schema=True, quote_model=False): 'fail': None, 'node': { 'alias': 'seed', - 'build_path': os.path.normpath( + 'build_path': _normalize( 'target/compiled/test/seed.csv' ), 'columns': {}, @@ -1734,7 +1749,7 @@ def expected_run_results(self, quote_schema=True, quote_model=False): 'fail': None, 'node': { 'alias': 'not_null_model_id', - 'build_path': os.path.normpath('target/compiled/test/schema_test/not_null_model_id.sql'), + 'build_path': _normalize('target/compiled/test/schema_test/not_null_model_id.sql'), 'column_name': 'id', 'columns': {}, 'compiled': True, @@ -1759,7 +1774,7 @@ def expected_run_results(self, quote_schema=True, quote_model=False): 'name': 'not_null_model_id', 'original_file_path': self.dir('models/schema.yml'), 'package_name': 'test', - 'path': os.path.normpath('schema_test/not_null_model_id.sql'), + 'path': _normalize('schema_test/not_null_model_id.sql'), 'raw_sql': "{{ test_not_null(model=ref('model'), column_name='id') }}", 'refs': [['model']], 'resource_type': 'test', @@ -1778,7 +1793,7 @@ def expected_run_results(self, quote_schema=True, quote_model=False): 'fail': None, 'node': { 'alias': 'nothing_model_', - 'build_path': os.path.normpath('target/compiled/test/schema_test/nothing_model_.sql'), + 'build_path': _normalize('target/compiled/test/schema_test/nothing_model_.sql'), 'columns': {}, 'compiled': True, 'compiled_sql': AnyStringWith('select 0'), @@ -1802,7 +1817,7 @@ def expected_run_results(self, quote_schema=True, quote_model=False): 'name': 'nothing_model_', 'original_file_path': self.dir('models/schema.yml'), 'package_name': 'test', - 'path': os.path.normpath('schema_test/nothing_model_.sql'), + 'path': _normalize('schema_test/nothing_model_.sql'), 'raw_sql': "{{ test_nothing(model=ref('model'), ) }}", 'refs': [['model']], 'resource_type': 'test', @@ -1821,7 +1836,7 @@ def expected_run_results(self, quote_schema=True, quote_model=False): 'fail': None, 'node': { 'alias': 'unique_model_id', - 'build_path': os.path.normpath('target/compiled/test/schema_test/unique_model_id.sql'), + 'build_path': _normalize('target/compiled/test/schema_test/unique_model_id.sql'), 'column_name': 'id', 'columns': {}, 'compiled': True, @@ -1846,7 +1861,7 @@ def expected_run_results(self, quote_schema=True, quote_model=False): 'name': 'unique_model_id', 'original_file_path': self.dir('models/schema.yml'), 'package_name': 'test', - 'path': os.path.normpath('schema_test/unique_model_id.sql'), + 'path': _normalize('schema_test/unique_model_id.sql'), 'raw_sql': "{{ test_unique(model=ref('model'), column_name='id') }}", 'refs': [['model']], 'resource_type': 'test', @@ -1890,7 +1905,7 @@ def expected_postgres_references_run_results(self): 'fail': None, 'node': { 'alias': 'ephemeral_summary', - 'build_path': os.path.normpath( + 'build_path': _normalize( 'target/compiled/test/ephemeral_summary.sql' ), 'columns': { @@ -1973,7 +1988,7 @@ def expected_postgres_references_run_results(self): 'fail': None, 'node': { 'alias': 'view_summary', - 'build_path': os.path.normpath( + 'build_path': _normalize( 'target/compiled/test/view_summary.sql' ), 'alias': 'view_summary', @@ -2055,7 +2070,7 @@ def expected_postgres_references_run_results(self): 'fail': None, 'node': { 'alias': 'seed', - 'build_path': os.path.normpath( + 'build_path': _normalize( 'target/compiled/test/seed.csv' ), 'columns': {}, diff --git a/test/unit/test_config.py b/test/unit/test_config.py index 7f924e02adf..92f268c33fa 100644 --- a/test/unit/test_config.py +++ b/test/unit/test_config.py @@ -475,7 +475,7 @@ def test_invalid_env_vars(self): self.env_override['env_value_port'] = 'hello' self.args.target = 'with-vars' with mock.patch.dict(os.environ, self.env_override): - with self.assertRaises(dbt.config.DbtProfileError) as exc: + with self.assertRaises(dbt.exceptions.DbtProfileError) as exc: self.from_args() self.assertIn("not of type 'integer'", str(exc.exception)) @@ -800,7 +800,7 @@ def test__get_unused_resource_config_paths_empty(self): ))}, []) self.assertEqual(len(unused), 0) - @mock.patch.object(dbt.config, 'logger') + @mock.patch.object(dbt.config.project, 'logger') def test__warn_for_unused_resource_config_paths_empty(self, mock_logger): project = dbt.config.Project.from_project_config( self.default_project_data @@ -884,7 +884,7 @@ def test__get_unused_resource_config_paths(self): self.assertEqual(len(unused), 1) self.assertEqual(unused[0], ('models', 'my_test_project', 'baz')) - @mock.patch.object(dbt.config, 'logger') + @mock.patch.object(dbt.config.project, 'logger') def test__warn_for_unused_resource_config_paths(self, mock_logger): project = dbt.config.Project.from_project_config( self.default_project_data @@ -892,7 +892,7 @@ def test__warn_for_unused_resource_config_paths(self, mock_logger): unused = project.warn_for_unused_resource_config_paths(self.used, []) mock_logger.info.assert_called_once() - @mock.patch.object(dbt.config, 'logger') + @mock.patch.object(dbt.config.project, 'logger') def test__warn_for_unused_resource_config_paths_disabled(self, mock_logger): project = dbt.config.Project.from_project_config( self.default_project_data diff --git a/test/unit/test_graph.py b/test/unit/test_graph.py index dc63b0e3b83..f989d2a167e 100644 --- a/test/unit/test_graph.py +++ b/test/unit/test_graph.py @@ -9,7 +9,6 @@ import dbt.flags import dbt.linker import dbt.config -import dbt.templates import dbt.utils try: diff --git a/tox.ini b/tox.ini index 366bf7a3e4a..4feae5dc355 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = unit-py27, unit-py36, integration-postgres-py27, integration-postgres- [testenv:pep8] basepython = python3.6 -commands = /bin/bash -c '$(which pep8) dbt/ --exclude dbt/templates.py' +commands = /bin/bash -c '$(which pep8) core/dbt plugins/*/dbt' deps = -r{toxinidir}/dev_requirements.txt @@ -12,14 +12,22 @@ deps = basepython = python2.7 commands = /bin/bash -c '$(which nosetests) -v {posargs} test/unit' deps = - -e .[postgres,snowflake,redshift,bigquery] + -e {toxinidir}/core + -e {toxinidir}/plugins/postgres + -e {toxinidir}/plugins/snowflake + -e {toxinidir}/plugins/bigquery + -e {toxinidir}/plugins/redshift -r{toxinidir}/dev_requirements.txt [testenv:unit-py36] basepython = python3.6 commands = /bin/bash -c '{envpython} $(which nosetests) -v {posargs} test/unit' deps = - -e .[postgres,snowflake,redshift,bigquery] + -e {toxinidir}/core + -e {toxinidir}/plugins/postgres + -e {toxinidir}/plugins/snowflake + -e {toxinidir}/plugins/bigquery + -e {toxinidir}/plugins/redshift -r{toxinidir}/dev_requirements.txt [testenv:integration-postgres-py27] @@ -29,7 +37,8 @@ setenv = HOME=/home/dbt_test_user commands = /bin/bash -c '{envpython} $(which nosetests) -v -a type=postgres {posargs} --with-coverage --cover-branches --cover-html --cover-html-dir=htmlcov test/integration/*' deps = - -e .[postgres] + -e {toxinidir}/core + -e {toxinidir}/plugins/postgres -r{toxinidir}/dev_requirements.txt [testenv:integration-snowflake-py27] @@ -39,7 +48,8 @@ setenv = HOME=/home/dbt_test_user commands = /bin/bash -c '{envpython} $(which nosetests) -v -a type=snowflake {posargs} --with-coverage --cover-branches --cover-html --cover-html-dir=htmlcov test/integration/*' deps = - -e .[snowflake] + -e {toxinidir}/core + -e {toxinidir}/plugins/snowflake -r{toxinidir}/dev_requirements.txt [testenv:integration-bigquery-py27] @@ -49,7 +59,8 @@ setenv = HOME=/home/dbt_test_user commands = /bin/bash -c '{envpython} $(which nosetests) -v -a type=bigquery {posargs} --with-coverage --cover-branches --cover-html --cover-html-dir=htmlcov test/integration/*' deps = - -e .[bigquery] + -e {toxinidir}/core + -e {toxinidir}/plugins/bigquery -r{toxinidir}/dev_requirements.txt [testenv:integration-redshift-py27] @@ -59,7 +70,9 @@ setenv = HOME=/home/dbt_test_user commands = /bin/bash -c '{envpython} $(which nosetests) -v -a type=redshift {posargs} --with-coverage --cover-branches --cover-html --cover-html-dir=htmlcov test/integration/*' deps = - -e .[redshift] + -e {toxinidir}/core + -e {toxinidir}/plugins/postgres + -e {toxinidir}/plugins/redshift -r{toxinidir}/dev_requirements.txt [testenv:integration-postgres-py36] @@ -69,7 +82,8 @@ setenv = HOME=/home/dbt_test_user commands = /bin/bash -c '{envpython} $(which nosetests) -v -a type=postgres --with-coverage --cover-branches --cover-html --cover-html-dir=htmlcov {posargs} test/integration/*' deps = - -e .[postgres] + -e {toxinidir}/core + -e {toxinidir}/plugins/postgres -r{toxinidir}/dev_requirements.txt [testenv:integration-snowflake-py36] @@ -79,7 +93,8 @@ setenv = HOME=/home/dbt_test_user commands = /bin/bash -c '{envpython} $(which nosetests) -v -a type=snowflake {posargs} --with-coverage --cover-branches --cover-html --cover-html-dir=htmlcov test/integration/*' deps = - -e .[snowflake] + -e {toxinidir}/core + -e {toxinidir}/plugins/snowflake -r{toxinidir}/dev_requirements.txt [testenv:integration-bigquery-py36] @@ -89,7 +104,8 @@ setenv = HOME=/home/dbt_test_user commands = /bin/bash -c '{envpython} $(which nosetests) -v -a type=bigquery {posargs} --with-coverage --cover-branches --cover-html --cover-html-dir=htmlcov test/integration/*' deps = - -e .[bigquery] + -e {toxinidir}/core + -e {toxinidir}/plugins/bigquery -r{toxinidir}/dev_requirements.txt [testenv:integration-redshift-py36] @@ -99,7 +115,9 @@ setenv = HOME=/home/dbt_test_user commands = /bin/bash -c '{envpython} $(which nosetests) -v -a type=redshift {posargs} --with-coverage --cover-branches --cover-html --cover-html-dir=htmlcov test/integration/*' deps = - -e .[redshift] + -e {toxinidir}/core + -e {toxinidir}/plugins/postgres + -e {toxinidir}/plugins/redshift -r{toxinidir}/dev_requirements.txt [testenv:explicit-py27] @@ -109,7 +127,11 @@ setenv = HOME=/home/dbt_test_user commands = /bin/bash -c '{envpython} $(which nosetests) -v {posargs}' deps = - -e .[postgres,snowflake,redshift,bigquery] + -e {toxinidir}/core + -e {toxinidir}/plugins/postgres + -e {toxinidir}/plugins/snowflake + -e {toxinidir}/plugins/bigquery + -e {toxinidir}/plugins/redshift -r{toxinidir}/dev_requirements.txt [testenv:explicit-py36] @@ -119,7 +141,11 @@ setenv = HOME=/home/dbt_test_user commands = /bin/bash -c '{envpython} $(which nosetests) -v {posargs}' deps = - -e .[postgres,snowflake,redshift,bigquery] + -e {toxinidir}/core + -e {toxinidir}/plugins/postgres + -e {toxinidir}/plugins/snowflake + -e {toxinidir}/plugins/bigquery + -e {toxinidir}/plugins/redshift -r{toxinidir}/dev_requirements.txt [testenv:pywin] @@ -130,5 +156,9 @@ setenv = DBT_INVOCATION_ENV = ci-appveyor commands = nosetests -v -a type=postgres -a type=snowflake -a type=bigquery --with-coverage --cover-branches --cover-html --cover-html-dir=htmlcov test/integration test/unit deps = - -e .[postgres,snowflake,redshift,bigquery] - -rdev_requirements.txt + -e {toxinidir}/core + -e {toxinidir}/plugins/postgres + -e {toxinidir}/plugins/snowflake + -e {toxinidir}/plugins/bigquery + -e {toxinidir}/plugins/redshift + -r{toxinidir}/dev_requirements.txt