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.
- {pysfi-0.1.11.dist-info → pysfi-0.1.12.dist-info}/METADATA +3 -1
- {pysfi-0.1.11.dist-info → pysfi-0.1.12.dist-info}/RECORD +9 -7
- {pysfi-0.1.11.dist-info → pysfi-0.1.12.dist-info}/entry_points.txt +1 -0
- sfi/__init__.py +1 -1
- sfi/bumpversion/__init__.py +1 -1
- sfi/docdiff/docdiff.py +238 -0
- sfi/docscan/__init__.py +1 -1
- sfi/llmclient/__init__.py +0 -0
- {pysfi-0.1.11.dist-info → pysfi-0.1.12.dist-info}/WHEEL +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pysfi
|
|
3
|
-
Version: 0.1.
|
|
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=
|
|
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=
|
|
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/
|
|
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.
|
|
58
|
-
pysfi-0.1.
|
|
59
|
-
pysfi-0.1.
|
|
60
|
-
pysfi-0.1.
|
|
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
sfi/bumpversion/__init__.py
CHANGED
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
|
File without changes
|
|
File without changes
|