obographs 0.0.3__py3-none-any.whl → 0.0.5__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,20 +176,34 @@ 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
- definition: StandardizedDefinition | None
206
+ definition: StandardizedDefinition | None = None
106
207
  subsets: list[Reference] | None = None
107
208
  xrefs: list[StandardizedXref] | None = None
108
209
  synonyms: list[StandardizedSynonym] | None = 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,25 @@ 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
+ if meta.definition is not None
266
+ else None,
267
+ subsets=[
268
+ _curie_or_uri_to_ref(subset, converter, strict=strict) for subset in meta.subsets
269
+ ]
155
270
  if meta.subsets
156
271
  else None,
157
272
  xrefs=xrefs or None,
@@ -162,31 +277,69 @@ class StandardizedMeta(BaseModel):
162
277
  properties=props or None,
163
278
  )
164
279
 
280
+ def to_raw(self, converter: Converter) -> Meta:
281
+ """Create a raw object."""
282
+ return Meta(
283
+ definition=self.definition.to_raw(converter)
284
+ if self.definition and self.definition.value
285
+ else None,
286
+ subsets=_expand_list(self.subsets, converter),
287
+ xrefs=[xref.to_raw(converter) for xref in self.xrefs] if self.xrefs else None,
288
+ synonyms=[s.to_raw(converter) for s in self.synonyms] if self.synonyms else None,
289
+ comments=self.comments,
290
+ version=self.version, # TODO might need some kind of expansion?
291
+ deprecated=self.deprecated,
292
+ basicPropertyValues=[p.to_raw(converter) for p in self.properties]
293
+ if self.properties
294
+ else None,
295
+ )
296
+
165
297
 
166
- class StandardizedNode(BaseModel):
298
+ class StandardizedNode(StandardizedBaseModel[Node]):
167
299
  """A standardized node."""
168
300
 
169
301
  reference: Reference
170
302
  label: str | None = Field(None)
171
303
  meta: StandardizedMeta | None = None
172
304
  type: NodeType | None = Field(None, description="Type of node")
305
+ property_type: PropertyType | None = Field(
306
+ None, description="Type of property, if the node type is a property"
307
+ )
173
308
 
174
309
  @classmethod
175
- def from_obograph_raw(cls, node: Node, converter: Converter) -> Self | None:
310
+ def from_obograph_raw(
311
+ cls, node: Node, converter: Converter, *, strict: bool = False
312
+ ) -> Self | None:
176
313
  """Instantiate by standardizing a raw OBO Graph object."""
177
- reference = _curie_or_uri_to_ref(node.id, converter)
314
+ reference = _curie_or_uri_to_ref(node.id, converter, strict=strict)
178
315
  if reference is None:
316
+ if strict:
317
+ raise ValueError(f"failed to parse node's ID: {node.id}")
179
318
  logger.warning("failed to parse node's ID %s", node.id)
180
319
  return None
320
+
181
321
  return cls(
182
322
  reference=reference,
183
323
  label=node.lbl,
184
- meta=StandardizedMeta.from_obograph_raw(node.meta, converter, flag=reference.curie),
324
+ meta=StandardizedMeta.from_obograph_raw(
325
+ node.meta, converter, flag=reference.curie, strict=strict
326
+ ),
185
327
  type=node.type,
328
+ property_type=node.propertyType,
329
+ )
330
+
331
+ def to_raw(self, converter: Converter) -> Node:
332
+ """Create a raw object."""
333
+ return Node(
334
+ id=converter.expand_reference(self.reference),
335
+ lbl=self.label,
336
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
337
+ type=self.type,
338
+ propertyType=self.property_type,
186
339
  )
187
340
 
188
341
 
189
- class StandardizedEdge(BaseModel):
342
+ class StandardizedEdge(Triple, StandardizedBaseModel[Edge]):
190
343
  """A standardized edge."""
191
344
 
192
345
  subject: Reference
@@ -195,18 +348,26 @@ class StandardizedEdge(BaseModel):
195
348
  meta: StandardizedMeta | None = None
196
349
 
197
350
  @classmethod
198
- def from_obograph_raw(cls, edge: Edge, converter: Converter) -> Self | None:
351
+ def from_obograph_raw(
352
+ cls, edge: Edge, converter: Converter, *, strict: bool = False
353
+ ) -> Self | None:
199
354
  """Instantiate by standardizing a raw OBO Graph object."""
200
- subject = _curie_or_uri_to_ref(edge.sub, converter)
355
+ subject = _curie_or_uri_to_ref(edge.sub, converter, strict=strict)
201
356
  if not subject:
357
+ if strict:
358
+ raise ValueError
202
359
  logger.warning("failed to parse edge's subject %s", edge.sub)
203
360
  return None
204
- predicate = _curie_or_uri_to_ref(edge.pred, converter)
361
+ predicate = _curie_or_uri_to_ref(edge.pred, converter, strict=strict)
205
362
  if not predicate:
363
+ if strict:
364
+ raise ValueError
206
365
  logger.warning("failed to parse edge's predicate %s", edge.pred)
207
366
  return None
208
- obj = _curie_or_uri_to_ref(edge.obj, converter)
367
+ obj = _curie_or_uri_to_ref(edge.obj, converter, strict=strict)
209
368
  if not obj:
369
+ if strict:
370
+ raise ValueError
210
371
  logger.warning("failed to parse edge's object %s", edge.obj)
211
372
  return None
212
373
  return cls(
@@ -214,12 +375,178 @@ class StandardizedEdge(BaseModel):
214
375
  predicate=predicate,
215
376
  object=obj,
216
377
  meta=StandardizedMeta.from_obograph_raw(
217
- edge.meta, converter, flag=f"{subject.curie} {predicate.curie} {obj.curie}"
378
+ edge.meta,
379
+ converter,
380
+ flag=f"{subject.curie} {predicate.curie} {obj.curie}",
381
+ strict=strict,
218
382
  ),
219
383
  )
220
384
 
385
+ def to_raw(self, converter: Converter) -> Edge:
386
+ """Create a raw object."""
387
+ if self.predicate in REVERSE_BUILTINS:
388
+ predicate = REVERSE_BUILTINS[self.predicate]
389
+ else:
390
+ predicate = converter.expand_reference(self.predicate, strict=True)
391
+
392
+ return Edge(
393
+ sub=converter.expand_reference(self.subject),
394
+ pred=predicate,
395
+ obj=converter.expand_reference(self.object),
396
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
397
+ )
398
+
399
+
400
+ class StandardizedDomainRangeAxiom(StandardizedBaseModel[DomainRangeAxiom]):
401
+ """Represents a domain/range axiom."""
402
+
403
+ predicate: Reference
404
+ domains: list[Reference] = Field(default_factory=list)
405
+ ranges: list[Reference] = Field(default_factory=list)
406
+ all_values_from_edges: list[StandardizedEdge] = Field(default_factory=list)
407
+ meta: StandardizedMeta | None = None
408
+
409
+ @classmethod
410
+ def from_obograph_raw(
411
+ cls, obj: DomainRangeAxiom, converter: Converter, *, strict: bool = False
412
+ ) -> Self | None:
413
+ """Parse a raw object."""
414
+ return cls(
415
+ predicate=_curie_or_uri_to_ref(obj.predicateId, converter, strict=strict),
416
+ domains=_parse_list(obj.domainClassIds, converter, strict=strict) or [],
417
+ ranges=_parse_list(obj.rangeClassIds, converter, strict=strict) or [],
418
+ all_values_from_edges=[
419
+ StandardizedEdge.from_obograph_raw(edge, converter, strict=strict)
420
+ for edge in obj.allValuesFromEdges or []
421
+ ],
422
+ meta=StandardizedMeta.from_obograph_raw(obj.meta, converter, strict=strict),
423
+ )
424
+
425
+ def to_raw(self, converter: Converter) -> DomainRangeAxiom:
426
+ """Create a raw object."""
427
+ return DomainRangeAxiom(
428
+ predicateId=converter.expand_reference(self.predicate),
429
+ domainClassIds=_expand_list(self.domains, converter),
430
+ rangeClassIds=_expand_list(self.ranges, converter),
431
+ allValuesFromEdges=[edge.to_raw(converter) for edge in self.all_values_from_edges]
432
+ if self.all_values_from_edges
433
+ else None,
434
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
435
+ )
436
+
437
+
438
+ class StandardizedPropertyChainAxiom(StandardizedBaseModel[PropertyChainAxiom]):
439
+ """Represents a property chain axiom."""
440
+
441
+ predicate: Reference
442
+ chain: list[Reference] = Field(default_factory=list)
443
+ meta: StandardizedMeta | None = None
444
+
445
+ @classmethod
446
+ def from_obograph_raw(
447
+ cls, obj: PropertyChainAxiom, converter: Converter, *, strict: bool = False
448
+ ) -> Self | None:
449
+ """Parse a raw object."""
450
+ return cls(
451
+ predicate=_curie_or_uri_to_ref(obj.predicateId, converter, strict=strict),
452
+ chain=_parse_list(obj.chainPredicateIds, converter, strict=strict),
453
+ meta=StandardizedMeta.from_obograph_raw(obj.meta, converter, strict=strict),
454
+ )
455
+
456
+ def to_raw(self, converter: Converter) -> PropertyChainAxiom:
457
+ """Create a raw object."""
458
+ return PropertyChainAxiom(
459
+ predicateId=converter.expand_reference(self.predicate),
460
+ chainPredicateIds=_expand_list(self.chain, converter),
461
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
462
+ )
463
+
464
+
465
+ class StandardizedEquivalentNodeSet(StandardizedBaseModel[EquivalentNodeSet]):
466
+ """Represents an equivalence set."""
467
+
468
+ node: Reference
469
+ equivalents: list[Reference] = Field(default_factory=list)
470
+ meta: StandardizedMeta | None = None
471
+
472
+ @classmethod
473
+ def from_obograph_raw(
474
+ cls, obj: EquivalentNodeSet, converter: Converter, *, strict: bool = False
475
+ ) -> Self | None:
476
+ """Parse a raw object."""
477
+ return cls(
478
+ node=_curie_or_uri_to_ref(obj.representativeNodeId, converter, strict=strict),
479
+ equivalents=_parse_list(obj.nodeIds, converter, strict=strict),
480
+ meta=StandardizedMeta.from_obograph_raw(obj.meta, converter, strict=strict),
481
+ )
482
+
483
+ def to_raw(self, converter: Converter) -> EquivalentNodeSet:
484
+ """Create a raw object."""
485
+ return EquivalentNodeSet(
486
+ representativeNodeId=converter.expand_reference(self.node),
487
+ nodeIds=_expand_list(self.equivalents, converter),
488
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
489
+ )
490
+
491
+
492
+ class StandardizedExistentialRestriction(StandardizedBaseModel[ExistentialRestrictionExpression]):
493
+ """Represents an existential restriction expression."""
494
+
495
+ predicate: Reference
496
+ target: Reference
497
+
498
+ @classmethod
499
+ def from_obograph_raw(
500
+ cls, obj: ExistentialRestrictionExpression, converter: Converter, *, strict: bool = False
501
+ ) -> Self | None:
502
+ """Parse a raw object."""
503
+ return cls(
504
+ predicate=_curie_or_uri_to_ref(obj.propertyId, converter, strict=strict),
505
+ target=_curie_or_uri_to_ref(obj.fillerId, converter, strict=strict),
506
+ )
507
+
508
+ def to_raw(self, converter: Converter) -> ExistentialRestrictionExpression:
509
+ """Create a raw object."""
510
+ return ExistentialRestrictionExpression(
511
+ propertyId=converter.expand_reference(self.predicate),
512
+ fillerId=converter.expand_reference(self.target),
513
+ )
514
+
221
515
 
222
- class StandardizedGraph(BaseModel):
516
+ class StandardizedLogicalDefinition(StandardizedBaseModel[LogicalDefinition]):
517
+ """Represents a logical definition axiom."""
518
+
519
+ node: Reference
520
+ geni: list[Reference] = Field(default_factory=list)
521
+ restrictions: list[StandardizedExistentialRestriction] = Field(default_factory=list)
522
+ meta: StandardizedMeta | None = None
523
+
524
+ @classmethod
525
+ def from_obograph_raw(
526
+ cls, obj: LogicalDefinition, converter: Converter, *, strict: bool = False
527
+ ) -> Self | None:
528
+ """Parse a raw object."""
529
+ return cls(
530
+ node=_curie_or_uri_to_ref(obj.definedClassId, converter, strict=strict),
531
+ geni=_parse_list(obj.genusIds, converter, strict=strict),
532
+ restrictions=[
533
+ StandardizedExistentialRestriction.from_obograph_raw(r, converter, strict=strict)
534
+ for r in obj.restrictions or []
535
+ ],
536
+ meta=StandardizedMeta.from_obograph_raw(obj.meta, converter, strict=strict),
537
+ )
538
+
539
+ def to_raw(self, converter: Converter) -> LogicalDefinition:
540
+ """Create a raw object."""
541
+ return LogicalDefinition(
542
+ definedClassId=converter.expand_reference(self.node),
543
+ genusIds=_expand_list(self.geni, converter),
544
+ restrictions=[r.to_raw(converter) for r in self.restrictions],
545
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
546
+ )
547
+
548
+
549
+ class StandardizedGraph(StandardizedBaseModel[Graph]):
223
550
  """A standardized graph."""
224
551
 
225
552
  id: str | None = None
@@ -227,26 +554,62 @@ class StandardizedGraph(BaseModel):
227
554
  nodes: list[StandardizedNode] = Field(default_factory=list)
228
555
  edges: list[StandardizedEdge] = Field(default_factory=list)
229
556
 
230
- # TODO other bits
557
+ equivalent_node_sets: list[StandardizedEquivalentNodeSet] = Field(default_factory=list)
558
+ logical_definition_axioms: list[StandardizedLogicalDefinition] = Field(default_factory=list)
559
+ domain_range_axioms: list[StandardizedDomainRangeAxiom] = Field(default_factory=list)
560
+ property_chain_axioms: list[StandardizedPropertyChainAxiom] = Field(default_factory=list)
231
561
 
232
562
  @classmethod
233
- def from_obograph_raw(cls, graph: Graph, converter: Converter) -> Self:
563
+ def from_obograph_raw(cls, graph: Graph, converter: Converter, *, strict: bool = False) -> Self:
234
564
  """Instantiate by standardizing a raw OBO Graph object."""
235
565
  return cls(
236
566
  id=graph.id,
237
- meta=StandardizedMeta.from_obograph_raw(graph.meta, converter, flag=graph.id or ""),
567
+ meta=StandardizedMeta.from_obograph_raw(
568
+ graph.meta, converter, flag=graph.id or "", strict=strict
569
+ ),
238
570
  nodes=[
239
571
  s_node
240
572
  for node in graph.nodes
241
- if (s_node := StandardizedNode.from_obograph_raw(node, converter))
573
+ if (s_node := StandardizedNode.from_obograph_raw(node, converter, strict=strict))
242
574
  ],
243
575
  edges=[
244
576
  s_edge
245
577
  for edge in graph.edges
246
- if (s_edge := StandardizedEdge.from_obograph_raw(edge, converter))
578
+ if (s_edge := StandardizedEdge.from_obograph_raw(edge, converter, strict=strict))
579
+ ],
580
+ equivalent_node_sets=[
581
+ StandardizedEquivalentNodeSet.from_obograph_raw(e, converter, strict=strict)
582
+ for e in graph.equivalentNodesSets or []
583
+ ],
584
+ logical_definition_axioms=[
585
+ StandardizedLogicalDefinition.from_obograph_raw(e, converter, strict=strict)
586
+ for e in graph.logicalDefinitionAxioms or []
587
+ ],
588
+ property_chain_axioms=[
589
+ StandardizedPropertyChainAxiom.from_obograph_raw(e, converter, strict=strict)
590
+ for e in graph.propertyChainAxioms or []
591
+ ],
592
+ domain_range_axioms=[
593
+ StandardizedDomainRangeAxiom.from_obograph_raw(e, converter, strict=strict)
594
+ for e in graph.domainRangeAxioms or []
247
595
  ],
248
596
  )
249
597
 
598
+ def to_raw(self, converter: Converter) -> Graph:
599
+ """Create a raw object."""
600
+ return Graph(
601
+ id=self.id,
602
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
603
+ nodes=[node.to_raw(converter) for node in self.nodes],
604
+ edges=[edge.to_raw(converter) for edge in self.edges],
605
+ logicalDefinitionAxioms=[
606
+ axiom.to_raw(converter) for axiom in self.logical_definition_axioms
607
+ ],
608
+ propertyChainAxioms=[axiom.to_raw(converter) for axiom in self.property_chain_axioms],
609
+ domainRangeAxioms=[axiom.to_raw(converter) for axiom in self.domain_range_axioms],
610
+ equivalentNodesSets=[axiom.to_raw(converter) for axiom in self.equivalent_node_sets],
611
+ )
612
+
250
613
  def _get_property(self, predicate: Reference) -> str | Reference | None:
251
614
  if self.meta is None:
252
615
  return None
@@ -266,29 +629,73 @@ class StandardizedGraph(BaseModel):
266
629
  return r
267
630
 
268
631
 
269
- def _parse_list(curie_or_uris: list[str] | None, converter: Converter) -> list[Reference] | None:
632
+ class StandardizedGraphDocument(StandardizedBaseModel[GraphDocument]):
633
+ """A standardized graph document."""
634
+
635
+ graphs: list[StandardizedGraph]
636
+ meta: StandardizedMeta | None = None
637
+
638
+ @classmethod
639
+ def from_obograph_raw(
640
+ cls, graph_document: GraphDocument, converter: Converter, *, strict: bool = False
641
+ ) -> Self:
642
+ """Instantiate by standardizing a raw OBO Graph Document object."""
643
+ return cls(
644
+ graphs=[
645
+ StandardizedGraph.from_obograph_raw(graph, converter, strict=strict)
646
+ for graph in graph_document.graphs
647
+ ],
648
+ meta=StandardizedMeta.from_obograph_raw(graph_document.meta, converter, strict=strict),
649
+ )
650
+
651
+ def to_raw(self, converter: Converter) -> GraphDocument:
652
+ """Create a raw object."""
653
+ return GraphDocument(
654
+ graphs=[graph.to_raw(converter) for graph in self.graphs],
655
+ meta=self.meta.to_raw(converter) if self.meta is not None else None,
656
+ )
657
+
658
+
659
+ def _parse_list(
660
+ curie_or_uris: list[str] | None, converter: Converter, *, strict: bool
661
+ ) -> list[Reference] | None:
270
662
  if not curie_or_uris:
271
663
  return None
272
664
  return [
273
665
  reference
274
666
  for curie_or_uri in curie_or_uris
275
- if (reference := _curie_or_uri_to_ref(curie_or_uri, converter))
667
+ if (reference := _curie_or_uri_to_ref(curie_or_uri, converter, strict=strict))
276
668
  ]
277
669
 
278
670
 
279
671
  #: defined in https://github.com/geneontology/obographs/blob/6676b10a5cce04707d75b9dd46fa08de70322b0b/obographs-owlapi/src/main/java/org/geneontology/obographs/owlapi/FromOwl.java#L36-L39
280
- BUILTINS = {
672
+ #: this list is complete.
673
+ BUILTINS: dict[str, Reference] = {
281
674
  "is_a": vocabulary.is_a,
282
675
  "subPropertyOf": vocabulary.subproperty_of,
283
676
  "type": vocabulary.rdf_type,
284
677
  "inverseOf": Reference(prefix="owl", identifier="inverseOf"),
285
678
  }
286
679
 
680
+ """maybe add these later?
681
+ # predicates, see https://github.com/geneontology/obographs/blob/6676b10a5cce04707d75b9dd46fa08de70322b0b/obographs-core/src/test/java/org/geneontology/obographs/core/model/axiom/PropertyChainAxiomTest.java#L12-L14
682
+ # "part_of": vocabulary.part_of,
683
+ # "has_part": vocabulary.has_part,
684
+ # "overlaps": Reference(prefix="RO", identifier="0002131"),
685
+ """
287
686
 
288
- def _curie_or_uri_to_ref(s: str, converter: Converter) -> Reference | None:
687
+ REVERSE_BUILTINS: dict[Reference, str] = {v: k for k, v in BUILTINS.items()}
688
+
689
+
690
+ def _curie_or_uri_to_ref(s: str, converter: Converter, *, strict: bool) -> Reference | None:
289
691
  if s in BUILTINS:
290
692
  return BUILTINS[s]
291
- reference_tuple = converter.parse(s, strict=False)
693
+ try:
694
+ reference_tuple = converter.parse(s, strict=False)
695
+ except curies.preprocessing.BlocklistError:
696
+ return None
292
697
  if reference_tuple is not None:
293
698
  return reference_tuple.to_pydantic()
699
+ if strict:
700
+ raise ValueError(f"could not parse {s}")
294
701
  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.5"
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.5
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=43ee42d342f33df38f24151339cdeb6525252af541366149c7719d188901a52f,25863
5
+ obographs/version.py,sha256=03a64be7e09716976ae72d4c1ce70afe06d2bf1772e45dd140a3903675f07332,961
6
+ obographs-0.0.5.dist-info/licenses/LICENSE,sha256=4be0ec343e3bf11fd54321a6b576d5616ebb7d18898f741f63c517209e33bcb2,1076
7
+ obographs-0.0.5.dist-info/WHEEL,sha256=65f6765ba93534713730ff2172cd912ef280aa42867625a73f77c2fef0639dae,78
8
+ obographs-0.0.5.dist-info/entry_points.txt,sha256=9a9819cedd2186e28d5d42ddce5e3de1417b0db2b07392ff35f9adc7c86a8619,50
9
+ obographs-0.0.5.dist-info/METADATA,sha256=5dd83cf5b36da7b18dbb68e37072463e342e189975429e9f3aa3535675e7874d,12768
10
+ obographs-0.0.5.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,,