Skip to content

Commit f25c82e

Browse files
committed
Add authentication support for Azure Artifacts in version retrieval
This commit enhances the version retrieval process by adding support for authenticated queries to Azure Artifacts. The main function now retrieves credentials from command-line arguments or environment variables, ensuring seamless integration in non-interactive environments. The query_registry_version and _query_azure_artifacts_version functions are updated to accept username and password parameters, allowing for secure access to package versions. Additionally, tests are introduced to verify the functionality of authenticated queries, improving overall reliability and user experience.
1 parent aab9f4d commit f25c82e

3 files changed

Lines changed: 75 additions & 4 deletions

File tree

src/python_package_folder/python_package_folder.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,21 @@ def build_cmd() -> None:
186186
repository = args.publish if args.publish else None
187187
repository_url = args.repository_url if args.publish else None
188188

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+
189204
if is_subfolder:
190205
# Workflow 1: subfolder build
191206
# src_dir is guaranteed to be relative to project_root due to is_subfolder check
@@ -199,6 +214,8 @@ def build_cmd() -> None:
199214
subfolder_path=subfolder_rel_path,
200215
repository=repository,
201216
repository_url=repository_url,
217+
username=query_username,
218+
password=query_password,
202219
)
203220
else:
204221
# Workflow 2: main package
@@ -221,6 +238,8 @@ def build_cmd() -> None:
221238
subfolder_path=None,
222239
repository=repository,
223240
repository_url=repository_url,
241+
username=query_username,
242+
password=query_password,
224243
)
225244

226245
if resolved_version:

src/python_package_folder/version_calculator.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ def query_registry_version(
2525
package_name: str,
2626
repository: str,
2727
repository_url: str | None = None,
28+
username: str | None = None,
29+
password: str | None = None,
2830
) -> str | None:
2931
"""
3032
Query package registry for the latest published version.
@@ -33,6 +35,8 @@ def query_registry_version(
3335
package_name: Package name to query
3436
repository: Repository type ('pypi', 'testpypi', or 'azure')
3537
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)
3640
3741
Returns:
3842
Latest version string or None if not found/unsupported
@@ -54,7 +58,7 @@ def query_registry_version(
5458
logger.warning("Azure Artifacts repository URL not provided")
5559
return None
5660
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)
5862
if version:
5963
logger.info(f"Found version {version} on Azure Artifacts")
6064
else:
@@ -184,6 +188,8 @@ def _extract_version_from_filename(self, filename: str) -> str | None:
184188
def _query_azure_artifacts_version(
185189
package_name: str,
186190
repository_url: str,
191+
username: str | None = None,
192+
password: str | None = None,
187193
) -> str | None:
188194
"""
189195
Query Azure Artifacts for the latest version.
@@ -194,6 +200,8 @@ def _query_azure_artifacts_version(
194200
Args:
195201
package_name: Package name to query
196202
repository_url: Azure Artifacts repository URL
203+
username: Optional username for authentication
204+
password: Optional password/token for authentication
197205
198206
Returns:
199207
Latest version string or None if not found/unsupported
@@ -212,8 +220,15 @@ def _query_azure_artifacts_version(
212220
return None
213221

214222
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)
217232
logger.info(f"Azure Artifacts response: status={response.status_code}, content_length={len(response.text)} bytes")
218233

219234
if response.status_code == 401:
@@ -628,6 +643,8 @@ def resolve_version(
628643
subfolder_path: Path | None = None,
629644
repository: str | None = None,
630645
repository_url: str | None = None,
646+
username: str | None = None,
647+
password: str | None = None,
631648
) -> tuple[str | None, str | None]:
632649
"""
633650
Resolve the next version using conventional commits.
@@ -640,6 +657,8 @@ def resolve_version(
640657
subfolder_path: Optional path to subfolder (relative to project_root)
641658
repository: Optional target repository ('pypi', 'testpypi', or 'azure')
642659
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)
643662
644663
Returns:
645664
Tuple of (version string if a release is determined, error message if any)
@@ -651,7 +670,7 @@ def resolve_version(
651670
baseline_version = None
652671
if repository and package_name:
653672
logger.info(f"Attempting to query {repository} for baseline version of '{package_name}'")
654-
baseline_version = query_registry_version(package_name, repository, repository_url)
673+
baseline_version = query_registry_version(package_name, repository, repository_url, username, password)
655674

656675
# Step 2: Fallback to git tags if registry query failed
657676
if not baseline_version:

tests/test_version_calculator.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,39 @@ def test_query_azure_artifacts_version_empty_html(self, mock_get: MagicMock) ->
151151
# Should return None when no versions found in HTML
152152
assert version is None
153153

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+
154187
@patch("python_package_folder.version_calculator.requests.get")
155188
def test_query_registry_version_error_handling(self, mock_get: MagicMock) -> None:
156189
"""Test that registry query errors are handled gracefully."""

0 commit comments

Comments
 (0)