discovery
napt.discovery.base
Discovery strategy base protocol and registry for NAPT.
This module defines the foundational components for the discovery system:
- DiscoveryStrategy protocol: Interface that all strategies must implement
- Strategy registry: Global dict mapping strategy names to implementations
- Registration and lookup functions: register_strategy() and get_strategy()
The discovery system uses a strategy pattern to support multiple ways of obtaining application installers and their versions:
- url_download: Direct download from a static URL (FILE-FIRST)
- web_scrape: Scrape vendor download pages to find links and extract versions (VERSION-FIRST)
- api_github: Fetch from GitHub releases API (VERSION-FIRST)
- api_json: Query JSON API endpoints for version and download URL (VERSION-FIRST)
Design Philosophy
- Strategies are Protocol classes (structural subtyping, not inheritance)
- Registration happens at module import time (strategies self-register)
- Registry is a simple dict (no complex dependency injection needed)
- Each strategy is stateless and can be instantiated on-demand
Protocol Benefits:
Using typing.Protocol instead of ABC allows:
- Duck typing: Classes don't need explicit inheritance
- Better IDE support: Type checkers verify interface compliance
- Flexibility: Third-party code can add strategies without touching base
Example
Implementing a custom strategy:
from napt.discovery.base import register_strategy, DiscoveryStrategy
from pathlib import Path
from typing import Any
from napt.versioning.keys import DiscoveredVersion
class MyCustomStrategy:
def discover_version(
self, app_config: dict[str, Any], output_dir: Path
) -> tuple[DiscoveredVersion, Path, str]:
# Implement your discovery logic here
...
# Register it (typically at module import)
register_strategy("my_custom", MyCustomStrategy)
# Now it can be used in recipes:
# source:
# strategy: my_custom
# ...
DiscoveryStrategy
Bases: Protocol
Protocol for version discovery strategies.
Each strategy must implement discover_version() which downloads and extracts version information based on the app config.
Strategies may optionally implement validate_config() to provide strategy-specific configuration validation without network calls.
Source code in napt/discovery/base.py
discover_version
discover_version(app_config: dict[str, Any], output_dir: Path) -> tuple[DiscoveredVersion, Path, str, dict]
Discover and download an application version.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app_config
|
dict[str, Any]
|
The app configuration from the recipe
( |
required |
output_dir
|
Path
|
Directory to download the installer to. |
required |
Returns:
| Type | Description |
|---|---|
tuple[DiscoveredVersion, Path, str, dict]
|
A tuple (discovered_version, file_path, sha256, headers), where discovered_version is the version information, file_path is the path to the downloaded file, sha256 is the SHA-256 hash, and headers contains HTTP response headers for caching. |
Raises:
| Type | Description |
|---|---|
ValueError
|
On discovery or download failures. |
RuntimeError
|
On discovery or download failures. |
Source code in napt/discovery/base.py
validate_config
Validate strategy-specific configuration (optional).
This method validates the app configuration for strategy-specific requirements without making network calls or downloading files. Useful for quick feedback during recipe development.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app_config
|
dict[str, Any]
|
The app configuration from the recipe
( |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
List of error messages. Empty list if configuration is valid. |
list[str]
|
Each error should be a human-readable description of the issue. |
Example
Check required fields:
Note
This method is optional; strategies without it will skip validation. Should NOT make network calls or download files. Should check field presence, types, and format only. Used by 'napt validate' command for fast recipe checking.
Source code in napt/discovery/base.py
register_strategy
Register a discovery strategy by name in the global registry.
This function should be called when a strategy module is imported, typically at module level. Registering the same name twice will overwrite the previous registration (allows monkey-patching for tests).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Strategy name (e.g., "url_download"). This is the value used in recipe YAML files under source.strategy. Names should be lowercase with underscores for readability. |
required |
strategy_class
|
type[DiscoveryStrategy]
|
The strategy class to register. Must implement the DiscoveryStrategy protocol (have a discover_version method with the correct signature). |
required |
Example
Register at module import time:
Note
No validation is performed at registration time. Type checkers will verify protocol compliance at static analysis time. Runtime errors occur at strategy instantiation or invocation.
Source code in napt/discovery/base.py
get_strategy
Get a discovery strategy instance by name from the global registry.
The strategy is instantiated on-demand (strategies are stateless, so a new instance is created for each call). The strategy module must have been imported first for registration to occur.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Strategy name (e.g., "url_download"). Must exactly match a name registered via register_strategy(). Case-sensitive. |
required |
Returns:
| Type | Description |
|---|---|
DiscoveryStrategy
|
A new instance of the requested strategy, ready to use. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If the strategy name is not registered. The error message includes a list of available strategies for troubleshooting. |
Example
Get and use a strategy:
from napt.discovery import get_strategy
strategy = get_strategy("url_download")
# Use strategy.discover_version(...)
Handle unknown strategy:
Note
Strategies must be registered before they can be retrieved. The url_download strategy is auto-registered when imported. New strategies can be added by creating a module and registering.
Source code in napt/discovery/base.py
napt.discovery.url_download
URL download discovery strategy for NAPT.
This is a FILE-FIRST strategy that downloads an installer from a fixed HTTP(S) URL and extracts version information from the downloaded file. Uses HTTP ETag conditional requests to avoid re-downloading unchanged files.
Key Advantages:
- Works with any fixed URL (version not required in URL)
- Extracts accurate version directly from installer metadata
- Uses ETag-based conditional requests for efficiency (~500ms vs full download)
- Simple and reliable for vendors with stable download URLs
- Fallback strategy when version not available via API/URL pattern
Supported Version Extraction:
- MSI files (
.msiextension): Automatically detected, extracts ProductVersion property from MSI files - Other file types: Not supported. Use a version-first strategy (api_github, api_json, web_scrape) or ensure file is an MSI installer.
- (Future) EXE files: Auto-detect and extract FileVersion from PE headers
Use Cases:
- Google Chrome: Fixed enterprise MSI URL, version embedded in MSI
- Mozilla Firefox: Fixed enterprise MSI URL, version embedded in MSI
- Vendors with stable download URLs and embedded version metadata
- When version not available via API, URL pattern, or GitHub tags
Recipe Configuration:
source:
strategy: url_download
url: "https://vendor.com/installer.msi" # Required: download URL
Configuration Fields:
- url (str, required): HTTP(S) URL to download the installer from. The URL should be stable and point to the latest version.
Version Extraction: Automatically detected by file extension. MSI files
(.msi extension) have versions extracted from ProductVersion property.
Other file types are not supported for version extraction - use a
version-first strategy (api_github, api_json, web_scrape) instead.
Error Handling:
- ConfigError: Missing or invalid configuration fields
- NetworkError: Download failures, version extraction errors
- Errors are chained with 'from err' for better debugging
Example
In a recipe YAML:
apps:
- name: "My App"
id: "my-app"
source:
strategy: url_download
url: "https://example.com/myapp-setup.msi"
From Python:
from pathlib import Path
from napt.discovery.url_download import UrlDownloadStrategy
strategy = UrlDownloadStrategy()
app_config = {
"source": {
"url": "https://example.com/app.msi",
}
}
# With cache for ETag optimization
cache = {"etag": 'W/"abc123"', "sha256": "..."}
discovered, file_path, sha256, headers = strategy.discover_version(
app_config, Path("./downloads"), cache=cache
)
print(f"Version {discovered.version} at {file_path}")
From Python (using core orchestration):
from pathlib import Path
from napt.core import discover_recipe
# Automatically uses ETag optimization
result = discover_recipe(Path("recipe.yaml"), Path("./downloads"))
print(f"Version {result.version} at {result.file_path}")
Note
- Must download file to extract version (architectural constraint)
- ETag optimization reduces bandwidth but still requires network round-trip
- Core orchestration automatically provides cached ETag if available
- Server must support ETag or Last-Modified headers for optimization
- If server doesn't support conditional requests, full download occurs every time
- Consider version-first strategies (web_scrape, api_github, api_json) for better performance when version available via web scraping or API
UrlDownloadStrategy
Discovery strategy for static HTTP(S) URLs.
Configuration example
source: strategy: url_download url: "https://example.com/installer.msi"
Source code in napt/discovery/url_download.py
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 | |
discover_version
discover_version(app_config: dict[str, Any], output_dir: Path, cache: dict[str, Any] | None = None) -> tuple[DiscoveredVersion, Path, str, dict]
Download from static URL and extract version from the file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app_config
|
dict[str, Any]
|
App configuration containing source.url and source.version. |
required |
output_dir
|
Path
|
Directory to save the downloaded file. |
required |
cache
|
dict[str, Any] | None
|
Cached state with etag, last_modified, file_path, and sha256 for conditional requests. If provided and file is unchanged (HTTP 304), the cached file is returned. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[DiscoveredVersion, Path, str, dict]
|
A tuple (version_info, file_path, sha256, headers), where version_info contains the discovered version information, file_path is the Path to the downloaded file, sha256 is the SHA-256 hash, and headers contains HTTP response headers. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If required config fields are missing or invalid. |
NetworkError
|
If download or version extraction fails. |
Source code in napt/discovery/url_download.py
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | |
validate_config
Validate url_download strategy configuration.
Checks for required fields and correct types without making network calls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app_config
|
dict[str, Any]
|
The app configuration from the recipe. |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
List of error messages (empty if valid). |
Source code in napt/discovery/url_download.py
napt.discovery.web_scrape
Web scraping discovery strategy for NAPT.
This is a VERSION-FIRST strategy that scrapes vendor download pages to find download links and extract version information from those links. This enables version discovery for vendors that don't provide APIs or static URLs.
Key Advantages:
- Discovers versions from vendor download pages
- Works for vendors without APIs or GitHub releases
- Version-first caching (can skip downloads when version unchanged)
- Supports both CSS selectors (recommended) and regex (fallback)
- No dependency on HTML structure stability (with good selectors)
- Handles relative and absolute URLs automatically
Supported Link Finding:
- CSS selectors: Modern, robust, recommended approach
- Regex patterns: Fallback for edge cases or when CSS won't work
Version Extraction:
- Extract version from the discovered download URL using regex
- Support for captured groups with formatting
- Transform version numbers (e.g., "2501" -> "25.01")
Use Cases:
- Vendors with download pages listing multiple versions (7-Zip, etc.)
- Legacy software without modern APIs
- Small vendors with simple download pages
- When GitHub releases and JSON APIs aren't available
Recipe Configuration
Alternative with regex
Configuration Fields:
- page_url (str, required): URL of the page to scrape for download links
- link_selector (str, optional): CSS selector to find download link. Recommended approach. Example: 'a[href$=".msi"]' finds links ending with .msi
- link_pattern (str, optional): Regex pattern as fallback when CSS won't work. Must have one capture group for the URL. Example: 'href="([^"]*.msi)"'
- version_pattern (str, required): Regex pattern to extract version from the discovered URL. Use capture groups to extract version parts. Example: "app-(\d+.\d+)" or "7z(\d{2})(\d{2})"
- version_format (str, optional): Python format string to combine captured groups. Use {0}, {1}, etc. for groups. Example: "{0}.{1}" transforms captures "25", "01" into "25.01". Defaults to "{0}" (first capture group only).
Error Handling:
- ValueError: Missing or invalid configuration fields
- RuntimeError: Page download failures, selector/pattern not found
- Errors are chained with 'from err' for better debugging
Finding CSS Selectors:
Use browser DevTools:
1. Open download page in Chrome/Edge/Firefox
2. Right-click download link -> Inspect
3. Right-click highlighted element -> Copy -> Copy selector
4. Simplify selector (e.g., 'a[href$=".msi"]' instead of complex nth-child)
Common CSS Patterns:
- 'a[href$=".msi"]' - Links ending with .msi
- 'a[href*="x64"]' - Links containing "x64"
- 'a.download' - Links with class="download"
- 'a[href$="-x64.msi"]:first-of-type' - First matching link
Example
In a recipe YAML:
apps:
- name: "7-Zip"
id: "napt-7zip"
source:
strategy: web_scrape
page_url: "https://www.7-zip.org/download.html"
link_selector: 'a[href$="-x64.msi"]'
version_pattern: "7z(\d{2})(\d{2})-x64"
version_format: "{0}.{1}"
From Python (version-first approach):
from napt.discovery.web_scrape import WebScrapeStrategy
from napt.download import download_file
strategy = WebScrapeStrategy()
app_config = {
"source": {
"page_url": "https://www.7-zip.org/download.html",
"link_selector": 'a[href$="-x64.msi"]',
"version_pattern": "7z(\d{2})(\d{2})-x64",
"version_format": "{0}.{1}",
}
}
# Get version WITHOUT downloading installer
version_info = strategy.get_version_info(app_config)
print(f"Latest version: {version_info.version}")
# Download only if needed
if need_to_download:
result = download_file(
version_info.download_url, Path("./downloads/my-app")
)
print(f"Downloaded to {result.file_path}")
From Python (using core orchestration):
Note
- Version discovery via web scraping (no installer download required)
- Core orchestration automatically skips download if version unchanged
- CSS selectors are recommended (more robust than regex)
- Use browser DevTools to find selectors easily
- Selector should match exactly one link (first match is used)
- BeautifulSoup4 required for CSS selectors
- Regex fallback works without BeautifulSoup
WebScrapeStrategy
Discovery strategy for web scraping download pages.
Configuration example
Source code in napt/discovery/web_scrape.py
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 | |
get_version_info
Scrape download page for version and URL without downloading (version-first path).
This method scrapes an HTML page, finds a download link using CSS selector or regex, extracts the version from that link, and returns version info. If the version matches cached state, the download can be skipped entirely.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app_config
|
dict[str, Any]
|
App configuration containing source.page_url, source.link_selector or source.link_pattern, and source.version_pattern. |
required |
Returns:
| Type | Description |
|---|---|
VersionInfo
|
Version info with version string, download URL, and source name. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If required config fields are missing, invalid, or if selectors/patterns don't match anything. |
RuntimeError
|
If page download fails (chained with 'from err'). |
Example
Scrape 7-Zip download page:
strategy = WebScrapeStrategy()
config = {
"source": {
"page_url": "https://www.7-zip.org/download.html",
"link_selector": 'a[href$="-x64.msi"]',
"version_pattern": "7z(\d{2})(\d{2})-x64",
"version_format": "{0}.{1}"
}
}
version_info = strategy.get_version_info(config)
# version_info.version returns: '25.01'
Source code in napt/discovery/web_scrape.py
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 | |
validate_config
Validate web_scrape strategy configuration.
Checks for required fields and correct types without making network calls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app_config
|
dict[str, Any]
|
The app configuration from the recipe. |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
List of error messages (empty if valid). |
Source code in napt/discovery/web_scrape.py
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 | |
napt.discovery.api_github
GitHub API discovery strategy for NAPT.
This is a VERSION-FIRST strategy that queries the GitHub API to get version and download URL WITHOUT downloading the installer. This enables fast version checks and efficient caching.
Key Advantages:
- Fast version discovery (GitHub API call ~100ms)
- Can skip downloads entirely when version unchanged
- Direct access to latest releases via stable GitHub API
- Version extraction from Git tags (semantic versioning friendly)
- Asset pattern matching for multi-platform releases
- Optional authentication for higher rate limits
- No web scraping required
- Ideal for CI/CD with scheduled checks
Supported Version Extraction:
- Tag-based: Extract version from release tag names
- Supports named capture groups: (?P
...) - Default pattern strips "v" prefix: v1.2.3 -> 1.2.3
- Falls back to full tag if no pattern match
- Supports named capture groups: (?P
Use Cases:
- Open-source projects (Git, VS Code, Node.js, etc.)
- Projects with GitHub releases (Firefox, Chrome alternatives)
- Vendors who publish installers as release assets
- Projects with semantic versioned tags
- CI/CD pipelines with frequent version checks
Recipe Configuration
source:
strategy: api_github
repo: "git-for-windows/git" # Required: owner/repo
asset_pattern: "Git-.*-64-bit\.exe$" # Required: regex for asset
version_pattern: "v?([0-9.]+)" # Optional: version extraction
prerelease: false # Optional: include prereleases
token: "${GITHUB_TOKEN}" # Optional: auth token
Configuration Fields:
- repo (str, required): GitHub repository in "owner/name" format (e.g., "git-for-windows/git")
- asset_pattern (str, required): Regular expression to match asset filename. If multiple assets match, the first match is used. Example: ".*-x64.msi$" matches assets ending with "-x64.msi"
- version_pattern (str, optional): Regular expression to extract version
from the release tag name. Use a named capture group (?P
...) or the entire match. Default: "v?([0-9.]+)" strips optional "v" prefix. Example: "release-([0-9.]+)" for tags like "release-1.2.3". - prerelease (bool, optional): If True, include pre-release versions. If False (default), only stable releases are considered. Uses GitHub's prerelease flag.
- token (str, optional): GitHub personal access token for authentication. Increases rate limit from 60 to 5000 requests per hour. Can use environment variable substitution: "${GITHUB_TOKEN}". No special permissions needed for public repositories.
Error Handling:
- ValueError: Missing or invalid configuration fields
- RuntimeError: API failures, no releases, no matching assets
- Errors are chained with 'from err' for better debugging
Rate Limits:
- Unauthenticated: 60 requests/hour per IP
- Authenticated: 5000 requests/hour per token
- Tip: Use a token for production use or frequent checks
Example
In a recipe YAML:
apps:
- name: "Git for Windows"
id: "git"
source:
strategy: api_github
repo: "git-for-windows/git"
asset_pattern: "Git-.*-64-bit\.exe$"
From Python (version-first approach):
from napt.discovery.api_github import ApiGithubStrategy
from napt.download import download_file
strategy = ApiGithubStrategy()
app_config = {
"source": {
"repo": "git-for-windows/git",
"asset_pattern": ".*-64-bit\.exe$",
}
}
# Get version WITHOUT downloading
version_info = strategy.get_version_info(app_config)
print(f"Latest version: {version_info.version}")
# Download only if needed
if need_to_download:
result = download_file(
version_info.download_url, Path("./downloads/my-app")
)
print(f"Downloaded to {result.file_path}")
From Python (using core orchestration):
Note
Version discovery via API only (no download required). Core orchestration automatically skips download if version unchanged. The GitHub API is stable and well-documented. Releases are fetched in order (latest first). Asset matching is case-sensitive by default (use (?i) for case-insensitive). Consider url_download if you need a direct download URL instead.
ApiGithubStrategy
Discovery strategy for GitHub releases.
Configuration example
source: strategy: api_github repo: "owner/repository" asset_pattern: ".*.msi$" version_pattern: "v?([0-9.]+)" prerelease: false token: "${GITHUB_TOKEN}"
Source code in napt/discovery/api_github.py
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 | |
get_version_info
Fetch latest release from GitHub API without downloading (version-first path).
This method queries the GitHub API for the latest release and extracts the version from the tag name and the download URL from matching assets. If the version matches cached state, the download can be skipped entirely.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app_config
|
dict[str, Any]
|
App configuration containing source.repo and optional fields. |
required |
Returns:
| Type | Description |
|---|---|
VersionInfo
|
Version info with version string, download URL, and source name. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If required config fields are missing, invalid, or if no matching assets are found. |
RuntimeError
|
If API call fails or release has no assets. |
Example
Get version from GitHub releases:
Source code in napt/discovery/api_github.py
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | |
validate_config
Validate api_github strategy configuration.
Checks for required fields and correct types without making network calls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app_config
|
dict[str, Any]
|
The app configuration from the recipe. |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
List of error messages (empty if valid). |
Source code in napt/discovery/api_github.py
napt.discovery.api_json
JSON API discovery strategy for NAPT.
This is a VERSION-FIRST strategy that queries JSON API endpoints to get version and download URL WITHOUT downloading the installer. This enables fast version checks and efficient caching.
Key Advantages:
- Fast version discovery (API call ~100ms)
- Can skip downloads entirely when version unchanged
- Direct API access for version and download URL
- Support for complex JSON structures with JSONPath
- Custom headers for authentication
- Support for GET and POST requests
- No file parsing required
- Ideal for CI/CD with scheduled checks
Supported Features:
- JSONPath navigation for nested structures
- Array indexing and filtering
- Custom HTTP headers (Authorization, etc.)
- POST requests with JSON body
- Environment variable expansion in values
Use Cases:
- Vendors with JSON APIs (Microsoft, Mozilla, etc.)
- Cloud services with version endpoints
- CDNs that provide metadata APIs
- Applications with update check APIs
- APIs requiring authentication or custom headers
- CI/CD pipelines with frequent version checks
Recipe Configuration
source:
strategy: api_json
api_url: "https://vendor.com/api/latest"
version_path: "version" # JSONPath to version
download_url_path: "download_url" # JSONPath to URL
method: "GET" # Optional: GET or POST
headers: # Optional: custom headers
Authorization: "Bearer ${API_TOKEN}"
Accept: "application/json"
body: # Optional: POST body
platform: "windows"
arch: "x64"
timeout: 30 # Optional: timeout in seconds
Configuration Fields:
- api_url (str, required): API endpoint URL that returns JSON with version and download information
- version_path (str, required): JSONPath expression to extract version from the API response. Examples: "version", "release.version", "data.version"
- download_url_path (str, required): JSONPath expression to extract download URL from the API response. Examples: "download_url", "assets.url", "platforms.windows.x64"
- method (str, optional): HTTP method to use. Either "GET" or "POST". Default is "GET"
- headers (dict, optional): Custom HTTP headers to send with the request. Useful for authentication or setting Accept headers. Values support environment variable expansion. Example: {"Authorization": "Bearer ${API_TOKEN}"}
- body (dict, optional): Request body for POST requests. Sent as JSON.
Only used when method="POST". Example: {"platform": "windows", "arch": "x64"}
- timeout (int, optional): Request timeout in seconds. Default is 30.
JSONPath Syntax:
- Simple paths: "version", "release.version"
- Array indexing: "data.version", "releases.version"
- Nested paths: "data.latest.download.url", "response.assets.browser_download_url"
Error Handling:
- ValueError: Missing or invalid configuration, invalid JSONPath, path not found
- RuntimeError: API failures, invalid JSON response
- Errors are chained with 'from err' for better debugging
Example
In a recipe YAML (simple API):
apps:
- name: "My App"
id: "my-app"
source:
strategy: api_json
api_url: "https://api.vendor.com/latest"
version_path: "version"
download_url_path: "download_url"
In a recipe YAML (nested structure):
apps:
- name: "My App"
id: "my-app"
source:
strategy: api_json
api_url: "https://api.vendor.com/releases"
version_path: "stable.version"
download_url_path: "stable.platforms.windows.x64"
headers:
Authorization: "Bearer ${API_TOKEN}"
From Python (version-first approach):
from napt.discovery.api_json import ApiJsonStrategy
from napt.download import download_file
strategy = ApiJsonStrategy()
app_config = {
"source": {
"api_url": "https://api.vendor.com/latest",
"version_path": "version",
"download_url_path": "download_url",
}
}
# Get version WITHOUT downloading
version_info = strategy.get_version_info(app_config)
print(f"Latest version: {version_info.version}")
# Download only if needed
if need_to_download:
result = download_file(
version_info.download_url, Path("./downloads/my-app")
)
print(f"Downloaded to {result.file_path}")
From Python (using core orchestration):
Note
- Version discovery via API only (no download required)
- Core orchestration automatically skips download if version unchanged
- JSONPath uses jsonpath-ng library for robust parsing
- Environment variable expansion works in headers and other string values
- POST body is sent as JSON (Content-Type: application/json)
- Timeout defaults to 30 seconds to prevent hanging on slow APIs
ApiJsonStrategy
Discovery strategy for JSON API endpoints.
Configuration example
source: strategy: api_json api_url: "https://api.vendor.com/latest" version_path: "version" download_url_path: "download_url" method: "GET" headers: Authorization: "Bearer ${API_TOKEN}"
Source code in napt/discovery/api_json.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 | |
get_version_info
Query JSON API for version and download URL without downloading (version-first path).
This method calls a JSON API, extracts version and download URL using JSONPath expressions. If the version matches cached state, the download can be skipped entirely.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app_config
|
dict[str, Any]
|
App configuration containing source.api_url, source.version_path, and source.download_url_path. |
required |
Returns:
| Type | Description |
|---|---|
VersionInfo
|
Version info with version string, download URL, and source name. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If required config fields are missing, invalid, or if JSONPath expressions don't match anything in the response. |
RuntimeError
|
If API call fails (chained with 'from err'). |
Example
Get version info from JSON API:
Source code in napt/discovery/api_json.py
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 | |
validate_config
Validate api_json strategy configuration.
Checks for required fields and correct types without making network calls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app_config
|
dict[str, Any]
|
The app configuration from the recipe. |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
List of error messages (empty if valid). |