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.
@@ -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]
@@ -0,0 +1,3 @@
1
+ """Utility functions for the Perspective CLI."""
2
+
3
+ from .utils import * # noqa: F403