python-package-folder 5.2.3__tar.gz → 5.3.0__tar.gz

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 (60) hide show
  1. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/PKG-INFO +1 -1
  2. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/coverage.svg +2 -2
  3. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/pyproject.toml +1 -1
  4. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/version_calculator.py +239 -1
  5. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/.copier-answers.yml +0 -0
  6. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  7. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  8. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/.cursor/rules/general.mdc +0 -0
  9. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/.cursor/rules/python.mdc +0 -0
  10. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/.github/workflows/ci.yml +0 -0
  11. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/.github/workflows/publish.yml +0 -0
  12. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/.gitignore +0 -0
  13. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/.vscode/settings.json +0 -0
  14. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/LICENSE +0 -0
  15. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/MANIFEST.in +0 -0
  16. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/Makefile +0 -0
  17. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/README.md +0 -0
  18. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/development.md +0 -0
  19. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/docs/DEVELOPMENT.md +0 -0
  20. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/docs/INSTALLATION.md +0 -0
  21. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/docs/PUBLISHING.md +0 -0
  22. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/docs/REFERENCE.md +0 -0
  23. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/docs/USAGE.md +0 -0
  24. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/docs/VERSION_RESOLUTION.md +0 -0
  25. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/installation.md +0 -0
  26. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/publishing.md +0 -0
  27. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/__init__.py +0 -0
  28. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/__main__.py +0 -0
  29. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/analyzer.py +0 -0
  30. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/finder.py +0 -0
  31. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/manager.py +0 -0
  32. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/publisher.py +0 -0
  33. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/py.typed +0 -0
  34. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/python_package_folder.py +0 -0
  35. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/subfolder_build.py +0 -0
  36. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/types.py +0 -0
  37. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/utils.py +0 -0
  38. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/src/python_package_folder/version.py +0 -0
  39. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/conftest.py +0 -0
  40. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/folder_structure/some_globals.py +0 -0
  41. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  42. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  43. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  44. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  45. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  46. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  47. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_build_with_external_deps.py +0 -0
  48. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_exclude_patterns.py +0 -0
  49. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_linting.py +0 -0
  50. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_preserve_directory_structure.py +0 -0
  51. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_publisher.py +0 -0
  52. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_shared_subdirectory_imports.py +0 -0
  53. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_spreadsheet_creation_imports.py +0 -0
  54. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_subfolder_build.py +0 -0
  55. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_third_party_dependencies.py +0 -0
  56. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_utils.py +0 -0
  57. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_version_calculator.py +0 -0
  58. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/test_version_manager.py +0 -0
  59. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/tests/tests.py +0 -0
  60. {python_package_folder-5.2.3 → python_package_folder-5.3.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 5.2.3
3
+ Version: 5.3.0
4
4
  Summary: Python package to automatically package and build a folder, fetching all relevant dependencies.
5
5
  Project-URL: Repository, https://github.com/alelom/python-package-folder
6
6
  Author-email: Alessio Lombardi <work@alelom.com>
@@ -14,7 +14,7 @@
14
14
  <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
15
15
  <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
16
16
  <text x="31.5" y="14">coverage</text>
17
- <text x="81" y="15" fill="#010101" fill-opacity=".3">67%</text>
18
- <text x="81" y="14">67%</text>
17
+ <text x="81" y="15" fill="#010101" fill-opacity=".3">66%</text>
18
+ <text x="81" y="14">66%</text>
19
19
  </g>
20
20
  </svg>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "5.2.3"
46
+ version = "5.3.0"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -13,6 +13,8 @@ from __future__ import annotations
13
13
  import logging
14
14
  import re
15
15
  import subprocess
16
+ import sys
17
+ import tempfile
16
18
  from html.parser import HTMLParser
17
19
  from pathlib import Path
18
20
 
@@ -185,6 +187,175 @@ class SimpleIndexParser(HTMLParser):
185
187
  return None
186
188
 
187
189
 
190
+ def _query_azure_artifacts_version_via_pip_index(
191
+ package_name: str,
192
+ repository_url: str,
193
+ username: str | None = None,
194
+ password: str | None = None,
195
+ ) -> str | None:
196
+ """
197
+ Query Azure Artifacts for latest version using 'pip index versions'.
198
+
199
+ This method uses pip's built-in index querying, which uses the same
200
+ authentication mechanism as pip install/publish.
201
+
202
+ Args:
203
+ package_name: Package name to query
204
+ repository_url: Azure Artifacts repository URL
205
+ username: Optional username for authentication
206
+ password: Optional password/token for authentication
207
+
208
+ Returns:
209
+ Latest version string or None if not found/unsupported
210
+ """
211
+ # Build pip index URL (remove /upload suffix if present)
212
+ index_url = repository_url.replace("/upload", "/simple")
213
+
214
+ logger.info(f"Querying Azure Artifacts via 'pip index versions' for '{package_name}'...")
215
+
216
+ # Build pip command
217
+ cmd = ["pip", "index", "versions", package_name, "--index-url", index_url]
218
+
219
+ # Add authentication if provided
220
+ # pip supports credentials in URL format: https://user:pass@host/path
221
+ if username and password:
222
+ auth_url = index_url.replace("https://", f"https://{username}:{password}@")
223
+ cmd[cmd.index("--index-url") + 1] = auth_url
224
+
225
+ try:
226
+ result = subprocess.run(
227
+ cmd,
228
+ capture_output=True,
229
+ text=True,
230
+ timeout=30,
231
+ )
232
+
233
+ if result.returncode == 0:
234
+ # Parse output: "Package 'data' versions: 0.1.0, 0.2.0, 0.3.0"
235
+ # Or: "data (0.1.0, 0.2.0, 0.3.0)"
236
+ match = re.search(r"versions?:\s*(.+)", result.stdout, re.IGNORECASE)
237
+ if not match:
238
+ # Try alternative format: "package-name (version1, version2, ...)"
239
+ match = re.search(rf"{re.escape(package_name)}\s*\(([^)]+)\)", result.stdout)
240
+
241
+ if match:
242
+ versions_str = match.group(1).strip()
243
+ # Split by comma and clean up
244
+ versions = [v.strip().strip("'\"") for v in versions_str.split(",")]
245
+ if versions:
246
+ # Sort versions to get the latest
247
+ try:
248
+ sorted_versions = sorted(versions, key=_parse_version_for_sort, reverse=True)
249
+ latest_version = sorted_versions[0]
250
+ logger.info(f"Found latest version via pip index: {latest_version}")
251
+ return latest_version
252
+ except Exception as e:
253
+ logger.warning(f"Error sorting versions from pip index: {e}. Using first version.")
254
+ return versions[-1] # Return last one as fallback
255
+
256
+ # Check if error indicates package doesn't exist
257
+ if "not found" in result.stderr.lower() or "no such package" in result.stderr.lower():
258
+ logger.info(f"Package '{package_name}' not found via pip index (first release)")
259
+ else:
260
+ logger.debug(f"pip index versions output: stdout={result.stdout}, stderr={result.stderr}")
261
+
262
+ return None
263
+ except subprocess.TimeoutExpired:
264
+ logger.warning(f"Timeout querying Azure Artifacts via pip index for '{package_name}'")
265
+ return None
266
+ except FileNotFoundError:
267
+ # pip command not found or pip index not available (pip < 21.2)
268
+ logger.debug("'pip index versions' not available, will try alternative method")
269
+ return None
270
+ except Exception as e:
271
+ logger.warning(f"Error querying Azure Artifacts via pip index for '{package_name}': {e}")
272
+ return None
273
+
274
+
275
+ def _query_azure_artifacts_version_via_pip_install(
276
+ package_name: str,
277
+ repository_url: str,
278
+ username: str | None = None,
279
+ password: str | None = None,
280
+ ) -> str | None:
281
+ """
282
+ Query Azure Artifacts by attempting to install the latest version
283
+ using 'pip install --dry-run', then extracting the version.
284
+
285
+ This is a fallback method when 'pip index versions' is not available.
286
+
287
+ Args:
288
+ package_name: Package name to query
289
+ repository_url: Azure Artifacts repository URL
290
+ username: Optional username for authentication
291
+ password: Optional password/token for authentication
292
+
293
+ Returns:
294
+ Latest version string or None if not found/unsupported
295
+ """
296
+ # Build index URL
297
+ index_url = repository_url.replace("/upload", "/simple")
298
+
299
+ logger.info(f"Querying Azure Artifacts via 'pip install --dry-run' for '{package_name}'...")
300
+
301
+ # Build pip command
302
+ cmd = [
303
+ sys.executable,
304
+ "-m",
305
+ "pip",
306
+ "install",
307
+ "--index-url",
308
+ index_url,
309
+ "--no-deps", # Don't install dependencies
310
+ "--dry-run", # Don't actually install
311
+ package_name,
312
+ ]
313
+
314
+ # Add authentication if provided
315
+ if username and password:
316
+ auth_url = index_url.replace("https://", f"https://{username}:{password}@")
317
+ cmd[cmd.index("--index-url") + 1] = auth_url
318
+
319
+ try:
320
+ result = subprocess.run(
321
+ cmd,
322
+ capture_output=True,
323
+ text=True,
324
+ timeout=60,
325
+ )
326
+
327
+ if result.returncode == 0:
328
+ # Parse output to find version
329
+ # pip shows: "Would install data-0.3.0" or "Collecting data==0.3.0"
330
+ # Try multiple patterns
331
+ patterns = [
332
+ rf"Would install\s+{re.escape(package_name)}-([\d.]+(?:[a-zA-Z0-9]+)?)",
333
+ rf"Collecting\s+{re.escape(package_name)}==([\d.]+(?:[a-zA-Z0-9]+)?)",
334
+ rf"Downloading\s+{re.escape(package_name)}-([\d.]+(?:[a-zA-Z0-9]+)?)",
335
+ ]
336
+
337
+ for pattern in patterns:
338
+ match = re.search(pattern, result.stdout, re.IGNORECASE)
339
+ if match:
340
+ version = match.group(1)
341
+ logger.info(f"Found version via pip install --dry-run: {version}")
342
+ return version
343
+
344
+ # Check if error indicates package doesn't exist
345
+ if "not found" in result.stderr.lower() or "no matching distribution" in result.stderr.lower():
346
+ logger.info(f"Package '{package_name}' not found via pip install (first release)")
347
+ else:
348
+ logger.debug(f"pip install --dry-run output: stdout={result.stdout[:500]}, stderr={result.stderr[:500]}")
349
+
350
+ return None
351
+ except subprocess.TimeoutExpired:
352
+ logger.warning(f"Timeout querying Azure Artifacts via pip install for '{package_name}'")
353
+ return None
354
+ except Exception as e:
355
+ logger.warning(f"Error querying Azure Artifacts via pip install for '{package_name}': {e}")
356
+ return None
357
+
358
+
188
359
  def _query_azure_artifacts_version(
189
360
  package_name: str,
190
361
  repository_url: str,
@@ -194,6 +365,50 @@ def _query_azure_artifacts_version(
194
365
  """
195
366
  Query Azure Artifacts for the latest version.
196
367
 
368
+ Tries multiple methods in order:
369
+ 1. pip index versions (fastest, uses same auth as pip install)
370
+ 2. pip install --dry-run (fallback if pip index not available)
371
+ 3. HTML parsing of simple index (last resort)
372
+
373
+ Args:
374
+ package_name: Package name to query
375
+ repository_url: Azure Artifacts repository URL
376
+ username: Optional username for authentication
377
+ password: Optional password/token for authentication
378
+
379
+ Returns:
380
+ Latest version string or None if not found/unsupported
381
+ """
382
+ # Method 1: Try pip index versions first (fastest, uses same auth as publishing)
383
+ version = _query_azure_artifacts_version_via_pip_index(
384
+ package_name, repository_url, username, password
385
+ )
386
+ if version:
387
+ return version
388
+
389
+ # Method 2: Fallback to pip install --dry-run
390
+ version = _query_azure_artifacts_version_via_pip_install(
391
+ package_name, repository_url, username, password
392
+ )
393
+ if version:
394
+ return version
395
+
396
+ # Method 3: Last resort - HTML parsing (original method)
397
+ logger.info(f"Falling back to HTML parsing for '{package_name}'...")
398
+ return _query_azure_artifacts_version_via_html(
399
+ package_name, repository_url, username, password
400
+ )
401
+
402
+
403
+ def _query_azure_artifacts_version_via_html(
404
+ package_name: str,
405
+ repository_url: str,
406
+ username: str | None = None,
407
+ password: str | None = None,
408
+ ) -> str | None:
409
+ """
410
+ Query Azure Artifacts for the latest version via HTML parsing.
411
+
197
412
  Azure Artifacts uses a simple index format (HTML) following PEP 503.
198
413
  Parses the HTML to extract version numbers from package filenames.
199
414
 
@@ -605,6 +820,7 @@ def parse_commit_for_bump(commit_message: str) -> str | None:
605
820
  def calculate_next_version(
606
821
  baseline_version: str,
607
822
  commits: list[str],
823
+ auto_bump_minor: bool = False,
608
824
  ) -> str | None:
609
825
  """
610
826
  Calculate next version from baseline and commits.
@@ -612,11 +828,21 @@ def calculate_next_version(
612
828
  Args:
613
829
  baseline_version: Current baseline version (e.g., "1.2.3")
614
830
  commits: List of commit messages since baseline
831
+ auto_bump_minor: If True and no conventional commits found, bump minor version
615
832
 
616
833
  Returns:
617
834
  Next version string or None if no changes require a version bump
618
835
  """
619
836
  if not commits:
837
+ # If no commits and auto_bump_minor is enabled, bump minor version
838
+ if auto_bump_minor:
839
+ try:
840
+ parts = baseline_version.split(".")
841
+ major = int(parts[0])
842
+ minor = int(parts[1]) if len(parts) > 1 else 0
843
+ return f"{major}.{minor + 1}.0"
844
+ except (ValueError, IndexError):
845
+ return None
620
846
  return None
621
847
 
622
848
  # Parse each commit to determine bump type
@@ -627,6 +853,16 @@ def calculate_next_version(
627
853
  bump_types.append(bump)
628
854
 
629
855
  if not bump_types:
856
+ # No conventional commits found
857
+ # If auto_bump_minor is enabled, bump minor version
858
+ if auto_bump_minor:
859
+ try:
860
+ parts = baseline_version.split(".")
861
+ major = int(parts[0])
862
+ minor = int(parts[1]) if len(parts) > 1 else 0
863
+ return f"{major}.{minor + 1}.0"
864
+ except (ValueError, IndexError):
865
+ return None
630
866
  return None
631
867
 
632
868
  # Determine highest bump (major > minor > patch)
@@ -760,7 +996,9 @@ def resolve_version(
760
996
 
761
997
  # Step 5: Calculate next version
762
998
  logger.info(f"Calculating next version from baseline {baseline_version} and {len(commits)} commits")
763
- next_version = calculate_next_version(baseline_version, commits)
999
+ # Auto-bump minor version if no conventional commits found (but baseline exists)
1000
+ auto_bump_minor = baseline_version is not None and baseline_version != "0.0.0"
1001
+ next_version = calculate_next_version(baseline_version, commits, auto_bump_minor=auto_bump_minor)
764
1002
 
765
1003
  if next_version:
766
1004
  logger.info(f"Calculated next version: {next_version}")