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,77 @@
1
+ """Common Typer options."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+
10
+ MetadataDir: Path = typer.Option(
11
+ Path(os.getenv("DBT_PROFILES_DIR", ".")) / "target",
12
+ "--metadata-dir",
13
+ "-m",
14
+ help="Specify the directory with dbt metadata files. Defaults to $DBT_PROFILES_DIR/target or ./target if not set.",
15
+ exists=True,
16
+ dir_okay=True,
17
+ resolve_path=True,
18
+ )
19
+
20
+ ConfigDir: Path = typer.Option(
21
+ "./.perspective",
22
+ "--config-dir",
23
+ "-c",
24
+ help="Specify the directory with the config files. Defaults to ./.perspective",
25
+ envvar="PERSPECTIVE_CONFIG_DIR",
26
+ dir_okay=True,
27
+ resolve_path=True,
28
+ )
29
+
30
+ Force: bool = typer.Option(
31
+ False,
32
+ "--force",
33
+ "-f",
34
+ help="Force the operation.",
35
+ )
36
+
37
+ DryRun: bool = typer.Option(
38
+ False,
39
+ "--dry-run",
40
+ "-D",
41
+ help="Perform a dry run. Print the payload but do not send it.",
42
+ )
43
+
44
+ NoConfig: bool = typer.Option(
45
+ False,
46
+ "--no-config",
47
+ "-n",
48
+ help="Set this flag to prevent sending configuration data along with the request.",
49
+ )
50
+
51
+ Follow: bool = typer.Option(
52
+ False, "--follow", help="Follow the ingestion process until it's completed."
53
+ )
54
+
55
+ IngestionId = Annotated[str, typer.Argument(help="Ingestion ID.")]
56
+
57
+ PerspectiveURL: str = typer.Option(
58
+ "http://localhost:8000/api/v1/",
59
+ "--url",
60
+ "-u",
61
+ help="URL of the Perspective API.",
62
+ envvar="PERSPECTIVE_API_URL",
63
+ )
64
+
65
+ FollowTimeout: int = typer.Option(
66
+ 30,
67
+ "--follow-timeout",
68
+ "-t",
69
+ help="How many seconds to wait for the ingestion process to complete.",
70
+ envvar="PERSPECTIVE_TIMEOUT",
71
+ )
72
+
73
+ IsDbtTestResultsIngestion: bool = typer.Option(
74
+ False,
75
+ "--test-results",
76
+ help="Set this flag to only ingest dbt test results.",
77
+ )
@@ -0,0 +1,274 @@
1
+ """Utility functions."""
2
+
3
+ from enum import Enum
4
+ import json
5
+ from pathlib import Path
6
+ import subprocess # noqa: S404
7
+ from traceback import format_exc
8
+ from urllib.parse import urljoin
9
+
10
+ from requests import Response, request
11
+ from requests.exceptions import ConnectionError as RequestsConnectionError
12
+ from requests.exceptions import Timeout
13
+ from rich import print as rprint
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ import typer
17
+
18
+
19
+ console = Console()
20
+
21
+
22
+ class IngestionStatus(Enum):
23
+ """Ingestion status values."""
24
+
25
+ successful = 0
26
+ failed = 1
27
+ pending = 2
28
+
29
+
30
+ class HttpMethod(str, Enum):
31
+ """HTTP methods."""
32
+
33
+ GET = "GET"
34
+ POST = "POST"
35
+ PUT = "PUT"
36
+ DELETE = "DELETE"
37
+ HEAD = "HEAD"
38
+ OPTIONS = "OPTIONS"
39
+ PATCH = "PATCH"
40
+
41
+
42
+ def safe_json_load(path: str | Path) -> dict | None:
43
+ """Reads a JSON file into a Python dictionary.
44
+
45
+ Args:
46
+ path (str | Path): The path to the JSON file.
47
+
48
+ Returns:
49
+ dict | None: A dictionary representation of the JSON file, or None if an error
50
+ occurs.
51
+ """
52
+ try:
53
+ with Path(path).open("r", encoding="utf-8") as f:
54
+ data = json.load(f)
55
+ except Exception:
56
+ data = None
57
+ return data
58
+
59
+
60
+ def run_command(command: str, capture_output: bool = False) -> str | None:
61
+ """Execute a shell command and optionally capture its output.
62
+
63
+ Args:
64
+ command (str): The shell command to be executed.
65
+ capture_output (bool, optional): Flag to determine if the command's output
66
+ should be captured. Defaults to False.
67
+
68
+ Returns:
69
+ str | None: The standard output of the command if `capture_output` is True,
70
+ otherwise None.
71
+
72
+ Raises:
73
+ typer.Exit: Exits the script if the command execution fails.
74
+ """
75
+ try:
76
+ result = subprocess.run( # noqa: S602
77
+ command,
78
+ shell=True,
79
+ check=True,
80
+ capture_output=True,
81
+ text=True,
82
+ )
83
+ except subprocess.CalledProcessError as e:
84
+ console.print(
85
+ Panel.fit(
86
+ f"[bold red]ERROR[/bold red]: An error occurred while running the command: [bold yellow]{e}[/bold yellow]",
87
+ title="Error",
88
+ border_style="red",
89
+ )
90
+ )
91
+ if e.output:
92
+ console.print(f"[bold cyan]Output[/bold cyan]: {e.output}")
93
+ if e.stderr:
94
+ console.print(f"[bold red]Error[/bold red]: {e.stderr}")
95
+ raise typer.Exit(1) from e
96
+
97
+ if capture_output:
98
+ return result.stdout.strip()
99
+
100
+ return None
101
+
102
+
103
+ def print_response(response: Response) -> None:
104
+ """Print an API response.
105
+
106
+ Args:
107
+ response (requests.Response): The response to print.
108
+ """
109
+ try:
110
+ parsed_response = response.json()
111
+ except Exception:
112
+ parsed_response = None
113
+
114
+ if response.ok:
115
+ msg = "[green]The request was successful.[/green]"
116
+ if parsed_response:
117
+ msg += f"\n[yellow]Response:\n{parsed_response}[/yellow]"
118
+ else:
119
+ msg += f"\n[green]Raw response:\n{response.text}[/green]"
120
+ else:
121
+ if parsed_response:
122
+ error_msg = f"\n\nError message: {parsed_response['message']}"
123
+ else:
124
+ error_msg = f"Raw response:\n{response.text}"
125
+ msg = f"[red]The request failed with status code {response.status_code}.{error_msg}[/red]"
126
+
127
+ rprint(msg)
128
+
129
+ if not response.ok or parsed_response is None:
130
+ raise typer.Exit(1)
131
+
132
+
133
+ def send_request(
134
+ url: str,
135
+ payload: dict | list | None = None,
136
+ verify: bool = True,
137
+ method: HttpMethod = HttpMethod.POST,
138
+ timeout: (float | tuple[float, float] | tuple[float, None]) = (21.05, 60 * 30),
139
+ headers: dict[str, str] | None = None,
140
+ params: dict[str, str | int | float] | None = None,
141
+ return_response_key: str | None = None,
142
+ ) -> Response | tuple[Response, str | None]:
143
+ """Send an HTTP request.
144
+
145
+ Args:
146
+ url (str): The URL for the request.
147
+ payload (dict | list | None): The payload for the request, if any.
148
+ verify (bool, optional): Whether to verify the server's TLS certificate.
149
+ Defaults to True.
150
+ method (HttpMethod, optional): _description_. Defaults to HttpMethod.POST.
151
+ timeout (optional[float | tuple[float, float] | tuple[float, None]]): The
152
+ timeout for the request. Defaults to (20, 60 * 30).
153
+ headers (optional[dict[str, str]], optional): Headers to be sent with the
154
+ request. Defaults to None.
155
+ params (optional[dict[str, str | int | float]]): URL parameters for the request.
156
+ Defaults to None.
157
+ return_response_key (str | None, optional): Key to extract from the response.
158
+ Defaults to None.
159
+
160
+ Raises:
161
+ typer.Exit: In case of timeout or connection error.
162
+
163
+ Returns:
164
+ [requests.Response | tuple[requests.Response, str | None]]: The HTTP response
165
+ and a value extracted from the response (if `return_response_key` is
166
+ provided).
167
+ """
168
+ rprint("[yellow]Sending request to Perspective...[/yellow]")
169
+
170
+ try:
171
+ response = request(
172
+ method=method,
173
+ url=url,
174
+ headers=headers,
175
+ params=params,
176
+ json=payload,
177
+ verify=verify,
178
+ timeout=timeout,
179
+ )
180
+ except (Timeout, RequestsConnectionError) as e:
181
+ error_message = (
182
+ "The request has failed. Please check your connection and try again."
183
+ )
184
+ if isinstance(e, Timeout):
185
+ error_message += " If you're using a VPN, ensure it's properly connected or try disabling it temporarily."
186
+ else:
187
+ error_message += " This could be due to maximum retries being exceeded or failure to establish a new connection. Please check your network configuration."
188
+
189
+ rprint(Panel(f"[red]{error_message}[/red]"))
190
+
191
+ # Print the traceback
192
+ traceback_info = format_exc()
193
+ rprint(Panel(f"[red]{traceback_info}[/red]"))
194
+
195
+ raise typer.Exit(1) from e
196
+
197
+ if not response.ok:
198
+ rprint(
199
+ Panel(
200
+ f"[red]The request to {url} FAILED with status: {response.status_code} ({response.reason})[/red]"
201
+ )
202
+ )
203
+ raise typer.Exit(1)
204
+
205
+ print_response(response)
206
+
207
+ if return_response_key:
208
+ extracted_value = response.json().get(return_response_key)
209
+ return response, extracted_value
210
+ return response
211
+
212
+
213
+ def check_ingestion_status(
214
+ perspective_url: str, ingestion_uuid: str, verify: bool = True
215
+ ) -> str:
216
+ """Fetches the status for a specific ingestion ID from Perspective.
217
+
218
+ Args:
219
+ perspective_url (str): The URL of the Perspective instance.
220
+ ingestion_uuid (str): The ingestion ID to fetch the status for.
221
+ verify (bool): Whether to verify the server's TLS certificate. Defaults to True.
222
+
223
+ Returns:
224
+ str: The status of the ingestion process.
225
+ """
226
+ status_endpoint = urljoin(perspective_url, "catalog/ingestions/")
227
+ response = send_request(
228
+ method="GET",
229
+ url=status_endpoint,
230
+ params={"uuid": ingestion_uuid},
231
+ verify=verify,
232
+ )
233
+ ingestion = response.json().get("data")
234
+ return ingestion.get("status")
235
+
236
+
237
+ def check_ingestion_results(
238
+ perspective_url: str, ingestion_uuid: str, verify: bool = True
239
+ ) -> str | dict:
240
+ """Fetche and interpret the results for a specific ingestion ID.
241
+
242
+ Args:
243
+ perspective_url (str): The URL of the Perspective instance.
244
+ ingestion_uuid (str): The ingestion ID to check the results for.
245
+ verify (bool): Whether to verify the server's TLS certificate. Defaults to True.
246
+
247
+ Returns:
248
+ str | dict: A message describing the status of the ingestion process or
249
+ the JSON response for successful completions.
250
+ """
251
+ status_endpoint = urljoin(perspective_url, "catalog/ingestions/")
252
+ response = send_request(
253
+ method="GET",
254
+ url=status_endpoint,
255
+ params={"uuid": ingestion_uuid},
256
+ verify=verify,
257
+ )
258
+ ingestion = response.json().get("data")
259
+ status = ingestion.get("status")
260
+
261
+ if status == IngestionStatus.pending.value:
262
+ return f"Ingestion ID {ingestion_uuid} is still pending."
263
+
264
+ if status == IngestionStatus.failed.value:
265
+ error_details = ingestion.get("error", "No additional error details provided.")
266
+ return (
267
+ f"Ingestion ID {ingestion_uuid} has failed. Error details: {error_details}"
268
+ )
269
+
270
+ if status == IngestionStatus.successful.value:
271
+ # Return the entire JSON response for successful completions
272
+ return ingestion.get("summary")
273
+
274
+ return f"Unrecognized status for ingestion ID {ingestion_uuid}: {status}"
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: perspective-cli
3
+ Version: 0.1.0
4
+ Summary: A CLI tool for managing the Perspective AI platform.
5
+ Author-email: Michal Zawadzki <mzawadzki@dyvenia.com>
6
+ Keywords: cli,dbt,perspective,data,catalog,ai
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: typer[all]<1.0,>=0.9
15
+ Requires-Dist: psycopg[binary]>=3.2.3
16
+ Requires-Dist: requests<3.0,>=2.20
17
+ Requires-Dist: pyyaml>=6.0.1
18
+ Requires-Dist: rich<13.8,>=13.7
19
+ Requires-Dist: loguru>=0.7.2
20
+ Requires-Dist: websocket-client>=1.8.0
21
+ Requires-Dist: requests-ntlm>=1.3.0
22
+ Requires-Dist: websockets>=10.4
23
+ Requires-Dist: pydantic[email]==2.11.7
24
+ Requires-Dist: azure-identity>=1.17.1
25
+ Requires-Dist: dlt[duckdb]>=1.11.0
26
+ Requires-Dist: duckdb>1.1.3
27
+ Requires-Dist: dbt-artifacts-parser<1.0,>=0.12.0
28
+ Provides-Extra: sap
29
+ Requires-Dist: pyrfc==2.5.0; extra == "sap"
30
+
31
+ # perspective-cli
32
+
33
+ ## Installation
34
+
35
+ ### `pip`
36
+
37
+ ```bash
38
+ pip install perspective-cli
39
+ ```
40
+
41
+ ### `uv`
42
+
43
+ ```bash
44
+ uv add perspective-cli
45
+ ```
46
+
47
+ ## Next Steps
48
+
49
+ Proceed to the [official documentation](dev.meetperspective.com/docs/catalog/) for next steps and detailed guides on how to utilize `perspective-cli` effectively.
@@ -0,0 +1,29 @@
1
+ perspective/__init__.py,sha256=RMqGNAtJDIHYnlTfMKR_Zljb9KV_ZAT714yhQ1jdRFM,23
2
+ perspective/config.py,sha256=0oBT522vbSLxyyGiJLTf5Qrhx8Yi5DbwPZwnT2ZBo4Q,7595
3
+ perspective/exceptions.py,sha256=awwIcnVIAqXPX5W8eMM0zREW8FJOdlqj29mJ7lkDNM0,426
4
+ perspective/main.py,sha256=1muq6dEnJt0h5loUxCPCNpkEfAiajTaSSOwcwEIo9gU,1765
5
+ perspective/ingest/dbt.py,sha256=Bn-vMJnOicENd5jAYr-tRjtJ9jWdUo6Ddu49gV-cA3c,5188
6
+ perspective/ingest/ingest.py,sha256=3n7ffsQBnskOoRhvHK5Iy06dlvKbDVrt5mIIPRplhfk,5034
7
+ perspective/ingest/postgres.py,sha256=m9QStEBbgSsEBmn7gEJQ5-Oc1ydstj3rG_lF1Xiu908,12248
8
+ perspective/ingest/sources/bi/powerbi/extract.py,sha256=m8eLA9fY7UT4xbdge0s8i1kXM3hjuj67XWaGNuWl2qo,6705
9
+ perspective/ingest/sources/bi/powerbi/models.py,sha256=v3Gcd0SjHvYif91ACUeCIpLz3_KUOFHyAKcq7zdS41A,2854
10
+ perspective/ingest/sources/bi/powerbi/pipeline.py,sha256=46lCTKAXtrcemIMfeG_1ul97jnetgSOcnyTjSvYwbbk,1002
11
+ perspective/ingest/sources/bi/powerbi/transform.py,sha256=kytJakFbCjZWz_qfG8_pGbRptvv3PetmbB2W-8FZCoI,17817
12
+ perspective/ingest/sources/bi/qlik_sense/extract.py,sha256=2FvQ9HGJZO1gMkBIFvBTOld4N9AXLfKjczIeakS5qls,10676
13
+ perspective/ingest/sources/bi/qlik_sense/models.py,sha256=dxhjAFzWhkx5vG3MQWhu0oaQAU9BlPTx89vmWiFpvb8,412
14
+ perspective/ingest/sources/bi/qlik_sense/pipeline.py,sha256=fa_aClpLbeOnh-z0YlO9rs8bvo0rqzrOjQ8N7UDedNw,522
15
+ perspective/ingest/sources/bi/qlik_sense/transform.py,sha256=oU8NbJUrnAZjSX1Vej1aRknm82FIc8eac60F9IfrHMw,2392
16
+ perspective/ingest/sources/database/sap/extract.py,sha256=oQGoB-I_fxa4Mbs3Fk-PTF65NwYZg9vr3l2-XwBGa1U,9063
17
+ perspective/ingest/sources/database/sap/pipeline.py,sha256=w-ExBhMAcXuySdEFs_1Z3pNBxmAGfrU_FYM15wo8F5A,810
18
+ perspective/ingest/sources/database/sap/transform.py,sha256=4KCurK-NrlPCqK4Wv52eSfTkSBN3lWlf_9cGqB_0kao,2745
19
+ perspective/models/configs.py,sha256=t5GJ0QORG6wlKyF2saGJyzE3H9E6psDXJ_6UwsDFSTw,9420
20
+ perspective/models/dashboards.py,sha256=8wcg-YfymPP_ErSbjvUgKWPju2PktjvZMShdmkL6APU,1084
21
+ perspective/models/databases.py,sha256=XLDZ29hLhrCVUMmnbodv1quNcuRcLeexiR_tZkxnCeM,536
22
+ perspective/utils/__init__.py,sha256=8PP1VNUc3DkZBgadUEvImVpuYUrAigkD7aYRCmSZ1l4,85
23
+ perspective/utils/options.py,sha256=a8eVilistizMOxFFOPpW8TKNvi5kaZoxUw67nzFbs8w,1751
24
+ perspective/utils/utils.py,sha256=CWUsV8Zp9dZJEiKSS1Hbu_Gxf6B_kCMT_NWlQ6L3jrU,8760
25
+ perspective_cli-0.1.0.dist-info/METADATA,sha256=TiO7EaOAWV3Qt2mZGUFpe1A6J2ep5Aha3bMQQombYho,1398
26
+ perspective_cli-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
27
+ perspective_cli-0.1.0.dist-info/entry_points.txt,sha256=FyAeBznbZy1x_I_ZMg3pB7TPOJ1oZRT3RHKho7Mr7mk,53
28
+ perspective_cli-0.1.0.dist-info/top_level.txt,sha256=-W1-t6tcSPy7mH-EhKzt_SJAICr8eZmyBwC3Eu5kqkU,12
29
+ perspective_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ perspective = perspective.main:app
@@ -0,0 +1 @@
1
+ perspective