pysfi 0.1.14__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.14.dist-info → pysfi-0.1.15.dist-info}/METADATA +1 -1
- {pysfi-0.1.14.dist-info → pysfi-0.1.15.dist-info}/RECORD +15 -13
- {pysfi-0.1.14.dist-info → pysfi-0.1.15.dist-info}/entry_points.txt +1 -0
- sfi/__init__.py +1 -1
- sfi/bumpversion/__init__.py +1 -1
- sfi/docscan/__init__.py +1 -1
- sfi/pdfcrypt/__init__.py +30 -0
- sfi/pdfcrypt/pdfcrypt.py +435 -0
- sfi/pyarchive/pyarchive.py +1 -1
- sfi/pyembedinstall/pyembedinstall.py +1 -1
- sfi/pyloadergen/pyloadergen.py +6 -3
- sfi/pypack/pypack.py +5 -1
- sfi/pyprojectparse/pyprojectparse.py +9 -31
- sfi/pysourcepack/pysourcepack.py +11 -14
- {pysfi-0.1.14.dist-info → pysfi-0.1.15.dist-info}/WHEEL +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
sfi/__init__.py,sha256=
|
|
1
|
+
sfi/__init__.py,sha256=TVJQhNdT9_xTAmwbMdf5rw-JogSq0EdM2iR-igNIHLk,685
|
|
2
2
|
sfi/cli.py,sha256=o-sFO6I3X3DMO2I_Gzc9LziWBdm2ruHXpjbraXGzZi0,933
|
|
3
3
|
sfi/alarmclock/__init__.py,sha256=AUnUxITgFPYgSjn9ug2DEejrz5HDP56zZKYu-6JW1vY,72
|
|
4
4
|
sfi/alarmclock/alarmclock.py,sha256=ixVkbg548smUivRsqyI3YSZ81BWIrKawnuezAp3BzyE,11635
|
|
5
|
-
sfi/bumpversion/__init__.py,sha256=
|
|
5
|
+
sfi/bumpversion/__init__.py,sha256=yAaQaGUKnwPERhTaS4XXANQc9AT31nseY2dKomRPRmU,122
|
|
6
6
|
sfi/bumpversion/bumpversion.py,sha256=Uxj0S8ZGmIRhfCx2ziWkVe5gHv74r68UPkiYzZcAwxo,21288
|
|
7
7
|
sfi/cleanbuild/__init__.py,sha256=_bXrBcQkHQTE2484-UMWAWz8kq-2Yjn9hleLcjF-pJg,98
|
|
8
8
|
sfi/cleanbuild/cleanbuild.py,sha256=Ilk4zBE48y6tUC3C5qp1pXdQ6CF0NZlra4ZbdiU5s2k,5270
|
|
@@ -10,7 +10,7 @@ sfi/condasetup/__init__.py,sha256=4W8VliAYUP1KY2gLJ_YDy2TmcXYVm-PY7XikQD_bFwA,2
|
|
|
10
10
|
sfi/condasetup/condasetup.py,sha256=tuRzRi4NKmIfqN4Gq5w-VmY7BnleizPhAbYta8AzH4c,4976
|
|
11
11
|
sfi/docdiff/__init__.py,sha256=4W8VliAYUP1KY2gLJ_YDy2TmcXYVm-PY7XikQD_bFwA,2
|
|
12
12
|
sfi/docdiff/docdiff.py,sha256=YR8e-gkbvjH0Bjt5fVVW0-8b7lbinQs6TRo4B5XgEjE,7783
|
|
13
|
-
sfi/docscan/__init__.py,sha256=
|
|
13
|
+
sfi/docscan/__init__.py,sha256=v7sLqtKfRfuxZ4bAKVvR5FBc_SX44W9MOBL_vmmAuss,121
|
|
14
14
|
sfi/docscan/docscan.py,sha256=hXSOrfMikyg-Zsi_mpZvn31ea1J0un90ftwpRoXxNdA,42765
|
|
15
15
|
sfi/docscan/docscan_gui.py,sha256=KoXVHOQjUwtkxh5tI4V6mLBMl9PAP3Hp7zydJ_lppoQ,52345
|
|
16
16
|
sfi/docscan/lang/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -30,12 +30,14 @@ sfi/llmserver/__init__.py,sha256=4W8VliAYUP1KY2gLJ_YDy2TmcXYVm-PY7XikQD_bFwA,2
|
|
|
30
30
|
sfi/llmserver/llmserver.py,sha256=Fm4Go7wif4xMGomMFDsyJnYMafXsWemGkr-VfaeYa6w,13530
|
|
31
31
|
sfi/makepython/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
32
|
sfi/makepython/makepython.py,sha256=GrzQTJyyB2PXugJK3yLxP1V5I_bSymW0qfkeqZvxRPA,43154
|
|
33
|
+
sfi/pdfcrypt/__init__.py,sha256=cKY_rosu4Qo0QvJF5d_vFgvMMcH14JH6uElOMRXo654,565
|
|
34
|
+
sfi/pdfcrypt/pdfcrypt.py,sha256=s7LMABQt4gW8UabuwdKdy_hgmSa8ZDePFrtab3tEkEA,13236
|
|
33
35
|
sfi/pdfsplit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
36
|
sfi/pdfsplit/pdfsplit.py,sha256=LOz7PuLlLBiXlFTjJej9G0UvJ03GZ9H-Gghy8ErvJzE,6576
|
|
35
37
|
sfi/pyarchive/__init__.py,sha256=4W8VliAYUP1KY2gLJ_YDy2TmcXYVm-PY7XikQD_bFwA,2
|
|
36
|
-
sfi/pyarchive/pyarchive.py,sha256=
|
|
38
|
+
sfi/pyarchive/pyarchive.py,sha256=ne8FZh79_0ZtIWCG6Ddg9uUF6JxTvFsvfilOLl4wYmI,38280
|
|
37
39
|
sfi/pyembedinstall/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
-
sfi/pyembedinstall/pyembedinstall.py,sha256=
|
|
40
|
+
sfi/pyembedinstall/pyembedinstall.py,sha256=1XOTR6Mg2YtP_-t9jRjiNGSSTw5TklDO60XPc3I9TL0,24158
|
|
39
41
|
sfi/pylibpack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
42
|
sfi/pylibpack/pylibpack.py,sha256=AJbMY4aHJq0YhyUXryBargNqCUTeSwaUuc3SiCcjcko,57685
|
|
41
43
|
sfi/pylibpack/rules/numpy.json,sha256=ee4gA5NBudFi3MaJA-QlBKQwiQAUb-eluF8HNVkl7Vk,384
|
|
@@ -45,13 +47,13 @@ sfi/pylibpack/rules/pyside2.json,sha256=uSSteT-3wDohWwQ36Z5mSOaSbxrR4565In4uZj_e
|
|
|
45
47
|
sfi/pylibpack/rules/scipy.json,sha256=vTSi3W5BGWcwMkaDnyD6Yg7ijZdicPEUMw4fnRTnNf4,468
|
|
46
48
|
sfi/pylibpack/rules/shiboken2.json,sha256=9Pl3eslvergyjlyHNknkyN0oZlcH3049WULe5WjsmKM,515
|
|
47
49
|
sfi/pyloadergen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
|
-
sfi/pyloadergen/pyloadergen.py,sha256=
|
|
50
|
+
sfi/pyloadergen/pyloadergen.py,sha256=NiQGk4I5LMNvEaMc9aasXKHGNSg127AcUR5hmDPm6sQ,45801
|
|
49
51
|
sfi/pypack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
|
-
sfi/pypack/pypack.py,sha256=
|
|
52
|
+
sfi/pypack/pypack.py,sha256=v2qdSGL4cANO28mbaJSGbThiyJG33djsTcVy9RlwDgc,23031
|
|
51
53
|
sfi/pyprojectparse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
-
sfi/pyprojectparse/pyprojectparse.py,sha256=
|
|
54
|
+
sfi/pyprojectparse/pyprojectparse.py,sha256=XPY1E3mW6qkvuAlGBK8fpTfPQJYkWpQFcYZTZ4IlROk,29784
|
|
53
55
|
sfi/pysourcepack/__init__.py,sha256=4W8VliAYUP1KY2gLJ_YDy2TmcXYVm-PY7XikQD_bFwA,2
|
|
54
|
-
sfi/pysourcepack/pysourcepack.py,sha256=
|
|
56
|
+
sfi/pysourcepack/pysourcepack.py,sha256=HNhO0Ry3kAzZJ5-bXlxn81jrh_GdjKzWFOyz7SQhN1Q,12158
|
|
55
57
|
sfi/quizbase/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
58
|
sfi/quizbase/quizbase.py,sha256=3tPUuYexZ9TVsNPPO_Itmr5OvyHSgY5OSUZwPoQt9zg,30605
|
|
57
59
|
sfi/quizbase/quizbase_gui.py,sha256=m_Lj3au1a8gEv5x7KOTjomiP1NpXHUgHSPE4lLv63hY,34733
|
|
@@ -62,7 +64,7 @@ sfi/taskkill/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
62
64
|
sfi/taskkill/taskkill.py,sha256=wM9g8sWJVTy4GxXe26rKdax2lIBI-uH9wP5wRenriH4,11606
|
|
63
65
|
sfi/which/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
66
|
sfi/which/which.py,sha256=2YbGgSiT1ySapKVV1ESoPf4P-JU8vvzmsZY39NiVr6k,2596
|
|
65
|
-
pysfi-0.1.
|
|
66
|
-
pysfi-0.1.
|
|
67
|
-
pysfi-0.1.
|
|
68
|
-
pysfi-0.1.
|
|
67
|
+
pysfi-0.1.15.dist-info/METADATA,sha256=LyUkSywzxLXVtmVvhzMuSKMxEo5EApdoyHk6q3eFcA0,4198
|
|
68
|
+
pysfi-0.1.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
69
|
+
pysfi-0.1.15.dist-info/entry_points.txt,sha256=j0zUSQnJ1YpYEKTK0oEICBjIyOAVZOd5Ss2_k0id530,1297
|
|
70
|
+
pysfi-0.1.15.dist-info/RECORD,,
|
|
@@ -14,6 +14,7 @@ llmqnt = sfi.llmquantize.llmquantize:main
|
|
|
14
14
|
llmsvr = sfi.llmserver.llmserver:main
|
|
15
15
|
makepython = sfi.makepython.makepython:main
|
|
16
16
|
mkp = sfi.makepython.makepython:main
|
|
17
|
+
pdfcrypt = sfi.pdfcrypt.pdfcrypt:main
|
|
17
18
|
pdfsplit = sfi.pdfsplit.pdfsplit:main
|
|
18
19
|
pyarchive = sfi.pyarchive.pyarchive:main
|
|
19
20
|
pyembedinstall = sfi.pyembedinstall.pyembedinstall:main
|
sfi/__init__.py
CHANGED
sfi/bumpversion/__init__.py
CHANGED
sfi/docscan/__init__.py
CHANGED
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/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/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..."
|
sfi/pypack/pypack.py
CHANGED
|
@@ -229,6 +229,10 @@ class PackageWorkflow:
|
|
|
229
229
|
"""
|
|
230
230
|
return self.solution.projects
|
|
231
231
|
|
|
232
|
+
@cached_property
|
|
233
|
+
def sorted_projects(self) -> dict[str, Project]:
|
|
234
|
+
return dict(sorted(self.projects.items()))
|
|
235
|
+
|
|
232
236
|
@property
|
|
233
237
|
def dist_dir(self) -> Path:
|
|
234
238
|
"""Get distribution directory path."""
|
|
@@ -403,7 +407,7 @@ class PackageWorkflow:
|
|
|
403
407
|
def list_projects(self) -> None:
|
|
404
408
|
"""List all available projects."""
|
|
405
409
|
logger.info(f"Listing projects in {self.root_dir}")
|
|
406
|
-
for project in self.
|
|
410
|
+
for project in self.sorted_projects.values():
|
|
407
411
|
logger.info(f" - {project}")
|
|
408
412
|
|
|
409
413
|
def _scan_executables(self) -> list[Path]:
|
|
@@ -457,7 +457,6 @@ class Solution:
|
|
|
457
457
|
|
|
458
458
|
root_dir: Path
|
|
459
459
|
projects: dict[str, Project]
|
|
460
|
-
update: bool = False
|
|
461
460
|
start_time: float = field(default_factory=time.perf_counter)
|
|
462
461
|
time_stamp: datetime.datetime = field(default_factory=datetime.datetime.now)
|
|
463
462
|
|
|
@@ -466,7 +465,6 @@ class Solution:
|
|
|
466
465
|
f"<Solution(\n"
|
|
467
466
|
f" root_dir={self.root_dir!r},\n"
|
|
468
467
|
f" projects: {len(self.projects)},\n"
|
|
469
|
-
f" update={self.update!r},\n"
|
|
470
468
|
f" time_used={self.elapsed_time:.4f}s,\n"
|
|
471
469
|
f" timestamp={self.time_stamp!r}\n"
|
|
472
470
|
f")>"
|
|
@@ -570,9 +568,7 @@ class Solution:
|
|
|
570
568
|
return None
|
|
571
569
|
|
|
572
570
|
@classmethod
|
|
573
|
-
def from_toml_files(
|
|
574
|
-
cls, root_dir: Path, toml_files: list[Path], update: bool = False
|
|
575
|
-
) -> Solution:
|
|
571
|
+
def from_toml_files(cls, root_dir: Path, toml_files: list[Path]) -> Solution:
|
|
576
572
|
"""Create a Solution instance by parsing multiple pyproject.toml files.
|
|
577
573
|
|
|
578
574
|
Args:
|
|
@@ -620,12 +616,10 @@ class Solution:
|
|
|
620
616
|
|
|
621
617
|
projects[project.name] = project
|
|
622
618
|
|
|
623
|
-
return cls(root_dir=root_dir, projects=projects
|
|
619
|
+
return cls(root_dir=root_dir, projects=projects)
|
|
624
620
|
|
|
625
621
|
@classmethod
|
|
626
|
-
def from_json_data(
|
|
627
|
-
cls, root_dir: Path, json_data: dict[str, Any], update: bool = False
|
|
628
|
-
) -> Solution:
|
|
622
|
+
def from_json_data(cls, root_dir: Path, json_data: dict[str, Any]) -> Solution:
|
|
629
623
|
"""Create a Solution instance from JSON data.
|
|
630
624
|
|
|
631
625
|
Args:
|
|
@@ -660,10 +654,10 @@ class Solution:
|
|
|
660
654
|
except Exception as e:
|
|
661
655
|
logger.error(f"Unknown error loading project data from JSON data: {e}")
|
|
662
656
|
|
|
663
|
-
return cls(root_dir=root_dir, projects=projects
|
|
657
|
+
return cls(root_dir=root_dir, projects=projects)
|
|
664
658
|
|
|
665
659
|
@classmethod
|
|
666
|
-
def from_json_file(cls, json_file: Path
|
|
660
|
+
def from_json_file(cls, json_file: Path) -> Solution:
|
|
667
661
|
"""Create a Solution instance from a JSON file.
|
|
668
662
|
|
|
669
663
|
Args:
|
|
@@ -682,7 +676,7 @@ class Solution:
|
|
|
682
676
|
with json_file.open("r", encoding="utf-8") as f:
|
|
683
677
|
loaded_data = json.load(f)
|
|
684
678
|
logger.debug(f"\t - Loaded project data from {json_file}")
|
|
685
|
-
return cls.from_json_data(json_file.parent, loaded_data
|
|
679
|
+
return cls.from_json_data(json_file.parent, loaded_data)
|
|
686
680
|
except (OSError, json.JSONDecodeError, KeyError) as e:
|
|
687
681
|
logger.error(f"Error loading project data from {json_file}: {e}")
|
|
688
682
|
return cls(root_dir=json_file.parent, projects={})
|
|
@@ -691,7 +685,7 @@ class Solution:
|
|
|
691
685
|
return cls(root_dir=json_file.parent, projects={})
|
|
692
686
|
|
|
693
687
|
@classmethod
|
|
694
|
-
def from_directory(cls, root_dir: Path
|
|
688
|
+
def from_directory(cls, root_dir: Path) -> Solution:
|
|
695
689
|
"""Create a Solution instance by scanning a directory for pyproject.toml files.
|
|
696
690
|
|
|
697
691
|
This method recursively searches the given directory for pyproject.toml files,
|
|
@@ -709,13 +703,9 @@ class Solution:
|
|
|
709
703
|
logger.error(f"Error: {root_dir} is not a directory")
|
|
710
704
|
return cls(root_dir=root_dir, projects={})
|
|
711
705
|
|
|
712
|
-
# Use walrus operator to avoid intermediate variable
|
|
713
|
-
if not update and (project_json := root_dir / "projects.json").is_file():
|
|
714
|
-
return cls.from_json_file(project_json, update=update)
|
|
715
|
-
|
|
716
706
|
logger.debug(f"Parsing pyproject.toml in {root_dir}...")
|
|
717
707
|
toml_files = list(root_dir.rglob("pyproject.toml"))
|
|
718
|
-
return cls.from_toml_files(root_dir, toml_files
|
|
708
|
+
return cls.from_toml_files(root_dir, toml_files)
|
|
719
709
|
|
|
720
710
|
def _write_project_json(self):
|
|
721
711
|
"""Write the project data to a projects.json file for caching.
|
|
@@ -729,12 +719,6 @@ class Solution:
|
|
|
729
719
|
"""
|
|
730
720
|
# Cache json_file reference to avoid repeated cached_property access
|
|
731
721
|
json_file = self.json_file
|
|
732
|
-
if json_file.exists() and not self.update:
|
|
733
|
-
logger.info(
|
|
734
|
-
f"\t - Skip write project data file {json_file}, already exists"
|
|
735
|
-
)
|
|
736
|
-
return
|
|
737
|
-
|
|
738
722
|
try:
|
|
739
723
|
# Pre-cache raw_data access to avoid repeated property access
|
|
740
724
|
serializable_data = {
|
|
@@ -772,12 +756,6 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
772
756
|
parser.add_argument(
|
|
773
757
|
"--debug", "-d", action="store_true", help="Enable debug logging output"
|
|
774
758
|
)
|
|
775
|
-
parser.add_argument(
|
|
776
|
-
"--update",
|
|
777
|
-
"-u",
|
|
778
|
-
action="store_true",
|
|
779
|
-
help="Force update by re-parsing projects instead of using cache",
|
|
780
|
-
)
|
|
781
759
|
return parser
|
|
782
760
|
|
|
783
761
|
|
|
@@ -794,4 +772,4 @@ def main() -> None:
|
|
|
794
772
|
if args.debug:
|
|
795
773
|
logger.setLevel(logging.DEBUG)
|
|
796
774
|
|
|
797
|
-
Solution.from_directory(Path(args.directory)
|
|
775
|
+
Solution.from_directory(Path(args.directory))
|
sfi/pysourcepack/pysourcepack.py
CHANGED
|
@@ -118,7 +118,6 @@ class ProjectPacker:
|
|
|
118
118
|
|
|
119
119
|
parent: PySourcePacker
|
|
120
120
|
project: Project
|
|
121
|
-
project_name: str
|
|
122
121
|
include_patterns: set[str]
|
|
123
122
|
exclude_patterns: set[str]
|
|
124
123
|
|
|
@@ -130,16 +129,13 @@ class ProjectPacker:
|
|
|
130
129
|
@cached_property
|
|
131
130
|
def project_path(self) -> Path:
|
|
132
131
|
"""Get project directory path."""
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if self.is_single_project
|
|
136
|
-
else self.parent.root_dir / self.project_name
|
|
137
|
-
)
|
|
132
|
+
# Use the actual directory from toml_path instead of project name
|
|
133
|
+
return self.project.toml_path.parent
|
|
138
134
|
|
|
139
135
|
@cached_property
|
|
140
136
|
def output_dir(self) -> Path:
|
|
141
137
|
"""Get output directory path."""
|
|
142
|
-
return self.parent.root_dir / "dist" / "src" / self.
|
|
138
|
+
return self.parent.root_dir / "dist" / "src" / self.project.normalized_name
|
|
143
139
|
|
|
144
140
|
def validate_project_path(self) -> bool:
|
|
145
141
|
"""Validate project path exists and is a directory."""
|
|
@@ -159,14 +155,14 @@ class ProjectPacker:
|
|
|
159
155
|
Returns:
|
|
160
156
|
True if packing succeeded, False otherwise
|
|
161
157
|
"""
|
|
162
|
-
logger.debug(f"Start packing project: {self.
|
|
158
|
+
logger.debug(f"Start packing project: {self.project.normalized_name}")
|
|
163
159
|
|
|
164
|
-
if not self.
|
|
160
|
+
if not self.project.normalized_name:
|
|
165
161
|
logger.error("Project name cannot be empty")
|
|
166
162
|
return False
|
|
167
163
|
|
|
168
164
|
logger.debug(
|
|
169
|
-
f"Project path: {self.project_path}, project_name: {self.
|
|
165
|
+
f"Project path: {self.project_path}, project_name: {self.project.normalized_name}"
|
|
170
166
|
)
|
|
171
167
|
if not self.validate_project_path():
|
|
172
168
|
return False
|
|
@@ -177,7 +173,9 @@ class ProjectPacker:
|
|
|
177
173
|
copied_files = 0
|
|
178
174
|
copied_dirs = 0
|
|
179
175
|
|
|
180
|
-
logger.info(
|
|
176
|
+
logger.info(
|
|
177
|
+
f"Packing project '{self.project.normalized_name}' to {self.output_dir}"
|
|
178
|
+
)
|
|
181
179
|
|
|
182
180
|
try:
|
|
183
181
|
for item in self.project_path.iterdir():
|
|
@@ -202,12 +200,12 @@ class ProjectPacker:
|
|
|
202
200
|
copied_dirs += 1
|
|
203
201
|
|
|
204
202
|
logger.info(
|
|
205
|
-
f"Successfully packed {self.
|
|
203
|
+
f"Successfully packed {self.project.normalized_name}: {copied_files} files, {copied_dirs} directories"
|
|
206
204
|
)
|
|
207
205
|
return True
|
|
208
206
|
|
|
209
207
|
except Exception as e:
|
|
210
|
-
logger.error(f"Error packing project {self.
|
|
208
|
+
logger.error(f"Error packing project {self.project.normalized_name}: {e}")
|
|
211
209
|
return False
|
|
212
210
|
|
|
213
211
|
|
|
@@ -248,7 +246,6 @@ class PySourcePacker:
|
|
|
248
246
|
packer = ProjectPacker(
|
|
249
247
|
parent=self,
|
|
250
248
|
project=project,
|
|
251
|
-
project_name=project_name,
|
|
252
249
|
include_patterns=self.include_patterns,
|
|
253
250
|
exclude_patterns=self.exclude_patterns,
|
|
254
251
|
)
|
|
File without changes
|