python-package-folder 5.2.0__tar.gz → 5.2.2__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.0 → python_package_folder-5.2.2}/PKG-INFO +1 -1
  2. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/coverage.svg +2 -2
  3. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/pyproject.toml +1 -1
  4. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/publisher.py +50 -2
  5. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/python_package_folder.py +19 -0
  6. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/version_calculator.py +93 -19
  7. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_version_calculator.py +33 -0
  8. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/.copier-answers.yml +0 -0
  9. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  10. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  11. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/.cursor/rules/general.mdc +0 -0
  12. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/.cursor/rules/python.mdc +0 -0
  13. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/.github/workflows/ci.yml +0 -0
  14. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/.github/workflows/publish.yml +0 -0
  15. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/.gitignore +0 -0
  16. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/.vscode/settings.json +0 -0
  17. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/LICENSE +0 -0
  18. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/MANIFEST.in +0 -0
  19. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/Makefile +0 -0
  20. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/README.md +0 -0
  21. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/development.md +0 -0
  22. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/docs/DEVELOPMENT.md +0 -0
  23. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/docs/INSTALLATION.md +0 -0
  24. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/docs/PUBLISHING.md +0 -0
  25. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/docs/REFERENCE.md +0 -0
  26. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/docs/USAGE.md +0 -0
  27. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/docs/VERSION_RESOLUTION.md +0 -0
  28. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/installation.md +0 -0
  29. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/publishing.md +0 -0
  30. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/__init__.py +0 -0
  31. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/__main__.py +0 -0
  32. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/analyzer.py +0 -0
  33. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/finder.py +0 -0
  34. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/manager.py +0 -0
  35. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/py.typed +0 -0
  36. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/subfolder_build.py +0 -0
  37. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/types.py +0 -0
  38. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/utils.py +0 -0
  39. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/src/python_package_folder/version.py +0 -0
  40. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/conftest.py +0 -0
  41. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/folder_structure/some_globals.py +0 -0
  42. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  43. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  44. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  45. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  46. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  47. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  48. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_build_with_external_deps.py +0 -0
  49. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_exclude_patterns.py +0 -0
  50. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_linting.py +0 -0
  51. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_preserve_directory_structure.py +0 -0
  52. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_publisher.py +0 -0
  53. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_shared_subdirectory_imports.py +0 -0
  54. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_spreadsheet_creation_imports.py +0 -0
  55. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_subfolder_build.py +0 -0
  56. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_third_party_dependencies.py +0 -0
  57. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_utils.py +0 -0
  58. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/test_version_manager.py +0 -0
  59. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/tests/tests.py +0 -0
  60. {python_package_folder-5.2.0 → python_package_folder-5.2.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 5.2.0
3
+ Version: 5.2.2
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.0"
46
+ version = "5.2.2"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -310,11 +310,59 @@ class Publisher:
310
310
  print(f"Files to upload: {len(dist_files)}")
311
311
 
312
312
  try:
313
- subprocess.run(cmd, check=True, text=True)
313
+ result = subprocess.run(
314
+ cmd,
315
+ check=True,
316
+ text=True,
317
+ capture_output=True,
318
+ )
319
+ # Print twine output if available
320
+ if result.stdout:
321
+ print(result.stdout)
322
+ if result.stderr:
323
+ print(result.stderr, file=sys.stderr)
314
324
  print(f"\n✓ Successfully published to {self.repository.value}")
315
325
  except subprocess.CalledProcessError as e:
316
326
  print(f"\n✗ Failed to publish to {self.repository.value}", file=sys.stderr)
317
- print(f"Error: {e}", file=sys.stderr)
327
+
328
+ # Extract and display twine's actual error message
329
+ error_details = []
330
+ if e.stdout:
331
+ error_details.append(f"stdout: {e.stdout}")
332
+ if e.stderr:
333
+ error_details.append(f"stderr: {e.stderr}")
334
+ if e.returncode is not None:
335
+ error_details.append(f"exit code: {e.returncode}")
336
+
337
+ if error_details:
338
+ print("Twine error details:", file=sys.stderr)
339
+ for detail in error_details:
340
+ print(f" {detail}", file=sys.stderr)
341
+ else:
342
+ # Fallback to generic error if no output captured
343
+ print(f"Command failed: {' '.join(cmd)}", file=sys.stderr)
344
+ print(f"Return code: {e.returncode}", file=sys.stderr)
345
+
346
+ # Provide helpful hints based on common errors
347
+ if e.returncode == 1:
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
+ )
355
+ elif e.stderr and ("401" in e.stderr or "unauthorized" in e.stderr.lower()):
356
+ print(
357
+ "\nHint: Authentication failed. Check your credentials.",
358
+ file=sys.stderr,
359
+ )
360
+ elif e.stderr and ("403" in e.stderr or "forbidden" in e.stderr.lower()):
361
+ print(
362
+ "\nHint: Access forbidden. Check your permissions for this repository.",
363
+ file=sys.stderr,
364
+ )
365
+
318
366
  raise
319
367
 
320
368
  def publish_interactive(self, skip_existing: bool = False) -> None:
@@ -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
@@ -199,6 +214,8 @@ def main() -> int:
199
214
  subfolder_path=subfolder_rel_path,
200
215
  repository=repository,
201
216
  repository_url=repository_url,
217
+ username=query_username,
218
+ password=query_password,
202
219
  )
203
220
  else:
204
221
  # Workflow 2: main package
@@ -221,6 +238,8 @@ def main() -> int:
221
238
  subfolder_path=None,
222
239
  repository=repository,
223
240
  repository_url=repository_url,
241
+ username=query_username,
242
+ password=query_password,
224
243
  )
225
244
 
226
245
  if resolved_version:
@@ -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:
@@ -120,6 +124,7 @@ class SimpleIndexParser(HTMLParser):
120
124
  self.versions: set[str] = set()
121
125
  self.in_anchor = False
122
126
  self.current_href = ""
127
+ self.links_processed = 0
123
128
 
124
129
  def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
125
130
  if tag == "a":
@@ -134,17 +139,24 @@ class SimpleIndexParser(HTMLParser):
134
139
  if self.in_anchor:
135
140
  # Extract version from link text or href
136
141
  # Format: package-name-version-... or package-name-version.tar.gz
137
- version = self._extract_version_from_filename(data.strip())
138
- if version:
139
- self.versions.add(version)
142
+ link_text = data.strip()
143
+ if link_text:
144
+ logger.debug(f"Processing link text: '{link_text}'")
145
+ version = self._extract_version_from_filename(link_text)
146
+ if version:
147
+ logger.debug(f"Extracted version '{version}' from link text: '{link_text}'")
148
+ self.versions.add(version)
140
149
  # Also check href if it contains version info
141
150
  if self.current_href:
151
+ logger.debug(f"Processing href: '{self.current_href}'")
142
152
  version = self._extract_version_from_filename(self.current_href)
143
153
  if version:
154
+ logger.debug(f"Extracted version '{version}' from href: '{self.current_href}'")
144
155
  self.versions.add(version)
145
156
 
146
157
  def handle_endtag(self, tag: str) -> None:
147
158
  if tag == "a":
159
+ self.links_processed += 1
148
160
  self.in_anchor = False
149
161
  self.current_href = ""
150
162
 
@@ -176,6 +188,8 @@ class SimpleIndexParser(HTMLParser):
176
188
  def _query_azure_artifacts_version(
177
189
  package_name: str,
178
190
  repository_url: str,
191
+ username: str | None = None,
192
+ password: str | None = None,
179
193
  ) -> str | None:
180
194
  """
181
195
  Query Azure Artifacts for the latest version.
@@ -186,6 +200,8 @@ def _query_azure_artifacts_version(
186
200
  Args:
187
201
  package_name: Package name to query
188
202
  repository_url: Azure Artifacts repository URL
203
+ username: Optional username for authentication
204
+ password: Optional password/token for authentication
189
205
 
190
206
  Returns:
191
207
  Latest version string or None if not found/unsupported
@@ -198,60 +214,114 @@ def _query_azure_artifacts_version(
198
214
  simple_index_url = repository_url.replace("/upload", f"/simple/{package_name}/")
199
215
  else:
200
216
  simple_index_url = repository_url.rstrip("/") + f"/simple/{package_name}/"
201
- logger.debug(f"Constructed Azure Artifacts simple index URL: {simple_index_url}")
217
+ logger.info(f"Constructed Azure Artifacts simple index URL: {simple_index_url}")
202
218
  except Exception as e:
203
219
  logger.warning(f"Error constructing Azure Artifacts URL for '{package_name}': {e}")
204
220
  return None
205
221
 
206
222
  try:
207
- response = requests.get(simple_index_url, timeout=10)
208
- logger.debug(f"Azure Artifacts response status: {response.status_code}")
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)
232
+ logger.info(f"Azure Artifacts response: status={response.status_code}, content_length={len(response.text)} bytes")
209
233
 
210
234
  if response.status_code == 401:
211
- logger.warning(f"Authentication required for Azure Artifacts (401). Package '{package_name}' may require authentication to query.")
235
+ logger.warning(
236
+ f"Authentication required for Azure Artifacts (401). "
237
+ f"Package '{package_name}' may require authentication to query. "
238
+ f"URL: {simple_index_url}"
239
+ )
212
240
  return None
213
241
  elif response.status_code == 403:
214
- logger.warning(f"Access forbidden for Azure Artifacts (403). Package '{package_name}' may not be accessible or requires different permissions.")
242
+ logger.warning(
243
+ f"Access forbidden for Azure Artifacts (403). "
244
+ f"Package '{package_name}' may not be accessible or requires different permissions. "
245
+ f"URL: {simple_index_url}"
246
+ )
215
247
  return None
216
248
  elif response.status_code == 404:
217
- logger.debug(f"Package '{package_name}' not found on Azure Artifacts (404) - first release")
249
+ logger.info(
250
+ f"Package '{package_name}' not found on Azure Artifacts (404) - this appears to be the first release. "
251
+ f"URL: {simple_index_url}"
252
+ )
218
253
  return None
219
254
  elif response.status_code != 200:
220
- logger.warning(f"Unexpected status code {response.status_code} from Azure Artifacts for '{package_name}'")
255
+ logger.warning(
256
+ f"Unexpected status code {response.status_code} from Azure Artifacts for '{package_name}'. "
257
+ f"URL: {simple_index_url}, Response preview: {response.text[:200]}"
258
+ )
221
259
  return None
222
260
 
223
261
  # Parse HTML to extract versions
262
+ logger.info(f"Parsing HTML response to extract versions for '{package_name}'...")
224
263
  parser = SimpleIndexParser(package_name)
225
264
  try:
226
265
  parser.feed(response.text)
266
+ logger.info(
267
+ f"HTML parsing completed: processed {parser.links_processed} link(s), "
268
+ f"found {len(parser.versions)} unique version(s)"
269
+ )
227
270
  except Exception as e:
228
- logger.warning(f"Error parsing Azure Artifacts HTML for '{package_name}': {e}")
271
+ logger.warning(
272
+ f"Error parsing Azure Artifacts HTML for '{package_name}': {e}. "
273
+ f"Response length: {len(response.text)} bytes, "
274
+ f"Response preview: {response.text[:500]}"
275
+ )
229
276
  return None
230
277
 
231
278
  if not parser.versions:
232
- logger.debug(f"No versions found in Azure Artifacts HTML for '{package_name}'")
279
+ if parser.links_processed == 0:
280
+ logger.info(
281
+ f"No links found in Azure Artifacts HTML for '{package_name}'. "
282
+ f"This may indicate: (1) HTML structure differs from PEP 503 format, "
283
+ f"(2) package doesn't exist, or (3) authentication required. "
284
+ f"Response preview: {response.text[:500]}"
285
+ )
286
+ else:
287
+ logger.info(
288
+ f"Found {parser.links_processed} link(s) but no versions extracted for '{package_name}'. "
289
+ f"This may indicate: (1) package name mismatch (expected '{package_name}'), "
290
+ f"(2) filename format differs from expected pattern, or (3) first release. "
291
+ f"Response preview: {response.text[:500]}"
292
+ )
233
293
  return None
234
294
 
235
295
  # Find the latest version
236
296
  versions = list(parser.versions)
237
- logger.debug(f"Found {len(versions)} versions in Azure Artifacts: {versions}")
297
+ logger.info(f"Found {len(versions)} version(s) in Azure Artifacts HTML: {versions}")
238
298
 
239
299
  # Sort versions to find the latest
240
300
  try:
241
301
  sorted_versions = sorted(versions, key=_parse_version_for_sort, reverse=True)
242
302
  latest_version = sorted_versions[0]
243
- logger.info(f"Found latest version {latest_version} on Azure Artifacts for '{package_name}'")
303
+ logger.info(f"Latest version on Azure Artifacts for '{package_name}': {latest_version}")
244
304
  return latest_version
245
305
  except Exception as e:
246
- logger.warning(f"Error sorting versions for '{package_name}': {e}")
306
+ logger.warning(
307
+ f"Error sorting versions for '{package_name}': {e}. "
308
+ f"Versions found: {versions}. Using first version as fallback."
309
+ )
247
310
  # Fallback: return the first version found
248
311
  return versions[0]
249
312
 
250
313
  except requests.RequestException as e:
251
- logger.warning(f"Network error querying Azure Artifacts for '{package_name}': {e}")
314
+ logger.warning(
315
+ f"Network error querying Azure Artifacts for '{package_name}': {e}. "
316
+ f"URL: {simple_index_url}"
317
+ )
252
318
  return None
253
319
  except Exception as e:
254
- logger.warning(f"Unexpected error querying Azure Artifacts for '{package_name}': {e}", exc_info=True)
320
+ logger.warning(
321
+ f"Unexpected error querying Azure Artifacts for '{package_name}': {e}. "
322
+ f"URL: {simple_index_url}",
323
+ exc_info=True
324
+ )
255
325
  return None
256
326
 
257
327
 
@@ -573,6 +643,8 @@ def resolve_version(
573
643
  subfolder_path: Path | None = None,
574
644
  repository: str | None = None,
575
645
  repository_url: str | None = None,
646
+ username: str | None = None,
647
+ password: str | None = None,
576
648
  ) -> tuple[str | None, str | None]:
577
649
  """
578
650
  Resolve the next version using conventional commits.
@@ -585,6 +657,8 @@ def resolve_version(
585
657
  subfolder_path: Optional path to subfolder (relative to project_root)
586
658
  repository: Optional target repository ('pypi', 'testpypi', or 'azure')
587
659
  repository_url: Optional repository URL (required for Azure Artifacts)
660
+ username: Optional username for authenticated registry queries (Azure Artifacts)
661
+ password: Optional password/token for authenticated registry queries (Azure Artifacts)
588
662
 
589
663
  Returns:
590
664
  Tuple of (version string if a release is determined, error message if any)
@@ -596,7 +670,7 @@ def resolve_version(
596
670
  baseline_version = None
597
671
  if repository and package_name:
598
672
  logger.info(f"Attempting to query {repository} for baseline version of '{package_name}'")
599
- baseline_version = query_registry_version(package_name, repository, repository_url)
673
+ baseline_version = query_registry_version(package_name, repository, repository_url, username, password)
600
674
 
601
675
  # Step 2: Fallback to git tags if registry query failed
602
676
  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."""