Each graph is a navigable map for agents to locate code, trace dependencies, and understand data flow. All paths relative to
src/protonfetcher/unless noted.
graph TD
cli_core["cli/core.py"] --> github_fetcher["github_fetcher.py"]
cli_core --> forgejo_fetcher["forgejo_fetcher.py"]
cli_core --> cli_dispatch["cli/dispatch.py"]
cli_core --> cli_handlers["cli/handlers.py"]
cli_core --> cli_validators["cli/validators.py"]
cli_core --> cli_argparse["cli/argparse_builder.py"]
cli_dispatch --> cli_handlers
cli_handlers --> github_fetcher
cli_handlers --> forgejo_fetcher
cli_argparse --> cli_fork["cli/fork_utils.py"]
github_fetcher --> base_fetcher["base_release_fetcher.py"]
forgejo_fetcher --> base_fetcher
base_fetcher --> release_mgr["release_manager.py"]
base_fetcher --> asset_dl["asset_downloader.py"]
base_fetcher --> archive_ext["archive_extractor.py"]
base_fetcher --> link_mgr["link_manager.py"]
base_fetcher --> net["network.py"]
base_fetcher --> fs["filesystem.py"]
base_fetcher --> adapters["platform_adapters.py"]
release_mgr --> adapters
release_mgr --> net
release_mgr --> fs
asset_dl --> net
asset_dl --> fs
asset_dl --> release_mgr
asset_dl --> spinner["spinner.py"]
note_dl["asset_downloader also uses urllib.request
for spinner-based downloads"] -.-> asset_dl
archive_ext --> fs
archive_ext --> spinner
link_mgr --> version_finder["version_finder.py"]
link_mgr --> candidate_selection["candidate_selection.py"]
link_mgr --> link_status["link_status.py"]
link_mgr --> symlink_ops["symlink_operations.py"]
link_mgr --> prune_ops["prune_operations.py"]
link_mgr --> release_ops["release_operations.py"]
link_mgr --> fs
adapters --> common_ad["common.py"]
net --> common_net["common.py"]
common["common.py"] -.-> cli_core
common -.-> base_fetcher
common -.-> release_mgr
common -.-> asset_dl
common -.-> archive_ext
common -.-> link_mgr
common -.-> adapters
common -.-> net
common -.-> fs
exceptions["exceptions.py"] -.-> base_fetcher
exceptions -.-> release_mgr
exceptions -.-> asset_dl
exceptions -.-> archive_ext
exceptions -.-> link_mgr
utils["utils.py"] -.-> base_fetcher
utils -.-> link_mgr
utils -.-> release_mgr
utils -.-> spinner
utils -.-> adapters
class common fill:#d4edda
class exceptions fill:#f8d7da
class utils fill:#d1ecf1
Legend: --> = direct import, -.-> = shared dependency (common/exceptions/utils imported by many)
Notes on actual imports:
cli/core.pyinstantiatesGitHubReleaseFetcherandForgejoReleaseFetcherdirectly (no import ofbase_release_fetcher)release_manager.pyimportsgithub_adapterfromplatform_adaptersdirectly (not via base_fetcher)asset_downloader.pyusesurllib.request.urlopen()for spinner-based downloads (bypassesNetworkClient)spinner.pyimportsformat_ratefromutils.pynetwork.pyimportsHeaders,ProcessResultfromcommon.pyarchive_extractor.pyusessubprocessandtarfilefrom stdliblink_manager.pyis a thin facade — delegates to 6 extracted modules (version_finder,candidate_selection,link_status,symlink_operations,prune_operations,release_operations)resolve_directory()andresolve_directory_candidates()are module-level functions inlink_manager.py(not class methods)
graph TD
L6["**Layer 6 — Interface**<br/>cli/core.py · cli/dispatch.py · cli/handlers.py<br/>cli/validators.py · cli/argparse_builder.py · cli/fork_utils.py"]
L5["**Layer 5 — Markers**<br/>github_fetcher.py · forgejo_fetcher.py"]
L4["**Layer 4 — Orchestrator**<br/>base_release_fetcher.py"]
L3["**Layer 3 — Components**<br/>release_manager.py · asset_downloader.py · archive_extractor.py · link_manager.py"]
L3a["**Layer 3a — Extracted**<br/>version_finder.py · candidate_selection.py · link_status.py<br/>symlink_operations.py · prune_operations.py · release_operations.py"]
L3b["**Layer 3b — Progress**<br/>spinner.py"]
L2["**Layer 2 — Adapters**<br/>platform_adapters.py (github_adapter · forgejo_adapter)"]
L1["**Layer 1 — Clients**<br/>network.py · filesystem.py"]
L0["**Layer 0 — Data**<br/>common.py · exceptions.py · utils.py · __version__.py"]
L6 --> L5
L5 --> L4
L4 --> L3
L4 --> L3b
L3 --> L3a
L3 --> L2
L3 --> L3b
L3a --> L0
L2 --> L1
L1 --> L0
L3 --> L0
L3b --> L0
L4 --> L0
class L0 fill:#e8f5e9,stroke:#4caf50
class L1 fill:#e3f2fd,stroke:#2196f3
class L2 fill:#fff3e0,stroke:#ff9800
class L3 fill:#f3e5f5,stroke:#9c27b0
class L3a fill:#e1f5fe,stroke:#00bcd4
class L3b fill:#e1f5fe,stroke:#00bcd4
class L4 fill:#fce4ec,stroke:#e91e63
class L5 fill:#fff9c4,stroke:#fdd835
class L6 fill:#e0f7fa,stroke:#00bcd4
Cross-layer notes:
release_managerimportsplatform_adaptersdirectly (not viabase_release_fetcher)asset_downloadercallsrelease_manager.get_remote_asset_size()(reverse dependency for size checks)asset_downloaderusesurllib.requestdirectly for spinner downloads (bypassesnetwork.py)spinnerimportsformat_ratefromutilslink_managerdelegates to Layer 3a extracted modules — it is a thin orchestration facade
classDiagram
class BaseReleaseFetcher {
+str platform
+int timeout
+NetworkClient network_client
+FileSystemClient file_system_client
+ReleaseManager release_manager
+AssetDownloader asset_downloader
+ArchiveExtractor archive_extractor
+LinkManager link_manager
+fetch_and_extract(repo, output_dir, extract_dir, release_tag, fork, show_progress, show_file_details, dry_run) Path|None
+list_links(extract_dir, fork) dict
+remove_release(extract_dir, tag, fork) bool
+prune_releases(extract_dir, fork, keep=1, dry_run=False) tuple
+relink_fork(extract_dir, fork) bool
+update_all_managed_forks(output_dir, extract_dir, dry_run) dict
+check_for_updates(extract_dir, fork) str|None
+fetch_latest_tag(repo) str
+find_asset_by_name(repo, tag, fork) str|None
+get_remote_asset_size(repo, tag, asset_name) int
+list_recent_releases(repo) list
+_validate_environment() void
+_ensure_directories_writable(output_dir, extract_dir) void
+_ensure_directory_is_writable(directory) void
+_determine_release_tag(repo, release_tag) str
+_get_expected_directories(extract_dir, release_tag, fork) tuple
+_check_existing_directory(unpacked, alternative, fork) tuple
+_check_post_download_directory(extract_dir, release_tag, fork, is_manual_release) tuple
+_handle_existing_directory(extract_dir, release_tag, fork, actual_directory, is_manual_release) tuple
+_handle_already_extracted(extract_dir, tag, fork, directory, is_manual_release) tuple
+_download_asset(repo, release_tag, fork, output_dir) Path
+_dry_run_workflow(repo, output_dir, extract_dir, release_tag, fork, is_manual_release) None
+_extract_and_manage_links(archive_path, extract_dir, release_tag, fork, is_manual_release, show_progress, show_file_details) Path
+_find_extracted_directory(extract_dir, release_tag, fork) Path
+_build_download_url(repo, tag, asset_name) str
}
class GitHubReleaseFetcher {
+platform = "github"
}
class ForgejoReleaseFetcher {
+platform = "forgejo"
}
BaseReleaseFetcher <|-- GitHubReleaseFetcher
BaseReleaseFetcher <|-- ForgejoReleaseFetcher
note for BaseReleaseFetcher "Concrete class — ALL logic here.\nNo abstract methods.\n__init__ selects adapter from\nself.platform attribute."
note for GitHubReleaseFetcher "Marker subclass — zero overrides.\nplatform='github' selects github_adapter."
note for ForgejoReleaseFetcher "Marker subclass — zero overrides.\nplatform='forgejo' selects forgejo_adapter."
classDiagram
PlatformAdapter o-- GitHubPlatformAdapter
PlatformAdapter o-- ForgejoPlatformAdapter
class PlatformAdapter {
<<Protocol in common.py>>
+str api_base
+str host_base
+build_api_url(repo, *parts) str
+build_download_url(repo, tag, asset_name) str
+build_host_url(repo, *parts) str
+dict default_headers
}
class GitHubPlatformAdapter {
<<Concrete class>>
api_base = "https://api.github.com"
host_base = "https://github.com"
headers = {Accept: vnd.github.v3+json, User-Agent: ...}
}
class ForgejoPlatformAdapter {
<<Concrete class>>
api_base = "https://dawn.wine/api/v1"
host_base = "https://dawn.wine"
headers = {Accept: application/json, User-Agent: ...}
}
note for PlatformAdapter "Protocol defined in common.py.\nConcrete classes in platform_adapters.py\nuse duck typing (no explicit inheritance).\nSingletons exported:\ngithub_adapter · forgejo_adapter\nSelection in BaseReleaseFetcher.__init__\nby self.platform attribute."
Selection flow:
BaseReleaseFetcher.__init__readsself.platform(set by subclass)- Chooses
github_adapterifplatform == "github", elseforgejo_adapter - Adapter is passed to
ReleaseManagerconstructor ReleaseManageruses adapter for all URL construction
sequenceDiagram
participant CLI as cli.py
participant Fetcher as BaseReleaseFetcher
participant RM as ReleaseManager
participant AD as AssetDownloader
participant AE as ArchiveExtractor
participant LM as LinkManager
participant PA as PlatformAdapter
CLI->>Fetcher: fetch_and_extract(repo, output_dir, extract_dir, release_tag, fork, dry_run)
Fetcher->>Fetcher: _validate_environment()
Fetcher->>Fetcher: _ensure_directories_writable(output_dir, extract_dir)
Fetcher->>Fetcher: _determine_release_tag(repo, release_tag)
Fetcher->>RM: fetch_latest_tag()
RM->>PA: build_host_url(repo, "releases", "latest")
PA-->>RM: URL for tag discovery
RM-->>Fetcher: tag
Fetcher->>Fetcher: _get_expected_directories(extract_dir, release_tag, fork)
Note over Fetcher: resolve_directory_candidates() from link_manager module-level
Fetcher->>Fetcher: _check_existing_directory(unpacked, alternative, fork)
alt already extracted
Fetcher->>Fetcher: _handle_existing_directory()
Fetcher->>LM: are_links_up_to_date()
alt links stale
Fetcher->>LM: manage_proton_links()
end
Fetcher-->>CLI: existing Path
end
Fetcher->>RM: find_asset_by_name(repo, tag, fork)
RM->>RM: _try_api_approach() (primary)
RM->>PA: build_api_url(repo, "releases", "tags", tag)
alt API fails
RM->>RM: _try_html_fallback()
RM->>PA: build_host_url(repo, "releases", "tag", tag)
end
RM-->>Fetcher: asset name
Fetcher->>PA: build_download_url(repo, tag, asset_name)
Fetcher->>AD: download_asset(repo, tag, asset_name, out_path, release_manager, download_url)
AD->>RM: get_remote_asset_size() (skip if local matches)
AD->>AD: download_with_spinner() (urllib.request)
AD-->>Fetcher: downloaded Path
Fetcher->>Fetcher: _check_post_download_directory()
alt directory appeared during download
Fetcher->>Fetcher: _handle_already_extracted()
Fetcher-->>CLI: Path
end
Fetcher->>AE: extract_archive(archive_path, extract_dir, show_progress, show_file_details)
AE-->>Fetcher: extracted Path
Fetcher->>LM: manage_proton_links(extract_dir, tag, fork, is_manual_release)
Note over LM: find_version_candidates() → resolve_directory() → symlink CRUD
Fetcher-->>CLI: Path to extracted dir
Dry-run path: If dry_run=True, _dry_run_workflow() is called instead of download/extract. It resolves asset info, shows what would be downloaded/extracted/linked, and returns None.
Multi-fork path: update_all_managed_forks() iterates FORKS filtered by self.platform, skips forks without managed links, and calls fetch_and_extract() for each.
sequenceDiagram
participant CLI as cli.py
participant GH as GitHubReleaseFetcher
participant FJ as ForgejoReleaseFetcher
participant LM as LinkManager
CLI->>CLI: -f without value → multi-fork
CLI->>GH: update_all_managed_forks()
CLI->>FJ: update_all_managed_forks()
loop each fork on platform (GH: GE-Proton, Proton-EM, CachyOS)
GH->>LM: has_managed_links(fork)
alt managed links exist
GH->>GH: fetch_and_extract(fork)
end
end
loop each fork on platform (FJ: DW-Proton)
FJ->>LM: has_managed_links(fork)
alt managed links exist
FJ->>FJ: fetch_and_extract(fork)
end
end
CLI-->>CLI: dict[ForkName, Path|None]
graph TD
ROOT["CLI Entry\ncli/core.py::main()"]
ROOT --> DEFAULT["default (no flags)\n→ ls all forks' links"]
ROOT --> FETCH["--fork / -f VALUE\nfetch_and_extract()"]
ROOT --> MULTI["-f (no value)\nupdate_all_managed_forks()"]
ROOT --> LIST["--list / -l\nlist_recent_releases()"]
ROOT --> LS["--ls\nlist_links()"]
ROOT --> RM["--rm TAG\nremove release + symlinks\n--rm --fork\nremove all fork symlinks"]
ROOT --> RELINK["--relink\nrelink_fork() (requires --fork)"]
ROOT --> PRUNE["--prune\nprune_releases()\n--keep N (default: 1)"]
ROOT --> CHECK["--check / -c\ncheck_for_updates()"]
ROOT --> DRY["--dry-run / -n\ndry_run=True"]
ROOT --> VERSION["--version / -V\nprint version, exit"]
ROOT --> RELEASE["--release / -r\nmanual tag (mutually exclusive with -l/-r)"]
FETCH --> GH["platform=github → GitHubReleaseFetcher"]
FETCH --> FJ["platform=forgejo → ForgejoReleaseFetcher"]
MULTI --> GH2["GitHubReleaseFetcher.update_all_managed_forks()"]
MULTI --> FJ2["ForgejoReleaseFetcher.update_all_managed_forks()"]
class DEFAULT fill:#e8f5e9
class FETCH fill:#e3f2fd
class MULTI fill:#fff3e0
class LIST fill:#f3e5f5
class LS fill:#e0f7fa
class RM fill:#ffebee
class RELINK fill:#fce4ec
class PRUNE fill:#fff9c4
class CHECK fill:#e1f5fe
class DRY fill:#f1f8e9
class VERSION fill:#e0f7fa
class RELEASE fill:#fff9c4
Default behavior (no flags): Calls _handle_ls_operation with list_all_forks=True — lists links for ALL forks.
Dispatch logic in cli/dispatch.py::dispatch(): Operations are resolved by checking args flags in priority order: ls → list → relink → rm → prune → check. If none match, falls through to _resolve_default_operation() which checks for explicit --fork/--release flags or defaults to listing all forks' links.
Validation: cli/validators.py::validate_mutually_exclusive_args() enforces: --check vs --dry-run, --check vs --list/--ls, --prune vs --check, --keep >= 1, --dry-run vs read-only ops, --relink requires --fork. --rm is no longer mutually exclusive with other operations.
graph LR
FC["ForkConfig (common.py)\nSingle source of truth"]
GEP["GE-Proton"]
PEM["Proton-EM"]
CCH["CachyOS"]
DWP["DW-Proton"]
FC --> GEP
FC --> PEM
FC --> CCH
FC --> DWP
subgraph GitHub ["platform = github"]
GEP
PEM
CCH
end
subgraph Forgejo ["platform = forgejo"]
DWP
end
click GEP "https://github.com/GloriousEggroll/proton-ge-custom"
click PEM "https://github.com/Etaash-mathamsetty/Proton"
click CCH "https://github.com/CachyOS/proton-cachyos"
click DWP "https://dawn.wine/dawn-winery/dwproton"
classDef github fill:#24292e,stroke:#333,color:#fff
classDef forgejo fill:#ffcc00,stroke:#333,color:#000
class GEP,PEM,CCH github
class DWP forgejo
ForkConfig fields used by each component:
| Field | Used By |
|---|---|
repo |
ReleaseManager, PlatformAdapter |
archive_format |
ReleaseManager (extension matching) |
api_base / host_base |
ForkConfig override (DW-Proton), PlatformAdapter |
version_pattern / version_prefix |
utils.parse_version(), LinkManager |
is_ge_proton |
utils.parse_version() (GE-Proton tuple mapping) |
link_names |
LinkManager.manage_proton_links() |
skip_prefixes |
LinkManager._should_skip_directory() |
asset_template |
utils.get_proton_asset_name(), ReleaseManager |
dir_name_templates |
resolve_directory(), resolve_directory_candidates() |
platform |
BaseReleaseFetcher → adapter selection |
ForkName enum values: GE_PROTON, PROTON_EM, CACHYOS, DW_PROTON
DEFAULT_FORK: ForkName.GE_PROTON (used when --fork not specified and operation requires a single fork)
graph TD
PF["ProtonFetcherError\n(base, alias: FetchError)"]
NE["NetworkError\nHTTP errors, timeouts, rate limits"]
EE["ExtractionError\ncorrupted archives, disk space"]
LME["LinkManagementError\npermissions, broken symlinks"]
MLME["MultiLinkManagementError\nExceptionGroup for batch ops"]
PF --> NE
PF --> EE
PF --> LME
PF --> MLME
class PF fill:#f8d7da,stroke:#dc3545,color:#fff
class NE fill:#fff3cd,stroke:#ffc107
class EE fill:#d1ecf1,stroke:#0c5460
class LME fill:#d4edda,stroke:#155724
class MLME fill:#e2e3e5,stroke:#383d41
graph TD
NCP["NetworkClientProtocol v1.0"]
FSP["FileSystemClientProtocol v1.0"]
PA["PlatformAdapter Protocol"]
NCP --> GET["get(url, headers, stream)"]
NCP --> HEAD["head(url, headers, follow_redirects)"]
NCP --> DL["download(url, output_path, headers)"]
FSP --> EXISTS["exists(path)"]
FSP --> ISDIR["is_dir(path)"]
FSP --> ISLINK["is_symlink(path)"]
FSP --> MKDIR["mkdir(path, parents, exist_ok)"]
FSP --> WRITE["write(path, data)"]
FSP --> READ["read(path)"]
FSP --> SIZE["size(path)"]
FSP --> MTIME["mtime(path)"]
FSP --> SYMLINK["symlink_to(link, target)"]
FSP --> RESOLVE["resolve(path)"]
FSP --> UNLINK["unlink(path)"]
FSP --> RMR["rmtree(path)"]
FSP --> ITER["iterdir(path)"]
PA --> APIURL["build_api_url(repo, *parts)"]
PA --> DLURL["build_download_url(repo, tag, asset)"]
PA --> HOSTURL["build_host_url(repo, *parts)"]
PA --> HDRS["default_headers"]
NET["NetworkClient\n(curl-based)"] -.-> NCP
FSC["FileSystemClient\n(pathlib-based)"] -.-> FSP
GHA["GitHubPlatformAdapter"] -.-> PA
FJA["ForgejoPlatformAdapter"] -.-> PA
class NCP fill:#e3f2fd,stroke:#2196f3
class FSP fill:#e8f5e9,stroke:#4caf50
class PA fill:#fff3e0,stroke:#ff9800
graph LR
conftest["conftest.py\nshared fixtures"]
test_fetcher["test_base_release_fetcher.py"]
test_cli["test_cli.py"]
test_cli_dispatch["test_cli_dispatch.py"]
test_cli_handlers["test_cli_handlers.py"]
test_cli_validators["test_cli_validators.py"]
test_extract["test_extraction.py"]
test_github["test_github_fetcher.py"]
test_forgejo["test_forgejo_fetcher.py"]
test_integration["test_integration.py"]
test_links["test_link_manager_e2e.py"]
test_link_status["test_link_status.py"]
test_prune["test_prune.py"]
test_prune_ops["test_prune_operations.py"]
test_rm_e2e["test_release_manager_e2e.py"]
test_release_ops["test_release_operations.py"]
test_symlink_ops["test_symlink_operations.py"]
test_utils["test_utils.py"]
test_version["test_version_finder.py"]
conftest -.-> test_fetcher
conftest -.-> test_cli
conftest -.-> test_cli_dispatch
conftest -.-> test_cli_handlers
conftest -.-> test_cli_validators
conftest -.-> test_extract
conftest -.-> test_github
conftest -.-> test_forgejo
conftest -.-> test_integration
conftest -.-> test_links
conftest -.-> test_link_status
conftest -.-> test_prune
conftest -.-> test_prune_ops
conftest -.-> test_rm_e2e
conftest -.-> test_release_ops
conftest -.-> test_symlink_ops
conftest -.-> test_utils
conftest -.-> test_version
class test_fetcher fill:#fce4ec
class test_cli fill:#e0f7fa
class test_cli_dispatch fill:#e0f7fa
class test_cli_handlers fill:#e0f7fa
class test_cli_validators fill:#e0f7fa
class test_extract fill:#e8f5e9
class test_github fill:#e3f2fd
class test_forgejo fill:#fff3e0
class test_integration fill:#f3e5f5
class test_links fill:#e1f5fe
class test_link_status fill:#e1f5fe
class test_prune fill:#fff9c4
class test_prune_ops fill:#fff9c4
class test_rm_e2e fill:#ffebee
class test_release_ops fill:#ffebee
class test_symlink_ops fill:#e1f5fe
class test_utils fill:#f1f8e9
class test_version fill:#f1f8e9
| Question | File | Key Symbol |
|---|---|---|
| Add a new fork? | common.py |
ForkConfig, FORKS dict, ForkName enum |
| Add a new platform? | platform_adapters.py + new fetcher marker |
PlatformAdapter protocol, singleton |
| Change URL construction? | platform_adapters.py |
build_api_url, build_download_url |
| Change download logic? | asset_downloader.py |
download_asset(), download_with_spinner() |
| Change extraction? | archive_extractor.py |
extract_archive(), extract_gz_archive() |
| Change symlink behavior? | symlink_operations.py |
create_symlinks(), cleanup_unwanted_links() |
| Directory resolution (tag → path)? | link_manager.py + version_finder.py |
resolve_directory(), resolve_directory_candidates(), find_version_candidates() |
| Change CLI flags? | cli/argparse_builder.py |
build_parser(), parse_args() |
| Change CLI dispatch? | cli/dispatch.py + cli/handlers.py |
dispatch(), handle_*() functions |
| Change CLI validation? | cli/validators.py |
validate_mutually_exclusive_args(), set_default_fork() |
| Change version parsing? | utils.py + common.py |
parse_version(), ForkConfig.version_pattern |
| Change caching? | release_manager.py |
_cache_* methods, XDG path |
| Change progress display? | spinner.py |
Spinner class, format_progress_bar(), build_display_line() |
| Change error types? | exceptions.py |
ProtonFetcherError hierarchy |
| Wire up a new operation? | cli/handlers.py + cli/dispatch.py |
Add handler, register in dispatch() |
| Network calls? | network.py |
NetworkClient (curl subprocess) |
| Filesystem abstraction? | filesystem.py |
FileSystemClient (pathlib wrapper) |
| Version string? | __version__.py |
__version__, _get_version() |
| Asset discovery (API → HTML fallback)? | release_manager.py |
_try_api_approach(), _try_html_fallback() |
| Multi-fork update loop? | base_release_fetcher.py |
update_all_managed_forks() |
| Dry-run logic? | base_release_fetcher.py |
_dry_run_workflow() |
| Pruning logic? | prune_operations.py |
prune_releases(), compute_prune_plan() (symlinks are protected as keep-eligible) |
| Update checking? | base_release_fetcher.py + release_manager.py |
check_for_updates(), check_for_newer_release() |