obographs 0.0.3__py3-none-any.whl → 0.0.4__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.
obographs/__init__.py CHANGED
@@ -1,30 +1,70 @@
1
1
  """A python data model for OBO Graphs."""
2
2
 
3
- from .model import Graph, GraphDocument, Meta, Node, NodeType, Property, Synonym, Xref, read
3
+ from .model import (
4
+ Definition,
5
+ DomainRangeAxiom,
6
+ Edge,
7
+ EquivalentNodeSet,
8
+ ExistentialRestrictionExpression,
9
+ Graph,
10
+ GraphDocument,
11
+ LogicalDefinition,
12
+ Meta,
13
+ Node,
14
+ NodeType,
15
+ Property,
16
+ PropertyChainAxiom,
17
+ PropertyType,
18
+ Synonym,
19
+ Xref,
20
+ read,
21
+ )
4
22
  from .standardized import (
23
+ StandardizedBaseModel,
5
24
  StandardizedDefinition,
25
+ StandardizedDomainRangeAxiom,
6
26
  StandardizedEdge,
27
+ StandardizedEquivalentNodeSet,
28
+ StandardizedExistentialRestriction,
7
29
  StandardizedGraph,
30
+ StandardizedGraphDocument,
31
+ StandardizedLogicalDefinition,
8
32
  StandardizedMeta,
9
33
  StandardizedNode,
10
34
  StandardizedProperty,
35
+ StandardizedPropertyChainAxiom,
11
36
  StandardizedSynonym,
12
37
  StandardizedXref,
13
38
  )
14
39
 
15
40
  __all__ = [
41
+ "Definition",
42
+ "DomainRangeAxiom",
43
+ "Edge",
44
+ "EquivalentNodeSet",
45
+ "ExistentialRestrictionExpression",
16
46
  "Graph",
17
47
  "GraphDocument",
48
+ "LogicalDefinition",
18
49
  "Meta",
19
50
  "Node",
20
51
  "NodeType",
21
52
  "Property",
53
+ "PropertyChainAxiom",
54
+ "PropertyType",
55
+ "StandardizedBaseModel",
22
56
  "StandardizedDefinition",
57
+ "StandardizedDomainRangeAxiom",
23
58
  "StandardizedEdge",
59
+ "StandardizedEquivalentNodeSet",
60
+ "StandardizedExistentialRestriction",
24
61
  "StandardizedGraph",
62
+ "StandardizedGraphDocument",
63
+ "StandardizedLogicalDefinition",
25
64
  "StandardizedMeta",
26
65
  "StandardizedNode",
27
66
  "StandardizedProperty",
67
+ "StandardizedPropertyChainAxiom",
28
68
  "StandardizedSynonym",
29
69
  "StandardizedXref",
30
70
  "Synonym",
obographs/model.py CHANGED
@@ -9,27 +9,35 @@
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import gzip
12
13
  import json
13
14
  import logging
14
15
  from collections import defaultdict
15
16
  from pathlib import Path
16
- from typing import TYPE_CHECKING, Any, Literal, TypeAlias, overload
17
+ from typing import TYPE_CHECKING, Literal, TypeAlias, overload
17
18
 
19
+ import curies
20
+ from curies.vocabulary import SynonymScopeOIO
18
21
  from pydantic import BaseModel, Field
19
22
 
20
23
  if TYPE_CHECKING:
21
- import curies
22
-
23
24
  from .standardized import StandardizedGraph
24
25
 
25
26
  __all__ = [
26
27
  "Definition",
28
+ "DomainRangeAxiom",
27
29
  "Edge",
30
+ "EquivalentNodeSet",
31
+ "ExistentialRestrictionExpression",
28
32
  "Graph",
29
33
  "GraphDocument",
34
+ "LogicalDefinition",
30
35
  "Meta",
31
36
  "Node",
37
+ "NodeType",
32
38
  "Property",
39
+ "PropertyChainAxiom",
40
+ "PropertyType",
33
41
  "Synonym",
34
42
  "Xref",
35
43
  "read",
@@ -40,24 +48,12 @@ logger = logging.getLogger(__name__)
40
48
  OBO_URI_PREFIX = "http://purl.obolibrary.org/obo/"
41
49
  OBO_URI_PREFIX_LEN = len(OBO_URI_PREFIX)
42
50
 
43
- SynonymPredicate: TypeAlias = Literal[
44
- "hasExactSynonym",
45
- "hasBroadSynonym",
46
- "hasNarrowSynonym",
47
- "hasRelatedSynonym",
48
- ]
49
51
  NodeType: TypeAlias = Literal["CLASS", "PROPERTY", "INDIVIDUAL"]
50
52
 
51
- TimeoutHint = int | float | None
53
+ #: When node type is ``PROPERTY``, this is extra information
54
+ PropertyType: TypeAlias = Literal["ANNOTATION", "OBJECT", "DATA"]
52
55
 
53
- #: A mapping from OBO flat file format internal synonym types to OBO in OWL vocabulary
54
- #: identifiers. See https://owlcollab.github.io/oboformat/doc/GO.format.obo-1_4.html
55
- OBO_SYNONYM_TO_OIO: dict[str, SynonymPredicate] = {
56
- "EXACT": "hasExactSynonym",
57
- "BROAD": "hasBroadSynonym",
58
- "NARROW": "hasNarrowSynonym",
59
- "RELATED": "hasRelatedSynonym",
60
- }
56
+ TimeoutHint = int | float | None
61
57
 
62
58
 
63
59
  class Property(BaseModel):
@@ -91,7 +87,7 @@ class Synonym(BaseModel):
91
87
  """Represents a synonym inside an object meta."""
92
88
 
93
89
  val: str | None = Field(default=None)
94
- pred: str = Field(default="hasExactSynonym")
90
+ pred: SynonymScopeOIO = Field(default="hasExactSynonym")
95
91
  synonymType: str | None = Field(None, examples=["OMO:0003000"]) # noqa:N815
96
92
  xrefs: list[str] = Field(
97
93
  default_factory=list,
@@ -129,6 +125,51 @@ class Node(BaseModel):
129
125
  lbl: str | None = Field(None, description="The name of the node")
130
126
  meta: Meta | None = None
131
127
  type: NodeType | None = Field(None, description="Type of node")
128
+ propertyType: PropertyType | None = Field( # noqa:N815
129
+ None, description="Type of property, if the node type is a property"
130
+ )
131
+
132
+
133
+ class DomainRangeAxiom(BaseModel):
134
+ """Represents a domain/range axiom."""
135
+
136
+ predicateId: str # noqa:N815
137
+ domainClassIds: list[str] | None = None # noqa:N815
138
+ rangeClassIds: list[str] | None = None # noqa:N815
139
+ allValuesFromEdges: list[Edge] | None = None # noqa:N815
140
+ meta: Meta | None = None
141
+
142
+
143
+ class PropertyChainAxiom(BaseModel):
144
+ """Represents a property chain axiom."""
145
+
146
+ predicateId: str # noqa:N815
147
+ chainPredicateIds: list[str] # noqa:N815
148
+ meta: Meta | None = None
149
+
150
+
151
+ class ExistentialRestrictionExpression(BaseModel):
152
+ """Represents an existential restriction."""
153
+
154
+ propertyId: str # noqa:N815
155
+ fillerId: str # noqa:N815
156
+
157
+
158
+ class LogicalDefinition(BaseModel):
159
+ """Represents a logical definition chain axiom."""
160
+
161
+ definedClassId: str # noqa:N815
162
+ genusIds: list[str] | None = None # noqa:N815
163
+ restrictions: list[ExistentialRestrictionExpression] | None = None
164
+ meta: Meta | None = None
165
+
166
+
167
+ class EquivalentNodeSet(BaseModel):
168
+ """Represents a set of equivalent nodes."""
169
+
170
+ representativeNodeId: str # noqa:N815
171
+ nodeIds: list[str] # noqa:N815
172
+ meta: Meta | None = None
132
173
 
133
174
 
134
175
  class Graph(BaseModel):
@@ -138,10 +179,10 @@ class Graph(BaseModel):
138
179
  meta: Meta | None = None
139
180
  nodes: list[Node] = Field(default_factory=list)
140
181
  edges: list[Edge] = Field(default_factory=list)
141
- equivalentNodesSets: list[Any] = Field(default_factory=list) # noqa:N815
142
- logicalDefinitionAxioms: list[Any] = Field(default_factory=list) # noqa:N815
143
- domainRangeAxioms: list[Any] = Field(default_factory=list) # noqa:N815
144
- propertyChainAxioms: list[Any] = Field(default_factory=list) # noqa:N815
182
+ equivalentNodesSets: list[EquivalentNodeSet] = Field(default_factory=list) # noqa:N815
183
+ logicalDefinitionAxioms: list[LogicalDefinition] = Field(default_factory=list) # noqa:N815
184
+ domainRangeAxioms: list[DomainRangeAxiom] = Field(default_factory=list) # noqa:N815
185
+ propertyChainAxioms: list[PropertyChainAxiom] = Field(default_factory=list) # noqa:N815
145
186
 
146
187
  def standardize(self, converter: curies.Converter) -> StandardizedGraph:
147
188
  """Standardize the graph."""
@@ -210,12 +251,14 @@ def read(
210
251
 
211
252
  elif isinstance(source, str | Path):
212
253
  path = Path(source).expanduser().resolve()
213
- if path.is_file():
214
- if path.suffix.endswith(".gz"):
215
- raise NotImplementedError
216
- else:
217
- with path.open() as file:
218
- graph_document = GraphDocument.model_validate(json.load(file))
254
+ if not path.is_file():
255
+ raise FileNotFoundError
256
+ if path.suffix.endswith(".gz"):
257
+ with gzip.open(path, mode="rt") as file:
258
+ graph_document = GraphDocument.model_validate(json.load(file))
259
+ else:
260
+ with path.open() as file:
261
+ graph_document = GraphDocument.model_validate(json.load(file))
219
262
  else:
220
263
  raise TypeError(f"Unhandled source: {source}")
221
264
 
obographs/standardized.py CHANGED
@@ -3,20 +3,48 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from abc import ABC, abstractmethod
7
+ from typing import Generic, TypeVar, cast
6
8
 
7
- from curies import Converter, Reference, vocabulary
9
+ import curies.preprocessing
10
+ from curies import Converter, Reference, Triple, vocabulary
11
+ from curies.vocabulary import SynonymScopeOIO
8
12
  from pydantic import BaseModel, Field
9
13
  from typing_extensions import Self
10
14
 
11
- from obographs.model import Definition, Edge, Graph, Meta, Node, NodeType, Property, Synonym, Xref
15
+ from obographs.model import (
16
+ Definition,
17
+ DomainRangeAxiom,
18
+ Edge,
19
+ EquivalentNodeSet,
20
+ ExistentialRestrictionExpression,
21
+ Graph,
22
+ GraphDocument,
23
+ LogicalDefinition,
24
+ Meta,
25
+ Node,
26
+ NodeType,
27
+ Property,
28
+ PropertyChainAxiom,
29
+ PropertyType,
30
+ Synonym,
31
+ Xref,
32
+ )
12
33
 
13
34
  __all__ = [
35
+ "StandardizedBaseModel",
14
36
  "StandardizedDefinition",
37
+ "StandardizedDomainRangeAxiom",
15
38
  "StandardizedEdge",
39
+ "StandardizedEquivalentNodeSet",
40
+ "StandardizedExistentialRestriction",
16
41
  "StandardizedGraph",
42
+ "StandardizedGraphDocument",
43
+ "StandardizedLogicalDefinition",
17
44
  "StandardizedMeta",
18
45
  "StandardizedNode",
19
46
  "StandardizedProperty",
47
+ "StandardizedPropertyChainAxiom",
20
48
  "StandardizedSynonym",
21
49
  "StandardizedXref",
22
50
  ]
@@ -24,7 +52,33 @@ __all__ = [
24
52
  logger = logging.getLogger(__name__)
25
53
 
26
54
 
27
- class StandardizedProperty(BaseModel):
55
+ def _expand_list(references: list[Reference] | None, converter: Converter) -> list[str] | None:
56
+ if references is None or not references:
57
+ return None
58
+ return [converter.expand_reference(r, strict=True) for r in references]
59
+
60
+
61
+ X = TypeVar("X")
62
+
63
+
64
+ class StandardizedBaseModel(BaseModel, ABC, Generic[X]):
65
+ """A standardized property."""
66
+
67
+ @classmethod
68
+ @abstractmethod
69
+ def from_obograph_raw(
70
+ cls, obj: X, converter: Converter, *, strict: bool = False
71
+ ) -> Self | None:
72
+ """Instantiate by standardizing a raw OBO Graph object."""
73
+ raise NotImplementedError
74
+
75
+ @abstractmethod
76
+ def to_raw(self, converter: Converter) -> X:
77
+ """Create a raw object."""
78
+ raise NotImplementedError
79
+
80
+
81
+ class StandardizedProperty(StandardizedBaseModel[Property]):
28
82
  """A standardized property."""
29
83
 
30
84
  predicate: Reference
@@ -35,52 +89,85 @@ class StandardizedProperty(BaseModel):
35
89
  meta: StandardizedMeta | None = None
36
90
 
37
91
  @classmethod
38
- def from_obograph_raw(cls, prop: Property, converter: Converter) -> Self:
92
+ def from_obograph_raw(
93
+ cls, prop: Property, converter: Converter, *, strict: bool = False
94
+ ) -> Self:
39
95
  """Instantiate by standardizing a raw OBO Graph object."""
40
96
  if not prop.val or not prop.pred:
41
97
  raise ValueError
42
98
  value: Reference | str | None
43
- if not prop.val.startswith("http://") and not prop.val.startswith("https"):
44
- value = _curie_or_uri_to_ref(prop.val, converter)
99
+
100
+ if (
101
+ prop.val.startswith("http://")
102
+ or prop.val.startswith("https")
103
+ or converter.is_curie(prop.val)
104
+ or prop.val in BUILTINS
105
+ ):
106
+ value = _curie_or_uri_to_ref(prop.val, converter, strict=False) or prop.val
45
107
  else:
46
108
  value = prop.val
47
- if value is None:
48
- raise ValueError
49
109
  return cls(
50
- predicate=_curie_or_uri_to_ref(prop.pred, converter),
110
+ predicate=_curie_or_uri_to_ref(prop.pred, converter, strict=strict),
51
111
  value=value,
52
112
  )
53
113
 
114
+ def to_raw(self, converter: Converter) -> Property:
115
+ """Create a raw object."""
116
+ return Property(
117
+ pred=converter.expand_reference(self.predicate),
118
+ val=converter.expand_reference(self.value)
119
+ if isinstance(self.value, Reference)
120
+ else self.value,
121
+ xrefs=_expand_list(self.xrefs, converter),
122
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
123
+ )
124
+
54
125
 
55
- class StandardizedDefinition(BaseModel):
126
+ class StandardizedDefinition(StandardizedBaseModel[Definition]):
56
127
  """A standardized definition."""
57
128
 
58
129
  value: str | None = Field(default=None)
59
130
  xrefs: list[Reference] | None = Field(default=None)
60
131
 
61
132
  @classmethod
62
- def from_obograph_raw(cls, definition: Definition | None, converter: Converter) -> Self | None:
63
- """Instantiate by standardizing a raw OBO Graph object."""
133
+ def from_obograph_raw(
134
+ cls, definition: Definition | None, converter: Converter, *, strict: bool = False
135
+ ) -> Self | None:
136
+ """Parse a raw object."""
64
137
  if definition is None:
65
138
  return None
66
139
  return cls(
67
140
  value=definition.val,
68
- xrefs=_parse_list(definition.xrefs, converter),
141
+ xrefs=_parse_list(definition.xrefs, converter, strict=strict),
142
+ )
143
+
144
+ def to_raw(self, converter: Converter) -> Definition:
145
+ """Create a raw object."""
146
+ return Definition(
147
+ val=self.value,
148
+ xrefs=_expand_list(self.xrefs, converter),
69
149
  )
70
150
 
71
151
 
72
- class StandardizedXref(BaseModel):
152
+ class StandardizedXref(StandardizedBaseModel[Xref]):
73
153
  """A standardized database cross-reference."""
74
154
 
75
155
  reference: Reference
76
156
 
77
157
  @classmethod
78
- def from_obograph_raw(cls, xref: Xref, converter: Converter) -> Self:
158
+ def from_obograph_raw(cls, xref: Xref, converter: Converter, *, strict: bool = False) -> Self:
79
159
  """Instantiate by standardizing a raw OBO Graph object."""
80
- return cls(reference=_curie_or_uri_to_ref(xref.val, converter))
160
+ reference = _curie_or_uri_to_ref(xref.val, converter, strict=strict)
161
+ if reference is None:
162
+ raise ValueError(f"could not parse xref: {xref.val}")
163
+ return cls(reference=reference)
81
164
 
165
+ def to_raw(self, converter: Converter) -> Xref:
166
+ """Create a raw object."""
167
+ return Xref(val=self.reference.curie)
82
168
 
83
- class StandardizedSynonym(BaseModel):
169
+
170
+ class StandardizedSynonym(StandardizedBaseModel[Synonym]):
84
171
  """A standardized synonym."""
85
172
 
86
173
  text: str
@@ -89,17 +176,31 @@ class StandardizedSynonym(BaseModel):
89
176
  xrefs: list[Reference] | None = None
90
177
 
91
178
  @classmethod
92
- def from_obograph_raw(cls, synonym: Synonym, converter: Converter) -> Self:
179
+ def from_obograph_raw(
180
+ cls, synonym: Synonym, converter: Converter, *, strict: bool = False
181
+ ) -> Self:
93
182
  """Instantiate by standardizing a raw OBO Graph object."""
94
183
  return cls(
95
184
  text=synonym.val,
96
185
  predicate=Reference(prefix="oboInOwl", identifier=synonym.pred),
97
- type=synonym.synonymType and _curie_or_uri_to_ref(synonym.synonymType, converter),
98
- xrefs=_parse_list(synonym.xrefs, converter),
186
+ type=synonym.synonymType
187
+ and _curie_or_uri_to_ref(synonym.synonymType, converter, strict=strict),
188
+ xrefs=_parse_list(synonym.xrefs, converter, strict=strict),
189
+ )
190
+
191
+ def to_raw(self, converter: Converter) -> Synonym:
192
+ """Create a raw object."""
193
+ if self.predicate.prefix.lower() != "oboinowl":
194
+ raise ValueError
195
+ return Synonym(
196
+ val=self.text,
197
+ pred=cast(SynonymScopeOIO, self.predicate.identifier),
198
+ synonymType=converter.expand_reference(self.type) if self.type is not None else None,
199
+ xrefs=_expand_list(self.xrefs, converter) or [],
99
200
  )
100
201
 
101
202
 
102
- class StandardizedMeta(BaseModel):
203
+ class StandardizedMeta(StandardizedBaseModel[Meta]):
103
204
  """A standardized meta object."""
104
205
 
105
206
  definition: StandardizedDefinition | None
@@ -113,7 +214,7 @@ class StandardizedMeta(BaseModel):
113
214
 
114
215
  @classmethod
115
216
  def from_obograph_raw( # noqa:C901
116
- cls, meta: Meta | None, converter: Converter, flag: str = ""
217
+ cls, meta: Meta | None, converter: Converter, flag: str = "", strict: bool = False
117
218
  ) -> Self | None:
118
219
  """Instantiate by standardizing a raw OBO Graph object."""
119
220
  if meta is None:
@@ -123,8 +224,10 @@ class StandardizedMeta(BaseModel):
123
224
  for raw_xref in meta.xrefs or []:
124
225
  if raw_xref.val:
125
226
  try:
126
- st_xref = StandardizedXref.from_obograph_raw(raw_xref, converter)
227
+ st_xref = StandardizedXref.from_obograph_raw(raw_xref, converter, strict=strict)
127
228
  except ValueError:
229
+ if strict:
230
+ raise
128
231
  logger.debug("[%s] failed to standardize xref: %s", flag, raw_xref)
129
232
  else:
130
233
  xrefs.append(st_xref)
@@ -133,8 +236,10 @@ class StandardizedMeta(BaseModel):
133
236
  for raw_synonym in meta.synonyms or []:
134
237
  if raw_synonym.val:
135
238
  try:
136
- s = StandardizedSynonym.from_obograph_raw(raw_synonym, converter)
239
+ s = StandardizedSynonym.from_obograph_raw(raw_synonym, converter, strict=strict)
137
240
  except ValueError:
241
+ if strict:
242
+ raise
138
243
  logger.debug("[%s] failed to standardize synonym: %s", flag, raw_synonym)
139
244
  else:
140
245
  synonyms.append(s)
@@ -143,15 +248,23 @@ class StandardizedMeta(BaseModel):
143
248
  for raw_prop in meta.basicPropertyValues or []:
144
249
  if raw_prop.val and raw_prop.pred:
145
250
  try:
146
- prop = StandardizedProperty.from_obograph_raw(raw_prop, converter)
251
+ prop = StandardizedProperty.from_obograph_raw(
252
+ raw_prop, converter, strict=strict
253
+ )
147
254
  except ValueError:
255
+ if strict:
256
+ raise
148
257
  logger.debug("[%s] failed to standardize property: %s", flag, raw_prop)
149
258
  else:
150
259
  props.append(prop)
151
260
 
152
261
  return cls(
153
- definition=StandardizedDefinition.from_obograph_raw(meta.definition, converter),
154
- subsets=[_curie_or_uri_to_ref(subset, converter) for subset in meta.subsets]
262
+ definition=StandardizedDefinition.from_obograph_raw(
263
+ meta.definition, converter, strict=strict
264
+ ),
265
+ subsets=[
266
+ _curie_or_uri_to_ref(subset, converter, strict=strict) for subset in meta.subsets
267
+ ]
155
268
  if meta.subsets
156
269
  else None,
157
270
  xrefs=xrefs or None,
@@ -162,31 +275,69 @@ class StandardizedMeta(BaseModel):
162
275
  properties=props or None,
163
276
  )
164
277
 
278
+ def to_raw(self, converter: Converter) -> Meta:
279
+ """Create a raw object."""
280
+ return Meta(
281
+ definition=self.definition.to_raw(converter)
282
+ if self.definition and self.definition.value
283
+ else None,
284
+ subsets=_expand_list(self.subsets, converter),
285
+ xrefs=[xref.to_raw(converter) for xref in self.xrefs] if self.xrefs else None,
286
+ synonyms=[s.to_raw(converter) for s in self.synonyms] if self.synonyms else None,
287
+ comments=self.comments,
288
+ version=self.version, # TODO might need some kind of expansion?
289
+ deprecated=self.deprecated,
290
+ basicPropertyValues=[p.to_raw(converter) for p in self.properties]
291
+ if self.properties
292
+ else None,
293
+ )
294
+
165
295
 
166
- class StandardizedNode(BaseModel):
296
+ class StandardizedNode(StandardizedBaseModel[Node]):
167
297
  """A standardized node."""
168
298
 
169
299
  reference: Reference
170
300
  label: str | None = Field(None)
171
301
  meta: StandardizedMeta | None = None
172
302
  type: NodeType | None = Field(None, description="Type of node")
303
+ property_type: PropertyType | None = Field(
304
+ None, description="Type of property, if the node type is a property"
305
+ )
173
306
 
174
307
  @classmethod
175
- def from_obograph_raw(cls, node: Node, converter: Converter) -> Self | None:
308
+ def from_obograph_raw(
309
+ cls, node: Node, converter: Converter, *, strict: bool = False
310
+ ) -> Self | None:
176
311
  """Instantiate by standardizing a raw OBO Graph object."""
177
- reference = _curie_or_uri_to_ref(node.id, converter)
312
+ reference = _curie_or_uri_to_ref(node.id, converter, strict=strict)
178
313
  if reference is None:
314
+ if strict:
315
+ raise ValueError(f"failed to parse node's ID: {node.id}")
179
316
  logger.warning("failed to parse node's ID %s", node.id)
180
317
  return None
318
+
181
319
  return cls(
182
320
  reference=reference,
183
321
  label=node.lbl,
184
- meta=StandardizedMeta.from_obograph_raw(node.meta, converter, flag=reference.curie),
322
+ meta=StandardizedMeta.from_obograph_raw(
323
+ node.meta, converter, flag=reference.curie, strict=strict
324
+ ),
185
325
  type=node.type,
326
+ property_type=node.propertyType,
327
+ )
328
+
329
+ def to_raw(self, converter: Converter) -> Node:
330
+ """Create a raw object."""
331
+ return Node(
332
+ id=converter.expand_reference(self.reference),
333
+ lbl=self.label,
334
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
335
+ type=self.type,
336
+ propertyType=self.property_type,
186
337
  )
187
338
 
188
339
 
189
- class StandardizedEdge(BaseModel):
340
+ class StandardizedEdge(Triple, StandardizedBaseModel[Edge]):
190
341
  """A standardized edge."""
191
342
 
192
343
  subject: Reference
@@ -195,18 +346,26 @@ class StandardizedEdge(BaseModel):
195
346
  meta: StandardizedMeta | None = None
196
347
 
197
348
  @classmethod
198
- def from_obograph_raw(cls, edge: Edge, converter: Converter) -> Self | None:
349
+ def from_obograph_raw(
350
+ cls, edge: Edge, converter: Converter, *, strict: bool = False
351
+ ) -> Self | None:
199
352
  """Instantiate by standardizing a raw OBO Graph object."""
200
- subject = _curie_or_uri_to_ref(edge.sub, converter)
353
+ subject = _curie_or_uri_to_ref(edge.sub, converter, strict=strict)
201
354
  if not subject:
355
+ if strict:
356
+ raise ValueError
202
357
  logger.warning("failed to parse edge's subject %s", edge.sub)
203
358
  return None
204
- predicate = _curie_or_uri_to_ref(edge.pred, converter)
359
+ predicate = _curie_or_uri_to_ref(edge.pred, converter, strict=strict)
205
360
  if not predicate:
361
+ if strict:
362
+ raise ValueError
206
363
  logger.warning("failed to parse edge's predicate %s", edge.pred)
207
364
  return None
208
- obj = _curie_or_uri_to_ref(edge.obj, converter)
365
+ obj = _curie_or_uri_to_ref(edge.obj, converter, strict=strict)
209
366
  if not obj:
367
+ if strict:
368
+ raise ValueError
210
369
  logger.warning("failed to parse edge's object %s", edge.obj)
211
370
  return None
212
371
  return cls(
@@ -214,12 +373,178 @@ class StandardizedEdge(BaseModel):
214
373
  predicate=predicate,
215
374
  object=obj,
216
375
  meta=StandardizedMeta.from_obograph_raw(
217
- edge.meta, converter, flag=f"{subject.curie} {predicate.curie} {obj.curie}"
376
+ edge.meta,
377
+ converter,
378
+ flag=f"{subject.curie} {predicate.curie} {obj.curie}",
379
+ strict=strict,
218
380
  ),
219
381
  )
220
382
 
383
+ def to_raw(self, converter: Converter) -> Edge:
384
+ """Create a raw object."""
385
+ if self.predicate in REVERSE_BUILTINS:
386
+ predicate = REVERSE_BUILTINS[self.predicate]
387
+ else:
388
+ predicate = converter.expand_reference(self.predicate, strict=True)
389
+
390
+ return Edge(
391
+ sub=converter.expand_reference(self.subject),
392
+ pred=predicate,
393
+ obj=converter.expand_reference(self.object),
394
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
395
+ )
396
+
397
+
398
+ class StandardizedDomainRangeAxiom(StandardizedBaseModel[DomainRangeAxiom]):
399
+ """Represents a domain/range axiom."""
400
+
401
+ predicate: Reference
402
+ domains: list[Reference] = Field(default_factory=list)
403
+ ranges: list[Reference] = Field(default_factory=list)
404
+ all_values_from_edges: list[StandardizedEdge] = Field(default_factory=list)
405
+ meta: StandardizedMeta | None = None
406
+
407
+ @classmethod
408
+ def from_obograph_raw(
409
+ cls, obj: DomainRangeAxiom, converter: Converter, *, strict: bool = False
410
+ ) -> Self | None:
411
+ """Parse a raw object."""
412
+ return cls(
413
+ predicate=_curie_or_uri_to_ref(obj.predicateId, converter, strict=strict),
414
+ domains=_parse_list(obj.domainClassIds, converter, strict=strict) or [],
415
+ ranges=_parse_list(obj.rangeClassIds, converter, strict=strict) or [],
416
+ all_values_from_edges=[
417
+ StandardizedEdge.from_obograph_raw(edge, converter, strict=strict)
418
+ for edge in obj.allValuesFromEdges or []
419
+ ],
420
+ meta=StandardizedMeta.from_obograph_raw(obj.meta, converter, strict=strict),
421
+ )
422
+
423
+ def to_raw(self, converter: Converter) -> DomainRangeAxiom:
424
+ """Create a raw object."""
425
+ return DomainRangeAxiom(
426
+ predicateId=converter.expand_reference(self.predicate),
427
+ domainClassIds=_expand_list(self.domains, converter),
428
+ rangeClassIds=_expand_list(self.ranges, converter),
429
+ allValuesFromEdges=[edge.to_raw(converter) for edge in self.all_values_from_edges]
430
+ if self.all_values_from_edges
431
+ else None,
432
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
433
+ )
434
+
435
+
436
+ class StandardizedPropertyChainAxiom(StandardizedBaseModel[PropertyChainAxiom]):
437
+ """Represents a property chain axiom."""
438
+
439
+ predicate: Reference
440
+ chain: list[Reference] = Field(default_factory=list)
441
+ meta: StandardizedMeta | None = None
442
+
443
+ @classmethod
444
+ def from_obograph_raw(
445
+ cls, obj: PropertyChainAxiom, converter: Converter, *, strict: bool = False
446
+ ) -> Self | None:
447
+ """Parse a raw object."""
448
+ return cls(
449
+ predicate=_curie_or_uri_to_ref(obj.predicateId, converter, strict=strict),
450
+ chain=_parse_list(obj.chainPredicateIds, converter, strict=strict),
451
+ meta=StandardizedMeta.from_obograph_raw(obj.meta, converter, strict=strict),
452
+ )
453
+
454
+ def to_raw(self, converter: Converter) -> PropertyChainAxiom:
455
+ """Create a raw object."""
456
+ return PropertyChainAxiom(
457
+ predicateId=converter.expand_reference(self.predicate),
458
+ chainPredicateIds=_expand_list(self.chain, converter),
459
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
460
+ )
461
+
462
+
463
+ class StandardizedEquivalentNodeSet(StandardizedBaseModel[EquivalentNodeSet]):
464
+ """Represents an equivalence set."""
465
+
466
+ node: Reference
467
+ equivalents: list[Reference] = Field(default_factory=list)
468
+ meta: StandardizedMeta | None = None
469
+
470
+ @classmethod
471
+ def from_obograph_raw(
472
+ cls, obj: EquivalentNodeSet, converter: Converter, *, strict: bool = False
473
+ ) -> Self | None:
474
+ """Parse a raw object."""
475
+ return cls(
476
+ node=_curie_or_uri_to_ref(obj.representativeNodeId, converter, strict=strict),
477
+ equivalents=_parse_list(obj.nodeIds, converter, strict=strict),
478
+ meta=StandardizedMeta.from_obograph_raw(obj.meta, converter, strict=strict),
479
+ )
480
+
481
+ def to_raw(self, converter: Converter) -> EquivalentNodeSet:
482
+ """Create a raw object."""
483
+ return EquivalentNodeSet(
484
+ representativeNodeId=converter.expand_reference(self.node),
485
+ nodeIds=_expand_list(self.equivalents, converter),
486
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
487
+ )
488
+
489
+
490
+ class StandardizedExistentialRestriction(StandardizedBaseModel[ExistentialRestrictionExpression]):
491
+ """Represents an existential restriction expression."""
492
+
493
+ predicate: Reference
494
+ target: Reference
495
+
496
+ @classmethod
497
+ def from_obograph_raw(
498
+ cls, obj: ExistentialRestrictionExpression, converter: Converter, *, strict: bool = False
499
+ ) -> Self | None:
500
+ """Parse a raw object."""
501
+ return cls(
502
+ predicate=_curie_or_uri_to_ref(obj.propertyId, converter, strict=strict),
503
+ target=_curie_or_uri_to_ref(obj.fillerId, converter, strict=strict),
504
+ )
505
+
506
+ def to_raw(self, converter: Converter) -> ExistentialRestrictionExpression:
507
+ """Create a raw object."""
508
+ return ExistentialRestrictionExpression(
509
+ propertyId=converter.expand_reference(self.predicate),
510
+ fillerId=converter.expand_reference(self.target),
511
+ )
512
+
221
513
 
222
- class StandardizedGraph(BaseModel):
514
+ class StandardizedLogicalDefinition(StandardizedBaseModel[LogicalDefinition]):
515
+ """Represents a logical definition axiom."""
516
+
517
+ node: Reference
518
+ geni: list[Reference] = Field(default_factory=list)
519
+ restrictions: list[StandardizedExistentialRestriction] = Field(default_factory=list)
520
+ meta: StandardizedMeta | None = None
521
+
522
+ @classmethod
523
+ def from_obograph_raw(
524
+ cls, obj: LogicalDefinition, converter: Converter, *, strict: bool = False
525
+ ) -> Self | None:
526
+ """Parse a raw object."""
527
+ return cls(
528
+ node=_curie_or_uri_to_ref(obj.definedClassId, converter, strict=strict),
529
+ geni=_parse_list(obj.genusIds, converter, strict=strict),
530
+ restrictions=[
531
+ StandardizedExistentialRestriction.from_obograph_raw(r, converter, strict=strict)
532
+ for r in obj.restrictions or []
533
+ ],
534
+ meta=StandardizedMeta.from_obograph_raw(obj.meta, converter, strict=strict),
535
+ )
536
+
537
+ def to_raw(self, converter: Converter) -> LogicalDefinition:
538
+ """Create a raw object."""
539
+ return LogicalDefinition(
540
+ definedClassId=converter.expand_reference(self.node),
541
+ genusIds=_expand_list(self.geni, converter),
542
+ restrictions=[r.to_raw(converter) for r in self.restrictions],
543
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
544
+ )
545
+
546
+
547
+ class StandardizedGraph(StandardizedBaseModel[Graph]):
223
548
  """A standardized graph."""
224
549
 
225
550
  id: str | None = None
@@ -227,26 +552,62 @@ class StandardizedGraph(BaseModel):
227
552
  nodes: list[StandardizedNode] = Field(default_factory=list)
228
553
  edges: list[StandardizedEdge] = Field(default_factory=list)
229
554
 
230
- # TODO other bits
555
+ equivalent_node_sets: list[StandardizedEquivalentNodeSet] = Field(default_factory=list)
556
+ logical_definition_axioms: list[StandardizedLogicalDefinition] = Field(default_factory=list)
557
+ domain_range_axioms: list[StandardizedDomainRangeAxiom] = Field(default_factory=list)
558
+ property_chain_axioms: list[StandardizedPropertyChainAxiom] = Field(default_factory=list)
231
559
 
232
560
  @classmethod
233
- def from_obograph_raw(cls, graph: Graph, converter: Converter) -> Self:
561
+ def from_obograph_raw(cls, graph: Graph, converter: Converter, *, strict: bool = False) -> Self:
234
562
  """Instantiate by standardizing a raw OBO Graph object."""
235
563
  return cls(
236
564
  id=graph.id,
237
- meta=StandardizedMeta.from_obograph_raw(graph.meta, converter, flag=graph.id or ""),
565
+ meta=StandardizedMeta.from_obograph_raw(
566
+ graph.meta, converter, flag=graph.id or "", strict=strict
567
+ ),
238
568
  nodes=[
239
569
  s_node
240
570
  for node in graph.nodes
241
- if (s_node := StandardizedNode.from_obograph_raw(node, converter))
571
+ if (s_node := StandardizedNode.from_obograph_raw(node, converter, strict=strict))
242
572
  ],
243
573
  edges=[
244
574
  s_edge
245
575
  for edge in graph.edges
246
- if (s_edge := StandardizedEdge.from_obograph_raw(edge, converter))
576
+ if (s_edge := StandardizedEdge.from_obograph_raw(edge, converter, strict=strict))
577
+ ],
578
+ equivalent_node_sets=[
579
+ StandardizedEquivalentNodeSet.from_obograph_raw(e, converter, strict=strict)
580
+ for e in graph.equivalentNodesSets or []
581
+ ],
582
+ logical_definition_axioms=[
583
+ StandardizedLogicalDefinition.from_obograph_raw(e, converter, strict=strict)
584
+ for e in graph.logicalDefinitionAxioms or []
585
+ ],
586
+ property_chain_axioms=[
587
+ StandardizedPropertyChainAxiom.from_obograph_raw(e, converter, strict=strict)
588
+ for e in graph.propertyChainAxioms or []
589
+ ],
590
+ domain_range_axioms=[
591
+ StandardizedDomainRangeAxiom.from_obograph_raw(e, converter, strict=strict)
592
+ for e in graph.domainRangeAxioms or []
247
593
  ],
248
594
  )
249
595
 
596
+ def to_raw(self, converter: Converter) -> Graph:
597
+ """Create a raw object."""
598
+ return Graph(
599
+ id=self.id,
600
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
601
+ nodes=[node.to_raw(converter) for node in self.nodes],
602
+ edges=[edge.to_raw(converter) for edge in self.edges],
603
+ logicalDefinitionAxioms=[
604
+ axiom.to_raw(converter) for axiom in self.logical_definition_axioms
605
+ ],
606
+ propertyChainAxioms=[axiom.to_raw(converter) for axiom in self.property_chain_axioms],
607
+ domainRangeAxioms=[axiom.to_raw(converter) for axiom in self.domain_range_axioms],
608
+ equivalentNodesSets=[axiom.to_raw(converter) for axiom in self.equivalent_node_sets],
609
+ )
610
+
250
611
  def _get_property(self, predicate: Reference) -> str | Reference | None:
251
612
  if self.meta is None:
252
613
  return None
@@ -266,29 +627,73 @@ class StandardizedGraph(BaseModel):
266
627
  return r
267
628
 
268
629
 
269
- def _parse_list(curie_or_uris: list[str] | None, converter: Converter) -> list[Reference] | None:
630
+ class StandardizedGraphDocument(StandardizedBaseModel[GraphDocument]):
631
+ """A standardized graph document."""
632
+
633
+ graphs: list[StandardizedGraph]
634
+ meta: StandardizedMeta | None = None
635
+
636
+ @classmethod
637
+ def from_obograph_raw(
638
+ cls, graph_document: GraphDocument, converter: Converter, *, strict: bool = False
639
+ ) -> Self:
640
+ """Instantiate by standardizing a raw OBO Graph Document object."""
641
+ return cls(
642
+ graphs=[
643
+ StandardizedGraph.from_obograph_raw(graph, converter, strict=strict)
644
+ for graph in graph_document.graphs
645
+ ],
646
+ meta=StandardizedMeta.from_obograph_raw(graph_document.meta, converter, strict=strict),
647
+ )
648
+
649
+ def to_raw(self, converter: Converter) -> GraphDocument:
650
+ """Create a raw object."""
651
+ return GraphDocument(
652
+ graphs=[graph.to_raw(converter) for graph in self.graphs],
653
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
654
+ )
655
+
656
+
657
+ def _parse_list(
658
+ curie_or_uris: list[str] | None, converter: Converter, *, strict: bool
659
+ ) -> list[Reference] | None:
270
660
  if not curie_or_uris:
271
661
  return None
272
662
  return [
273
663
  reference
274
664
  for curie_or_uri in curie_or_uris
275
- if (reference := _curie_or_uri_to_ref(curie_or_uri, converter))
665
+ if (reference := _curie_or_uri_to_ref(curie_or_uri, converter, strict=strict))
276
666
  ]
277
667
 
278
668
 
279
669
  #: defined in https://github.com/geneontology/obographs/blob/6676b10a5cce04707d75b9dd46fa08de70322b0b/obographs-owlapi/src/main/java/org/geneontology/obographs/owlapi/FromOwl.java#L36-L39
280
- BUILTINS = {
670
+ #: this list is complete.
671
+ BUILTINS: dict[str, Reference] = {
281
672
  "is_a": vocabulary.is_a,
282
673
  "subPropertyOf": vocabulary.subproperty_of,
283
674
  "type": vocabulary.rdf_type,
284
675
  "inverseOf": Reference(prefix="owl", identifier="inverseOf"),
285
676
  }
286
677
 
678
+ """maybe add these later?
679
+ # predicates, see https://github.com/geneontology/obographs/blob/6676b10a5cce04707d75b9dd46fa08de70322b0b/obographs-core/src/test/java/org/geneontology/obographs/core/model/axiom/PropertyChainAxiomTest.java#L12-L14
680
+ # "part_of": vocabulary.part_of,
681
+ # "has_part": vocabulary.has_part,
682
+ # "overlaps": Reference(prefix="RO", identifier="0002131"),
683
+ """
287
684
 
288
- def _curie_or_uri_to_ref(s: str, converter: Converter) -> Reference | None:
685
+ REVERSE_BUILTINS: dict[Reference, str] = {v: k for k, v in BUILTINS.items()}
686
+
687
+
688
+ def _curie_or_uri_to_ref(s: str, converter: Converter, *, strict: bool) -> Reference | None:
289
689
  if s in BUILTINS:
290
690
  return BUILTINS[s]
291
- reference_tuple = converter.parse(s, strict=False)
691
+ try:
692
+ reference_tuple = converter.parse(s, strict=False)
693
+ except curies.preprocessing.BlocklistError:
694
+ return None
292
695
  if reference_tuple is not None:
293
696
  return reference_tuple.to_pydantic()
697
+ if strict:
698
+ raise ValueError(f"could not parse {s}")
294
699
  return None
obographs/version.py CHANGED
@@ -12,7 +12,7 @@ __all__ = [
12
12
  "get_version",
13
13
  ]
14
14
 
15
- VERSION = "0.0.3"
15
+ VERSION = "0.0.4"
16
16
 
17
17
 
18
18
  def get_git_hash() -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: obographs
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: A python data model for OBO Graphs
5
5
  Keywords: snekpack,cookiecutter
6
6
  Author: Charles Tapley Hoyt
@@ -23,15 +23,8 @@ Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Programming Language :: Python :: 3 :: Only
24
24
  Classifier: Typing :: Typed
25
25
  Requires-Dist: pydantic
26
- Requires-Dist: curies>=0.10.7
26
+ Requires-Dist: curies>=0.10.19
27
27
  Requires-Dist: typing-extensions
28
- Requires-Dist: sphinx>=8 ; extra == 'docs'
29
- Requires-Dist: sphinx-rtd-theme>=3.0 ; extra == 'docs'
30
- Requires-Dist: sphinx-automodapi ; extra == 'docs'
31
- Requires-Dist: autodoc-pydantic ; extra == 'docs'
32
- Requires-Dist: requests ; extra == 'network'
33
- Requires-Dist: pytest ; extra == 'tests'
34
- Requires-Dist: coverage[toml] ; extra == 'tests'
35
28
  Maintainer: Charles Tapley Hoyt
36
29
  Maintainer-email: Charles Tapley Hoyt <cthoyt@gmail.com>
37
30
  Requires-Python: >=3.10
@@ -40,9 +33,6 @@ Project-URL: Documentation, https://obographs.readthedocs.io
40
33
  Project-URL: Funding, https://github.com/sponsors/cthoyt
41
34
  Project-URL: Homepage, https://github.com/cthoyt/obographs
42
35
  Project-URL: Repository, https://github.com/cthoyt/obographs.git
43
- Provides-Extra: docs
44
- Provides-Extra: network
45
- Provides-Extra: tests
46
36
  Description-Content-Type: text/markdown
47
37
 
48
38
  <!--
@@ -129,18 +119,15 @@ $ python3 -m pip install obographs
129
119
  The most recent code and data can be installed directly from GitHub with uv:
130
120
 
131
121
  ```console
132
- $ uv --preview pip install git+https://github.com/cthoyt/obographs.git
122
+ $ uv pip install git+https://github.com/cthoyt/obographs.git
133
123
  ```
134
124
 
135
125
  or with pip:
136
126
 
137
127
  ```console
138
- $ UV_PREVIEW=1 python3 -m pip install git+https://github.com/cthoyt/obographs.git
128
+ $ python3 -m pip install git+https://github.com/cthoyt/obographs.git
139
129
  ```
140
130
 
141
- Note that this requires setting `UV_PREVIEW` mode enabled until the uv build
142
- backend becomes a stable feature.
143
-
144
131
  ## 👐 Contributing
145
132
 
146
133
  Contributions, whether filing an issue, making a pull request, or forking, are
@@ -203,18 +190,15 @@ To install in development mode, use the following:
203
190
  ```console
204
191
  $ git clone git+https://github.com/cthoyt/obographs.git
205
192
  $ cd obographs
206
- $ uv --preview pip install -e .
193
+ $ uv pip install -e .
207
194
  ```
208
195
 
209
196
  Alternatively, install using pip:
210
197
 
211
198
  ```console
212
- $ UV_PREVIEW=1 python3 -m pip install -e .
199
+ $ python3 -m pip install -e .
213
200
  ```
214
201
 
215
- Note that this requires setting `UV_PREVIEW` mode enabled until the uv build
216
- backend becomes a stable feature.
217
-
218
202
  ### Updating Package Boilerplate
219
203
 
220
204
  This project uses `cruft` to keep boilerplate (i.e., configuration, contribution
@@ -0,0 +1,10 @@
1
+ obographs/__init__.py,sha256=55f4bf18441bd7f6c41afa68d094532933b02b21dbdd20679cf8807c1791d494,1601
2
+ obographs/model.py,sha256=1f1bda132b83d8e08d7301eddb9be8d58b7e61c5b30e9baab9c24fd22348db53,8457
3
+ obographs/py.typed,sha256=01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b,1
4
+ obographs/standardized.py,sha256=69d26bbd35b49232b8d4bc2ae462f864f4fffcadf3acb7d541783fc8745dbd5e,25791
5
+ obographs/version.py,sha256=c50bb52fc81eb5b19dedfb8583a0c19b6127aff659301cb2d3d51f4d244fefc3,961
6
+ obographs-0.0.4.dist-info/licenses/LICENSE,sha256=4be0ec343e3bf11fd54321a6b576d5616ebb7d18898f741f63c517209e33bcb2,1076
7
+ obographs-0.0.4.dist-info/WHEEL,sha256=65f6765ba93534713730ff2172cd912ef280aa42867625a73f77c2fef0639dae,78
8
+ obographs-0.0.4.dist-info/entry_points.txt,sha256=9a9819cedd2186e28d5d42ddce5e3de1417b0db2b07392ff35f9adc7c86a8619,50
9
+ obographs-0.0.4.dist-info/METADATA,sha256=ac4daf3fa7f23379788e758debc91663ee15663ee1c9fabbf79bc69ca79c5e5b,12768
10
+ obographs-0.0.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.5.31
2
+ Generator: uv 0.7.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,10 +0,0 @@
1
- obographs/version.py,sha256=3709d2674acd39467891048cff0ca525f08edfba747910e241f23b6fb168d2dc,961
2
- obographs/__init__.py,sha256=0c3d73d035bde44a5375cbc4dffcf314fc89994853777e0048f1307b0fdd53a8,706
3
- obographs/model.py,sha256=445dbea604eb3d732c691afee71291348a6185750121ac41fcfeb8973b6a219b,7120
4
- obographs/py.typed,sha256=01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b,1
5
- obographs/standardized.py,sha256=27955c754e0f077aa8a4c36e9941331e5b8a716bb2cc21295b7ede7917bbc18d,9859
6
- obographs-0.0.3.dist-info/licenses/LICENSE,sha256=4be0ec343e3bf11fd54321a6b576d5616ebb7d18898f741f63c517209e33bcb2,1076
7
- obographs-0.0.3.dist-info/WHEEL,sha256=e3765529bb0cc791d07188d72ec6a759d7625ff6d3a5e4b710d25409bae03770,79
8
- obographs-0.0.3.dist-info/entry_points.txt,sha256=9a9819cedd2186e28d5d42ddce5e3de1417b0db2b07392ff35f9adc7c86a8619,50
9
- obographs-0.0.3.dist-info/METADATA,sha256=ce84eea275fad376b42901cbd5d93982cae494d0595cea5cbd7ee44748f6631c,13438
10
- obographs-0.0.3.dist-info/RECORD,,