encode-toolkit 0.3.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.
@@ -0,0 +1,367 @@
1
+ """Pydantic models for ENCODE API response objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ def _extract_assemblies(file_list: list | None) -> list[str]:
9
+ """Extract sorted, deduplicated assembly names from a list of file dicts."""
10
+ if not file_list:
11
+ return []
12
+ return sorted(set(f.get("assembly", "") for f in file_list if isinstance(f, dict) and f.get("assembly")))
13
+
14
+
15
+ def _extract_audit_counts(data: dict) -> dict[str, int]:
16
+ """Extract all 4 ENCODE audit level counts from an experiment dict.
17
+
18
+ Returns a dict with keys: error, not_compliant, warning, internal_action.
19
+ Each level value must be a list; non-list values are treated as 0.
20
+ """
21
+ _zero = {"error": 0, "not_compliant": 0, "warning": 0, "internal_action": 0}
22
+ audit = data.get("audit", {})
23
+ if not isinstance(audit, dict):
24
+ return _zero
25
+ return {
26
+ "error": len(v) if isinstance(v := audit.get("ERROR", []), list) else 0,
27
+ "not_compliant": len(v) if isinstance(v := audit.get("NOT_COMPLIANT", []), list) else 0,
28
+ "warning": len(v) if isinstance(v := audit.get("WARNING", []), list) else 0,
29
+ "internal_action": len(v) if isinstance(v := audit.get("INTERNAL_ACTION", []), list) else 0,
30
+ }
31
+
32
+
33
+ class ExperimentSummary(BaseModel):
34
+ """Condensed experiment info returned from search results."""
35
+
36
+ accession: str
37
+ assay_title: str = ""
38
+ target: str = ""
39
+ biosample_summary: str = ""
40
+ organism: str = ""
41
+ organ: str = ""
42
+ biosample_type: str = ""
43
+ status: str = ""
44
+ date_released: str = ""
45
+ description: str = ""
46
+ lab: str = ""
47
+ file_count: int = 0
48
+ replication_type: str = ""
49
+ life_stage: str = ""
50
+ assembly: list[str] = Field(default_factory=list)
51
+ audit_error_count: int = 0
52
+ audit_not_compliant_count: int = 0
53
+ audit_warning_count: int = 0
54
+ audit_internal_action_count: int = 0
55
+ dbxrefs: list[str] = Field(default_factory=list)
56
+ url: str = ""
57
+
58
+ @classmethod
59
+ def from_api(cls, data: dict) -> ExperimentSummary:
60
+ """Parse from ENCODE API experiment object (frame=object)."""
61
+ # Extract target label
62
+ target = ""
63
+ if data.get("target"):
64
+ t = data["target"]
65
+ if isinstance(t, str):
66
+ # Reference path like /targets/H3K27me3-human/
67
+ target = t.strip("/").split("/")[-1] if "/" in t else t
68
+ elif isinstance(t, dict):
69
+ target = t.get("label", "")
70
+
71
+ # Extract lab
72
+ lab = ""
73
+ if data.get("lab"):
74
+ lab_raw = data["lab"]
75
+ if isinstance(lab_raw, str):
76
+ lab = lab_raw.strip("/").split("/")[-1]
77
+ elif isinstance(lab_raw, dict):
78
+ lab = lab_raw.get("title", lab_raw.get("name", ""))
79
+
80
+ # Extract biosample type from ontology
81
+ biosample_type = ""
82
+ organ = ""
83
+ if data.get("biosample_ontology"):
84
+ ont = data["biosample_ontology"]
85
+ if isinstance(ont, dict):
86
+ biosample_type = ont.get("classification", "")
87
+ organ_slims = ont.get("organ_slims", [])
88
+ organ = ", ".join(organ_slims) if organ_slims else ""
89
+ elif isinstance(ont, str):
90
+ biosample_type = ont
91
+
92
+ file_list = data.get("files") or []
93
+ assemblies = _extract_assemblies(file_list)
94
+ audit_counts = _extract_audit_counts(data)
95
+
96
+ # Extract dbxrefs (GEO accessions, etc.)
97
+ dbxrefs = data.get("dbxrefs") or []
98
+
99
+ accession_val = data.get("accession", "")
100
+
101
+ return cls(
102
+ accession=accession_val,
103
+ assay_title=data.get("assay_title", ""),
104
+ target=target,
105
+ biosample_summary=data.get("biosample_summary", ""),
106
+ organism=data.get("organism", {}).get("scientific_name", "")
107
+ if isinstance(data.get("organism"), dict)
108
+ else "",
109
+ organ=organ,
110
+ biosample_type=biosample_type,
111
+ status=data.get("status", ""),
112
+ date_released=data.get("date_released", ""),
113
+ description=data.get("description", ""),
114
+ lab=lab,
115
+ file_count=len(file_list),
116
+ replication_type=data.get("replication_type", ""),
117
+ life_stage=data.get("life_stage_age", ""),
118
+ assembly=assemblies,
119
+ audit_error_count=audit_counts["error"],
120
+ audit_not_compliant_count=audit_counts["not_compliant"],
121
+ audit_warning_count=audit_counts["warning"],
122
+ audit_internal_action_count=audit_counts["internal_action"],
123
+ dbxrefs=dbxrefs,
124
+ url=f"https://www.encodeproject.org/experiments/{accession_val}/" if accession_val else "",
125
+ )
126
+
127
+
128
+ class FileSummary(BaseModel):
129
+ """Condensed file info from search/listing results."""
130
+
131
+ accession: str
132
+ file_format: str = ""
133
+ file_type: str = ""
134
+ output_type: str = ""
135
+ output_category: str = ""
136
+ file_size: int = 0
137
+ file_size_human: str = ""
138
+ assembly: str = ""
139
+ biological_replicates: list[int] = Field(default_factory=list)
140
+ technical_replicates: list[str] = Field(default_factory=list)
141
+ status: str = ""
142
+ download_url: str = ""
143
+ s3_uri: str = ""
144
+ md5sum: str = ""
145
+ experiment_accession: str = ""
146
+ experiment_assay: str = ""
147
+ biosample_summary: str = ""
148
+ preferred_default: bool = False
149
+ date_created: str = ""
150
+
151
+ @classmethod
152
+ def from_api(cls, data: dict, base_url: str = "https://www.encodeproject.org") -> FileSummary:
153
+ """Parse from ENCODE API file object."""
154
+ href = data.get("href", "")
155
+ download_url = f"{base_url}{href}" if href and not href.startswith("http") else href
156
+
157
+ file_size = data.get("file_size", 0) or 0
158
+
159
+ # Extract experiment accession from dataset path
160
+ experiment_accession = ""
161
+ dataset = data.get("dataset", "")
162
+ if isinstance(dataset, str) and "/experiments/" in dataset:
163
+ experiment_accession = dataset.strip("/").split("/")[-1]
164
+ elif isinstance(dataset, dict):
165
+ experiment_accession = dataset.get("accession", "")
166
+
167
+ return cls(
168
+ accession=data.get("accession", ""),
169
+ file_format=data.get("file_format", ""),
170
+ file_type=data.get("file_type", ""),
171
+ output_type=data.get("output_type", ""),
172
+ output_category=data.get("output_category", ""),
173
+ file_size=file_size,
174
+ file_size_human=_human_size(file_size),
175
+ assembly=data.get("assembly", "") or "",
176
+ biological_replicates=data.get("biological_replicates", []),
177
+ technical_replicates=data.get("technical_replicates", []),
178
+ status=data.get("status", ""),
179
+ download_url=download_url,
180
+ s3_uri=data.get("s3_uri", "") or "",
181
+ md5sum=data.get("md5sum", "") or "",
182
+ experiment_accession=experiment_accession,
183
+ experiment_assay=data.get("assay_title", ""),
184
+ biosample_summary=data.get("biosample_summary", "") or "",
185
+ preferred_default=data.get("preferred_default", False) or False,
186
+ date_created=data.get("date_created", ""),
187
+ )
188
+
189
+
190
+ def _extract_organism(data: dict) -> str:
191
+ """Extract organism from nested ENCODE API data."""
192
+ # Direct organism field (frame=object)
193
+ org = data.get("organism")
194
+ if isinstance(org, dict):
195
+ return org.get("scientific_name", "")
196
+ if isinstance(org, str) and org:
197
+ return org.strip("/").split("/")[-1]
198
+ # Try replicates path (frame=embedded)
199
+ for rep in data.get("replicates", []):
200
+ if isinstance(rep, dict):
201
+ lib = rep.get("library", {})
202
+ if isinstance(lib, dict):
203
+ bs = lib.get("biosample", {})
204
+ if isinstance(bs, dict):
205
+ donor = bs.get("donor", {})
206
+ if isinstance(donor, dict):
207
+ org2 = donor.get("organism", {})
208
+ if isinstance(org2, dict):
209
+ name = org2.get("scientific_name", "")
210
+ if name:
211
+ return name
212
+ return ""
213
+
214
+
215
+ class ExperimentDetail(BaseModel):
216
+ """Full experiment details including files list."""
217
+
218
+ accession: str
219
+ assay_title: str = ""
220
+ assay_term_name: str = ""
221
+ target: str = ""
222
+ biosample_summary: str = ""
223
+ description: str = ""
224
+ status: str = ""
225
+ date_released: str = ""
226
+ lab: str = ""
227
+ award: str = ""
228
+ organism: str = ""
229
+ organ: str = ""
230
+ biosample_type: str = ""
231
+ life_stage: str = ""
232
+ replication_type: str = ""
233
+ assembly: list[str] = Field(default_factory=list)
234
+ bio_replicate_count: int = 0
235
+ tech_replicate_count: int = 0
236
+ possible_controls: list[str] = Field(default_factory=list)
237
+ related_series: list[str] = Field(default_factory=list)
238
+ documents: list[str] = Field(default_factory=list)
239
+ url: str = ""
240
+ files: list[FileSummary] = Field(default_factory=list)
241
+
242
+ # Quality/audit info (all 4 ENCODE levels: ERROR, NOT_COMPLIANT, WARNING, INTERNAL_ACTION)
243
+ audit_error_count: int = 0
244
+ audit_not_compliant_count: int = 0
245
+ audit_warning_count: int = 0
246
+ audit_internal_action_count: int = 0
247
+
248
+ @classmethod
249
+ def from_api(cls, data: dict, files: list[dict] | None = None) -> ExperimentDetail:
250
+ """Parse from ENCODE API experiment object (frame=embedded preferred)."""
251
+ # Extract target
252
+ target = ""
253
+ if data.get("target"):
254
+ t = data["target"]
255
+ if isinstance(t, str):
256
+ target = t.strip("/").split("/")[-1]
257
+ elif isinstance(t, dict):
258
+ target = t.get("label", "")
259
+
260
+ # Extract lab
261
+ lab = ""
262
+ if data.get("lab"):
263
+ lab_raw = data["lab"]
264
+ if isinstance(lab_raw, str):
265
+ lab = lab_raw.strip("/").split("/")[-1]
266
+ elif isinstance(lab_raw, dict):
267
+ lab = lab_raw.get("title", lab_raw.get("name", ""))
268
+
269
+ # Extract award/project
270
+ award = ""
271
+ if data.get("award"):
272
+ a = data["award"]
273
+ if isinstance(a, str):
274
+ award = a.strip("/").split("/")[-1]
275
+ elif isinstance(a, dict):
276
+ award = a.get("project", a.get("name", ""))
277
+
278
+ # Extract biosample details
279
+ biosample_type = ""
280
+ organ = ""
281
+ if data.get("biosample_ontology"):
282
+ ont = data["biosample_ontology"]
283
+ if isinstance(ont, dict):
284
+ biosample_type = ont.get("classification", "")
285
+ organ_slims = ont.get("organ_slims", [])
286
+ organ = ", ".join(organ_slims) if organ_slims else ""
287
+
288
+ # Controls
289
+ controls = []
290
+ for ctrl in data.get("possible_controls", []):
291
+ if isinstance(ctrl, str):
292
+ controls.append(ctrl.strip("/").split("/")[-1])
293
+ elif isinstance(ctrl, dict):
294
+ controls.append(ctrl.get("accession", ""))
295
+
296
+ # Use files param if provided, otherwise fall back to data["files"]
297
+ file_dicts = files if files is not None else (data.get("files") or [])
298
+ assemblies = _extract_assemblies(file_dicts)
299
+ audit_counts = _extract_audit_counts(data)
300
+
301
+ # Parse files into summaries
302
+ file_summaries = []
303
+ if files:
304
+ file_summaries = [FileSummary.from_api(f) for f in files]
305
+
306
+ return cls(
307
+ accession=data.get("accession", ""),
308
+ assay_title=data.get("assay_title", ""),
309
+ assay_term_name=data.get("assay_term_name", ""),
310
+ target=target,
311
+ biosample_summary=data.get("biosample_summary", ""),
312
+ description=data.get("description", ""),
313
+ status=data.get("status", ""),
314
+ date_released=data.get("date_released", ""),
315
+ lab=lab,
316
+ award=award,
317
+ organism=_extract_organism(data),
318
+ organ=organ,
319
+ biosample_type=biosample_type,
320
+ life_stage=data.get("life_stage_age", ""),
321
+ replication_type=data.get("replication_type", ""),
322
+ assembly=assemblies,
323
+ bio_replicate_count=data.get("bio_replicate_count", 0) or 0,
324
+ tech_replicate_count=data.get("tech_replicate_count", 0) or 0,
325
+ possible_controls=controls,
326
+ related_series=[
327
+ s if isinstance(s, str) else s.get("accession", "")
328
+ for s in data.get("related_series", [])
329
+ if isinstance(s, str | dict)
330
+ ],
331
+ documents=[
332
+ d if isinstance(d, str) else d.get("@id", "")
333
+ for d in data.get("documents", [])
334
+ if isinstance(d, str | dict)
335
+ ],
336
+ url=f"https://www.encodeproject.org/experiments/{data.get('accession', '')}/",
337
+ files=file_summaries,
338
+ audit_error_count=audit_counts["error"],
339
+ audit_not_compliant_count=audit_counts["not_compliant"],
340
+ audit_warning_count=audit_counts["warning"],
341
+ audit_internal_action_count=audit_counts["internal_action"],
342
+ )
343
+
344
+
345
+ class DownloadResult(BaseModel):
346
+ """Result of a file download operation."""
347
+
348
+ accession: str
349
+ file_path: str = ""
350
+ file_size: int = 0
351
+ file_size_human: str = ""
352
+ success: bool = False
353
+ error: str = ""
354
+ md5_verified: bool = False
355
+
356
+
357
+ def _human_size(size_bytes: int) -> str:
358
+ """Convert bytes to human-readable string."""
359
+ if size_bytes == 0:
360
+ return "0 B"
361
+ units = ["B", "KB", "MB", "GB", "TB"]
362
+ i = 0
363
+ size = float(size_bytes)
364
+ while size >= 1024 and i < len(units) - 1:
365
+ size /= 1024
366
+ i += 1
367
+ return f"{size:.1f} {units[i]}"