Skip to content

fix(iProX Aspera): batched --file-list form (match working ascp); password (env/prompt) or --aspera-key; no batch hang#117

Open
ypriverol wants to merge 3 commits into
devfrom
fix/iprox-aspera-key-auth
Open

fix(iProX Aspera): batched --file-list form (match working ascp); password (env/prompt) or --aspera-key; no batch hang#117
ypriverol wants to merge 3 commits into
devfrom
fix/iprox-aspera-key-auth

Conversation

@ypriverol

@ypriverol ypriverol commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Why

The iProX Aspera support merged in #115 didn't work: it authenticated with a password via ASPERA_SCP_PASS and used the user@host:/data/iprox/<path> dest form, one ascp per file — iProX rejects that. A verified-working manual invocation is the batched --file-list / --mode recv --host --user form:

ascp -T -l 500M -P33001 -k 1 [-i <key>] --mode recv --host download.iprox.org --file-list file_list.txt --user <user> <dest>

where each file-list line is the remote path == urlparse(url).path of our records, e.g. /IPX0003474000/IPX0003474001/<file>.raw (no /data/iprox prefix). The invocation form was the bug, not the auth method — password auth works fine with this form (confirmed end-to-end against download.iprox.org).

What changed

  • IproxProvider.aspera_download rewritten to the working batched --file-list form (one ascp session for the whole dataset; FASP parallelizes internally). Temp file-list always cleaned up.
  • Auth (fail-fast, no hang):
    • Default password via the IPROX_ASPERA_PASSWORD env var (→ ASPERA_SCP_PASS), or a hidden interactive prompt when on a TTY. No plaintext password CLI flag (per the earlier security review).
    • --aspera-key <path> optional alternative (-i key) for users with a registered key.
    • If neither credential is available in a non-interactive run → clear ValueError (does not fall through to ascp's interactive Password: prompt).
  • ascp runs with stdin=subprocess.DEVNULL so a missing/rejected credential errors immediately instead of blocking on Password: — critical for sbatch jobs.
  • Password never appears on argv; not logged.
  • Note: ascp --file-list recreates the /IPX…/IPX…/ tree under the output dir, so the Aspera path preserves structure (flatten N/A) — documented.

Usage

# password auth via env — safe for sbatch (no prompt, no hang)
IPROX_ASPERA_PASSWORD=... pridepy download-px-raw-files -a PXD077178 -o ./out \
  --protocol aspera --iprox-user <user>

# or a registered Aspera key
pridepy download-px-raw-files -a PXD077178 -o ./out \
  --protocol aspera --iprox-user <user> --aspera-key /path/to/key

Interactively (TTY), if IPROX_ASPERA_PASSWORD isn't set you're prompted (hidden). In batch you must set the env var (or use --aspera-key) or it errors fast. HTTP parallel (-w) remains the account-free default.

Tests

test_iprox_aspera.py: both auth modes (key → -i, no ASPERA_SCP_PASS; password → ASPERA_SCP_PASS set, no -i, password never on argv), stdin=DEVNULL asserted, missing-credential → ValueError (ascp not called), missing-user / nonexistent-key errors, failed transfer → RuntimeError, file-list contents = URL paths, and PX aspera routing forwards key/password. Full suite: 167 passed, 4 skipped.

Verified

The batched form + password auth was confirmed working end-to-end against download.iprox.org (99-file dataset). The generic bundled Aspera key is not accepted by iProX, which is why password (or a site-registered key via --aspera-key) is the path here.

ypriverol added 2 commits July 2, 2026 08:10
…working ascp), replace password with --aspera-key

The shipped implementation authenticated with ASPERA_SCP_PASS and issued
one ascp per file against user@host:/data/iprox/<path>, which does not
work against iProX. Switch to the verified working invocation: key-based
auth (-i <key>) and a single batched `ascp --mode recv --file-list`
session per dataset, with file-list entries being exactly the URL path
(no /data/iprox prefix). Password auth is dropped entirely; --iprox-user
and the new --aspera-key (env IPROX_ASPERA_KEY) are required for
--protocol aspera. Aspera downloads always preserve the iProX directory
tree (ascp --file-list recreates the remote path layout), so flatten no
longer applies to that path.
…key now optional; zero-config with just --iprox-user)
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f33c05f-352b-40e8-ab1c-e10ca4b8e1d8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/iprox-aspera-key-auth

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Fix iProX Aspera downloads: key auth + batched --file-list (drop password)

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Switch iProX Aspera to key-based auth and a single batched ascp --file-list transfer.
• Remove password handling and flattening for Aspera; preserve iProX directory tree.
• Add --aspera-key CLI/env support with a bundled-key default; update and expand tests.
Diagram

graph TD
  CLI(["pridepy CLI"]) --> PX["ProteomeXchangeProvider"] --> Iprox["IproxProvider.aspera_download"] --> FileList[/"Temp file-list"/] --> Ascp["ascp subprocess"] --> Host{{"download.iprox.org"}}
  Iprox --> Key["Bundled/override key"] --> Ascp
  Ascp --> Out[("Output folder")]
  subgraph Legend
    direction LR
    _entry(["Entry point"]) ~~~ _mod["Module/function"] ~~~ _file[/"Temp file"/] ~~~ _fs[("Filesystem")] ~~~ _ext{{"External host"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Key auth but keep per-file `ascp` invocations
  • ➕ Maintains current per-file destination control (e.g., could still flatten)
  • ➕ Failure can be isolated per-file with finer-grained error reporting
  • ➖ Higher overhead (many processes) and slower overall transfers
  • ➖ Does not match the known-working iProX invocation pattern as closely as --file-list
2. Use `ascp --file-pair-list` instead of `--file-list`
  • ➕ Could explicitly map each source path to a destination path (optionally enabling flattening)
  • ➕ More control over output layout without relying on Aspera's tree recreation
  • ➖ More complex command/file generation and harder to reason about
  • ➖ May diverge from the simplest proven working command for iProX
3. Chunk very large transfers into multiple `--file-list` batches
  • ➕ Limits file-list size and can improve resumability/retry behavior
  • ➕ Reduces blast radius if one batch fails
  • ➖ More orchestration code (batching, retries, partial success semantics)
  • ➖ Potentially lower throughput if batches are too small

Recommendation: The chosen approach (single batched ascp --file-list + key auth) is the best default because it matches the verified working iProX command line and reduces process overhead while leveraging Aspera’s internal parallelism. Consider a follow-up to (1) optionally chunk very large file-lists for retry/resume and (2) align documentation wording around --aspera-key being optional when the bundled key is acceptable.

Files changed (6) +211 / -345

Enhancement (2) +16 / -12
client.pyReplace iprox_password argument with aspera_key in download client API +2/-2

Replace iprox_password argument with aspera_key in download client API

• Renames the public 'download_px_raw_files' argument from 'iprox_password' to 'aspera_key' and forwards it through to the provider layer. This aligns the client API with key-based iProX Aspera authentication.

pridepy/download/client.py

pridepy.pyCLI: add --aspera-key option and remove password prompting +14/-10

CLI: add --aspera-key option and remove password prompting

• Introduces '--aspera-key' (env 'IPROX_ASPERA_KEY') and updates '--iprox-user' help to reflect key-only Aspera auth. Removes reading/prompting for 'IPROX_ASPERA_PASSWORD' and wires 'aspera_key' through to the download client.

pridepy/pridepy.py

Bug fix (2) +60 / -125
iprox.pyRewrite iProX Aspera implementation: key auth + batched --file-list + bundled default key +55/-108

Rewrite iProX Aspera implementation: key auth + batched --file-list + bundled default key

• Removes per-file Aspera transfers, password env handling, and the '/data/iprox' prefix. Implements a single batched 'ascp --mode recv --host --user --file-list' transfer, writes URL paths into a temp file-list, and ensures cleanup. Adds a default bundled Aspera key when 'key_path' is not provided and validates user/key inputs.

pridepy/download/iprox.py

proteomexchange.pyRoute Aspera downloads to iProX with key parameter; drop Aspera flattening logic +5/-17

Route Aspera downloads to iProX with key parameter; drop Aspera flattening logic

• Updates the Aspera routing branch to pass 'aspera_key' to 'IproxProvider.aspera_download' and removes relative-path/flatten handling for the Aspera path. Clarifies docstring expectations that Aspera preserves the source tree.

pridepy/download/proteomexchange.py

Tests (1) +119 / -198
test_iprox_aspera.pyRewrite iProX Aspera tests for key-auth + --file-list batching and defaults +119/-198

Rewrite iProX Aspera tests for key-auth + --file-list batching and defaults

• Replaces password/env assertions with key-auth and '--file-list' argv assertions, including verifying file-list contents are exact URL paths. Adds coverage for missing user, missing/nonexistent key behavior (including bundled default), temp file cleanup, subprocess failure -> RuntimeError, and PX routing updates.

pridepy/tests/test_iprox_aspera.py

Documentation (1) +16 / -10
usage.mdDocument key-only iProX Aspera usage and preserved directory structure +16/-10

Document key-only iProX Aspera usage and preserved directory structure

• Updates CLI option docs to introduce '--aspera-key' and remove password guidance. Adds a note that Aspera uses a single 'ascp --file-list' session and always preserves the iProX directory tree under the output folder.

docs/usage.md

@qodo-code-review

qodo-code-review Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Aspera ignores skip flag 🐞 Bug ≡ Correctness
Description
When protocol == "aspera", skip_if_downloaded_already is not forwarded and cannot be honored
because IproxProvider.aspera_download() no longer accepts it. This can cause re-downloading large
datasets even when the CLI flag requests skipping already-downloaded files.
Code

pridepy/download/proteomexchange.py[R199-209]

        if protocol.lower() == "aspera":
            from pridepy.download.iprox import IproxProvider
-            iprox_urls, rels = [], []
+            iprox_urls = []
            for r in records:
                loc = self.get_download_url(r, protocol)
                host = (urlparse(loc).hostname or "").lower()
                if host == "download.iprox.org":
                    iprox_urls.append(loc)
-                    rels.append(r.get("relativePath"))
            if not iprox_urls:
                raise ValueError(
                    "Aspera requested but no iProX-hosted files found in this dataset."
Evidence
The CLI path passes skip_if_downloaded_already into PX downloads, but the Aspera routing path
drops it entirely and the callee signature no longer supports it, making skipping impossible for
Aspera.

pridepy/pridepy.py[426-436]
pridepy/download/client.py[315-338]
pridepy/download/proteomexchange.py[199-226]
pridepy/download/iprox.py[72-80]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The CLI exposes `--skip-if-downloaded-already`, and `Files.download_px_raw_files()` forwards it into `ProteomeXchangeProvider.download_from_accession_or_url()`. However, the Aspera branch ignores it and routes into `IproxProvider.aspera_download()` which no longer supports skipping.

## Issue Context
Aspera now uses a single `ascp --file-list` session and preserves remote directory structure under `output_folder`, so the expected local destination for a remote path like `/IPX.../file.raw` is typically `os.path.join(output_folder, remote_path.lstrip("/"))`.

## Fix Focus Areas
- pridepy/download/proteomexchange.py[199-226]
- pridepy/download/iprox.py[72-128]

## Suggested change
- Add a `skip_if_downloaded_already: bool` parameter back to `IproxProvider.aspera_download()`.
- In the Aspera branch in `ProteomeXchangeProvider.download_from_accession_or_url()`, forward `skip_if_downloaded_already`.
- Implement skipping by filtering `remote_paths` before writing the file-list (exclude entries whose destination file already exists and has non-zero size), and no-op if all paths are skipped.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Tempfile cleanup masks errors 🐞 Bug ☼ Reliability
Description
IproxProvider.aspera_download() unconditionally calls os.remove(list_path) in a finally block
without guarding against FileNotFoundError. If the file is unexpectedly missing, that cleanup
exception will override/mask the real transfer failure (or success).
Code

pridepy/download/iprox.py[R105-127]

+        fd, list_path = tempfile.mkstemp(prefix="iprox_aspera_", suffix=".txt", text=True)
+        try:
+            with os.fdopen(fd, "w") as fh:
+                for path in remote_paths:
+                    fh.write(path + "\n")
+            argv = [
+                ascp, "-T", "-l", maximum_bandwidth, "-P", cls.ASPERA_PORT,
+                "-k", "1", "-i", key_path, "--mode", "recv",
+                "--host", cls.ASPERA_HOST, "--file-list", list_path,
+                "--user", user, output_folder,
+            ]
+            logging.info(
+                "iProX Aspera: transferring %d file(s) via ascp --file-list",
+                len(remote_paths),
            )
+            try:
+                subprocess.run(argv, check=True)
+            except subprocess.CalledProcessError as e:
+                raise RuntimeError(
+                    f"iProX Aspera transfer failed (exit {e.returncode})"
+                ) from e
+        finally:
+            os.remove(list_path)
Evidence
The current implementation always removes the file-list path in finally without exception
handling, so a cleanup failure would replace the real exception from the transfer attempt.

pridepy/download/iprox.py[105-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `finally: os.remove(list_path)` cleanup can raise `FileNotFoundError` and mask the original exception (including the `RuntimeError` raised for `subprocess.CalledProcessError`).

## Issue Context
This is an edge case, but it is a standard robustness pattern to ensure cleanup never hides the primary failure signal.

## Fix Focus Areas
- pridepy/download/iprox.py[105-127]

## Suggested change
Wrap `os.remove(list_path)` in `try/except FileNotFoundError:` (optionally log at debug level), so the original error is preserved.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Aspera key docs mismatch ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
docs/usage.md says --aspera-key is required and must be the user’s registered private key, but the
CLI and code default key_path=None to a bundled key; the code/help also calls that bundled file
“public” even though it’s a private key. This inconsistency can mislead users about required
configuration and what kind of key file to supply.
Code

docs/usage.md[R247-252]

+| `--iprox-user` | Your registered iProX username (required with `--protocol aspera`; env fallback: `IPROX_USER`) | — |
+| `--aspera-key` | Path to your Aspera private key (required with `--protocol aspera`; env fallback: `IPROX_ASPERA_KEY`) | — |

-The iProX Aspera password is never accepted as a command-line flag. Set the
-`IPROX_ASPERA_PASSWORD` environment variable, or omit it and `pridepy` will
-prompt for it securely (hidden input) when `--protocol aspera` is used.
+iProX Aspera uses key-based authentication only — there is no password
+option. `--aspera-key` must point at the private key you registered when
+setting up your Aspera client.
Evidence
The docs claim --aspera-key is required, but the CLI and implementation explicitly allow it to be
omitted and fall back to a bundled key; the bundled key file content shows it is a private key,
contradicting the “public key” wording.

docs/usage.md[238-252]
pridepy/pridepy.py[392-410]
pridepy/download/iprox.py[58-100]
pridepy/aspera/key/asperaweb_id_dsa.openssh[1-12]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`--aspera-key` is documented as required in `docs/usage.md`, while the CLI/help and implementation treat it as optional (defaulting to a bundled key). Additionally, the bundled key is described as “public” even though the bundled file is a private key, which can confuse users about what to provide.

## Issue Context
The runtime behavior is: if `key_path` is not provided, `IproxProvider.aspera_download()` uses `_default_aspera_key()`.

## Fix Focus Areas
- docs/usage.md[247-252]
- pridepy/pridepy.py[392-410]
- pridepy/download/iprox.py[58-70]

## Suggested change
- Update `docs/usage.md` to mark `--aspera-key` as optional and document the bundled default.
- Update CLI/help text and `_default_aspera_key()` docstring to stop calling the bundled key “public” (it’s a bundled/private key file shipped with the package).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread docs/usage.md Outdated
Comment on lines 199 to 209
if protocol.lower() == "aspera":
from pridepy.download.iprox import IproxProvider
iprox_urls, rels = [], []
iprox_urls = []
for r in records:
loc = self.get_download_url(r, protocol)
host = (urlparse(loc).hostname or "").lower()
if host == "download.iprox.org":
iprox_urls.append(loc)
rels.append(r.get("relativePath"))
if not iprox_urls:
raise ValueError(
"Aspera requested but no iProX-hosted files found in this dataset."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Aspera ignores skip flag 🐞 Bug ≡ Correctness

When protocol == "aspera", skip_if_downloaded_already is not forwarded and cannot be honored
because IproxProvider.aspera_download() no longer accepts it. This can cause re-downloading large
datasets even when the CLI flag requests skipping already-downloaded files.
Agent Prompt
## Issue description
The CLI exposes `--skip-if-downloaded-already`, and `Files.download_px_raw_files()` forwards it into `ProteomeXchangeProvider.download_from_accession_or_url()`. However, the Aspera branch ignores it and routes into `IproxProvider.aspera_download()` which no longer supports skipping.

## Issue Context
Aspera now uses a single `ascp --file-list` session and preserves remote directory structure under `output_folder`, so the expected local destination for a remote path like `/IPX.../file.raw` is typically `os.path.join(output_folder, remote_path.lstrip("/"))`.

## Fix Focus Areas
- pridepy/download/proteomexchange.py[199-226]
- pridepy/download/iprox.py[72-128]

## Suggested change
- Add a `skip_if_downloaded_already: bool` parameter back to `IproxProvider.aspera_download()`.
- In the Aspera branch in `ProteomeXchangeProvider.download_from_accession_or_url()`, forward `skip_if_downloaded_already`.
- Implement skipping by filtering `remote_paths` before writing the file-list (exclude entries whose destination file already exists and has non-zero size), and no-op if all paths are skipped.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread pridepy/download/iprox.py
Comment on lines +105 to +127
fd, list_path = tempfile.mkstemp(prefix="iprox_aspera_", suffix=".txt", text=True)
try:
with os.fdopen(fd, "w") as fh:
for path in remote_paths:
fh.write(path + "\n")
argv = [
ascp, "-T", "-l", maximum_bandwidth, "-P", cls.ASPERA_PORT,
"-k", "1", "-i", key_path, "--mode", "recv",
"--host", cls.ASPERA_HOST, "--file-list", list_path,
"--user", user, output_folder,
]
logging.info(
"iProX Aspera: transferring %d file(s) via ascp --file-list",
len(remote_paths),
)
try:
subprocess.run(argv, check=True)
except subprocess.CalledProcessError as e:
raise RuntimeError(
f"iProX Aspera transfer failed (exit {e.returncode})"
) from e
finally:
os.remove(list_path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

3. Tempfile cleanup masks errors 🐞 Bug ☼ Reliability

IproxProvider.aspera_download() unconditionally calls os.remove(list_path) in a finally block
without guarding against FileNotFoundError. If the file is unexpectedly missing, that cleanup
exception will override/mask the real transfer failure (or success).
Agent Prompt
## Issue description
The `finally: os.remove(list_path)` cleanup can raise `FileNotFoundError` and mask the original exception (including the `RuntimeError` raised for `subprocess.CalledProcessError`).

## Issue Context
This is an edge case, but it is a standard robustness pattern to ensure cleanup never hides the primary failure signal.

## Fix Focus Areas
- pridepy/download/iprox.py[105-127]

## Suggested change
Wrap `os.remove(list_path)` in `try/except FileNotFoundError:` (optionally log at debug level), so the original error is preserved.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

…ng --file-list form; key optional; no interactive hang
@ypriverol ypriverol changed the title fix(iProX Aspera): key auth + --file-list batch (match working ascp); bundled-key default, drop password fix(iProX Aspera): batched --file-list form (match working ascp); password (env/prompt) or --aspera-key; no batch hang Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant