excel-vis 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.
excel_vis/__init__.py ADDED
@@ -0,0 +1,62 @@
1
+ """excel-vis: A lightweight web application for Excel data management.
2
+
3
+ Usage:
4
+ # CLI
5
+ excel-vis --file data.xlsx --port 8000
6
+
7
+ # Python API
8
+ import excel_vis
9
+ excel_vis.run(file="data.xlsx", port=8000)
10
+ """
11
+
12
+ __version__ = "0.1.0"
13
+
14
+
15
+ def create_app(file: str = "data.xlsx"):
16
+ """Create a FastAPI application instance.
17
+
18
+ Args:
19
+ file: Path to the Excel file to use as data source.
20
+
21
+ Returns:
22
+ Configured FastAPI application.
23
+ """
24
+ from excel_vis.app import create_app as _create_app
25
+
26
+ return _create_app(file=file)
27
+
28
+
29
+ def run(
30
+ file: str = "data.xlsx",
31
+ host: str = "0.0.0.0",
32
+ port: int = 8000,
33
+ open_browser: bool = True,
34
+ ):
35
+ """Start the excel-vis Web service.
36
+
37
+ Args:
38
+ file: Path to the Excel file to use as data source.
39
+ host: Host address to bind to.
40
+ port: Port number to listen on.
41
+ open_browser: Whether to automatically open a browser window.
42
+ """
43
+ import webbrowser
44
+ from threading import Timer
45
+
46
+ import uvicorn
47
+
48
+ from excel_vis.app import create_app as _create_app
49
+ from excel_vis.config import update_settings
50
+
51
+ update_settings(file_path=file, host=host, port=port, open_browser=open_browser)
52
+
53
+ if open_browser:
54
+
55
+ def _open():
56
+ browse_host = "127.0.0.1" if host == "0.0.0.0" else host
57
+ webbrowser.open(f"http://{browse_host}:{port}")
58
+
59
+ Timer(1.5, _open).start()
60
+
61
+ application = _create_app(file=file)
62
+ uvicorn.run(application, host=host, port=port, log_level="info")
excel_vis/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow running excel-vis as a module: python -m excel_vis"""
2
+
3
+ from excel_vis.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1 @@
1
+ """API package."""
@@ -0,0 +1,17 @@
1
+ """Columns API route."""
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+
5
+ from excel_vis.services import excel_service
6
+
7
+ router = APIRouter(prefix="/api")
8
+
9
+
10
+ @router.get("/columns")
11
+ async def get_columns():
12
+ """Get column names from the Excel file."""
13
+ try:
14
+ columns = excel_service.get_columns()
15
+ return {"code": 200, "columns": columns}
16
+ except Exception as e:
17
+ raise HTTPException(status_code=500, detail=f"获取列信息失败: {str(e)}")
excel_vis/api/data.py ADDED
@@ -0,0 +1,73 @@
1
+ """CRUD API routes for data records."""
2
+
3
+ from fastapi import APIRouter, HTTPException, Request
4
+
5
+ from excel_vis.services import excel_service
6
+
7
+ router = APIRouter(prefix="/api")
8
+
9
+
10
+ @router.get("/data")
11
+ async def get_all_data():
12
+ """Get all records."""
13
+ try:
14
+ data = excel_service.read_all()
15
+ return {"code": 200, "data": data}
16
+ except Exception as e:
17
+ raise HTTPException(status_code=500, detail=f"读取数据失败: {str(e)}")
18
+
19
+
20
+ @router.get("/data/{record_id}")
21
+ async def get_one_data(record_id: int):
22
+ """Get a single record by ID."""
23
+ try:
24
+ record = excel_service.read_one(record_id)
25
+ if record is None:
26
+ raise HTTPException(status_code=404, detail=f"记录 ID={record_id} 不存在")
27
+ return {"code": 200, "data": record}
28
+ except HTTPException:
29
+ raise
30
+ except Exception as e:
31
+ raise HTTPException(status_code=500, detail=f"读取数据失败: {str(e)}")
32
+
33
+
34
+ @router.post("/data")
35
+ async def create_data(request: Request):
36
+ """Create a new record."""
37
+ try:
38
+ body = await request.json()
39
+ # Remove id if provided, backend will auto-generate
40
+ body.pop("id", None)
41
+ new_id = await excel_service.create(body)
42
+ return {"code": 200, "message": "ok", "id": new_id}
43
+ except Exception as e:
44
+ raise HTTPException(status_code=500, detail=f"新增记录失败: {str(e)}")
45
+
46
+
47
+ @router.put("/data/{record_id}")
48
+ async def update_data(record_id: int, request: Request):
49
+ """Update an existing record."""
50
+ try:
51
+ body = await request.json()
52
+ success = await excel_service.update(record_id, body)
53
+ if not success:
54
+ raise HTTPException(status_code=404, detail=f"记录 ID={record_id} 不存在")
55
+ return {"code": 200, "message": "ok"}
56
+ except HTTPException:
57
+ raise
58
+ except Exception as e:
59
+ raise HTTPException(status_code=500, detail=f"更新记录失败: {str(e)}")
60
+
61
+
62
+ @router.delete("/data/{record_id}")
63
+ async def delete_data(record_id: int):
64
+ """Delete a record by ID."""
65
+ try:
66
+ success = await excel_service.delete(record_id)
67
+ if not success:
68
+ raise HTTPException(status_code=404, detail=f"记录 ID={record_id} 不存在")
69
+ return {"code": 200, "message": "ok"}
70
+ except HTTPException:
71
+ raise
72
+ except Exception as e:
73
+ raise HTTPException(status_code=500, detail=f"删除记录失败: {str(e)}")
@@ -0,0 +1,69 @@
1
+ """File operations API routes (import/export)."""
2
+
3
+ from datetime import datetime
4
+ from urllib.parse import quote
5
+
6
+ from fastapi import APIRouter, File, Form, HTTPException, UploadFile
7
+ from fastapi.responses import Response
8
+
9
+ from excel_vis.config import get_settings
10
+ from excel_vis.services import excel_service
11
+
12
+ router = APIRouter(prefix="/api")
13
+
14
+ ALLOWED_EXTENSIONS = {".xlsx", ".xls"}
15
+
16
+
17
+ def _validate_file(filename: str) -> None:
18
+ """Validate uploaded file extension."""
19
+ if not filename:
20
+ raise HTTPException(status_code=400, detail="未提供文件名")
21
+ ext = "." + filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
22
+ if ext not in ALLOWED_EXTENSIONS:
23
+ raise HTTPException(
24
+ status_code=400,
25
+ detail=f"不支持的文件格式: {ext},仅支持 .xlsx 和 .xls",
26
+ )
27
+
28
+
29
+ @router.post("/import")
30
+ async def import_excel(file: UploadFile = File(...), mode: str = Form("append")):
31
+ """Import Excel file (append or overwrite mode)."""
32
+ _validate_file(file.filename)
33
+
34
+ if mode not in ("append", "overwrite"):
35
+ raise HTTPException(status_code=400, detail="mode 参数必须为 'append' 或 'overwrite'")
36
+
37
+ # Check file size
38
+ content = await file.read()
39
+ max_size = get_settings().max_upload_size
40
+ if len(content) > max_size:
41
+ raise HTTPException(
42
+ status_code=400,
43
+ detail=f"文件大小超过限制 ({max_size // 1024 // 1024}MB)",
44
+ )
45
+
46
+ try:
47
+ message = await excel_service.import_data(content, mode)
48
+ return {"code": 200, "message": message}
49
+ except ValueError as e:
50
+ raise HTTPException(status_code=400, detail=str(e))
51
+ except Exception as e:
52
+ raise HTTPException(status_code=500, detail=f"导入失败: {str(e)}")
53
+
54
+
55
+ @router.get("/export")
56
+ async def export_excel():
57
+ """Export current data as an Excel file download."""
58
+ try:
59
+ content = excel_service.export_data()
60
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
61
+ filename = f"数据导出_{timestamp}.xlsx"
62
+ encoded_filename = quote(filename)
63
+ return Response(
64
+ content=content,
65
+ media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
66
+ headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"},
67
+ )
68
+ except Exception as e:
69
+ raise HTTPException(status_code=500, detail=f"导出失败: {str(e)}")
excel_vis/app.py ADDED
@@ -0,0 +1,54 @@
1
+ """FastAPI application factory."""
2
+
3
+ from pathlib import Path
4
+
5
+ from fastapi import FastAPI
6
+ from fastapi.responses import HTMLResponse
7
+ from fastapi.staticfiles import StaticFiles
8
+ from jinja2 import Environment, FileSystemLoader
9
+
10
+ from excel_vis.api import columns, data, file_ops
11
+ from excel_vis.config import get_settings
12
+
13
+ TEMPLATES_DIR = Path(__file__).parent / "templates"
14
+ STATIC_DIR = Path(__file__).parent / "static"
15
+
16
+
17
+ def create_app(file: str = "data.xlsx") -> FastAPI:
18
+ """Create and configure a FastAPI application instance.
19
+
20
+ Args:
21
+ file: Path to the Excel file to use as data source.
22
+
23
+ Returns:
24
+ Configured FastAPI application.
25
+ """
26
+ from excel_vis.config import update_settings
27
+
28
+ update_settings(file_path=file)
29
+
30
+ app = FastAPI(
31
+ title="Excel 数据管理系统",
32
+ description="基于 Excel 的轻量级 Web 数据管理应用",
33
+ version="0.1.0",
34
+ )
35
+
36
+ # Register API routers
37
+ app.include_router(data.router)
38
+ app.include_router(columns.router)
39
+ app.include_router(file_ops.router)
40
+
41
+ # Mount static files if directory exists
42
+ if STATIC_DIR.exists():
43
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
44
+
45
+ # Setup Jinja2 template
46
+ jinja_env = Environment(loader=FileSystemLoader(str(TEMPLATES_DIR)))
47
+
48
+ @app.get("/", response_class=HTMLResponse)
49
+ async def index():
50
+ """Serve the main HTML page."""
51
+ template = jinja_env.get_template("index.html")
52
+ return template.render()
53
+
54
+ return app
excel_vis/cli.py ADDED
@@ -0,0 +1,48 @@
1
+ """Typer CLI entry point for excel-vis."""
2
+
3
+ import webbrowser
4
+ from threading import Timer
5
+ from typing import Optional
6
+
7
+ import typer
8
+ import uvicorn
9
+
10
+ app = typer.Typer(
11
+ name="excel-vis",
12
+ help="Excel 数据管理与展示系统 - 基于 FastAPI 的轻量级 Web 应用",
13
+ add_completion=False,
14
+ )
15
+
16
+
17
+ @app.callback(invoke_without_command=True)
18
+ def main(
19
+ file: str = typer.Option("data.xlsx", "--file", "-f", help="Excel 文件路径"),
20
+ host: str = typer.Option("127.0.0.1", "--host", "-H", help="监听地址"),
21
+ port: int = typer.Option(8888, "--port", "-p", help="端口号"),
22
+ no_browser: bool = typer.Option(False, "--no-browser", help="不自动打开浏览器"),
23
+ ):
24
+ """启动 Excel 数据管理 Web 服务。"""
25
+ from excel_vis.app import create_app
26
+ from excel_vis.config import update_settings
27
+
28
+ update_settings(file_path=file, host=host, port=port, open_browser=not no_browser)
29
+
30
+ typer.echo(f"📊 Excel-Vis 启动中...")
31
+ typer.echo(f" 文件: {file}")
32
+ typer.echo(f" 地址: http://{host}:{port}")
33
+
34
+ if not no_browser:
35
+
36
+ def open_browser():
37
+ browse_host = "127.0.0.1" if host == "0.0.0.0" else host
38
+ webbrowser.open(f"http://{browse_host}:{port}")
39
+
40
+ Timer(1.5, open_browser).start()
41
+
42
+ # Create app and run
43
+ application = create_app(file=file)
44
+ uvicorn.run(application, host=host, port=port, log_level="info")
45
+
46
+
47
+ if __name__ == "__main__":
48
+ app()
excel_vis/config.py ADDED
@@ -0,0 +1,35 @@
1
+ """Configuration module for excel-vis."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+
7
+ @dataclass
8
+ class Settings:
9
+ """Application settings."""
10
+
11
+ file_path: str = "data.xlsx"
12
+ host: str = "0.0.0.0"
13
+ port: int = 8000
14
+ open_browser: bool = True
15
+ max_upload_size: int = 10 * 1024 * 1024 # 10MB
16
+
17
+ @property
18
+ def excel_path(self) -> Path:
19
+ return Path(self.file_path).resolve()
20
+
21
+
22
+ # Global settings instance
23
+ _settings: Settings = Settings()
24
+
25
+
26
+ def get_settings() -> Settings:
27
+ """Get the current settings instance."""
28
+ return _settings
29
+
30
+
31
+ def update_settings(**kwargs) -> Settings:
32
+ """Update global settings with provided values."""
33
+ global _settings
34
+ _settings = Settings(**kwargs)
35
+ return _settings
@@ -0,0 +1 @@
1
+ """Services package."""
@@ -0,0 +1,390 @@
1
+ """Excel service layer for reading and writing Excel files."""
2
+
3
+ import asyncio
4
+ from datetime import datetime
5
+ from io import BytesIO
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
9
+ from openpyxl import Workbook, load_workbook
10
+ from openpyxl.utils import get_column_letter
11
+
12
+ from excel_vis.config import get_settings
13
+
14
+ # Global lock for file write operations
15
+ _file_lock = asyncio.Lock()
16
+
17
+ # Default sample data when no file exists
18
+ DEFAULT_COLUMNS = ["id", "姓名", "年龄", "邮箱", "部门", "入职日期"]
19
+ DEFAULT_DATA = [
20
+ [1, "张三", 28, "zh@ex.com", "技术部", "2023-05-01"],
21
+ [2, "李芳", 32, "li@ex.com", "市场部", "2023-07-15"],
22
+ ]
23
+
24
+
25
+ def _get_file_path() -> Path:
26
+ """Get the configured Excel file path."""
27
+ return get_settings().excel_path
28
+
29
+
30
+ def _ensure_file_exists() -> None:
31
+ """Ensure the Excel file exists; create with sample data if not."""
32
+ file_path = _get_file_path()
33
+ if not file_path.exists():
34
+ file_path.parent.mkdir(parents=True, exist_ok=True)
35
+ wb = Workbook()
36
+ ws = wb.active
37
+ ws.title = "Sheet1"
38
+ ws.append(DEFAULT_COLUMNS)
39
+ for row in DEFAULT_DATA:
40
+ ws.append(row)
41
+ wb.save(str(file_path))
42
+
43
+
44
+ def _load_workbook_safe() -> Tuple[Workbook, Any]:
45
+ """Load workbook safely, creating it if needed."""
46
+ _ensure_file_exists()
47
+ file_path = _get_file_path()
48
+ try:
49
+ wb = load_workbook(str(file_path))
50
+ except Exception:
51
+ # File is corrupted, recreate it
52
+ file_path.unlink(missing_ok=True)
53
+ _ensure_file_exists()
54
+ wb = load_workbook(str(file_path))
55
+ ws = wb.active
56
+ return wb, ws
57
+
58
+
59
+ def _get_next_id(ws) -> int:
60
+ """Get the next auto-increment ID."""
61
+ headers = [cell.value for cell in ws[1]]
62
+ if "id" not in headers:
63
+ return 1
64
+ id_col_idx = headers.index("id")
65
+ max_id = 0
66
+ for row in ws.iter_rows(min_row=2, values_only=True):
67
+ if row[id_col_idx] is not None:
68
+ try:
69
+ val = int(row[id_col_idx])
70
+ if val > max_id:
71
+ max_id = val
72
+ except (ValueError, TypeError):
73
+ pass
74
+ return max_id + 1
75
+
76
+
77
+ def _cell_value_to_str(value) -> Any:
78
+ """Convert cell value to a JSON-serializable format."""
79
+ if value is None:
80
+ return None
81
+ if isinstance(value, datetime):
82
+ return value.strftime("%Y-%m-%d")
83
+ return value
84
+
85
+
86
+ def get_columns() -> List[str]:
87
+ """Get column names from the Excel file."""
88
+ _ensure_file_exists()
89
+ wb, ws = _load_workbook_safe()
90
+ headers = [cell.value for cell in ws[1] if cell.value is not None]
91
+ wb.close()
92
+ return headers
93
+
94
+
95
+ def read_all() -> List[Dict[str, Any]]:
96
+ """Read all records from the Excel file."""
97
+ wb, ws = _load_workbook_safe()
98
+ headers = [cell.value for cell in ws[1]]
99
+ # Filter out None headers
100
+ valid_indices = [i for i, h in enumerate(headers) if h is not None]
101
+ valid_headers = [headers[i] for i in valid_indices]
102
+
103
+ data = []
104
+ for row in ws.iter_rows(min_row=2, values_only=True):
105
+ if all(v is None for v in row):
106
+ continue
107
+ record = {}
108
+ for idx in valid_indices:
109
+ val = row[idx] if idx < len(row) else None
110
+ record[valid_headers[valid_indices.index(idx)]] = _cell_value_to_str(val)
111
+ data.append(record)
112
+ wb.close()
113
+ return data
114
+
115
+
116
+ def read_one(record_id: int) -> Optional[Dict[str, Any]]:
117
+ """Read a single record by ID."""
118
+ wb, ws = _load_workbook_safe()
119
+ headers = [cell.value for cell in ws[1]]
120
+ if "id" not in headers:
121
+ wb.close()
122
+ return None
123
+
124
+ id_col_idx = headers.index("id")
125
+ valid_indices = [i for i, h in enumerate(headers) if h is not None]
126
+ valid_headers = [headers[i] for i in valid_indices]
127
+
128
+ for row in ws.iter_rows(min_row=2, values_only=True):
129
+ if row[id_col_idx] is not None:
130
+ try:
131
+ if int(row[id_col_idx]) == record_id:
132
+ record = {}
133
+ for idx in valid_indices:
134
+ val = row[idx] if idx < len(row) else None
135
+ record[valid_headers[valid_indices.index(idx)]] = _cell_value_to_str(val)
136
+ wb.close()
137
+ return record
138
+ except (ValueError, TypeError):
139
+ pass
140
+ wb.close()
141
+ return None
142
+
143
+
144
+ async def create(data: Dict[str, Any]) -> int:
145
+ """Create a new record. Returns the new ID."""
146
+ async with _file_lock:
147
+ wb, ws = _load_workbook_safe()
148
+ headers = [cell.value for cell in ws[1]]
149
+ valid_headers = [h for h in headers if h is not None]
150
+
151
+ # Ensure id column exists
152
+ if "id" not in valid_headers:
153
+ valid_headers.insert(0, "id")
154
+ ws.insert_cols(1)
155
+ ws.cell(row=1, column=1, value="id")
156
+ headers = ["id"] + headers
157
+
158
+ new_id = _get_next_id(ws)
159
+ data["id"] = new_id
160
+
161
+ # Check if new columns need to be added
162
+ for key in data.keys():
163
+ if key not in valid_headers:
164
+ valid_headers.append(key)
165
+ col_num = len(valid_headers)
166
+ ws.cell(row=1, column=col_num, value=key)
167
+
168
+ # Re-read headers after potential additions
169
+ headers = [cell.value for cell in ws[1]]
170
+
171
+ # Create new row
172
+ new_row = []
173
+ for h in headers:
174
+ if h is not None:
175
+ new_row.append(data.get(h))
176
+ else:
177
+ new_row.append(None)
178
+ ws.append(new_row)
179
+ wb.save(str(_get_file_path()))
180
+ wb.close()
181
+ return new_id
182
+
183
+
184
+ async def update(record_id: int, data: Dict[str, Any]) -> bool:
185
+ """Update an existing record by ID. Returns True if found and updated."""
186
+ async with _file_lock:
187
+ wb, ws = _load_workbook_safe()
188
+ headers = [cell.value for cell in ws[1]]
189
+
190
+ if "id" not in headers:
191
+ wb.close()
192
+ return False
193
+
194
+ id_col_idx = headers.index("id")
195
+
196
+ # Check if new columns need to be added
197
+ for key in data.keys():
198
+ if key != "id" and key not in headers:
199
+ headers.append(key)
200
+ col_num = len(headers)
201
+ ws.cell(row=1, column=col_num, value=key)
202
+
203
+ # Re-read headers after potential additions
204
+ headers = [cell.value for cell in ws[1]]
205
+
206
+ for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
207
+ if row[id_col_idx] is not None:
208
+ try:
209
+ if int(row[id_col_idx]) == record_id:
210
+ for col_idx, header in enumerate(headers):
211
+ if header is not None and header != "id" and header in data:
212
+ ws.cell(row=row_idx, column=col_idx + 1, value=data[header])
213
+ wb.save(str(_get_file_path()))
214
+ wb.close()
215
+ return True
216
+ except (ValueError, TypeError):
217
+ pass
218
+ wb.close()
219
+ return False
220
+
221
+
222
+ async def delete(record_id: int) -> bool:
223
+ """Delete a record by ID. Returns True if found and deleted."""
224
+ async with _file_lock:
225
+ wb, ws = _load_workbook_safe()
226
+ headers = [cell.value for cell in ws[1]]
227
+
228
+ if "id" not in headers:
229
+ wb.close()
230
+ return False
231
+
232
+ id_col_idx = headers.index("id")
233
+
234
+ for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
235
+ if row[id_col_idx] is not None:
236
+ try:
237
+ if int(row[id_col_idx]) == record_id:
238
+ ws.delete_rows(row_idx)
239
+ wb.save(str(_get_file_path()))
240
+ wb.close()
241
+ return True
242
+ except (ValueError, TypeError):
243
+ pass
244
+ wb.close()
245
+ return False
246
+
247
+
248
+ async def import_data(file_content: bytes, mode: str = "append") -> str:
249
+ """Import data from an uploaded Excel file.
250
+
251
+ Args:
252
+ file_content: Binary content of the uploaded Excel file.
253
+ mode: 'append' to add rows, 'overwrite' to replace all data.
254
+
255
+ Returns:
256
+ A status message.
257
+ """
258
+ async with _file_lock:
259
+ # Load uploaded file
260
+ try:
261
+ upload_wb = load_workbook(BytesIO(file_content))
262
+ except Exception as e:
263
+ raise ValueError(f"无法读取上传的Excel文件: {str(e)}")
264
+
265
+ upload_ws = upload_wb.active
266
+ upload_headers = [cell.value for cell in upload_ws[1]]
267
+ upload_headers = [h for h in upload_headers if h is not None]
268
+
269
+ if not upload_headers:
270
+ raise ValueError("上传的Excel文件没有有效的表头")
271
+
272
+ # Read uploaded data rows
273
+ upload_data = []
274
+ for row in upload_ws.iter_rows(min_row=2, values_only=True):
275
+ if all(v is None for v in row):
276
+ continue
277
+ record = {}
278
+ for i, h in enumerate(upload_headers):
279
+ val = row[i] if i < len(row) else None
280
+ record[h] = _cell_value_to_str(val)
281
+ upload_data.append(record)
282
+ upload_wb.close()
283
+
284
+ if mode == "overwrite":
285
+ # Create new workbook with uploaded structure
286
+ wb = Workbook()
287
+ ws = wb.active
288
+ ws.title = "Sheet1"
289
+
290
+ # Ensure id column
291
+ if "id" not in upload_headers:
292
+ final_headers = ["id"] + upload_headers
293
+ else:
294
+ final_headers = upload_headers
295
+
296
+ ws.append(final_headers)
297
+
298
+ # Write data with auto-generated IDs
299
+ current_id = 1
300
+ for record in upload_data:
301
+ row_data = []
302
+ for h in final_headers:
303
+ if h == "id":
304
+ if "id" in record and record["id"] is not None:
305
+ try:
306
+ row_data.append(int(record["id"]))
307
+ if int(record["id"]) >= current_id:
308
+ current_id = int(record["id"]) + 1
309
+ except (ValueError, TypeError):
310
+ row_data.append(current_id)
311
+ current_id += 1
312
+ else:
313
+ row_data.append(current_id)
314
+ current_id += 1
315
+ else:
316
+ row_data.append(record.get(h))
317
+ ws.append(row_data)
318
+
319
+ wb.save(str(_get_file_path()))
320
+ wb.close()
321
+ return f"覆盖导入成功,共导入 {len(upload_data)} 条记录"
322
+
323
+ else: # append mode
324
+ wb, ws = _load_workbook_safe()
325
+ existing_headers = [cell.value for cell in ws[1]]
326
+ existing_headers = [h for h in existing_headers if h is not None]
327
+
328
+ # Merge headers: add new columns from upload
329
+ merged_headers = list(existing_headers)
330
+ for h in upload_headers:
331
+ if h not in merged_headers and h != "id":
332
+ merged_headers.append(h)
333
+
334
+ # Ensure id column
335
+ if "id" not in merged_headers:
336
+ merged_headers.insert(0, "id")
337
+
338
+ # If headers changed, rebuild the worksheet
339
+ if merged_headers != existing_headers:
340
+ # Read existing data
341
+ existing_data = []
342
+ for row in ws.iter_rows(min_row=2, values_only=True):
343
+ if all(v is None for v in row):
344
+ continue
345
+ record = {}
346
+ for i, h in enumerate(existing_headers):
347
+ val = row[i] if i < len(row) else None
348
+ record[h] = _cell_value_to_str(val)
349
+ existing_data.append(record)
350
+
351
+ # Recreate workbook
352
+ wb = Workbook()
353
+ ws = wb.active
354
+ ws.title = "Sheet1"
355
+ ws.append(merged_headers)
356
+
357
+ for record in existing_data:
358
+ row_data = [record.get(h) for h in merged_headers]
359
+ ws.append(row_data)
360
+
361
+ # Get next ID
362
+ next_id = _get_next_id(ws)
363
+
364
+ # Append uploaded data
365
+ final_headers = [cell.value for cell in ws[1]]
366
+ for record in upload_data:
367
+ row_data = []
368
+ for h in final_headers:
369
+ if h == "id":
370
+ row_data.append(next_id)
371
+ next_id += 1
372
+ elif h is not None:
373
+ row_data.append(record.get(h))
374
+ else:
375
+ row_data.append(None)
376
+ ws.append(row_data)
377
+
378
+ wb.save(str(_get_file_path()))
379
+ wb.close()
380
+ return f"追加导入成功,共导入 {len(upload_data)} 条记录"
381
+
382
+
383
+ def export_data() -> bytes:
384
+ """Export current data as Excel bytes."""
385
+ wb, ws = _load_workbook_safe()
386
+ output = BytesIO()
387
+ wb.save(output)
388
+ wb.close()
389
+ output.seek(0)
390
+ return output.getvalue()
@@ -0,0 +1,485 @@
1
+ Metadata-Version: 2.4
2
+ Name: excel-vis
3
+ Version: 0.1.0
4
+ Summary: A lightweight web application for Excel data management and visualization, powered by FastAPI + Tailwind CSS + Alpine.js
5
+ Author-email: Chandler <275737875@qq.com>
6
+ License-Expression: MIT
7
+ Keywords: excel,fastapi,web,visualization,data-management
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Framework :: FastAPI
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: fastapi>=0.100.0
21
+ Requires-Dist: uvicorn[standard]>=0.20.0
22
+ Requires-Dist: openpyxl>=3.1.0
23
+ Requires-Dist: typer>=0.9.0
24
+ Requires-Dist: python-multipart>=0.0.6
25
+ Requires-Dist: jinja2>=3.1.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
28
+ Requires-Dist: httpx>=0.24.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # Excel-Vis
33
+
34
+ **一个让你用浏览器轻松管理 Excel 数据的工具。**
35
+
36
+ Excel-Vis 是一个基于 Python 的轻量级 Web 应用,它把你电脑上的 Excel 文件变成一个可以在浏览器中查看、编辑、新增、删除数据的网页系统。你不需要安装任何数据库,Excel 文件本身就是你的"数据库"。
37
+
38
+ 简单来说:**安装 → 一行命令启动 → 打开浏览器就能管理你的 Excel 数据。**
39
+
40
+ ---
41
+
42
+ ## 功能特性
43
+
44
+ - **浏览器查看 Excel 数据**:以美观的网页表格展示 Excel 文件内容,支持手机访问
45
+ - **在线增删改查**:直接在网页上新增、编辑、删除记录,修改会自动保存到 Excel 文件
46
+ - **单击查看详情**:点击任意一行,弹窗展示该行所有字段的完整信息
47
+ - **Excel 导入/导出**:支持上传 Excel 文件导入数据(追加或覆盖),也可导出当前数据
48
+ - **支持任意结构**:不限定 Excel 的列名和列数,系统自动识别表头
49
+ - **自定义列显示**:可以选择显示哪些列、修改列的显示名称、拖拽调整列顺序
50
+ - **布局设置**:支持紧凑/标准/宽松三种表格密度,可设置每页显示行数
51
+ - **双渠道编辑**:网页端修改自动写入 Excel;你也可以直接编辑 Excel 文件,刷新网页即可看到最新内容
52
+ - **一键启动**:安装后一条命令即可运行,自动打开浏览器
53
+ - **零配置**:无需数据库,无需复杂配置,开箱即用
54
+
55
+ ---
56
+
57
+ ## 安装指南
58
+
59
+ ### 环境要求
60
+
61
+ - **Python 版本**:3.9 或更高版本
62
+ - **操作系统**:Windows、macOS、Linux 均可
63
+ - **浏览器**:Chrome、Firefox、Edge、Safari 等现代浏览器
64
+
65
+ ### 第一步:检查 Python 版本
66
+
67
+ 打开你的终端(Windows 用户打开"命令提示符"或"PowerShell"),输入:
68
+
69
+ ```bash
70
+ python --version
71
+ ```
72
+
73
+ 如果显示 `Python 3.9.x` 或更高版本,说明环境已就绪。如果版本过低或未安装,请前往 [python.org](https://www.python.org/downloads/) 下载安装。
74
+
75
+ ### 第二步:安装 excel-vis
76
+
77
+ ```bash
78
+ pip install excel-vis
79
+ ```
80
+
81
+ 等待安装完成即可。这会自动安装所有需要的依赖包。
82
+
83
+ ### 第三步:验证安装
84
+
85
+ ```bash
86
+ excel-vis --help
87
+ ```
88
+
89
+ 如果看到帮助信息输出,说明安装成功。
90
+
91
+ ---
92
+
93
+ ## 使用教程
94
+
95
+ ### 快速启动(最简单的方式)
96
+
97
+ 在终端中进入你想存放 Excel 文件的目录,然后运行:
98
+
99
+ ```bash
100
+ excel-vis
101
+ ```
102
+
103
+ 系统会自动:
104
+ 1. 在当前目录创建一个带有示例数据的 `data.xlsx` 文件(如果不存在的话)
105
+ 2. 启动 Web 服务
106
+ 3. 自动打开浏览器展示数据
107
+
108
+ ### 指定已有的 Excel 文件
109
+
110
+ 如果你已经有一个 Excel 文件,可以直接指定它:
111
+
112
+ ```bash
113
+ excel-vis --file /path/to/your/file.xlsx
114
+ ```
115
+
116
+ **Windows 示例:**
117
+ ```bash
118
+ excel-vis --file C:\Users\张三\Documents\员工信息.xlsx
119
+ ```
120
+
121
+ **macOS/Linux 示例:**
122
+ ```bash
123
+ excel-vis --file ~/Documents/员工信息.xlsx
124
+ ```
125
+
126
+ ### 更多启动参数
127
+
128
+ ```bash
129
+ # 指定端口号(默认 8000)
130
+ excel-vis --port 8080
131
+
132
+ # 指定监听地址(让局域网内其他电脑也能访问)
133
+ excel-vis --host 0.0.0.0 --port 8080
134
+
135
+ # 不自动打开浏览器
136
+ excel-vis --no-browser
137
+
138
+ # 完整示例:指定文件、端口、不打开浏览器
139
+ excel-vis --file mydata.xlsx --port 9000 --no-browser
140
+ ```
141
+
142
+ ### 在 Python 代码中使用
143
+
144
+ 如果你是开发者,也可以在 Python 代码中直接调用:
145
+
146
+ ```python
147
+ import excel_vis
148
+
149
+ # 最简单的方式:一行代码启动
150
+ excel_vis.run()
151
+
152
+ # 自定义参数启动
153
+ excel_vis.run(
154
+ file="mydata.xlsx", # Excel 文件路径
155
+ host="0.0.0.0", # 监听地址
156
+ port=8080, # 端口号
157
+ open_browser=True # 是否自动打开浏览器
158
+ )
159
+
160
+ # 只创建 FastAPI 应用实例(用于集成到自己的项目中)
161
+ app = excel_vis.create_app(file="mydata.xlsx")
162
+ ```
163
+
164
+ ### 通过模块方式运行
165
+
166
+ ```bash
167
+ python -m excel_vis --file data.xlsx --port 8000
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 网页操作指南
173
+
174
+ 启动后打开浏览器,你会看到一个数据管理界面。以下是各功能的使用方法:
175
+
176
+ ### 查看数据
177
+
178
+ - 页面加载后自动显示 Excel 中的所有数据
179
+ - 表格支持分页浏览,底部可切换页码
180
+ - 在手机上访问时,表格可左右滑动
181
+
182
+ ### 查看某行详情
183
+
184
+ - **单击表格中的任意一行**,会弹出一个窗口,展示该行所有字段的名称和值
185
+ - 在详情窗口中可以直接点击"编辑"进入编辑模式
186
+
187
+ ### 新增记录
188
+
189
+ 1. 点击顶部工具栏的 **"新增"** 按钮
190
+ 2. 在弹出的表单中填写各字段(ID 会自动生成,不需要手动填)
191
+ 3. 点击 **"保存"**,数据将写入 Excel 文件并刷新表格
192
+
193
+ ### 编辑记录
194
+
195
+ 1. 点击某行右侧的 **编辑图标**(笔形图标)
196
+ 2. 修改需要更改的字段
197
+ 3. 点击 **"保存"**,修改将同步到 Excel 文件
198
+
199
+ ### 删除记录
200
+
201
+ 1. 点击某行右侧的 **删除图标**(垃圾桶图标)
202
+ 2. 系统会弹出确认对话框,防止误操作
203
+ 3. 确认后该行将从 Excel 文件中永久删除
204
+
205
+ ### 导入 Excel 文件
206
+
207
+ 1. 点击顶部工具栏的 **"导入"** 按钮
208
+ 2. 选择一个 `.xlsx` 或 `.xls` 文件
209
+ 3. 选择导入模式:
210
+ - **追加模式**:保留原有数据,把新文件中的数据添加到末尾
211
+ - **覆盖模式**:删除所有原有数据,完全用新文件替换
212
+ 4. 点击 **"开始导入"**
213
+
214
+ ### 导出 Excel 文件
215
+
216
+ - 点击顶部工具栏的 **"导出"** 按钮
217
+ - 浏览器会自动下载一个 Excel 文件,文件名格式为 `数据导出_日期_时间.xlsx`
218
+
219
+ ### 刷新数据
220
+
221
+ - 如果你在外部直接编辑了 Excel 文件,点击 **"刷新"** 按钮即可加载最新数据
222
+
223
+ ### 自定义列设置
224
+
225
+ 1. 点击顶部工具栏的 **"列设置"** 按钮
226
+ 2. 在右侧弹出的面板中可以:
227
+ - **勾选/取消勾选**:控制哪些列在表格中显示
228
+ - **修改显示名称**:把英文列名改成中文等自定义名称
229
+ - **拖拽排序**:按住左侧拖拽图标调整列的显示顺序
230
+ - **切换表格密度**:紧凑/标准/宽松
231
+ - **设置每页行数**:10/20/50/100
232
+ 3. 点击 **"应用"** 保存设置
233
+ 4. 这些设置保存在浏览器中,下次打开页面会自动恢复
234
+
235
+ ---
236
+
237
+ ## API 接口说明
238
+
239
+ Excel-Vis 提供标准的 RESTful API,可以被其他程序调用。启动服务后,API 基础地址为 `http://localhost:8000`(端口号取决于你的设置)。
240
+
241
+ ### 获取所有数据
242
+
243
+ ```
244
+ GET /api/data
245
+ ```
246
+
247
+ **响应示例:**
248
+ ```json
249
+ {
250
+ "code": 200,
251
+ "data": [
252
+ {"id": 1, "姓名": "张三", "年龄": 28, "邮箱": "zh@ex.com", "部门": "技术部"},
253
+ {"id": 2, "姓名": "李芳", "年龄": 32, "邮箱": "li@ex.com", "部门": "市场部"}
254
+ ]
255
+ }
256
+ ```
257
+
258
+ ### 获取单条记录
259
+
260
+ ```
261
+ GET /api/data/{id}
262
+ ```
263
+
264
+ **示例:** `GET /api/data/1`
265
+
266
+ **响应:**
267
+ ```json
268
+ {
269
+ "code": 200,
270
+ "data": {"id": 1, "姓名": "张三", "年龄": 28, "邮箱": "zh@ex.com", "部门": "技术部"}
271
+ }
272
+ ```
273
+
274
+ ### 新增记录
275
+
276
+ ```
277
+ POST /api/data
278
+ Content-Type: application/json
279
+
280
+ {"姓名": "王五", "年龄": 25, "邮箱": "wang@ex.com", "部门": "产品部"}
281
+ ```
282
+
283
+ **响应:**
284
+ ```json
285
+ {"code": 200, "message": "ok", "id": 3}
286
+ ```
287
+
288
+ ### 更新记录
289
+
290
+ ```
291
+ PUT /api/data/{id}
292
+ Content-Type: application/json
293
+
294
+ {"姓名": "王五改", "年龄": 26}
295
+ ```
296
+
297
+ **响应:**
298
+ ```json
299
+ {"code": 200, "message": "ok"}
300
+ ```
301
+
302
+ ### 删除记录
303
+
304
+ ```
305
+ DELETE /api/data/{id}
306
+ ```
307
+
308
+ **响应:**
309
+ ```json
310
+ {"code": 200, "message": "ok"}
311
+ ```
312
+
313
+ ### 获取列信息
314
+
315
+ ```
316
+ GET /api/columns
317
+ ```
318
+
319
+ **响应:**
320
+ ```json
321
+ {"code": 200, "columns": ["id", "姓名", "年龄", "邮箱", "部门", "入职日期"]}
322
+ ```
323
+
324
+ ### 导入 Excel
325
+
326
+ ```
327
+ POST /api/import
328
+ Content-Type: multipart/form-data
329
+
330
+ file: <Excel 文件>
331
+ mode: append 或 overwrite
332
+ ```
333
+
334
+ **响应:**
335
+ ```json
336
+ {"code": 200, "message": "追加导入成功,共导入 5 条记录"}
337
+ ```
338
+
339
+ ### 导出 Excel
340
+
341
+ ```
342
+ GET /api/export
343
+ ```
344
+
345
+ 返回 `.xlsx` 文件下载。
346
+
347
+ ---
348
+
349
+ ## 依赖项清单
350
+
351
+ | 依赖包 | 最低版本 | 用途 |
352
+ |--------|---------|------|
353
+ | fastapi | 0.100.0 | Web 框架,提供 API 服务 |
354
+ | uvicorn[standard] | 0.20.0 | ASGI 服务器,运行 FastAPI 应用 |
355
+ | openpyxl | 3.1.0 | 读写 Excel (.xlsx) 文件 |
356
+ | typer | 0.9.0 | 命令行界面(CLI)框架 |
357
+ | python-multipart | 0.0.6 | 处理文件上传 |
358
+ | jinja2 | 3.1.0 | HTML 模板渲染 |
359
+
360
+ **开发依赖(可选):**
361
+
362
+ | 依赖包 | 用途 |
363
+ |--------|------|
364
+ | pytest | 运行测试 |
365
+ | httpx | API 测试客户端 |
366
+ | pytest-asyncio | 异步测试支持 |
367
+
368
+ ---
369
+
370
+ ## 常见问题
371
+
372
+ ### Q: 启动后浏览器没有自动打开怎么办?
373
+
374
+ 手动打开浏览器,访问终端中显示的地址(默认为 `http://127.0.0.1:8000`)。
375
+
376
+ ### Q: 提示端口被占用怎么办?
377
+
378
+ 换一个端口号启动:
379
+
380
+ ```bash
381
+ excel-vis --port 9000
382
+ ```
383
+
384
+ ### Q: 我的 Excel 文件没有 id 列怎么办?
385
+
386
+ 没有关系!系统会自动添加 `id` 列并为每行生成唯一编号,不会影响你原有的数据。
387
+
388
+ ### Q: 我直接用 Excel 软件修改了文件,网页上怎么看到新数据?
389
+
390
+ 点击网页上的 **"刷新"** 按钮,即可重新加载 Excel 文件中的最新内容。
391
+
392
+ ### Q: 可以多人同时使用吗?
393
+
394
+ 本工具设计为**单用户或小团队轻量使用**。由于 Excel 文件不支持并发写入,建议同一时间只有一个人进行编辑操作。多人同时查看数据是没有问题的。
395
+
396
+ ### Q: 支持哪些格式的 Excel 文件?
397
+
398
+ 支持 `.xlsx`(推荐)和 `.xls` 格式。导出时统一使用 `.xlsx` 格式。
399
+
400
+ ### Q: 数据量有限制吗?
401
+
402
+ 建议 Excel 文件不超过 **10,000 行 × 30 列**。超过此规模可能会导致加载变慢。
403
+
404
+ ### Q: 如何让局域网内其他电脑访问?
405
+
406
+ 启动时指定 host 为 `0.0.0.0`:
407
+
408
+ ```bash
409
+ excel-vis --host 0.0.0.0 --port 8000
410
+ ```
411
+
412
+ 然后其他电脑在浏览器中输入 `http://你的IP地址:8000` 即可访问。
413
+
414
+ ### Q: 上传文件有大小限制吗?
415
+
416
+ 默认限制为 **10MB**。如果需要导入更大的文件,建议先精简数据或分批导入。
417
+
418
+ ---
419
+
420
+ ## 免责声明
421
+
422
+ ### 使用风险
423
+
424
+ 1. **数据安全**:本软件直接读写 Excel 文件,操作不可撤销。删除或覆盖导入等操作会**永久修改**你的 Excel 文件。强烈建议在使用前**备份重要数据**。
425
+
426
+ 2. **非生产环境工具**:本软件为轻量级数据管理工具,适用于个人使用、小团队内部管理、数据演示等场景。**不建议**用于生产环境、金融交易、医疗记录等对数据安全性和可靠性有严格要求的场景。
427
+
428
+ 3. **并发限制**:本软件使用 Excel 文件作为数据存储,不具备数据库级别的并发控制能力。多人同时写入可能导致数据冲突或丢失。
429
+
430
+ 4. **网络安全**:本软件默认不包含用户认证和权限控制。如果部署在公网环境中,任何能够访问该地址的人都可以查看和修改你的数据。请勿在不受信任的网络环境中使用,或自行配置防火墙和访问控制。
431
+
432
+ 5. **文件完整性**:如果 Excel 文件在使用过程中被外部程序(如 Excel 软件)同时打开并编辑,可能导致文件损坏或数据不一致。
433
+
434
+ ### 责任归属
435
+
436
+ - 本软件按"现状"提供,作者**不对因使用本软件而导致的任何数据丢失、损坏、泄露或其他损失承担责任**。
437
+ - 用户应自行评估使用本软件的风险,并采取必要的数据备份和安全防护措施。
438
+ - 用户应确保其使用本软件的方式符合所在地区的法律法规。
439
+
440
+ ### 使用建议
441
+
442
+ - 使用前务必**备份** Excel 文件
443
+ - 定期通过"导出"功能下载数据备份
444
+ - 避免在公网环境中不加防护地部署
445
+ - 避免多人同时进行写入操作
446
+ - 重要数据请使用专业数据库系统管理
447
+
448
+ ---
449
+
450
+ ## 贡献指南
451
+
452
+ 欢迎贡献代码、报告问题或提出建议!
453
+
454
+ 1. Fork 本项目
455
+ 2. 创建你的功能分支:`git checkout -b feature/my-feature`
456
+ 3. 提交更改:`git commit -m "Add my feature"`
457
+ 4. 推送到分支:`git push origin feature/my-feature`
458
+ 5. 创建 Pull Request
459
+
460
+ ### 开发环境搭建
461
+
462
+ ```bash
463
+ # 克隆项目
464
+ git clone https://github.com/your-repo/excel-vis.git
465
+ cd excel-vis
466
+
467
+ # 安装开发依赖
468
+ pip install -e ".[dev]"
469
+
470
+ # 运行测试
471
+ pytest tests/ -v
472
+ ```
473
+
474
+ ---
475
+
476
+ ## 许可证信息
477
+
478
+ 本项目采用 **MIT 许可证** 开源。
479
+
480
+ 你可以自由地使用、复制、修改、合并、发布、分发本软件,但需要在软件的所有副本中包含版权声明和许可证声明。
481
+
482
+ 详见 [LICENSE](./LICENSE) 文件。
483
+
484
+
485
+
@@ -0,0 +1,17 @@
1
+ excel_vis/__init__.py,sha256=SXejOEnXfG0yNsckHg0DUd6qc_-52EcTjYIj-i4jHG4,1534
2
+ excel_vis/__main__.py,sha256=iAozqHypcRmnK63_nb0KPFxsL2sTbHtta9mNVEwlu44,132
3
+ excel_vis/app.py,sha256=hQmxHXKTyWRDQA6GhiYUgECxJnkJp7X4lvKODyQOPnw,1535
4
+ excel_vis/cli.py,sha256=v7oHGuqDitDeWiLdpkRnoNJLpiVc6F-piSxtPpfPyD4,1453
5
+ excel_vis/config.py,sha256=vGH-X3lWLEdLTe34GgZJ2MXCLqGZrwHCl0WDePvBpA8,767
6
+ excel_vis/api/__init__.py,sha256=d7XMEse6wwtLORqrQmnhaKMI8KM3lJSzRVE7HVskq1w,19
7
+ excel_vis/api/columns.py,sha256=Ijt6zaWgoHj8pKByaQNpPUsuB7Da_wYNVJWsInWpIro,470
8
+ excel_vis/api/data.py,sha256=XPpSqgYHuzhvHesa2QpO4HuPhZ39rfxfz-Jor8BbXh0,2435
9
+ excel_vis/api/file_ops.py,sha256=6hB2V35gswGGchTv50_Mi5mDJTYdi7zP8wrZEMIxsv8,2455
10
+ excel_vis/services/__init__.py,sha256=_rAF0b_ESOnKNSNRTDgBtv5D6k8lWfwKnOfH0_3aOIw,24
11
+ excel_vis/services/excel_service.py,sha256=1ys4qLpr_DVEWrhtrIaTyRHKCjccjA_qQeiCFPVcAjM,13205
12
+ excel_vis-0.1.0.dist-info/licenses/LICENSE,sha256=vypY5l3jhuff7dyqqcpsxh0s3mR3eevFfq2CuOn5Gh8,1065
13
+ excel_vis-0.1.0.dist-info/METADATA,sha256=7Ffakrlo4xKlKQ6n4BWH9fpLUGCdmMqp06aBl93g8r8,13676
14
+ excel_vis-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
15
+ excel_vis-0.1.0.dist-info/entry_points.txt,sha256=rLG5Wxbw1laPV5Vdcb0GxhZsBx8h21pOGbpfO_ayU2s,48
16
+ excel_vis-0.1.0.dist-info/top_level.txt,sha256=-sB2kIq9KXT3Fvi4s3DW1OwbxJjbsvtPyNYpcWhup5o,10
17
+ excel_vis-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ excel-vis = excel_vis.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chandler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ excel_vis