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.
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/PKG-INFO +1 -1
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/pyproject.toml +1 -1
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/version_calculator.py +97 -9
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_version_calculator.py +54 -2
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.copier-answers.yml +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
- {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
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.cursor/rules/general.mdc +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.cursor/rules/python.mdc +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.github/workflows/ci.yml +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.github/workflows/publish.yml +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.gitignore +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/.vscode/settings.json +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/LICENSE +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/MANIFEST.in +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/Makefile +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/README.md +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/coverage.svg +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/development.md +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/DEVELOPMENT.md +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/INSTALLATION.md +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/PUBLISHING.md +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/REFERENCE.md +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/USAGE.md +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/docs/VERSION_RESOLUTION.md +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/installation.md +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/publishing.md +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/__init__.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/__main__.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/analyzer.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/finder.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/manager.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/publisher.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/py.typed +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/python_package_folder.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/subfolder_build.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/types.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/utils.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/version.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/conftest.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/some_globals.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_build_with_external_deps.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_exclude_patterns.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_linting.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_preserve_directory_structure.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_publisher.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_shared_subdirectory_imports.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_spreadsheet_creation_imports.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_subfolder_build.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_third_party_dependencies.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_utils.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_version_manager.py +0 -0
- {python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/tests.py +0 -0
- {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.
|
|
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>
|
|
@@ -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)
|
|
121
|
-
|
|
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=
|
|
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
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_version_calculator.py
RENAMED
|
@@ -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
|
|
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
|
-
#
|
|
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")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/__init__.py
RENAMED
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/__main__.py
RENAMED
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/analyzer.py
RENAMED
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/finder.py
RENAMED
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/manager.py
RENAMED
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/publisher.py
RENAMED
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/types.py
RENAMED
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/utils.py
RENAMED
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/src/python_package_folder/version.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/folder_structure/some_globals.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_build_with_external_deps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_package_folder-5.1.6 → python_package_folder-5.2.0}/tests/test_third_party_dependencies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|