pysfi 0.1.7__py3-none-any.whl → 0.1.11__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.
Files changed (55) hide show
  1. {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/METADATA +11 -9
  2. pysfi-0.1.11.dist-info/RECORD +60 -0
  3. pysfi-0.1.11.dist-info/entry_points.txt +28 -0
  4. sfi/__init__.py +1 -1
  5. sfi/alarmclock/alarmclock.py +40 -40
  6. sfi/bumpversion/__init__.py +1 -1
  7. sfi/cleanbuild/cleanbuild.py +155 -0
  8. sfi/condasetup/condasetup.py +116 -0
  9. sfi/docscan/__init__.py +1 -1
  10. sfi/docscan/docscan.py +407 -103
  11. sfi/docscan/docscan_gui.py +1282 -596
  12. sfi/docscan/lang/eng.py +152 -0
  13. sfi/docscan/lang/zhcn.py +170 -0
  14. sfi/filedate/filedate.py +185 -112
  15. sfi/gittool/__init__.py +2 -0
  16. sfi/gittool/gittool.py +401 -0
  17. sfi/llmclient/llmclient.py +592 -0
  18. sfi/llmquantize/llmquantize.py +480 -0
  19. sfi/llmserver/llmserver.py +335 -0
  20. sfi/makepython/makepython.py +31 -30
  21. sfi/pdfsplit/pdfsplit.py +173 -173
  22. sfi/pyarchive/pyarchive.py +418 -0
  23. sfi/pyembedinstall/pyembedinstall.py +629 -0
  24. sfi/pylibpack/__init__.py +0 -0
  25. sfi/pylibpack/pylibpack.py +1457 -0
  26. sfi/pylibpack/rules/numpy.json +22 -0
  27. sfi/pylibpack/rules/pymupdf.json +10 -0
  28. sfi/pylibpack/rules/pyqt5.json +19 -0
  29. sfi/pylibpack/rules/pyside2.json +23 -0
  30. sfi/pylibpack/rules/scipy.json +23 -0
  31. sfi/pylibpack/rules/shiboken2.json +24 -0
  32. sfi/pyloadergen/pyloadergen.py +512 -227
  33. sfi/pypack/__init__.py +0 -0
  34. sfi/pypack/pypack.py +1142 -0
  35. sfi/pyprojectparse/__init__.py +0 -0
  36. sfi/pyprojectparse/pyprojectparse.py +500 -0
  37. sfi/pysourcepack/pysourcepack.py +308 -0
  38. sfi/quizbase/__init__.py +0 -0
  39. sfi/quizbase/quizbase.py +828 -0
  40. sfi/quizbase/quizbase_gui.py +987 -0
  41. sfi/regexvalidate/__init__.py +0 -0
  42. sfi/regexvalidate/regex_help.html +284 -0
  43. sfi/regexvalidate/regexvalidate.py +468 -0
  44. sfi/taskkill/taskkill.py +0 -2
  45. sfi/workflowengine/__init__.py +0 -0
  46. sfi/workflowengine/workflowengine.py +444 -0
  47. pysfi-0.1.7.dist-info/RECORD +0 -31
  48. pysfi-0.1.7.dist-info/entry_points.txt +0 -15
  49. sfi/embedinstall/embedinstall.py +0 -418
  50. sfi/projectparse/projectparse.py +0 -152
  51. sfi/pypacker/fspacker.py +0 -91
  52. {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/WHEEL +0 -0
  53. /sfi/{embedinstall → docscan/lang}/__init__.py +0 -0
  54. /sfi/{projectparse → llmquantize}/__init__.py +0 -0
  55. /sfi/{pypacker → pyembedinstall}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pysfi
3
- Version: 0.1.7
3
+ Version: 0.1.11
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'
@@ -12,6 +12,7 @@ Requires-Dist: openpyxl>=3.1.0; extra == 'all'
12
12
  Requires-Dist: pillow>=10.0.0; extra == 'all'
13
13
  Requires-Dist: pymupdf>=1.24.11; extra == 'all'
14
14
  Requires-Dist: pypdf>=3.0.0; extra == 'all'
15
+ Requires-Dist: pyside2-stubs>=5.15.2.1.2; extra == 'all'
15
16
  Requires-Dist: pyside2>=5.15.2.1; extra == 'all'
16
17
  Requires-Dist: pytesseract>=0.3.10; extra == 'all'
17
18
  Requires-Dist: python-docx>=1.1.0; extra == 'all'
@@ -22,6 +23,7 @@ Requires-Dist: markdown>=3.5; extra == 'extra'
22
23
  Requires-Dist: odfpy>=1.4.1; extra == 'extra'
23
24
  Requires-Dist: pypdf>=3.0.0; extra == 'extra'
24
25
  Provides-Extra: gui
26
+ Requires-Dist: pyside2-stubs>=5.15.2.1.2; extra == 'gui'
25
27
  Requires-Dist: pyside2>=5.15.2.1; extra == 'gui'
26
28
  Provides-Extra: ocr
27
29
  Requires-Dist: pillow>=10.0.0; extra == 'ocr'
@@ -45,12 +47,12 @@ pysfi is a Python project that provides single-file command-line utilities, desi
45
47
 
46
48
  - **alarmclk**: Alarm clock functionality
47
49
  - **[bumpversion](sfi/bumpversion/README.md)**: Automated version number management tool
48
- - **embedinstall**: Embed installation utilities
50
+ - **pyembedinstall**: Embed installation utilities
49
51
  - **[filedate](sfi/filedate/README.md)**: A file date management tool that normalizes date prefixes in filenames
50
52
  - **mkp**: Make Python project utilities
51
- - **projectparse**: Project parsing and analysis tools
53
+ - **pyprojectparse**: Project parsing and analysis tools
52
54
  - **pyloadergen**: Python loader code generation
53
- - **pypacker**: Python packaging utilities
55
+ - **pypack**: Python packaging utilities
54
56
 
55
57
  ## Installation
56
58
 
@@ -99,8 +101,8 @@ pysfi/
99
101
  │ ├── alarmclock.py
100
102
  │ ├── pyproject.toml
101
103
  │ └── __init__.py
102
- ├── embedinstall/ # embedinstall command module
103
- │ ├── embedinstall.py
104
+ ├── pyembedinstall/ # pyembedinstall command module
105
+ │ ├── pyembedinstall.py
104
106
  │ ├── pyproject.toml
105
107
  │ └── __init__.py
106
108
  ├── filedate/ # filedate command module
@@ -112,15 +114,15 @@ pysfi/
112
114
  │ ├── makepython.py
113
115
  │ ├── pyproject.toml
114
116
  │ └── __init__.py
115
- ├── projectparse/ # projectparse command module
116
- │ ├── projectparse.py
117
+ ├── pyprojectparse/ # pyprojectparse command module
118
+ │ ├── pyprojectparse.py
117
119
  │ ├── pyproject.toml
118
120
  │ └── __init__.py
119
121
  ├── pyloadergen/ # pyloadergen command module
120
122
  │ ├── pyloadergen.py
121
123
  │ ├── pyproject.toml
122
124
  │ └── __init__.py
123
- └── pypacker/ # pypacker command module
125
+ └── pypack/ # pypack command module
124
126
  ├── fspacker.py
125
127
  └── pyproject.toml
126
128
  ```
@@ -0,0 +1,60 @@
1
+ sfi/__init__.py,sha256=W9IqwgCQr3sYmRyRC3ryCaJDJcMWBtyLzxp5CVsT3Nc,75
2
+ sfi/cli.py,sha256=bUUTOg18sJQbSKSfsVANhlMgSj9yzO2txIzFAd9B2Ok,296
3
+ sfi/alarmclock/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ sfi/alarmclock/alarmclock.py,sha256=0HoacKlGdYq_hINAdl54Cz2E_z6nNjPyqif2xcEBQss,12381
5
+ sfi/bumpversion/__init__.py,sha256=j3XC03YiSDWRJV6UOcDWWsp09STfV5LrvzFkjsehSwA,86
6
+ sfi/bumpversion/bumpversion.py,sha256=HOyHLaE0sZajrlcVZ8hsim8mPjz77qwQVSo6aIzjMXE,20735
7
+ sfi/cleanbuild/cleanbuild.py,sha256=Fr6_cr3rj4llcEQ8yNTK-DHdSzmx1I4hYFJJHu5YEz0,5200
8
+ sfi/condasetup/condasetup.py,sha256=RlbXVYcAJYMau-ZzHOMzHrHl4r-lqNZO0bT-zWuzP_k,4581
9
+ sfi/docscan/__init__.py,sha256=qKkwfRoVBNMzNNdQk69QnFUrmJACtW9qbvoRloTDHfk,121
10
+ sfi/docscan/docscan.py,sha256=rk8mjEI2SKNIliV-Yb41pfUmYBQ1tUhk5LHUNEjkszI,41890
11
+ sfi/docscan/docscan_gui.py,sha256=T_blCyGGaWxL6rtjLIYW3nGdX8DpLQv73YbDnITR4eg,50671
12
+ sfi/docscan/lang/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ sfi/docscan/lang/eng.py,sha256=GcOcT9FLcPZRdJ-MbLRYyf6vDweZTQBu_zUnEFzRY84,8529
14
+ sfi/docscan/lang/zhcn.py,sha256=1SZwQjZF3oi9FsnzuZB-9v7P64sGm5oNmVjuL-rhcEQ,8885
15
+ sfi/filedate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ sfi/filedate/filedate.py,sha256=5FARcsB2Rlz2uTBxeYYjbIEJb9l1cyXj9WSoNKvSrRo,6068
17
+ sfi/gittool/__init__.py,sha256=Xqxw7UUX-TKkWOCB1QHq8AdIKTkU7x87Xr-E0yVmObA,24
18
+ sfi/gittool/gittool.py,sha256=BBE6gm9qP1fAWLqKprmsf7bOFgDvBvia8_bMaXc7dR4,11960
19
+ sfi/llmclient/llmclient.py,sha256=SnFZ9c2cNvFeLeobJV1ls7Ewftaam4s-HVBYW2tgHPo,21706
20
+ sfi/llmquantize/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ sfi/llmquantize/llmquantize.py,sha256=ILmfdJg7Rc7xAygfcVgkSKJ_qRAHDRZXjBymYFBy6fg,17693
22
+ sfi/llmserver/llmserver.py,sha256=vYEcNOV-OEvDULpzzCAJ0zHrXgqJFolOhqNdCZU0Bjs,11339
23
+ sfi/makepython/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ sfi/makepython/makepython.py,sha256=87lySTg0j1lZNIJMf8U_Go_fkuLkuwGDUf6LUbR_r6c,11445
25
+ sfi/pdfsplit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ sfi/pdfsplit/pdfsplit.py,sha256=QWtW3GU28U2ZOyN5sCbH7jEMBpNbuAIzjXWOAXXW44M,6209
27
+ sfi/pyarchive/pyarchive.py,sha256=1rkWY96U_DWbgTvdFGZto7dfutfhUo-OxmoVoaTo6WU,12892
28
+ sfi/pyembedinstall/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ sfi/pyembedinstall/pyembedinstall.py,sha256=kxp5YuwNHB29AVBMEJvzaIqTc7bEx-oBpKReGRJ1Pyw,23737
30
+ sfi/pylibpack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ sfi/pylibpack/pylibpack.py,sha256=lcGrzijibFFaJnxL8ZVALAGNHGs3g7-N4DO_AuiKwPs,54436
32
+ sfi/pylibpack/rules/numpy.json,sha256=ee4gA5NBudFi3MaJA-QlBKQwiQAUb-eluF8HNVkl7Vk,384
33
+ sfi/pylibpack/rules/pymupdf.json,sha256=Hkzh8dvXKCzKx4aeHbu5E0qwgfbwQxZH2VLtQZzlMO4,153
34
+ sfi/pylibpack/rules/pyqt5.json,sha256=JKGnVSUMfXGR5XK1sbL1F6cAsEhl7hK12QkrulAB00M,374
35
+ sfi/pylibpack/rules/pyside2.json,sha256=uSSteT-3wDohWwQ36Z5mSOaSbxrR4565In4uZj_eR4w,557
36
+ sfi/pylibpack/rules/scipy.json,sha256=vTSi3W5BGWcwMkaDnyD6Yg7ijZdicPEUMw4fnRTnNf4,468
37
+ sfi/pylibpack/rules/shiboken2.json,sha256=9Pl3eslvergyjlyHNknkyN0oZlcH3049WULe5WjsmKM,515
38
+ sfi/pyloadergen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
+ sfi/pyloadergen/pyloadergen.py,sha256=R2E6YBCUwfgT3SLr7paBBYEcuySLaj1q_CTZC2slwWQ,39741
40
+ sfi/pypack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ sfi/pypack/pypack.py,sha256=-l3jc4akSSlgEmwiB-kITP0ioBBy42-taHkEsGNEQNw,35168
42
+ sfi/pyprojectparse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
+ sfi/pyprojectparse/pyprojectparse.py,sha256=MLDuQm5LFZW-TQ_GfaaeFSa18lsQq5u2ZTjnlkFQ_Ao,19112
44
+ sfi/pysourcepack/pysourcepack.py,sha256=qjBCFnY_3S7xwPgQ2GB0dr0WFbEj3uusZQ_udiU0Bok,9452
45
+ sfi/quizbase/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
+ sfi/quizbase/quizbase.py,sha256=3tPUuYexZ9TVsNPPO_Itmr5OvyHSgY5OSUZwPoQt9zg,30605
47
+ sfi/quizbase/quizbase_gui.py,sha256=7prc5tWkbUPzs1ofNc4xIC_aRrMCWB6RYN_NTq70p0Q,34729
48
+ sfi/regexvalidate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ sfi/regexvalidate/regex_help.html,sha256=3ltx3nh-Y5kkbHy5D67KfWtLig3u5XEhIlPHdHLEuTE,12436
50
+ sfi/regexvalidate/regexvalidate.py,sha256=5C_M2EKt9Jlonq03v9zrqtsFfAKK3D1vF1kBxD6iUpE,18600
51
+ sfi/taskkill/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ sfi/taskkill/taskkill.py,sha256=kRacCP78mDsZk44tfm4qblOplPxQuo3_6lHl6UQEmkU,7744
53
+ sfi/which/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
+ sfi/which/which.py,sha256=zVIAwZA-pGGngxkkwZ6IxDX3ozVHg7cLSYwYO9FjaIc,2439
55
+ sfi/workflowengine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
+ 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,,
@@ -0,0 +1,28 @@
1
+ [console_scripts]
2
+ alarmclk = sfi.alarmclock.alarmclock:main
3
+ bumpversion = sfi.bumpversion.bumpversion:main
4
+ cleanbuild = sfi.cleanbuild.cleanbuild:main
5
+ condasetup = sfi.condasetup.condasetup:main
6
+ docscan = sfi.docscan.docscan:main
7
+ docscan-gui = sfi.docscan.docscan_gui:main
8
+ filedate = sfi.filedate.filedate:main
9
+ gitt = sfi.gittool.gittool:main
10
+ llmcli = sfi.llmclient.llmclient:main
11
+ llmqnt = sfi.llmquantize.llmquantize:main
12
+ llmsvr = sfi.llmserver.llmserver:main
13
+ mkp = sfi.makepython.makepython:main
14
+ pdfsplit = sfi.pdfsplit.pdfsplit:main
15
+ pyarchive = sfi.pyarchive.pyarchive:main
16
+ pyembedinstall = sfi.pyembedinstall.pyembedinstall:main
17
+ pylibpack = sfi.pylibpack.pylibpack:main
18
+ pyloadergen = sfi.pyloadergen.pyloadergen:main
19
+ pyp = sfi.pypack.pypack:main
20
+ pypack = sfi.pypack.pypack:main
21
+ pyprojectparse = sfi.pyprojectparse.pyprojectparse:main
22
+ pysourcepack = sfi.pysourcepack.pysourcepack:main
23
+ quizbase = sfi.quizbase.quizbase:main
24
+ quizbase-gui = sfi.quizbase.quizbase_gui:main
25
+ regval = sfi.regexvalidate.regexvalidate:main
26
+ sfi = sfi.cli:main
27
+ taskk = sfi.taskkill.taskkill:main
28
+ wch = sfi.which.which:main
sfi/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Single File commands for Interactive python."""
2
2
 
3
- __version__ = "0.1.7"
3
+ __version__ = "0.1.11"
@@ -4,9 +4,9 @@ import argparse
4
4
  import logging
5
5
  import random
6
6
  import sys
7
+ from dataclasses import dataclass
7
8
  from datetime import datetime, timedelta, timezone
8
9
  from functools import partial
9
- from typing import ClassVar
10
10
 
11
11
  import qdarkstyle
12
12
  from PySide2.QtCore import QSize, Qt, QTime, QTimer
@@ -24,42 +24,42 @@ from PySide2.QtWidgets import (
24
24
  QWidget,
25
25
  )
26
26
 
27
- __version__ = "0.1.2"
28
- __build_date__ = "2025-09-16"
27
+ __version__ = "0.1.3"
28
+ __build_date__ = "2026-01-22"
29
29
 
30
30
 
31
+ @dataclass(frozen=True)
31
32
  class AlarmClockConfig:
32
- """Alarm clock configuration."""
33
-
34
- ALARM_CLOCK_TITLE = "Digital Alarm Clock"
33
+ """Configuration for the alarm clock application."""
35
34
 
35
+ ALARM_CLOCK_TITLE: str = "Digital Alarm Clock"
36
36
  DIGITAL_FONT: str = "bold italic 81px 'Consolas'"
37
37
  DIGITAL_COLOR: str = "#ccee00"
38
- DIGITAL_BORDER_COLORS: ClassVar[list[str]] = [
38
+ DIGITAL_BORDER_COLORS: tuple[str, ...] = (
39
39
  "#00aa00",
40
40
  "#eecc00",
41
41
  "#aa00aa",
42
42
  "#c0e0b0",
43
- ]
43
+ )
44
44
  DIGITAL_TIMER_FORMAT: str = "%H:%M:%S"
45
45
  DIGITAL_UPDATE_INTERVAL: int = 1000
46
46
 
47
47
  BLINK_TITLE: str = "Alarm Reminder!"
48
48
  BLINK_CONTENT: str = "⏰ Time's Up!"
49
49
  BLINK_TYPE: str = "color" # Options: 'color' or 'opacity'
50
- BLINK_BG_COLORS: ClassVar[list[str]] = [
50
+ BLINK_BG_COLORS: tuple[str, ...] = (
51
51
  "#baf1ba",
52
52
  "#f8ccc3",
53
53
  "#aab4f0",
54
54
  "#efaec0",
55
- ]
56
- BLINK_INTERVAL: ClassVar[int] = 300 # ms
57
- DELAY_STEPS: ClassVar[list[int]] = [1, 5, 10, 15, 30, 60] # minutes
55
+ )
56
+ BLINK_INTERVAL: int = 300 # ms
57
+ DELAY_STEPS: tuple[int, ...] = (1, 5, 10, 15, 30, 60) # minutes
58
58
 
59
59
 
60
60
  logging.basicConfig(level=logging.INFO, format="%(message)s")
61
61
 
62
- conf = AlarmClockConfig()
62
+ config = AlarmClockConfig()
63
63
  logger = logging.getLogger(__name__)
64
64
 
65
65
 
@@ -69,30 +69,30 @@ class DigitalClock(QLabel):
69
69
  def __init__(self, parent: QWidget | None = None) -> None:
70
70
  super().__init__(parent)
71
71
 
72
- self.setAlignment(Qt.AlignCenter) # type: ignore
72
+ self.setAlignment(Qt.AlignCenter)
73
73
 
74
- self._color = conf.DIGITAL_BORDER_COLORS[0]
74
+ self._color = config.DIGITAL_BORDER_COLORS[0]
75
75
 
76
76
  # Timer to update current time
77
77
  self._timer = QTimer()
78
- self._timer.timeout.connect(self.update_time) # type: ignore
79
- self._timer.start(conf.DIGITAL_UPDATE_INTERVAL) # Update every second
78
+ self._timer.timeout.connect(self.update_time)
79
+ self._timer.start(config.DIGITAL_UPDATE_INTERVAL) # Update every second
80
80
 
81
81
  self.update_time()
82
82
 
83
83
  def update_time(self) -> None:
84
84
  """Update current time display."""
85
85
  current = datetime.now(timezone.utc) + timedelta(hours=8) # Beijing time
86
- self.setText(current.strftime(conf.DIGITAL_TIMER_FORMAT))
86
+ self.setText(current.strftime(config.DIGITAL_TIMER_FORMAT))
87
87
  logger.debug(f"Updated time: {current}")
88
88
 
89
89
  # Add blink effect
90
90
  self._color = random.choice(
91
- [_ for _ in conf.DIGITAL_BORDER_COLORS if _ != self._color],
91
+ [_ for _ in config.DIGITAL_BORDER_COLORS if _ != self._color],
92
92
  )
93
93
  self.setStyleSheet(f"""
94
- font: {conf.DIGITAL_FONT};
95
- color: {conf.DIGITAL_COLOR};
94
+ font: {config.DIGITAL_FONT};
95
+ color: {config.DIGITAL_COLOR};
96
96
  background-color: black;
97
97
  border: 2px dashed {self._color};
98
98
  border-radius: 10px;
@@ -106,47 +106,47 @@ class BlinkDialog(QDialog):
106
106
  def __init__(self) -> None:
107
107
  super().__init__()
108
108
 
109
- self.setWindowTitle(conf.BLINK_TITLE)
109
+ self.setWindowTitle(config.BLINK_TITLE)
110
110
  self.setModal(True)
111
111
  self.setWindowFlags(
112
- self.windowFlags() | Qt.WindowStaysOnTopHint | Qt.WindowType.Dialog, # type: ignore
112
+ self.windowFlags() | Qt.WindowStaysOnTopHint | Qt.WindowType.Dialog,
113
113
  )
114
114
  self.setFixedSize(QSize(400, 240))
115
115
 
116
116
  layout = QVBoxLayout()
117
- msg_label = QLabel(conf.BLINK_CONTENT)
117
+ msg_label = QLabel(config.BLINK_CONTENT)
118
118
  msg_label.setStyleSheet("""
119
119
  color: red;
120
120
  font-size: 24px;
121
121
  """)
122
- msg_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # type: ignore
122
+ msg_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
123
123
 
124
124
  close_button = QPushButton("Close Alarm")
125
- close_button.clicked.connect(self.accept) # type: ignore
125
+ close_button.clicked.connect(self.accept)
126
126
 
127
127
  layout.addWidget(msg_label)
128
128
  layout.addWidget(close_button)
129
129
  self.setLayout(layout)
130
130
 
131
131
  # Prevent user from closing dialog by other means, ensure button click only
132
- self.setWindowFlag(Qt.WindowCloseButtonHint, False) # type: ignore
132
+ self.setWindowFlag(Qt.WindowCloseButtonHint, False)
133
133
 
134
134
  # Blink control variables and timer
135
135
  self.blink_timer = QTimer(self)
136
- self.blink_timer.timeout.connect(self.update_blink) # type: ignore
136
+ self.blink_timer.timeout.connect(self.update_blink)
137
137
  self.blink_state = False
138
- self.blink_type = conf.BLINK_TYPE
138
+ self.blink_type = config.BLINK_TYPE
139
139
 
140
140
  # Initialize style
141
- self.bg_color = random.choice(conf.BLINK_BG_COLORS)
141
+ self.bg_color = random.choice(config.BLINK_BG_COLORS)
142
142
  self.origin_style = self.styleSheet()
143
- self.blink_timer.start(conf.BLINK_INTERVAL)
143
+ self.blink_timer.start(config.BLINK_INTERVAL)
144
144
 
145
145
  def update_blink(self) -> None:
146
146
  """Timer timeout, update blink state."""
147
147
  if self.blink_type == "color":
148
148
  # Color blink logic
149
- colors = [_ for _ in conf.BLINK_BG_COLORS[:] if _ != self.bg_color]
149
+ colors = [_ for _ in config.BLINK_BG_COLORS[:] if _ != self.bg_color]
150
150
  self.setStyleSheet(f"background-color: {random.choice(colors)}")
151
151
  elif self.blink_type == "opacity":
152
152
  # Opacity blink logic - Note: Some systems may not fully support window opacity
@@ -172,7 +172,7 @@ class AlarmClock(QMainWindow):
172
172
 
173
173
  def __init__(self) -> None:
174
174
  super().__init__()
175
- self.setWindowTitle(f"{conf.ALARM_CLOCK_TITLE} v{__version__}")
175
+ self.setWindowTitle(f"{config.ALARM_CLOCK_TITLE} v{__version__}")
176
176
  self.setGeometry(
177
177
  QApplication.desktop().screenGeometry().center().x() - self.width() // 4,
178
178
  QApplication.desktop().screenGeometry().center().y() - self.height() // 2,
@@ -238,7 +238,7 @@ class AlarmClock(QMainWindow):
238
238
  self.alarm_time_edit = QTimeEdit()
239
239
  self.alarm_time_edit.setDisplayFormat("HH:mm:ss")
240
240
  self.alarm_time_edit.setTime(
241
- QTime.currentTime().addSecs(conf.DELAY_STEPS[0] * 60),
241
+ QTime.currentTime().addSecs(config.DELAY_STEPS[0] * 60),
242
242
  )
243
243
 
244
244
  time_layout.addWidget(time_label)
@@ -249,10 +249,10 @@ class AlarmClock(QMainWindow):
249
249
  delay_label = QLabel("Delay (min):")
250
250
  delay_label.setStyleSheet("color: white; font-size: 16px;")
251
251
  delay_layout.addWidget(delay_label)
252
- for minutes in conf.DELAY_STEPS:
252
+ for minutes in config.DELAY_STEPS:
253
253
  button = QPushButton(str(minutes))
254
254
  button.setStyleSheet("color: white; font-size: 16px;")
255
- button.clicked.connect(partial(self.set_delay, minutes)) # type: ignore
255
+ button.clicked.connect(partial(self.set_delay, minutes))
256
256
  delay_layout.addWidget(button)
257
257
  main_layout.addLayout(delay_layout)
258
258
 
@@ -263,9 +263,9 @@ class AlarmClock(QMainWindow):
263
263
  # Control buttons
264
264
  button_layout = QHBoxLayout()
265
265
  self.set_alarm_button = QPushButton("Set Alarm")
266
- self.set_alarm_button.clicked.connect(self.set_alarm) # type: ignore
266
+ self.set_alarm_button.clicked.connect(self.set_alarm)
267
267
  self.cancel_alarm_button = QPushButton("Cancel Alarm")
268
- self.cancel_alarm_button.clicked.connect(self.cancel_alarm) # type: ignore
268
+ self.cancel_alarm_button.clicked.connect(self.cancel_alarm)
269
269
  self.cancel_alarm_button.setEnabled(False)
270
270
  button_layout.addWidget(self.set_alarm_button)
271
271
  button_layout.addWidget(self.cancel_alarm_button)
@@ -273,13 +273,13 @@ class AlarmClock(QMainWindow):
273
273
 
274
274
  # Status display
275
275
  self.status_label = QLabel("Alarm not set")
276
- self.status_label.setAlignment(Qt.AlignCenter) # type: ignore
276
+ self.status_label.setAlignment(Qt.AlignCenter)
277
277
  self.status_label.setStyleSheet("color: #aaaaaa; font-size: 16px;")
278
278
  main_layout.addWidget(self.status_label)
279
279
 
280
280
  # Alarm timer
281
281
  self.alarm_timer = QTimer()
282
- self.alarm_timer.timeout.connect(self.check_alarm) # type: ignore
282
+ self.alarm_timer.timeout.connect(self.check_alarm)
283
283
 
284
284
  # Alarm state
285
285
  self.alarm_set = False
@@ -1,3 +1,3 @@
1
1
  """Bumpversion - Automated version number management tool."""
2
2
 
3
- __version__ = "0.1.7"
3
+ __version__ = "0.1.11"
@@ -0,0 +1,155 @@
1
+ """Clean build folders, such as .venv, node_modules, dist, build, etc."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import os
8
+ import shutil
9
+ import stat
10
+ import time
11
+ from collections import Counter
12
+ from concurrent.futures import ThreadPoolExecutor, as_completed
13
+ from dataclasses import dataclass, field
14
+ from functools import cached_property
15
+ from pathlib import Path
16
+
17
+
18
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
19
+ logger = logging.getLogger(__name__)
20
+
21
+ CWD = Path.cwd()
22
+ PROJECT_ROOT = Path(__file__).parent.parent.parent
23
+ _BUILD_FOLDERS = ("build", "dist", ".venv", "node_modules")
24
+
25
+ @dataclass(frozen=True)
26
+ class SearchResult:
27
+ """Search result for build folders."""
28
+
29
+ root_dir: Path = field(default_factory=Path)
30
+ folders_to_delete: list[Path] = field(default_factory=list)
31
+ folders_to_skip: list[Path] = field(default_factory=list)
32
+
33
+ def __repr__(self) -> str:
34
+ return f"<SearchResult: root={self.root_dir}>, delete={self.delete_counter}, skip={self.skip_counter}\n>"
35
+
36
+ @cached_property
37
+ def delete_counter(self) -> Counter:
38
+ """Get the most common folder names."""
39
+ return Counter([folder.name for folder in self.folders_to_delete])
40
+
41
+ @cached_property
42
+ def skip_counter(self) -> Counter:
43
+ """Get the most common folder names."""
44
+ return Counter([folder.name for folder in self.folders_to_skip])
45
+
46
+ @cached_property
47
+ def delete_count(self) -> int:
48
+ """Get the total number of folders to delete."""
49
+ return len(self.folders_to_delete)
50
+
51
+ def perform_delete(self):
52
+ """Perform the delete operation."""
53
+ t0 = time.perf_counter()
54
+ deleted_count = 0
55
+ failed_folders = []
56
+ with ThreadPoolExecutor(max_workers=4) as executor:
57
+ future_to_folder = {
58
+ executor.submit(SearchResult.delete_folder, folder): folder
59
+ for folder in self.folders_to_delete
60
+ }
61
+ for future in as_completed(future_to_folder):
62
+ folder, error = future.result()
63
+ if error:
64
+ logger.error(f"Error deleting {folder}: {error}")
65
+ failed_folders.append(folder)
66
+ else:
67
+ logger.info(f"Deleted {folder}")
68
+ deleted_count += 1
69
+
70
+ if failed_folders:
71
+ logger.warning(f"Failed to delete {len(failed_folders)} folders")
72
+ logger.info(f"Successfully deleted {deleted_count}/{self.delete_count} folders")
73
+ logger.info(f"Time taken: {time.perf_counter() - t0:.4f}s")
74
+
75
+ @staticmethod
76
+ def handle_remove_readonly(func, path, _):
77
+ """Handle Windows readonly files when removing."""
78
+ try:
79
+ os.chmod(path, stat.S_IWUSR)
80
+ func(path)
81
+ except OSError:
82
+ if not os.path.exists(path):
83
+ pass
84
+ else:
85
+ raise
86
+
87
+ @staticmethod
88
+ def delete_folder(folder: Path):
89
+ """Delete a single folder with error handling."""
90
+ try:
91
+ shutil.rmtree(folder, onerror=SearchResult.handle_remove_readonly)
92
+ return folder, None
93
+ except Exception as e:
94
+ return folder, e
95
+
96
+ @staticmethod
97
+ def is_target_folder(entry: Path) -> bool:
98
+ """Check if a path is a build folder."""
99
+ return entry.is_dir() and entry.name in _BUILD_FOLDERS
100
+
101
+ @staticmethod
102
+ def from_path(directory: Path) -> SearchResult:
103
+ """Create a SearchResult from a path."""
104
+ logger.info(f"Searching for build folders in {directory}")
105
+
106
+ folders_to_delete = []
107
+ folders_to_skip = []
108
+ for root, dirs, _ in os.walk(directory, onerror=lambda _: None):
109
+ root_path = Path(root)
110
+ for dir_name in dirs:
111
+ entry = root_path / dir_name
112
+ if entry == PROJECT_ROOT / ".venv":
113
+ folders_to_skip.append(entry)
114
+ continue
115
+ elif SearchResult.is_target_folder(entry):
116
+ folders_to_delete.append(entry)
117
+
118
+ result = SearchResult(
119
+ root_dir=directory,
120
+ folders_to_delete=folders_to_delete,
121
+ folders_to_skip=folders_to_skip,
122
+ )
123
+ logger.info(f"Found {result.delete_count} build folders to delete")
124
+ logger.info(f"Summary: {result}")
125
+ return result
126
+
127
+
128
+ def parse_args():
129
+ parser = argparse.ArgumentParser(description="Clean build folders")
130
+ parser.add_argument(
131
+ "directory", type=str, nargs="?", default=str(CWD), help="Directory to clean"
132
+ )
133
+ parser.add_argument("--yes", "-y", action="store_true", help="Skip confirmation")
134
+ return parser.parse_args()
135
+
136
+
137
+ def main():
138
+ args = parse_args()
139
+
140
+ result = SearchResult.from_path(Path(args.directory))
141
+ if not args.yes:
142
+ logger.info(
143
+ f"\nAre you sure you want to clean build folders in {args.directory}? (y/n) [n]"
144
+ )
145
+ choice = input().lower()
146
+ if choice != "y":
147
+ logger.info("Dry run, no changes will be made")
148
+ return
149
+
150
+ logger.info("Running clean build")
151
+ result.perform_delete()
152
+
153
+
154
+ if __name__ == "__main__":
155
+ main()
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import os
6
+ from pathlib import Path
7
+
8
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
9
+ logger = logging.getLogger(__name__)
10
+ cwd = Path.cwd()
11
+
12
+ _CONDA_MIRROR_URLS: dict[str, frozenset[str]] = {
13
+ "tsinghua": frozenset(
14
+ [
15
+ "https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/",
16
+ "https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/",
17
+ "https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r/",
18
+ "https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/msys2/",
19
+ "https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/pro/",
20
+ "https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/",
21
+ "https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/bioconda/",
22
+ "https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/menpo/",
23
+ "https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/",
24
+ ]
25
+ ),
26
+ "ustc": frozenset(
27
+ [
28
+ "https://mirrors.ustc.edu.cn/anaconda/pkgs/main/",
29
+ "https://mirrors.ustc.edu.cn/anaconda/pkgs/free/",
30
+ "https://mirrors.ustc.edu.cn/anaconda/pkgs/r/",
31
+ "https://mirrors.ustc.edu.cn/anaconda/pkgs/msys2/",
32
+ "https://mirrors.ustc.edu.cn/anaconda/pkgs/pro/",
33
+ "https://mirrors.ustc.edu.cn/anaconda/pkgs/dev/",
34
+ "https://mirrors.ustc.edu.cn/anaconda/cloud/conda-forge/",
35
+ "https://mirrors.ustc.edu.cn/anaconda/cloud/bioconda/",
36
+ "https://mirrors.ustc.edu.cn/anaconda/cloud/menpo/",
37
+ "https://mirrors.ustc.edu.cn/anaconda/cloud/pytorch/",
38
+ ]
39
+ ),
40
+ "bsfu": frozenset(
41
+ [
42
+ "https://mirrors.bsfu.edu.cn/anaconda/pkgs/main/",
43
+ "https://mirrors.bsfu.edu.cn/anaconda/pkgs/free/",
44
+ "https://mirrors.bsfu.edu.cn/anaconda/pkgs/r/",
45
+ "https://mirrors.bsfu.edu.cn/anaconda/pkgs/msys2/",
46
+ "https://mirrors.bsfu.edu.cn/anaconda/pkgs/pro/",
47
+ "https://mirrors.bsfu.edu.cn/anaconda/pkgs/dev/",
48
+ "https://mirrors.bsfu.edu.cn/anaconda/cloud/conda-forge/",
49
+ "https://mirrors.bsfu.edu.cn/anaconda/cloud/bioconda/",
50
+ "https://mirrors.bsfu.edu.cn/anaconda/cloud/menpo/",
51
+ "https://mirrors.bsfu.edu.cn/anaconda/cloud/pytorch/",
52
+ ]
53
+ ),
54
+ "aliyun": frozenset(
55
+ [
56
+ "https://mirrors.aliyun.com/anaconda/pkgs/main/",
57
+ "https://mirrors.aliyun.com/anaconda/pkgs/free/",
58
+ "https://mirrors.aliyun.com/anaconda/pkgs/r/",
59
+ "https://mirrors.aliyun.com/anaconda/pkgs/msys2/",
60
+ "https://mirrors.aliyun.com/anaconda/pkgs/pro/",
61
+ "https://mirrors.aliyun.com/anaconda/pkgs/dev/",
62
+ "https://mirrors.aliyun.com/anaconda/cloud/conda-forge/",
63
+ "https://mirrors.aliyun.com/anaconda/cloud/bioconda/",
64
+ "https://mirrors.aliyun.com/anaconda/cloud/menpo/",
65
+ "https://mirrors.aliyun.com/anaconda/cloud/pytorch/",
66
+ ]
67
+ ),
68
+ }
69
+
70
+
71
+ def set_conda_mirror(mirror: str = "tsinghua") -> None:
72
+ """Set the Conda mirror for the given channel."""
73
+ if mirror in _CONDA_MIRROR_URLS:
74
+ old_config = Path.home() / ".condarc"
75
+ if old_config.exists():
76
+ logger.info("Found existing .condarc file, backing it up")
77
+ os.rename(old_config, Path.home() / ".condarc.bak")
78
+ else:
79
+ logger.debug("No existing .condarc file found")
80
+
81
+ mirror_urls = _CONDA_MIRROR_URLS[mirror]
82
+ for url in mirror_urls:
83
+ logger.debug(f"Adding mirror: {url}")
84
+ os.system(f"conda config --add channels {url}")
85
+ os.system("conda config --set show_channel_urls yes")
86
+ logger.info("Conda mirror set successfully")
87
+ else:
88
+ logger.error(f"Invalid mirror: {mirror}")
89
+
90
+
91
+ def parse_args():
92
+ parser = argparse.ArgumentParser(description="Setup Conda environment for SFI")
93
+ parser.add_argument(
94
+ "mirror",
95
+ type=str,
96
+ choices=["tsinghua", "ustc", "bsfu", "aliyun"],
97
+ nargs="?",
98
+ default="tsinghua",
99
+ help="Conda mirror to use",
100
+ )
101
+ parser.add_argument("-d", "--debug", help="Debug mode", action="store_true")
102
+
103
+ return parser.parse_args()
104
+
105
+
106
+ def main():
107
+ args = parse_args()
108
+
109
+ if args.debug:
110
+ logger.setLevel(logging.DEBUG)
111
+
112
+ set_conda_mirror(args.mirror)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
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.7"
3
+ __version__ = "0.1.11"