ipaapi 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ipaapi/__init__.py +92 -0
- ipaapi/_payload.py +150 -0
- ipaapi/auth.py +575 -0
- ipaapi/cli.py +971 -0
- ipaapi/client.py +658 -0
- ipaapi/dataset.py +285 -0
- ipaapi/errors.py +101 -0
- ipaapi/history.py +145 -0
- ipaapi/mapping.py +485 -0
- ipaapi/models.py +193 -0
- ipaapi/triage.py +136 -0
- ipaapi-1.0.0.dist-info/METADATA +833 -0
- ipaapi-1.0.0.dist-info/RECORD +16 -0
- ipaapi-1.0.0.dist-info/WHEEL +4 -0
- ipaapi-1.0.0.dist-info/entry_points.txt +2 -0
- ipaapi-1.0.0.dist-info/licenses/LICENSE +21 -0
ipaapi/__init__.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Python client for QIAGEN Ingenuity Pathway Analysis (IPA).
|
|
2
|
+
|
|
3
|
+
Upload a dataset into an IPA project using an explicit column mapping, submit it
|
|
4
|
+
for analysis, and track or retrieve the results.
|
|
5
|
+
|
|
6
|
+
Typical use::
|
|
7
|
+
|
|
8
|
+
from ipaapi import (
|
|
9
|
+
ColumnMapping, Dataset, IPAClient, Measurement, MeasurementType, Observation,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
mapping = ColumnMapping(
|
|
13
|
+
gene_id_column="ID",
|
|
14
|
+
gene_id_type="ensembl",
|
|
15
|
+
observations=[
|
|
16
|
+
Observation("Gemfib vs ctrl", [
|
|
17
|
+
Measurement("Fold Change", MeasurementType.FOLD_CHANGE, cutoff=1.5),
|
|
18
|
+
Measurement("p-value", MeasurementType.P_VALUE),
|
|
19
|
+
Measurement("FDR", MeasurementType.FALSE_DISCOVERY, cutoff=0.01),
|
|
20
|
+
]),
|
|
21
|
+
],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
dataset = Dataset.from_file("Data/Gemfibrozil vs Ctrl RNAseq.txt", mapping)
|
|
25
|
+
client = IPAClient.login()
|
|
26
|
+
analysis_ids = client.submit(dataset, project="PythonAPI_Demo")
|
|
27
|
+
client.wait_for(analysis_ids)
|
|
28
|
+
|
|
29
|
+
This package builds on QIAGEN's ``python-api-demo`` example code. It is not an
|
|
30
|
+
official QIAGEN product.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from .auth import (
|
|
36
|
+
AUTHORIZATION_BASE_URL,
|
|
37
|
+
DEFAULT_CLIENT_ID,
|
|
38
|
+
DEFAULT_HOST,
|
|
39
|
+
TOKEN_URL,
|
|
40
|
+
Credentials,
|
|
41
|
+
TokenCache,
|
|
42
|
+
login,
|
|
43
|
+
)
|
|
44
|
+
from .client import AnalysisResults, IPAClient
|
|
45
|
+
from .dataset import Dataset, load_table
|
|
46
|
+
from .errors import (
|
|
47
|
+
AnalysisError,
|
|
48
|
+
AuthenticationError,
|
|
49
|
+
IPAError,
|
|
50
|
+
MappingError,
|
|
51
|
+
ResultsUnavailableError,
|
|
52
|
+
SubmissionError,
|
|
53
|
+
)
|
|
54
|
+
from .mapping import ColumnMapping, Measurement, Observation
|
|
55
|
+
from .models import GENE_ID_TYPES, AnalysisStatus, MeasurementType, ReferenceSet
|
|
56
|
+
|
|
57
|
+
#: Single source of truth for the package version; pyproject.toml reads it from
|
|
58
|
+
#: here at build time. Bump it in this file and nowhere else.
|
|
59
|
+
__version__ = "1.0.0"
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"__version__",
|
|
63
|
+
# mapping and data
|
|
64
|
+
"ColumnMapping",
|
|
65
|
+
"Measurement",
|
|
66
|
+
"Observation",
|
|
67
|
+
"Dataset",
|
|
68
|
+
"load_table",
|
|
69
|
+
# enums
|
|
70
|
+
"MeasurementType",
|
|
71
|
+
"AnalysisStatus",
|
|
72
|
+
"ReferenceSet",
|
|
73
|
+
"GENE_ID_TYPES",
|
|
74
|
+
# auth
|
|
75
|
+
"login",
|
|
76
|
+
"Credentials",
|
|
77
|
+
"TokenCache",
|
|
78
|
+
"DEFAULT_CLIENT_ID",
|
|
79
|
+
"DEFAULT_HOST",
|
|
80
|
+
"AUTHORIZATION_BASE_URL",
|
|
81
|
+
"TOKEN_URL",
|
|
82
|
+
# client
|
|
83
|
+
"IPAClient",
|
|
84
|
+
"AnalysisResults",
|
|
85
|
+
# errors
|
|
86
|
+
"IPAError",
|
|
87
|
+
"AuthenticationError",
|
|
88
|
+
"MappingError",
|
|
89
|
+
"SubmissionError",
|
|
90
|
+
"AnalysisError",
|
|
91
|
+
"ResultsUnavailableError",
|
|
92
|
+
]
|
ipaapi/_payload.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Build the ``multiobsanalysis`` request body.
|
|
2
|
+
|
|
3
|
+
IPA's ``/pa/api/v2/multiobsanalysis`` endpoint takes a single
|
|
4
|
+
``application/x-www-form-urlencoded`` body that carries both the analysis
|
|
5
|
+
settings and the entire dataset, as a long run of repeated parameters.
|
|
6
|
+
|
|
7
|
+
The parameter naming is positional and slightly irregular, so it is worth
|
|
8
|
+
writing down. For measurement slot ``k`` (zero-based) and observation ``i``
|
|
9
|
+
(zero-based):
|
|
10
|
+
|
|
11
|
+
============================ ==============================================
|
|
12
|
+
Parameter Meaning
|
|
13
|
+
============================ ==============================================
|
|
14
|
+
``expvaltype`` / ``expvaltypeK+1`` measurement type for slot k (global)
|
|
15
|
+
``cutoff`` / ``cutoffK+1`` cutoff for slot k (global, optional)
|
|
16
|
+
``obsI+1name`` observation name
|
|
17
|
+
``expvalname`` / ``expvalK+1name`` column label, slot k, first observation
|
|
18
|
+
``obsI+1expvalname`` column label, slot k=0, later observations
|
|
19
|
+
``obsI+1expvalK+1name`` column label, slot k>0, later observations
|
|
20
|
+
``geneid`` one per data row
|
|
21
|
+
``expvalue`` / ``expvalK+1`` one per slot per observation, per row
|
|
22
|
+
============================ ==============================================
|
|
23
|
+
|
|
24
|
+
Note that the per-row value parameters carry no observation prefix: they simply
|
|
25
|
+
repeat, cycling through the slots of observation 1, then observation 2, and so
|
|
26
|
+
on. Order is therefore load-bearing, which is why this module builds an explicit
|
|
27
|
+
ordered list of pairs.
|
|
28
|
+
|
|
29
|
+
The original demo concatenated these into a string by hand with no
|
|
30
|
+
percent-encoding, so any gene identifier, column header or observation name
|
|
31
|
+
containing ``&``, ``=``, ``+``, ``%`` or a space silently corrupted the request.
|
|
32
|
+
Here the pairs are handed to :func:`urllib.parse.urlencode`, which encodes them
|
|
33
|
+
correctly.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from typing import TYPE_CHECKING, Iterator, List, Optional, Tuple
|
|
39
|
+
from urllib.parse import urlencode
|
|
40
|
+
|
|
41
|
+
from .mapping import ColumnMapping
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
44
|
+
import pandas as pd
|
|
45
|
+
|
|
46
|
+
__all__ = ["build_submission_pairs", "encode_submission"]
|
|
47
|
+
|
|
48
|
+
Pair = Tuple[str, str]
|
|
49
|
+
|
|
50
|
+
_MISSING = "NaN"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _slot_key(base: str, index: int) -> str:
|
|
54
|
+
"""``expvaltype``, ``expvaltype2``, ``expvaltype3`` ... for slot *index*."""
|
|
55
|
+
return base if index == 0 else f"{base}{index + 1}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _column_name_key(obs_index: int, slot_index: int) -> str:
|
|
59
|
+
"""Parameter naming the source column for (*obs_index*, *slot_index*)."""
|
|
60
|
+
prefix = "" if obs_index == 0 else f"obs{obs_index + 1}"
|
|
61
|
+
if slot_index == 0:
|
|
62
|
+
return f"{prefix}expvalname"
|
|
63
|
+
return f"{prefix}expval{slot_index + 1}name"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _value_key(slot_index: int) -> str:
|
|
67
|
+
"""Parameter carrying a data value in slot *slot_index*."""
|
|
68
|
+
return "expvalue" if slot_index == 0 else f"expval{slot_index + 1}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _format(value) -> str:
|
|
72
|
+
"""Render a cell as IPA expects, mapping missing values to ``NaN``."""
|
|
73
|
+
if value is None:
|
|
74
|
+
return _MISSING
|
|
75
|
+
if isinstance(value, float) and value != value:
|
|
76
|
+
return _MISSING
|
|
77
|
+
text = str(value).strip()
|
|
78
|
+
if text == "" or text.lower() in {"na", "nan", "none", "null"}:
|
|
79
|
+
return _MISSING
|
|
80
|
+
return text
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_submission_pairs(
|
|
84
|
+
frame: "pd.DataFrame",
|
|
85
|
+
mapping: ColumnMapping,
|
|
86
|
+
application_name: str,
|
|
87
|
+
project_name: str,
|
|
88
|
+
dataset_name: str,
|
|
89
|
+
analysis_name: Optional[str] = None,
|
|
90
|
+
reference_set: Optional[str] = None,
|
|
91
|
+
ipa_view: str = "none",
|
|
92
|
+
) -> List[Pair]:
|
|
93
|
+
"""Return the full ordered parameter list for one submission.
|
|
94
|
+
|
|
95
|
+
Kept separate from :func:`encode_submission` so tests can assert on the
|
|
96
|
+
structure without decoding a URL-encoded blob.
|
|
97
|
+
"""
|
|
98
|
+
import pandas as pd
|
|
99
|
+
|
|
100
|
+
types = mapping.measurement_types
|
|
101
|
+
pairs: List[Pair] = [
|
|
102
|
+
("applicationname", application_name),
|
|
103
|
+
("projectname", project_name),
|
|
104
|
+
("ipaview", ipa_view),
|
|
105
|
+
("datasetname", dataset_name),
|
|
106
|
+
("analysisname", analysis_name or dataset_name),
|
|
107
|
+
]
|
|
108
|
+
# Omitted unless explicitly asked for, so IPA applies its own default.
|
|
109
|
+
# Sending referenceset=dataset -- as QIAGEN's demo did -- makes the
|
|
110
|
+
# background the uploaded genes, which for a pre-filtered hit list is the
|
|
111
|
+
# same set as the foreground and leaves the enrichment statistics
|
|
112
|
+
# degenerate: z-scores appear, overlap p-values do not.
|
|
113
|
+
if reference_set is not None:
|
|
114
|
+
pairs.append(("referenceset", reference_set))
|
|
115
|
+
pairs += [
|
|
116
|
+
("geneidtype", mapping.gene_id_type),
|
|
117
|
+
("genecolname", mapping.gene_id_label or mapping.gene_id_column),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
for i, obs in enumerate(mapping.observations):
|
|
121
|
+
pairs.append((f"obs{i + 1}name", obs.name))
|
|
122
|
+
|
|
123
|
+
for k, mtype in enumerate(types):
|
|
124
|
+
pairs.append((_slot_key("expvaltype", k), mtype.value))
|
|
125
|
+
|
|
126
|
+
for i, obs in enumerate(mapping.observations):
|
|
127
|
+
for k, measurement in enumerate(mapping.ordered_measurements(obs)):
|
|
128
|
+
pairs.append((_column_name_key(i, k), measurement.display_name))
|
|
129
|
+
|
|
130
|
+
for k, cutoff in enumerate(mapping.cutoffs):
|
|
131
|
+
if cutoff is not None:
|
|
132
|
+
pairs.append((_slot_key("cutoff", k), f"{cutoff:g}"))
|
|
133
|
+
|
|
134
|
+
# Identifiers may be coalesced from a fallback column; values are taken in
|
|
135
|
+
# canonical submission order. itertuples keeps this workable on large files.
|
|
136
|
+
gene_ids, _ = mapping.resolve_gene_ids(frame)
|
|
137
|
+
values = frame.loc[:, mapping.value_columns]
|
|
138
|
+
n_slots = len(types)
|
|
139
|
+
|
|
140
|
+
for gene_id, row in zip(gene_ids.tolist(), values.itertuples(index=False, name=None)):
|
|
141
|
+
pairs.append(("geneid", _format(gene_id)))
|
|
142
|
+
for offset, value in enumerate(row):
|
|
143
|
+
pairs.append((_value_key(offset % n_slots), _format(value)))
|
|
144
|
+
|
|
145
|
+
return pairs
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def encode_submission(pairs: List[Pair]) -> str:
|
|
149
|
+
"""Percent-encode *pairs* as an ``x-www-form-urlencoded`` body."""
|
|
150
|
+
return urlencode(pairs)
|