perspective-cli 0.1.0__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.
- perspective/__init__.py +1 -0
- perspective/config.py +240 -0
- perspective/exceptions.py +15 -0
- perspective/ingest/dbt.py +150 -0
- perspective/ingest/ingest.py +164 -0
- perspective/ingest/postgres.py +388 -0
- perspective/ingest/sources/bi/powerbi/extract.py +184 -0
- perspective/ingest/sources/bi/powerbi/models.py +137 -0
- perspective/ingest/sources/bi/powerbi/pipeline.py +29 -0
- perspective/ingest/sources/bi/powerbi/transform.py +478 -0
- perspective/ingest/sources/bi/qlik_sense/extract.py +297 -0
- perspective/ingest/sources/bi/qlik_sense/models.py +22 -0
- perspective/ingest/sources/bi/qlik_sense/pipeline.py +19 -0
- perspective/ingest/sources/bi/qlik_sense/transform.py +76 -0
- perspective/ingest/sources/database/sap/extract.py +253 -0
- perspective/ingest/sources/database/sap/pipeline.py +23 -0
- perspective/ingest/sources/database/sap/transform.py +85 -0
- perspective/main.py +74 -0
- perspective/models/configs.py +422 -0
- perspective/models/dashboards.py +44 -0
- perspective/models/databases.py +26 -0
- perspective/utils/__init__.py +3 -0
- perspective/utils/options.py +77 -0
- perspective/utils/utils.py +274 -0
- perspective_cli-0.1.0.dist-info/METADATA +49 -0
- perspective_cli-0.1.0.dist-info/RECORD +29 -0
- perspective_cli-0.1.0.dist-info/WHEEL +5 -0
- perspective_cli-0.1.0.dist-info/entry_points.txt +2 -0
- perspective_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Extract a database table manifest from SAP metadata.
|
|
2
|
+
|
|
3
|
+
We use the local DuckDB instance to join the two source tables in order to be able
|
|
4
|
+
to produce models expected by Luma.
|
|
5
|
+
|
|
6
|
+
Manifests are saved in batches (50,000 tables per batch) in order to limit payload size.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from itertools import batched
|
|
10
|
+
|
|
11
|
+
import dlt
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from lumaCLI.metadata.models.database import (
|
|
15
|
+
DatabaseTable,
|
|
16
|
+
DatabaseTableColumn,
|
|
17
|
+
DatabaseTableManifest,
|
|
18
|
+
DatabaseTableSchemaMetadata,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
pipeline = dlt.pipeline(
|
|
23
|
+
pipeline_name="sap", destination="duckdb", dataset_name="bronze"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
custom_tables_query = """
|
|
27
|
+
SELECT tabname, fieldname, datatype, leng, ddtext
|
|
28
|
+
FROM custom_tables_details
|
|
29
|
+
LEFT JOIN column_details ON custom_tables_details.fieldname = column_details.rollname
|
|
30
|
+
ORDER BY tabname
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
standard_tables_query = """
|
|
34
|
+
SELECT tabname, fieldname, datatype, leng, ddtext
|
|
35
|
+
FROM standard_tables_details
|
|
36
|
+
LEFT JOIN column_details ON standard_tables_details.fieldname = column_details.rollname
|
|
37
|
+
ORDER BY tabname
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def transform():
|
|
42
|
+
with (
|
|
43
|
+
pipeline.sql_client() as client,
|
|
44
|
+
client.execute_query(standard_tables_query) as cursor,
|
|
45
|
+
):
|
|
46
|
+
table_to_columns = {}
|
|
47
|
+
# Create a table -> columns mapping.
|
|
48
|
+
for i, df in enumerate(cursor.iter_df(chunk_size=50000)):
|
|
49
|
+
logger.info(
|
|
50
|
+
f"Extracting table->column mappings from chunk {i} ({len(df)} rows)..."
|
|
51
|
+
)
|
|
52
|
+
for _, row in df.iterrows():
|
|
53
|
+
table_name = row["tabname"]
|
|
54
|
+
column = DatabaseTableColumn(
|
|
55
|
+
name=row["fieldname"],
|
|
56
|
+
type=row["datatype"],
|
|
57
|
+
length=str(row["leng"]),
|
|
58
|
+
description=row["ddtext"],
|
|
59
|
+
)
|
|
60
|
+
if table_name not in table_to_columns:
|
|
61
|
+
table_to_columns[table_name] = {"columns": []}
|
|
62
|
+
table_to_columns[table_name]["columns"].append(column)
|
|
63
|
+
|
|
64
|
+
# Create table manifests from the mapping.
|
|
65
|
+
tables = (
|
|
66
|
+
DatabaseTable(
|
|
67
|
+
name=table_name, columns=table_to_columns[table_name]["columns"], type="sap"
|
|
68
|
+
)
|
|
69
|
+
for table_name in table_to_columns
|
|
70
|
+
)
|
|
71
|
+
batch_size = 50000
|
|
72
|
+
for i, table_batch in enumerate(batched(tables, batch_size)):
|
|
73
|
+
logger.info(f"Generating manifest for table batch {i}...")
|
|
74
|
+
|
|
75
|
+
manifest = DatabaseTableManifest(
|
|
76
|
+
metadata=DatabaseTableSchemaMetadata(schema="database_table", version=1),
|
|
77
|
+
payload=list(table_batch),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
yield manifest
|
|
81
|
+
|
|
82
|
+
# logger.info(f"Writing {len(manifest.payload)} tables to batch {i} manifest...")
|
|
83
|
+
|
|
84
|
+
# with open(f"database_table_manifest__batch_{i}.json", "w") as f:
|
|
85
|
+
# f.write(manifest.json())
|
perspective/main.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""A command-line interface (CLI) for the Perspective application.
|
|
2
|
+
|
|
3
|
+
Provides commands for database operations, configuration management, and more.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import importlib.metadata
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
import typer
|
|
11
|
+
import urllib3
|
|
12
|
+
|
|
13
|
+
from perspective import config
|
|
14
|
+
from perspective.ingest import ingest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Set the logging level to INFO by default.
|
|
18
|
+
logger.remove()
|
|
19
|
+
logger.add(sys.stdout, level="INFO")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__version__ = importlib.metadata.version("perspective-cli")
|
|
23
|
+
|
|
24
|
+
# Disable warnings related to insecure requests for specific cases
|
|
25
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Create a Typer application with configured properties
|
|
29
|
+
app = typer.Typer(
|
|
30
|
+
name="perspective",
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
pretty_exceptions_enable=True,
|
|
33
|
+
pretty_exceptions_show_locals=False,
|
|
34
|
+
pretty_exceptions_short=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def version_callback(show_version: bool) -> str | None:
|
|
39
|
+
"""Print the version of the application.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
show_version (bool): If True, shows the version.
|
|
43
|
+
"""
|
|
44
|
+
if show_version:
|
|
45
|
+
typer.echo(__version__)
|
|
46
|
+
raise typer.Exit()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.callback()
|
|
50
|
+
def main(
|
|
51
|
+
version: bool = typer.Option(
|
|
52
|
+
None,
|
|
53
|
+
"--version",
|
|
54
|
+
"-v",
|
|
55
|
+
callback=version_callback,
|
|
56
|
+
is_eager=True,
|
|
57
|
+
help="Show the version and exit.",
|
|
58
|
+
),
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Main function for the Typer application.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
version (bool): Flag to show the version and exit.
|
|
64
|
+
config_dir (str): The directory containing the Perspective configuration file.
|
|
65
|
+
"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
app.add_typer(
|
|
70
|
+
config.app, name="config", help="Manage Perspective instance configuration."
|
|
71
|
+
)
|
|
72
|
+
app.add_typer(
|
|
73
|
+
ingest.app, name="ingest", help="Ingest model metadata from various data sources."
|
|
74
|
+
)
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""Pydantic models for Perspective instance configuration."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, EmailStr, field_validator, model_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
allowed_icons = (
|
|
9
|
+
"AcademicCap",
|
|
10
|
+
"AdjustmentsHorizontal",
|
|
11
|
+
"AdjustmentsVertical",
|
|
12
|
+
"ArchiveBox",
|
|
13
|
+
"ArchiveBoxArrowDown",
|
|
14
|
+
"ArchiveBoxXMark",
|
|
15
|
+
"ArrowDown",
|
|
16
|
+
"ArrowDownCircle",
|
|
17
|
+
"ArrowDownLeft",
|
|
18
|
+
"ArrowDownOnSquare",
|
|
19
|
+
"ArrowDownOnSquareStack",
|
|
20
|
+
"ArrowDownRight",
|
|
21
|
+
"ArrowDownTray",
|
|
22
|
+
"ArrowLeft",
|
|
23
|
+
"ArrowLeftCircle",
|
|
24
|
+
"ArrowLeftOnRectangle",
|
|
25
|
+
"ArrowLongDown",
|
|
26
|
+
"ArrowLongLeft",
|
|
27
|
+
"ArrowLongRight",
|
|
28
|
+
"ArrowLongUp",
|
|
29
|
+
"ArrowPath",
|
|
30
|
+
"ArrowPathRoundedSquare",
|
|
31
|
+
"ArrowRight",
|
|
32
|
+
"ArrowRightCircle",
|
|
33
|
+
"ArrowRightOnRectangle",
|
|
34
|
+
"ArrowSmallDown",
|
|
35
|
+
"ArrowSmallLeft",
|
|
36
|
+
"ArrowSmallRight",
|
|
37
|
+
"ArrowSmallUp",
|
|
38
|
+
"ArrowTopRightOnSquare",
|
|
39
|
+
"ArrowTrendingDown",
|
|
40
|
+
"ArrowTrendingUp",
|
|
41
|
+
"ArrowUp",
|
|
42
|
+
"ArrowUpCircle",
|
|
43
|
+
"ArrowUpLeft",
|
|
44
|
+
"ArrowUpOnSquare",
|
|
45
|
+
"ArrowUpOnSquareStack",
|
|
46
|
+
"ArrowUpRight",
|
|
47
|
+
"ArrowUpTray",
|
|
48
|
+
"ArrowUturnDown",
|
|
49
|
+
"ArrowUturnLeft",
|
|
50
|
+
"ArrowUturnRight",
|
|
51
|
+
"ArrowUturnUp",
|
|
52
|
+
"ArrowsPointingIn",
|
|
53
|
+
"ArrowsPointingOut",
|
|
54
|
+
"ArrowsRightLeft",
|
|
55
|
+
"ArrowsUpDown",
|
|
56
|
+
"AtSymbol",
|
|
57
|
+
"Backspace",
|
|
58
|
+
"Backward",
|
|
59
|
+
"Banknotes",
|
|
60
|
+
"Bars2",
|
|
61
|
+
"Bars3",
|
|
62
|
+
"Bars3BottomLeft",
|
|
63
|
+
"Bars3BottomRight",
|
|
64
|
+
"Bars3CenterLeft",
|
|
65
|
+
"Bars4",
|
|
66
|
+
"BarsArrowDown",
|
|
67
|
+
"BarsArrowUp",
|
|
68
|
+
"Battery0",
|
|
69
|
+
"Battery100",
|
|
70
|
+
"Battery50",
|
|
71
|
+
"Beaker",
|
|
72
|
+
"Bell",
|
|
73
|
+
"BellAlert",
|
|
74
|
+
"BellSlash",
|
|
75
|
+
"BellSnooze",
|
|
76
|
+
"Bolt",
|
|
77
|
+
"BoltSlash",
|
|
78
|
+
"BookOpen",
|
|
79
|
+
"Bookmark",
|
|
80
|
+
"BookmarkSlash",
|
|
81
|
+
"BookmarkSquare",
|
|
82
|
+
"Briefcase",
|
|
83
|
+
"BugAnt",
|
|
84
|
+
"BuildingLibrary",
|
|
85
|
+
"BuildingOffice",
|
|
86
|
+
"BuildingOffice2",
|
|
87
|
+
"BuildingStorefront",
|
|
88
|
+
"Cake",
|
|
89
|
+
"Calculator",
|
|
90
|
+
"Calendar",
|
|
91
|
+
"CalendarDays",
|
|
92
|
+
"Camera",
|
|
93
|
+
"ChartBar",
|
|
94
|
+
"ChartBarSquare",
|
|
95
|
+
"ChartPie",
|
|
96
|
+
"ChatBubbleBottomCenter",
|
|
97
|
+
"ChatBubbleBottomCenterText",
|
|
98
|
+
"ChatBubbleLeft",
|
|
99
|
+
"ChatBubbleLeftEllipsis",
|
|
100
|
+
"ChatBubbleLeftRight",
|
|
101
|
+
"ChatBubbleOvalLeft",
|
|
102
|
+
"ChatBubbleOvalLeftEllipsis",
|
|
103
|
+
"Check",
|
|
104
|
+
"CheckBadge",
|
|
105
|
+
"CheckCircle",
|
|
106
|
+
"ChevronDoubleDown",
|
|
107
|
+
"ChevronDoubleLeft",
|
|
108
|
+
"ChevronDoubleRight",
|
|
109
|
+
"ChevronDoubleUp",
|
|
110
|
+
"ChevronDown",
|
|
111
|
+
"ChevronLeft",
|
|
112
|
+
"ChevronRight",
|
|
113
|
+
"ChevronUp",
|
|
114
|
+
"ChevronUpDown",
|
|
115
|
+
"CircleStack",
|
|
116
|
+
"Clipboard",
|
|
117
|
+
"ClipboardDocument",
|
|
118
|
+
"ClipboardDocumentCheck",
|
|
119
|
+
"ClipboardDocumentList",
|
|
120
|
+
"Clock",
|
|
121
|
+
"Cloud",
|
|
122
|
+
"CloudArrowDown",
|
|
123
|
+
"CloudArrowUp",
|
|
124
|
+
"CodeBracket",
|
|
125
|
+
"CodeBracketSquare",
|
|
126
|
+
"Cog",
|
|
127
|
+
"Cog6Tooth",
|
|
128
|
+
"Cog8Tooth",
|
|
129
|
+
"CommandLine",
|
|
130
|
+
"ComputerDesktop",
|
|
131
|
+
"CpuChip",
|
|
132
|
+
"CreditCard",
|
|
133
|
+
"Cube",
|
|
134
|
+
"CubeTransparent",
|
|
135
|
+
"CurrencyBangladeshi",
|
|
136
|
+
"CurrencyDollar",
|
|
137
|
+
"CurrencyEuro",
|
|
138
|
+
"CurrencyPound",
|
|
139
|
+
"CurrencyRupee",
|
|
140
|
+
"CurrencyYen",
|
|
141
|
+
"CursorArrowRays",
|
|
142
|
+
"CursorArrowRipple",
|
|
143
|
+
"DevicePhoneMobile",
|
|
144
|
+
"DeviceTablet",
|
|
145
|
+
"Document",
|
|
146
|
+
"DocumentArrowDown",
|
|
147
|
+
"DocumentArrowUp",
|
|
148
|
+
"DocumentChartBar",
|
|
149
|
+
"DocumentCheck",
|
|
150
|
+
"DocumentDuplicate",
|
|
151
|
+
"DocumentMagnifyingGlass",
|
|
152
|
+
"DocumentMinus",
|
|
153
|
+
"DocumentPlus",
|
|
154
|
+
"DocumentText",
|
|
155
|
+
"EllipsisHorizontal",
|
|
156
|
+
"EllipsisHorizontalCircle",
|
|
157
|
+
"EllipsisVertical",
|
|
158
|
+
"Envelope",
|
|
159
|
+
"EnvelopeOpen",
|
|
160
|
+
"ExclamationCircle",
|
|
161
|
+
"ExclamationTriangle",
|
|
162
|
+
"Eye",
|
|
163
|
+
"EyeDropper",
|
|
164
|
+
"EyeSlash",
|
|
165
|
+
"FaceFrown",
|
|
166
|
+
"FaceSmile",
|
|
167
|
+
"Film",
|
|
168
|
+
"FingerPrint",
|
|
169
|
+
"Fire",
|
|
170
|
+
"Flag",
|
|
171
|
+
"Folder",
|
|
172
|
+
"FolderArrowDown",
|
|
173
|
+
"FolderMinus",
|
|
174
|
+
"FolderOpen",
|
|
175
|
+
"FolderPlus",
|
|
176
|
+
"Forward",
|
|
177
|
+
"Funnel",
|
|
178
|
+
"Gif",
|
|
179
|
+
"Gift",
|
|
180
|
+
"GiftTop",
|
|
181
|
+
"GlobeAlt",
|
|
182
|
+
"GlobeAmericas",
|
|
183
|
+
"GlobeAsiaAustralia",
|
|
184
|
+
"GlobeEuropeAfrica",
|
|
185
|
+
"HandRaised",
|
|
186
|
+
"HandThumbDown",
|
|
187
|
+
"HandThumbUp",
|
|
188
|
+
"Hashtag",
|
|
189
|
+
"Heart",
|
|
190
|
+
"Home",
|
|
191
|
+
"HomeModern",
|
|
192
|
+
"Identification",
|
|
193
|
+
"Inbox",
|
|
194
|
+
"InboxArrowDown",
|
|
195
|
+
"InboxStack",
|
|
196
|
+
"InformationCircle",
|
|
197
|
+
"Key",
|
|
198
|
+
"Language",
|
|
199
|
+
"Lifebuoy",
|
|
200
|
+
"LightBulb",
|
|
201
|
+
"Link",
|
|
202
|
+
"ListBullet",
|
|
203
|
+
"LockClosed",
|
|
204
|
+
"LockOpen",
|
|
205
|
+
"MagnifyingGlass",
|
|
206
|
+
"MagnifyingGlassCircle",
|
|
207
|
+
"MagnifyingGlassMinus",
|
|
208
|
+
"MagnifyingGlassPlus",
|
|
209
|
+
"Map",
|
|
210
|
+
"MapPin",
|
|
211
|
+
"Megaphone",
|
|
212
|
+
"Microphone",
|
|
213
|
+
"Minus",
|
|
214
|
+
"MinusCircle",
|
|
215
|
+
"MinusSmall",
|
|
216
|
+
"Moon",
|
|
217
|
+
"MusicalNote",
|
|
218
|
+
"Newspaper",
|
|
219
|
+
"NoSymbol",
|
|
220
|
+
"PaintBrush",
|
|
221
|
+
"PaperAirplane",
|
|
222
|
+
"PaperClip",
|
|
223
|
+
"Pause",
|
|
224
|
+
"PauseCircle",
|
|
225
|
+
"Pencil",
|
|
226
|
+
"PencilSquare",
|
|
227
|
+
"Phone",
|
|
228
|
+
"PhoneArrowDownLeft",
|
|
229
|
+
"PhoneArrowUpRight",
|
|
230
|
+
"PhoneXMark",
|
|
231
|
+
"Photo",
|
|
232
|
+
"Play",
|
|
233
|
+
"PlayCircle",
|
|
234
|
+
"PlayPause",
|
|
235
|
+
"Plus",
|
|
236
|
+
"PlusCircle",
|
|
237
|
+
"PlusSmall",
|
|
238
|
+
"Power",
|
|
239
|
+
"PresentationChartBar",
|
|
240
|
+
"PresentationChartLine",
|
|
241
|
+
"Printer",
|
|
242
|
+
"PuzzlePiece",
|
|
243
|
+
"QrCode",
|
|
244
|
+
"QuestionMarkCircle",
|
|
245
|
+
"QueueList",
|
|
246
|
+
"Radio",
|
|
247
|
+
"ReceiptPercent",
|
|
248
|
+
"ReceiptRefund",
|
|
249
|
+
"RectangleGroup",
|
|
250
|
+
"RectangleStack",
|
|
251
|
+
"RocketLaunch",
|
|
252
|
+
"Rss",
|
|
253
|
+
"Scale",
|
|
254
|
+
"Scissors",
|
|
255
|
+
"Server",
|
|
256
|
+
"ServerStack",
|
|
257
|
+
"Share",
|
|
258
|
+
"ShieldCheck",
|
|
259
|
+
"ShieldExclamation",
|
|
260
|
+
"ShoppingBag",
|
|
261
|
+
"ShoppingCart",
|
|
262
|
+
"Signal",
|
|
263
|
+
"SignalSlash",
|
|
264
|
+
"Sparkles",
|
|
265
|
+
"SpeakerWave",
|
|
266
|
+
"SpeakerXMark",
|
|
267
|
+
"Square2Stack",
|
|
268
|
+
"Square3Stack3d",
|
|
269
|
+
"Squares2x2",
|
|
270
|
+
"SquaresPlus",
|
|
271
|
+
"Star",
|
|
272
|
+
"Stop",
|
|
273
|
+
"StopCircle",
|
|
274
|
+
"Sun",
|
|
275
|
+
"Swatch",
|
|
276
|
+
"TableCells",
|
|
277
|
+
"Tag",
|
|
278
|
+
"Ticket",
|
|
279
|
+
"Trash",
|
|
280
|
+
"Trophy",
|
|
281
|
+
"Truck",
|
|
282
|
+
"Tv",
|
|
283
|
+
"User",
|
|
284
|
+
"UserCircle",
|
|
285
|
+
"UserGroup",
|
|
286
|
+
"UserMinus",
|
|
287
|
+
"UserPlus",
|
|
288
|
+
"Users",
|
|
289
|
+
"Variable",
|
|
290
|
+
"VideoCamera",
|
|
291
|
+
"VideoCameraSlash",
|
|
292
|
+
"ViewColumns",
|
|
293
|
+
"ViewfinderCircle",
|
|
294
|
+
"Wallet",
|
|
295
|
+
"Wifi",
|
|
296
|
+
"Window",
|
|
297
|
+
"Wrench",
|
|
298
|
+
"WrenchScrewdriver",
|
|
299
|
+
"XCircle",
|
|
300
|
+
"XMark",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class Group(BaseModel):
|
|
305
|
+
"""Represents a group with metadata, slug, labels, and an optional icon.
|
|
306
|
+
|
|
307
|
+
Attributes:
|
|
308
|
+
meta_key (str): A key for metadata associated with the group.
|
|
309
|
+
slug (str): A slug for the group, used in URLs or as an identifier.
|
|
310
|
+
label_plural (str): The plural label for the group.
|
|
311
|
+
label_singular (str): The singular label for the group.
|
|
312
|
+
icon (str, optional): An optional icon for the group. Must be one of the allowed
|
|
313
|
+
icons.
|
|
314
|
+
in_sidebar (bool): Whether the group should be displayed in the sidebar.
|
|
315
|
+
visible (bool): Whether the group should be displayed in Perspective UI.
|
|
316
|
+
|
|
317
|
+
Methods:
|
|
318
|
+
icon_validator: Validates that the provided icon (if any) is in the allowed set.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
meta_key: str
|
|
322
|
+
slug: str
|
|
323
|
+
label_plural: str
|
|
324
|
+
label_singular: str
|
|
325
|
+
icon: str | None = None
|
|
326
|
+
in_sidebar: bool = True
|
|
327
|
+
visible: bool = True
|
|
328
|
+
|
|
329
|
+
@field_validator("icon")
|
|
330
|
+
def icon_validator(cls, v) -> str | None: # noqa: N805, ANN001
|
|
331
|
+
"""Validates the icon attribute.
|
|
332
|
+
|
|
333
|
+
Ensures that if an icon is provided, it is one of the pre-approved icons.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
v (str): The icon to validate.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
str: The validated icon.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
ValueError: If the icon is not in the allowed set.
|
|
343
|
+
"""
|
|
344
|
+
if v is not None and v not in allowed_icons:
|
|
345
|
+
msg = "Icon must be one of the allowed icons."
|
|
346
|
+
raise ValueError(msg)
|
|
347
|
+
return v
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class Owner(BaseModel):
|
|
351
|
+
"""Represents an owner with email, name, and title.
|
|
352
|
+
|
|
353
|
+
Attributes:
|
|
354
|
+
email (EmailStr): The email address of the owner.
|
|
355
|
+
first_name (str): The first name of the owner.
|
|
356
|
+
last_name (str): The last name of the owner.
|
|
357
|
+
title (str): The title of the owner.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
email: EmailStr
|
|
361
|
+
first_name: str
|
|
362
|
+
last_name: str
|
|
363
|
+
title: str
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class MetadataField(BaseModel):
|
|
367
|
+
"""Represents metadata field configuration.
|
|
368
|
+
|
|
369
|
+
Attributes:
|
|
370
|
+
name (str): The name of the field.
|
|
371
|
+
default (str): The default value to use.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
name: str
|
|
375
|
+
default: Any
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class Config(BaseModel):
|
|
379
|
+
"""Configuration model holding information about groups and owners.
|
|
380
|
+
|
|
381
|
+
Attributes:
|
|
382
|
+
groups (list[Group] | None): A list of Group objects. Ensures uniqueness of
|
|
383
|
+
meta_keys and slugs among groups.
|
|
384
|
+
owners (list[Owner] | None): A list of Owner objects.
|
|
385
|
+
|
|
386
|
+
Methods:
|
|
387
|
+
validate_unique: Validates the uniqueness of meta_keys and slugs within the
|
|
388
|
+
groups.
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
groups: list[Group] | None
|
|
392
|
+
owners: list[Owner] | None
|
|
393
|
+
|
|
394
|
+
@model_validator(mode="before")
|
|
395
|
+
@classmethod
|
|
396
|
+
def validate_unique(cls, values: dict) -> dict:
|
|
397
|
+
"""Validates the uniqueness of 'meta_key' and 'slug' for each group.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
values (dict): The values to validate.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
dict: The validated values.
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
ValueError: If 'meta_key' or 'slug' is not unique across all groups.
|
|
407
|
+
"""
|
|
408
|
+
groups = values.get("groups")
|
|
409
|
+
if groups:
|
|
410
|
+
# Check for unique meta_key
|
|
411
|
+
|
|
412
|
+
meta_keys = {group["meta_key"] for group in groups}
|
|
413
|
+
if len(meta_keys) != len(groups):
|
|
414
|
+
msg = "meta_key must be unique for each group."
|
|
415
|
+
raise ValueError(msg)
|
|
416
|
+
|
|
417
|
+
# Check for unique slug
|
|
418
|
+
slugs = {group["slug"] for group in groups}
|
|
419
|
+
if len(slugs) != len(groups):
|
|
420
|
+
msg = "slug must be unique for each group."
|
|
421
|
+
raise ValueError(msg)
|
|
422
|
+
return values
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Pydantic models for dashboard metadata."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DataModel(BaseModel):
|
|
10
|
+
name: str
|
|
11
|
+
schema_field: str = Field(..., alias="schema")
|
|
12
|
+
database: str
|
|
13
|
+
columns: list[dict[str, str]]
|
|
14
|
+
tags: list[dict[str, Any]] = Field(default_factory=list)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DashboardOwner(BaseModel):
|
|
18
|
+
user_id: str # Typically an email or a uuid.
|
|
19
|
+
username: (
|
|
20
|
+
str | None
|
|
21
|
+
) # Typically an email address. Empty in PBI if a group or app is the owner.
|
|
22
|
+
name: str # Typically the human name.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Dashboard(BaseModel):
|
|
26
|
+
external_id: str
|
|
27
|
+
url: str
|
|
28
|
+
type: Literal["powerbi", "qliksense"]
|
|
29
|
+
name: str
|
|
30
|
+
workspace: str
|
|
31
|
+
created_at: datetime | None
|
|
32
|
+
modified_at: datetime
|
|
33
|
+
owners: list[DashboardOwner]
|
|
34
|
+
parent_models: list[DataModel]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DashboardSchemaMetadata(BaseModel):
|
|
38
|
+
schema_field: str = Field(..., alias="schema")
|
|
39
|
+
version: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DashboardManifest(BaseModel):
|
|
43
|
+
metadata: DashboardSchemaMetadata
|
|
44
|
+
payload: list[Dashboard]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Pydantic models for database table metadata."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DatabaseTableColumn(BaseModel):
|
|
7
|
+
name: str
|
|
8
|
+
type: str
|
|
9
|
+
length: str
|
|
10
|
+
description: str | None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DatabaseTable(BaseModel):
|
|
14
|
+
type: str
|
|
15
|
+
name: str
|
|
16
|
+
columns: list[DatabaseTableColumn]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DatabaseTableSchemaMetadata(BaseModel):
|
|
20
|
+
schema_field: str = Field(..., alias="schema")
|
|
21
|
+
version: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DatabaseTableManifest(BaseModel):
|
|
25
|
+
metadata: DatabaseTableSchemaMetadata
|
|
26
|
+
payload: list[DatabaseTable]
|