pysfi 0.1.12__py3-none-any.whl → 0.1.14__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 (42) hide show
  1. {pysfi-0.1.12.dist-info → pysfi-0.1.14.dist-info}/METADATA +1 -1
  2. pysfi-0.1.14.dist-info/RECORD +68 -0
  3. {pysfi-0.1.12.dist-info → pysfi-0.1.14.dist-info}/entry_points.txt +3 -0
  4. sfi/__init__.py +19 -2
  5. sfi/alarmclock/__init__.py +3 -0
  6. sfi/alarmclock/alarmclock.py +23 -40
  7. sfi/bumpversion/__init__.py +3 -1
  8. sfi/bumpversion/bumpversion.py +64 -15
  9. sfi/cleanbuild/__init__.py +3 -0
  10. sfi/cleanbuild/cleanbuild.py +5 -1
  11. sfi/cli.py +25 -4
  12. sfi/condasetup/__init__.py +1 -0
  13. sfi/condasetup/condasetup.py +91 -76
  14. sfi/docdiff/__init__.py +1 -0
  15. sfi/docdiff/docdiff.py +3 -2
  16. sfi/docscan/__init__.py +1 -1
  17. sfi/docscan/docscan.py +78 -23
  18. sfi/docscan/docscan_gui.py +152 -48
  19. sfi/filedate/filedate.py +12 -5
  20. sfi/img2pdf/img2pdf.py +453 -0
  21. sfi/llmclient/llmclient.py +31 -8
  22. sfi/llmquantize/llmquantize.py +76 -37
  23. sfi/llmserver/__init__.py +1 -0
  24. sfi/llmserver/llmserver.py +63 -13
  25. sfi/makepython/makepython.py +1145 -201
  26. sfi/pdfsplit/pdfsplit.py +45 -12
  27. sfi/pyarchive/__init__.py +1 -0
  28. sfi/pyarchive/pyarchive.py +908 -278
  29. sfi/pyembedinstall/pyembedinstall.py +88 -89
  30. sfi/pylibpack/pylibpack.py +561 -463
  31. sfi/pyloadergen/pyloadergen.py +372 -218
  32. sfi/pypack/pypack.py +510 -959
  33. sfi/pyprojectparse/pyprojectparse.py +337 -40
  34. sfi/pysourcepack/__init__.py +1 -0
  35. sfi/pysourcepack/pysourcepack.py +210 -131
  36. sfi/quizbase/quizbase_gui.py +2 -2
  37. sfi/taskkill/taskkill.py +168 -59
  38. sfi/which/which.py +11 -3
  39. pysfi-0.1.12.dist-info/RECORD +0 -62
  40. sfi/workflowengine/workflowengine.py +0 -444
  41. {pysfi-0.1.12.dist-info → pysfi-0.1.14.dist-info}/WHEEL +0 -0
  42. /sfi/{workflowengine → img2pdf}/__init__.py +0 -0
@@ -5,19 +5,23 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import logging
7
7
  import shutil
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from dataclasses import dataclass, field
10
+ from functools import cached_property
8
11
  from pathlib import Path
12
+ from typing import Final
13
+
14
+ from sfi.pyprojectparse.pyprojectparse import Project, Solution
9
15
 
10
16
  __version__ = "0.1.0"
11
- __all__ = [
12
- "pack_project",
13
- ]
17
+ __all__ = ["PySourcePacker"]
14
18
 
15
19
  cwd = Path.cwd()
16
20
  logging.basicConfig(level=logging.INFO, format="%(message)s")
17
21
  logger = logging.getLogger(__name__)
18
22
 
19
23
  # Default directories and files to exclude during packaging
20
- DEFAULT_EXCLUDE = {
24
+ DEFAULT_EXCLUDE: Final = {
21
25
  "__pycache__",
22
26
  "*.pyc",
23
27
  "*.pyo",
@@ -35,7 +39,7 @@ DEFAULT_EXCLUDE = {
35
39
  }
36
40
 
37
41
  # Default files to include in packaging
38
- DEFAULT_INCLUDE = {
42
+ DEFAULT_INCLUDE: Final = {
39
43
  "*.py",
40
44
  "README.md",
41
45
  "LICENSE",
@@ -108,98 +112,205 @@ def should_include(
108
112
  return False
109
113
 
110
114
 
111
- def _check_project_path(project_path: Path) -> bool:
112
- if not project_path.exists():
113
- logger.error(f"Project directory {project_path} does not exist")
114
- return False
115
+ @dataclass
116
+ class ProjectPacker:
117
+ """Helper class to pack individual projects."""
118
+
119
+ parent: PySourcePacker
120
+ project: Project
121
+ project_name: str
122
+ include_patterns: set[str]
123
+ exclude_patterns: set[str]
124
+
125
+ @cached_property
126
+ def is_single_project(self) -> bool:
127
+ """Check if the project is the only one in the solution."""
128
+ return len(self.parent.solution.projects) == 1
129
+
130
+ @cached_property
131
+ def project_path(self) -> Path:
132
+ """Get project directory path."""
133
+ return (
134
+ self.parent.root_dir
135
+ if self.is_single_project
136
+ else self.parent.root_dir / self.project_name
137
+ )
115
138
 
116
- if not project_path.is_dir():
117
- logger.error(f"{project_path} is not a directory")
118
- return False
139
+ @cached_property
140
+ def output_dir(self) -> Path:
141
+ """Get output directory path."""
142
+ return self.parent.root_dir / "dist" / "src" / self.project_name
119
143
 
120
- return True
144
+ def validate_project_path(self) -> bool:
145
+ """Validate project path exists and is a directory."""
146
+ if not self.project_path.exists():
147
+ logger.error(f"Project directory {self.project_path} does not exist")
148
+ return False
121
149
 
150
+ if not self.project_path.is_dir():
151
+ logger.error(f"{self.project_path} is not a directory")
152
+ return False
122
153
 
123
- def pack_project(
124
- base_dir: Path,
125
- projects: dict,
126
- project_name: str,
127
- include_patterns: set[str] | None = None,
128
- exclude_patterns: set[str] | None = None,
129
- ) -> bool:
130
- """Pack project source code and resources to dist/src directory.
154
+ return True
131
155
 
132
- Args:
133
- base_dir: Base directory containing project folders
134
- project_name: Name of the project to pack
135
- include_patterns: File patterns to include (default: DEFAULT_INCLUDE)
136
- exclude_patterns: File patterns to exclude (default: DEFAULT_EXCLUDE)
156
+ def pack(self) -> bool:
157
+ """Pack project source code and resources to dist/src directory.
137
158
 
138
- Returns:
139
- True if packing succeeded, False otherwise
140
- """
141
- logger.debug(f"Start packing project: {project_name}")
159
+ Returns:
160
+ True if packing succeeded, False otherwise
161
+ """
162
+ logger.debug(f"Start packing project: {self.project_name}")
142
163
 
143
- if project_name not in projects:
144
- logger.error(f"Project '{project_name}' not found in projects.json")
145
- return False
164
+ if not self.project_name:
165
+ logger.error("Project name cannot be empty")
166
+ return False
146
167
 
147
- if not project_name:
148
- logger.error("Project name cannot be empty")
149
- return False
168
+ logger.debug(
169
+ f"Project path: {self.project_path}, project_name: {self.project_name}"
170
+ )
171
+ if not self.validate_project_path():
172
+ return False
173
+
174
+ self.output_dir.mkdir(parents=True, exist_ok=True)
175
+
176
+ # Copy files
177
+ copied_files = 0
178
+ copied_dirs = 0
179
+
180
+ logger.info(f"Packing project '{self.project_name}' to {self.output_dir}")
181
+
182
+ try:
183
+ for item in self.project_path.iterdir():
184
+ if item.is_file():
185
+ # For files, check if should be included
186
+ if should_include(
187
+ item, self.include_patterns, self.exclude_patterns
188
+ ):
189
+ dest_file = self.output_dir / item.name
190
+ shutil.copy2(item, dest_file)
191
+ logger.debug(f"Copied file: {item.name}")
192
+ copied_files += 1
193
+ elif item.is_dir() and not should_exclude(item, self.exclude_patterns):
194
+ # Copy directory recursively with exclude patterns
195
+ dest_dir = self.output_dir / item.name
196
+ if dest_dir.exists():
197
+ shutil.rmtree(dest_dir)
198
+ shutil.copytree(
199
+ item, dest_dir, ignore=shutil.ignore_patterns(*DEFAULT_EXCLUDE)
200
+ )
201
+ logger.debug(f"Copied directory: {item.name}")
202
+ copied_dirs += 1
203
+
204
+ logger.info(
205
+ f"Successfully packed {self.project_name}: {copied_files} files, {copied_dirs} directories"
206
+ )
207
+ return True
208
+
209
+ except Exception as e:
210
+ logger.error(f"Error packing project {self.project_name}: {e}")
211
+ return False
212
+
213
+
214
+ @dataclass
215
+ class PySourcePacker:
216
+ """Main class for packing Python source code."""
217
+
218
+ root_dir: Path
219
+ include_patterns: set[str] = field(default_factory=lambda: DEFAULT_INCLUDE)
220
+ exclude_patterns: set[str] = field(default_factory=lambda: DEFAULT_EXCLUDE)
221
+
222
+ @cached_property
223
+ def solution(self) -> Solution:
224
+ """Get the solution from the target directory."""
225
+ return Solution.from_directory(self.root_dir)
226
+
227
+ @cached_property
228
+ def projects(self) -> dict[str, Project]:
229
+ """Get the projects from the solution."""
230
+ return self.solution.projects
231
+
232
+ def pack_project(self, project_name: str) -> bool:
233
+ """Pack a single project.
234
+
235
+ Args:
236
+ project_name: Name of the project to pack
237
+
238
+ Returns:
239
+ True if packing succeeded, False otherwise
240
+ """
241
+ projects = self.solution.projects
242
+
243
+ if project_name not in projects:
244
+ logger.error(f"Project '{project_name}' not found in projects.json")
245
+ return False
246
+
247
+ project = projects[project_name]
248
+ packer = ProjectPacker(
249
+ parent=self,
250
+ project=project,
251
+ project_name=project_name,
252
+ include_patterns=self.include_patterns,
253
+ exclude_patterns=self.exclude_patterns,
254
+ )
150
255
 
151
- project_path = base_dir if len(projects) == 1 else base_dir / project_name
152
- output_dir = base_dir / "dist" / "src" / project_name
256
+ return packer.pack()
153
257
 
154
- logger.debug(f"Project path: {project_path}, project_name: {project_name}")
155
- if not _check_project_path(project_path):
156
- return False
258
+ def run(self, project_name: str | None = None) -> None:
259
+ """Pack specified project or all projects concurrently.
260
+
261
+ Args:
262
+ project_name: Name of the project to pack, or None to pack all projects
263
+ """
157
264
 
158
- output_dir.mkdir(parents=True, exist_ok=True)
159
-
160
- # Use default patterns if not specified
161
- if include_patterns is None:
162
- include_patterns = DEFAULT_INCLUDE
163
- if exclude_patterns is None:
164
- exclude_patterns = DEFAULT_EXCLUDE
165
-
166
- # Copy files
167
- copied_files = 0
168
- copied_dirs = 0
169
-
170
- logger.info(f"Packing project '{project_name}' to {output_dir}")
171
-
172
- try:
173
- for item in project_path.iterdir():
174
- if item.is_file():
175
- # For files, check if should be included
176
- if should_include(item, include_patterns, exclude_patterns):
177
- dest_file = output_dir / item.name
178
- shutil.copy2(item, dest_file)
179
- logger.debug(f"Copied file: {item.name}")
180
- copied_files += 1
181
- elif item.is_dir() and not should_exclude(item, exclude_patterns):
182
- # Copy directory recursively with exclude patterns
183
- dest_dir = output_dir / item.name
184
- if dest_dir.exists():
185
- shutil.rmtree(dest_dir)
186
- shutil.copytree(
187
- item, dest_dir, ignore=shutil.ignore_patterns(*DEFAULT_EXCLUDE)
188
- )
189
- logger.debug(f"Copied directory: {item.name}")
190
- copied_dirs += 1
265
+ if not self.projects:
266
+ logger.error("No projects found in projects.json")
267
+ return
268
+
269
+ # Pack specific project
270
+ if project_name:
271
+ if self.pack_project(project_name):
272
+ logger.info(f"Packed project '{project_name}' successfully")
273
+ else:
274
+ logger.error(f"Failed to pack project '{project_name}'")
275
+ return
276
+
277
+ # Pack all projects
278
+ logger.info(f"Start packing, projects: {self.projects}")
279
+
280
+ if len(self.projects) == 1:
281
+ # Single project: process directly
282
+ name = next(iter(self.projects))
283
+ success_count = 1 if self.pack_project(name) else 0
284
+ else:
285
+ # Multiple projects: process concurrently
286
+ logger.info(f"Packing {len(self.projects)} projects concurrently...")
287
+ success_count = 0
288
+
289
+ with ThreadPoolExecutor(max_workers=None) as executor:
290
+ future_to_name = {
291
+ executor.submit(self.pack_project, name): name
292
+ for name in self.projects
293
+ }
294
+
295
+ for future in as_completed(future_to_name):
296
+ name = future_to_name[future]
297
+ try:
298
+ if future.result():
299
+ success_count += 1
300
+ except Exception as e:
301
+ logger.error(f"Project {name} packing failed: {e}")
191
302
 
192
303
  logger.info(
193
- f"Successfully packed {project_name}: {copied_files} files, {copied_dirs} directories"
304
+ f"Packed {success_count}/{len(self.projects)} projects successfully"
194
305
  )
195
- return True
196
306
 
197
- except Exception as e:
198
- logger.error(f"Error packing project {project_name}: {e}")
199
- return False
307
+ def list_projects(self) -> None:
308
+ """List all available projects."""
309
+ for name, project in self.projects.items():
310
+ logger.info(f"{name}: {project}")
200
311
 
201
312
 
202
- def create_parser() -> argparse.ArgumentParser:
313
+ def parse_args() -> argparse.Namespace:
203
314
  """Create and return an argument parser for the PySourcePack tool."""
204
315
  parser = argparse.ArgumentParser(
205
316
  description="PySourcePack - Source code packaging tool",
@@ -241,68 +352,36 @@ def create_parser() -> argparse.ArgumentParser:
241
352
  action="store_true",
242
353
  help="Enable debug mode",
243
354
  )
244
- return parser
355
+ return parser.parse_args()
245
356
 
246
357
 
247
358
  def main() -> None:
248
- from sfi.pyprojectparse.pyprojectparse import Solution
359
+ """Main entry point for pysourcepack."""
360
+ args = parse_args()
249
361
 
250
- parser = create_parser()
251
- args = parser.parse_args()
362
+ root_dir = Path(args.directory)
363
+ if not root_dir.exists():
364
+ logger.error(f"Root directory {root_dir} does not exist")
365
+ return
252
366
 
253
367
  if args.debug:
254
368
  logger.setLevel(logging.DEBUG)
255
369
 
256
- # Load projects file
257
- base_dir = Path(args.directory)
258
- project_json = base_dir / "projects.json"
259
- projects = Solution.from_directory(base_dir, recursive=True, update=True).projects
260
-
261
- if not project_json.exists():
262
- logger.error("Failed to create projects.json")
263
- return
370
+ # Create packer instance
371
+ include_patterns = set(args.include) if args.include else DEFAULT_INCLUDE
372
+ exclude_patterns = set(args.exclude) if args.exclude else DEFAULT_EXCLUDE
264
373
 
265
- if not projects:
266
- logger.error("No projects found in projects.json")
267
- return
374
+ packer = PySourcePacker(
375
+ root_dir=root_dir,
376
+ include_patterns=include_patterns,
377
+ exclude_patterns=exclude_patterns,
378
+ )
268
379
 
269
380
  if args.list:
270
- logger.info("Available projects:")
271
- for info in projects.values():
272
- logger.info(info)
273
- return
381
+ packer.list_projects()
274
382
 
275
- # Process include and exclude patterns
276
- include_patterns = set(args.include) if args.include else None
277
- exclude_patterns = set(args.exclude) if args.exclude else None
383
+ packer.run(project_name=args.project)
278
384
 
279
- logger.debug(
280
- f"Start packing, config: base_dir={base_dir}, project_name={args.project}, include_patterns={include_patterns}, exclude_patterns={exclude_patterns}"
281
- )
282
- # Pack specified project or all projects
283
- if args.project:
284
- if pack_project(
285
- base_dir=base_dir,
286
- projects=projects,
287
- project_name=args.project,
288
- include_patterns=include_patterns,
289
- exclude_patterns=exclude_patterns,
290
- ):
291
- logger.info(f"Packed project '{args.project}' successfully")
292
- else:
293
- logger.error(f"Failed to pack project '{args.project}'")
294
- return
295
385
 
296
- # Pack all projects
297
- logger.info("Packing all projects...")
298
- success_count = 0
299
- for project_name in projects:
300
- if pack_project(
301
- base_dir=base_dir,
302
- projects=projects,
303
- project_name=project_name,
304
- include_patterns=include_patterns,
305
- exclude_patterns=exclude_patterns,
306
- ):
307
- success_count += 1
308
- logger.info(f"Packed {success_count}/{len(projects)} projects successfully")
386
+ if __name__ == "__main__":
387
+ main()
@@ -85,7 +85,7 @@ plugin_path = str(qt_dir / "plugins" / "platforms")
85
85
  os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = plugin_path
86
86
 
87
87
 
88
- _WRONG_ANSWERS_FILE = Path.home() / ".sfi" / "wrong_answers.json"
88
+ _WRONG_ANSWERS_FILE = Path.home() / ".pysfi" / "wrong_answers.json"
89
89
 
90
90
 
91
91
  @dataclass
@@ -119,7 +119,7 @@ class ConfigManager:
119
119
  def __init__(self, config_file: Path | None = None):
120
120
  """Initialize configuration manager."""
121
121
  if config_file is None:
122
- config_dir = Path.home() / ".sfi"
122
+ config_dir = Path.home() / ".pysfi"
123
123
  config_dir.mkdir(exist_ok=True)
124
124
  config_file = config_dir / "quizbase_gui.json"
125
125
  self.config_file = config_file