ullr-data-cli 1.0.0__tar.gz

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,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: ullr-data-cli
3
+ Version: 1.0.0
4
+ Summary: An offline, AI-free Data Analytics and Dashboard Auditing CLI
5
+ Home-page: https://github.com/yourusername/ullr-data-cli
6
+ Author: jshlydnzl
7
+ Author-email: jshlydnzl@users.noreply.github.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: rich
16
+ Requires-Dist: pandas
17
+ Requires-Dist: openpyxl
18
+ Dynamic: author
19
+ Dynamic: author-email
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: requires-dist
25
+ Dynamic: requires-python
26
+ Dynamic: summary
27
+
28
+ Ullr is a pure Python Data Engine that automatically audits CSV/Excel files for data quality (ghost data, invisible spaces, duplicates) and generates Dynamic Dashboard Blueprints for Excel, Power BI, Tableau, and Looker Studio.
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,31 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name='ullr-data-cli',
5
+ version='1.0.0',
6
+ author='jshlydnzl',
7
+ author_email='jshlydnzl@users.noreply.github.com',
8
+ description='An offline, AI-free Data Analytics and Dashboard Auditing CLI',
9
+ long_description='Ullr is a pure Python Data Engine that automatically audits CSV/Excel files for data quality (ghost data, invisible spaces, duplicates) and generates Dynamic Dashboard Blueprints for Excel, Power BI, Tableau, and Looker Studio.',
10
+ long_description_content_type='text/markdown',
11
+ url='https://github.com/yourusername/ullr-data-cli',
12
+ packages=find_packages(),
13
+ install_requires=[
14
+ 'rich',
15
+ 'pandas',
16
+ 'openpyxl'
17
+ ],
18
+ classifiers=[
19
+ 'Programming Language :: Python :: 3',
20
+ 'License :: OSI Approved :: MIT License',
21
+ 'Operating System :: OS Independent',
22
+ 'Intended Audience :: Developers',
23
+ 'Topic :: Scientific/Engineering :: Information Analysis',
24
+ ],
25
+ python_requires='>=3.7',
26
+ entry_points={
27
+ 'console_scripts': [
28
+ 'ullr=ullr.main:cli',
29
+ ],
30
+ },
31
+ )
@@ -0,0 +1 @@
1
+ # This file makes the folder a Python package
@@ -0,0 +1,369 @@
1
+ import argparse
2
+ import pandas as pd
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from rich.panel import Panel
6
+ import os
7
+ import sys
8
+ import time
9
+
10
+ console = Console()
11
+
12
+ def print_banner():
13
+ banner = """
14
+ ██╗ ██╗██╗ ██╗ ██████╗
15
+ ██║ ██║██║ ██║ ██╔══██╗
16
+ ██║ ██║██║ ██║ ██████╔╝
17
+ ██║ ██║██║ ██║ ██╔══██╗
18
+ ╚██████╔╝███████╗███████╗██║ ██║
19
+ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝
20
+ The Offline Data Auditor
21
+ """
22
+ console.print(banner, style="bold cyan")
23
+
24
+ def find_file_globally(filename):
25
+ home_dir = os.path.expanduser('~')
26
+
27
+ with console.status(f"[yellow]Scanning your computer for '{filename}'...[/]", spinner="dots"):
28
+ for root, dirs, files in os.walk(home_dir):
29
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
30
+ if filename in files:
31
+ full_path = os.path.join(root, filename)
32
+ console.print(f"[bold green]Found it at:[/] {full_path}\n")
33
+ return full_path
34
+
35
+ return None
36
+
37
+ def load_dataframe(filepath):
38
+ """Smart loader that handles CSVs and multi-tab Excel files."""
39
+ try:
40
+ if filepath.lower().endswith('.csv'):
41
+ with console.status(f"[cyan]● Loading '{os.path.basename(filepath)}'...[/]", spinner="bouncingBar"):
42
+ time.sleep(0.6)
43
+ return pd.read_csv(filepath)
44
+ elif filepath.lower().endswith(('.xlsx', '.xls')):
45
+ xls = pd.ExcelFile(filepath)
46
+ sheet_names = xls.sheet_names
47
+
48
+ if len(sheet_names) == 1:
49
+ with console.status(f"[cyan]● Loading '{os.path.basename(filepath)}'...[/]", spinner="bouncingBar"):
50
+ time.sleep(0.6)
51
+ return pd.read_excel(xls, sheet_name=sheet_names[0])
52
+
53
+ from rich.prompt import Prompt
54
+ console.print(f"\n[bold cyan]📂 Multiple tabs detected in this Excel file![/]")
55
+ for i, sheet in enumerate(sheet_names):
56
+ console.print(f" [bold green]{i+1}.[/] {sheet}")
57
+
58
+ choices = [str(i+1) for i in range(len(sheet_names))]
59
+ choice = Prompt.ask("\nWhich tab would you like to load?", choices=choices, default="1")
60
+ selected_sheet = sheet_names[int(choice)-1]
61
+
62
+ with console.status(f"[cyan]● Loading tab: '{selected_sheet}'...[/]", spinner="bouncingBar"):
63
+ time.sleep(0.6)
64
+ return pd.read_excel(xls, sheet_name=selected_sheet)
65
+ else:
66
+ console.print(f"[bold red]Error:[/] Unsupported file format. Please provide a .csv or .xlsx file.")
67
+ return None
68
+ except Exception as e:
69
+ console.print(f"[bold red]Error reading file:[/] {e}")
70
+ return None
71
+
72
+ def audit_data(filepath):
73
+ if not os.path.exists(filepath):
74
+ found_path = find_file_globally(os.path.basename(filepath))
75
+ if found_path:
76
+ filepath = found_path
77
+ else:
78
+ console.print(f"[bold red]Error:[/] Could not find '{os.path.basename(filepath)}' anywhere on your computer.")
79
+ return
80
+
81
+ df = load_dataframe(filepath)
82
+ if df is None:
83
+ return
84
+
85
+ rows, cols = df.shape
86
+ console.print(f"\n[bold green]✅ Successfully loaded![/] {rows:,} rows, {cols} columns.\n")
87
+
88
+ # Separate real data columns from 'Unnamed' ghost columns
89
+ real_cols = [col for col in df.columns if not str(col).lower().startswith('unnamed')]
90
+ unnamed_cols = [col for col in df.columns if str(col).lower().startswith('unnamed')]
91
+
92
+ # 1. Check Missing Values (Only on real columns!)
93
+ missing = df[real_cols].isnull().sum()
94
+ actual_missing_cols = missing[missing > 0]
95
+
96
+ # 2. Check Duplicates
97
+ duplicates = df.duplicated().sum()
98
+
99
+ # 3. Check for Invisible Spaces (Messy Text)
100
+ text_cols = df[real_cols].select_dtypes(include=['object']).columns
101
+ space_issues = {}
102
+ dirty_numbers = {}
103
+
104
+ for col in text_cols:
105
+ # Find leading/trailing spaces
106
+ mask = df[col].notna() & df[col].astype(str).str.contains(r'^\s+|\s+$', regex=True)
107
+ spaces = mask.sum()
108
+ if spaces > 0:
109
+ space_issues[col] = spaces
110
+
111
+ # Find numbers trapped as text (like $1,000)
112
+ curr_mask = df[col].notna() & df[col].astype(str).str.contains(r'[\$£€,]', regex=True) & df[col].astype(str).str.contains(r'\d', regex=True)
113
+ dirty_num = curr_mask.sum()
114
+ if dirty_num > 0:
115
+ dirty_numbers[col] = dirty_num
116
+
117
+ # Build Output Panel
118
+ console.print("[bold yellow]🩺 DATA HEALTH CHECK REPORT[/]")
119
+ console.print("Here is what needs to be cleaned up before you can use this data:\n")
120
+
121
+ table = Table(show_header=True, header_style="bold magenta")
122
+ table.add_column("What we checked")
123
+ table.add_column("What we found")
124
+ table.add_column("Next Steps")
125
+
126
+ if duplicates > 0:
127
+ table.add_row("Copy-Paste Errors (Duplicates)", f"{duplicates:,} exact duplicate rows", "[bold red]Delete duplicates in Excel[/]")
128
+ else:
129
+ table.add_row("Copy-Paste Errors (Duplicates)", "0 duplicate rows", "[bold green]Looks Good![/]")
130
+
131
+ if not actual_missing_cols.empty:
132
+ table.add_row("Blank Cells (Missing Data)", f"Found in {len(actual_missing_cols)} columns", "[bold red]Fill in or remove blanks[/]")
133
+ else:
134
+ table.add_row("Blank Cells (Missing Data)", "0 blank cells", "[bold green]Looks Good![/]")
135
+
136
+ if space_issues:
137
+ table.add_row("Invisible Spaces (Messy Text)", f"Found in {len(space_issues)} columns", "[bold red]Use TRIM() in Excel[/]")
138
+ else:
139
+ table.add_row("Invisible Spaces (Messy Text)", "0 messy text cells", "[bold green]Looks Good![/]")
140
+
141
+ if dirty_numbers:
142
+ table.add_row("Numbers Trapped as Text", f"Found in {len(dirty_numbers)} columns", "[bold red]Remove $ or commas[/]")
143
+ else:
144
+ table.add_row("Numbers Trapped as Text", "0 trapped numbers", "[bold green]Looks Good![/]")
145
+
146
+ console.print(table)
147
+
148
+ if not actual_missing_cols.empty:
149
+ console.print("\n[bold red]⚠️ Where to find the Blank Cells:[/]")
150
+ for col, count in actual_missing_cols.items():
151
+ console.print(f" - Column [cyan]{col}[/]: {count:,} empty cells")
152
+
153
+ if space_issues:
154
+ console.print("\n[bold red]⚠️ Where to find Invisible Spaces:[/]")
155
+ for col, count in space_issues.items():
156
+ console.print(f" - Column [cyan]{col}[/]: {count:,} cells have hidden spaces.")
157
+
158
+ if dirty_numbers:
159
+ console.print("\n[bold red]⚠️ Where to find Trapped Numbers:[/]")
160
+ for col, count in dirty_numbers.items():
161
+ console.print(f" - Column [cyan]{col}[/]: {count:,} cells have $ or commas making them text.")
162
+
163
+ if unnamed_cols:
164
+ console.print(f"\n[bold yellow]👻 Note: We ignored {len(unnamed_cols)} 'Unnamed' columns.[/]")
165
+ console.print("Excel sometimes creates invisible columns if you have a floating summary table off to the right side of your data. We filtered them out to keep this report clean!")
166
+
167
+ if actual_missing_cols.empty and duplicates == 0 and not space_issues and not dirty_numbers:
168
+ console.print("\n[bold green]🎉 AMAZING! Your actual data is 100% clean and ready for analysis![/]")
169
+
170
+ def analyze_data(filepath):
171
+ if not os.path.exists(filepath):
172
+ found_path = find_file_globally(os.path.basename(filepath))
173
+ if found_path:
174
+ filepath = found_path
175
+ else:
176
+ console.print(f"[bold red]Error:[/] Could not find '{os.path.basename(filepath)}' anywhere on your computer.")
177
+ return
178
+
179
+ df = load_dataframe(filepath)
180
+ if df is None:
181
+ return
182
+
183
+ if df.empty:
184
+ console.print("[bold red]Data is empty![/]")
185
+ return
186
+
187
+ # Filter out Ghost Data for analysis
188
+ real_cols = [col for col in df.columns if not str(col).lower().startswith('unnamed')]
189
+ numeric_cols = df[real_cols].select_dtypes(include=['number']).columns.tolist()
190
+ text_cols = df[real_cols].select_dtypes(include=['object', 'category']).columns.tolist()
191
+ date_cols = [col for col in real_cols if 'date' in col.lower() or 'time' in col.lower() or 'year' in col.lower()]
192
+
193
+ console.print("\n[bold yellow]📊 FULL DASHBOARD BLUEPRINT[/]\n")
194
+
195
+ # 1. Executive Summary
196
+ console.print("[bold cyan]🧠 Executive Summary[/]")
197
+ console.print(f" - We analyzed [bold]{len(df):,}[/] rows of clean data.")
198
+
199
+ if numeric_cols:
200
+ primary_num = numeric_cols[0]
201
+ console.print(f" - Your total [bold cyan]{primary_num}[/] generated is [bold green]{df[primary_num].sum():,.2f}[/].")
202
+
203
+ if text_cols:
204
+ # Find a categorical column that has a few distinct groups (like Price_Tier)
205
+ good_cats = [col for col in text_cols if 1 < df[col].nunique() <= 10]
206
+ if good_cats:
207
+ best_cat = good_cats[0]
208
+ top_val = df[best_cat].value_counts().index[0]
209
+ top_pct = (df[best_cat].value_counts().iloc[0] / len(df)) * 100
210
+ console.print(f" - [bold cyan]{top_val}[/] is your dominant {best_cat}, making up [bold green]{top_pct:.1f}%[/] of the entire dataset.")
211
+ console.print("")
212
+
213
+ # 2. Key Metrics
214
+ if numeric_cols:
215
+ console.print("[bold cyan]💰 Money & Numbers (Totals and Averages)[/]")
216
+ num_table = Table(show_header=True, header_style="bold magenta")
217
+ num_table.add_column("Data Column")
218
+ num_table.add_column("Grand Total")
219
+ num_table.add_column("Average (Mean)")
220
+ num_table.add_column("Lowest Value")
221
+ num_table.add_column("Highest Value")
222
+
223
+ for col in numeric_cols:
224
+ if "id" not in col.lower() and "zip" not in col.lower():
225
+ total = f"{df[col].sum():,.2f}"
226
+ avg = f"{df[col].mean():,.2f}"
227
+ min_val = f"{df[col].min():,.2f}"
228
+ max_val = f"{df[col].max():,.2f}"
229
+ num_table.add_row(col, total, avg, min_val, max_val)
230
+ console.print(num_table)
231
+ console.print("")
232
+
233
+ # 3. Dynamic Dashboard Layout
234
+ from rich.prompt import Prompt
235
+ console.print("\n[bold cyan]🛠️ Dashboard Construction[/]")
236
+ console.print("I can provide step-by-step instructions for building this dashboard.")
237
+ console.print(" [1] Microsoft Excel")
238
+ console.print(" [2] Tableau")
239
+ console.print(" [3] Power BI")
240
+ console.print(" [4] Google Looker Studio")
241
+ bi_choice = Prompt.ask("Select your BI Tool", choices=["1", "2", "3", "4"], default="1")
242
+
243
+ bi_name = {"1": "Excel", "2": "Tableau", "3": "Power BI", "4": "Looker Studio"}[bi_choice]
244
+ console.print(f"\n[bold yellow]🏗️ Recommended Dashboard Layout (How to build in {bi_name}):[/]")
245
+
246
+ if numeric_cols:
247
+ kpis = ", ".join([f"Total {c}" for c in numeric_cols[:2]])
248
+ console.print(f" [bold green]Top (KPI Cards):[/] Put large, bold text showing {kpis} at the very top of your dashboard.")
249
+ if bi_choice == "1":
250
+ console.print(f" [dim]↳ Steps: Insert Shapes (Rounded Rectangles) -> Click formula bar -> Type '=' and click the Grand Total in your Pivot Table.[/dim]")
251
+ elif bi_choice == "2":
252
+ console.print(f" [dim]↳ Steps: Create New Sheet -> Drag your field to the 'Text' box on the Marks card -> Format font size.[/dim]")
253
+ elif bi_choice == "3":
254
+ console.print(f" [dim]↳ Steps: Visualizations Pane -> Click 'Card' visual -> Drag your numeric field into the 'Fields' bucket.[/dim]")
255
+ elif bi_choice == "4":
256
+ console.print(f" [dim]↳ Steps: Click 'Add a chart' -> Select 'Scorecard' -> Drag your numeric field into the 'Metric' section.[/dim]")
257
+
258
+ if date_cols and numeric_cols:
259
+ console.print(f" [bold cyan]Center (Line Chart):[/] Show the trend of [bold]{numeric_cols[0]}[/] over [bold]{date_cols[0]}[/] to track growth over time.")
260
+ if bi_choice == "1":
261
+ console.print(f" [dim]↳ Steps: Insert PivotChart (Line) -> Drag '{date_cols[0]}' to Axis (Categories) -> Drag '{numeric_cols[0]}' to Values.[/dim]")
262
+ elif bi_choice == "2":
263
+ console.print(f" [dim]↳ Steps: Columns Shelf: '{date_cols[0]}' (Continuous) -> Rows Shelf: '{numeric_cols[0]}'.[/dim]")
264
+ elif bi_choice == "3":
265
+ console.print(f" [dim]↳ Steps: Visualizations Pane -> Click 'Line chart' -> X-axis: '{date_cols[0]}', Y-axis: '{numeric_cols[0]}'.[/dim]")
266
+ elif bi_choice == "4":
267
+ console.print(f" [dim]↳ Steps: Click 'Add a chart' -> Select 'Time series chart' -> Dimension: '{date_cols[0]}', Metric: '{numeric_cols[0]}'.[/dim]")
268
+ elif numeric_cols and text_cols:
269
+ main_cats = [c for c in text_cols if df[c].nunique() <= 15]
270
+ if main_cats:
271
+ console.print(f" [bold cyan]Center (Column Chart):[/] Show the highest performing [bold]{main_cats[0]}[/] categories based on [bold]{numeric_cols[0]}[/].")
272
+ if bi_choice == "1":
273
+ console.print(f" [dim]↳ Steps: Insert PivotChart (Column) -> Drag '{main_cats[0]}' to Axis (Categories) -> Drag '{numeric_cols[0]}' to Values.[/dim]")
274
+ elif bi_choice == "2":
275
+ console.print(f" [dim]↳ Steps: Columns Shelf: '{main_cats[0]}' -> Rows Shelf: '{numeric_cols[0]}' -> Click the Sort icon.[/dim]")
276
+ elif bi_choice == "3":
277
+ console.print(f" [dim]↳ Steps: Visualizations Pane -> Click 'Clustered column chart' -> X-axis: '{main_cats[0]}', Y-axis: '{numeric_cols[0]}'.[/dim]")
278
+ elif bi_choice == "4":
279
+ console.print(f" [dim]↳ Steps: Click 'Add a chart' -> Select 'Column chart' -> Dimension: '{main_cats[0]}', Metric: '{numeric_cols[0]}'.[/dim]")
280
+
281
+ if text_cols:
282
+ small_cats = [c for c in text_cols if 1 < df[c].nunique() <= 5]
283
+ if small_cats:
284
+ console.print(f" [bold magenta]Bottom Left (Donut Chart):[/] Break down the percentage share of [bold]{small_cats[0]}[/].")
285
+ if bi_choice == "1":
286
+ console.print(f" [dim]↳ Steps: Insert PivotChart (Pie/Donut) -> Drag '{small_cats[0]}' to Axis -> Drag '{small_cats[0]}' to Values (Count).[/dim]")
287
+ elif bi_choice == "2":
288
+ console.print(f" [dim]↳ Steps: Marks Card: Select 'Pie' -> Drag '{small_cats[0]}' to Color -> Drag '{small_cats[0]}' (Count) to Angle.[/dim]")
289
+ elif bi_choice == "3":
290
+ console.print(f" [dim]↳ Steps: Visualizations Pane -> Click 'Donut chart' -> Legend: '{small_cats[0]}', Values: '{small_cats[0]}'.[/dim]")
291
+ elif bi_choice == "4":
292
+ console.print(f" [dim]↳ Steps: Click 'Add a chart' -> Select 'Donut chart' -> Dimension: '{small_cats[0]}', Metric: Record Count.[/dim]")
293
+
294
+ med_cats = [c for c in text_cols if 5 < df[c].nunique() <= 15]
295
+ if med_cats and numeric_cols:
296
+ console.print(f" [bold magenta]Bottom Right (Bar Chart):[/] Rank the top [bold]{med_cats[0]}[/] by [bold]{numeric_cols[0]}[/].")
297
+ if bi_choice == "1":
298
+ console.print(f" [dim]↳ Steps: Insert PivotChart (Bar) -> Drag '{med_cats[0]}' to Axis -> Drag '{numeric_cols[0]}' to Values -> Right-click chart and Sort.[/dim]")
299
+ elif bi_choice == "2":
300
+ console.print(f" [dim]↳ Steps: Columns Shelf: '{numeric_cols[0]}' -> Rows Shelf: '{med_cats[0]}' -> Click the Sort Descending icon.[/dim]")
301
+ elif bi_choice == "3":
302
+ console.print(f" [dim]↳ Steps: Visualizations Pane -> Click 'Clustered bar chart' -> Y-axis: '{med_cats[0]}', X-axis: '{numeric_cols[0]}'.[/dim]")
303
+ elif bi_choice == "4":
304
+ console.print(f" [dim]↳ Steps: Click 'Add a chart' -> Select 'Bar chart' -> Dimension: '{med_cats[0]}', Metric: '{numeric_cols[0]}'.[/dim]")
305
+
306
+ if text_cols:
307
+ valid_slicers = [f'[bold]{c}[/]' for c in text_cols if df[c].nunique() <= 20][:3]
308
+ if valid_slicers:
309
+ console.print(f" [bold blue]Left Sidebar (Slicers):[/] Add clickable filters for {', '.join(valid_slicers)} so users can drill down into the data.")
310
+ if bi_choice == "1":
311
+ console.print(f" [dim]↳ Steps: Click any PivotChart -> PivotChart Analyze Ribbon -> Insert Slicer -> Check boxes for these columns.[/dim]")
312
+ elif bi_choice == "2":
313
+ console.print(f" [dim]↳ Steps: Drag your field to the Filters shelf -> Right-click the pill -> Select 'Show Filter'.[/dim]")
314
+ elif bi_choice == "3":
315
+ console.print(f" [dim]↳ Steps: Visualizations Pane -> Click 'Slicer' -> Drag your field into the Field bucket.[/dim]")
316
+ elif bi_choice == "4":
317
+ console.print(f" [dim]↳ Steps: Click 'Add a control' -> Select 'Drop-down list' -> Control field: your category.[/dim]")
318
+ console.print("")
319
+
320
+ def interactive_mode():
321
+ from rich.prompt import Prompt
322
+
323
+ print_banner()
324
+ console.print("[bold green]Welcome to the Ullr Data Engine![/]\n")
325
+
326
+ while True:
327
+ console.print("[bold cyan]What would you like to do?[/]")
328
+ console.print(" [bold green]1.[/] Audit raw data (Data Quality Check)")
329
+ console.print(" [bold blue]2.[/] Analyze cleaned data (Dashboard Blueprint)")
330
+ console.print(" [bold red]3.[/] Exit")
331
+
332
+ choice = Prompt.ask("\nSelect an option", choices=["1", "2", "3"], default="1")
333
+
334
+ if choice == "3":
335
+ console.print("[yellow]Goodbye![/]")
336
+ break
337
+ elif choice == "1":
338
+ filepath = Prompt.ask("\n[bold yellow]Enter the filename or path to audit (e.g., data.csv)[/]")
339
+ audit_data(filepath.strip())
340
+ elif choice == "2":
341
+ filepath = Prompt.ask("\n[bold yellow]Enter the filename or path to analyze (e.g., data.csv)[/]")
342
+ analyze_data(filepath.strip())
343
+
344
+ console.print("\n" + "-"*50 + "\n")
345
+
346
+ def cli():
347
+ parser = argparse.ArgumentParser(description="Ullr: The Offline Data Auditor")
348
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
349
+
350
+ audit_parser = subparsers.add_parser("audit", help="Check raw data for flaws (NULLs, duplicates).")
351
+ audit_parser.add_argument("file", help="Path to the CSV file")
352
+
353
+ analyze_parser = subparsers.add_parser("analyze", help="Generate dashboard blueprints for cleaned data.")
354
+ analyze_parser.add_argument("file", help="Path to the cleaned CSV file")
355
+
356
+ args = parser.parse_args()
357
+
358
+ if not args.command:
359
+ interactive_mode()
360
+ sys.exit(0)
361
+
362
+ print_banner()
363
+ if args.command == "audit":
364
+ audit_data(args.file)
365
+ elif args.command == "analyze":
366
+ analyze_data(args.file)
367
+
368
+ if __name__ == "__main__":
369
+ cli()
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: ullr-data-cli
3
+ Version: 1.0.0
4
+ Summary: An offline, AI-free Data Analytics and Dashboard Auditing CLI
5
+ Home-page: https://github.com/yourusername/ullr-data-cli
6
+ Author: jshlydnzl
7
+ Author-email: jshlydnzl@users.noreply.github.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: rich
16
+ Requires-Dist: pandas
17
+ Requires-Dist: openpyxl
18
+ Dynamic: author
19
+ Dynamic: author-email
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: requires-dist
25
+ Dynamic: requires-python
26
+ Dynamic: summary
27
+
28
+ Ullr is a pure Python Data Engine that automatically audits CSV/Excel files for data quality (ghost data, invisible spaces, duplicates) and generates Dynamic Dashboard Blueprints for Excel, Power BI, Tableau, and Looker Studio.
@@ -0,0 +1,9 @@
1
+ setup.py
2
+ ullr/__init__.py
3
+ ullr/main.py
4
+ ullr_data_cli.egg-info/PKG-INFO
5
+ ullr_data_cli.egg-info/SOURCES.txt
6
+ ullr_data_cli.egg-info/dependency_links.txt
7
+ ullr_data_cli.egg-info/entry_points.txt
8
+ ullr_data_cli.egg-info/requires.txt
9
+ ullr_data_cli.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ullr = ullr.main:cli
@@ -0,0 +1,3 @@
1
+ rich
2
+ pandas
3
+ openpyxl