dbt-ls 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbt_ls/__init__.py +201 -0
- dbt_ls/alias.py +12 -0
- dbt_ls/column.py +7 -0
- dbt_ls/model.py +94 -0
- dbt_ls/pattern.py +36 -0
- dbt_ls/profiles.py +74 -0
- dbt_ls/project.py +21 -0
- dbt_ls/source.py +71 -0
- dbt_ls-0.1.1.dist-info/METADATA +20 -0
- dbt_ls-0.1.1.dist-info/RECORD +12 -0
- dbt_ls-0.1.1.dist-info/WHEEL +4 -0
- dbt_ls-0.1.1.dist-info/entry_points.txt +2 -0
dbt_ls/__init__.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from pygls.lsp.server import LanguageServer
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from lsprotocol import types
|
|
6
|
+
from importlib.metadata import version
|
|
7
|
+
from dbt_ls.pattern import completion_context, ref_model_at
|
|
8
|
+
from dbt_ls.model import (
|
|
9
|
+
discover_models,
|
|
10
|
+
enrich_models_from_database,
|
|
11
|
+
)
|
|
12
|
+
from dbt_ls.source import discover_sources, enrich_sources_from_catalog
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from dbt_ls.alias import parse_aliases
|
|
15
|
+
from dbt_ls.project import Project
|
|
16
|
+
from dbt_ls.profiles import Profiles
|
|
17
|
+
import debugpy
|
|
18
|
+
import argparse
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
stream=sys.stderr,
|
|
22
|
+
level=os.environ.get("DBT_LS_LOG_LEVEL", "DEBUG").upper(),
|
|
23
|
+
force=True, # tear down pygls' root handler and use ours
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logging.getLogger("pygls").setLevel(logging.WARNING)
|
|
27
|
+
log = logging.getLogger("dbt_ls")
|
|
28
|
+
|
|
29
|
+
__version__ = version("dbt-ls")
|
|
30
|
+
|
|
31
|
+
server = LanguageServer("dbt-ls", __version__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def find_dbt_project_root(root: str) -> str:
|
|
35
|
+
for p in Path(root).rglob("dbt_project.yml"):
|
|
36
|
+
if "target" not in p.parts:
|
|
37
|
+
return str(p.parent)
|
|
38
|
+
return "."
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@server.feature(types.INITIALIZE)
|
|
42
|
+
def on_initialize(params: types.InitializeParams):
|
|
43
|
+
global models
|
|
44
|
+
global sources
|
|
45
|
+
global dbt_root
|
|
46
|
+
global project
|
|
47
|
+
|
|
48
|
+
if params.root_path:
|
|
49
|
+
dbt_root = find_dbt_project_root(params.root_path)
|
|
50
|
+
project = Project(dbt_root)
|
|
51
|
+
catalog_path = Path(f"{dbt_root}/target/catalog.json")
|
|
52
|
+
|
|
53
|
+
profile = Profiles.locate(project.root)
|
|
54
|
+
profile_target = profile.resolve(project.profile) if profile else None
|
|
55
|
+
|
|
56
|
+
models = discover_models(root=params.root_path, model_paths=project.model_paths)
|
|
57
|
+
log.debug("Finished parsing documented models")
|
|
58
|
+
sources = discover_sources(params.root_path)
|
|
59
|
+
log.debug("Finished parsing documented sources")
|
|
60
|
+
sources = enrich_sources_from_catalog(sources, catalog_path)
|
|
61
|
+
log.debug("Finished parsing column info for sources from catalog")
|
|
62
|
+
database_models = enrich_models_from_database(
|
|
63
|
+
models, profile_target, project.root
|
|
64
|
+
)
|
|
65
|
+
if database_models:
|
|
66
|
+
models = database_models
|
|
67
|
+
log.debug("Finished parsing column info for models from database")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@server.feature(
|
|
71
|
+
types.TEXT_DOCUMENT_COMPLETION,
|
|
72
|
+
types.CompletionOptions(trigger_characters=["'", '"', "(", "."]),
|
|
73
|
+
)
|
|
74
|
+
def completions(params: types.CompletionParams):
|
|
75
|
+
document = server.workspace.get_text_document(params.text_document.uri)
|
|
76
|
+
current_line = document.lines[params.position.line].strip()
|
|
77
|
+
pos = params.position
|
|
78
|
+
line = document.lines[pos.line] if pos.line < len(document.lines) else ""
|
|
79
|
+
line_prefix = line[: pos.character]
|
|
80
|
+
|
|
81
|
+
ctx = completion_context(line_prefix)
|
|
82
|
+
if ctx is None:
|
|
83
|
+
log.debug("no pattern matched for %r", current_line, " (early exit)")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
log.debug("completion @ %d:%d | line=%r", pos.line, pos.character, current_line)
|
|
87
|
+
|
|
88
|
+
kind, info = ctx
|
|
89
|
+
|
|
90
|
+
if kind == "ref":
|
|
91
|
+
log.info(
|
|
92
|
+
"REF path matched %r → serving %d models: %s",
|
|
93
|
+
current_line,
|
|
94
|
+
len(models),
|
|
95
|
+
[m.name for m in models[:15]],
|
|
96
|
+
)
|
|
97
|
+
return [
|
|
98
|
+
types.CompletionItem(
|
|
99
|
+
m.name,
|
|
100
|
+
kind=types.CompletionItemKind(18),
|
|
101
|
+
label_details=types.CompletionItemLabelDetails(
|
|
102
|
+
m.path.split(dbt_root)[-1]
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
for m in models
|
|
106
|
+
]
|
|
107
|
+
elif kind == "source_name":
|
|
108
|
+
[log.debug(c) for m in (*models, *sources) for c in m.columns]
|
|
109
|
+
log.info(
|
|
110
|
+
"SOURCE path matched %r → serving %d sources: %s",
|
|
111
|
+
current_line,
|
|
112
|
+
len(sources),
|
|
113
|
+
[s.name for s in sources[:15]],
|
|
114
|
+
)
|
|
115
|
+
return [
|
|
116
|
+
types.CompletionItem(
|
|
117
|
+
s.name,
|
|
118
|
+
kind=types.CompletionItemKind(10),
|
|
119
|
+
label_details=types.CompletionItemLabelDetails(s.database),
|
|
120
|
+
insert_text=f'{s.source_name}", "{s.name}',
|
|
121
|
+
insert_text_format=types.InsertTextFormat.PlainText,
|
|
122
|
+
)
|
|
123
|
+
for s in sources
|
|
124
|
+
]
|
|
125
|
+
elif kind == "column":
|
|
126
|
+
alias = info["alias"]
|
|
127
|
+
alias_map = parse_aliases(document.source)
|
|
128
|
+
model_name = alias_map.get(alias)
|
|
129
|
+
log.info("COLUMN path: alias=%r → model=%r", alias, model_name)
|
|
130
|
+
|
|
131
|
+
return [
|
|
132
|
+
types.CompletionItem(
|
|
133
|
+
label=c.name,
|
|
134
|
+
kind=types.CompletionItemKind(5),
|
|
135
|
+
label_details=types.CompletionItemLabelDetails(c.data_type),
|
|
136
|
+
)
|
|
137
|
+
for m in (*models, *sources)
|
|
138
|
+
for c in m.columns
|
|
139
|
+
if m.name == model_name
|
|
140
|
+
]
|
|
141
|
+
else:
|
|
142
|
+
log.debug("no pattern matched for %r", current_line)
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@server.feature(types.TEXT_DOCUMENT_DEFINITION)
|
|
147
|
+
def definition(params: types.DefinitionParams):
|
|
148
|
+
"""Jump from a ref('model') to that model's .sql file."""
|
|
149
|
+
document = server.workspace.get_text_document(params.text_document.uri)
|
|
150
|
+
pos = params.position
|
|
151
|
+
line = document.lines[pos.line] if pos.line < len(document.lines) else ""
|
|
152
|
+
|
|
153
|
+
model_name = ref_model_at(line, pos.character)
|
|
154
|
+
if model_name is None:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
target = next((m for m in models if m.name == model_name and m.path), None)
|
|
158
|
+
if target is None:
|
|
159
|
+
log.info("DEFINITION: no model file found for %r", model_name)
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
log.info("DEFINITION: %r → %s", model_name, target.path)
|
|
163
|
+
start = types.Position(line=0, character=0)
|
|
164
|
+
return types.Location(
|
|
165
|
+
uri=Path(target.path).as_uri(),
|
|
166
|
+
range=types.Range(start=start, end=start),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main():
|
|
171
|
+
banner = f"""
|
|
172
|
+
╔═══════════════════════════════════════╗
|
|
173
|
+
║ ║
|
|
174
|
+
║ _ _ _ _ ║
|
|
175
|
+
║ __| | |__ | |_ | |___ ║
|
|
176
|
+
║ / _` | '_ \\| __|____| / __| ║
|
|
177
|
+
║ | (_| | |_) | ||_____| \\__ \\ ║
|
|
178
|
+
║ \\__,_|_.__/ \\__| |_|___/ ║
|
|
179
|
+
║ ║
|
|
180
|
+
║ {__version__:^5} · Language Server · stdio ║
|
|
181
|
+
║ ║
|
|
182
|
+
╚═══════════════════════════════════════╝
|
|
183
|
+
"""
|
|
184
|
+
print(banner)
|
|
185
|
+
|
|
186
|
+
p = argparse.ArgumentParser()
|
|
187
|
+
p.add_argument("--tcp", action="store_true")
|
|
188
|
+
p.add_argument("--host", default="127.0.0.1")
|
|
189
|
+
p.add_argument("--port", type=int, default=8765)
|
|
190
|
+
args = p.parse_args()
|
|
191
|
+
if args.tcp:
|
|
192
|
+
debugpy.listen(("127.0.0.1", 5678))
|
|
193
|
+
debugpy.wait_for_client()
|
|
194
|
+
server.start_tcp(args.host, args.port)
|
|
195
|
+
else:
|
|
196
|
+
server.start_io()
|
|
197
|
+
logging.info("DBT Language Server started")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
main()
|
dbt_ls/alias.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_aliases(text: str) -> dict[str, str]:
|
|
5
|
+
aliases = {}
|
|
6
|
+
for match in re.finditer(r"""\{{\s*ref\((['"])(\w+)\1\)\s*}}\s+(\w+)""", text):
|
|
7
|
+
aliases[match.group(3)] = match.group(2)
|
|
8
|
+
for match in re.finditer(
|
|
9
|
+
r"""\{{\s*source\((['"])(\w+)\1,\s*(['"])(\w+)\3\)\s*}}\s+(\w+)""", text
|
|
10
|
+
):
|
|
11
|
+
aliases[match.group(5)] = match.group(4)
|
|
12
|
+
return aliases
|
dbt_ls/column.py
ADDED
dbt_ls/model.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from dbt_ls.column import Column
|
|
4
|
+
import json
|
|
5
|
+
from dbt_ls.profiles import ProfileTarget
|
|
6
|
+
import ibis
|
|
7
|
+
from ibis.expr.schema import Schema
|
|
8
|
+
from ibis.expr.types.relations import (
|
|
9
|
+
Table,
|
|
10
|
+
)
|
|
11
|
+
from dbt_ls.profiles import DuckDBTarget, DatabaseTarget
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Model:
|
|
16
|
+
name: str
|
|
17
|
+
path: str
|
|
18
|
+
columns: tuple[Column, ...] = ()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def discover_models(root: str, model_paths: list[str]) -> list[Model]:
|
|
22
|
+
return [
|
|
23
|
+
Model(name=p.stem, path=str(p))
|
|
24
|
+
for model_path in model_paths
|
|
25
|
+
for p in (Path(root) / model_path).rglob("*.sql")
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def enrich_models_from_catalog(models: list[Model], catalog_path: Path) -> list[Model]:
|
|
30
|
+
path = Path(catalog_path)
|
|
31
|
+
if not path.is_file():
|
|
32
|
+
return models
|
|
33
|
+
|
|
34
|
+
catalog = json.loads(path.read_text())
|
|
35
|
+
nodes = catalog.get("nodes", {})
|
|
36
|
+
|
|
37
|
+
# Build a lookup: model name -> columns
|
|
38
|
+
columns_by_name: dict[str, tuple[Column, ...]] = {}
|
|
39
|
+
for node in nodes.values():
|
|
40
|
+
if not node.get("unique_id", "").startswith("model."):
|
|
41
|
+
continue
|
|
42
|
+
name = node["metadata"]["name"]
|
|
43
|
+
columns_by_name[name] = tuple(
|
|
44
|
+
Column(name=c["name"], data_type=c.get("type"))
|
|
45
|
+
for c in node.get("columns", {}).values()
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return [
|
|
49
|
+
Model(name=m.name, path=m.path, columns=columns_by_name.get(m.name, m.columns))
|
|
50
|
+
for m in models
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_duckdb_models(
|
|
55
|
+
models: list[Model], profile_target: DuckDBTarget, project_root: str | Path
|
|
56
|
+
) -> list[Model] | None:
|
|
57
|
+
ibis.set_backend("duckdb")
|
|
58
|
+
|
|
59
|
+
connection_path = (
|
|
60
|
+
profile_target.path
|
|
61
|
+
if Path(profile_target.path).is_absolute()
|
|
62
|
+
else Path(project_root).joinpath(profile_target.path)
|
|
63
|
+
)
|
|
64
|
+
con = ibis.duckdb.connect(connection_path)
|
|
65
|
+
|
|
66
|
+
con = ibis.duckdb.connect("myproject/" + profile_target.path)
|
|
67
|
+
columns_by_name: dict[str, tuple[Column, ...]] = {}
|
|
68
|
+
|
|
69
|
+
tables = con.list_tables()
|
|
70
|
+
for t in tables:
|
|
71
|
+
table: Table = con.table(t)
|
|
72
|
+
schema: Schema = table.schema()
|
|
73
|
+
|
|
74
|
+
columns_by_name[t] = tuple(
|
|
75
|
+
Column(name=name, data_type=str(dtype)) for name, dtype in schema.items()
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
Model(name=m.name, path=m.path, columns=columns_by_name.get(m.name, ()))
|
|
80
|
+
for m in models
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def enrich_models_from_database(
|
|
85
|
+
models: list[Model],
|
|
86
|
+
profile_target: DuckDBTarget | DatabaseTarget,
|
|
87
|
+
project_root: str | Path,
|
|
88
|
+
) -> list[Model] | None:
|
|
89
|
+
match profile_target:
|
|
90
|
+
case DuckDBTarget():
|
|
91
|
+
return get_duckdb_models(models, profile_target, project_root)
|
|
92
|
+
case _:
|
|
93
|
+
print("here")
|
|
94
|
+
return None
|
dbt_ls/pattern.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
REF_RE = re.compile(r"""ref\(\s*(?P<q>['"])(?P<model>[^'"]*)$""")
|
|
5
|
+
# Full ref('model') call, used to find the model the cursor is *inside of*.
|
|
6
|
+
REF_FULL_RE = re.compile(r"""ref\(\s*['"](?P<model>[^'"]+)['"]""")
|
|
7
|
+
SOURCE_RE = re.compile(
|
|
8
|
+
r"""source\(\s*(?P<q1>['"])(?P<src>[^'"]*)"""
|
|
9
|
+
r"""(?:(?P=q1)\s*,\s*(?P<q2>['"])(?P<tbl>[^'"]*))?$"""
|
|
10
|
+
)
|
|
11
|
+
COLUMN_RE = re.compile(r"(?P<alias>[a-zA-Z_]\w*)\.(?P<col>[a-zA-Z0-9_]*)$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def completion_context(line_prefix: str):
|
|
15
|
+
"""What is the cursor currently completing? None if not in a ref/source."""
|
|
16
|
+
if m := SOURCE_RE.search(line_prefix):
|
|
17
|
+
# second arg started -> completing the table within a known source
|
|
18
|
+
if m.group("tbl") is not None:
|
|
19
|
+
return ("source_table", {"source": m.group("src")})
|
|
20
|
+
# still in first arg -> completing the source name
|
|
21
|
+
return ("source_name", {})
|
|
22
|
+
if m := REF_RE.search(line_prefix):
|
|
23
|
+
return ("ref", {})
|
|
24
|
+
if m := COLUMN_RE.search(line_prefix):
|
|
25
|
+
return ("column", {"alias": m.group("alias")})
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def ref_model_at(line: str, character: int) -> str | None:
|
|
30
|
+
"""
|
|
31
|
+
Check if cursor is on a model
|
|
32
|
+
"""
|
|
33
|
+
for m in REF_FULL_RE.finditer(line):
|
|
34
|
+
if m.start("model") <= character <= m.end("model"):
|
|
35
|
+
return m.group("model")
|
|
36
|
+
return None
|
dbt_ls/profiles.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from dataclasses import dataclass, fields
|
|
3
|
+
import yaml
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(kw_only=True)
|
|
8
|
+
class ProfileTarget:
|
|
9
|
+
type: str
|
|
10
|
+
threads: int = 1
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def from_dict(cls, data: dict) -> "ProfileTarget":
|
|
14
|
+
target_cls = _TARGET_REGISTRY.get(data.get("type", ""), cls)
|
|
15
|
+
allowed = {f.name for f in fields(target_cls)}
|
|
16
|
+
return target_cls(**{k: v for k, v in data.items() if k in allowed})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(kw_only=True)
|
|
20
|
+
class DuckDBTarget(ProfileTarget):
|
|
21
|
+
path: str
|
|
22
|
+
|
|
23
|
+
# def __post_init__(self):
|
|
24
|
+
# if not Path(self.path).is_absolute():
|
|
25
|
+
# self.path = str(Path("root").joinpath(self.path))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(kw_only=True)
|
|
29
|
+
class DatabaseTarget(ProfileTarget):
|
|
30
|
+
user: str
|
|
31
|
+
password: str
|
|
32
|
+
host: str
|
|
33
|
+
port: int
|
|
34
|
+
dbname: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_TARGET_REGISTRY: dict[str, type[ProfileTarget]] = {
|
|
38
|
+
"duckdb": DuckDBTarget,
|
|
39
|
+
"postgres": DatabaseTarget,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Profiles:
|
|
44
|
+
def __init__(self, path: Path):
|
|
45
|
+
self.path = path
|
|
46
|
+
self.config: dict[str, Any] = yaml.safe_load(self.path.read_text()) or {}
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def locate(cls, project_root: str) -> "Profiles | None":
|
|
50
|
+
candidate = cls._search_dirs(project_root)
|
|
51
|
+
if not candidate:
|
|
52
|
+
return None
|
|
53
|
+
if candidate.exists():
|
|
54
|
+
return cls(candidate)
|
|
55
|
+
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _search_dirs(project_root: str) -> Path | None:
|
|
60
|
+
profile_paths = [Path(project_root), Path.home().joinpath(".dbt")]
|
|
61
|
+
|
|
62
|
+
for profile_path in profile_paths:
|
|
63
|
+
if Path(profile_path.joinpath("profiles.yml")).exists():
|
|
64
|
+
return Path(profile_path.joinpath("profiles.yml"))
|
|
65
|
+
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def resolve(self, profile_name: str, target: str | None = None) -> ProfileTarget:
|
|
69
|
+
if not target:
|
|
70
|
+
target = self.config[profile_name]["target"]
|
|
71
|
+
|
|
72
|
+
profile_target = self.config[profile_name]["outputs"][target]
|
|
73
|
+
|
|
74
|
+
return ProfileTarget.from_dict(profile_target)
|
dbt_ls/project.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Project:
|
|
6
|
+
def __init__(self, root: str = "."):
|
|
7
|
+
self.root = root
|
|
8
|
+
self.config = self._load_config()
|
|
9
|
+
|
|
10
|
+
def _load_config(self) -> dict:
|
|
11
|
+
path = os.path.join(self.root, "dbt_project.yml")
|
|
12
|
+
with open(path) as f:
|
|
13
|
+
return yaml.safe_load(f) or {}
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def model_paths(self) -> list[str]:
|
|
17
|
+
return self.config.get("model-paths", [])
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def profile(self) -> str:
|
|
21
|
+
return self.config.get("profile", "")
|
dbt_ls/source.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import yaml
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from dbt_ls.column import Column
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class SourceTable:
|
|
11
|
+
name: str
|
|
12
|
+
source_name: str
|
|
13
|
+
database: str | None = None
|
|
14
|
+
schema: str | None = None
|
|
15
|
+
columns: tuple[Column, ...] = ()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def discover_sources(root: str) -> list:
|
|
19
|
+
sources = []
|
|
20
|
+
for p in Path(root).rglob("*.yml"):
|
|
21
|
+
if "target" in p.parts or not p.is_file():
|
|
22
|
+
continue
|
|
23
|
+
doc = yaml.safe_load(p.read_text())
|
|
24
|
+
if not doc or "sources" not in doc:
|
|
25
|
+
continue
|
|
26
|
+
for src in doc["sources"]:
|
|
27
|
+
source_name = src.get("name", "")
|
|
28
|
+
for table in src.get("tables", []):
|
|
29
|
+
sources.append(
|
|
30
|
+
SourceTable(
|
|
31
|
+
name=table["name"],
|
|
32
|
+
source_name=source_name,
|
|
33
|
+
database=src.get("database"),
|
|
34
|
+
schema=src.get("schema"),
|
|
35
|
+
columns=tuple(
|
|
36
|
+
[
|
|
37
|
+
Column(name=c["name"], data_type=c.get("data_type"))
|
|
38
|
+
for c in table.get("columns", [])
|
|
39
|
+
]
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
return sources
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def enrich_sources_from_catalog(
|
|
47
|
+
sources: list[SourceTable], catalog_path: Path
|
|
48
|
+
) -> list[SourceTable]:
|
|
49
|
+
path = Path(catalog_path)
|
|
50
|
+
if not path.is_file():
|
|
51
|
+
return sources
|
|
52
|
+
catalog = json.loads(path.read_text())
|
|
53
|
+
catalog_sources = catalog.get("sources", {})
|
|
54
|
+
# Build a lookup: source name -> columns
|
|
55
|
+
columns_by_name: dict[str, tuple[Column, ...]] = {}
|
|
56
|
+
for source in catalog_sources.values():
|
|
57
|
+
if not source.get("unique_id", "").startswith("source."):
|
|
58
|
+
continue
|
|
59
|
+
name = source["metadata"]["name"]
|
|
60
|
+
columns_by_name[name] = tuple(
|
|
61
|
+
Column(name=c["name"], data_type=c.get("type"))
|
|
62
|
+
for c in source.get("columns", {}).values()
|
|
63
|
+
)
|
|
64
|
+
return [
|
|
65
|
+
SourceTable(
|
|
66
|
+
name=s.name,
|
|
67
|
+
source_name=s.source_name,
|
|
68
|
+
columns=columns_by_name.get(s.name) or s.columns,
|
|
69
|
+
)
|
|
70
|
+
for s in sources
|
|
71
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dbt-ls
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: HuhtaLauri <huhta.lauri@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: black>=25.12.0
|
|
8
|
+
Requires-Dist: dbt-core>1.5.1
|
|
9
|
+
Requires-Dist: dbt-duckdb>=1.10.1
|
|
10
|
+
Requires-Dist: hatch>=1.16.5
|
|
11
|
+
Requires-Dist: ibis-framework[duckdb]>=12.0.0
|
|
12
|
+
Requires-Dist: pygls>=2.1.1
|
|
13
|
+
Requires-Dist: pytest>=9.0.3
|
|
14
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# DBT-Language-Server
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Prompt a hover tooltip what columns model returns
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
dbt_ls/__init__.py,sha256=1PtsUfZKT5b7zko-etaPqum9HZVLDtv9MzgxU5_WDBA,6874
|
|
2
|
+
dbt_ls/alias.py,sha256=jDDM2wmU47M76E49cuiURKZ7NIyIUJ-vX1xQePnmrMs,396
|
|
3
|
+
dbt_ls/column.py,sha256=Pf_uZ5PZfLUc3TNCdbVbcdYcVa_UJjmbuF2fmoXQeM0,121
|
|
4
|
+
dbt_ls/model.py,sha256=BZOrcZye8mEwT3doeRnKF0YJUcbLwpsiTFNDtj_yQBo,2690
|
|
5
|
+
dbt_ls/pattern.py,sha256=-Wr82H_O0bNRTv6CFGm45LeCF_uc842l8Xmx5MiiJiA,1316
|
|
6
|
+
dbt_ls/profiles.py,sha256=ivRNRctDaIlGU2YhdPF-oUZwBzXJzwQooJKkcEbjR00,2008
|
|
7
|
+
dbt_ls/project.py,sha256=p0hGVAUjjzArBkbWgr23irFDuOHDO9wgUEEWf5-JMB0,511
|
|
8
|
+
dbt_ls/source.py,sha256=nVoJdRJ3GJ6dyk9qPbt0gBH6Y4DfaKxFdkONkmAcWEU,2226
|
|
9
|
+
dbt_ls-0.1.1.dist-info/METADATA,sha256=harh3ZezkOd-05TozPPePGilsigg9waf75NdFsq9CIo,528
|
|
10
|
+
dbt_ls-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
dbt_ls-0.1.1.dist-info/entry_points.txt,sha256=18sOzg1GYOzOESCOT1JDK6HDd4iqMGffxR5WnvzdM2s,39
|
|
12
|
+
dbt_ls-0.1.1.dist-info/RECORD,,
|