python-package-folder 5.2.1__tar.gz → 5.2.3__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.1 → python_package_folder-5.2.3}/PKG-INFO +1 -1
  2. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/coverage.svg +2 -2
  3. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/pyproject.toml +1 -1
  4. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/manager.py +8 -0
  5. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/publisher.py +18 -6
  6. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/python_package_folder.py +27 -0
  7. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/subfolder_build.py +9 -0
  8. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/version_calculator.py +44 -8
  9. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_version_calculator.py +33 -0
  10. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/.copier-answers.yml +0 -0
  11. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  12. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  13. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/.cursor/rules/general.mdc +0 -0
  14. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/.cursor/rules/python.mdc +0 -0
  15. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/.github/workflows/ci.yml +0 -0
  16. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/.github/workflows/publish.yml +0 -0
  17. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/.gitignore +0 -0
  18. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/.vscode/settings.json +0 -0
  19. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/LICENSE +0 -0
  20. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/MANIFEST.in +0 -0
  21. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/Makefile +0 -0
  22. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/README.md +0 -0
  23. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/development.md +0 -0
  24. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/docs/DEVELOPMENT.md +0 -0
  25. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/docs/INSTALLATION.md +0 -0
  26. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/docs/PUBLISHING.md +0 -0
  27. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/docs/REFERENCE.md +0 -0
  28. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/docs/USAGE.md +0 -0
  29. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/docs/VERSION_RESOLUTION.md +0 -0
  30. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/installation.md +0 -0
  31. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/publishing.md +0 -0
  32. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/__init__.py +0 -0
  33. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/__main__.py +0 -0
  34. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/analyzer.py +0 -0
  35. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/finder.py +0 -0
  36. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/py.typed +0 -0
  37. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/types.py +0 -0
  38. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/utils.py +0 -0
  39. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/src/python_package_folder/version.py +0 -0
  40. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/conftest.py +0 -0
  41. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/folder_structure/some_globals.py +0 -0
  42. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  43. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  44. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  45. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  46. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  47. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  48. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_build_with_external_deps.py +0 -0
  49. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_exclude_patterns.py +0 -0
  50. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_linting.py +0 -0
  51. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_preserve_directory_structure.py +0 -0
  52. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_publisher.py +0 -0
  53. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_shared_subdirectory_imports.py +0 -0
  54. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_spreadsheet_creation_imports.py +0 -0
  55. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_subfolder_build.py +0 -0
  56. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_third_party_dependencies.py +0 -0
  57. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_utils.py +0 -0
  58. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/test_version_manager.py +0 -0
  59. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/tests/tests.py +0 -0
  60. {python_package_folder-5.2.1 → python_package_folder-5.2.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 5.2.1
3
+ Version: 5.2.3
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">68%</text>
18
- <text x="81" y="14">68%</text>
17
+ <text x="81" y="15" fill="#010101" fill-opacity=".3">67%</text>
18
+ <text x="81" y="14">67%</text>
19
19
  </g>
20
20
  </svg>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "5.2.1"
46
+ version = "5.2.3"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -1227,6 +1227,14 @@ class BuildManager:
1227
1227
  publish_package_name = (
1228
1228
  self.src_dir.name.replace("_", "-").replace(" ", "-").lower().strip("-")
1229
1229
  )
1230
+
1231
+ # Log the package name being used for publishing
1232
+ import logging
1233
+ logger = logging.getLogger(__name__)
1234
+ logger.info(
1235
+ f"Publishing package with name: '{publish_package_name}' "
1236
+ f"(captured: {captured_package_name}, param: {package_name}, src_dir: '{self.src_dir.name}')"
1237
+ )
1230
1238
  else:
1231
1239
  # For regular builds, get package name from pyproject.toml
1232
1240
  try:
@@ -346,12 +346,24 @@ class Publisher:
346
346
  # Provide helpful hints based on common errors
347
347
  if e.returncode == 1:
348
348
  if e.stderr and ("already exists" in e.stderr.lower() or "409" in e.stderr or "conflict" in e.stderr.lower()):
349
- print(
350
- "\nHint: This version may already exist on the repository. "
351
- "Use --skip-existing to skip files that already exist, "
352
- "or publish a new version.",
353
- file=sys.stderr,
354
- )
349
+ if self.repository == Repository.AZURE:
350
+ print(
351
+ "\nHint for Azure Artifacts: If the version query returned 404 (not found) "
352
+ "with authentication but the upload returns 409 Conflict, this could indicate:\n"
353
+ " - Different authentication requirements between simple index (read) and upload (write) endpoints\n"
354
+ " - The simple index endpoint may require different permissions or authentication method\n"
355
+ " - The package already exists with this version\n"
356
+ " - Use --skip-existing to skip files that already exist\n"
357
+ " - Check if your token has 'Packaging (read)' scope in addition to 'Packaging (read & write)'",
358
+ file=sys.stderr,
359
+ )
360
+ else:
361
+ print(
362
+ "\nHint: This version may already exist on the repository. "
363
+ "Use --skip-existing to skip files that already exist, "
364
+ "or publish a new version.",
365
+ file=sys.stderr,
366
+ )
355
367
  elif e.stderr and ("401" in e.stderr or "unauthorized" in e.stderr.lower()):
356
368
  print(
357
369
  "\nHint: Authentication failed. Check your credentials.",
@@ -186,6 +186,21 @@ def main() -> int:
186
186
  repository = args.publish if args.publish else None
187
187
  repository_url = args.repository_url if args.publish else None
188
188
 
189
+ # Get credentials for authenticated registry queries (especially Azure Artifacts)
190
+ # Try to get them from args or environment variables
191
+ query_username = args.username
192
+ query_password = args.password
193
+
194
+ # If not provided via args, check environment variables (for Azure Artifacts)
195
+ if repository == "azure" and not query_username:
196
+ query_username = os.getenv("TWINE_USERNAME") or os.getenv("PYPI_USERNAME")
197
+ if repository == "azure" and not query_password:
198
+ query_password = (
199
+ os.getenv("TWINE_PASSWORD")
200
+ or os.getenv("PYPI_PASSWORD")
201
+ or os.getenv("AZURE_ARTIFACTS_TOKEN")
202
+ )
203
+
189
204
  if is_subfolder:
190
205
  # Workflow 1: subfolder build
191
206
  # src_dir is guaranteed to be relative to project_root due to is_subfolder check
@@ -193,12 +208,22 @@ def main() -> int:
193
208
  " ", "-"
194
209
  ).lower().strip("-")
195
210
  subfolder_rel_path = src_dir.relative_to(project_root)
211
+
212
+ # Log the package name being used for version query
213
+ logger = logging.getLogger(__name__)
214
+ logger.info(
215
+ f"Querying registry for package name: '{package_name}' "
216
+ f"(derived from src_dir: '{src_dir.name}', args.package_name: {args.package_name})"
217
+ )
218
+
196
219
  resolved_version, error_details = resolve_version(
197
220
  project_root,
198
221
  package_name=package_name,
199
222
  subfolder_path=subfolder_rel_path,
200
223
  repository=repository,
201
224
  repository_url=repository_url,
225
+ username=query_username,
226
+ password=query_password,
202
227
  )
203
228
  else:
204
229
  # Workflow 2: main package
@@ -221,6 +246,8 @@ def main() -> int:
221
246
  subfolder_path=None,
222
247
  repository=repository,
223
248
  repository_url=repository_url,
249
+ username=query_username,
250
+ password=query_password,
224
251
  )
225
252
 
226
253
  if resolved_version:
@@ -402,6 +402,10 @@ class SubfolderBuildConfig:
402
402
  # Modify using parsed data
403
403
  if "project" in data:
404
404
  data["project"]["name"] = self.package_name
405
+ # Log the package name being set
406
+ import logging
407
+ logger = logging.getLogger(__name__)
408
+ logger.info(f"Setting package name in temporary pyproject.toml: '{self.package_name}'")
405
409
  if "version" in data["project"]:
406
410
  data["project"]["version"] = self.version
407
411
  elif "dynamic" in data["project"]:
@@ -475,6 +479,11 @@ class SubfolderBuildConfig:
475
479
  if not package_dirs:
476
480
  package_dirs = []
477
481
 
482
+ # Log the package name being set via string manipulation
483
+ import logging
484
+ logger = logging.getLogger(__name__)
485
+ logger.info(f"Setting package name in temporary pyproject.toml (string manipulation): '{self.package_name}'")
486
+
478
487
  for _i, line in enumerate(lines):
479
488
  # Skip build-system section - we'll add our own for subfolder builds
480
489
  if line.strip().startswith("[build-system]"):
@@ -25,6 +25,8 @@ def query_registry_version(
25
25
  package_name: str,
26
26
  repository: str,
27
27
  repository_url: str | None = None,
28
+ username: str | None = None,
29
+ password: str | None = None,
28
30
  ) -> str | None:
29
31
  """
30
32
  Query package registry for the latest published version.
@@ -33,6 +35,8 @@ def query_registry_version(
33
35
  package_name: Package name to query
34
36
  repository: Repository type ('pypi', 'testpypi', or 'azure')
35
37
  repository_url: Repository URL (required for Azure Artifacts)
38
+ username: Optional username for authenticated queries (Azure Artifacts)
39
+ password: Optional password/token for authenticated queries (Azure Artifacts)
36
40
 
37
41
  Returns:
38
42
  Latest version string or None if not found/unsupported
@@ -54,7 +58,7 @@ def query_registry_version(
54
58
  logger.warning("Azure Artifacts repository URL not provided")
55
59
  return None
56
60
  logger.info(f"Querying Azure Artifacts for package '{package_name}' at {repository_url}")
57
- version = _query_azure_artifacts_version(package_name, repository_url)
61
+ version = _query_azure_artifacts_version(package_name, repository_url, username, password)
58
62
  if version:
59
63
  logger.info(f"Found version {version} on Azure Artifacts")
60
64
  else:
@@ -184,6 +188,8 @@ class SimpleIndexParser(HTMLParser):
184
188
  def _query_azure_artifacts_version(
185
189
  package_name: str,
186
190
  repository_url: str,
191
+ username: str | None = None,
192
+ password: str | None = None,
187
193
  ) -> str | None:
188
194
  """
189
195
  Query Azure Artifacts for the latest version.
@@ -194,6 +200,8 @@ def _query_azure_artifacts_version(
194
200
  Args:
195
201
  package_name: Package name to query
196
202
  repository_url: Azure Artifacts repository URL
203
+ username: Optional username for authentication
204
+ password: Optional password/token for authentication
197
205
 
198
206
  Returns:
199
207
  Latest version string or None if not found/unsupported
@@ -212,8 +220,15 @@ def _query_azure_artifacts_version(
212
220
  return None
213
221
 
214
222
  try:
215
- logger.info(f"Fetching Azure Artifacts simple index for '{package_name}'...")
216
- response = requests.get(simple_index_url, timeout=10)
223
+ # Prepare authentication if credentials are provided
224
+ auth = None
225
+ if username and password:
226
+ auth = (username, password)
227
+ logger.info(f"Fetching Azure Artifacts simple index for '{package_name}' with authentication...")
228
+ else:
229
+ logger.info(f"Fetching Azure Artifacts simple index for '{package_name}' (no authentication)...")
230
+
231
+ response = requests.get(simple_index_url, auth=auth, timeout=10)
217
232
  logger.info(f"Azure Artifacts response: status={response.status_code}, content_length={len(response.text)} bytes")
218
233
 
219
234
  if response.status_code == 401:
@@ -231,10 +246,27 @@ def _query_azure_artifacts_version(
231
246
  )
232
247
  return None
233
248
  elif response.status_code == 404:
234
- logger.info(
235
- f"Package '{package_name}' not found on Azure Artifacts (404) - this appears to be the first release. "
236
- f"URL: {simple_index_url}"
237
- )
249
+ if auth:
250
+ # If we're using authentication and still get 404, it could be various issues
251
+ logger.warning(
252
+ f"Package '{package_name}' not found on Azure Artifacts (404) with authentication. "
253
+ f"This could indicate:\n"
254
+ f" (1) Different authentication requirements between simple index (read) and upload (write) endpoints\n"
255
+ f" (2) The simple index endpoint may require different permissions or authentication method\n"
256
+ f" (3) This is the first release (package doesn't exist yet)\n"
257
+ f" (4) Package name mismatch between query and publish\n"
258
+ f"URL: {simple_index_url}\n"
259
+ f"Note: If publishing succeeds but querying fails with 404, check:\n"
260
+ f" - Whether the simple index endpoint requires different authentication\n"
261
+ f" - Whether your token has 'Packaging (read)' scope in addition to 'Packaging (read & write)'\n"
262
+ f" - Whether the package name used for querying matches the published package name"
263
+ )
264
+ else:
265
+ logger.info(
266
+ f"Package '{package_name}' not found on Azure Artifacts (404) - this appears to be the first release. "
267
+ f"Note: If authentication is required, provide credentials via --username/--password or environment variables. "
268
+ f"URL: {simple_index_url}"
269
+ )
238
270
  return None
239
271
  elif response.status_code != 200:
240
272
  logger.warning(
@@ -628,6 +660,8 @@ def resolve_version(
628
660
  subfolder_path: Path | None = None,
629
661
  repository: str | None = None,
630
662
  repository_url: str | None = None,
663
+ username: str | None = None,
664
+ password: str | None = None,
631
665
  ) -> tuple[str | None, str | None]:
632
666
  """
633
667
  Resolve the next version using conventional commits.
@@ -640,6 +674,8 @@ def resolve_version(
640
674
  subfolder_path: Optional path to subfolder (relative to project_root)
641
675
  repository: Optional target repository ('pypi', 'testpypi', or 'azure')
642
676
  repository_url: Optional repository URL (required for Azure Artifacts)
677
+ username: Optional username for authenticated registry queries (Azure Artifacts)
678
+ password: Optional password/token for authenticated registry queries (Azure Artifacts)
643
679
 
644
680
  Returns:
645
681
  Tuple of (version string if a release is determined, error message if any)
@@ -651,7 +687,7 @@ def resolve_version(
651
687
  baseline_version = None
652
688
  if repository and package_name:
653
689
  logger.info(f"Attempting to query {repository} for baseline version of '{package_name}'")
654
- baseline_version = query_registry_version(package_name, repository, repository_url)
690
+ baseline_version = query_registry_version(package_name, repository, repository_url, username, password)
655
691
 
656
692
  # Step 2: Fallback to git tags if registry query failed
657
693
  if not baseline_version:
@@ -151,6 +151,39 @@ class TestQueryRegistryVersion:
151
151
  # Should return None when no versions found in HTML
152
152
  assert version is None
153
153
 
154
+ @patch("python_package_folder.version_calculator.requests.get")
155
+ def test_query_azure_artifacts_version_with_auth(self, mock_get: MagicMock) -> None:
156
+ """Test querying Azure Artifacts with authentication."""
157
+ mock_response = Mock()
158
+ mock_response.status_code = 200
159
+ mock_response.text = """<!DOCTYPE html>
160
+ <html>
161
+ <head>
162
+ <title>Links for test-package</title>
163
+ </head>
164
+ <body>
165
+ <h1>Links for test-package</h1>
166
+ <a href="test-package-1.0.0-py3-none-any.whl">test-package-1.0.0-py3-none-any.whl</a>
167
+ <a href="test-package-1.1.0-py3-none-any.whl">test-package-1.1.0-py3-none-any.whl</a>
168
+ </body>
169
+ </html>"""
170
+ mock_get.return_value = mock_response
171
+
172
+ version = query_registry_version(
173
+ "test-package",
174
+ "azure",
175
+ repository_url="https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/pypi/upload",
176
+ username="testuser",
177
+ password="testtoken",
178
+ )
179
+ # Should parse HTML and return the latest version
180
+ assert version == "1.1.0"
181
+
182
+ # Verify authentication was used
183
+ mock_get.assert_called_once()
184
+ call_args = mock_get.call_args
185
+ assert call_args[1]["auth"] == ("testuser", "testtoken")
186
+
154
187
  @patch("python_package_folder.version_calculator.requests.get")
155
188
  def test_query_registry_version_error_handling(self, mock_get: MagicMock) -> None:
156
189
  """Test that registry query errors are handled gracefully."""