napt 0.3.1__py3-none-any.whl
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.
- napt/__init__.py +91 -0
- napt/build/__init__.py +47 -0
- napt/build/manager.py +1087 -0
- napt/build/packager.py +315 -0
- napt/build/template.py +301 -0
- napt/cli.py +602 -0
- napt/config/__init__.py +42 -0
- napt/config/loader.py +465 -0
- napt/core.py +385 -0
- napt/detection.py +630 -0
- napt/discovery/__init__.py +86 -0
- napt/discovery/api_github.py +445 -0
- napt/discovery/api_json.py +452 -0
- napt/discovery/base.py +244 -0
- napt/discovery/url_download.py +304 -0
- napt/discovery/web_scrape.py +467 -0
- napt/exceptions.py +149 -0
- napt/io/__init__.py +42 -0
- napt/io/download.py +357 -0
- napt/io/upload.py +37 -0
- napt/logging.py +230 -0
- napt/policy/__init__.py +50 -0
- napt/policy/updates.py +126 -0
- napt/psadt/__init__.py +43 -0
- napt/psadt/release.py +309 -0
- napt/requirements.py +566 -0
- napt/results.py +143 -0
- napt/state/__init__.py +58 -0
- napt/state/tracker.py +371 -0
- napt/validation.py +467 -0
- napt/versioning/__init__.py +115 -0
- napt/versioning/keys.py +309 -0
- napt/versioning/msi.py +725 -0
- napt-0.3.1.dist-info/METADATA +114 -0
- napt-0.3.1.dist-info/RECORD +38 -0
- napt-0.3.1.dist-info/WHEEL +4 -0
- napt-0.3.1.dist-info/entry_points.txt +3 -0
- napt-0.3.1.dist-info/licenses/LICENSE +202 -0
napt/policy/updates.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Copyright 2025 Roger Cibrian
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Update decision policy for NAPT.
|
|
16
|
+
|
|
17
|
+
Determines whether a newly discovered remote artifact should be staged,
|
|
18
|
+
based on version, hash, and org policy.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
Check if a new version should be staged:
|
|
22
|
+
```python
|
|
23
|
+
from napt.policy.updates import should_stage, UpdatePolicy
|
|
24
|
+
|
|
25
|
+
decision = should_stage(
|
|
26
|
+
remote_version="124.0.6367.91",
|
|
27
|
+
remote_hash="abc...",
|
|
28
|
+
current_version="124.0.6367.70",
|
|
29
|
+
current_hash="def...",
|
|
30
|
+
policy=UpdatePolicy(
|
|
31
|
+
strategy="version_then_hash",
|
|
32
|
+
allow_same_version_hash_change=True,
|
|
33
|
+
comparator="semver"
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
from typing import Literal
|
|
44
|
+
|
|
45
|
+
from napt.versioning import is_newer_any
|
|
46
|
+
|
|
47
|
+
Strategy = Literal["version_only", "version_then_hash", "hash_or_version", "hash_only"]
|
|
48
|
+
Comparator = Literal["semver", "lexicographic"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class UpdatePolicy:
|
|
53
|
+
"""Update policy configuration for determining when to stage new versions.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
strategy: Strategy for version comparison (default: "version_then_hash").
|
|
57
|
+
allow_same_version_hash_change: Allow staging when version is same but
|
|
58
|
+
hash differs (default: True).
|
|
59
|
+
comparator: Version comparison method - "semver" or "lexicographic"
|
|
60
|
+
(default: "semver").
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
strategy: Strategy = "version_then_hash"
|
|
64
|
+
allow_same_version_hash_change: bool = True
|
|
65
|
+
comparator: Comparator = "semver"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def should_stage(
|
|
69
|
+
*,
|
|
70
|
+
remote_version: str,
|
|
71
|
+
remote_hash: str,
|
|
72
|
+
current_version: str | None,
|
|
73
|
+
current_hash: str | None,
|
|
74
|
+
policy: UpdatePolicy,
|
|
75
|
+
) -> bool:
|
|
76
|
+
"""Decide whether to stage a newly discovered artifact.
|
|
77
|
+
|
|
78
|
+
Compares remote version/hash against current state using the configured
|
|
79
|
+
policy strategy to determine if the new artifact should be staged.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
remote_version: Version found during discovery.
|
|
83
|
+
remote_hash: SHA-256 hash of the newly downloaded artifact.
|
|
84
|
+
current_version: Version we last staged/deployed (None if none).
|
|
85
|
+
current_hash: Hash we last staged/deployed (None if none).
|
|
86
|
+
policy: UpdatePolicy controlling the decision algorithm.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if the new artifact should be staged, False otherwise.
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
# If we have no prior state, stage the first artifact.
|
|
93
|
+
if current_version is None and current_hash is None:
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
# Normalized comparisons
|
|
97
|
+
version_changed = (
|
|
98
|
+
True
|
|
99
|
+
if current_version is None
|
|
100
|
+
else is_newer_any(remote_version, current_version, policy.comparator)
|
|
101
|
+
or remote_version != current_version
|
|
102
|
+
# Treat "different version string" as change even if comparator
|
|
103
|
+
# treats them equal
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
hash_changed = (current_hash or "").lower() != (remote_hash or "").lower()
|
|
107
|
+
|
|
108
|
+
if policy.strategy == "version_only":
|
|
109
|
+
return version_changed
|
|
110
|
+
|
|
111
|
+
if policy.strategy == "version_then_hash":
|
|
112
|
+
if version_changed:
|
|
113
|
+
return True
|
|
114
|
+
if not version_changed and policy.allow_same_version_hash_change:
|
|
115
|
+
# Same version string but bits changed (repack, resign, silent fix)
|
|
116
|
+
return hash_changed
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
if policy.strategy == "hash_or_version":
|
|
120
|
+
return version_changed or hash_changed
|
|
121
|
+
|
|
122
|
+
if policy.strategy == "hash_only":
|
|
123
|
+
return hash_changed
|
|
124
|
+
|
|
125
|
+
# Safe default: do not stage on unknown strategy
|
|
126
|
+
return False
|
napt/psadt/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright 2025 Roger Cibrian
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""PSAppDeployToolkit integration for NAPT.
|
|
16
|
+
|
|
17
|
+
This module handles PSAppDeployToolkit (PSADT) release management, caching,
|
|
18
|
+
and integration with NAPT's build system.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
Basic usage:
|
|
22
|
+
```python
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from napt.psadt import get_psadt_release, fetch_latest_psadt_version
|
|
25
|
+
|
|
26
|
+
# Get latest version
|
|
27
|
+
latest = fetch_latest_psadt_version()
|
|
28
|
+
print(f"Latest PSADT: {latest}")
|
|
29
|
+
|
|
30
|
+
# Download and cache
|
|
31
|
+
psadt_path = get_psadt_release("latest", Path("cache/psadt"))
|
|
32
|
+
print(f"PSADT cached at: {psadt_path}")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from .release import (
|
|
38
|
+
fetch_latest_psadt_version,
|
|
39
|
+
get_psadt_release,
|
|
40
|
+
is_psadt_cached,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = ["fetch_latest_psadt_version", "get_psadt_release", "is_psadt_cached"]
|
napt/psadt/release.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Copyright 2025 Roger Cibrian
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""PSADT release management for NAPT.
|
|
16
|
+
|
|
17
|
+
This module handles fetching, downloading, and caching PSAppDeployToolkit
|
|
18
|
+
releases from the official GitHub repository. It reuses NAPT's existing
|
|
19
|
+
GitHub release discovery infrastructure for consistency.
|
|
20
|
+
|
|
21
|
+
Key Features:
|
|
22
|
+
|
|
23
|
+
- Fetch latest PSADT version from GitHub API
|
|
24
|
+
- Download and cache specific PSADT versions
|
|
25
|
+
- Extract releases to cache directory
|
|
26
|
+
- Version resolution ("latest" keyword support)
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
Get and cache PSADT releases:
|
|
30
|
+
```python
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from napt.psadt import get_psadt_release, is_psadt_cached
|
|
33
|
+
|
|
34
|
+
# Get latest PSADT
|
|
35
|
+
psadt_dir = get_psadt_release("latest", Path("cache/psadt"))
|
|
36
|
+
|
|
37
|
+
# Get specific version
|
|
38
|
+
psadt_dir = get_psadt_release("4.1.7", Path("cache/psadt"))
|
|
39
|
+
|
|
40
|
+
# Check if cached
|
|
41
|
+
if is_psadt_cached("4.1.7", Path("cache/psadt")):
|
|
42
|
+
print("Already cached!")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Note:
|
|
46
|
+
- Reuses notapkgtool.discovery.api_github for API calls
|
|
47
|
+
- Caches releases by version: cache/psadt/{version}/
|
|
48
|
+
- Downloads .zip releases and extracts to cache
|
|
49
|
+
- Validates extracted PSADT structure (PSAppDeployToolkit/ folder exists)
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
import re
|
|
57
|
+
import zipfile
|
|
58
|
+
|
|
59
|
+
import requests
|
|
60
|
+
|
|
61
|
+
from napt.exceptions import NetworkError, PackagingError
|
|
62
|
+
|
|
63
|
+
PSADT_REPO = "PSAppDeployToolkit/PSAppDeployToolkit"
|
|
64
|
+
PSADT_GITHUB_API = f"https://api.github.com/repos/{PSADT_REPO}/releases/latest"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def fetch_latest_psadt_version() -> str:
|
|
68
|
+
"""Fetch the latest PSADT release version from GitHub.
|
|
69
|
+
|
|
70
|
+
Queries the GitHub API for the latest release and extracts the version
|
|
71
|
+
number from the tag name (e.g., "4.1.7" from tag "4.1.7").
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Version number (e.g., "4.1.7").
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
RuntimeError: If the GitHub API request fails or version cannot be
|
|
78
|
+
extracted.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
Get latest PSADT version from GitHub:
|
|
82
|
+
```python
|
|
83
|
+
version = fetch_latest_psadt_version()
|
|
84
|
+
print(version) # Output: "4.1.7"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Note:
|
|
88
|
+
- Uses GitHub's public API (60 requests/hour limit without auth)
|
|
89
|
+
- Version is extracted from release tag name
|
|
90
|
+
- For higher rate limits, set GITHUB_TOKEN environment variable
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
from napt.logging import get_global_logger
|
|
94
|
+
|
|
95
|
+
logger = get_global_logger()
|
|
96
|
+
logger.verbose("PSADT", f"Querying GitHub API: {PSADT_GITHUB_API}")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
headers = {
|
|
100
|
+
"Accept": "application/vnd.github+json",
|
|
101
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
response = requests.get(PSADT_GITHUB_API, headers=headers, timeout=30)
|
|
105
|
+
response.raise_for_status()
|
|
106
|
+
except requests.RequestException as err:
|
|
107
|
+
raise NetworkError(
|
|
108
|
+
f"Failed to fetch latest PSADT release from GitHub: {err}"
|
|
109
|
+
) from err
|
|
110
|
+
|
|
111
|
+
data = response.json()
|
|
112
|
+
tag_name = data.get("tag_name", "")
|
|
113
|
+
|
|
114
|
+
if not tag_name:
|
|
115
|
+
raise NetworkError("GitHub API response missing 'tag_name' field")
|
|
116
|
+
|
|
117
|
+
# Extract version from tag (e.g., "4.1.7" or "v4.1.7")
|
|
118
|
+
# PSADT uses tags without 'v' prefix
|
|
119
|
+
version_match = re.match(r"v?(\d+\.\d+\.\d+)", tag_name)
|
|
120
|
+
if not version_match:
|
|
121
|
+
raise NetworkError(f"Could not extract version from tag: {tag_name!r}")
|
|
122
|
+
|
|
123
|
+
version = version_match.group(1)
|
|
124
|
+
logger.verbose("PSADT", f"Latest PSADT version: {version}")
|
|
125
|
+
|
|
126
|
+
return version
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def is_psadt_cached(version: str, cache_dir: Path) -> bool:
|
|
130
|
+
"""Check if a PSADT version is already cached.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
version: PSADT version to check (e.g., "4.1.7").
|
|
134
|
+
cache_dir: Base cache directory (e.g., Path("cache/psadt")).
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if the version is cached and valid, False otherwise.
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
Check if PSADT version is cached:
|
|
141
|
+
```python
|
|
142
|
+
from pathlib import Path
|
|
143
|
+
|
|
144
|
+
if is_psadt_cached("4.1.7", Path("cache/psadt")):
|
|
145
|
+
print("Already downloaded!")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Note:
|
|
149
|
+
Validates that the cache contains the expected PSADT structure:
|
|
150
|
+
|
|
151
|
+
- PSAppDeployToolkit/ folder must exist
|
|
152
|
+
- PSAppDeployToolkit.psd1 manifest must exist
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
version_dir = cache_dir / version
|
|
156
|
+
psadt_dir = version_dir / "PSAppDeployToolkit"
|
|
157
|
+
manifest = psadt_dir / "PSAppDeployToolkit.psd1"
|
|
158
|
+
|
|
159
|
+
return psadt_dir.exists() and manifest.exists()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_psadt_release(release_spec: str, cache_dir: Path) -> Path:
|
|
163
|
+
"""Download and extract a PSADT release to the cache directory.
|
|
164
|
+
|
|
165
|
+
Resolves "latest" to the current latest version from GitHub, then
|
|
166
|
+
downloads the release .zip file and extracts it to the cache.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
release_spec: Version specifier - either "latest" or specific version
|
|
170
|
+
(e.g., "4.1.7").
|
|
171
|
+
cache_dir: Base cache directory for PSADT releases.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Path to the cached PSADT directory (cache_dir/{version}).
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
NetworkError: If download fails.
|
|
178
|
+
PackagingError: If extraction fails.
|
|
179
|
+
ConfigError: If release_spec is invalid.
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
Get latest version:
|
|
183
|
+
```python
|
|
184
|
+
from pathlib import Path
|
|
185
|
+
|
|
186
|
+
psadt = get_psadt_release("latest", Path("cache/psadt"))
|
|
187
|
+
print(psadt) # Output: cache/psadt/4.1.7
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Get specific version:
|
|
191
|
+
```python
|
|
192
|
+
psadt = get_psadt_release("4.1.7", Path("cache/psadt"))
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Note:
|
|
196
|
+
- Caches by version: cache/psadt/{version}/PSAppDeployToolkit/
|
|
197
|
+
- If already cached, returns path immediately (no re-download)
|
|
198
|
+
- Downloads from GitHub releases as .zip files
|
|
199
|
+
- Extracts entire archive to version directory
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
from napt.logging import get_global_logger
|
|
203
|
+
|
|
204
|
+
logger = get_global_logger()
|
|
205
|
+
# Resolve "latest" to actual version
|
|
206
|
+
if release_spec == "latest":
|
|
207
|
+
logger.verbose("PSADT", "Resolving 'latest' to current version...")
|
|
208
|
+
version = fetch_latest_psadt_version()
|
|
209
|
+
else:
|
|
210
|
+
version = release_spec
|
|
211
|
+
|
|
212
|
+
logger.verbose("PSADT", f"PSADT version: {version}")
|
|
213
|
+
|
|
214
|
+
# Check if already cached
|
|
215
|
+
if is_psadt_cached(version, cache_dir):
|
|
216
|
+
version_dir = cache_dir / version
|
|
217
|
+
logger.verbose("PSADT", f"Using cached PSADT: {version_dir}")
|
|
218
|
+
return version_dir
|
|
219
|
+
|
|
220
|
+
# Need to download
|
|
221
|
+
logger.verbose("PSADT", f"Downloading PSADT {version}...")
|
|
222
|
+
|
|
223
|
+
# Get release info from GitHub
|
|
224
|
+
release_url = f"https://api.github.com/repos/{PSADT_REPO}/releases/tags/{version}"
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
headers = {
|
|
228
|
+
"Accept": "application/vnd.github+json",
|
|
229
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
response = requests.get(release_url, headers=headers, timeout=30)
|
|
233
|
+
response.raise_for_status()
|
|
234
|
+
except requests.RequestException as err:
|
|
235
|
+
raise NetworkError(
|
|
236
|
+
f"Failed to fetch PSADT release {version} from GitHub: {err}"
|
|
237
|
+
) from err
|
|
238
|
+
|
|
239
|
+
release_data = response.json()
|
|
240
|
+
|
|
241
|
+
# Find the Template_v4 .zip asset (the full v4 template structure)
|
|
242
|
+
assets = release_data.get("assets", [])
|
|
243
|
+
zip_asset = None
|
|
244
|
+
|
|
245
|
+
# Look for Template_v4 version specifically
|
|
246
|
+
for asset in assets:
|
|
247
|
+
name = asset.get("name", "")
|
|
248
|
+
if name.endswith(".zip") and "Template_v4" in name:
|
|
249
|
+
zip_asset = asset
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
# Fallback to any PSADT zip if Template_v4 not found
|
|
253
|
+
if not zip_asset:
|
|
254
|
+
for asset in assets:
|
|
255
|
+
name = asset.get("name", "")
|
|
256
|
+
if name.endswith(".zip") and "PSAppDeployToolkit" in name:
|
|
257
|
+
zip_asset = asset
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
if not zip_asset:
|
|
261
|
+
raise NetworkError(
|
|
262
|
+
f"No .zip asset found in PSADT release {version}. "
|
|
263
|
+
f"Available assets: {[a.get('name') for a in assets]}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
download_url = zip_asset.get("browser_download_url")
|
|
267
|
+
if not download_url:
|
|
268
|
+
raise NetworkError(f"Asset missing download URL: {zip_asset}")
|
|
269
|
+
|
|
270
|
+
logger.verbose("PSADT", f"Downloading: {zip_asset['name']}")
|
|
271
|
+
|
|
272
|
+
# Download the .zip file
|
|
273
|
+
try:
|
|
274
|
+
zip_response = requests.get(download_url, timeout=300)
|
|
275
|
+
zip_response.raise_for_status()
|
|
276
|
+
except requests.RequestException as err:
|
|
277
|
+
raise NetworkError(f"Failed to download PSADT release: {err}") from err
|
|
278
|
+
|
|
279
|
+
# Create cache directory
|
|
280
|
+
version_dir = cache_dir / version
|
|
281
|
+
version_dir.mkdir(parents=True, exist_ok=True)
|
|
282
|
+
|
|
283
|
+
# Save .zip temporarily
|
|
284
|
+
zip_path = version_dir / f"psadt_{version}.zip"
|
|
285
|
+
zip_path.write_bytes(zip_response.content)
|
|
286
|
+
|
|
287
|
+
logger.verbose("PSADT", f"Extracting to: {version_dir}")
|
|
288
|
+
|
|
289
|
+
# Extract .zip
|
|
290
|
+
try:
|
|
291
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
292
|
+
zf.extractall(version_dir)
|
|
293
|
+
except zipfile.BadZipFile as err:
|
|
294
|
+
raise PackagingError(f"Failed to extract PSADT archive: {err}") from err
|
|
295
|
+
finally:
|
|
296
|
+
# Clean up .zip file
|
|
297
|
+
if zip_path.exists():
|
|
298
|
+
zip_path.unlink()
|
|
299
|
+
|
|
300
|
+
# Verify extracted structure
|
|
301
|
+
if not is_psadt_cached(version, cache_dir):
|
|
302
|
+
raise PackagingError(
|
|
303
|
+
f"PSADT extraction failed: PSAppDeployToolkit/ folder "
|
|
304
|
+
f"not found in {version_dir}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
logger.verbose("PSADT", f"PSADT {version} cached successfully")
|
|
308
|
+
|
|
309
|
+
return version_dir
|