variantgrid-api 1.3.0__tar.gz → 1.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {variantgrid_api-1.3.0/src/variantgrid_api.egg-info → variantgrid_api-1.3.1}/PKG-INFO +5 -23
  2. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/README.md +4 -22
  3. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/pyproject.toml +4 -1
  4. variantgrid_api-1.3.1/src/variantgrid_api/cli.py +137 -0
  5. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1/src/variantgrid_api.egg-info}/PKG-INFO +5 -23
  6. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/src/variantgrid_api.egg-info/SOURCES.txt +3 -0
  7. variantgrid_api-1.3.1/src/variantgrid_api.egg-info/entry_points.txt +2 -0
  8. variantgrid_api-1.3.1/tests/test_cli.py +98 -0
  9. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/LICENSE +0 -0
  10. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/setup.cfg +0 -0
  11. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/src/variantgrid_api/api_client.py +0 -0
  12. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/src/variantgrid_api/data_models.py +0 -0
  13. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/src/variantgrid_api/mock_variantgrid_api.py +0 -0
  14. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/src/variantgrid_api.egg-info/dependency_links.txt +0 -0
  15. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/src/variantgrid_api.egg-info/requires.txt +0 -0
  16. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/src/variantgrid_api.egg-info/top_level.txt +0 -0
  17. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/tests/test_api_client.py +0 -0
  18. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/tests/test_api_client_annotation.py +0 -0
  19. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/tests/test_api_client_bulk.py +0 -0
  20. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/tests/test_api_client_validation.py +0 -0
  21. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/tests/test_data_models.py +0 -0
  22. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/tests/test_mock_variantgrid_api.py +0 -0
  23. {variantgrid_api-1.3.0 → variantgrid_api-1.3.1}/tests/test_sequencer_model_from_name.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: variantgrid_api
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: A Python API client for VariantGrid
5
5
  Author-email: Dave Lawrence <davmlaw@gmail.com>
6
6
  License: MIT License
@@ -81,28 +81,10 @@ path = api.annotate_vcf("input.vcf", export_type="vcf", dest_path="/data/results
81
81
  print(f"Annotated VCF written to {path}")
82
82
  ```
83
83
 
84
- Or drive each step yourself:
85
-
86
- ```python
87
- # For ad-hoc uploads pass path=None so the upload isn't treated as a SeqAuto backend-link hint
88
- upload = api.upload_file("input.vcf", path=None)
89
- uploaded_file_id = upload["uploaded_file_id"] # or upload["sha256_hash"]
90
- api.wait_for_annotation(uploaded_file_id, timeout=3600, poll_interval=10) # raises on error/timeout
91
- path = api.download_annotated(uploaded_file_id, export_type="csv", dest_path="/data/results/")
92
- ```
93
-
94
- `poll_upload_status(uploaded_file_id)` returns the raw status dict (including `annotation_complete`,
95
- `progress_percent`, `error`, `vcf_id`, `samples`, ...) if you want to inspect progress directly. All of
96
- these accept `sha256=<hash>` instead of `uploaded_file_id` - the server dedups on the content hash, so it is
97
- stable across machines and useful if you didn't retain the id.
98
-
99
- Notes:
100
-
101
- - The `annotate_vcf` wrapper already uploads with `path=None`. Only pass a `path` to `upload_file` for SeqAuto
102
- uploads that link to a registered `JointCalledVCF` / `SingleSampleVCF` (that is what `path` is for - it is
103
- ignored on non-SeqAuto deployments).
104
- - Downloads require the target deployment to have the cohort export analysis templates configured
105
- (`ANALYSIS_TEMPLATES_AUTO_COHORT_EXPORT`) - otherwise a clear error is returned.
84
+ There's also a `vg_api annotate_vcf` command line tool (first call uploads, run it again to download once
85
+ ready), a step-by-step form (`upload_file` → `wait_for_annotation` → `download_annotated`), and a "submit now,
86
+ download later" pattern for long-running jobs. See
87
+ **[Annotate a VCF](https://github.com/SACGF/variantgrid_api/wiki/Annotate-a-VCF)** on the wiki.
106
88
 
107
89
  ## Testing
108
90
 
@@ -38,28 +38,10 @@ path = api.annotate_vcf("input.vcf", export_type="vcf", dest_path="/data/results
38
38
  print(f"Annotated VCF written to {path}")
39
39
  ```
40
40
 
41
- Or drive each step yourself:
42
-
43
- ```python
44
- # For ad-hoc uploads pass path=None so the upload isn't treated as a SeqAuto backend-link hint
45
- upload = api.upload_file("input.vcf", path=None)
46
- uploaded_file_id = upload["uploaded_file_id"] # or upload["sha256_hash"]
47
- api.wait_for_annotation(uploaded_file_id, timeout=3600, poll_interval=10) # raises on error/timeout
48
- path = api.download_annotated(uploaded_file_id, export_type="csv", dest_path="/data/results/")
49
- ```
50
-
51
- `poll_upload_status(uploaded_file_id)` returns the raw status dict (including `annotation_complete`,
52
- `progress_percent`, `error`, `vcf_id`, `samples`, ...) if you want to inspect progress directly. All of
53
- these accept `sha256=<hash>` instead of `uploaded_file_id` - the server dedups on the content hash, so it is
54
- stable across machines and useful if you didn't retain the id.
55
-
56
- Notes:
57
-
58
- - The `annotate_vcf` wrapper already uploads with `path=None`. Only pass a `path` to `upload_file` for SeqAuto
59
- uploads that link to a registered `JointCalledVCF` / `SingleSampleVCF` (that is what `path` is for - it is
60
- ignored on non-SeqAuto deployments).
61
- - Downloads require the target deployment to have the cohort export analysis templates configured
62
- (`ANALYSIS_TEMPLATES_AUTO_COHORT_EXPORT`) - otherwise a clear error is returned.
41
+ There's also a `vg_api annotate_vcf` command line tool (first call uploads, run it again to download once
42
+ ready), a step-by-step form (`upload_file` → `wait_for_annotation` → `download_annotated`), and a "submit now,
43
+ download later" pattern for long-running jobs. See
44
+ **[Annotate a VCF](https://github.com/SACGF/variantgrid_api/wiki/Annotate-a-VCF)** on the wiki.
63
45
 
64
46
  ## Testing
65
47
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "variantgrid_api"
7
- version = "1.3.0"
7
+ version = "1.3.1"
8
8
  description = "A Python API client for VariantGrid"
9
9
  authors = [
10
10
  { name = "Dave Lawrence", email = "davmlaw@gmail.com" }
@@ -23,6 +23,9 @@ dependencies = [
23
23
  "dataclasses-json",
24
24
  ]
25
25
 
26
+ [project.scripts]
27
+ vg_api = "variantgrid_api.cli:main"
28
+
26
29
  [project.optional-dependencies]
27
30
  test = ["pytest>=8", "pytest-cov>=5", "responses"]
28
31
 
@@ -0,0 +1,137 @@
1
+ """Command line interface for the VariantGrid API client.
2
+
3
+ Currently provides `vg_api annotate_vcf <file>`: a stateless upload/download helper. Because the server
4
+ dedups uploads on the file's SHA-256, the VCF file itself is the receipt - the first call uploads it, and
5
+ running the same command again downloads the annotated result (or reports that it isn't ready yet). No local
6
+ state (upload ids, pending.json, ...) is kept, so you can poll from any machine that has the VCF.
7
+ """
8
+ import argparse
9
+ import hashlib
10
+ import logging
11
+ import os
12
+ import sys
13
+
14
+ import requests
15
+
16
+ from variantgrid_api.api_client import VariantGridAPI, AnnotationError
17
+
18
+ DEFAULT_SERVER = "https://variantgrid.com"
19
+
20
+ EXIT_OK = 0 # annotated file downloaded
21
+ EXIT_ERROR = 1 # bad input / auth / server or annotation error
22
+ EXIT_PENDING = 3 # uploaded just now, or still annotating - come back later
23
+
24
+
25
+ def _sha256(path):
26
+ """Content hash the server dedups on - identical to `sha256sum <file>`."""
27
+ h = hashlib.sha256()
28
+ with open(path, "rb") as f:
29
+ for chunk in iter(lambda: f.read(1 << 20), b""):
30
+ h.update(chunk)
31
+ return h.hexdigest()
32
+
33
+
34
+ def _build_api(args):
35
+ token = args.token or os.environ.get("VARIANTGRID_API_TOKEN")
36
+ if not token:
37
+ raise SystemExit("No API token - pass --token or set VARIANTGRID_API_TOKEN")
38
+ server = args.server or os.environ.get("VARIANTGRID_API_SERVER") or DEFAULT_SERVER
39
+ # Own logger so we can silence the expected 404 when a file hasn't been uploaded yet.
40
+ logger = logging.getLogger("vg_api.cli")
41
+ return VariantGridAPI(server=server, api_token=token, logger=logger), logger
42
+
43
+
44
+ def _is_not_found(exc):
45
+ resp = getattr(exc, "response", None)
46
+ return resp is not None and resp.status_code == 404
47
+
48
+
49
+ def annotate_vcf_cmd(args):
50
+ if not os.path.isfile(args.vcf):
51
+ print(f"No such file: {args.vcf}", file=sys.stderr)
52
+ return EXIT_ERROR
53
+
54
+ api, logger = _build_api(args)
55
+ name = os.path.basename(args.vcf)
56
+
57
+ if args.wait:
58
+ # Blocking one-shot: upload, wait (can take hours), download.
59
+ path = api.annotate_vcf(args.vcf, export_type=args.export_type, dest_path=args.dest,
60
+ poll_interval=args.poll_interval)
61
+ print(f"Annotated {args.export_type} written to {path}")
62
+ return EXIT_OK
63
+
64
+ sha256 = _sha256(args.vcf)
65
+
66
+ # Probe by content hash. A 404 means we've never uploaded this file - so upload it now.
67
+ prev_level = logger.level
68
+ logger.setLevel(logging.CRITICAL) # the probe 404 is expected; don't scare the user
69
+ try:
70
+ status = api.poll_upload_status(sha256=sha256)
71
+ except requests.HTTPError as e:
72
+ if _is_not_found(e):
73
+ up = api.upload_file(args.vcf, path=None)
74
+ print(f"Uploaded {name} (id={up['uploaded_file_id']}). "
75
+ f"Annotating - run the same command again later to download.")
76
+ return EXIT_PENDING
77
+ raise
78
+ finally:
79
+ logger.setLevel(prev_level)
80
+
81
+ if err := status.get("error"):
82
+ print(f"{name}: annotation error - {err}", file=sys.stderr)
83
+ return EXIT_ERROR
84
+ if status.get("annotation_complete"):
85
+ path = api.download_annotated(sha256=sha256, export_type=args.export_type, dest_path=args.dest)
86
+ print(f"Annotated {args.export_type} written to {path}")
87
+ return EXIT_OK
88
+
89
+ progress = status.get("progress_percent")
90
+ suffix = f" (progress {progress}%)" if progress is not None else ""
91
+ print(f"{name}: not ready yet{suffix} - run the same command again later.")
92
+ return EXIT_PENDING
93
+
94
+
95
+ def build_parser():
96
+ common = argparse.ArgumentParser(add_help=False)
97
+ common.add_argument("--server",
98
+ help="VariantGrid server URL (default: $VARIANTGRID_API_SERVER or "
99
+ f"{DEFAULT_SERVER})")
100
+ common.add_argument("--token", help="API token (default: $VARIANTGRID_API_TOKEN)")
101
+
102
+ parser = argparse.ArgumentParser(prog="vg_api", description="VariantGrid API command line tool")
103
+ sub = parser.add_subparsers(dest="command", required=True)
104
+
105
+ p = sub.add_parser("annotate_vcf", parents=[common],
106
+ help="Upload a VCF for annotation; run again to download it once ready",
107
+ description="Upload a VCF for annotation. Because uploads are keyed on the file's "
108
+ "SHA-256, running the same command again downloads the annotated result "
109
+ "when ready, or reports that it isn't ready yet - no local state is kept.")
110
+ p.add_argument("vcf", help="Path to the VCF (.vcf / .vcf.gz) to annotate")
111
+ p.add_argument("--export-type", choices=("vcf", "csv"), default="vcf",
112
+ help="Download format (default: vcf)")
113
+ p.add_argument("-o", "--dest", default=".",
114
+ help="Destination directory or file for the download (default: current directory)")
115
+ p.add_argument("--wait", action="store_true",
116
+ help="Block until annotation finishes, then download (can take hours)")
117
+ p.add_argument("--poll-interval", type=float, default=10,
118
+ help="Seconds between status polls when using --wait (default: 10)")
119
+ p.set_defaults(func=annotate_vcf_cmd)
120
+ return parser
121
+
122
+
123
+ def main(argv=None):
124
+ parser = build_parser()
125
+ args = parser.parse_args(argv)
126
+ try:
127
+ return args.func(args)
128
+ except AnnotationError as e:
129
+ print(f"Annotation error: {e}", file=sys.stderr)
130
+ return EXIT_ERROR
131
+ except requests.HTTPError as e:
132
+ print(f"HTTP error: {e}", file=sys.stderr)
133
+ return EXIT_ERROR
134
+
135
+
136
+ if __name__ == "__main__":
137
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: variantgrid_api
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: A Python API client for VariantGrid
5
5
  Author-email: Dave Lawrence <davmlaw@gmail.com>
6
6
  License: MIT License
@@ -81,28 +81,10 @@ path = api.annotate_vcf("input.vcf", export_type="vcf", dest_path="/data/results
81
81
  print(f"Annotated VCF written to {path}")
82
82
  ```
83
83
 
84
- Or drive each step yourself:
85
-
86
- ```python
87
- # For ad-hoc uploads pass path=None so the upload isn't treated as a SeqAuto backend-link hint
88
- upload = api.upload_file("input.vcf", path=None)
89
- uploaded_file_id = upload["uploaded_file_id"] # or upload["sha256_hash"]
90
- api.wait_for_annotation(uploaded_file_id, timeout=3600, poll_interval=10) # raises on error/timeout
91
- path = api.download_annotated(uploaded_file_id, export_type="csv", dest_path="/data/results/")
92
- ```
93
-
94
- `poll_upload_status(uploaded_file_id)` returns the raw status dict (including `annotation_complete`,
95
- `progress_percent`, `error`, `vcf_id`, `samples`, ...) if you want to inspect progress directly. All of
96
- these accept `sha256=<hash>` instead of `uploaded_file_id` - the server dedups on the content hash, so it is
97
- stable across machines and useful if you didn't retain the id.
98
-
99
- Notes:
100
-
101
- - The `annotate_vcf` wrapper already uploads with `path=None`. Only pass a `path` to `upload_file` for SeqAuto
102
- uploads that link to a registered `JointCalledVCF` / `SingleSampleVCF` (that is what `path` is for - it is
103
- ignored on non-SeqAuto deployments).
104
- - Downloads require the target deployment to have the cohort export analysis templates configured
105
- (`ANALYSIS_TEMPLATES_AUTO_COHORT_EXPORT`) - otherwise a clear error is returned.
84
+ There's also a `vg_api annotate_vcf` command line tool (first call uploads, run it again to download once
85
+ ready), a step-by-step form (`upload_file` → `wait_for_annotation` → `download_annotated`), and a "submit now,
86
+ download later" pattern for long-running jobs. See
87
+ **[Annotate a VCF](https://github.com/SACGF/variantgrid_api/wiki/Annotate-a-VCF)** on the wiki.
106
88
 
107
89
  ## Testing
108
90
 
@@ -2,17 +2,20 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/variantgrid_api/api_client.py
5
+ src/variantgrid_api/cli.py
5
6
  src/variantgrid_api/data_models.py
6
7
  src/variantgrid_api/mock_variantgrid_api.py
7
8
  src/variantgrid_api.egg-info/PKG-INFO
8
9
  src/variantgrid_api.egg-info/SOURCES.txt
9
10
  src/variantgrid_api.egg-info/dependency_links.txt
11
+ src/variantgrid_api.egg-info/entry_points.txt
10
12
  src/variantgrid_api.egg-info/requires.txt
11
13
  src/variantgrid_api.egg-info/top_level.txt
12
14
  tests/test_api_client.py
13
15
  tests/test_api_client_annotation.py
14
16
  tests/test_api_client_bulk.py
15
17
  tests/test_api_client_validation.py
18
+ tests/test_cli.py
16
19
  tests/test_data_models.py
17
20
  tests/test_mock_variantgrid_api.py
18
21
  tests/test_sequencer_model_from_name.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vg_api = variantgrid_api.cli:main
@@ -0,0 +1,98 @@
1
+ import re
2
+
3
+ import pytest
4
+ import responses
5
+
6
+ from variantgrid_api import cli
7
+
8
+ SERVER = "https://vg.example.org"
9
+
10
+ STATUS_RE = re.compile(rf"{SERVER}/upload/api/v1/upload_status/sha256/[0-9a-f]+$")
11
+ UPLOAD_URL = f"{SERVER}/upload/api/v1/file_upload"
12
+ DOWNLOAD_RE = re.compile(rf"{SERVER}/upload/api/v1/download/sha256/[0-9a-f]+/(vcf|csv)$")
13
+
14
+
15
+ @pytest.fixture
16
+ def vcf(tmp_path):
17
+ p = tmp_path / "input.vcf"
18
+ p.write_text("##fileformat=VCFv4.2\n")
19
+ return str(p)
20
+
21
+
22
+ def _argv(vcf, *extra):
23
+ return ["annotate_vcf", vcf, "--server", SERVER, "--token", "T", *extra]
24
+
25
+
26
+ @responses.activate
27
+ def test_first_call_uploads(vcf):
28
+ """Unknown file (status 404) -> upload it and report pending."""
29
+ responses.add(responses.GET, STATUS_RE, json={"detail": "not found"}, status=404)
30
+ responses.add(responses.POST, UPLOAD_URL, json={"uploaded_file_id": 42}, status=200)
31
+
32
+ rc = cli.main(_argv(vcf))
33
+
34
+ assert rc == cli.EXIT_PENDING
35
+ assert any(c.request.method == "POST" and c.request.url.startswith(UPLOAD_URL)
36
+ for c in responses.calls)
37
+ # Ad-hoc upload must omit the SeqAuto path hint
38
+ upload_call = next(c for c in responses.calls if c.request.url.startswith(UPLOAD_URL))
39
+ assert "path=" not in upload_call.request.url
40
+
41
+
42
+ @responses.activate
43
+ def test_second_call_downloads_when_ready(vcf, tmp_path):
44
+ responses.add(responses.GET, STATUS_RE, json={"annotation_complete": True, "error": None}, status=200)
45
+ responses.add(responses.GET, DOWNLOAD_RE, body=b"vcfdata", status=200,
46
+ headers={"Content-Disposition": 'attachment; filename="out.vcf.gz"'})
47
+
48
+ rc = cli.main(_argv(vcf, "-o", str(tmp_path)))
49
+
50
+ assert rc == cli.EXIT_OK
51
+ assert (tmp_path / "out.vcf.gz").read_bytes() == b"vcfdata"
52
+ # It must NOT re-upload when the file is already known
53
+ assert not any(c.request.method == "POST" for c in responses.calls)
54
+
55
+
56
+ @responses.activate
57
+ def test_not_ready_reports_pending(vcf):
58
+ responses.add(responses.GET, STATUS_RE,
59
+ json={"annotation_complete": False, "error": None, "progress_percent": 40}, status=200)
60
+
61
+ rc = cli.main(_argv(vcf))
62
+
63
+ assert rc == cli.EXIT_PENDING
64
+ assert not any(re.match(DOWNLOAD_RE, c.request.url) for c in responses.calls)
65
+
66
+
67
+ @responses.activate
68
+ def test_server_error_status_is_error(vcf):
69
+ responses.add(responses.GET, STATUS_RE,
70
+ json={"annotation_complete": False, "error": "Pipeline failed"}, status=200)
71
+
72
+ rc = cli.main(_argv(vcf))
73
+ assert rc == cli.EXIT_ERROR
74
+
75
+
76
+ @responses.activate
77
+ def test_csv_export_type(vcf, tmp_path):
78
+ responses.add(responses.GET, STATUS_RE, json={"annotation_complete": True, "error": None}, status=200)
79
+ responses.add(responses.GET, DOWNLOAD_RE, body=b"zip", status=200,
80
+ headers={"Content-Disposition": 'attachment; filename="out.csv.zip"'})
81
+
82
+ rc = cli.main(_argv(vcf, "--export-type", "csv", "-o", str(tmp_path)))
83
+
84
+ assert rc == cli.EXIT_OK
85
+ assert (tmp_path / "out.csv.zip").read_bytes() == b"zip"
86
+ download_call = next(c for c in responses.calls if re.match(DOWNLOAD_RE, c.request.url))
87
+ assert download_call.request.url.endswith("/csv")
88
+
89
+
90
+ def test_missing_file_is_error(tmp_path):
91
+ rc = cli.main(_argv(str(tmp_path / "nope.vcf")))
92
+ assert rc == cli.EXIT_ERROR
93
+
94
+
95
+ def test_missing_token_raises(vcf, monkeypatch):
96
+ monkeypatch.delenv("VARIANTGRID_API_TOKEN", raising=False)
97
+ with pytest.raises(SystemExit):
98
+ cli.main(["annotate_vcf", vcf, "--server", SERVER])
File without changes