pysfi 0.1.7__py3-none-any.whl → 0.1.10__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.
@@ -0,0 +1,369 @@
1
+ """Source code packaging tool for projects defined in projects.json."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import logging
8
+ import shutil
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ cwd = Path.cwd()
15
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Default directories and files to exclude during packaging
19
+ DEFAULT_EXCLUDE = {
20
+ "__pycache__",
21
+ "*.pyc",
22
+ "*.pyo",
23
+ ".pytest_cache",
24
+ ".benchmarks",
25
+ "tests",
26
+ ".git",
27
+ ".gitignore",
28
+ "dist",
29
+ "build",
30
+ "*.egg-info",
31
+ "node_modules",
32
+ ".idea",
33
+ "*.log",
34
+ }
35
+
36
+ # Default files to include in packaging
37
+ DEFAULT_INCLUDE = {
38
+ "*.py",
39
+ "README.md",
40
+ "LICENSE",
41
+ "pyproject.toml",
42
+ }
43
+
44
+
45
+ def load_projects(projects_file: Path) -> dict:
46
+ """Load projects from JSON file.
47
+
48
+ Args:
49
+ projects_file: Path to projects.json file
50
+
51
+ Returns:
52
+ Dictionary containing project information
53
+ """
54
+ if not projects_file.exists():
55
+ logger.error(f"Projects file {projects_file} does not exist")
56
+ return {}
57
+
58
+ try:
59
+ with projects_file.open("r", encoding="utf-8") as f:
60
+ return json.load(f)
61
+ except Exception as e:
62
+ logger.error(f"Error loading projects from {projects_file}: {e}")
63
+ return {}
64
+
65
+
66
+ def list_projects(projects: dict) -> None:
67
+ """List all available projects.
68
+
69
+ Args:
70
+ projects: Dictionary containing project information
71
+ """
72
+ if not projects:
73
+ logger.warning("No projects found")
74
+ return
75
+
76
+ logger.info("Available projects:")
77
+ for name, info in sorted(projects.items()):
78
+ version = info.get("version", "N/A")
79
+ description = info.get("description", "No description")
80
+ logger.info(f" - {name} (v{version}): {description}")
81
+
82
+
83
+ def should_exclude(path: Path, exclude_patterns: set[str]) -> bool:
84
+ """Check if a path should be excluded based on patterns.
85
+
86
+ Args:
87
+ path: Path to check
88
+ exclude_patterns: Set of patterns to exclude
89
+
90
+ Returns:
91
+ True if path should be excluded, False otherwise
92
+ """
93
+ for pattern in exclude_patterns:
94
+ if pattern.startswith("*"):
95
+ # Wildcard pattern (e.g., "*.pyc")
96
+ if path.match(pattern):
97
+ return True
98
+ else:
99
+ # Exact name pattern
100
+ if path.name == pattern:
101
+ return True
102
+ if path.parts:
103
+ # Check if any parent directory matches
104
+ for part in path.parts:
105
+ if part == pattern:
106
+ return True
107
+ return False
108
+
109
+
110
+ def should_include(path: Path, include_patterns: set[str], exclude_patterns: set[str]) -> bool:
111
+ """Check if a path should be included based on include and exclude patterns.
112
+
113
+ Args:
114
+ path: Path to check
115
+ include_patterns: Set of patterns to include
116
+ exclude_patterns: Set of patterns to exclude
117
+
118
+ Returns:
119
+ True if path should be included, False otherwise
120
+ """
121
+ # Check if should be excluded first
122
+ if should_exclude(path, exclude_patterns):
123
+ return False
124
+
125
+ # If no include patterns, include everything not excluded
126
+ if not include_patterns:
127
+ return True
128
+
129
+ # Check if matches any include pattern
130
+ for pattern in include_patterns:
131
+ if pattern.startswith("*"):
132
+ # Wildcard pattern (e.g., "*.py")
133
+ if path.match(pattern):
134
+ return True
135
+ else:
136
+ # Exact name pattern
137
+ if path.name == pattern:
138
+ return True
139
+ if path.is_dir() and path.name == pattern:
140
+ # Include directory and all its contents
141
+ return True
142
+
143
+ return False
144
+
145
+
146
+ def pack_project(
147
+ project_name: str,
148
+ projects: dict,
149
+ base_dir: Path,
150
+ output_dir: Path | None = None,
151
+ include_patterns: set[str] | None = None,
152
+ exclude_patterns: set[str] | None = None,
153
+ ) -> bool:
154
+ """Pack project source code and resources to dist/src directory.
155
+
156
+ Args:
157
+ project_name: Name of the project to pack
158
+ projects: Dictionary containing project information
159
+ base_dir: Base directory containing project folders
160
+ output_dir: Output directory (default: project's dist/src)
161
+ include_patterns: File patterns to include (default: DEFAULT_INCLUDE)
162
+ exclude_patterns: File patterns to exclude (default: DEFAULT_EXCLUDE)
163
+
164
+ Returns:
165
+ True if packing succeeded, False otherwise
166
+ """
167
+ if project_name not in projects:
168
+ logger.error(f"Project '{project_name}' not found in projects.json")
169
+ return False
170
+
171
+ project_path = base_dir / project_name
172
+
173
+ # If project directory doesn't exist in base_dir, try to search for it
174
+ if not project_path.exists():
175
+ logger.debug(f"Project directory {project_path} does not exist in base_dir")
176
+ # Search in parent directories
177
+ search_dirs = [base_dir, base_dir.parent, base_dir.parent.parent]
178
+ for search_dir in search_dirs:
179
+ candidate = search_dir / project_name
180
+ if candidate.exists() and candidate.is_dir():
181
+ logger.info(f"Found project at {candidate}")
182
+ project_path = candidate
183
+ break
184
+
185
+ if not project_path.exists():
186
+ logger.error(f"Project directory for '{project_name}' not found")
187
+ logger.debug(f"Searched in: {base_dir}, {base_dir.parent}, {base_dir.parent.parent}")
188
+ return False
189
+
190
+ if not project_path.is_dir():
191
+ logger.error(f"{project_path} is not a directory")
192
+ return False
193
+
194
+ # Determine output directory
195
+ output_dir = project_path / "dist" / "src" if output_dir is None else Path(output_dir) / project_name
196
+
197
+ # Create output directory
198
+ try:
199
+ output_dir.mkdir(parents=True, exist_ok=True)
200
+ except Exception as e:
201
+ logger.error(f"Error creating output directory {output_dir}: {e}")
202
+ return False
203
+
204
+ # Use default patterns if not specified
205
+ if include_patterns is None:
206
+ include_patterns = DEFAULT_INCLUDE
207
+ if exclude_patterns is None:
208
+ exclude_patterns = DEFAULT_EXCLUDE
209
+
210
+ # Copy files
211
+ copied_files = 0
212
+ copied_dirs = 0
213
+
214
+ logger.info(f"Packing project '{project_name}' to {output_dir}")
215
+
216
+ try:
217
+ for item in project_path.iterdir():
218
+ if item.is_file():
219
+ # For files, check if should be included
220
+ if should_include(item, include_patterns, exclude_patterns):
221
+ dest_file = output_dir / item.name
222
+ shutil.copy2(item, dest_file)
223
+ logger.debug(f"Copied file: {item.name}")
224
+ copied_files += 1
225
+ elif item.is_dir():
226
+ # For directories, exclude only if in exclude patterns
227
+ if not should_exclude(item, exclude_patterns):
228
+ # Copy directory recursively with exclude patterns
229
+ dest_dir = output_dir / item.name
230
+ if dest_dir.exists():
231
+ shutil.rmtree(dest_dir)
232
+ shutil.copytree(item, dest_dir, ignore=shutil.ignore_patterns(*DEFAULT_EXCLUDE))
233
+ logger.debug(f"Copied directory: {item.name}")
234
+ copied_dirs += 1
235
+
236
+ logger.info(f"Successfully packed {project_name}: {copied_files} files, {copied_dirs} directories")
237
+ return True
238
+
239
+ except Exception as e:
240
+ logger.error(f"Error packing project {project_name}: {e}")
241
+ return False
242
+
243
+
244
+ def main():
245
+ parser = argparse.ArgumentParser(
246
+ description="PySourcePack - Source code packaging tool",
247
+ epilog="Pack source code and resources for projects defined in projects.json",
248
+ )
249
+ parser.add_argument(
250
+ "-i",
251
+ "--input",
252
+ default="projects.json",
253
+ help="Input projects.json file path (default: projects.json)",
254
+ )
255
+ parser.add_argument(
256
+ "-d",
257
+ "--directory",
258
+ default=str(cwd),
259
+ help="Base directory containing project folders (default: current directory)",
260
+ )
261
+ parser.add_argument(
262
+ "-o",
263
+ "--output-dir",
264
+ default=None,
265
+ help="Output directory for packed files (default: <project>/dist/src)",
266
+ )
267
+ parser.add_argument(
268
+ "-p",
269
+ "--project",
270
+ default=None,
271
+ help="Project name to pack (default: pack all projects)",
272
+ )
273
+ parser.add_argument(
274
+ "-l",
275
+ "--list",
276
+ action="store_true",
277
+ help="List all available projects",
278
+ )
279
+ parser.add_argument(
280
+ "--include",
281
+ nargs="*",
282
+ default=None,
283
+ help="File patterns to include (default: *.py, README.md, pyproject.toml)",
284
+ )
285
+ parser.add_argument(
286
+ "--exclude",
287
+ nargs="*",
288
+ default=None,
289
+ help="File patterns to exclude (default: __pycache__, *.pyc, tests, .benchmarks, etc.)",
290
+ )
291
+ parser.add_argument(
292
+ "--debug",
293
+ action="store_true",
294
+ help="Enable debug mode",
295
+ )
296
+
297
+ args = parser.parse_args()
298
+
299
+ if args.debug:
300
+ logger.setLevel(logging.DEBUG)
301
+
302
+ # Load projects file
303
+ projects_file = Path(args.input)
304
+ if not projects_file.is_absolute():
305
+ # Relative to base directory
306
+ base_dir = Path(args.directory)
307
+ projects_file = base_dir / args.input
308
+ else:
309
+ base_dir = Path(args.directory)
310
+
311
+ # If projects_file is relative and not found, try to find it in current directory
312
+ if not projects_file.exists():
313
+ logger.warning(f"Projects file {projects_file} not found, trying current directory...")
314
+ alt_file = Path.cwd() / args.input
315
+ if alt_file.exists():
316
+ projects_file = alt_file
317
+ base_dir = alt_file.parent
318
+ logger.info(f"Found projects file at {projects_file}")
319
+
320
+ # Check if user explicitly specified the -d parameter
321
+ explicit_dir = any(arg in ["-d", "--directory"] for arg in sys.argv)
322
+ if not explicit_dir and projects_file.exists():
323
+ # If -d was not explicitly specified, set base_dir to projects_file's parent
324
+ base_dir = projects_file.parent
325
+ logger.debug(f"Auto-set base_dir to {base_dir} (parent of projects.json)")
326
+
327
+ projects = load_projects(projects_file)
328
+ if not projects:
329
+ return
330
+
331
+ # List projects if requested
332
+ if args.list:
333
+ list_projects(projects)
334
+ return
335
+
336
+ # Process include and exclude patterns
337
+ include_patterns = set(args.include) if args.include else None
338
+ exclude_patterns = set(args.exclude) if args.exclude else None
339
+
340
+ # Pack specified project or all projects
341
+ if args.project:
342
+ success = pack_project(
343
+ args.project,
344
+ projects,
345
+ base_dir,
346
+ output_dir=Path(args.output_dir) if args.output_dir else None,
347
+ include_patterns=include_patterns,
348
+ exclude_patterns=exclude_patterns,
349
+ )
350
+ if success:
351
+ logger.info(f"Packed project '{args.project}' successfully")
352
+ else:
353
+ logger.error(f"Failed to pack project '{args.project}'")
354
+ else:
355
+ # Pack all projects
356
+ logger.info("Packing all projects...")
357
+ success_count = 0
358
+ for project_name in projects:
359
+ if pack_project(
360
+ project_name,
361
+ projects,
362
+ base_dir,
363
+ output_dir=Path(args.output_dir) if args.output_dir else None,
364
+ include_patterns=include_patterns,
365
+ exclude_patterns=exclude_patterns,
366
+ ):
367
+ success_count += 1
368
+
369
+ logger.info(f"Packed {success_count}/{len(projects)} projects successfully")
File without changes