python-package-folder 5.1.6__tar.gz → 5.2.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.1.6 → python_package_folder-5.2.0}/PKG-INFO +1 -1
  2. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/pyproject.toml +1 -1
  3. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/version_calculator.py +97 -9
  4. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_version_calculator.py +54 -2
  5. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.copier-answers.yml +0 -0
  6. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  7. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  8. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.cursor/rules/general.mdc +0 -0
  9. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.cursor/rules/python.mdc +0 -0
  10. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.github/workflows/ci.yml +0 -0
  11. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.github/workflows/publish.yml +0 -0
  12. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.gitignore +0 -0
  13. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.vscode/settings.json +0 -0
  14. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/LICENSE +0 -0
  15. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/MANIFEST.in +0 -0
  16. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/Makefile +0 -0
  17. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/README.md +0 -0
  18. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/coverage.svg +0 -0
  19. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/development.md +0 -0
  20. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/DEVELOPMENT.md +0 -0
  21. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/INSTALLATION.md +0 -0
  22. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/PUBLISHING.md +0 -0
  23. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/REFERENCE.md +0 -0
  24. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/USAGE.md +0 -0
  25. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/VERSION_RESOLUTION.md +0 -0
  26. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/installation.md +0 -0
  27. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/publishing.md +0 -0
  28. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/__init__.py +0 -0
  29. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/__main__.py +0 -0
  30. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/analyzer.py +0 -0
  31. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/finder.py +0 -0
  32. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/manager.py +0 -0
  33. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/publisher.py +0 -0
  34. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/py.typed +0 -0
  35. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/python_package_folder.py +0 -0
  36. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/subfolder_build.py +0 -0
  37. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/types.py +0 -0
  38. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/utils.py +0 -0
  39. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/version.py +0 -0
  40. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/conftest.py +0 -0
  41. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/some_globals.py +0 -0
  42. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  43. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  44. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  45. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  46. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  47. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  48. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_build_with_external_deps.py +0 -0
  49. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_exclude_patterns.py +0 -0
  50. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_linting.py +0 -0
  51. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_preserve_directory_structure.py +0 -0
  52. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_publisher.py +0 -0
  53. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_shared_subdirectory_imports.py +0 -0
  54. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_spreadsheet_creation_imports.py +0 -0
  55. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_subfolder_build.py +0 -0
  56. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_third_party_dependencies.py +0 -0
  57. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_utils.py +0 -0
  58. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_version_manager.py +0 -0
  59. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/tests.py +0 -0
  60. {python_package_folder-5.1.6 → python_package_folder-5.2.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 5.1.6
3
+ Version: 5.2.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>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "5.1.6"
46
+ version = "5.2.0"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
  import logging
14
14
  import re
15
15
  import subprocess
16
+ from html.parser import HTMLParser
16
17
  from pathlib import Path
17
18
 
18
19
  import requests
@@ -110,6 +111,68 @@ def _query_pypi_version(package_name: str, registry: str) -> str | None:
110
111
  return None
111
112
 
112
113
 
114
+ class SimpleIndexParser(HTMLParser):
115
+ """Parser for PEP 503 simple index HTML to extract package versions."""
116
+
117
+ def __init__(self, package_name: str):
118
+ super().__init__()
119
+ self.package_name = package_name
120
+ self.versions: set[str] = set()
121
+ self.in_anchor = False
122
+ self.current_href = ""
123
+
124
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
125
+ if tag == "a":
126
+ self.in_anchor = True
127
+ # Extract href attribute
128
+ for attr_name, attr_value in attrs:
129
+ if attr_name == "href" and attr_value:
130
+ self.current_href = attr_value
131
+ break
132
+
133
+ def handle_data(self, data: str) -> None:
134
+ if self.in_anchor:
135
+ # Extract version from link text or href
136
+ # 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)
140
+ # Also check href if it contains version info
141
+ if self.current_href:
142
+ version = self._extract_version_from_filename(self.current_href)
143
+ if version:
144
+ self.versions.add(version)
145
+
146
+ def handle_endtag(self, tag: str) -> None:
147
+ if tag == "a":
148
+ self.in_anchor = False
149
+ self.current_href = ""
150
+
151
+ def _extract_version_from_filename(self, filename: str) -> str | None:
152
+ """Extract version number from package filename."""
153
+ # Pattern: package-name-version-... or package-name-version.tar.gz
154
+ # Examples: data-0.1.0-py3-none-any.whl, data-0.1.0.tar.gz
155
+ # The version is between the package name and the next separator
156
+
157
+ # Normalize package name (replace - with _ for matching)
158
+ normalized_package = self.package_name.replace("-", "_").replace(".", "_")
159
+
160
+ # Try to match: package-name-version- or package-name-version.
161
+ # Version format: X.Y.Z (semantic versioning)
162
+ pattern = rf"{re.escape(self.package_name)}-(\d+\.\d+\.\d+(?:\.\d+)?(?:[a-zA-Z0-9]+)?)"
163
+ match = re.search(pattern, filename, re.IGNORECASE)
164
+ if match:
165
+ return match.group(1)
166
+
167
+ # Fallback: try with normalized package name
168
+ pattern = rf"{re.escape(normalized_package)}-(\d+\.\d+\.\d+(?:\.\d+)?(?:[a-zA-Z0-9]+)?)"
169
+ match = re.search(pattern, filename, re.IGNORECASE)
170
+ if match:
171
+ return match.group(1)
172
+
173
+ return None
174
+
175
+
113
176
  def _query_azure_artifacts_version(
114
177
  package_name: str,
115
178
  repository_url: str,
@@ -117,8 +180,8 @@ def _query_azure_artifacts_version(
117
180
  """
118
181
  Query Azure Artifacts for the latest version.
119
182
 
120
- Azure Artifacts uses a simple index format (HTML) which is more complex to parse.
121
- For now, we'll attempt to query but fall back gracefully if it fails.
183
+ Azure Artifacts uses a simple index format (HTML) following PEP 503.
184
+ Parses the HTML to extract version numbers from package filenames.
122
185
 
123
186
  Args:
124
187
  package_name: Package name to query
@@ -141,24 +204,49 @@ def _query_azure_artifacts_version(
141
204
  return None
142
205
 
143
206
  try:
144
- response = requests.get(simple_index_url, timeout=5)
207
+ response = requests.get(simple_index_url, timeout=10)
145
208
  logger.debug(f"Azure Artifacts response status: {response.status_code}")
146
209
 
147
210
  if response.status_code == 401:
148
211
  logger.warning(f"Authentication required for Azure Artifacts (401). Package '{package_name}' may require authentication to query.")
212
+ return None
149
213
  elif response.status_code == 403:
150
214
  logger.warning(f"Access forbidden for Azure Artifacts (403). Package '{package_name}' may not be accessible or requires different permissions.")
215
+ return None
151
216
  elif response.status_code == 404:
152
217
  logger.debug(f"Package '{package_name}' not found on Azure Artifacts (404) - first release")
218
+ return None
153
219
  elif response.status_code != 200:
154
220
  logger.warning(f"Unexpected status code {response.status_code} from Azure Artifacts for '{package_name}'")
221
+ return None
155
222
 
156
- # Azure Artifacts simple index returns HTML, not JSON
157
- # Parsing HTML is complex and may require authentication
158
- # For now, we'll return None to fall back to git tags
159
- # This can be enhanced later with proper HTML parsing or API endpoint discovery
160
- logger.info(f"Azure Artifacts version query not fully implemented (HTML parsing required). Falling back to git tags.")
161
- return None
223
+ # Parse HTML to extract versions
224
+ parser = SimpleIndexParser(package_name)
225
+ try:
226
+ parser.feed(response.text)
227
+ except Exception as e:
228
+ logger.warning(f"Error parsing Azure Artifacts HTML for '{package_name}': {e}")
229
+ return None
230
+
231
+ if not parser.versions:
232
+ logger.debug(f"No versions found in Azure Artifacts HTML for '{package_name}'")
233
+ return None
234
+
235
+ # Find the latest version
236
+ versions = list(parser.versions)
237
+ logger.debug(f"Found {len(versions)} versions in Azure Artifacts: {versions}")
238
+
239
+ # Sort versions to find the latest
240
+ try:
241
+ sorted_versions = sorted(versions, key=_parse_version_for_sort, reverse=True)
242
+ latest_version = sorted_versions[0]
243
+ logger.info(f"Found latest version {latest_version} on Azure Artifacts for '{package_name}'")
244
+ return latest_version
245
+ except Exception as e:
246
+ logger.warning(f"Error sorting versions for '{package_name}': {e}")
247
+ # Fallback: return the first version found
248
+ return versions[0]
249
+
162
250
  except requests.RequestException as e:
163
251
  logger.warning(f"Network error querying Azure Artifacts for '{package_name}': {e}")
164
252
  return None
@@ -86,9 +86,22 @@ class TestQueryRegistryVersion:
86
86
 
87
87
  @patch("python_package_folder.version_calculator.requests.get")
88
88
  def test_query_azure_artifacts_version(self, mock_get: MagicMock) -> None:
89
- """Test querying Azure Artifacts (basic support, returns None for now)."""
89
+ """Test querying Azure Artifacts with HTML parsing."""
90
90
  mock_response = Mock()
91
91
  mock_response.status_code = 200
92
+ # Simulate PEP 503 simple index HTML response
93
+ mock_response.text = """<!DOCTYPE html>
94
+ <html>
95
+ <head>
96
+ <title>Links for test-package</title>
97
+ </head>
98
+ <body>
99
+ <h1>Links for test-package</h1>
100
+ <a href="test-package-0.1.0-py3-none-any.whl">test-package-0.1.0-py3-none-any.whl</a>
101
+ <a href="test-package-0.2.0-py3-none-any.whl">test-package-0.2.0-py3-none-any.whl</a>
102
+ <a href="test-package-0.1.5.tar.gz">test-package-0.1.5.tar.gz</a>
103
+ </body>
104
+ </html>"""
92
105
  mock_get.return_value = mock_response
93
106
 
94
107
  version = query_registry_version(
@@ -96,7 +109,46 @@ class TestQueryRegistryVersion:
96
109
  "azure",
97
110
  repository_url="https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/pypi/upload",
98
111
  )
99
- # Azure Artifacts parsing not fully implemented, returns None
112
+ # Should parse HTML and return the latest version
113
+ assert version == "0.2.0"
114
+
115
+ @patch("python_package_folder.version_calculator.requests.get")
116
+ def test_query_azure_artifacts_version_not_found(self, mock_get: MagicMock) -> None:
117
+ """Test querying Azure Artifacts when package doesn't exist (404)."""
118
+ mock_response = Mock()
119
+ mock_response.status_code = 404
120
+ mock_get.return_value = mock_response
121
+
122
+ version = query_registry_version(
123
+ "test-package",
124
+ "azure",
125
+ repository_url="https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/pypi/upload",
126
+ )
127
+ # Should return None for 404 (first release)
128
+ assert version is None
129
+
130
+ @patch("python_package_folder.version_calculator.requests.get")
131
+ def test_query_azure_artifacts_version_empty_html(self, mock_get: MagicMock) -> None:
132
+ """Test querying Azure Artifacts with empty HTML (no versions)."""
133
+ mock_response = Mock()
134
+ mock_response.status_code = 200
135
+ mock_response.text = """<!DOCTYPE html>
136
+ <html>
137
+ <head>
138
+ <title>Links for test-package</title>
139
+ </head>
140
+ <body>
141
+ <h1>Links for test-package</h1>
142
+ </body>
143
+ </html>"""
144
+ mock_get.return_value = mock_response
145
+
146
+ version = query_registry_version(
147
+ "test-package",
148
+ "azure",
149
+ repository_url="https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/pypi/upload",
150
+ )
151
+ # Should return None when no versions found in HTML
100
152
  assert version is None
101
153
 
102
154
  @patch("python_package_folder.version_calculator.requests.get")