pytrilogy 0.0.1.102__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.
Potentially problematic release.
This version of pytrilogy might be problematic. Click here for more details.
- pytrilogy-0.0.1.102.dist-info/LICENSE.md +19 -0
- pytrilogy-0.0.1.102.dist-info/METADATA +277 -0
- pytrilogy-0.0.1.102.dist-info/RECORD +77 -0
- pytrilogy-0.0.1.102.dist-info/WHEEL +5 -0
- pytrilogy-0.0.1.102.dist-info/entry_points.txt +2 -0
- pytrilogy-0.0.1.102.dist-info/top_level.txt +1 -0
- trilogy/__init__.py +8 -0
- trilogy/compiler.py +0 -0
- trilogy/constants.py +30 -0
- trilogy/core/__init__.py +0 -0
- trilogy/core/constants.py +3 -0
- trilogy/core/enums.py +270 -0
- trilogy/core/env_processor.py +33 -0
- trilogy/core/environment_helpers.py +156 -0
- trilogy/core/ergonomics.py +187 -0
- trilogy/core/exceptions.py +23 -0
- trilogy/core/functions.py +320 -0
- trilogy/core/graph_models.py +55 -0
- trilogy/core/internal.py +37 -0
- trilogy/core/models.py +3145 -0
- trilogy/core/processing/__init__.py +0 -0
- trilogy/core/processing/concept_strategies_v3.py +603 -0
- trilogy/core/processing/graph_utils.py +44 -0
- trilogy/core/processing/node_generators/__init__.py +25 -0
- trilogy/core/processing/node_generators/basic_node.py +71 -0
- trilogy/core/processing/node_generators/common.py +239 -0
- trilogy/core/processing/node_generators/concept_merge.py +152 -0
- trilogy/core/processing/node_generators/filter_node.py +83 -0
- trilogy/core/processing/node_generators/group_node.py +92 -0
- trilogy/core/processing/node_generators/group_to_node.py +99 -0
- trilogy/core/processing/node_generators/merge_node.py +148 -0
- trilogy/core/processing/node_generators/multiselect_node.py +189 -0
- trilogy/core/processing/node_generators/rowset_node.py +130 -0
- trilogy/core/processing/node_generators/select_node.py +328 -0
- trilogy/core/processing/node_generators/unnest_node.py +37 -0
- trilogy/core/processing/node_generators/window_node.py +85 -0
- trilogy/core/processing/nodes/__init__.py +76 -0
- trilogy/core/processing/nodes/base_node.py +251 -0
- trilogy/core/processing/nodes/filter_node.py +49 -0
- trilogy/core/processing/nodes/group_node.py +110 -0
- trilogy/core/processing/nodes/merge_node.py +326 -0
- trilogy/core/processing/nodes/select_node_v2.py +198 -0
- trilogy/core/processing/nodes/unnest_node.py +54 -0
- trilogy/core/processing/nodes/window_node.py +34 -0
- trilogy/core/processing/utility.py +278 -0
- trilogy/core/query_processor.py +331 -0
- trilogy/dialect/__init__.py +0 -0
- trilogy/dialect/base.py +679 -0
- trilogy/dialect/bigquery.py +80 -0
- trilogy/dialect/common.py +43 -0
- trilogy/dialect/config.py +55 -0
- trilogy/dialect/duckdb.py +83 -0
- trilogy/dialect/enums.py +95 -0
- trilogy/dialect/postgres.py +86 -0
- trilogy/dialect/presto.py +82 -0
- trilogy/dialect/snowflake.py +82 -0
- trilogy/dialect/sql_server.py +89 -0
- trilogy/docs/__init__.py +0 -0
- trilogy/engine.py +48 -0
- trilogy/executor.py +242 -0
- trilogy/hooks/__init__.py +0 -0
- trilogy/hooks/base_hook.py +37 -0
- trilogy/hooks/graph_hook.py +24 -0
- trilogy/hooks/query_debugger.py +133 -0
- trilogy/metadata/__init__.py +0 -0
- trilogy/parser.py +10 -0
- trilogy/parsing/__init__.py +0 -0
- trilogy/parsing/common.py +176 -0
- trilogy/parsing/config.py +5 -0
- trilogy/parsing/exceptions.py +2 -0
- trilogy/parsing/helpers.py +1 -0
- trilogy/parsing/parse_engine.py +1951 -0
- trilogy/parsing/render.py +483 -0
- trilogy/py.typed +0 -0
- trilogy/scripts/__init__.py +0 -0
- trilogy/scripts/trilogy.py +127 -0
- trilogy/utility.py +31 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from trilogy.core.models import (
|
|
2
|
+
Function,
|
|
3
|
+
Concept,
|
|
4
|
+
AggregateWrapper,
|
|
5
|
+
Parenthetical,
|
|
6
|
+
arg_to_datatype,
|
|
7
|
+
WindowItem,
|
|
8
|
+
DataType,
|
|
9
|
+
ListType,
|
|
10
|
+
StructType,
|
|
11
|
+
MapType,
|
|
12
|
+
)
|
|
13
|
+
from trilogy.core.enums import FunctionType, Purpose, Granularity, DatePart
|
|
14
|
+
from trilogy.core.exceptions import InvalidSyntaxException
|
|
15
|
+
from trilogy.constants import MagicConstants
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_function_derived_concept(
|
|
20
|
+
name: str,
|
|
21
|
+
namespace: str,
|
|
22
|
+
operator: FunctionType,
|
|
23
|
+
arguments: list[Concept],
|
|
24
|
+
output_type: Optional[DataType | ListType | StructType | MapType] = None,
|
|
25
|
+
output_purpose: Optional[Purpose] = None,
|
|
26
|
+
) -> Concept:
|
|
27
|
+
purpose = (
|
|
28
|
+
function_args_to_output_purpose(arguments)
|
|
29
|
+
if output_purpose is None
|
|
30
|
+
else output_purpose
|
|
31
|
+
)
|
|
32
|
+
output_type = arg_to_datatype(arguments[0]) if output_type is None else output_type
|
|
33
|
+
return Concept(
|
|
34
|
+
name=name,
|
|
35
|
+
namespace=namespace,
|
|
36
|
+
datatype=output_type,
|
|
37
|
+
purpose=purpose,
|
|
38
|
+
lineage=Function(
|
|
39
|
+
operator=operator,
|
|
40
|
+
arguments=arguments,
|
|
41
|
+
output_datatype=output_type,
|
|
42
|
+
output_purpose=purpose,
|
|
43
|
+
arg_count=len(arguments),
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def argument_to_purpose(arg) -> Purpose:
|
|
49
|
+
if isinstance(arg, Function):
|
|
50
|
+
return arg.output_purpose
|
|
51
|
+
elif isinstance(arg, AggregateWrapper):
|
|
52
|
+
return arg.function.output_purpose
|
|
53
|
+
elif isinstance(arg, Parenthetical):
|
|
54
|
+
return argument_to_purpose(arg.content)
|
|
55
|
+
elif isinstance(arg, WindowItem):
|
|
56
|
+
return Purpose.PROPERTY
|
|
57
|
+
elif isinstance(arg, Concept):
|
|
58
|
+
return arg.purpose
|
|
59
|
+
elif isinstance(arg, (int, float, str, bool, list)):
|
|
60
|
+
return Purpose.CONSTANT
|
|
61
|
+
elif isinstance(arg, DataType):
|
|
62
|
+
return Purpose.CONSTANT
|
|
63
|
+
elif isinstance(arg, DatePart):
|
|
64
|
+
return Purpose.CONSTANT
|
|
65
|
+
elif isinstance(arg, MagicConstants):
|
|
66
|
+
return Purpose.CONSTANT
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f"Cannot parse arg purpose for {arg} of type {type(arg)}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def function_args_to_output_purpose(args) -> Purpose:
|
|
72
|
+
has_metric = False
|
|
73
|
+
has_non_constant = False
|
|
74
|
+
has_non_single_row_constant = False
|
|
75
|
+
if not args:
|
|
76
|
+
return Purpose.CONSTANT
|
|
77
|
+
for arg in args:
|
|
78
|
+
purpose = argument_to_purpose(arg)
|
|
79
|
+
if purpose == Purpose.METRIC:
|
|
80
|
+
has_metric = True
|
|
81
|
+
if purpose != Purpose.CONSTANT:
|
|
82
|
+
has_non_constant = True
|
|
83
|
+
if isinstance(arg, Concept) and arg.granularity != Granularity.SINGLE_ROW:
|
|
84
|
+
has_non_single_row_constant = True
|
|
85
|
+
if args and not has_non_constant and not has_non_single_row_constant:
|
|
86
|
+
return Purpose.CONSTANT
|
|
87
|
+
if has_metric:
|
|
88
|
+
return Purpose.METRIC
|
|
89
|
+
return Purpose.PROPERTY
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def Unnest(args: list[Concept]) -> Function:
|
|
93
|
+
output = arg_to_datatype(args[0])
|
|
94
|
+
if isinstance(output, (ListType)):
|
|
95
|
+
output = output.value_data_type
|
|
96
|
+
return Function(
|
|
97
|
+
operator=FunctionType.UNNEST,
|
|
98
|
+
arguments=args,
|
|
99
|
+
output_datatype=output,
|
|
100
|
+
output_purpose=Purpose.KEY,
|
|
101
|
+
arg_count=1,
|
|
102
|
+
valid_inputs={
|
|
103
|
+
DataType.ARRAY,
|
|
104
|
+
DataType.LIST,
|
|
105
|
+
ListType(type=DataType.STRING),
|
|
106
|
+
ListType(type=DataType.INTEGER),
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def Group(args: list[Concept]) -> Function:
|
|
112
|
+
output = args[0]
|
|
113
|
+
return Function(
|
|
114
|
+
operator=FunctionType.GROUP,
|
|
115
|
+
arguments=args,
|
|
116
|
+
output_datatype=output.datatype,
|
|
117
|
+
output_purpose=Purpose.PROPERTY,
|
|
118
|
+
arg_count=-1,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def Count(args: list[Concept]) -> Function:
|
|
123
|
+
return Function(
|
|
124
|
+
operator=FunctionType.COUNT,
|
|
125
|
+
arguments=args,
|
|
126
|
+
output_datatype=DataType.INTEGER,
|
|
127
|
+
output_purpose=Purpose.METRIC,
|
|
128
|
+
arg_count=1,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def CountDistinct(args: list[Concept]) -> Function:
|
|
133
|
+
return Function(
|
|
134
|
+
operator=FunctionType.COUNT_DISTINCT,
|
|
135
|
+
arguments=args,
|
|
136
|
+
output_datatype=DataType.INTEGER,
|
|
137
|
+
output_purpose=Purpose.METRIC,
|
|
138
|
+
arg_count=1,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def Max(args: list[Concept]) -> Function:
|
|
143
|
+
return Function(
|
|
144
|
+
operator=FunctionType.MAX,
|
|
145
|
+
arguments=args,
|
|
146
|
+
output_datatype=args[0].datatype,
|
|
147
|
+
output_purpose=Purpose.METRIC,
|
|
148
|
+
valid_inputs={
|
|
149
|
+
DataType.INTEGER,
|
|
150
|
+
DataType.FLOAT,
|
|
151
|
+
DataType.NUMBER,
|
|
152
|
+
DataType.DATE,
|
|
153
|
+
DataType.DATETIME,
|
|
154
|
+
DataType.TIMESTAMP,
|
|
155
|
+
},
|
|
156
|
+
arg_count=1,
|
|
157
|
+
# output_grain=Grain(components=arguments),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def Min(args: list[Concept]) -> Function:
|
|
162
|
+
return Function(
|
|
163
|
+
operator=FunctionType.MIN,
|
|
164
|
+
arguments=args,
|
|
165
|
+
output_datatype=args[0].datatype,
|
|
166
|
+
output_purpose=Purpose.METRIC,
|
|
167
|
+
valid_inputs={
|
|
168
|
+
DataType.INTEGER,
|
|
169
|
+
DataType.FLOAT,
|
|
170
|
+
DataType.NUMBER,
|
|
171
|
+
DataType.DATE,
|
|
172
|
+
DataType.DATETIME,
|
|
173
|
+
DataType.TIMESTAMP,
|
|
174
|
+
},
|
|
175
|
+
arg_count=1,
|
|
176
|
+
# output_grain=Grain(components=arguments),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def Split(args: list[Concept]) -> Function:
|
|
181
|
+
# TODO: overload this for non-string types?
|
|
182
|
+
return Function(
|
|
183
|
+
operator=FunctionType.SPLIT,
|
|
184
|
+
arguments=args,
|
|
185
|
+
# first arg sets properties
|
|
186
|
+
output_datatype=ListType(type=DataType.STRING),
|
|
187
|
+
output_purpose=function_args_to_output_purpose(args),
|
|
188
|
+
valid_inputs={DataType.STRING},
|
|
189
|
+
arg_count=2,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def IndexAccess(args: list[Concept]):
|
|
194
|
+
return Function(
|
|
195
|
+
operator=FunctionType.INDEX_ACCESS,
|
|
196
|
+
arguments=args,
|
|
197
|
+
output_datatype=(
|
|
198
|
+
args[0].datatype.value_data_type
|
|
199
|
+
if isinstance(args[0].datatype, ListType)
|
|
200
|
+
else args[0].datatype
|
|
201
|
+
),
|
|
202
|
+
output_purpose=Purpose.PROPERTY,
|
|
203
|
+
valid_inputs=[
|
|
204
|
+
{
|
|
205
|
+
DataType.LIST,
|
|
206
|
+
ListType(type=DataType.STRING),
|
|
207
|
+
ListType(type=DataType.INTEGER),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
DataType.INTEGER,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
arg_count=2,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def AttrAccess(args: list[Concept]):
|
|
218
|
+
return Function(
|
|
219
|
+
operator=FunctionType.ATTR_ACCESS,
|
|
220
|
+
arguments=args,
|
|
221
|
+
output_datatype=args[0].field_map[args[1]].datatype, # type: ignore
|
|
222
|
+
output_purpose=Purpose.PROPERTY,
|
|
223
|
+
valid_inputs=[
|
|
224
|
+
{DataType.STRUCT},
|
|
225
|
+
{
|
|
226
|
+
DataType.STRING,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
arg_count=2,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def Abs(args: list[Concept]) -> Function:
|
|
234
|
+
return Function(
|
|
235
|
+
operator=FunctionType.ABS,
|
|
236
|
+
arguments=args,
|
|
237
|
+
output_datatype=args[0].datatype,
|
|
238
|
+
output_purpose=function_args_to_output_purpose(args),
|
|
239
|
+
valid_inputs={
|
|
240
|
+
DataType.INTEGER,
|
|
241
|
+
DataType.FLOAT,
|
|
242
|
+
DataType.NUMBER,
|
|
243
|
+
},
|
|
244
|
+
arg_count=1,
|
|
245
|
+
# output_grain=Grain(components=arguments),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def Coalesce(args: list[Concept]) -> Function:
|
|
250
|
+
non_null = [x for x in args if not x == MagicConstants.NULL]
|
|
251
|
+
if not len(set(arg_to_datatype(x) for x in non_null if x)) == 1:
|
|
252
|
+
raise InvalidSyntaxException(
|
|
253
|
+
f"All arguments to coalesce must be of the same type, have {set(arg_to_datatype(x) for x in args)}"
|
|
254
|
+
)
|
|
255
|
+
return Function(
|
|
256
|
+
operator=FunctionType.COALESCE,
|
|
257
|
+
arguments=args,
|
|
258
|
+
output_datatype=arg_to_datatype(non_null[0]),
|
|
259
|
+
output_purpose=function_args_to_output_purpose(non_null),
|
|
260
|
+
arg_count=-1,
|
|
261
|
+
# output_grain=Grain(components=arguments),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def CurrentDate(args: list[Concept]) -> Function:
|
|
266
|
+
return Function(
|
|
267
|
+
operator=FunctionType.CURRENT_DATE,
|
|
268
|
+
arguments=args,
|
|
269
|
+
output_datatype=DataType.DATE,
|
|
270
|
+
output_purpose=Purpose.CONSTANT,
|
|
271
|
+
arg_count=0,
|
|
272
|
+
# output_grain=Grain(components=arguments),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def CurrentDatetime(args: list[Concept]) -> Function:
|
|
277
|
+
return Function(
|
|
278
|
+
operator=FunctionType.CURRENT_DATETIME,
|
|
279
|
+
arguments=args,
|
|
280
|
+
output_datatype=DataType.DATE,
|
|
281
|
+
output_purpose=Purpose.CONSTANT,
|
|
282
|
+
arg_count=0,
|
|
283
|
+
# output_grain=Grain(components=arguments),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def IsNull(args: list[Concept]) -> Function:
|
|
288
|
+
return Function(
|
|
289
|
+
operator=FunctionType.IS_NULL,
|
|
290
|
+
arguments=args,
|
|
291
|
+
output_datatype=DataType.BOOL,
|
|
292
|
+
output_purpose=function_args_to_output_purpose(args),
|
|
293
|
+
arg_count=1,
|
|
294
|
+
# output_grain=Grain(components=arguments),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def StrPos(args: list[Concept]) -> Function:
|
|
299
|
+
return Function(
|
|
300
|
+
operator=FunctionType.STRPOS,
|
|
301
|
+
arguments=args,
|
|
302
|
+
output_datatype=DataType.INTEGER,
|
|
303
|
+
output_purpose=function_args_to_output_purpose(args),
|
|
304
|
+
arg_count=2,
|
|
305
|
+
valid_inputs=[
|
|
306
|
+
{DataType.STRING},
|
|
307
|
+
{DataType.STRING},
|
|
308
|
+
],
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def SubString(args: list[Concept]) -> Function:
|
|
313
|
+
return Function(
|
|
314
|
+
operator=FunctionType.SUBSTRING,
|
|
315
|
+
arguments=args,
|
|
316
|
+
output_datatype=DataType.STRING,
|
|
317
|
+
output_purpose=function_args_to_output_purpose(args),
|
|
318
|
+
arg_count=3,
|
|
319
|
+
valid_inputs=[{DataType.STRING}, {DataType.INTEGER}, {DataType.INTEGER}],
|
|
320
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import networkx as nx
|
|
2
|
+
|
|
3
|
+
from trilogy.core.models import Concept, Datasource
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def concept_to_node(input: Concept) -> str:
|
|
7
|
+
# if input.purpose == Purpose.METRIC:
|
|
8
|
+
# return f"c~{input.namespace}.{input.name}@{input.grain}"
|
|
9
|
+
return f"c~{input.namespace}.{input.name}@{input.grain}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def datasource_to_node(input: Datasource) -> str:
|
|
13
|
+
# if isinstance(input, JoinedDataSource):
|
|
14
|
+
# return "ds~join~" + ",".join(
|
|
15
|
+
# [datasource_to_node(sub) for sub in input.datasources]
|
|
16
|
+
# )
|
|
17
|
+
return f"ds~{input.namespace}.{input.identifier}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReferenceGraph(nx.DiGraph):
|
|
21
|
+
def __init__(self, *args, **kwargs):
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
|
|
24
|
+
def add_node(self, node_for_adding, **attr):
|
|
25
|
+
if isinstance(node_for_adding, Concept):
|
|
26
|
+
node_name = concept_to_node(node_for_adding)
|
|
27
|
+
attr["type"] = "concept"
|
|
28
|
+
attr["concept"] = node_for_adding
|
|
29
|
+
attr["grain"] = node_for_adding.grain
|
|
30
|
+
elif isinstance(node_for_adding, Datasource):
|
|
31
|
+
node_name = datasource_to_node(node_for_adding)
|
|
32
|
+
attr["type"] = "datasource"
|
|
33
|
+
attr["ds"] = node_for_adding
|
|
34
|
+
attr["grain"] = node_for_adding.grain
|
|
35
|
+
else:
|
|
36
|
+
node_name = node_for_adding
|
|
37
|
+
super().add_node(node_name, **attr)
|
|
38
|
+
|
|
39
|
+
def add_edge(self, u_of_edge, v_of_edge, **attr):
|
|
40
|
+
if isinstance(u_of_edge, Concept):
|
|
41
|
+
orig = u_of_edge
|
|
42
|
+
u_of_edge = concept_to_node(u_of_edge)
|
|
43
|
+
if u_of_edge not in self.nodes:
|
|
44
|
+
self.add_node(orig)
|
|
45
|
+
elif isinstance(u_of_edge, Datasource):
|
|
46
|
+
u_of_edge = datasource_to_node(u_of_edge)
|
|
47
|
+
|
|
48
|
+
if isinstance(v_of_edge, Concept):
|
|
49
|
+
orig = v_of_edge
|
|
50
|
+
v_of_edge = concept_to_node(v_of_edge)
|
|
51
|
+
if v_of_edge not in self.nodes:
|
|
52
|
+
self.add_node(orig)
|
|
53
|
+
elif isinstance(v_of_edge, Datasource):
|
|
54
|
+
v_of_edge = datasource_to_node(v_of_edge)
|
|
55
|
+
super().add_edge(u_of_edge, v_of_edge, **attr)
|
trilogy/core/internal.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from trilogy.core.models import Concept, DataType, Function
|
|
2
|
+
from trilogy.core.enums import Purpose, FunctionType
|
|
3
|
+
from trilogy.core.constants import ALL_ROWS_CONCEPT, INTERNAL_NAMESPACE
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
DEFAULT_CONCEPTS = {
|
|
7
|
+
ALL_ROWS_CONCEPT: Concept(
|
|
8
|
+
name=ALL_ROWS_CONCEPT,
|
|
9
|
+
namespace=INTERNAL_NAMESPACE,
|
|
10
|
+
datatype=DataType.INTEGER,
|
|
11
|
+
purpose=Purpose.CONSTANT,
|
|
12
|
+
lineage=Function(
|
|
13
|
+
operator=FunctionType.CONSTANT,
|
|
14
|
+
arguments=[1],
|
|
15
|
+
output_datatype=DataType.INTEGER,
|
|
16
|
+
output_purpose=Purpose.CONSTANT,
|
|
17
|
+
),
|
|
18
|
+
),
|
|
19
|
+
"concept_name": Concept(
|
|
20
|
+
name="concept_name",
|
|
21
|
+
namespace=INTERNAL_NAMESPACE,
|
|
22
|
+
datatype=DataType.STRING,
|
|
23
|
+
purpose=Purpose.KEY,
|
|
24
|
+
),
|
|
25
|
+
"datasource": Concept(
|
|
26
|
+
name="datasource",
|
|
27
|
+
namespace=INTERNAL_NAMESPACE,
|
|
28
|
+
datatype=DataType.STRING,
|
|
29
|
+
purpose=Purpose.KEY,
|
|
30
|
+
),
|
|
31
|
+
"query_text": Concept(
|
|
32
|
+
name="query_text",
|
|
33
|
+
namespace=INTERNAL_NAMESPACE,
|
|
34
|
+
datatype=DataType.STRING,
|
|
35
|
+
purpose=Purpose.KEY,
|
|
36
|
+
),
|
|
37
|
+
}
|