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 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)