pysfi 0.1.10__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.
Files changed (49) hide show
  1. {pysfi-0.1.10.dist-info → pysfi-0.1.12.dist-info}/METADATA +9 -7
  2. pysfi-0.1.12.dist-info/RECORD +62 -0
  3. {pysfi-0.1.10.dist-info → pysfi-0.1.12.dist-info}/entry_points.txt +13 -2
  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/docdiff/docdiff.py +238 -0
  10. sfi/docscan/__init__.py +1 -1
  11. sfi/docscan/docscan_gui.py +1 -1
  12. sfi/docscan/lang/eng.py +152 -152
  13. sfi/docscan/lang/zhcn.py +170 -170
  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 +2 -2
  21. sfi/pdfsplit/pdfsplit.py +4 -4
  22. sfi/pyarchive/pyarchive.py +418 -0
  23. sfi/pyembedinstall/__init__.py +0 -0
  24. sfi/pyembedinstall/pyembedinstall.py +629 -0
  25. sfi/pylibpack/pylibpack.py +813 -269
  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 +271 -572
  33. sfi/pypack/pypack.py +822 -471
  34. sfi/pyprojectparse/__init__.py +0 -0
  35. sfi/pyprojectparse/pyprojectparse.py +500 -0
  36. sfi/pysourcepack/pysourcepack.py +308 -369
  37. sfi/quizbase/__init__.py +0 -0
  38. sfi/quizbase/quizbase.py +828 -0
  39. sfi/quizbase/quizbase_gui.py +987 -0
  40. sfi/regexvalidate/__init__.py +0 -0
  41. sfi/regexvalidate/regex_help.html +284 -0
  42. sfi/regexvalidate/regexvalidate.py +468 -0
  43. sfi/taskkill/taskkill.py +0 -2
  44. pysfi-0.1.10.dist-info/RECORD +0 -39
  45. sfi/embedinstall/embedinstall.py +0 -478
  46. sfi/projectparse/projectparse.py +0 -152
  47. {pysfi-0.1.10.dist-info → pysfi-0.1.12.dist-info}/WHEEL +0 -0
  48. /sfi/{embedinstall → llmclient}/__init__.py +0 -0
  49. /sfi/{projectparse → llmquantize}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pysfi
3
- Version: 0.1.10
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
@@ -47,10 +49,10 @@ pysfi is a Python project that provides single-file command-line utilities, desi
47
49
 
48
50
  - **alarmclk**: Alarm clock functionality
49
51
  - **[bumpversion](sfi/bumpversion/README.md)**: Automated version number management tool
50
- - **embedinstall**: Embed installation utilities
52
+ - **pyembedinstall**: Embed installation utilities
51
53
  - **[filedate](sfi/filedate/README.md)**: A file date management tool that normalizes date prefixes in filenames
52
54
  - **mkp**: Make Python project utilities
53
- - **projectparse**: Project parsing and analysis tools
55
+ - **pyprojectparse**: Project parsing and analysis tools
54
56
  - **pyloadergen**: Python loader code generation
55
57
  - **pypack**: Python packaging utilities
56
58
 
@@ -101,8 +103,8 @@ pysfi/
101
103
  │ ├── alarmclock.py
102
104
  │ ├── pyproject.toml
103
105
  │ └── __init__.py
104
- ├── embedinstall/ # embedinstall command module
105
- │ ├── embedinstall.py
106
+ ├── pyembedinstall/ # pyembedinstall command module
107
+ │ ├── pyembedinstall.py
106
108
  │ ├── pyproject.toml
107
109
  │ └── __init__.py
108
110
  ├── filedate/ # filedate command module
@@ -114,8 +116,8 @@ pysfi/
114
116
  │ ├── makepython.py
115
117
  │ ├── pyproject.toml
116
118
  │ └── __init__.py
117
- ├── projectparse/ # projectparse command module
118
- │ ├── projectparse.py
119
+ ├── pyprojectparse/ # pyprojectparse command module
120
+ │ ├── pyprojectparse.py
119
121
  │ ├── pyproject.toml
120
122
  │ └── __init__.py
121
123
  ├── pyloadergen/ # pyloadergen command module
@@ -0,0 +1,62 @@
1
+ sfi/__init__.py,sha256=nJ-7R5yswQ4ew8RsZyWKYV8E6Xq9EGrknyOhesxJBSU,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=v7s0aqGOHApzAHVwDoBCCtuqmjYvDEEVKS-VIAKN3PE,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/docdiff/docdiff.py,sha256=RIq5cL2WnLohfqY-PRxlwhRJoYEUOOY1hpCAbfhzStk,7734
10
+ sfi/docscan/__init__.py,sha256=Zog5sFgdZvJmsmNgGBSsyvu-Gcb1ecYf-l6e5ThSOH4,121
11
+ sfi/docscan/docscan.py,sha256=rk8mjEI2SKNIliV-Yb41pfUmYBQ1tUhk5LHUNEjkszI,41890
12
+ sfi/docscan/docscan_gui.py,sha256=T_blCyGGaWxL6rtjLIYW3nGdX8DpLQv73YbDnITR4eg,50671
13
+ sfi/docscan/lang/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ sfi/docscan/lang/eng.py,sha256=GcOcT9FLcPZRdJ-MbLRYyf6vDweZTQBu_zUnEFzRY84,8529
15
+ sfi/docscan/lang/zhcn.py,sha256=1SZwQjZF3oi9FsnzuZB-9v7P64sGm5oNmVjuL-rhcEQ,8885
16
+ sfi/filedate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ sfi/filedate/filedate.py,sha256=5FARcsB2Rlz2uTBxeYYjbIEJb9l1cyXj9WSoNKvSrRo,6068
18
+ sfi/gittool/__init__.py,sha256=Xqxw7UUX-TKkWOCB1QHq8AdIKTkU7x87Xr-E0yVmObA,24
19
+ sfi/gittool/gittool.py,sha256=BBE6gm9qP1fAWLqKprmsf7bOFgDvBvia8_bMaXc7dR4,11960
20
+ sfi/llmclient/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ sfi/llmclient/llmclient.py,sha256=SnFZ9c2cNvFeLeobJV1ls7Ewftaam4s-HVBYW2tgHPo,21706
22
+ sfi/llmquantize/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ sfi/llmquantize/llmquantize.py,sha256=ILmfdJg7Rc7xAygfcVgkSKJ_qRAHDRZXjBymYFBy6fg,17693
24
+ sfi/llmserver/llmserver.py,sha256=vYEcNOV-OEvDULpzzCAJ0zHrXgqJFolOhqNdCZU0Bjs,11339
25
+ sfi/makepython/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ sfi/makepython/makepython.py,sha256=87lySTg0j1lZNIJMf8U_Go_fkuLkuwGDUf6LUbR_r6c,11445
27
+ sfi/pdfsplit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ sfi/pdfsplit/pdfsplit.py,sha256=QWtW3GU28U2ZOyN5sCbH7jEMBpNbuAIzjXWOAXXW44M,6209
29
+ sfi/pyarchive/pyarchive.py,sha256=1rkWY96U_DWbgTvdFGZto7dfutfhUo-OxmoVoaTo6WU,12892
30
+ sfi/pyembedinstall/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ sfi/pyembedinstall/pyembedinstall.py,sha256=kxp5YuwNHB29AVBMEJvzaIqTc7bEx-oBpKReGRJ1Pyw,23737
32
+ sfi/pylibpack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ sfi/pylibpack/pylibpack.py,sha256=lcGrzijibFFaJnxL8ZVALAGNHGs3g7-N4DO_AuiKwPs,54436
34
+ sfi/pylibpack/rules/numpy.json,sha256=ee4gA5NBudFi3MaJA-QlBKQwiQAUb-eluF8HNVkl7Vk,384
35
+ sfi/pylibpack/rules/pymupdf.json,sha256=Hkzh8dvXKCzKx4aeHbu5E0qwgfbwQxZH2VLtQZzlMO4,153
36
+ sfi/pylibpack/rules/pyqt5.json,sha256=JKGnVSUMfXGR5XK1sbL1F6cAsEhl7hK12QkrulAB00M,374
37
+ sfi/pylibpack/rules/pyside2.json,sha256=uSSteT-3wDohWwQ36Z5mSOaSbxrR4565In4uZj_eR4w,557
38
+ sfi/pylibpack/rules/scipy.json,sha256=vTSi3W5BGWcwMkaDnyD6Yg7ijZdicPEUMw4fnRTnNf4,468
39
+ sfi/pylibpack/rules/shiboken2.json,sha256=9Pl3eslvergyjlyHNknkyN0oZlcH3049WULe5WjsmKM,515
40
+ sfi/pyloadergen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ sfi/pyloadergen/pyloadergen.py,sha256=R2E6YBCUwfgT3SLr7paBBYEcuySLaj1q_CTZC2slwWQ,39741
42
+ sfi/pypack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
+ sfi/pypack/pypack.py,sha256=-l3jc4akSSlgEmwiB-kITP0ioBBy42-taHkEsGNEQNw,35168
44
+ sfi/pyprojectparse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ sfi/pyprojectparse/pyprojectparse.py,sha256=MLDuQm5LFZW-TQ_GfaaeFSa18lsQq5u2ZTjnlkFQ_Ao,19112
46
+ sfi/pysourcepack/pysourcepack.py,sha256=qjBCFnY_3S7xwPgQ2GB0dr0WFbEj3uusZQ_udiU0Bok,9452
47
+ sfi/quizbase/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
+ sfi/quizbase/quizbase.py,sha256=3tPUuYexZ9TVsNPPO_Itmr5OvyHSgY5OSUZwPoQt9zg,30605
49
+ sfi/quizbase/quizbase_gui.py,sha256=7prc5tWkbUPzs1ofNc4xIC_aRrMCWB6RYN_NTq70p0Q,34729
50
+ sfi/regexvalidate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
+ sfi/regexvalidate/regex_help.html,sha256=3ltx3nh-Y5kkbHy5D67KfWtLig3u5XEhIlPHdHLEuTE,12436
52
+ sfi/regexvalidate/regexvalidate.py,sha256=5C_M2EKt9Jlonq03v9zrqtsFfAKK3D1vF1kBxD6iUpE,18600
53
+ sfi/taskkill/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
+ sfi/taskkill/taskkill.py,sha256=kRacCP78mDsZk44tfm4qblOplPxQuo3_6lHl6UQEmkU,7744
55
+ sfi/which/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
+ sfi/which/which.py,sha256=zVIAwZA-pGGngxkkwZ6IxDX3ozVHg7cLSYwYO9FjaIc,2439
57
+ sfi/workflowengine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
+ sfi/workflowengine/workflowengine.py,sha256=ck5PjyyjtWtbjN4ePEKsTWV6QR-BUlrfwrY6jih52jQ,17055
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,,
@@ -1,18 +1,29 @@
1
1
  [console_scripts]
2
2
  alarmclk = sfi.alarmclock.alarmclock:main
3
3
  bumpversion = sfi.bumpversion.bumpversion:main
4
+ cleanbuild = sfi.cleanbuild.cleanbuild:main
5
+ condasetup = sfi.condasetup.condasetup:main
6
+ docdiff = sfi.docdiff.docdiff:main
4
7
  docscan = sfi.docscan.docscan:main
5
8
  docscan-gui = sfi.docscan.docscan_gui:main
6
- embedinstall = sfi.embedinstall.embedinstall:main
7
9
  filedate = sfi.filedate.filedate:main
10
+ gitt = sfi.gittool.gittool:main
11
+ llmcli = sfi.llmclient.llmclient:main
12
+ llmqnt = sfi.llmquantize.llmquantize:main
13
+ llmsvr = sfi.llmserver.llmserver:main
8
14
  mkp = sfi.makepython.makepython:main
9
15
  pdfsplit = sfi.pdfsplit.pdfsplit:main
10
- projectparse = sfi.projectparse.projectparse:main
16
+ pyarchive = sfi.pyarchive.pyarchive:main
17
+ pyembedinstall = sfi.pyembedinstall.pyembedinstall:main
11
18
  pylibpack = sfi.pylibpack.pylibpack:main
12
19
  pyloadergen = sfi.pyloadergen.pyloadergen:main
13
20
  pyp = sfi.pypack.pypack:main
14
21
  pypack = sfi.pypack.pypack:main
22
+ pyprojectparse = sfi.pyprojectparse.pyprojectparse:main
15
23
  pysourcepack = sfi.pysourcepack.pysourcepack:main
24
+ quizbase = sfi.quizbase.quizbase:main
25
+ quizbase-gui = sfi.quizbase.quizbase_gui:main
26
+ regval = sfi.regexvalidate.regexvalidate:main
16
27
  sfi = sfi.cli:main
17
28
  taskk = sfi.taskkill.taskkill:main
18
29
  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.10"
3
+ __version__ = "0.1.12"
@@ -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.10"
3
+ __version__ = "0.1.12"
@@ -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()