pysfi 0.1.11__py3-none-any.whl → 0.1.13__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.13
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
@@ -0,0 +1,70 @@
1
+ sfi/__init__.py,sha256=dZ1dacNjP1yRdG5N4-UtVhSZGUdEzRv4usjC1kr-3jY,116
2
+ sfi/cli.py,sha256=QF6-bdw8vOUMmFovjp4lYpWesHtae4oxHeEWDnt7_k8,504
3
+ sfi/alarmclock/__init__.py,sha256=yyFexrNdi65kz3l68YjT5Bdlq4FFqBR80H8vV7e_D58,75
4
+ sfi/alarmclock/alarmclock.py,sha256=ixVkbg548smUivRsqyI3YSZ81BWIrKawnuezAp3BzyE,11635
5
+ sfi/bumpversion/__init__.py,sha256=ajzMFse8CnLshD5qpKEr4fze3tnup2S9GWWyd7xpC2A,127
6
+ sfi/bumpversion/bumpversion.py,sha256=HOyHLaE0sZajrlcVZ8hsim8mPjz77qwQVSo6aIzjMXE,20735
7
+ sfi/cleanbuild/__init__.py,sha256=V4WV0xUvTaNGxawfYqlLT98t_8FeiA7ec1NW6R0-pGE,101
8
+ sfi/cleanbuild/cleanbuild.py,sha256=Fr6_cr3rj4llcEQ8yNTK-DHdSzmx1I4hYFJJHu5YEz0,5200
9
+ sfi/condasetup/__init__.py,sha256=a99mtb8qROZYvqLuhmlasVCgbmAL9nzVzOJFrVSWLGE,3
10
+ sfi/condasetup/condasetup.py,sha256=RlbXVYcAJYMau-ZzHOMzHrHl4r-lqNZO0bT-zWuzP_k,4581
11
+ sfi/docdiff/__init__.py,sha256=a99mtb8qROZYvqLuhmlasVCgbmAL9nzVzOJFrVSWLGE,3
12
+ sfi/docdiff/docdiff.py,sha256=anilgq16icu-UxdgRR7B_57G9CFJ79xBSjOm9DOQluY,7736
13
+ sfi/docscan/__init__.py,sha256=HQzSYiozGNtCdC771DXce57YwdXzQiSWFQKaTnnjAQU,124
14
+ sfi/docscan/docscan.py,sha256=rk8mjEI2SKNIliV-Yb41pfUmYBQ1tUhk5LHUNEjkszI,41890
15
+ sfi/docscan/docscan_gui.py,sha256=c7sJXvq5201Kh7iewalc50qoMnNR_eEna0NqoEJ7igw,52335
16
+ sfi/docscan/lang/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ sfi/docscan/lang/eng.py,sha256=GcOcT9FLcPZRdJ-MbLRYyf6vDweZTQBu_zUnEFzRY84,8529
18
+ sfi/docscan/lang/zhcn.py,sha256=1SZwQjZF3oi9FsnzuZB-9v7P64sGm5oNmVjuL-rhcEQ,8885
19
+ sfi/filedate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ sfi/filedate/filedate.py,sha256=5FARcsB2Rlz2uTBxeYYjbIEJb9l1cyXj9WSoNKvSrRo,6068
21
+ sfi/gittool/__init__.py,sha256=Xqxw7UUX-TKkWOCB1QHq8AdIKTkU7x87Xr-E0yVmObA,24
22
+ sfi/gittool/gittool.py,sha256=BBE6gm9qP1fAWLqKprmsf7bOFgDvBvia8_bMaXc7dR4,11960
23
+ sfi/img2pdf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ sfi/img2pdf/img2pdf.py,sha256=rR6f5bMg-HoKMvSyu2rWfDbx0kmo6F6e1dg5z3710Wo,15129
25
+ sfi/llmclient/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ sfi/llmclient/llmclient.py,sha256=zvaT-HEiL3CM3uEpvzuseLPEqFQe6RO4_fhnA2djHo0,22681
27
+ sfi/llmquantize/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ sfi/llmquantize/llmquantize.py,sha256=N4h0RwdpVZdZM3qDWbL34hLGpQ3XuPkP_F8kyhhM8ZI,18855
29
+ sfi/llmserver/__init__.py,sha256=a99mtb8qROZYvqLuhmlasVCgbmAL9nzVzOJFrVSWLGE,3
30
+ sfi/llmserver/llmserver.py,sha256=Fm4Go7wif4xMGomMFDsyJnYMafXsWemGkr-VfaeYa6w,13530
31
+ sfi/makepython/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
+ sfi/makepython/makepython.py,sha256=srinUE1Yr6zF-NjiUFVC1sUrpy8fYpDnCs7p648jWqc,23278
33
+ sfi/pdfsplit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ sfi/pdfsplit/pdfsplit.py,sha256=QWtW3GU28U2ZOyN5sCbH7jEMBpNbuAIzjXWOAXXW44M,6209
35
+ sfi/pyarchive/__init__.py,sha256=a99mtb8qROZYvqLuhmlasVCgbmAL9nzVzOJFrVSWLGE,3
36
+ sfi/pyarchive/pyarchive.py,sha256=OnPbIRA0C9JdeyNsVZ6rJg7ExItKyJz4jVw5W4c92DA,38293
37
+ sfi/pyembedinstall/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
+ sfi/pyembedinstall/pyembedinstall.py,sha256=LHnuvr63DXuntdS6a_7uQynOfarK-30WBUerSzawSHE,24171
39
+ sfi/pylibpack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
+ sfi/pylibpack/pylibpack.py,sha256=qK6J2_0VLMWQsKjcZ5njsvfqCnMRmGx8RweH2olhWTo,57818
41
+ sfi/pylibpack/rules/numpy.json,sha256=ee4gA5NBudFi3MaJA-QlBKQwiQAUb-eluF8HNVkl7Vk,384
42
+ sfi/pylibpack/rules/pymupdf.json,sha256=Hkzh8dvXKCzKx4aeHbu5E0qwgfbwQxZH2VLtQZzlMO4,153
43
+ sfi/pylibpack/rules/pyqt5.json,sha256=JKGnVSUMfXGR5XK1sbL1F6cAsEhl7hK12QkrulAB00M,374
44
+ sfi/pylibpack/rules/pyside2.json,sha256=uSSteT-3wDohWwQ36Z5mSOaSbxrR4565In4uZj_eR4w,557
45
+ sfi/pylibpack/rules/scipy.json,sha256=vTSi3W5BGWcwMkaDnyD6Yg7ijZdicPEUMw4fnRTnNf4,468
46
+ sfi/pylibpack/rules/shiboken2.json,sha256=9Pl3eslvergyjlyHNknkyN0oZlcH3049WULe5WjsmKM,515
47
+ sfi/pyloadergen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
+ sfi/pyloadergen/pyloadergen.py,sha256=VWJzc0opmMVthHE_RGbWeLe-DBaMn94gZo2yYLkf8cI,45696
49
+ sfi/pypack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
+ sfi/pypack/pypack.py,sha256=3LTCZMk8TqYhJbGv9AMLbe_Vi6bV7-cQUuHrWWO7QZU,22385
51
+ sfi/pyprojectparse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ sfi/pyprojectparse/pyprojectparse.py,sha256=pHuwVCpVUlRoC9pRo3r6d0DfqqoEbsREsUuYjnkhqhU,30575
53
+ sfi/pysourcepack/__init__.py,sha256=a99mtb8qROZYvqLuhmlasVCgbmAL9nzVzOJFrVSWLGE,3
54
+ sfi/pysourcepack/pysourcepack.py,sha256=5_KrI2Y1TKKcoYfsFpTNXWguj6n8CKdca8lgoBCsL8k,12160
55
+ sfi/quizbase/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
+ sfi/quizbase/quizbase.py,sha256=3tPUuYexZ9TVsNPPO_Itmr5OvyHSgY5OSUZwPoQt9zg,30605
57
+ sfi/quizbase/quizbase_gui.py,sha256=m_Lj3au1a8gEv5x7KOTjomiP1NpXHUgHSPE4lLv63hY,34733
58
+ sfi/regexvalidate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
+ sfi/regexvalidate/regex_help.html,sha256=3ltx3nh-Y5kkbHy5D67KfWtLig3u5XEhIlPHdHLEuTE,12436
60
+ sfi/regexvalidate/regexvalidate.py,sha256=5C_M2EKt9Jlonq03v9zrqtsFfAKK3D1vF1kBxD6iUpE,18600
61
+ sfi/taskkill/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
+ sfi/taskkill/taskkill.py,sha256=wM9g8sWJVTy4GxXe26rKdax2lIBI-uH9wP5wRenriH4,11606
63
+ sfi/which/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
+ sfi/which/which.py,sha256=2YbGgSiT1ySapKVV1ESoPf4P-JU8vvzmsZY39NiVr6k,2596
65
+ sfi/workflowengine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
+ sfi/workflowengine/workflowengine.py,sha256=pPRsxWB2ZoDwcVTjsDlpiml-xZYiZBKjLcINu4TGBcE,19209
67
+ pysfi-0.1.13.dist-info/METADATA,sha256=6MAsufstxqnCpDvLj0O0xA6ZjTMvMAhwlccsrvu9uT4,4198
68
+ pysfi-0.1.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
69
+ pysfi-0.1.13.dist-info/entry_points.txt,sha256=ju_Bwp3L8-5Bpcj0PSCCDL8vGsGb4ZYpjfzprSdFrfA,1215
70
+ pysfi-0.1.13.dist-info/RECORD,,
@@ -3,10 +3,12 @@ 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
9
10
  gitt = sfi.gittool.gittool:main
11
+ img2pdf = sfi.img2pdf.img2pdf:main
10
12
  llmcli = sfi.llmclient.llmclient:main
11
13
  llmqnt = sfi.llmquantize.llmquantize:main
12
14
  llmsvr = sfi.llmserver.llmserver:main
@@ -18,6 +20,7 @@ pylibpack = sfi.pylibpack.pylibpack:main
18
20
  pyloadergen = sfi.pyloadergen.pyloadergen:main
19
21
  pyp = sfi.pypack.pypack:main
20
22
  pypack = sfi.pypack.pypack:main
23
+ pypp = sfi.pyprojectparse.pyprojectparse:main
21
24
  pyprojectparse = sfi.pyprojectparse.pyprojectparse:main
22
25
  pysourcepack = sfi.pysourcepack.pysourcepack:main
23
26
  quizbase = sfi.quizbase.quizbase:main
sfi/__init__.py CHANGED
@@ -1,3 +1,5 @@
1
- """Single File commands for Interactive python."""
2
-
3
- __version__ = "0.1.11"
1
+ """Single File commands for Interactive python."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.13"
@@ -0,0 +1,3 @@
1
+ """Alarm clock module for pysfi."""
2
+
3
+ from __future__ import annotations
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import argparse
4
3
  import logging
5
4
  import random
6
5
  import sys
@@ -8,7 +7,6 @@ from dataclasses import dataclass
8
7
  from datetime import datetime, timedelta, timezone
9
8
  from functools import partial
10
9
 
11
- import qdarkstyle
12
10
  from PySide2.QtCore import QSize, Qt, QTime, QTimer
13
11
  from PySide2.QtGui import QCloseEvent
14
12
  from PySide2.QtWidgets import (
@@ -28,7 +26,7 @@ __version__ = "0.1.3"
28
26
  __build_date__ = "2026-01-22"
29
27
 
30
28
 
31
- @dataclass(frozen=True)
29
+ @dataclass
32
30
  class AlarmClockConfig:
33
31
  """Configuration for the alarm clock application."""
34
32
 
@@ -87,9 +85,9 @@ class DigitalClock(QLabel):
87
85
  logger.debug(f"Updated time: {current}")
88
86
 
89
87
  # Add blink effect
90
- self._color = random.choice(
91
- [_ for _ in config.DIGITAL_BORDER_COLORS if _ != self._color],
92
- )
88
+ self._color = random.choice([
89
+ c for c in config.DIGITAL_BORDER_COLORS if c != self._color
90
+ ])
93
91
  self.setStyleSheet(f"""
94
92
  font: {config.DIGITAL_FONT};
95
93
  color: {config.DIGITAL_COLOR};
@@ -112,6 +110,7 @@ class BlinkDialog(QDialog):
112
110
  self.windowFlags() | Qt.WindowStaysOnTopHint | Qt.WindowType.Dialog,
113
111
  )
114
112
  self.setFixedSize(QSize(400, 240))
113
+ self.setWindowFlag(Qt.WindowCloseButtonHint, False)
115
114
 
116
115
  layout = QVBoxLayout()
117
116
  msg_label = QLabel(config.BLINK_CONTENT)
@@ -122,32 +121,28 @@ class BlinkDialog(QDialog):
122
121
  msg_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
123
122
 
124
123
  close_button = QPushButton("Close Alarm")
125
- close_button.clicked.connect(self.accept)
124
+ close_button.clicked.connect(self.close_alarm)
126
125
 
127
126
  layout.addWidget(msg_label)
128
127
  layout.addWidget(close_button)
129
128
  self.setLayout(layout)
130
129
 
131
- # Prevent user from closing dialog by other means, ensure button click only
132
- self.setWindowFlag(Qt.WindowCloseButtonHint, False)
133
-
134
130
  # Blink control variables and timer
135
131
  self.blink_timer = QTimer(self)
136
132
  self.blink_timer.timeout.connect(self.update_blink)
137
133
  self.blink_state = False
138
134
  self.blink_type = config.BLINK_TYPE
139
-
140
- # Initialize style
141
135
  self.bg_color = random.choice(config.BLINK_BG_COLORS)
142
- self.origin_style = self.styleSheet()
143
136
  self.blink_timer.start(config.BLINK_INTERVAL)
144
137
 
145
138
  def update_blink(self) -> None:
146
139
  """Timer timeout, update blink state."""
147
140
  if self.blink_type == "color":
148
141
  # Color blink logic
149
- colors = [_ for _ in config.BLINK_BG_COLORS[:] if _ != self.bg_color]
150
- self.setStyleSheet(f"background-color: {random.choice(colors)}")
142
+ colors = [c for c in config.BLINK_BG_COLORS if c != self.bg_color]
143
+ new_color = random.choice(colors)
144
+ self.setStyleSheet(f"background-color: {new_color}")
145
+ self.bg_color = new_color
151
146
  elif self.blink_type == "opacity":
152
147
  # Opacity blink logic - Note: Some systems may not fully support window opacity
153
148
  new_opacity = 0.3 if self.blink_state else 1.0
@@ -155,10 +150,14 @@ class BlinkDialog(QDialog):
155
150
 
156
151
  self.blink_state = not self.blink_state # Toggle state
157
152
 
153
+ def close_alarm(self) -> None:
154
+ """Close alarm dialog and stop blinking."""
155
+ self.stop_blinking()
156
+ self.accept()
157
+
158
158
  def stop_blinking(self) -> None:
159
- """Stop blinking, restore original style."""
159
+ """Stop blinking."""
160
160
  self.blink_timer.stop()
161
- self.setStyleSheet(self.origin_style) # Restore original style
162
161
  self.setWindowOpacity(1.0) # Ensure opacity is restored
163
162
 
164
163
  def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
@@ -173,13 +172,6 @@ class AlarmClock(QMainWindow):
173
172
  def __init__(self) -> None:
174
173
  super().__init__()
175
174
  self.setWindowTitle(f"{config.ALARM_CLOCK_TITLE} v{__version__}")
176
- self.setGeometry(
177
- QApplication.desktop().screenGeometry().center().x() - self.width() // 4,
178
- QApplication.desktop().screenGeometry().center().y() - self.height() // 2,
179
- self.width(),
180
- self.height(),
181
- )
182
- self.adjustSize()
183
175
 
184
176
  # Set window style
185
177
  self.setStyleSheet("""
@@ -277,6 +269,13 @@ class AlarmClock(QMainWindow):
277
269
  self.status_label.setStyleSheet("color: #aaaaaa; font-size: 16px;")
278
270
  main_layout.addWidget(self.status_label)
279
271
 
272
+ # Center window on screen
273
+ self.adjustSize()
274
+ screen = QApplication.desktop().screenGeometry()
275
+ x = (screen.width() - self.width()) // 2
276
+ y = (screen.height() - self.height()) // 2
277
+ self.move(x, y)
278
+
280
279
  # Alarm timer
281
280
  self.alarm_timer = QTimer()
282
281
  self.alarm_timer.timeout.connect(self.check_alarm)
@@ -330,11 +329,6 @@ class AlarmClock(QMainWindow):
330
329
  dialog.exec_()
331
330
 
332
331
  self.status_label.setText("⏰ Alarm Rang! ⏰")
333
- self.status_label.setStyleSheet(
334
- "color: #ff5555; font-size: 18px; font-weight: bold;",
335
- )
336
-
337
- # Add blink effect
338
332
  self.status_label.setStyleSheet("""
339
333
  color: #ff0000;
340
334
  font-size: 18px;
@@ -350,18 +344,7 @@ class AlarmClock(QMainWindow):
350
344
 
351
345
 
352
346
  def main() -> None:
353
- parser = argparse.ArgumentParser(
354
- prog="alarmclock", description="Digital Alarm Clock"
355
- )
356
- parser.add_argument("-d", "--debug", action="store_true", help="Enable debug mode")
357
- parser.add_argument("-v", "--version", action="version", version=f"{__version__}")
358
-
359
- args = parser.parse_args()
360
- if args.debug:
361
- logger.setLevel(logging.DEBUG)
362
-
363
347
  app = QApplication(sys.argv)
364
- app.setStyleSheet(qdarkstyle.load_stylesheet_pyside2())
365
348
  window = AlarmClock()
366
349
  window.show()
367
350
  sys.exit(app.exec_())
@@ -1,3 +1,5 @@
1
- """Bumpversion - Automated version number management tool."""
2
-
3
- __version__ = "0.1.11"
1
+ """Bumpversion - Automated version number management tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.13"
@@ -0,0 +1,3 @@
1
+ """Cleanbuild - Clean build artifacts and temporary files."""
2
+
3
+ from __future__ import annotations
sfi/cli.py CHANGED
@@ -1,11 +1,21 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
2
4
 
3
5
  from sfi import __version__ as VERSION # noqa: N812
4
6
 
5
7
 
6
- def main():
8
+ def main() -> None:
9
+ """Main entry point for the pysfi CLI tool.
10
+
11
+ Parses command line arguments and handles version printing.
12
+ """
7
13
  parser = argparse.ArgumentParser()
8
14
  parser.add_argument(
9
- "-v", "--version", action="version", version=f"%(prog)s v{VERSION}", help="Print version and exit"
15
+ "-v",
16
+ "--version",
17
+ action="version",
18
+ version=f"%(prog)s v{VERSION}",
19
+ help="Print version and exit",
10
20
  )
11
21
  parser.parse_args()
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
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() / ".pysfi" / "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
- """Document scanner module for scanning and extracting content from various document formats."""
2
-
3
- __version__ = "0.1.11"
1
+ """Document scanner module for scanning and extracting content from various document formats."""
2
+
3
+ __version__ = "0.1.13"