pysfi 0.1.13__py3-none-any.whl → 0.1.15__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.13.dist-info → pysfi-0.1.15.dist-info}/METADATA +1 -1
- {pysfi-0.1.13.dist-info → pysfi-0.1.15.dist-info}/RECORD +35 -35
- {pysfi-0.1.13.dist-info → pysfi-0.1.15.dist-info}/entry_points.txt +2 -0
- sfi/__init__.py +20 -5
- sfi/alarmclock/__init__.py +3 -3
- sfi/bumpversion/__init__.py +5 -5
- sfi/bumpversion/bumpversion.py +64 -15
- sfi/cleanbuild/__init__.py +3 -3
- sfi/cleanbuild/cleanbuild.py +5 -1
- sfi/cli.py +13 -2
- sfi/condasetup/__init__.py +1 -1
- sfi/condasetup/condasetup.py +91 -76
- sfi/docdiff/__init__.py +1 -1
- sfi/docdiff/docdiff.py +3 -2
- sfi/docscan/__init__.py +3 -3
- sfi/docscan/docscan.py +78 -23
- sfi/docscan/docscan_gui.py +5 -5
- sfi/filedate/filedate.py +12 -5
- sfi/img2pdf/img2pdf.py +5 -5
- sfi/llmquantize/llmquantize.py +44 -33
- sfi/llmserver/__init__.py +1 -1
- sfi/makepython/makepython.py +880 -319
- sfi/pdfcrypt/__init__.py +30 -0
- sfi/pdfcrypt/pdfcrypt.py +435 -0
- sfi/pdfsplit/pdfsplit.py +45 -12
- sfi/pyarchive/__init__.py +1 -1
- sfi/pyarchive/pyarchive.py +1 -1
- sfi/pyembedinstall/pyembedinstall.py +1 -1
- sfi/pylibpack/pylibpack.py +5 -13
- sfi/pyloadergen/pyloadergen.py +6 -3
- sfi/pypack/pypack.py +131 -105
- sfi/pyprojectparse/pyprojectparse.py +19 -44
- sfi/pysourcepack/__init__.py +1 -1
- sfi/pysourcepack/pysourcepack.py +11 -14
- sfi/workflowengine/__init__.py +0 -0
- sfi/workflowengine/workflowengine.py +0 -547
- {pysfi-0.1.13.dist-info → pysfi-0.1.15.dist-info}/WHEEL +0 -0
sfi/pdfcrypt/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""PDF encryption/decryption utilities.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to encrypt and decrypt PDF files
|
|
4
|
+
using strong encryption algorithms. It supports batch processing
|
|
5
|
+
of multiple files with configurable passwords and security settings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .pdfcrypt import (
|
|
9
|
+
PDFCryptConfig,
|
|
10
|
+
conf,
|
|
11
|
+
decrypt,
|
|
12
|
+
decrypt_pdf,
|
|
13
|
+
encrypt,
|
|
14
|
+
encrypt_pdf,
|
|
15
|
+
is_encrypted,
|
|
16
|
+
main,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"PDFCryptConfig",
|
|
21
|
+
"conf",
|
|
22
|
+
"decrypt",
|
|
23
|
+
"decrypt_pdf",
|
|
24
|
+
"encrypt",
|
|
25
|
+
"encrypt_pdf",
|
|
26
|
+
"is_encrypted",
|
|
27
|
+
"main",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.15"
|
sfi/pdfcrypt/pdfcrypt.py
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""Function: Encrypt/decrypt all PDF files in the current directory.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to encrypt and decrypt PDF files
|
|
4
|
+
using strong encryption algorithms. It supports batch processing
|
|
5
|
+
of multiple files with configurable passwords and security settings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import atexit
|
|
12
|
+
import contextlib
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from functools import cached_property, partial
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Final
|
|
20
|
+
|
|
21
|
+
import pypdf
|
|
22
|
+
|
|
23
|
+
# Constants
|
|
24
|
+
MIN_PASSWORD_LENGTH: Final[int] = 6
|
|
25
|
+
CONFIG_FILE: Final[Path] = Path.home() / ".pysfi" / "pdfcrypt.json"
|
|
26
|
+
CWD: Final[Path] = Path.cwd()
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
__version__ = "1.0.0"
|
|
31
|
+
__build__ = "20260204"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class PDFCryptConfig:
|
|
36
|
+
"""Configuration for PDF encryption/decryption operations."""
|
|
37
|
+
|
|
38
|
+
max_workers: int = 4
|
|
39
|
+
default_password: str = ""
|
|
40
|
+
output_suffix_encrypted: str = ".enc.pdf"
|
|
41
|
+
output_suffix_decrypted: str = ".dec.pdf"
|
|
42
|
+
_config_loaded: bool = False
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
"""Initialize configuration and load from file if exists."""
|
|
46
|
+
if CONFIG_FILE.exists():
|
|
47
|
+
try:
|
|
48
|
+
config_data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
49
|
+
for key, value in config_data.items():
|
|
50
|
+
if hasattr(self, key):
|
|
51
|
+
setattr(self, key, value)
|
|
52
|
+
self._config_loaded = True
|
|
53
|
+
logger.info("Configuration loaded successfully")
|
|
54
|
+
except (json.JSONDecodeError, TypeError, AttributeError) as e:
|
|
55
|
+
logger.warning(f"Failed to load configuration: {e}")
|
|
56
|
+
logger.info("Using default configuration")
|
|
57
|
+
else:
|
|
58
|
+
logger.info("Using default configuration")
|
|
59
|
+
|
|
60
|
+
def save(self) -> None:
|
|
61
|
+
"""Save current configuration to file."""
|
|
62
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
config_dict = {
|
|
64
|
+
"max_workers": self.max_workers,
|
|
65
|
+
"default_password": self.default_password,
|
|
66
|
+
"output_suffix_encrypted": self.output_suffix_encrypted,
|
|
67
|
+
"output_suffix_decrypted": self.output_suffix_decrypted,
|
|
68
|
+
}
|
|
69
|
+
CONFIG_FILE.write_text(json.dumps(config_dict, indent=4), encoding="utf-8")
|
|
70
|
+
logger.info("Configuration saved successfully")
|
|
71
|
+
|
|
72
|
+
@cached_property
|
|
73
|
+
def is_configured(self) -> bool:
|
|
74
|
+
"""Check if configuration has been loaded from file."""
|
|
75
|
+
return self._config_loaded
|
|
76
|
+
|
|
77
|
+
@cached_property
|
|
78
|
+
def config_summary(self) -> dict[str, str | int]:
|
|
79
|
+
"""Get configuration summary for display/logging purposes."""
|
|
80
|
+
return {
|
|
81
|
+
"max_workers": self.max_workers,
|
|
82
|
+
"output_suffix_encrypted": self.output_suffix_encrypted,
|
|
83
|
+
"output_suffix_decrypted": self.output_suffix_decrypted,
|
|
84
|
+
"source": "loaded" if self._config_loaded else "default",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Global configuration instance
|
|
89
|
+
conf = PDFCryptConfig()
|
|
90
|
+
atexit.register(conf.save)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_encrypted(filepath: Path) -> bool:
|
|
94
|
+
"""Check if a PDF file is encrypted.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
filepath: Path to the PDF file
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
bool: True if file is encrypted, False otherwise
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
return pypdf.PdfReader(filepath).is_encrypted
|
|
104
|
+
except Exception:
|
|
105
|
+
logger.error(f"Failed to check encryption status for {filepath}")
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _validate_password_strength(password: str) -> None:
|
|
110
|
+
"""Validate password strength and log warnings if needed.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
password: Password to validate
|
|
114
|
+
"""
|
|
115
|
+
if len(password) < MIN_PASSWORD_LENGTH:
|
|
116
|
+
logger.warning(
|
|
117
|
+
f"Password length less than {MIN_PASSWORD_LENGTH} characters, "
|
|
118
|
+
f"recommend using a stronger password"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _setup_pdf_writer(reader: pypdf.PdfReader) -> pypdf.PdfWriter:
|
|
123
|
+
"""Create and configure PDF writer with pages from reader.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
reader: Source PDF reader
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Configured PDF writer
|
|
130
|
+
"""
|
|
131
|
+
writer = pypdf.PdfWriter()
|
|
132
|
+
for page in reader.pages:
|
|
133
|
+
writer.add_page(page)
|
|
134
|
+
return writer
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _handle_output_file_conflict(filepath: Path) -> None:
|
|
138
|
+
"""Log warning if output file already exists.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
filepath: Target file path
|
|
142
|
+
"""
|
|
143
|
+
if filepath.exists():
|
|
144
|
+
logger.warning(f"Target file already exists, will overwrite: {filepath}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _cleanup_resources(reader=None, writer=None) -> None:
|
|
148
|
+
"""Clean up PDF resources properly.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
reader: PDF reader to close
|
|
152
|
+
writer: PDF writer to close
|
|
153
|
+
"""
|
|
154
|
+
if reader:
|
|
155
|
+
with contextlib.suppress(Exception):
|
|
156
|
+
reader.stream.close()
|
|
157
|
+
# pypdf.PdfWriter has no explicit close method
|
|
158
|
+
if writer:
|
|
159
|
+
with contextlib.suppress(Exception):
|
|
160
|
+
writer.close()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def encrypt_pdf(
|
|
164
|
+
filepath: Path,
|
|
165
|
+
password: str,
|
|
166
|
+
) -> tuple[Path, Path | None]:
|
|
167
|
+
"""Encrypt a single PDF file.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
filepath: Path to the PDF file to encrypt
|
|
171
|
+
password: Encryption password
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Tuple containing original file path and encrypted file path (if successful)
|
|
175
|
+
"""
|
|
176
|
+
# Validate password strength
|
|
177
|
+
_validate_password_strength(password)
|
|
178
|
+
|
|
179
|
+
reader = None
|
|
180
|
+
writer = None
|
|
181
|
+
enc_pdf_file = filepath.with_suffix(conf.output_suffix_encrypted)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
# Use context manager to ensure resource cleanup
|
|
185
|
+
with filepath.open("rb") as input_file:
|
|
186
|
+
reader = pypdf.PdfReader(input_file)
|
|
187
|
+
writer = _setup_pdf_writer(reader)
|
|
188
|
+
|
|
189
|
+
writer.encrypt(
|
|
190
|
+
user_password=password,
|
|
191
|
+
owner_password=password,
|
|
192
|
+
use_128bit=True,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Check if target file already exists
|
|
196
|
+
_handle_output_file_conflict(enc_pdf_file)
|
|
197
|
+
|
|
198
|
+
with enc_pdf_file.open("wb") as output_file:
|
|
199
|
+
writer.write(output_file)
|
|
200
|
+
except OSError:
|
|
201
|
+
logger.exception(f"Failed to write encrypted file [{enc_pdf_file.name}]")
|
|
202
|
+
return filepath, None
|
|
203
|
+
except Exception:
|
|
204
|
+
logger.exception(f"Error encrypting file [{filepath.name}]")
|
|
205
|
+
return filepath, None
|
|
206
|
+
else:
|
|
207
|
+
logger.info(f"Successfully encrypted file: {enc_pdf_file}")
|
|
208
|
+
return filepath, enc_pdf_file
|
|
209
|
+
finally:
|
|
210
|
+
# Explicitly clean up resources
|
|
211
|
+
_cleanup_resources(reader, writer)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _attempt_pdf_decryption(
|
|
215
|
+
reader: pypdf.PdfReader, password: str, filepath: Path
|
|
216
|
+
) -> bool:
|
|
217
|
+
"""Attempt to decrypt PDF file and log results.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
reader: PDF reader instance
|
|
221
|
+
password: Decryption password
|
|
222
|
+
filepath: Path to the PDF file
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if decryption successful, False otherwise
|
|
226
|
+
"""
|
|
227
|
+
if reader.decrypt(password):
|
|
228
|
+
logger.info(f"Successfully decrypted file [{filepath.name}]!")
|
|
229
|
+
return True
|
|
230
|
+
else:
|
|
231
|
+
logger.error(f"Failed to decrypt file [{filepath.name}], incorrect password.")
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _copy_pdf_pages(reader: pypdf.PdfReader) -> pypdf.PdfWriter:
|
|
236
|
+
"""Copy all pages from reader to a new writer.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
reader: Source PDF reader
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
PDF writer with copied pages
|
|
243
|
+
"""
|
|
244
|
+
writer = pypdf.PdfWriter()
|
|
245
|
+
for page_num in range(len(reader.pages)):
|
|
246
|
+
page = reader.pages[page_num]
|
|
247
|
+
writer.add_page(page)
|
|
248
|
+
return writer
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def decrypt_pdf(
|
|
252
|
+
filepath: Path,
|
|
253
|
+
password: str,
|
|
254
|
+
) -> tuple[Path, Path | None]:
|
|
255
|
+
"""Decrypt a PDF file.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
filepath: Path to the PDF file to decrypt
|
|
259
|
+
password: Decryption password
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Tuple containing original file path and decrypted file path (if successful)
|
|
263
|
+
"""
|
|
264
|
+
reader = None
|
|
265
|
+
writer = None
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
# Open the input PDF file
|
|
269
|
+
with filepath.open("rb") as f:
|
|
270
|
+
reader = pypdf.PdfReader(f)
|
|
271
|
+
|
|
272
|
+
# Try to decrypt the file
|
|
273
|
+
if not _attempt_pdf_decryption(reader, password, filepath):
|
|
274
|
+
return filepath, None
|
|
275
|
+
|
|
276
|
+
# Create a new PdfWriter object and copy pages
|
|
277
|
+
writer = _copy_pdf_pages(reader)
|
|
278
|
+
|
|
279
|
+
# Write the decrypted PDF to output file
|
|
280
|
+
outfile = filepath.with_suffix(conf.output_suffix_decrypted)
|
|
281
|
+
|
|
282
|
+
# Check if target file already exists
|
|
283
|
+
_handle_output_file_conflict(outfile)
|
|
284
|
+
|
|
285
|
+
with outfile.open("wb") as output_file:
|
|
286
|
+
writer.write(output_file)
|
|
287
|
+
logger.info(f"Wrote decrypted file to [{outfile}]")
|
|
288
|
+
return filepath, outfile
|
|
289
|
+
|
|
290
|
+
except OSError:
|
|
291
|
+
logger.exception("Failed to write decrypted file")
|
|
292
|
+
return filepath, None
|
|
293
|
+
except Exception:
|
|
294
|
+
logger.exception(f"Error decrypting file [{filepath.name}]")
|
|
295
|
+
return filepath, None
|
|
296
|
+
finally:
|
|
297
|
+
# Explicitly clean up resources
|
|
298
|
+
_cleanup_resources(reader, writer)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def list_pdf() -> tuple[list[Path], list[Path]]:
|
|
302
|
+
"""List PDF files in current directory.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Tuple containing lists of unencrypted and encrypted PDF files
|
|
306
|
+
"""
|
|
307
|
+
un_encrypted = [_ for _ in CWD.rglob("*.pdf") if not is_encrypted(_)]
|
|
308
|
+
encrypted = [_ for _ in CWD.rglob("*.pdf") if is_encrypted(_)]
|
|
309
|
+
|
|
310
|
+
logger.info(f"Encrypted files: [green bold]{encrypted}")
|
|
311
|
+
logger.info(f"Unencrypted files: [green bold]{un_encrypted}")
|
|
312
|
+
return un_encrypted, encrypted
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def run_batch(func, files: list[Path]) -> None:
|
|
316
|
+
"""Run function on multiple files concurrently.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
func: Function to apply to each file
|
|
320
|
+
files: List of files to process
|
|
321
|
+
"""
|
|
322
|
+
if not files:
|
|
323
|
+
logger.info("No files to process")
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
logger.info(f"Processing {len(files)} files...")
|
|
327
|
+
|
|
328
|
+
with ThreadPoolExecutor(max_workers=conf.max_workers) as executor:
|
|
329
|
+
# Submit all tasks
|
|
330
|
+
future_to_file = {executor.submit(func, file): file for file in files}
|
|
331
|
+
|
|
332
|
+
# Process completed tasks
|
|
333
|
+
for future in as_completed(future_to_file):
|
|
334
|
+
file = future_to_file[future]
|
|
335
|
+
try:
|
|
336
|
+
result = future.result()
|
|
337
|
+
if result[1]: # If processing was successful
|
|
338
|
+
logger.info(f"Completed: {file.name}")
|
|
339
|
+
else:
|
|
340
|
+
logger.error(f"Failed: {file.name}")
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.error(f"Error processing {file.name}: {e}")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def decrypt(password: str) -> None:
|
|
346
|
+
"""Execute decryption operation.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
password: Decryption password
|
|
350
|
+
"""
|
|
351
|
+
_, encrypted_files = list_pdf()
|
|
352
|
+
if not encrypted_files:
|
|
353
|
+
logger.error(f"No encrypted PDF files found in directory: {CWD}")
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
dec_func = partial(decrypt_pdf, password=password)
|
|
357
|
+
run_batch(dec_func, encrypted_files)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def encrypt(password: str) -> None:
|
|
361
|
+
"""Execute encryption operation.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
password: Encryption password
|
|
365
|
+
"""
|
|
366
|
+
unencrypted_files, _ = list_pdf()
|
|
367
|
+
if not unencrypted_files:
|
|
368
|
+
logger.error(f"No unencrypted PDF files found in directory: {CWD}")
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
enc_func = partial(encrypt_pdf, password=password)
|
|
372
|
+
run_batch(enc_func, unencrypted_files)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def parse_args() -> argparse.Namespace:
|
|
376
|
+
"""Parse command line arguments.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Parsed arguments namespace
|
|
380
|
+
"""
|
|
381
|
+
parser = argparse.ArgumentParser(
|
|
382
|
+
description="Encrypt/decrypt PDF files in current directory",
|
|
383
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
384
|
+
epilog="""
|
|
385
|
+
Examples:
|
|
386
|
+
pdfcrypt encrypt mypassword # Encrypt all unencrypted PDFs
|
|
387
|
+
pdfcrypt decrypt mypassword # Decrypt all encrypted PDFs
|
|
388
|
+
pdfcrypt --help # Show this help message
|
|
389
|
+
""",
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
parser.add_argument(
|
|
393
|
+
"action",
|
|
394
|
+
choices=["encrypt", "decrypt"],
|
|
395
|
+
help="Action to perform: encrypt or decrypt PDF files",
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
parser.add_argument("password", help="Password for encryption/decryption")
|
|
399
|
+
|
|
400
|
+
parser.add_argument(
|
|
401
|
+
"--workers",
|
|
402
|
+
type=int,
|
|
403
|
+
default=4,
|
|
404
|
+
help="Number of concurrent workers (default: 4)",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
parser.add_argument(
|
|
408
|
+
"-v",
|
|
409
|
+
"--version",
|
|
410
|
+
action="version",
|
|
411
|
+
version=f"%(prog)s {__version__} (build {__build__})",
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
return parser.parse_args()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def main() -> None:
|
|
418
|
+
"""Main entry point for the pdfcrypt CLI tool."""
|
|
419
|
+
args = parse_args()
|
|
420
|
+
|
|
421
|
+
# Update configuration
|
|
422
|
+
conf.max_workers = args.workers
|
|
423
|
+
conf.default_password = args.password
|
|
424
|
+
|
|
425
|
+
# Execute requested action
|
|
426
|
+
if args.action == "encrypt":
|
|
427
|
+
encrypt(args.password)
|
|
428
|
+
elif args.action == "decrypt":
|
|
429
|
+
decrypt(args.password)
|
|
430
|
+
else:
|
|
431
|
+
logger.error(f"Unknown action: {args.action}")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
if __name__ == "__main__":
|
|
435
|
+
main()
|
sfi/pdfsplit/pdfsplit.py
CHANGED
|
@@ -7,7 +7,6 @@ from pathlib import Path
|
|
|
7
7
|
import fitz
|
|
8
8
|
|
|
9
9
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
10
|
-
cwd = Path.cwd()
|
|
11
10
|
logger = logging.getLogger(__name__)
|
|
12
11
|
|
|
13
12
|
|
|
@@ -50,7 +49,9 @@ def split_by_number(input_file: Path, output_file: Path, number: int) -> None:
|
|
|
50
49
|
|
|
51
50
|
end_page = min(current_page + pages_in_this_part, total_pages)
|
|
52
51
|
|
|
53
|
-
part_file =
|
|
52
|
+
part_file = (
|
|
53
|
+
output_file.parent / f"{output_file.stem}_part{i + 1}{output_file.suffix}"
|
|
54
|
+
)
|
|
54
55
|
part_doc = fitz.open()
|
|
55
56
|
|
|
56
57
|
for page_num in range(current_page, end_page):
|
|
@@ -58,7 +59,9 @@ def split_by_number(input_file: Path, output_file: Path, number: int) -> None:
|
|
|
58
59
|
|
|
59
60
|
part_doc.save(part_file)
|
|
60
61
|
part_doc.close()
|
|
61
|
-
logger.info(
|
|
62
|
+
logger.info(
|
|
63
|
+
f"Created part {i + 1}: {part_file} (pages {current_page + 1}-{end_page})"
|
|
64
|
+
)
|
|
62
65
|
|
|
63
66
|
current_page = end_page
|
|
64
67
|
|
|
@@ -77,7 +80,10 @@ def split_by_size(input_file: Path, output_file: Path, size: int) -> None:
|
|
|
77
80
|
|
|
78
81
|
while start_page < total_pages:
|
|
79
82
|
end_page = min(start_page + size, total_pages)
|
|
80
|
-
part_file =
|
|
83
|
+
part_file = (
|
|
84
|
+
output_file.parent
|
|
85
|
+
/ f"{output_file.stem}_part{part + 1}{output_file.suffix}"
|
|
86
|
+
)
|
|
81
87
|
part_doc = fitz.open()
|
|
82
88
|
|
|
83
89
|
for page_num in range(start_page, end_page):
|
|
@@ -85,7 +91,9 @@ def split_by_size(input_file: Path, output_file: Path, size: int) -> None:
|
|
|
85
91
|
|
|
86
92
|
part_doc.save(part_file)
|
|
87
93
|
part_doc.close()
|
|
88
|
-
logger.info(
|
|
94
|
+
logger.info(
|
|
95
|
+
f"Created part {part + 1}: {part_file} (pages {start_page + 1}-{end_page})"
|
|
96
|
+
)
|
|
89
97
|
|
|
90
98
|
start_page = end_page
|
|
91
99
|
part += 1
|
|
@@ -122,18 +130,37 @@ def split_by_range(input_file: Path, output_file: Path, range_str: str) -> None:
|
|
|
122
130
|
|
|
123
131
|
|
|
124
132
|
def main() -> None:
|
|
133
|
+
"""Main entry point for pdfsplit CLI."""
|
|
125
134
|
parser = argparse.ArgumentParser(description="Split PDF files")
|
|
126
135
|
parser.add_argument("input", help="Input PDF file")
|
|
127
|
-
parser.add_argument(
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"output", nargs="?", help="Output PDF file (optional for -n and -s modes)"
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"-o",
|
|
141
|
+
"--output-dir",
|
|
142
|
+
default=".",
|
|
143
|
+
help="Output directory (default: current directory)",
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"-f",
|
|
147
|
+
"--output-format",
|
|
148
|
+
help="Output file format pattern, e.g., 'split_{part:02d}.pdf'",
|
|
149
|
+
)
|
|
130
150
|
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
131
151
|
|
|
132
152
|
# Split by number, size, or range
|
|
133
153
|
group = parser.add_mutually_exclusive_group(required=True)
|
|
134
154
|
group.add_argument("-n", "--number", type=int, help="Number of splits")
|
|
135
|
-
group.add_argument(
|
|
136
|
-
|
|
155
|
+
group.add_argument(
|
|
156
|
+
"-s", "--size", type=int, default=1, help="Size of each split in pages"
|
|
157
|
+
)
|
|
158
|
+
group.add_argument(
|
|
159
|
+
"-r",
|
|
160
|
+
"--range",
|
|
161
|
+
type=str,
|
|
162
|
+
help="Range of pages to extract, e.g., '1,2,4-10,15-20,25-'",
|
|
163
|
+
)
|
|
137
164
|
|
|
138
165
|
args = parser.parse_args()
|
|
139
166
|
|
|
@@ -142,7 +169,9 @@ def main() -> None:
|
|
|
142
169
|
|
|
143
170
|
output_dir = Path(args.output_dir)
|
|
144
171
|
if not output_dir.is_dir():
|
|
145
|
-
logger.error(
|
|
172
|
+
logger.error(
|
|
173
|
+
f"Output directory {args.output_dir} does not exist, please check the path."
|
|
174
|
+
)
|
|
146
175
|
return
|
|
147
176
|
|
|
148
177
|
input_file = Path(args.input)
|
|
@@ -157,7 +186,11 @@ def main() -> None:
|
|
|
157
186
|
return
|
|
158
187
|
|
|
159
188
|
if not args.range:
|
|
160
|
-
output_file =
|
|
189
|
+
output_file = (
|
|
190
|
+
output_dir / (input_file.stem + "_split.pdf")
|
|
191
|
+
if not args.output
|
|
192
|
+
else Path(args.output)
|
|
193
|
+
)
|
|
161
194
|
else:
|
|
162
195
|
output_file = Path(args.output)
|
|
163
196
|
|
sfi/pyarchive/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
|
sfi/pyarchive/pyarchive.py
CHANGED
|
@@ -312,7 +312,7 @@ class PyArchiver:
|
|
|
312
312
|
@cached_property
|
|
313
313
|
def solution(self) -> Solution:
|
|
314
314
|
"""Get the solution from the target directory."""
|
|
315
|
-
return Solution.from_directory(self.root_dir
|
|
315
|
+
return Solution.from_directory(self.root_dir)
|
|
316
316
|
|
|
317
317
|
@cached_property
|
|
318
318
|
def projects(self) -> dict[str, Project]:
|
|
@@ -456,7 +456,7 @@ class EmbedInstaller:
|
|
|
456
456
|
@cached_property
|
|
457
457
|
def solution(self) -> Solution:
|
|
458
458
|
"""Get the solution from the target directory."""
|
|
459
|
-
return Solution.from_directory(self.root_dir
|
|
459
|
+
return Solution.from_directory(self.root_dir)
|
|
460
460
|
|
|
461
461
|
@cached_property
|
|
462
462
|
def arch(self) -> str:
|
sfi/pylibpack/pylibpack.py
CHANGED
|
@@ -54,7 +54,7 @@ DEFAULT_MAX_WORKERS: Final[int] = 4
|
|
|
54
54
|
DEFAULT_MIRROR: Final[str] = "aliyun"
|
|
55
55
|
DEFAULT_OPTIMIZE: Final[bool] = True
|
|
56
56
|
|
|
57
|
-
PYPI_MIRRORS = {
|
|
57
|
+
PYPI_MIRRORS: Final[dict[str, str]] = {
|
|
58
58
|
"pypi": "https://pypi.org/simple",
|
|
59
59
|
"tsinghua": "https://pypi.tuna.tsinghua.edu.cn/simple",
|
|
60
60
|
"aliyun": "https://mirrors.aliyun.com/pypi/simple/",
|
|
@@ -915,14 +915,10 @@ class LibraryCache:
|
|
|
915
915
|
@staticmethod
|
|
916
916
|
def _should_skip_dist_info(file_path: Path) -> bool:
|
|
917
917
|
"""Check if the file path should be skipped because it's a dist-info directory."""
|
|
918
|
-
|
|
919
|
-
if name.endswith(".dist-info"):
|
|
918
|
+
if file_path.name.endswith(".dist-info"):
|
|
920
919
|
return True
|
|
921
920
|
# Check if any parent directory ends with .dist-info
|
|
922
|
-
for part in file_path.parts
|
|
923
|
-
if part.endswith(".dist-info"):
|
|
924
|
-
return True
|
|
925
|
-
return False
|
|
921
|
+
return any(part.endswith(".dist-info") for part in file_path.parts)
|
|
926
922
|
|
|
927
923
|
def clear_cache(self) -> None:
|
|
928
924
|
"""Clear all cached packages."""
|
|
@@ -1410,14 +1406,10 @@ class PyLibPacker:
|
|
|
1410
1406
|
@staticmethod
|
|
1411
1407
|
def _should_skip_dist_info(file_path: Path) -> bool:
|
|
1412
1408
|
"""Check if the file path should be skipped because it's a dist-info directory."""
|
|
1413
|
-
|
|
1414
|
-
if name.endswith(".dist-info"):
|
|
1409
|
+
if file_path.name.endswith(".dist-info"):
|
|
1415
1410
|
return True
|
|
1416
1411
|
# Check if any parent directory ends with .dist-info
|
|
1417
|
-
for part in file_path.parts
|
|
1418
|
-
if part.endswith(".dist-info"):
|
|
1419
|
-
return True
|
|
1420
|
-
return False
|
|
1412
|
+
return any(part.endswith(".dist-info") for part in file_path.parts)
|
|
1421
1413
|
|
|
1422
1414
|
def run(self) -> None:
|
|
1423
1415
|
"""Pack project dependencies from base directory with concurrent processing."""
|
sfi/pyloadergen/pyloadergen.py
CHANGED
|
@@ -967,8 +967,10 @@ class EntryFile:
|
|
|
967
967
|
try:
|
|
968
968
|
content = self.source_file.read_text(encoding="utf-8")
|
|
969
969
|
gui_keywords = ["pyside2", "pyqt5", "pyqt6", "tkinter", "tkinter.ttk"]
|
|
970
|
-
return any(
|
|
971
|
-
|
|
970
|
+
return any(
|
|
971
|
+
f"from {kw}" in content.lower() or f"import {kw}" in content.lower()
|
|
972
|
+
for kw in gui_keywords
|
|
973
|
+
)
|
|
972
974
|
except Exception:
|
|
973
975
|
return False
|
|
974
976
|
|
|
@@ -1358,7 +1360,8 @@ class PyLoaderGenerator:
|
|
|
1358
1360
|
# Prepare project tasks
|
|
1359
1361
|
project_tasks = []
|
|
1360
1362
|
for project_name, project in projects.items():
|
|
1361
|
-
|
|
1363
|
+
# Use the actual directory from toml_path instead of project name
|
|
1364
|
+
project_dir = project.toml_path.parent
|
|
1362
1365
|
if not project_dir.is_dir():
|
|
1363
1366
|
logger.error(
|
|
1364
1367
|
f"Project directory not found: {project_dir}, skipping..."
|