pyobo 0.11.2__py3-none-any.whl → 0.12.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.
Files changed (227) hide show
  1. pyobo/.DS_Store +0 -0
  2. pyobo/__init__.py +95 -20
  3. pyobo/__main__.py +0 -0
  4. pyobo/api/__init__.py +81 -10
  5. pyobo/api/alts.py +52 -42
  6. pyobo/api/combine.py +39 -0
  7. pyobo/api/edges.py +68 -0
  8. pyobo/api/hierarchy.py +231 -203
  9. pyobo/api/metadata.py +14 -19
  10. pyobo/api/names.py +207 -127
  11. pyobo/api/properties.py +117 -113
  12. pyobo/api/relations.py +68 -94
  13. pyobo/api/species.py +24 -21
  14. pyobo/api/typedefs.py +11 -11
  15. pyobo/api/utils.py +66 -13
  16. pyobo/api/xrefs.py +108 -114
  17. pyobo/cli/__init__.py +0 -0
  18. pyobo/cli/cli.py +35 -50
  19. pyobo/cli/database.py +183 -161
  20. pyobo/{xrefdb/xrefs_pipeline.py → cli/database_utils.py} +54 -73
  21. pyobo/cli/lookup.py +163 -195
  22. pyobo/cli/utils.py +19 -6
  23. pyobo/constants.py +102 -3
  24. pyobo/getters.py +196 -118
  25. pyobo/gilda_utils.py +79 -200
  26. pyobo/identifier_utils/__init__.py +41 -0
  27. pyobo/identifier_utils/api.py +296 -0
  28. pyobo/identifier_utils/model.py +130 -0
  29. pyobo/identifier_utils/preprocessing.json +812 -0
  30. pyobo/identifier_utils/preprocessing.py +61 -0
  31. pyobo/identifier_utils/relations/__init__.py +8 -0
  32. pyobo/identifier_utils/relations/api.py +162 -0
  33. pyobo/identifier_utils/relations/data.json +5824 -0
  34. pyobo/identifier_utils/relations/data_owl.json +57 -0
  35. pyobo/identifier_utils/relations/data_rdf.json +1 -0
  36. pyobo/identifier_utils/relations/data_rdfs.json +7 -0
  37. pyobo/mocks.py +9 -6
  38. pyobo/ner/__init__.py +9 -0
  39. pyobo/ner/api.py +72 -0
  40. pyobo/ner/normalizer.py +33 -0
  41. pyobo/obographs.py +43 -39
  42. pyobo/plugins.py +5 -4
  43. pyobo/py.typed +0 -0
  44. pyobo/reader.py +1358 -395
  45. pyobo/reader_utils.py +155 -0
  46. pyobo/resource_utils.py +42 -22
  47. pyobo/resources/__init__.py +0 -0
  48. pyobo/resources/goc.py +75 -0
  49. pyobo/resources/goc.tsv +188 -0
  50. pyobo/resources/ncbitaxon.py +4 -5
  51. pyobo/resources/ncbitaxon.tsv.gz +0 -0
  52. pyobo/resources/ro.py +3 -2
  53. pyobo/resources/ro.tsv +0 -0
  54. pyobo/resources/so.py +0 -0
  55. pyobo/resources/so.tsv +0 -0
  56. pyobo/sources/README.md +12 -8
  57. pyobo/sources/__init__.py +52 -29
  58. pyobo/sources/agrovoc.py +0 -0
  59. pyobo/sources/antibodyregistry.py +11 -12
  60. pyobo/sources/bigg/__init__.py +13 -0
  61. pyobo/sources/bigg/bigg_compartment.py +81 -0
  62. pyobo/sources/bigg/bigg_metabolite.py +229 -0
  63. pyobo/sources/bigg/bigg_model.py +46 -0
  64. pyobo/sources/bigg/bigg_reaction.py +77 -0
  65. pyobo/sources/biogrid.py +1 -2
  66. pyobo/sources/ccle.py +7 -12
  67. pyobo/sources/cgnc.py +0 -5
  68. pyobo/sources/chebi.py +1 -1
  69. pyobo/sources/chembl/__init__.py +9 -0
  70. pyobo/sources/{chembl.py → chembl/chembl_compound.py} +13 -25
  71. pyobo/sources/chembl/chembl_target.py +160 -0
  72. pyobo/sources/civic_gene.py +55 -15
  73. pyobo/sources/clinicaltrials.py +160 -0
  74. pyobo/sources/complexportal.py +24 -24
  75. pyobo/sources/conso.py +14 -22
  76. pyobo/sources/cpt.py +0 -0
  77. pyobo/sources/credit.py +1 -9
  78. pyobo/sources/cvx.py +27 -5
  79. pyobo/sources/depmap.py +9 -12
  80. pyobo/sources/dictybase_gene.py +2 -7
  81. pyobo/sources/drugbank/__init__.py +9 -0
  82. pyobo/sources/{drugbank.py → drugbank/drugbank.py} +11 -16
  83. pyobo/sources/{drugbank_salt.py → drugbank/drugbank_salt.py} +3 -8
  84. pyobo/sources/drugcentral.py +17 -13
  85. pyobo/sources/expasy.py +31 -34
  86. pyobo/sources/famplex.py +13 -18
  87. pyobo/sources/flybase.py +3 -8
  88. pyobo/sources/gard.py +62 -0
  89. pyobo/sources/geonames/__init__.py +9 -0
  90. pyobo/sources/geonames/features.py +28 -0
  91. pyobo/sources/{geonames.py → geonames/geonames.py} +87 -26
  92. pyobo/sources/geonames/utils.py +115 -0
  93. pyobo/sources/gmt_utils.py +6 -7
  94. pyobo/sources/go.py +20 -13
  95. pyobo/sources/gtdb.py +154 -0
  96. pyobo/sources/gwascentral/__init__.py +9 -0
  97. pyobo/sources/{gwascentral_phenotype.py → gwascentral/gwascentral_phenotype.py} +5 -7
  98. pyobo/sources/{gwascentral_study.py → gwascentral/gwascentral_study.py} +1 -7
  99. pyobo/sources/hgnc/__init__.py +9 -0
  100. pyobo/sources/{hgnc.py → hgnc/hgnc.py} +56 -70
  101. pyobo/sources/{hgncgenefamily.py → hgnc/hgncgenefamily.py} +8 -18
  102. pyobo/sources/icd/__init__.py +9 -0
  103. pyobo/sources/{icd10.py → icd/icd10.py} +35 -37
  104. pyobo/sources/icd/icd11.py +148 -0
  105. pyobo/sources/{icd_utils.py → icd/icd_utils.py} +66 -20
  106. pyobo/sources/interpro.py +4 -9
  107. pyobo/sources/itis.py +0 -5
  108. pyobo/sources/kegg/__init__.py +0 -0
  109. pyobo/sources/kegg/api.py +16 -38
  110. pyobo/sources/kegg/genes.py +9 -20
  111. pyobo/sources/kegg/genome.py +1 -7
  112. pyobo/sources/kegg/pathway.py +9 -21
  113. pyobo/sources/mesh.py +58 -24
  114. pyobo/sources/mgi.py +3 -10
  115. pyobo/sources/mirbase/__init__.py +11 -0
  116. pyobo/sources/{mirbase.py → mirbase/mirbase.py} +8 -11
  117. pyobo/sources/{mirbase_constants.py → mirbase/mirbase_constants.py} +0 -0
  118. pyobo/sources/{mirbase_family.py → mirbase/mirbase_family.py} +4 -8
  119. pyobo/sources/{mirbase_mature.py → mirbase/mirbase_mature.py} +3 -7
  120. pyobo/sources/msigdb.py +74 -39
  121. pyobo/sources/ncbi/__init__.py +9 -0
  122. pyobo/sources/ncbi/ncbi_gc.py +162 -0
  123. pyobo/sources/{ncbigene.py → ncbi/ncbigene.py} +18 -19
  124. pyobo/sources/nih_reporter.py +60 -0
  125. pyobo/sources/nlm/__init__.py +9 -0
  126. pyobo/sources/nlm/nlm_catalog.py +48 -0
  127. pyobo/sources/nlm/nlm_publisher.py +36 -0
  128. pyobo/sources/nlm/utils.py +116 -0
  129. pyobo/sources/npass.py +6 -8
  130. pyobo/sources/omim_ps.py +10 -3
  131. pyobo/sources/pathbank.py +4 -8
  132. pyobo/sources/pfam/__init__.py +9 -0
  133. pyobo/sources/{pfam.py → pfam/pfam.py} +3 -8
  134. pyobo/sources/{pfam_clan.py → pfam/pfam_clan.py} +2 -7
  135. pyobo/sources/pharmgkb/__init__.py +15 -0
  136. pyobo/sources/pharmgkb/pharmgkb_chemical.py +89 -0
  137. pyobo/sources/pharmgkb/pharmgkb_disease.py +77 -0
  138. pyobo/sources/pharmgkb/pharmgkb_gene.py +108 -0
  139. pyobo/sources/pharmgkb/pharmgkb_pathway.py +63 -0
  140. pyobo/sources/pharmgkb/pharmgkb_variant.py +84 -0
  141. pyobo/sources/pharmgkb/utils.py +86 -0
  142. pyobo/sources/pid.py +1 -6
  143. pyobo/sources/pombase.py +6 -10
  144. pyobo/sources/pubchem.py +4 -9
  145. pyobo/sources/reactome.py +5 -11
  146. pyobo/sources/rgd.py +11 -16
  147. pyobo/sources/rhea.py +37 -36
  148. pyobo/sources/ror.py +69 -42
  149. pyobo/sources/selventa/__init__.py +0 -0
  150. pyobo/sources/selventa/schem.py +4 -7
  151. pyobo/sources/selventa/scomp.py +1 -6
  152. pyobo/sources/selventa/sdis.py +4 -7
  153. pyobo/sources/selventa/sfam.py +1 -6
  154. pyobo/sources/sgd.py +6 -11
  155. pyobo/sources/signor/__init__.py +7 -0
  156. pyobo/sources/signor/download.py +41 -0
  157. pyobo/sources/signor/signor_complexes.py +105 -0
  158. pyobo/sources/slm.py +12 -15
  159. pyobo/sources/umls/__init__.py +7 -1
  160. pyobo/sources/umls/__main__.py +0 -0
  161. pyobo/sources/umls/get_synonym_types.py +20 -4
  162. pyobo/sources/umls/sty.py +57 -0
  163. pyobo/sources/umls/synonym_types.tsv +1 -1
  164. pyobo/sources/umls/umls.py +18 -22
  165. pyobo/sources/unimod.py +46 -0
  166. pyobo/sources/uniprot/__init__.py +1 -1
  167. pyobo/sources/uniprot/uniprot.py +40 -32
  168. pyobo/sources/uniprot/uniprot_ptm.py +4 -34
  169. pyobo/sources/utils.py +3 -2
  170. pyobo/sources/wikipathways.py +7 -10
  171. pyobo/sources/zfin.py +5 -10
  172. pyobo/ssg/__init__.py +12 -16
  173. pyobo/ssg/base.html +0 -0
  174. pyobo/ssg/index.html +26 -13
  175. pyobo/ssg/term.html +12 -2
  176. pyobo/ssg/typedef.html +0 -0
  177. pyobo/struct/__init__.py +54 -8
  178. pyobo/struct/functional/__init__.py +1 -0
  179. pyobo/struct/functional/dsl.py +2572 -0
  180. pyobo/struct/functional/macros.py +423 -0
  181. pyobo/struct/functional/obo_to_functional.py +385 -0
  182. pyobo/struct/functional/ontology.py +270 -0
  183. pyobo/struct/functional/utils.py +112 -0
  184. pyobo/struct/reference.py +331 -136
  185. pyobo/struct/struct.py +1413 -643
  186. pyobo/struct/struct_utils.py +1078 -0
  187. pyobo/struct/typedef.py +162 -210
  188. pyobo/struct/utils.py +12 -5
  189. pyobo/struct/vocabulary.py +138 -0
  190. pyobo/utils/__init__.py +0 -0
  191. pyobo/utils/cache.py +13 -11
  192. pyobo/utils/io.py +17 -31
  193. pyobo/utils/iter.py +5 -5
  194. pyobo/utils/misc.py +41 -53
  195. pyobo/utils/ndex_utils.py +0 -0
  196. pyobo/utils/path.py +76 -70
  197. pyobo/version.py +3 -3
  198. {pyobo-0.11.2.dist-info → pyobo-0.12.0.dist-info}/METADATA +228 -229
  199. pyobo-0.12.0.dist-info/RECORD +202 -0
  200. pyobo-0.12.0.dist-info/WHEEL +4 -0
  201. {pyobo-0.11.2.dist-info → pyobo-0.12.0.dist-info}/entry_points.txt +1 -0
  202. pyobo-0.12.0.dist-info/licenses/LICENSE +21 -0
  203. pyobo/aws.py +0 -162
  204. pyobo/cli/aws.py +0 -47
  205. pyobo/identifier_utils.py +0 -142
  206. pyobo/normalizer.py +0 -232
  207. pyobo/registries/__init__.py +0 -16
  208. pyobo/registries/metaregistry.json +0 -507
  209. pyobo/registries/metaregistry.py +0 -135
  210. pyobo/sources/icd11.py +0 -105
  211. pyobo/xrefdb/__init__.py +0 -1
  212. pyobo/xrefdb/canonicalizer.py +0 -214
  213. pyobo/xrefdb/priority.py +0 -59
  214. pyobo/xrefdb/sources/__init__.py +0 -60
  215. pyobo/xrefdb/sources/biomappings.py +0 -36
  216. pyobo/xrefdb/sources/cbms2019.py +0 -91
  217. pyobo/xrefdb/sources/chembl.py +0 -83
  218. pyobo/xrefdb/sources/compath.py +0 -82
  219. pyobo/xrefdb/sources/famplex.py +0 -64
  220. pyobo/xrefdb/sources/gilda.py +0 -50
  221. pyobo/xrefdb/sources/intact.py +0 -113
  222. pyobo/xrefdb/sources/ncit.py +0 -133
  223. pyobo/xrefdb/sources/pubchem.py +0 -27
  224. pyobo/xrefdb/sources/wikidata.py +0 -116
  225. pyobo-0.11.2.dist-info/RECORD +0 -157
  226. pyobo-0.11.2.dist-info/WHEEL +0 -5
  227. pyobo-0.11.2.dist-info/top_level.txt +0 -1
@@ -0,0 +1,1078 @@
1
+ """Utiltites on top of the reference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime
6
+ import itertools as itt
7
+ import logging
8
+ from abc import ABC, abstractmethod
9
+ from collections import defaultdict
10
+ from collections.abc import Iterable, Mapping, Sequence
11
+ from typing import TYPE_CHECKING, Literal, NamedTuple, TypeAlias, overload
12
+
13
+ import curies
14
+ from curies import ReferenceTuple
15
+ from curies import vocabulary as _v
16
+ from curies.vocabulary import SynonymScope
17
+ from pydantic import BaseModel, ConfigDict
18
+ from ssslm import LiteralMapping
19
+ from typing_extensions import Self
20
+
21
+ from . import vocabulary as v
22
+ from .reference import (
23
+ OBOLiteral,
24
+ Reference,
25
+ Referenced,
26
+ comma_separate_references,
27
+ default_reference,
28
+ get_preferred_curie,
29
+ multi_reference_escape,
30
+ reference_escape,
31
+ reference_or_literal_to_str,
32
+ unspecified_matching,
33
+ )
34
+ from .utils import obo_escape_slim
35
+ from ..identifier_utils import (
36
+ NotCURIEError,
37
+ ParseError,
38
+ _is_valid_identifier,
39
+ _parse_str_or_curie_or_uri_helper,
40
+ )
41
+
42
+ if TYPE_CHECKING:
43
+ from pyobo.struct.struct import Synonym, TypeDef
44
+
45
+ __all__ = [
46
+ "AnnotationsDict",
47
+ "HasReferencesMixin",
48
+ "ReferenceHint",
49
+ "Stanza",
50
+ ]
51
+
52
+ logger = logging.getLogger(__name__)
53
+
54
+
55
+ class Annotation(NamedTuple):
56
+ """A tuple representing a predicate-object pair."""
57
+
58
+ predicate: Reference
59
+ value: Reference | OBOLiteral
60
+
61
+ @classmethod
62
+ def float(cls, predicate: Reference, value: float) -> Self:
63
+ """Return a literal property for a float."""
64
+ return cls(predicate, OBOLiteral.float(value))
65
+
66
+ @staticmethod
67
+ def _sort_key(x: Annotation):
68
+ return x.predicate, _reference_or_literal_key(x.value)
69
+
70
+
71
+ def _property_resolve(p: ReferenceHint, o: Reference | Referenced | OBOLiteral) -> Annotation:
72
+ p = _ensure_ref(p)
73
+ if isinstance(o, Referenced):
74
+ o = o.reference
75
+ return Annotation(p, o)
76
+
77
+
78
+ PropertiesHint: TypeAlias = dict[Reference, list[Reference | OBOLiteral]]
79
+ RelationsHint: TypeAlias = dict[Reference, list[Reference]]
80
+ AnnotationsDict: TypeAlias = dict[Annotation, list[Annotation]]
81
+ # note that an intersection is not valid in ROBOT with a literal, even though this _might_ make sense.
82
+ IntersectionOfHint: TypeAlias = list[Reference | tuple[Reference, Reference]]
83
+ UnionOfHint: TypeAlias = list[Reference]
84
+
85
+ StanzaType: TypeAlias = Literal["Term", "Instance", "TypeDef"]
86
+
87
+ stanza_type_to_prop: dict[StanzaType, Reference] = {
88
+ "Term": v.is_a,
89
+ "Instance": v.rdf_type,
90
+ "TypeDef": v.subproperty_of,
91
+ }
92
+
93
+ stanza_type_to_eq_prop: dict[StanzaType, Reference] = {
94
+ "Term": v.equivalent_class,
95
+ "Instance": v.owl_same_as,
96
+ "TypeDef": v.equivalent_property,
97
+ }
98
+
99
+
100
+ class HasReferencesMixin(ABC):
101
+ """A class that can report on the references it contains."""
102
+
103
+ def _get_prefixes(self) -> set[str]:
104
+ return set(self._get_references())
105
+
106
+ @abstractmethod
107
+ def _get_references(self) -> dict[str, set[Reference]]:
108
+ raise NotImplementedError
109
+
110
+
111
+ class Stanza(Referenced, HasReferencesMixin):
112
+ """A high-level class for stanzas."""
113
+
114
+ reference: Reference
115
+ relationships: RelationsHint
116
+ properties: PropertiesHint
117
+ xrefs: list[Reference]
118
+ parents: list[Reference]
119
+ intersection_of: IntersectionOfHint
120
+ equivalent_to: list[Reference]
121
+ union_of: UnionOfHint
122
+ subsets: list[Reference]
123
+ disjoint_from: list[Reference]
124
+ synonyms: list[Synonym]
125
+
126
+ type: StanzaType
127
+
128
+ _axioms: AnnotationsDict
129
+
130
+ #: An annotation for obsolescence. By default, is None, but this means that it is not obsolete.
131
+ is_obsolete: bool | None
132
+
133
+ #: A description of the entity
134
+ definition: str | None = None
135
+
136
+ @staticmethod
137
+ def _reference(
138
+ reference: Reference, ontology_prefix: str, add_name_comment: bool = False
139
+ ) -> str:
140
+ return reference_escape(
141
+ reference, ontology_prefix=ontology_prefix, add_name_comment=add_name_comment
142
+ )
143
+
144
+ def _get_prefixes(self) -> set[str]:
145
+ return set(self._get_references())
146
+
147
+ def _get_references(self) -> dict[str, set[Reference]]:
148
+ """Get all prefixes used by the typedef."""
149
+ rv: defaultdict[str, set[Reference]] = defaultdict(set)
150
+
151
+ def _add(r: Reference) -> None:
152
+ rv[r.prefix].add(r)
153
+
154
+ _add(self.reference)
155
+
156
+ for synonym in self.synonyms:
157
+ for prefix, references in synonym._get_references().items():
158
+ rv[prefix].update(references)
159
+
160
+ if self.xrefs:
161
+ # xrefs themselves added in the chain below
162
+ _add(v.has_dbxref)
163
+
164
+ for predicate, values in self.properties.items():
165
+ _add(predicate)
166
+ for value in values:
167
+ if isinstance(value, Reference):
168
+ _add(value)
169
+ elif isinstance(value, OBOLiteral):
170
+ _add(v._c(value.datatype))
171
+ for parent in itt.chain(
172
+ self.parents,
173
+ self.union_of,
174
+ self.equivalent_to,
175
+ self.disjoint_from,
176
+ self.subsets,
177
+ self.xrefs,
178
+ ):
179
+ _add(parent)
180
+ for intersection_of in self.intersection_of:
181
+ match intersection_of:
182
+ case Reference():
183
+ _add(intersection_of)
184
+ case (intersection_predicate, intersection_value):
185
+ _add(intersection_predicate)
186
+ _add(intersection_value)
187
+
188
+ for rel_predicate, rel_values in self.relationships.items():
189
+ _add(rel_predicate)
190
+ for r in rel_values:
191
+ _add(r)
192
+ for p_o, annotations_ in self._axioms.items():
193
+ _add(p_o.predicate)
194
+ if isinstance(p_o.value, Reference):
195
+ _add(p_o.value)
196
+ for prefix, references in _get_references_from_annotations(annotations_).items():
197
+ rv[prefix].update(references)
198
+ return rv
199
+
200
+ def get_literal_mappings(self) -> list[LiteralMapping]:
201
+ """Get synonym objects for this term, including one for its label."""
202
+ rv = [_convert_synoynym(self, synonym) for synonym in self.synonyms]
203
+ if self.reference.name:
204
+ rv.append(_get_stanza_name_synonym(self))
205
+ return rv
206
+
207
+ def append_relationship(
208
+ self,
209
+ typedef: ReferenceHint,
210
+ reference: ReferenceHint,
211
+ *,
212
+ annotations: Iterable[Annotation] | None = None,
213
+ ) -> Self:
214
+ """Append a relationship."""
215
+ typedef = _ensure_ref(typedef)
216
+ reference = _ensure_ref(reference)
217
+ self.relationships[typedef].append(reference)
218
+ self._extend_annotations(typedef, reference, annotations)
219
+ return self
220
+
221
+ def _extend_annotations(
222
+ self, p: Reference, o: Reference | OBOLiteral, annotations: Iterable[Annotation] | None
223
+ ) -> None:
224
+ if annotations is None:
225
+ return
226
+ for annotation in annotations:
227
+ self._append_annotation(p, o, annotation)
228
+
229
+ def _append_annotation(
230
+ self, p: ReferenceHint, o: Reference | OBOLiteral, annotation: Annotation
231
+ ) -> None:
232
+ self._axioms[_property_resolve(p, o)].append(annotation)
233
+
234
+ def append_equivalent(
235
+ self,
236
+ reference: ReferenceHint,
237
+ *,
238
+ annotations: Iterable[Annotation] | None = None,
239
+ ) -> Self:
240
+ """Append an equivalent class axiom."""
241
+ return self.append_relationship(
242
+ stanza_type_to_eq_prop[self.type], reference, annotations=annotations
243
+ )
244
+
245
+ def append_xref(
246
+ self,
247
+ reference: ReferenceHint,
248
+ *,
249
+ mapping_justification: Reference | None = None,
250
+ confidence: float | None = None,
251
+ contributor: Reference | None = None,
252
+ annotations: list[Annotation] | None = None,
253
+ ) -> Self:
254
+ """Append an xref."""
255
+ reference = _ensure_ref(reference)
256
+ self.xrefs.append(reference)
257
+ if annotations is None:
258
+ annotations = []
259
+ annotations.extend(
260
+ self._prepare_mapping_annotations(
261
+ mapping_justification=mapping_justification,
262
+ confidence=confidence,
263
+ contributor=contributor,
264
+ )
265
+ )
266
+ self._extend_annotations(v.has_dbxref, reference, annotations)
267
+ return self
268
+
269
+ def _prepare_mapping_annotations(
270
+ self,
271
+ *,
272
+ mapping_justification: Reference | None = None,
273
+ confidence: float | None = None,
274
+ contributor: Reference | None = None,
275
+ ) -> Iterable[Annotation]:
276
+ if mapping_justification is not None:
277
+ yield Annotation(v.mapping_has_justification, mapping_justification)
278
+ if contributor is not None:
279
+ yield Annotation(v.has_contributor, contributor)
280
+ if confidence is not None:
281
+ yield Annotation.float(v.mapping_has_confidence, confidence)
282
+
283
+ def append_parent(
284
+ self,
285
+ reference: ReferenceHint,
286
+ *,
287
+ annotations: Iterable[Annotation] | None = None,
288
+ ) -> Self:
289
+ """Add a parent to this entity."""
290
+ reference = _ensure_ref(reference)
291
+ if reference not in self.parents:
292
+ self.parents.append(reference)
293
+ self._extend_annotations(stanza_type_to_prop[self.type], reference, annotations)
294
+ return self
295
+
296
+ def append_intersection_of(
297
+ self,
298
+ /,
299
+ reference: ReferenceHint | tuple[ReferenceHint, ReferenceHint],
300
+ r2: ReferenceHint | None = None,
301
+ *,
302
+ annotations: Iterable[Annotation] | None = None,
303
+ ) -> Self:
304
+ """Append an intersection of."""
305
+ if r2 is not None:
306
+ if isinstance(reference, tuple):
307
+ raise TypeError
308
+ self.intersection_of.append((_ensure_ref(reference), _ensure_ref(r2)))
309
+ elif isinstance(reference, tuple):
310
+ self.intersection_of.append((_ensure_ref(reference[0]), _ensure_ref(reference[1])))
311
+ else:
312
+ self.intersection_of.append(_ensure_ref(reference))
313
+ return self
314
+
315
+ def append_union_of(self, reference: ReferenceHint) -> Self:
316
+ """Append to the "union of" list."""
317
+ self.union_of.append(_ensure_ref(reference))
318
+ return self
319
+
320
+ def append_equivalent_to(
321
+ self, reference: ReferenceHint, *, annotations: Iterable[Annotation] | None = None
322
+ ) -> Self:
323
+ """Append to the "equivalent to" list."""
324
+ reference = _ensure_ref(reference)
325
+ self.equivalent_to.append(reference)
326
+ self._extend_annotations(stanza_type_to_eq_prop[self.type], reference, annotations)
327
+ return self
328
+
329
+ def _iterate_intersection_of_obo(self, *, ontology_prefix: str) -> Iterable[str]:
330
+ for element in sorted(self.intersection_of, key=self._intersection_of_key):
331
+ match element:
332
+ case Reference():
333
+ end = reference_escape(
334
+ element, ontology_prefix=ontology_prefix, add_name_comment=True
335
+ )
336
+ case (predicate, object):
337
+ match object:
338
+ case Reference():
339
+ end = multi_reference_escape(
340
+ [predicate, object],
341
+ ontology_prefix=ontology_prefix,
342
+ add_name_comment=True,
343
+ )
344
+ case OBOLiteral():
345
+ raise NotImplementedError
346
+ case _:
347
+ raise TypeError
348
+ yield f"intersection_of: {end}"
349
+
350
+ @staticmethod
351
+ def _intersection_of_key(io: Reference | tuple[Reference, Reference]):
352
+ if isinstance(io, Reference):
353
+ return 0, io
354
+ else:
355
+ return 1, io
356
+
357
+ def _iterate_xref_obo(self, *, ontology_prefix) -> Iterable[str]:
358
+ for xref in sorted(self.xrefs):
359
+ xref_yv = f"xref: {reference_escape(xref, ontology_prefix=ontology_prefix, add_name_comment=False)}"
360
+ xref_yv += _get_obo_trailing_modifiers(
361
+ v.has_dbxref, xref, self._axioms, ontology_prefix=ontology_prefix
362
+ )
363
+ if xref.name:
364
+ xref_yv += f" ! {xref.name}"
365
+ yield xref_yv
366
+
367
+ def _get_annotations(
368
+ self, p: ReferenceHint, o: Reference | Referenced | OBOLiteral | str
369
+ ) -> list[Annotation]:
370
+ if isinstance(o, str):
371
+ o = OBOLiteral.string(o)
372
+ return self._axioms.get(_property_resolve(p, o), [])
373
+
374
+ def _get_annotation(
375
+ self, p: ReferenceHint, o: Reference | OBOLiteral, ap: Reference
376
+ ) -> Reference | OBOLiteral | None:
377
+ ap_norm = _ensure_ref(ap)
378
+ for annotation in self._get_annotations(p, o):
379
+ if annotation.predicate.pair == ap_norm.pair:
380
+ return annotation.value
381
+ return None
382
+
383
+ def append_property(
384
+ self, prop: Annotation, *, annotations: Iterable[Annotation] | None = None
385
+ ) -> Self:
386
+ """Annotate a property."""
387
+ self.properties[prop.predicate].append(prop.value)
388
+ self._extend_annotations(prop.predicate, prop.value, annotations)
389
+ return self
390
+
391
+ def annotate_literal(
392
+ self,
393
+ prop: ReferenceHint,
394
+ value: OBOLiteral,
395
+ *,
396
+ annotations: Iterable[Annotation] | None = None,
397
+ ) -> Self:
398
+ """Append an object annotation."""
399
+ prop = _ensure_ref(prop)
400
+ self.properties[prop].append(value)
401
+ self._extend_annotations(prop, value, annotations)
402
+ return self
403
+
404
+ def annotate_string(
405
+ self,
406
+ prop: ReferenceHint,
407
+ value: str,
408
+ *,
409
+ annotations: Iterable[Annotation] | None = None,
410
+ language: str | None = None,
411
+ ) -> Self:
412
+ """Append an object annotation."""
413
+ return self.annotate_literal(
414
+ prop, OBOLiteral.string(value, language=language), annotations=annotations
415
+ )
416
+
417
+ def annotate_boolean(
418
+ self,
419
+ prop: ReferenceHint,
420
+ value: bool,
421
+ *,
422
+ annotations: Iterable[Annotation] | None = None,
423
+ ) -> Self:
424
+ """Append an object annotation."""
425
+ return self.annotate_literal(prop, OBOLiteral.boolean(value), annotations=annotations)
426
+
427
+ def annotate_integer(
428
+ self,
429
+ prop: ReferenceHint,
430
+ value: int | str,
431
+ *,
432
+ annotations: Iterable[Annotation] | None = None,
433
+ ) -> Self:
434
+ """Append an object annotation."""
435
+ return self.annotate_literal(prop, OBOLiteral.integer(value), annotations=annotations)
436
+
437
+ def annotate_float(
438
+ self, prop: ReferenceHint, value: float, *, annotations: Iterable[Annotation] | None = None
439
+ ) -> Self:
440
+ """Append a float annotation."""
441
+ return self.annotate_literal(prop, OBOLiteral.float(value), annotations=annotations)
442
+
443
+ def annotate_decimal(
444
+ self, prop: ReferenceHint, value: float, *, annotations: Iterable[Annotation] | None = None
445
+ ) -> Self:
446
+ """Append a decimal annotation."""
447
+ return self.annotate_literal(prop, OBOLiteral.decimal(value), annotations=annotations)
448
+
449
+ def annotate_year(
450
+ self,
451
+ prop: ReferenceHint,
452
+ value: int | str,
453
+ *,
454
+ annotations: Iterable[Annotation] | None = None,
455
+ ) -> Self:
456
+ """Append a year annotation."""
457
+ return self.annotate_literal(prop, OBOLiteral.year(value), annotations=annotations)
458
+
459
+ def annotate_uri(
460
+ self, prop: ReferenceHint, value: str, *, annotations: Iterable[Annotation] | None = None
461
+ ) -> Self:
462
+ """Append a URI annotation."""
463
+ return self.annotate_literal(prop, OBOLiteral.uri(value), annotations=annotations)
464
+
465
+ def annotate_datetime(
466
+ self,
467
+ prop: ReferenceHint,
468
+ value: datetime.datetime | str,
469
+ *,
470
+ annotations: Iterable[Annotation] | None = None,
471
+ ) -> Self:
472
+ """Append a datetime annotation."""
473
+ return self.annotate_literal(prop, OBOLiteral.datetime(value), annotations=annotations)
474
+
475
+ def _iterate_obo_properties(
476
+ self,
477
+ *,
478
+ ontology_prefix: str,
479
+ skip_predicate_objects: Iterable[Reference] | None = None,
480
+ skip_predicate_literals: Iterable[Reference] | None = None,
481
+ typedefs: Mapping[ReferenceTuple, TypeDef],
482
+ ) -> Iterable[str]:
483
+ for line in _iterate_obo_relations(
484
+ # the type checker seems to be a bit confused, this is an okay typing since we're
485
+ # passing a more explicit version. The issue is that list is used for the typing,
486
+ # which means it can't narrow properly
487
+ self.properties, # type:ignore
488
+ self._axioms,
489
+ ontology_prefix=ontology_prefix,
490
+ skip_predicate_objects=skip_predicate_objects,
491
+ skip_predicate_literals=skip_predicate_literals,
492
+ typedefs=typedefs,
493
+ ):
494
+ yield f"property_value: {line}"
495
+
496
+ def _iterate_obo_relations(
497
+ self, *, ontology_prefix: str, typedefs: Mapping[ReferenceTuple, TypeDef]
498
+ ) -> Iterable[str]:
499
+ for line in _iterate_obo_relations(
500
+ # the type checker seems to be a bit confused, this is an okay typing since we're
501
+ # passing a more explicit version. The issue is that list is used for the typing,
502
+ # which means it can't narrow properly
503
+ self.relationships, # type:ignore
504
+ self._axioms,
505
+ ontology_prefix=ontology_prefix,
506
+ typedefs=typedefs,
507
+ ):
508
+ yield f"relationship: {line}"
509
+
510
+ def append_subset(self, subset: ReferenceHint) -> Self:
511
+ """Add a subset."""
512
+ self.subsets.append(_ensure_ref(subset))
513
+ return self
514
+
515
+ def append_disjoint_from(self, reference: ReferenceHint) -> Self:
516
+ """Add a disjoint from."""
517
+ self.disjoint_from.append(_ensure_ref(reference))
518
+ return self
519
+
520
+ def annotate_object(
521
+ self,
522
+ typedef: ReferenceHint,
523
+ value: ReferenceHint,
524
+ *,
525
+ annotations: Iterable[Annotation] | None = None,
526
+ ) -> Self:
527
+ """Append an object annotation."""
528
+ typedef = _ensure_ref(typedef)
529
+ value = _ensure_ref(value)
530
+ self.properties[typedef].append(value)
531
+ self._extend_annotations(typedef, value, annotations)
532
+ return self
533
+
534
+ def append_contributor(self, reference: ReferenceHint) -> Self:
535
+ """Append contributor."""
536
+ return self.annotate_object(v.has_contributor, reference)
537
+
538
+ def append_creation_date(self, date: datetime.datetime | str) -> Self:
539
+ """Append contributor."""
540
+ return self.annotate_datetime(v.obo_creation_date, date)
541
+
542
+ def get_see_also(self) -> list[Reference]:
543
+ """Get all see also objects."""
544
+ return self.get_property_objects(v.see_also)
545
+
546
+ def get_replaced_by(self) -> list[Reference]:
547
+ """Get all replaced by."""
548
+ return self.get_property_objects(v.term_replaced_by)
549
+
550
+ def append_replaced_by(
551
+ self, reference: Reference, *, annotations: Iterable[Annotation] | None = None
552
+ ) -> Self:
553
+ """Add a replaced by property."""
554
+ return self.annotate_object(v.term_replaced_by, reference, annotations=annotations)
555
+
556
+ def iterate_relations(self) -> Iterable[tuple[Reference, Reference]]:
557
+ """Iterate over pairs of typedefs and targets."""
558
+ for typedef, targets in sorted(self.relationships.items()):
559
+ for target in sorted(targets):
560
+ yield typedef, target
561
+
562
+ def iterate_object_properties(self) -> Iterable[tuple[Reference, Reference]]:
563
+ """Iterate over properties with references as their targets."""
564
+ for predicate, values in self.properties.items():
565
+ for value in values:
566
+ if isinstance(value, Reference):
567
+ yield predicate, value
568
+
569
+ def iterate_literal_properties(self) -> Iterable[tuple[Reference, OBOLiteral]]:
570
+ """Iterate over properties with literals as their targets."""
571
+ for predicate, values in self.properties.items():
572
+ for value in values:
573
+ if isinstance(value, OBOLiteral):
574
+ yield predicate, value
575
+
576
+ def get_relationships(self, typedef: ReferenceHint) -> list[Reference]:
577
+ """Get relationships from the given type."""
578
+ return self.relationships.get(_ensure_ref(typedef), [])
579
+
580
+ def get_relationship(self, typedef: ReferenceHint) -> Reference | None:
581
+ """Get a single relationship of the given type."""
582
+ r = self.get_relationships(typedef)
583
+ if not r:
584
+ return None
585
+ if len(r) > 1:
586
+ raise ValueError
587
+ return r[0]
588
+
589
+ def iterate_relation_targets(self, typedef: ReferenceHint) -> list[Reference]:
590
+ """Iterate over pairs of typedefs and targets."""
591
+ return sorted(self.relationships.get(_ensure_ref(typedef), []))
592
+
593
+ def get_property_annotations(self) -> list[Annotation]:
594
+ """Iterate over pairs of property and values."""
595
+ return [
596
+ Annotation(prop, value)
597
+ for prop, values in sorted(self.properties.items())
598
+ for value in sorted(values, key=_reference_or_literal_key)
599
+ ]
600
+
601
+ def get_property_values(self, typedef: ReferenceHint) -> list[Reference | OBOLiteral]:
602
+ """Iterate over references or values."""
603
+ return sorted(self.properties.get(_ensure_ref(typedef), []))
604
+
605
+ def get_property_objects(self, prop: ReferenceHint) -> list[Reference]:
606
+ """Get properties from the given key."""
607
+ return sorted(
608
+ reference
609
+ for reference in self.properties.get(_ensure_ref(prop), [])
610
+ if isinstance(reference, curies.Reference)
611
+ )
612
+
613
+ def append_exact_synonym(
614
+ self,
615
+ synonym: str | Synonym,
616
+ *,
617
+ type: Reference | Referenced | None = None,
618
+ provenance: Sequence[Reference | OBOLiteral] | None = None,
619
+ annotations: Iterable[Annotation] | None = None,
620
+ language: str | None = None,
621
+ ) -> Self:
622
+ """Add an exact synonym."""
623
+ return self.append_synonym(
624
+ synonym,
625
+ type=type,
626
+ specificity="EXACT",
627
+ provenance=provenance,
628
+ annotations=annotations,
629
+ language=language,
630
+ )
631
+
632
+ def append_synonym(
633
+ self,
634
+ synonym: str | Synonym,
635
+ *,
636
+ type: Reference | Referenced | None = None,
637
+ specificity: SynonymScope | None = None,
638
+ provenance: Sequence[Reference | OBOLiteral] | None = None,
639
+ annotations: Iterable[Annotation] | None = None,
640
+ language: str | None = None,
641
+ ) -> Self:
642
+ """Add a synonym."""
643
+ if isinstance(type, Referenced):
644
+ type = type.reference
645
+ if isinstance(synonym, str):
646
+ from pyobo.struct.struct import Synonym
647
+
648
+ synonym = Synonym(
649
+ synonym,
650
+ type=type,
651
+ specificity=specificity,
652
+ provenance=list(provenance or []),
653
+ annotations=list(annotations or []),
654
+ language=language,
655
+ )
656
+ self.synonyms.append(synonym)
657
+ return self
658
+
659
+ def append_alt(
660
+ self, alt: Reference, *, annotations: Iterable[Annotation] | None = None
661
+ ) -> Self:
662
+ """Add an alternative identifier."""
663
+ return self.annotate_object(v.alternative_term, alt, annotations=annotations)
664
+
665
+ def append_see_also(
666
+ self, reference: ReferenceHint, *, annotations: Iterable[Annotation] | None = None
667
+ ) -> Self:
668
+ """Add a see also property."""
669
+ _reference = _ensure_ref(reference)
670
+ return self.annotate_object(v.see_also, _reference, annotations=annotations)
671
+
672
+ def append_comment(
673
+ self,
674
+ value: str,
675
+ *,
676
+ annotations: Iterable[Annotation] | None = None,
677
+ language: str | None = None,
678
+ ) -> Self:
679
+ """Add a comment property."""
680
+ return self.annotate_string(v.comment, value, annotations=annotations, language=language)
681
+
682
+ @property
683
+ def alt_ids(self) -> Sequence[Reference]:
684
+ """Get alternative terms."""
685
+ return tuple(self.get_property_objects(v.alternative_term))
686
+
687
+ def get_edges(self) -> list[tuple[Reference, Reference]]:
688
+ """Get edges."""
689
+ return list(self._iter_edges())
690
+
691
+ def _iter_parents(self) -> Iterable[tuple[Reference, Reference]]:
692
+ parent_prop = stanza_type_to_prop[self.type]
693
+ for parent in itt.chain(self.parents, self.union_of):
694
+ yield parent_prop, parent
695
+
696
+ def _iter_intersections(self) -> Iterable[tuple[Reference, Reference]]:
697
+ parent_prop = stanza_type_to_prop[self.type]
698
+ for intersection_of in self.intersection_of:
699
+ match intersection_of:
700
+ case Reference():
701
+ yield parent_prop, intersection_of
702
+ case (predicate, target):
703
+ yield predicate, target
704
+
705
+ def _iter_edges(self) -> Iterable[tuple[Reference, Reference]]:
706
+ # The following are "object" properties, meaning
707
+ # they're part of the definition of the object
708
+ yield from self.iterate_relations()
709
+ yield from self._iter_parents()
710
+ yield from self._iter_intersections()
711
+ for equivalent_to in self.equivalent_to:
712
+ yield stanza_type_to_eq_prop[self.type], equivalent_to
713
+
714
+ # The following are "annotation" properties
715
+ for subset in self.subsets:
716
+ yield v.in_subset, subset
717
+ yield from self.iterate_object_properties()
718
+ for xref_reference in self.xrefs:
719
+ yield v.has_dbxref, xref_reference
720
+
721
+ # TODO disjoint_from
722
+
723
+ # docstr-coverage:excused `overload`
724
+ @overload
725
+ def get_mappings(
726
+ self, *, include_xrefs: bool = ..., add_context: Literal[True] = True
727
+ ) -> list[tuple[Reference, Reference, MappingContext]]: ...
728
+
729
+ # docstr-coverage:excused `overload`
730
+ @overload
731
+ def get_mappings(
732
+ self, *, include_xrefs: bool = ..., add_context: Literal[False] = False
733
+ ) -> list[tuple[Reference, Reference]]: ...
734
+
735
+ def get_mappings(
736
+ self, *, include_xrefs: bool = True, add_context: bool = False
737
+ ) -> list[tuple[Reference, Reference]] | list[tuple[Reference, Reference, MappingContext]]:
738
+ """Get mappings with preferred curies."""
739
+ rows = []
740
+ for predicate in v.extended_match_typedefs:
741
+ for xref_reference in itt.chain(
742
+ self.get_property_objects(predicate), self.get_relationships(predicate)
743
+ ):
744
+ rows.append((predicate, xref_reference))
745
+ if include_xrefs:
746
+ for xref_reference in self.xrefs:
747
+ rows.append((v.has_dbxref, xref_reference))
748
+ for equivalent_to in self.equivalent_to:
749
+ rows.append((v.equivalent_class, equivalent_to))
750
+ rv = sorted(set(rows))
751
+ if not add_context:
752
+ return rv
753
+ return [(k, v, self._get_mapping_context(k, v)) for k, v in rv]
754
+
755
+ def _get_object_annotation_target(
756
+ self, p: Reference, o: Reference | OBOLiteral, ap: Reference
757
+ ) -> Reference | None:
758
+ match self._get_annotation(p, o, ap):
759
+ case OBOLiteral():
760
+ raise TypeError
761
+ case Reference() as target:
762
+ return target
763
+ case None:
764
+ return None
765
+ case _:
766
+ raise TypeError
767
+
768
+ def _get_str_annotation_target(
769
+ self, p: Reference, o: Reference | OBOLiteral, ap: Reference
770
+ ) -> str | None:
771
+ match self._get_annotation(p, o, ap):
772
+ case OBOLiteral(value, _):
773
+ return value
774
+ case Reference():
775
+ raise TypeError
776
+ case None:
777
+ return None
778
+ case _:
779
+ raise TypeError
780
+
781
+ def _get_mapping_context(self, p: Reference, o: Reference) -> MappingContext:
782
+ return MappingContext(
783
+ justification=self._get_object_annotation_target(p, o, v.mapping_has_justification)
784
+ or unspecified_matching,
785
+ contributor=self._get_object_annotation_target(p, o, v.has_contributor),
786
+ confidence=self._get_str_annotation_target(p, o, v.mapping_has_confidence),
787
+ )
788
+
789
+ def _definition_fp(self) -> str:
790
+ definition = obo_escape_slim(self.definition) if self.definition else ""
791
+ dp = self._get_definition_provenance()
792
+ if dp:
793
+ return f'"{definition}" [{comma_separate_references(dp)}]'
794
+ else:
795
+ return f'"{definition}"'
796
+
797
+ def _get_definition_provenance(self) -> Sequence[Reference | OBOLiteral]:
798
+ if self.definition is None:
799
+ return []
800
+ return [
801
+ annotation.value
802
+ for annotation in self._get_annotations(v.has_description, self.definition)
803
+ if annotation.predicate.pair == v.has_dbxref.pair
804
+ ]
805
+
806
+ @property
807
+ def provenance(self) -> Sequence[Reference | OBOLiteral]:
808
+ """Get definition provenance."""
809
+ # return as a tuple to make sure nobody is appending on it
810
+ return (
811
+ *self.get_property_objects(v.has_citation),
812
+ # This gets all of the xrefs on _any_ axiom,
813
+ # which includes the definition provenance
814
+ *(
815
+ annotation.value
816
+ for annotation in itt.chain.from_iterable(self._axioms.values())
817
+ if annotation.predicate.pair == v.has_dbxref.pair
818
+ ),
819
+ )
820
+
821
+ def append_definition_xref(self, reference: ReferenceHint) -> Self:
822
+ """Add a reference to this term's definition."""
823
+ if not self.definition:
824
+ raise ValueError("can not append definition provenance if no definition is set")
825
+ self._append_annotation(
826
+ v.has_description,
827
+ OBOLiteral.string(self.definition),
828
+ Annotation(v.has_dbxref, _ensure_ref(reference)),
829
+ )
830
+ return self
831
+
832
+ def append_provenance(
833
+ self,
834
+ reference: Reference,
835
+ *,
836
+ annotations: Iterable[Annotation] | None = None,
837
+ ) -> Self:
838
+ """Append a citation."""
839
+ return self.annotate_object(v.has_citation, reference, annotations=annotations)
840
+
841
+
842
+ ReferenceHint: TypeAlias = (
843
+ Reference | Referenced | curies.Reference | curies.NamedReference | tuple[str, str] | str
844
+ )
845
+
846
+
847
+ def _ensure_ref(
848
+ reference: ReferenceHint,
849
+ *,
850
+ ontology_prefix: str | None = None,
851
+ ) -> Reference:
852
+ if isinstance(reference, Referenced):
853
+ return reference.reference
854
+ if isinstance(reference, tuple):
855
+ return Reference(prefix=reference[0], identifier=reference[1])
856
+ if isinstance(reference, Reference):
857
+ return reference
858
+ if isinstance(reference, curies.NamedReference):
859
+ return Reference(
860
+ prefix=reference.prefix, identifier=reference.identifier, name=reference.name
861
+ )
862
+ if isinstance(reference, curies.Reference):
863
+ return Reference(prefix=reference.prefix, identifier=reference.identifier)
864
+
865
+ match _parse_str_or_curie_or_uri_helper(reference, ontology_prefix=ontology_prefix):
866
+ case Reference() as parsed_reference:
867
+ return parsed_reference
868
+ case NotCURIEError() as exc:
869
+ if ontology_prefix and _is_valid_identifier(reference):
870
+ return default_reference(ontology_prefix, reference)
871
+ else:
872
+ raise exc
873
+ case ParseError() as exc:
874
+ raise exc
875
+
876
+ raise TypeError
877
+
878
+
879
+ def _chain_tag(
880
+ tag: str, chains: list[list[Reference]] | None, ontology_prefix: str
881
+ ) -> Iterable[str]:
882
+ for chain in chains or []:
883
+ yield f"{tag}: {multi_reference_escape(chain, ontology_prefix=ontology_prefix, add_name_comment=True)}"
884
+
885
+
886
+ def _tag_property_targets(
887
+ tag: str, stanza: Stanza, prod: ReferenceHint, *, ontology_prefix: str
888
+ ) -> Iterable[str]:
889
+ for x in stanza.get_property_values(_ensure_ref(prod)):
890
+ if isinstance(x, Reference):
891
+ yield f"{tag}: {reference_escape(x, ontology_prefix=ontology_prefix, add_name_comment=True)}"
892
+
893
+
894
+ def _iterate_obo_relations(
895
+ relations: Mapping[Reference, Sequence[Reference | OBOLiteral]],
896
+ annotations: AnnotationsDict,
897
+ *,
898
+ ontology_prefix: str,
899
+ skip_predicate_objects: Iterable[Reference] | None = None,
900
+ skip_predicate_literals: Iterable[Reference] | None = None,
901
+ typedefs: Mapping[ReferenceTuple, TypeDef],
902
+ ) -> Iterable[str]:
903
+ """Iterate over relations/property values for OBO."""
904
+ skip_predicate_objects = set(skip_predicate_objects or [])
905
+ skip_predicate_literals = set(skip_predicate_literals or [])
906
+ for predicate, values in sorted(relations.items()):
907
+ _typedef_warn(prefix=ontology_prefix, predicate=predicate, typedefs=typedefs)
908
+ pc = reference_escape(predicate, ontology_prefix=ontology_prefix)
909
+ start = f"{pc} "
910
+ for value in sorted(values, key=_reference_or_literal_key):
911
+ match value:
912
+ case OBOLiteral(dd, datatype, _language):
913
+ if predicate in skip_predicate_literals:
914
+ continue
915
+ # TODO how to clean/escape value?
916
+ end = f'"{dd}" {get_preferred_curie(datatype)}'
917
+ name = None
918
+ case curies.Reference(): # it's a reference
919
+ if predicate in skip_predicate_objects:
920
+ # this allows us to special case out iterating over
921
+ # ones that are configured with their own tags
922
+ continue
923
+ end = reference_escape(value, ontology_prefix=ontology_prefix)
924
+ name = value.name
925
+ case _:
926
+ raise TypeError(f"got unexpected value: {values}")
927
+ end += _get_obo_trailing_modifiers(
928
+ predicate, value, annotations, ontology_prefix=ontology_prefix
929
+ )
930
+ if predicate.name and name:
931
+ end += f" ! {predicate.name} {name}"
932
+ yield start + end
933
+
934
+
935
+ def _reference_or_literal_key(x: Reference | OBOLiteral) -> tuple[int, Reference | OBOLiteral]:
936
+ if isinstance(x, Reference):
937
+ return 0, x
938
+ else:
939
+ return 1, x
940
+
941
+
942
+ def _get_obo_trailing_modifiers(
943
+ p: ReferenceHint,
944
+ o: Reference | OBOLiteral,
945
+ annotations_dict: AnnotationsDict,
946
+ *,
947
+ ontology_prefix: str,
948
+ ) -> str:
949
+ """Lookup then format a sequence of annotations for OBO trailing modifiers."""
950
+ if annotations := annotations_dict.get(_property_resolve(p, o), []):
951
+ return _format_obo_trailing_modifiers(annotations, ontology_prefix=ontology_prefix)
952
+ return ""
953
+
954
+
955
+ def _format_obo_trailing_modifiers(
956
+ annotations: Sequence[Annotation], *, ontology_prefix: str
957
+ ) -> str:
958
+ """Format a sequence of annotations for OBO trailing modifiers.
959
+
960
+ :param annotations: A list of annnotations
961
+ :param ontology_prefix: The ontology prefix
962
+
963
+ :returns: The trailing modifiers string
964
+
965
+ See https://owlcollab.github.io/oboformat/doc/GO.format.obo-1_4.html#S.1.4 trailing
966
+ modifiers can be both annotations and some other implementation-specific things, so
967
+ split up the place where annotations are put in here.
968
+ """
969
+ modifiers: list[tuple[str, str]] = []
970
+ for prop in sorted(annotations, key=Annotation._sort_key):
971
+ left = reference_escape(prop.predicate, ontology_prefix=ontology_prefix)
972
+ match prop.value:
973
+ case Reference():
974
+ right = reference_escape(prop.value, ontology_prefix=ontology_prefix)
975
+ case OBOLiteral(value, _datatype, _language):
976
+ right = value
977
+ modifiers.append((left, right))
978
+ inner = ", ".join(f"{key}={value}" for key, value in modifiers)
979
+ return " {" + inner + "}"
980
+
981
+
982
+ #: A set of warnings, used to make sure we don't show the same one over and over
983
+ _TYPEDEF_WARNINGS: set[tuple[str, Reference]] = set()
984
+
985
+
986
+ def _typedef_warn(
987
+ prefix: str, predicate: Reference, typedefs: Mapping[ReferenceTuple, TypeDef]
988
+ ) -> None:
989
+ from pyobo.struct.typedef import default_typedefs
990
+
991
+ if predicate.pair in default_typedefs or predicate.pair in typedefs:
992
+ return None
993
+ key = prefix, predicate
994
+ if key not in _TYPEDEF_WARNINGS:
995
+ _TYPEDEF_WARNINGS.add(key)
996
+ if predicate.prefix == "obo":
997
+ # Throw our hands up in the air. By using `obo` as the prefix,
998
+ # we already threw using "real" definitions out the window
999
+ logger.warning(
1000
+ f"[{prefix}] predicate with OBO prefix not defined: {predicate.curie}."
1001
+ f"\n\tThis might be because you used an unqualified prefix in an OBO file, "
1002
+ f"which automatically gets an OBO prefix."
1003
+ )
1004
+ else:
1005
+ logger.warning(f"[{prefix}] typedef not defined: {predicate.curie}")
1006
+
1007
+
1008
+ class MappingContext(BaseModel):
1009
+ """Context for a mapping, corresponding to SSSOM."""
1010
+
1011
+ justification: Reference = unspecified_matching
1012
+ contributor: Reference | None = None
1013
+ confidence: float | None = None
1014
+
1015
+ model_config = ConfigDict(
1016
+ frozen=True, # Makes the model immutable and hashable
1017
+ )
1018
+
1019
+
1020
+ def _get_prefixes_from_annotations(annotations: Iterable[Annotation]) -> set[str]:
1021
+ return set(_get_references_from_annotations(annotations))
1022
+
1023
+
1024
+ def _get_references_from_annotations(
1025
+ annotations: Iterable[Annotation],
1026
+ ) -> dict[str, set[Reference]]:
1027
+ rv: defaultdict[str, set[Reference]] = defaultdict(set)
1028
+ for left, right in annotations:
1029
+ rv[left.prefix].add(left)
1030
+ if isinstance(right, Reference):
1031
+ rv[right.prefix].add(right)
1032
+ return dict(rv)
1033
+
1034
+
1035
+ def _get_stanza_name_synonym(stanza: Stanza) -> LiteralMapping:
1036
+ return LiteralMapping(
1037
+ text=stanza.reference.name,
1038
+ reference=stanza.reference,
1039
+ predicate=_v.has_label,
1040
+ type=None,
1041
+ provenance=[p for p in stanza.provenance if isinstance(p, curies.Reference)],
1042
+ contributor=None, # TODO
1043
+ comment=None, # TODO
1044
+ source=stanza.reference.prefix,
1045
+ date=None, # TODO
1046
+ )
1047
+
1048
+
1049
+ def _convert_synoynym(stanza: Stanza, synonym: Synonym) -> LiteralMapping:
1050
+ o = OBOLiteral.string(synonym.name, language=synonym.language)
1051
+ # TODO make this indexing reusable? similar code used for SSSOM export
1052
+ idx: dict[Reference, Reference | OBOLiteral] = {
1053
+ annotation.predicate: annotation.value
1054
+ for annotation in stanza._get_annotations(synonym.predicate, o)
1055
+ }
1056
+
1057
+ comment = _safe_str(idx.get(v.comment))
1058
+ contributor = _safe_str(idx.get(v.has_contributor))
1059
+ date = _safe_str(idx.get(v.has_date))
1060
+
1061
+ return LiteralMapping(
1062
+ text=synonym.name,
1063
+ language=synonym.language,
1064
+ reference=stanza.reference,
1065
+ predicate=synonym.predicate,
1066
+ type=synonym.type,
1067
+ provenance=[p for p in synonym.provenance if isinstance(p, curies.Reference)],
1068
+ contributor=contributor,
1069
+ comment=comment,
1070
+ source=stanza.reference.prefix,
1071
+ date=date,
1072
+ )
1073
+
1074
+
1075
+ def _safe_str(x: Reference | OBOLiteral | None) -> str | None:
1076
+ if x is None:
1077
+ return None
1078
+ return reference_or_literal_to_str(x)