pysfi 0.1.11__py3-none-any.whl → 0.1.12__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pysfi
3
- Version: 0.1.11
3
+ Version: 0.1.12
4
4
  Summary: Single File commands for Interactive python.
5
5
  Requires-Python: >=3.8
6
6
  Requires-Dist: tomli>=2.4.0; python_version < '3.11'
@@ -17,6 +17,7 @@ Requires-Dist: pyside2>=5.15.2.1; extra == 'all'
17
17
  Requires-Dist: pytesseract>=0.3.10; extra == 'all'
18
18
  Requires-Dist: python-docx>=1.1.0; extra == 'all'
19
19
  Requires-Dist: python-pptx>=0.6.21; extra == 'all'
20
+ Requires-Dist: pywin32>=311; (sys_platform == 'win32') and extra == 'all'
20
21
  Provides-Extra: extra
21
22
  Requires-Dist: ebooklib>=0.18; extra == 'extra'
22
23
  Requires-Dist: markdown>=3.5; extra == 'extra'
@@ -33,6 +34,7 @@ Requires-Dist: openpyxl>=3.1.0; extra == 'office'
33
34
  Requires-Dist: pymupdf>=1.24.11; extra == 'office'
34
35
  Requires-Dist: python-docx>=1.1.0; extra == 'office'
35
36
  Requires-Dist: python-pptx>=0.6.21; extra == 'office'
37
+ Requires-Dist: pywin32>=311; (sys_platform == 'win32') and extra == 'office'
36
38
  Description-Content-Type: text/markdown
37
39
 
38
40
  # pysfi
@@ -1,12 +1,13 @@
1
- sfi/__init__.py,sha256=W9IqwgCQr3sYmRyRC3ryCaJDJcMWBtyLzxp5CVsT3Nc,75
1
+ sfi/__init__.py,sha256=nJ-7R5yswQ4ew8RsZyWKYV8E6Xq9EGrknyOhesxJBSU,75
2
2
  sfi/cli.py,sha256=bUUTOg18sJQbSKSfsVANhlMgSj9yzO2txIzFAd9B2Ok,296
3
3
  sfi/alarmclock/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  sfi/alarmclock/alarmclock.py,sha256=0HoacKlGdYq_hINAdl54Cz2E_z6nNjPyqif2xcEBQss,12381
5
- sfi/bumpversion/__init__.py,sha256=j3XC03YiSDWRJV6UOcDWWsp09STfV5LrvzFkjsehSwA,86
5
+ sfi/bumpversion/__init__.py,sha256=v7s0aqGOHApzAHVwDoBCCtuqmjYvDEEVKS-VIAKN3PE,86
6
6
  sfi/bumpversion/bumpversion.py,sha256=HOyHLaE0sZajrlcVZ8hsim8mPjz77qwQVSo6aIzjMXE,20735
7
7
  sfi/cleanbuild/cleanbuild.py,sha256=Fr6_cr3rj4llcEQ8yNTK-DHdSzmx1I4hYFJJHu5YEz0,5200
8
8
  sfi/condasetup/condasetup.py,sha256=RlbXVYcAJYMau-ZzHOMzHrHl4r-lqNZO0bT-zWuzP_k,4581
9
- sfi/docscan/__init__.py,sha256=qKkwfRoVBNMzNNdQk69QnFUrmJACtW9qbvoRloTDHfk,121
9
+ sfi/docdiff/docdiff.py,sha256=RIq5cL2WnLohfqY-PRxlwhRJoYEUOOY1hpCAbfhzStk,7734
10
+ sfi/docscan/__init__.py,sha256=Zog5sFgdZvJmsmNgGBSsyvu-Gcb1ecYf-l6e5ThSOH4,121
10
11
  sfi/docscan/docscan.py,sha256=rk8mjEI2SKNIliV-Yb41pfUmYBQ1tUhk5LHUNEjkszI,41890
11
12
  sfi/docscan/docscan_gui.py,sha256=T_blCyGGaWxL6rtjLIYW3nGdX8DpLQv73YbDnITR4eg,50671
12
13
  sfi/docscan/lang/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -16,6 +17,7 @@ sfi/filedate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
17
  sfi/filedate/filedate.py,sha256=5FARcsB2Rlz2uTBxeYYjbIEJb9l1cyXj9WSoNKvSrRo,6068
17
18
  sfi/gittool/__init__.py,sha256=Xqxw7UUX-TKkWOCB1QHq8AdIKTkU7x87Xr-E0yVmObA,24
18
19
  sfi/gittool/gittool.py,sha256=BBE6gm9qP1fAWLqKprmsf7bOFgDvBvia8_bMaXc7dR4,11960
20
+ sfi/llmclient/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
21
  sfi/llmclient/llmclient.py,sha256=SnFZ9c2cNvFeLeobJV1ls7Ewftaam4s-HVBYW2tgHPo,21706
20
22
  sfi/llmquantize/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
23
  sfi/llmquantize/llmquantize.py,sha256=ILmfdJg7Rc7xAygfcVgkSKJ_qRAHDRZXjBymYFBy6fg,17693
@@ -54,7 +56,7 @@ sfi/which/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
56
  sfi/which/which.py,sha256=zVIAwZA-pGGngxkkwZ6IxDX3ozVHg7cLSYwYO9FjaIc,2439
55
57
  sfi/workflowengine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
58
  sfi/workflowengine/workflowengine.py,sha256=ck5PjyyjtWtbjN4ePEKsTWV6QR-BUlrfwrY6jih52jQ,17055
57
- pysfi-0.1.11.dist-info/METADATA,sha256=4dkuiM92FRkYo-8ti_ZEmAxq1eoFa6HF0xs8ErJyc68,4047
58
- pysfi-0.1.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
59
- pysfi-0.1.11.dist-info/entry_points.txt,sha256=FhBsBY75x9e-AAyJ0t0HD1rb90FDllsM_IdVGHWZx9o,1099
60
- pysfi-0.1.11.dist-info/RECORD,,
59
+ pysfi-0.1.12.dist-info/METADATA,sha256=ahFnr70NGB0JnR_EHBiVbzFt6wLgxaY5ZAvuP8A8bCo,4198
60
+ pysfi-0.1.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
61
+ pysfi-0.1.12.dist-info/entry_points.txt,sha256=HwFhwv46r2HgTfGgV0Rvhv7p84XXg9C3tVRiUcA4_sE,1134
62
+ pysfi-0.1.12.dist-info/RECORD,,
@@ -3,6 +3,7 @@ alarmclk = sfi.alarmclock.alarmclock:main
3
3
  bumpversion = sfi.bumpversion.bumpversion:main
4
4
  cleanbuild = sfi.cleanbuild.cleanbuild:main
5
5
  condasetup = sfi.condasetup.condasetup:main
6
+ docdiff = sfi.docdiff.docdiff:main
6
7
  docscan = sfi.docscan.docscan:main
7
8
  docscan-gui = sfi.docscan.docscan_gui:main
8
9
  filedate = sfi.filedate.filedate:main
sfi/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Single File commands for Interactive python."""
2
2
 
3
- __version__ = "0.1.11"
3
+ __version__ = "0.1.12"
@@ -1,3 +1,3 @@
1
1
  """Bumpversion - Automated version number management tool."""
2
2
 
3
- __version__ = "0.1.11"
3
+ __version__ = "0.1.12"
sfi/docdiff/docdiff.py ADDED
@@ -0,0 +1,238 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import atexit
5
+ import json
6
+ import logging
7
+ import platform
8
+ import subprocess
9
+ import time
10
+ from dataclasses import dataclass
11
+ from functools import cached_property
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ CONFIG_FILE = Path.home() / ".sfi" / "docdiff.json"
16
+
17
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class DocDiffConfig:
23
+ """Document comparison configuration."""
24
+
25
+ DOC_DIFF_TITLE: str = "Comparison Result"
26
+ OUTPUT_DIR: str = str(Path.home()) # Use current directory if empty
27
+ COMPARE_MODE: str = "original" # Options: original, revised
28
+ SHOW_CHANGES: bool = True
29
+ TRACK_REVISIONS: bool = True
30
+
31
+ def __init__(self) -> None:
32
+ if CONFIG_FILE.exists():
33
+ logger.info("Loading configuration from %s", CONFIG_FILE)
34
+ config_data = json.loads(CONFIG_FILE.read_text())
35
+ # Update configuration items, keeping defaults as fallback
36
+ for key, value in config_data.items():
37
+ if hasattr(self, key):
38
+ setattr(self, key, value)
39
+ else:
40
+ logger.info("Using default configuration")
41
+
42
+ def save(self) -> None:
43
+ """Save configuration."""
44
+ CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
45
+ CONFIG_FILE.write_text(json.dumps(vars(self), indent=4))
46
+
47
+
48
+ conf = DocDiffConfig()
49
+ atexit.register(conf.save)
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class DiffDocCommand:
54
+ """Document comparison command."""
55
+
56
+ old_doc: Path
57
+ new_doc: Path
58
+ output_path: Path | None = None
59
+
60
+ def run(self) -> None:
61
+ """Run the document comparison command."""
62
+ if platform.system() != "Windows":
63
+ logger.error("This tool is only available on Windows.")
64
+ return
65
+ if not self.old_doc.exists():
66
+ logger.error(f"Old file does not exist: {self.old_doc}")
67
+ return
68
+
69
+ if not self.new_doc.exists():
70
+ logger.error(f"New file does not exist: {self.new_doc}")
71
+ return
72
+
73
+ if not self.validate_files:
74
+ logger.error("Invalid file paths or extensions")
75
+ return
76
+
77
+ if self.word_app is None:
78
+ logger.error("Word application is not available")
79
+ return
80
+
81
+ if self.compare_data is None:
82
+ return
83
+
84
+ self.output.parent.mkdir(parents=True, exist_ok=True)
85
+ try:
86
+ self.compare_data.SaveAs2(str(self.output))
87
+ self.compare_data.Close()
88
+ except Exception as e:
89
+ logger.exception(f"Comparison failed: {e}")
90
+ else:
91
+ logger.info(f"Comparison completed. Saved to: {self.output}")
92
+ finally:
93
+ try:
94
+ self.word_app.Documents.Close(SaveChanges=False)
95
+ except Exception:
96
+ logger.exception("Close document failed!")
97
+ else:
98
+ self.word_app.Quit()
99
+
100
+ try:
101
+ subprocess.run(
102
+ ["taskkill", "/f", "/t", "/im", "WINWORD.EXE"], check=False
103
+ )
104
+ except Exception:
105
+ logger.exception("Taskkill failed!")
106
+ else:
107
+ logger.info("Taskkill completed successfully")
108
+
109
+ @cached_property
110
+ def word_app(self) -> Any:
111
+ try:
112
+ import win32com.client as win32 # type: ignore
113
+ except ImportError:
114
+ logger.exception("win32com.client is not installed, exiting.")
115
+ raise
116
+ else:
117
+ logger.info("Started Word application")
118
+ app = win32.gencache.EnsureDispatch("Word.Application") # type: ignore
119
+ app.Visible = False
120
+ app.DisplayAlerts = False
121
+
122
+ try:
123
+ app.Options.TrackRevisions = conf.TRACK_REVISIONS
124
+ except AttributeError:
125
+ logger.warning(
126
+ "TrackRevisions option not available in this Word version"
127
+ )
128
+
129
+ return app
130
+
131
+ @cached_property
132
+ def validate_files(self) -> bool:
133
+ return all([
134
+ self.old_doc.exists(),
135
+ self.new_doc.exists(),
136
+ self.old_doc.suffix.lower() in [".doc", ".docx"],
137
+ self.new_doc.suffix.lower() in [".doc", ".docx"],
138
+ ])
139
+
140
+ @cached_property
141
+ def compare_data(self) -> Any:
142
+ try:
143
+ compared = self.word_app.CompareDocuments(
144
+ self.old_doc_data,
145
+ self.new_doc_data,
146
+ 0,
147
+ 2 if conf.COMPARE_MODE == "revised" else 0,
148
+ True,
149
+ )
150
+ except Exception as e:
151
+ logger.exception(f"Comparison failed: {e}")
152
+ return None
153
+ else:
154
+ if compared:
155
+ logger.info("Comparison completed successfully")
156
+ compared.ShowRevisions = conf.SHOW_CHANGES
157
+ return compared
158
+ return None
159
+
160
+ @cached_property
161
+ def old_doc_data(self) -> Any:
162
+ logger.info(f"Opening old file: {self.old_doc}")
163
+ return self.word_app.Documents.Open(str(self.old_doc.resolve()))
164
+
165
+ @cached_property
166
+ def new_doc_data(self) -> Any:
167
+ logger.info(f"Opening new file: {self.new_doc}")
168
+ return self.word_app.Documents.Open(str(self.new_doc.resolve()))
169
+
170
+ @cached_property
171
+ def output(self) -> Path:
172
+ """Determine the output directory for the comparison result."""
173
+ output_filename = (
174
+ f"{conf.DOC_DIFF_TITLE}@{time.strftime('%Y%m%d_%H_%M_%S')}.docx"
175
+ )
176
+
177
+ if self.output_path is None:
178
+ output_dir = (
179
+ Path(conf.OUTPUT_DIR) if conf.OUTPUT_DIR else self.new_doc.parent
180
+ )
181
+ return output_dir / output_filename
182
+
183
+ if self.output_path.is_dir():
184
+ return self.output_path / output_filename
185
+ elif self.output_path.is_file():
186
+ return self.output_path
187
+ else:
188
+ raise ValueError(f"Invalid output path: {self.output_path}")
189
+
190
+
191
+ def parse_args():
192
+ parser = argparse.ArgumentParser(description="Compare two doc/docx files.")
193
+ parser.add_argument(
194
+ "files", nargs=2, help="Two input files to compare (old_file new_file)"
195
+ )
196
+ parser.add_argument(
197
+ "-o", "--output", dest="output", default=".", help="Output file path"
198
+ )
199
+ parser.add_argument("--title", help="Title for the comparison result")
200
+ parser.add_argument(
201
+ "--show-changes", action="store_true", help="Show changes in the comparison"
202
+ )
203
+ parser.add_argument(
204
+ "--hide-changes", action="store_true", help="Hide changes in the comparison"
205
+ )
206
+ parser.add_argument(
207
+ "--compare-mode",
208
+ choices=["original", "revised"],
209
+ help="Compare mode: original or revised",
210
+ )
211
+ parser.add_argument("--output-dir", help="Output directory for the result file")
212
+
213
+ args = parser.parse_args()
214
+
215
+ # Update configuration from command line arguments
216
+ if args.title:
217
+ conf.DOC_DIFF_TITLE = args.title
218
+ if args.show_changes:
219
+ conf.SHOW_CHANGES = True
220
+ if args.hide_changes:
221
+ conf.SHOW_CHANGES = False
222
+ if args.compare_mode:
223
+ conf.COMPARE_MODE = args.compare_mode
224
+ if args.output_dir:
225
+ conf.OUTPUT_DIR = args.output_dir
226
+
227
+ return args
228
+
229
+
230
+ def main() -> None:
231
+ """Compare two doc/docx files."""
232
+ args = parse_args()
233
+
234
+ DiffDocCommand(
235
+ Path(args.files[0]),
236
+ Path(args.files[1]),
237
+ Path(args.output),
238
+ ).run()
sfi/docscan/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Document scanner module for scanning and extracting content from various document formats."""
2
2
 
3
- __version__ = "0.1.11"
3
+ __version__ = "0.1.12"
File without changes
File without changes