ms-file-toolkit 0.4.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,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: ms-file-toolkit
3
+ Version: 0.4.0
4
+ Summary: Microsoft fayllarni (DOCX/XLSX/PPTX) o'qish va yozish uchun AI function-calling Python toolkit
5
+ Author: muslihiddinlive
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/muslihiddinlive/ms-file-toolkit
8
+ Project-URL: Repository, https://github.com/muslihiddinlive/ms-file-toolkit
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: python-docx>=1.1.0
12
+ Requires-Dist: openpyxl>=3.1.0
13
+ Requires-Dist: python-pptx>=0.6.23
14
+ Requires-Dist: Pillow>=10.0.0
15
+ Requires-Dist: pypdf>=4.0.0
16
+ Requires-Dist: pdfplumber>=0.11.0
17
+ Requires-Dist: reportlab>=4.0.0
18
+ Provides-Extra: ai
19
+ Requires-Dist: anthropic>=0.40.0; extra == "ai"
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
22
+
23
+ # ms-file-toolkit
24
+
25
+ Microsoft fayllarni va PDF'larni **o'qish va yozish** uchun Python toolkit — **AI function calling** (Claude, GPT va h.k.) uchun tayyor tool schemalari, **xavfsizlik nazorati**, **avtomatik format aniqlash** va **CLI** bilan.
26
+
27
+ ## O'rnatish
28
+
29
+ ```bash
30
+ pip install -r requirements.txt
31
+ ```
32
+
33
+ Yoki paket sifatida (PyPI'ga chiqarishga tayyor, CLI bilan birga o'rnatiladi):
34
+
35
+ ```bash
36
+ pip install -e ".[ai,dev]"
37
+ ```
38
+
39
+ ## Qo'llab-quvvatlanadigan formatlar
40
+
41
+ | Format | O'qish | Yozish |
42
+ | ------------------------------ | ------------------------------------------------ | --------------------------------------------------------------------------------------- |
43
+ | `.docx` | matn, jadvallar, metama'lumot | yaratish, matn qo'shish/almashtirish, sarlavha, rasm, sahifa bo'linishi |
44
+ | `.xlsx` | varaq nomlari, ma'lumotlar, o'lcham xulosasi | yaratish, varaq/qator qo'shish, formula, formatlash, panellarni muzlatish, **diagramma** |
45
+ | `.pptx` | slayd matnlari, slaydlar soni, notes | yaratish, slayd/rasm qo'shish, fon rangi, **diagramma** |
46
+ | `.pdf` | matn, jadvallar, metama'lumot | yaratish, birlashtirish (merge), bo'lish (split) |
47
+ | `.doc` / `.xls` / `.ppt` (eski) | `read_legacy_file` — LibreOffice orqali | — |
48
+ | **Har qanday (avtomatik)** | `read_any_file`, `get_file_info` | — |
49
+
50
+ > Eski formatlar uchun tizimda **LibreOffice** (`soffice`) kerak: `apt-get install libreoffice`
51
+
52
+ ## Xavfsizlik
53
+
54
+ Fayl yo'llari AI tomonidan tanlanadi, shuning uchun **path traversal** himoyasi o'rnatilgan (`ms_toolkit/security.py`):
55
+
56
+ ```bash
57
+ export MS_TOOLKIT_BASE_DIR=/home/bot/user_files # faqat shu papka ichidagi fayllarga ruxsat
58
+ ```
59
+
60
+ ```python
61
+ dispatch("read_docx_text", {"file_path": "../../etc/passwd"})
62
+ # -> {"error": "Xavfsizlik xatosi: ..."}
63
+ ```
64
+
65
+ ## CLI (terminal orqali)
66
+
67
+ ```bash
68
+ ms-toolkit list # barcha tool'lar ro'yxati
69
+ ms-toolkit run create_docx '{"file_path": "a.docx", "title": "Salom", "overwrite": true}'
70
+ ms-toolkit run read_any_file '{"file_path": "hujjat.xlsx"}'
71
+ ```
72
+
73
+ ## Tez boshlash (API kalitisiz)
74
+
75
+ ```bash
76
+ pip install -e ".[dev]"
77
+ pytest tests/ -v # 23 ta test
78
+ python test_toolkit.py # tezkor smoke-test
79
+ ```
80
+
81
+ ## Function calling bilan (Claude API)
82
+
83
+ ```python
84
+ from ms_toolkit import TOOLS, dispatch
85
+ import anthropic
86
+
87
+ client = anthropic.Anthropic()
88
+ response = client.messages.create(
89
+ model="claude-sonnet-5",
90
+ max_tokens=1024,
91
+ tools=TOOLS,
92
+ messages=[{"role": "user", "content": "test.xlsx faylida nechta qator bor?"}],
93
+ )
94
+
95
+ for block in response.content:
96
+ if block.type == "tool_use":
97
+ print(dispatch(block.name, block.input))
98
+ ```
99
+
100
+ To'liq agentic misol: [`example.py`](example.py).
101
+
102
+ ## Diagramma qo'shish misoli
103
+
104
+ ```python
105
+ from ms_toolkit import dispatch
106
+
107
+ dispatch("add_xlsx_chart", {
108
+ "file_path": "hisobot.xlsx", "chart_type": "bar",
109
+ "data_range": "B1:B5", "categories_range": "A2:A5", "title": "Oylik savdo",
110
+ })
111
+
112
+ dispatch("add_pptx_chart", {
113
+ "file_path": "taqdimot.pptx", "chart_type": "pie",
114
+ "categories": ["A", "B", "C"], "series": {"2026": [40, 35, 25]},
115
+ })
116
+ ```
117
+
118
+ ## Barcha tool'lar ro'yxati (36 ta)
119
+
120
+ **DOCX (9):** `read_docx_text`, `read_docx_tables`, `get_docx_metadata`, `create_docx`, `append_docx_text`, `replace_docx_text`, `add_docx_heading`, `add_docx_image`, `add_docx_page_break`
121
+ **XLSX (10):** `get_xlsx_sheet_names`, `read_xlsx_data`, `get_xlsx_summary`, `create_xlsx`, `add_xlsx_sheet`, `append_xlsx_rows`, `set_xlsx_formula`, `format_xlsx_cells`, `freeze_xlsx_panes`, `add_xlsx_chart`
122
+ **PPTX (8):** `read_pptx_text`, `get_pptx_slide_count`, `extract_pptx_notes`, `create_pptx`, `add_pptx_slide`, `add_pptx_image`, `set_pptx_background_color`, `add_pptx_chart`
123
+ **PDF (6):** `read_pdf_text`, `read_pdf_tables`, `get_pdf_metadata`, `create_pdf`, `merge_pdfs`, `split_pdf`
124
+ **Eski formatlar (1):** `read_legacy_file`
125
+ **Auto-detect (2):** `read_any_file`, `get_file_info`
126
+
127
+ ## Struktura
128
+
129
+ ```
130
+ ms_toolkit/
131
+ __init__.py # TOOLS, dispatch export qiladi
132
+ docx_tool.py / docx_writer.py # Word
133
+ xlsx_tool.py / xlsx_writer.py # Excel (+ chart)
134
+ pptx_tool.py / pptx_writer.py # PowerPoint (+ chart)
135
+ pdf_tool.py / pdf_writer.py # PDF
136
+ legacy_tool.py # Eski formatlar
137
+ auto_tool.py # Avtomatik format aniqlash
138
+ security.py # Path-traversal himoyasi
139
+ cli.py # Terminal interfeysi (ms-toolkit)
140
+ schemas.py # AI uchun tool schemalari
141
+ registry.py # tool_name -> funksiya dispatcher
142
+ example.py, test_toolkit.py, tests/ # Misollar va testlar
143
+ pyproject.toml # pip/PyPI paketlash + CLI entry point
144
+ ```
145
+
146
+ ## PyPI'ga chiqarish
147
+
148
+ ```bash
149
+ pip install build twine
150
+ python -m build
151
+ twine upload dist/*
152
+ ```
153
+
154
+ ## Litsenziya
155
+
156
+ MIT
@@ -0,0 +1,20 @@
1
+ ms_toolkit/__init__.py,sha256=Z1W5C3-JR1AOKaG7CjW5d7UhRjALO3GdQG1GD5fh9eY,135
2
+ ms_toolkit/auto_tool.py,sha256=RPVPiRLxpJ9dhAETGQ-XdZPetSNmauyk3WOIevVFUIA,1898
3
+ ms_toolkit/cli.py,sha256=t9oacvxJNHdj2gyDP4md7g4IcpNrqcNzfZ8qPL1MoA8,1919
4
+ ms_toolkit/docx_tool.py,sha256=HlI_HHan6Yij2rc61vMuuMVq-HLor_Q5wHYDv1VsiHU,1370
5
+ ms_toolkit/docx_writer.py,sha256=qiCxW9Y-sIoBaIVx9TKPILCY9KceD_GMeEqLy34nlhQ,4786
6
+ ms_toolkit/legacy_tool.py,sha256=20Lg_Zi203KqqaGzLV7DO5qAzZk5IqlVPtll3IKnwzw,3703
7
+ ms_toolkit/pdf_tool.py,sha256=3eTVF-NkwnEMwqKw1t5_v9-0jtqWbhDNzXHJvuHz82Q,1892
8
+ ms_toolkit/pdf_writer.py,sha256=9obBykLonfvAUM7HikKeCuGxsuawCKSjJU9-QLrHpWo,2742
9
+ ms_toolkit/pptx_tool.py,sha256=5ylNTcJO0xuuwTuMidvqw3hIuidBY_6iCQh5DmZHDqw,1450
10
+ ms_toolkit/pptx_writer.py,sha256=8nsC2f-SorRadnc_gDv24vVXDXzSUv8dNVsvYAUxxrs,5903
11
+ ms_toolkit/registry.py,sha256=aERykycNpWDMRbY67ZxlmNgn0g0I3njfpJ3h_Y5VkhI,4775
12
+ ms_toolkit/schemas.py,sha256=fcSXwmh2uJJrCpTY3_CEtBB9ursF_-C0WAIAIxTjznM,22530
13
+ ms_toolkit/security.py,sha256=kFeM28aSZvDd_lWH-XHG6yInueAOzN_5xvcw0cbOPTU,2158
14
+ ms_toolkit/xlsx_tool.py,sha256=olL3fRFMVene9K3eTsc4rsyFVG_uo5zEYGnqg6c48Ko,1604
15
+ ms_toolkit/xlsx_writer.py,sha256=XgeOrzaOxnmGtueNn3qgNLGOXgS9gK81Z--Q_WwW76c,6190
16
+ ms_file_toolkit-0.4.0.dist-info/METADATA,sha256=oKUusuS9JM1Axbur1EGKIDFh2sD4QQLC_FJPKqfTYxs,6250
17
+ ms_file_toolkit-0.4.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
18
+ ms_file_toolkit-0.4.0.dist-info/entry_points.txt,sha256=CwTAHXr0k41Nw92z9BierGPbxik9JX1vtqji0n8HfqM,51
19
+ ms_file_toolkit-0.4.0.dist-info/top_level.txt,sha256=BMiFZC5zXYxp0YXqWU8kY2x2mQvEWbGe5Av_ShLEA40,11
20
+ ms_file_toolkit-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ms-toolkit = ms_toolkit.cli:main
@@ -0,0 +1 @@
1
+ ms_toolkit
ms_toolkit/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .schemas import TOOLS
2
+ from .registry import dispatch, REGISTRY
3
+
4
+ __all__ = ["TOOLS", "dispatch", "REGISTRY"]
5
+ __version__ = "0.4.0"
@@ -0,0 +1,61 @@
1
+ """Fayl turini avtomatik aniqlab, mos reader'ni chaqiradigan yordamchi modul."""
2
+
3
+ import os
4
+
5
+ _EXT_MAP = {
6
+ ".docx": "docx",
7
+ ".xlsx": "xlsx",
8
+ ".pptx": "pptx",
9
+ ".pdf": "pdf",
10
+ ".doc": "legacy",
11
+ ".xls": "legacy",
12
+ ".ppt": "legacy",
13
+ }
14
+
15
+
16
+ def get_file_info(file_path: str) -> dict:
17
+ """Fayl haqida asosiy ma'lumot: mavjudmi, kengaytmasi, hajmi, aniqlangan turi."""
18
+ ext = os.path.splitext(file_path)[1].lower()
19
+ exists = os.path.exists(file_path)
20
+ return {
21
+ "file_path": file_path,
22
+ "exists": exists,
23
+ "extension": ext,
24
+ "detected_type": _EXT_MAP.get(ext, "unknown"),
25
+ "size_bytes": os.path.getsize(file_path) if exists else None,
26
+ }
27
+
28
+
29
+ def read_any_file(file_path: str) -> dict:
30
+ """Fayl kengaytmasiga qarab mos o'qish funksiyasini avtomatik tanlab chaqiradi.
31
+
32
+ Foydalanuvchi/AI fayl turini oldindan bilmasa yoki noaniq bo'lsa foydali —
33
+ .docx/.xlsx/.pptx va eski .doc/.xls/.ppt formatlarni avtomatik aniqlaydi.
34
+ """
35
+ from .docx_tool import read_docx_text
36
+ from .xlsx_tool import read_xlsx_data
37
+ from .pptx_tool import read_pptx_text
38
+ from .pdf_tool import read_pdf_text
39
+ from .legacy_tool import read_legacy_file
40
+
41
+ if not os.path.exists(file_path):
42
+ return {"error": f"Fayl topilmadi: {file_path}"}
43
+
44
+ ext = os.path.splitext(file_path)[1].lower()
45
+ kind = _EXT_MAP.get(ext)
46
+
47
+ if kind == "docx":
48
+ result = read_docx_text(file_path)
49
+ elif kind == "xlsx":
50
+ result = read_xlsx_data(file_path)
51
+ elif kind == "pptx":
52
+ result = read_pptx_text(file_path)
53
+ elif kind == "pdf":
54
+ result = read_pdf_text(file_path)
55
+ elif kind == "legacy":
56
+ result = read_legacy_file(file_path)
57
+ else:
58
+ return {"error": f"Qo'llab-quvvatlanmaydigan yoki noma'lum fayl turi: {ext or '(kengaytmasiz)'}"}
59
+
60
+ result["detected_type"] = kind
61
+ return result
ms_toolkit/cli.py ADDED
@@ -0,0 +1,61 @@
1
+ """
2
+ ms_toolkit uchun komandalar qatori (CLI) interfeysi.
3
+
4
+ Ishlatish:
5
+ ms-toolkit list
6
+ ms-toolkit run create_docx '{"file_path": "test.docx", "title": "Salom"}'
7
+ ms-toolkit run read_docx_text '{"file_path": "test.docx"}'
8
+ ms-toolkit run read_any_file '{"file_path": "hujjat.xlsx"}'
9
+
10
+ MS_TOOLKIT_BASE_DIR muhit o'zgaruvchisi bilan fayl amallarini bitta papkaga
11
+ cheklash mumkin (xavfsizlik uchun tavsiya etiladi).
12
+ """
13
+
14
+ import sys
15
+ import json
16
+ import argparse
17
+
18
+ from .registry import REGISTRY, dispatch
19
+ from .schemas import TOOLS
20
+
21
+
22
+ def cmd_list(args):
23
+ for t in TOOLS:
24
+ print(f"{t['name']:<28} {t['description'][:80]}")
25
+
26
+
27
+ def cmd_run(args):
28
+ if args.tool_name not in REGISTRY:
29
+ print(f"Xato: noma'lum tool '{args.tool_name}'. Ro'yxat uchun: ms-toolkit list", file=sys.stderr)
30
+ sys.exit(1)
31
+
32
+ try:
33
+ tool_input = json.loads(args.json_input)
34
+ except json.JSONDecodeError as e:
35
+ print(f"Xato: JSON noto'g'ri formatda: {e}", file=sys.stderr)
36
+ sys.exit(1)
37
+
38
+ result = dispatch(args.tool_name, tool_input)
39
+ print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
40
+
41
+ if "error" in result:
42
+ sys.exit(1)
43
+
44
+
45
+ def main():
46
+ parser = argparse.ArgumentParser(prog="ms-toolkit", description="MS fayllar (DOCX/XLSX/PPTX/PDF) uchun CLI")
47
+ sub = parser.add_subparsers(dest="command", required=True)
48
+
49
+ sub.add_parser("list", help="Barcha mavjud tool'larni ro'yxatini chiqaradi").set_defaults(func=cmd_list)
50
+
51
+ run_parser = sub.add_parser("run", help="Bitta tool'ni ishga tushiradi")
52
+ run_parser.add_argument("tool_name", help="Ishga tushiriladigan tool nomi (masalan: create_docx)")
53
+ run_parser.add_argument("json_input", help="Tool uchun JSON formatidagi kirish, masalan '{\"file_path\": \"a.docx\"}'")
54
+ run_parser.set_defaults(func=cmd_run)
55
+
56
+ args = parser.parse_args()
57
+ args.func(args)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
@@ -0,0 +1,41 @@
1
+ """DOCX (Word) fayllarni o'qish va tahlil qilish funksiyalari."""
2
+
3
+ from docx import Document
4
+
5
+
6
+ def read_docx_text(file_path: str) -> dict:
7
+ """Word hujjatidagi barcha matnni paragraflar bo'yicha o'qiydi."""
8
+ doc = Document(file_path)
9
+ paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
10
+ return {
11
+ "file_path": file_path,
12
+ "paragraph_count": len(paragraphs),
13
+ "text": "\n".join(paragraphs),
14
+ }
15
+
16
+
17
+ def read_docx_tables(file_path: str) -> dict:
18
+ """Word hujjatidagi barcha jadvallarni o'qiydi."""
19
+ doc = Document(file_path)
20
+ tables = []
21
+ for table in doc.tables:
22
+ rows = []
23
+ for row in table.rows:
24
+ rows.append([cell.text for cell in row.cells])
25
+ tables.append(rows)
26
+ return {"file_path": file_path, "table_count": len(tables), "tables": tables}
27
+
28
+
29
+ def get_docx_metadata(file_path: str) -> dict:
30
+ """Word hujjatining metama'lumotlarini (muallif, sarlavha va h.k.) qaytaradi."""
31
+ doc = Document(file_path)
32
+ props = doc.core_properties
33
+ return {
34
+ "file_path": file_path,
35
+ "title": props.title,
36
+ "author": props.author,
37
+ "created": str(props.created) if props.created else None,
38
+ "modified": str(props.modified) if props.modified else None,
39
+ "paragraph_count": len(doc.paragraphs),
40
+ "table_count": len(doc.tables),
41
+ }
@@ -0,0 +1,141 @@
1
+ """DOCX (Word) fayllarni yaratish va tahrirlash (yozish) funksiyalari."""
2
+
3
+ import os
4
+ from docx import Document
5
+ from docx.shared import Pt, Inches
6
+ from docx.enum.text import WD_BREAK
7
+
8
+
9
+ def create_docx(
10
+ file_path: str,
11
+ title: str = None,
12
+ paragraphs: list = None,
13
+ table_data: list = None,
14
+ overwrite: bool = False,
15
+ ) -> dict:
16
+ """Yangi Word (.docx) hujjat yaratadi.
17
+
18
+ paragraphs: matn qatorlari ro'yxati (har biri alohida paragraf bo'ladi).
19
+ table_data: jadval uchun 2-o'lchamli ro'yxat, masalan
20
+ [["Ism", "Yosh"], ["Ali", "25"], ["Vali", "30"]] — birinchi qator sarlavha
21
+ sifatida qalin (bold) qilib chiqariladi.
22
+ """
23
+ if os.path.exists(file_path) and not overwrite:
24
+ return {"error": f"Fayl allaqachon mavjud: {file_path} (overwrite=True qiling)"}
25
+
26
+ doc = Document()
27
+
28
+ if title:
29
+ doc.add_heading(title, level=1)
30
+
31
+ for para in paragraphs or []:
32
+ doc.add_paragraph(para)
33
+
34
+ if table_data:
35
+ rows = len(table_data)
36
+ cols = len(table_data[0]) if rows else 0
37
+ if rows and cols:
38
+ table = doc.add_table(rows=rows, cols=cols)
39
+ table.style = "Light Grid Accent 1"
40
+ for r, row_data in enumerate(table_data):
41
+ for c, cell_value in enumerate(row_data):
42
+ cell = table.cell(r, c)
43
+ cell.text = str(cell_value)
44
+ if r == 0:
45
+ for p in cell.paragraphs:
46
+ for run in p.runs:
47
+ run.font.bold = True
48
+
49
+ os.makedirs(os.path.dirname(os.path.abspath(file_path)) or ".", exist_ok=True)
50
+ doc.save(file_path)
51
+
52
+ return {
53
+ "file_path": file_path,
54
+ "status": "created",
55
+ "paragraph_count": len(paragraphs or []),
56
+ "table_added": bool(table_data),
57
+ }
58
+
59
+
60
+ def append_docx_text(file_path: str, text: str, bold: bool = False) -> dict:
61
+ """Mavjud Word hujjatining oxiriga yangi paragraf qo'shadi."""
62
+ if not os.path.exists(file_path):
63
+ return {"error": f"Fayl topilmadi: {file_path}"}
64
+
65
+ doc = Document(file_path)
66
+ p = doc.add_paragraph()
67
+ run = p.add_run(text)
68
+ run.bold = bold
69
+ doc.save(file_path)
70
+
71
+ return {"file_path": file_path, "status": "appended", "paragraph_count": len(doc.paragraphs)}
72
+
73
+
74
+ def replace_docx_text(file_path: str, find: str, replace: str, save_as: str = None) -> dict:
75
+ """Hujjatdagi barcha 'find' matnini 'replace' bilan almashtiradi.
76
+
77
+ save_as berilmasa, asl faylning ustiga yoziladi.
78
+ """
79
+ if not os.path.exists(file_path):
80
+ return {"error": f"Fayl topilmadi: {file_path}"}
81
+
82
+ doc = Document(file_path)
83
+ replacements = 0
84
+
85
+ for p in doc.paragraphs:
86
+ if find in p.text:
87
+ for run in p.runs:
88
+ if find in run.text:
89
+ run.text = run.text.replace(find, replace)
90
+ replacements += 1
91
+
92
+ for table in doc.tables:
93
+ for row in table.rows:
94
+ for cell in row.cells:
95
+ if find in cell.text:
96
+ cell.text = cell.text.replace(find, replace)
97
+ replacements += 1
98
+
99
+ out_path = save_as or file_path
100
+ doc.save(out_path)
101
+
102
+ return {"file_path": out_path, "status": "replaced", "replacements": replacements}
103
+
104
+
105
+ def add_docx_heading(file_path: str, text: str, level: int = 1) -> dict:
106
+ """Mavjud Word hujjatiga sarlavha (heading, 1-9 daraja) qo'shadi."""
107
+ if not os.path.exists(file_path):
108
+ return {"error": f"Fayl topilmadi: {file_path}"}
109
+ if not 0 <= level <= 9:
110
+ return {"error": "level 0 dan 9 gacha bo'lishi kerak"}
111
+
112
+ doc = Document(file_path)
113
+ doc.add_heading(text, level=level)
114
+ doc.save(file_path)
115
+ return {"file_path": file_path, "status": "heading_added", "level": level}
116
+
117
+
118
+ def add_docx_image(file_path: str, image_path: str, width_inches: float = 6.0) -> dict:
119
+ """Mavjud Word hujjatining oxiriga rasm qo'shadi."""
120
+ if not os.path.exists(file_path):
121
+ return {"error": f"Hujjat topilmadi: {file_path}"}
122
+ if not os.path.exists(image_path):
123
+ return {"error": f"Rasm fayli topilmadi: {image_path}"}
124
+
125
+ doc = Document(file_path)
126
+ doc.add_picture(image_path, width=Inches(width_inches))
127
+ doc.save(file_path)
128
+ return {"file_path": file_path, "status": "image_added", "image_path": image_path}
129
+
130
+
131
+ def add_docx_page_break(file_path: str) -> dict:
132
+ """Mavjud Word hujjatining oxiriga sahifa bo'linishi (page break) qo'shadi."""
133
+ if not os.path.exists(file_path):
134
+ return {"error": f"Fayl topilmadi: {file_path}"}
135
+
136
+ doc = Document(file_path)
137
+ p = doc.add_paragraph()
138
+ run = p.add_run()
139
+ run.add_break(WD_BREAK.PAGE)
140
+ doc.save(file_path)
141
+ return {"file_path": file_path, "status": "page_break_added"}
@@ -0,0 +1,114 @@
1
+ """
2
+ Eski Microsoft formatlarini (.doc, .xls, .ppt) LibreOffice (soffice) yordamida
3
+ yangi formatga (.docx, .xlsx, .pptx) konvertatsiya qilib, keyin mavjud
4
+ docx_tool / xlsx_tool / pptx_tool funksiyalari bilan o'qish uchun modul.
5
+
6
+ Talab: tizimda `soffice` (LibreOffice) o'rnatilgan bo'lishi kerak.
7
+ """
8
+
9
+ import os
10
+ import subprocess
11
+ import tempfile
12
+ import uuid
13
+
14
+ _LEGACY_MAP = {
15
+ ".doc": "docx",
16
+ ".xls": "xlsx",
17
+ ".ppt": "pptx",
18
+ }
19
+
20
+
21
+ def is_legacy_format(file_path: str) -> bool:
22
+ """Fayl eski (.doc/.xls/.ppt) formatdami — shuni tekshiradi."""
23
+ ext = os.path.splitext(file_path)[1].lower()
24
+ return ext in _LEGACY_MAP
25
+
26
+
27
+ def convert_legacy_file(file_path: str, timeout: int = 60) -> str:
28
+ """Eski formatdagi faylni vaqtinchalik papkada yangi formatga o'giradi.
29
+
30
+ Qaytaradi: yangi (.docx/.xlsx/.pptx) faylning to'liq yo'li.
31
+ Chaqiruvchi tugagach vaqtinchalik faylni o'zi tozalashi kerak (yoki
32
+ read_legacy_file wrapper'idan foydalaning — u avtomatik tozalaydi).
33
+ """
34
+ ext = os.path.splitext(file_path)[1].lower()
35
+ if ext not in _LEGACY_MAP:
36
+ raise ValueError(f"Qo'llab-quvvatlanmaydigan eski format: {ext}")
37
+
38
+ target_format = _LEGACY_MAP[ext]
39
+ out_dir = os.path.join(tempfile.gettempdir(), f"ms_toolkit_legacy_{uuid.uuid4().hex}")
40
+ os.makedirs(out_dir, exist_ok=True)
41
+
42
+ result = subprocess.run(
43
+ [
44
+ "soffice",
45
+ "--headless",
46
+ "--convert-to",
47
+ target_format,
48
+ "--outdir",
49
+ out_dir,
50
+ file_path,
51
+ ],
52
+ capture_output=True,
53
+ text=True,
54
+ timeout=timeout,
55
+ )
56
+
57
+ if result.returncode != 0:
58
+ raise RuntimeError(f"LibreOffice konvertatsiya xatosi: {result.stderr or result.stdout}")
59
+
60
+ base_name = os.path.splitext(os.path.basename(file_path))[0]
61
+ converted_path = os.path.join(out_dir, f"{base_name}.{target_format}")
62
+
63
+ if not os.path.exists(converted_path):
64
+ raise RuntimeError(
65
+ f"Konvertatsiya qilingan fayl topilmadi: {converted_path} (chiqish: {result.stdout})"
66
+ )
67
+
68
+ return converted_path
69
+
70
+
71
+ def read_legacy_file(file_path: str) -> dict:
72
+ """Eski (.doc/.xls/.ppt) faylni avtomatik aniqlab, mos reader bilan o'qiydi.
73
+
74
+ Ichki ravishda yangi formatga o'giradi, tegishli o'qish funksiyasini
75
+ chaqiradi va natijaga original fayl yo'li va formatni qo'shib qaytaradi.
76
+ """
77
+ from .docx_tool import read_docx_text
78
+ from .xlsx_tool import read_xlsx_data
79
+ from .pptx_tool import read_pptx_text
80
+
81
+ ext = os.path.splitext(file_path)[1].lower()
82
+ if ext not in _LEGACY_MAP:
83
+ return {"error": f"Qo'llab-quvvatlanmaydigan eski format: {ext}"}
84
+
85
+ if not os.path.exists(file_path):
86
+ return {"error": f"Fayl topilmadi: {file_path}"}
87
+
88
+ converted_path = None
89
+ try:
90
+ converted_path = convert_legacy_file(file_path)
91
+ target_format = _LEGACY_MAP[ext]
92
+
93
+ if target_format == "docx":
94
+ result = read_docx_text(converted_path)
95
+ elif target_format == "xlsx":
96
+ result = read_xlsx_data(converted_path)
97
+ else:
98
+ result = read_pptx_text(converted_path)
99
+
100
+ result["original_file"] = file_path
101
+ result["original_format"] = ext
102
+ return result
103
+ except subprocess.TimeoutExpired:
104
+ return {"error": "Konvertatsiya vaqti tugadi (timeout)"}
105
+ except Exception as e:
106
+ return {"error": f"{type(e).__name__}: {e}"}
107
+ finally:
108
+ if converted_path and os.path.exists(converted_path):
109
+ out_dir = os.path.dirname(converted_path)
110
+ try:
111
+ os.remove(converted_path)
112
+ os.rmdir(out_dir)
113
+ except OSError:
114
+ pass
ms_toolkit/pdf_tool.py ADDED
@@ -0,0 +1,56 @@
1
+ """PDF fayllarni o'qish funksiyalari (pdfplumber — matn/jadval, pypdf — metama'lumot)."""
2
+
3
+ import os
4
+ import pdfplumber
5
+ from pypdf import PdfReader
6
+
7
+
8
+ def read_pdf_text(file_path: str, max_pages: int = None) -> dict:
9
+ """PDF fayldan barcha (yoki birinchi max_pages ta) sahifa matnini o'qiydi."""
10
+ if not os.path.exists(file_path):
11
+ return {"error": f"Fayl topilmadi: {file_path}"}
12
+
13
+ pages_text = []
14
+ with pdfplumber.open(file_path) as pdf:
15
+ pages = pdf.pages[:max_pages] if max_pages else pdf.pages
16
+ for i, page in enumerate(pages):
17
+ pages_text.append(page.extract_text() or "")
18
+
19
+ return {
20
+ "file_path": file_path,
21
+ "page_count": len(pages_text),
22
+ "text": "\n\n".join(pages_text),
23
+ }
24
+
25
+
26
+ def read_pdf_tables(file_path: str) -> dict:
27
+ """PDF fayldagi har bir sahifadan jadvallarni ajratib oladi."""
28
+ if not os.path.exists(file_path):
29
+ return {"error": f"Fayl topilmadi: {file_path}"}
30
+
31
+ all_tables = []
32
+ with pdfplumber.open(file_path) as pdf:
33
+ for page_num, page in enumerate(pdf.pages, start=1):
34
+ for table in page.extract_tables():
35
+ all_tables.append({"page": page_num, "table": table})
36
+
37
+ return {"file_path": file_path, "table_count": len(all_tables), "tables": all_tables}
38
+
39
+
40
+ def get_pdf_metadata(file_path: str) -> dict:
41
+ """PDF faylning metama'lumotini (sarlavha, muallif, sahifalar soni) qaytaradi."""
42
+ if not os.path.exists(file_path):
43
+ return {"error": f"Fayl topilmadi: {file_path}"}
44
+
45
+ reader = PdfReader(file_path)
46
+ meta = reader.metadata or {}
47
+
48
+ return {
49
+ "file_path": file_path,
50
+ "page_count": len(reader.pages),
51
+ "title": meta.get("/Title"),
52
+ "author": meta.get("/Author"),
53
+ "subject": meta.get("/Subject"),
54
+ "creator": meta.get("/Creator"),
55
+ "encrypted": reader.is_encrypted,
56
+ }