7
7
import shutil
8
8
import sys
9
9
import venv
10
- from collections .abc import Callable , Sequence , Mapping
10
+ from collections .abc import Callable , Iterable , Mapping , Sequence
11
11
from dataclasses import dataclass
12
12
from pathlib import Path
13
- from typing import Any , Literal
13
+ from typing import Any , Literal , cast
14
14
15
15
import attrs
16
16
import yaml
34
34
from pygls .exceptions import FeatureRequestError
35
35
from pygls .lsp .client import BaseLanguageClient
36
36
37
+ from databricks .labs .blueprint .installation import JsonValue , RootJsonValue
37
38
from databricks .labs .blueprint .wheels import ProductInfo
38
- from databricks .labs .lakebridge .config import LSPConfigOptionV1 , TranspileConfig , TranspileResult
39
+ from databricks .labs .lakebridge .config import LSPConfigOptionV1 , TranspileConfig , TranspileResult , extract_string_field
39
40
from databricks .labs .lakebridge .errors .exceptions import IllegalStateException
40
41
from databricks .labs .lakebridge .helpers .file_utils import is_dbt_project_file , is_sql_file
41
42
from databricks .labs .lakebridge .transpiler .transpile_engine import TranspileEngine
50
51
logger = logging .getLogger (__name__ )
51
52
52
53
54
+ def _is_all_strings (values : Iterable [object ]) -> bool :
55
+ """Typeguard, to check if all values in the iterable are strings."""
56
+ return all (isinstance (x , str ) for x in values )
57
+
58
+
59
+ def _is_all_sequences (values : Iterable [object ]) -> bool :
60
+ """Typeguard, to check if all values in the iterable are sequences."""
61
+ return all (isinstance (x , Sequence ) for x in values )
62
+
63
+
53
64
@dataclass
54
65
class _LSPRemorphConfigV1 :
55
66
name : str
@@ -58,29 +69,63 @@ class _LSPRemorphConfigV1:
58
69
command_line : Sequence [str ]
59
70
60
71
@classmethod
61
- def parse (cls , data : Mapping [str , Any ]) -> _LSPRemorphConfigV1 :
62
- version = data .get ("version" , 0 )
72
+ def parse (cls , data : Mapping [str , JsonValue ]) -> _LSPRemorphConfigV1 :
73
+ cls ._check_version (data )
74
+ name = extract_string_field (data , "name" )
75
+ dialects = cls ._extract_dialects (data )
76
+ env_vars = cls ._extract_env_vars (data )
77
+ command_line = cls ._extract_command_line (data )
78
+ return _LSPRemorphConfigV1 (name , dialects , env_vars , command_line )
79
+
80
+ @classmethod
81
+ def _check_version (cls , data : Mapping [str , JsonValue ]) -> None :
82
+ try :
83
+ version = data ["version" ]
84
+ except KeyError as e :
85
+ raise ValueError ("Missing 'version' attribute" ) from e
63
86
if version != 1 :
64
87
raise ValueError (f"Unsupported transpiler config version: { version } " )
65
- name : str | None = data .get ("name" , None )
66
- if not name :
67
- raise ValueError ("Missing 'name' entry" )
68
- dialects = data .get ("dialects" , [])
69
- if len (dialects ) == 0 :
70
- raise ValueError ("Missing 'dialects' entry" )
71
- env_vars = data .get ("environment" , {})
72
- command_line = data .get ("command_line" , [])
73
- if len (command_line ) == 0 :
74
- raise ValueError ("Missing 'command_line' entry" )
75
- return _LSPRemorphConfigV1 (name , dialects , env_vars , command_line )
88
+
89
+ @classmethod
90
+ def _extract_dialects (cls , data : Mapping [str , JsonValue ]) -> Sequence [str ]:
91
+ try :
92
+ dialects_unsafe = data ["dialects" ]
93
+ except KeyError as e :
94
+ raise ValueError ("Missing 'dialects' attribute" ) from e
95
+ if not isinstance (dialects_unsafe , list ) or not dialects_unsafe or not _is_all_strings (dialects_unsafe ):
96
+ msg = f"Invalid 'dialects' attribute, expected a non-empty list of strings but got: { dialects_unsafe } "
97
+ raise ValueError (msg )
98
+ return cast (list [str ], dialects_unsafe )
99
+
100
+ @classmethod
101
+ def _extract_env_vars (cls , data : Mapping [str , JsonValue ]) -> Mapping [str , str ]:
102
+ try :
103
+ env_vars_unsafe = data ["environment" ]
104
+ if not isinstance (env_vars_unsafe , Mapping ) or not _is_all_strings (env_vars_unsafe .values ()):
105
+ msg = f"Invalid 'environment' entry, expected a mapping with string values but got: { env_vars_unsafe } "
106
+ raise ValueError (msg )
107
+ return cast (dict [str , str ], env_vars_unsafe )
108
+ except KeyError :
109
+ return {}
110
+
111
+ @classmethod
112
+ def _extract_command_line (cls , data : Mapping [str , JsonValue ]) -> Sequence [str ]:
113
+ try :
114
+ command_line = data ["command_line" ]
115
+ except KeyError as e :
116
+ raise ValueError ("Missing 'command_line' attribute" ) from e
117
+ if not isinstance (command_line , list ) or not command_line or not _is_all_strings (command_line ):
118
+ msg = f"Invalid 'command_line' attribute, expected a non-empty list of strings but got: { command_line } "
119
+ raise ValueError (msg )
120
+ return cast (list [str ], command_line )
76
121
77
122
78
123
@dataclass
79
124
class LSPConfig :
80
125
path : Path
81
126
remorph : _LSPRemorphConfigV1
82
127
options : Mapping [str , Sequence [LSPConfigOptionV1 ]]
83
- custom : Mapping [str , Any ]
128
+ custom : Mapping [str , JsonValue ]
84
129
85
130
@property
86
131
def name (self ):
@@ -92,20 +137,52 @@ def options_for_dialect(self, source_dialect: str) -> Sequence[LSPConfigOptionV1
92
137
@classmethod
93
138
def load (cls , path : Path ) -> LSPConfig :
94
139
yaml_text = path .read_text ()
95
- data = yaml .safe_load (yaml_text )
96
- if not isinstance (data , dict ):
97
- raise ValueError (f"Invalid transpiler config, expecting a dict, got a { type (data ).__name__ } " )
98
- remorph_data = data .get ("remorph" , None )
99
- if not isinstance (remorph_data , dict ):
100
- raise ValueError (f"Invalid transpiler config, expecting a 'remorph' dict entry, got { remorph_data } " )
101
- remorph = _LSPRemorphConfigV1 .parse (remorph_data )
102
- options_data = data .get ("options" , {})
103
- if not isinstance (options_data , dict ):
104
- raise ValueError (f"Invalid transpiler config, expecting an 'options' dict entry, got { options_data } " )
105
- options = LSPConfigOptionV1 .parse_all (options_data )
106
- custom = data .get ("custom" , {})
140
+ data : RootJsonValue = yaml .safe_load (yaml_text )
141
+ if not isinstance (data , Mapping ):
142
+ msg = f"Invalid transpiler configuration, expecting a root object but got: { data } "
143
+ raise ValueError (msg )
144
+
145
+ remorph = cls ._extract_remorph_data (data )
146
+ options = cls ._extract_options (data )
147
+ custom = cls ._extract_custom (data )
107
148
return LSPConfig (path , remorph , options , custom )
108
149
150
+ @classmethod
151
+ def _extract_remorph_data (cls , data : Mapping [str , JsonValue ]) -> _LSPRemorphConfigV1 :
152
+ try :
153
+ remorph_data = data ["remorph" ]
154
+ except KeyError as e :
155
+ raise ValueError ("Missing 'remorph' attribute" ) from e
156
+ if not isinstance (remorph_data , Mapping ):
157
+ msg = f"Invalid transpiler config, 'remorph' entry must be an object but got: { remorph_data } "
158
+ raise ValueError (msg )
159
+ return _LSPRemorphConfigV1 .parse (remorph_data )
160
+
161
+ @classmethod
162
+ def _extract_options (cls , data : Mapping [str , JsonValue ]) -> Mapping [str , Sequence [LSPConfigOptionV1 ]]:
163
+ try :
164
+ options_data_unsfe = data ["options" ]
165
+ except KeyError :
166
+ # Optional, so no problem if missing
167
+ return {}
168
+ if not isinstance (options_data_unsfe , Mapping ) or not _is_all_sequences (options_data_unsfe .values ()):
169
+ msg = f"Invalid transpiler config, 'options' must be an object with list properties but got: { options_data_unsfe } "
170
+ raise ValueError (msg )
171
+ options_data = cast (dict [str , Sequence [JsonValue ]], options_data_unsfe )
172
+ return LSPConfigOptionV1 .parse_all (options_data )
173
+
174
+ @classmethod
175
+ def _extract_custom (cls , data : Mapping [str , JsonValue ]) -> Mapping [str , JsonValue ]:
176
+ try :
177
+ custom = data ["custom" ]
178
+ if not isinstance (custom , Mapping ):
179
+ msg = f"Invalid 'custom' entry, expected a mapping but got: { custom } "
180
+ raise ValueError (msg )
181
+ return custom
182
+ except KeyError :
183
+ # Optional, so no problem if missing
184
+ return {}
185
+
109
186
110
187
def lsp_feature (name : str , options : Any | None = None ):
111
188
def wrapped (func : Callable ):
0 commit comments