Skip to content

Commit 6745a6f

Browse files
jonsch1cursoragent
andcommitted
silver: add Relation schema and RelationBuilder; map SIGNOR interactions as relations
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6176be2 commit 6745a6f

3 files changed

Lines changed: 242 additions & 41 deletions

File tree

‎pypath/inputs_v2/signor.py‎

Lines changed: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
Member,
2929
MembersFromList,
3030
MembershipBuilder,
31+
RelationBuilder,
3132
)
3233

3334

@@ -395,8 +396,58 @@ def parsed_annotation_values(column_name: str, *, default_term: str | None = Non
395396
),
396397
)
397398

398-
interactions_schema = EntityBuilder(
399-
entity_type=EntityTypeCv.INTERACTION,
399+
def signor_predicate(row: dict[str, object]) -> str:
400+
raw_causal = str(row.get('Causal statement') or '').strip()
401+
if raw_causal and raw_causal != '-':
402+
mi_match = re.search(_MI_REGEX, raw_causal)
403+
if mi_match:
404+
return mi_match.group(1)
405+
raw_type = str(row.get('Interaction type(s)') or '').strip()
406+
if raw_type and raw_type != '-':
407+
mi_match = re.search(_MI_REGEX, raw_type)
408+
if mi_match:
409+
return mi_match.group(1)
410+
return 'interacts_with'
411+
412+
413+
interactor_a_builder = EntityBuilder(
414+
entity_type=interactor_entity_type('A'),
415+
identifiers=IdentifiersBuilder(
416+
general_identifier_cv('\ufeff#ID(s) interactor A'),
417+
general_identifier_cv('Alt. ID(s) interactor A'),
418+
interactor_tax_cv('A'),
419+
),
420+
annotations=AnnotationsBuilder(
421+
mi_term_cv('Biological role(s) interactor A'),
422+
mi_term_cv('Experimental role(s) interactor A'),
423+
CV(
424+
term=parsed_annotation_terms('Feature(s) interactor A'),
425+
value=parsed_annotation_values('Feature(s) interactor A'),
426+
),
427+
),
428+
)
429+
430+
interactor_b_builder = EntityBuilder(
431+
entity_type=interactor_entity_type('B'),
432+
identifiers=IdentifiersBuilder(
433+
general_identifier_cv('ID(s) interactor B'),
434+
general_identifier_cv('Alt. ID(s) interactor B'),
435+
interactor_tax_cv('B'),
436+
),
437+
annotations=AnnotationsBuilder(
438+
mi_term_cv('Biological role(s) interactor B'),
439+
mi_term_cv('Experimental role(s) interactor B'),
440+
CV(
441+
term=parsed_annotation_terms('Feature(s) interactor B'),
442+
value=parsed_annotation_values('Feature(s) interactor B'),
443+
),
444+
),
445+
)
446+
447+
interactions_schema = RelationBuilder(
448+
subject=interactor_a_builder,
449+
predicate=signor_predicate,
450+
object=interactor_b_builder,
400451
identifiers=IdentifiersBuilder(
401452
interaction_identifier_cv(),
402453
),
@@ -417,44 +468,6 @@ def parsed_annotation_values(column_name: str, *, default_term: str | None = Non
417468
),
418469
),
419470
),
420-
membership=MembershipBuilder(
421-
Member(
422-
entity=EntityBuilder(
423-
entity_type=interactor_entity_type('A'),
424-
identifiers=IdentifiersBuilder(
425-
general_identifier_cv('\ufeff#ID(s) interactor A'),
426-
general_identifier_cv('Alt. ID(s) interactor A'),
427-
),
428-
annotations=AnnotationsBuilder(interactor_tax_cv('A')),
429-
),
430-
annotations=AnnotationsBuilder(
431-
mi_term_cv('Biological role(s) interactor A'),
432-
mi_term_cv('Experimental role(s) interactor A'),
433-
CV(
434-
term=parsed_annotation_terms('Feature(s) interactor A'),
435-
value=parsed_annotation_values('Feature(s) interactor A'),
436-
),
437-
),
438-
),
439-
Member(
440-
entity=EntityBuilder(
441-
entity_type=interactor_entity_type('B'),
442-
identifiers=IdentifiersBuilder(
443-
general_identifier_cv('ID(s) interactor B'),
444-
general_identifier_cv('Alt. ID(s) interactor B'),
445-
),
446-
annotations=AnnotationsBuilder(interactor_tax_cv('B')),
447-
),
448-
annotations=AnnotationsBuilder(
449-
mi_term_cv('Biological role(s) interactor B'),
450-
mi_term_cv('Experimental role(s) interactor B'),
451-
CV(
452-
term=parsed_annotation_terms('Feature(s) interactor B'),
453-
value=parsed_annotation_values('Feature(s) interactor B'),
454-
),
455-
),
456-
),
457-
),
458471
)
459472

460473
resource = Resource(

‎pypath/internals/silver_schema.py‎

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
'EntityRef',
2424
'OntologyRelation',
2525
'Entity',
26+
'Relation',
2627
'ENTITY_SCHEMA',
28+
'RELATION_SCHEMA',
2729
]
2830

2931
class Identifier(NamedTuple):
@@ -115,7 +117,7 @@ class OntologyRelation(NamedTuple):
115117
class Entity(NamedTuple):
116118
""" Entity record matching entities schema."""
117119

118-
type: EntityTypeCv # e.g. EntityTypeCv.INTERACTION, EntityTypeCv.CV_TERM
120+
type: EntityTypeCv | str # e.g. EntityTypeCv.INTERACTION, EntityTypeCv.CV_TERM
119121
identifiers: List[Identifier] # e.g. IdentifierNamespaceCv.NAME and .SYNONYM)
120122
annotations: List[Annotation] | None = None
121123

@@ -195,6 +197,65 @@ def pretty(self, indent: int = 0) -> str:
195197

196198
return "\n".join(lines)
197199

200+
201+
class Relation(NamedTuple):
202+
"""Relation record representing a directed edge/interaction between entities."""
203+
204+
subject: EntityRef | Entity
205+
predicate: str
206+
object: EntityRef | Entity
207+
category: str | None = None
208+
interaction_class: str | None = None
209+
identifiers: List[Identifier] | None = None
210+
annotations: List[Annotation] | None = None
211+
212+
def __repr__(self) -> str:
213+
"""Compact representation of relation."""
214+
sub_str = repr(self.subject)
215+
obj_str = repr(self.object)
216+
pred_str = cv_term_label_accession(self.predicate) if hasattr(self.predicate, 'value') else str(self.predicate)
217+
extra = []
218+
if self.category:
219+
extra.append(f"category={self.category}")
220+
if self.interaction_class:
221+
extra.append(f"interaction_class={self.interaction_class}")
222+
if self.identifiers:
223+
extra.append(f"identifiers={self.identifiers!r}")
224+
if self.annotations:
225+
extra.append(f"annotations={self.annotations!r}")
226+
extra_str = f" ({', '.join(extra)})" if extra else ""
227+
return f"Relation({sub_str} -[{pred_str}]-> {obj_str}{extra_str})"
228+
229+
def __str__(self) -> str:
230+
return repr(self)
231+
232+
def pretty(self, indent: int = 0) -> str:
233+
"""Tree-like representation with indentation."""
234+
prefix = " " * indent
235+
pred_str = cv_term_label_accession(self.predicate) if hasattr(self.predicate, 'value') else str(self.predicate)
236+
lines = [f"{prefix}Relation: {pred_str}"]
237+
lines.append(f"{prefix}├─ subject:")
238+
sub_lines = (self.subject.pretty(0) if hasattr(self.subject, 'pretty') else repr(self.subject)).split("\n")
239+
for line in sub_lines:
240+
lines.append(f"{prefix}│ {line}")
241+
lines.append(f"{prefix}├─ object:")
242+
obj_lines = (self.object.pretty(0) if hasattr(self.object, 'pretty') else repr(self.object)).split("\n")
243+
for line in obj_lines:
244+
lines.append(f"{prefix}│ {line}")
245+
if self.category:
246+
lines.append(f"{prefix}├─ category: {self.category}")
247+
if self.interaction_class:
248+
lines.append(f"{prefix}├─ interaction_class: {self.interaction_class}")
249+
if self.identifiers:
250+
lines.append(f"{prefix}├─ identifiers: ({len(self.identifiers)})")
251+
for id_ in self.identifiers:
252+
lines.append(f"{prefix}│ ├─ {id_!r}")
253+
if self.annotations:
254+
lines.append(f"{prefix}└─ annotations: ({len(self.annotations)})")
255+
for ann in self.annotations:
256+
lines.append(f"{prefix} └─ {ann!r}")
257+
return "\n".join(lines)
258+
198259
#### PyArrow Schemas (needed for parquet files) ####
199260

200261
# Reusable field definitions
@@ -273,3 +334,17 @@ def pretty(self, indent: int = 0) -> str:
273334
]
274335

275336
ENTITY_SCHEMA = pa.schema(ENTITY_FIELDS)
337+
338+
# Full relation schema
339+
RELATION_FIELDS = [
340+
pa.field('subject', pa.struct(NESTED_ENTITY_FIELDS), nullable=False),
341+
pa.field('predicate', pa.string(), nullable=False),
342+
pa.field('object', pa.struct(NESTED_ENTITY_FIELDS), nullable=False),
343+
pa.field('category', pa.string()),
344+
pa.field('interaction_class', pa.string()),
345+
pa.field('identifiers', pa.list_(pa.struct(IDENTIFIER_FIELDS))),
346+
pa.field('annotations', pa.list_(pa.struct(ANNOTATION_FIELDS))),
347+
]
348+
349+
RELATION_SCHEMA = pa.schema(RELATION_FIELDS)
350+

‎pypath/internals/tabular_builder.py‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
Identifier as SilverIdentifier,
2424
Membership as SilverMembership,
2525
OntologyRelation as SilverOntologyRelation,
26+
Relation as SilverRelation,
2627
)
2728
from pypath.internals.cv_terms import (
2829
EntityTypeCv,
@@ -1318,6 +1319,117 @@ def _allows_empty_identifiers(self, entity_type: Any) -> bool:
13181319
}
13191320

13201321

1322+
class RelationBuilder:
1323+
"""Declarative spec that produces `Relation` records from rows."""
1324+
1325+
def __init__(
1326+
self,
1327+
*,
1328+
subject: EntityBuilder | Callable[[Any], SilverEntity | SilverEntityRef | None],
1329+
predicate: str | Column | Callable[[Any], str | None],
1330+
object: EntityBuilder | Callable[[Any], SilverEntity | SilverEntityRef | None],
1331+
category: str | Column | Callable[[Any], str | None] | None = None,
1332+
interaction_class: str | Column | Callable[[Any], str | None] | None = None,
1333+
identifiers: IdentifiersBuilder | None = None,
1334+
annotations: AnnotationsBuilder | None = None,
1335+
) -> None:
1336+
self.subject = subject
1337+
self.predicate = predicate
1338+
self.object = object
1339+
self.category = category
1340+
self.interaction_class = interaction_class
1341+
self.identifiers = identifiers
1342+
self.annotations = annotations
1343+
1344+
def __call__(self, row: Any) -> SilverRelation | None:
1345+
return self.build(row)
1346+
1347+
def build(self, row: Any) -> SilverRelation | None:
1348+
cache = ColumnCache()
1349+
1350+
# Build subject entity
1351+
if isinstance(self.subject, EntityBuilder):
1352+
subject_entity = self.subject.build(row)
1353+
elif callable(self.subject):
1354+
try:
1355+
subject_entity = self.subject(row)
1356+
except Exception as exc: # pragma: no cover - defensive
1357+
logger.debug("Relation subject callable failed: %s", exc)
1358+
return None
1359+
else:
1360+
subject_entity = self.subject
1361+
1362+
if subject_entity is None:
1363+
return None
1364+
1365+
# Build object entity
1366+
if isinstance(self.object, EntityBuilder):
1367+
object_entity = self.object.build(row)
1368+
elif callable(self.object):
1369+
try:
1370+
object_entity = self.object(row)
1371+
except Exception as exc: # pragma: no cover - defensive
1372+
logger.debug("Relation object callable failed: %s", exc)
1373+
return None
1374+
else:
1375+
object_entity = self.object
1376+
1377+
if object_entity is None:
1378+
return None
1379+
1380+
# Resolve predicate
1381+
if isinstance(self.predicate, Column):
1382+
pred_vals = cache.values(self.predicate, row)
1383+
predicate_val = str(pred_vals[0]) if pred_vals else 'interacts_with'
1384+
elif callable(self.predicate):
1385+
try:
1386+
predicate_val = str(self.predicate(row) or 'interacts_with')
1387+
except Exception as exc: # pragma: no cover - defensive
1388+
logger.debug("Relation predicate callable failed: %s", exc)
1389+
predicate_val = 'interacts_with'
1390+
else:
1391+
predicate_val = str(self.predicate)
1392+
1393+
# Resolve optional category
1394+
category_val = None
1395+
if isinstance(self.category, Column):
1396+
cat_vals = cache.values(self.category, row)
1397+
category_val = str(cat_vals[0]) if cat_vals else None
1398+
elif callable(self.category):
1399+
try:
1400+
category_val = self.category(row)
1401+
except Exception: # pragma: no cover
1402+
pass
1403+
elif self.category is not None:
1404+
category_val = str(self.category)
1405+
1406+
# Resolve optional interaction_class
1407+
class_val = None
1408+
if isinstance(self.interaction_class, Column):
1409+
cls_vals = cache.values(self.interaction_class, row)
1410+
class_val = str(cls_vals[0]) if cls_vals else None
1411+
elif callable(self.interaction_class):
1412+
try:
1413+
class_val = self.interaction_class(row)
1414+
except Exception: # pragma: no cover
1415+
pass
1416+
elif self.interaction_class is not None:
1417+
class_val = str(self.interaction_class)
1418+
1419+
identifiers = self.identifiers.build(row, cache) if self.identifiers else None
1420+
annotations = self.annotations.build(row, cache) if self.annotations else None
1421+
1422+
return SilverRelation(
1423+
subject=subject_entity,
1424+
predicate=predicate_val,
1425+
object=object_entity,
1426+
category=category_val,
1427+
interaction_class=class_val,
1428+
identifiers=identifiers if identifiers else None,
1429+
annotations=annotations if annotations else None,
1430+
)
1431+
1432+
13211433
__all__ = [
13221434
"AssociationBuilder",
13231435
"AssociationsBuilder",
@@ -1331,4 +1443,5 @@ def _allows_empty_identifiers(self, entity_type: Any) -> bool:
13311443
"Member",
13321444
"MembershipBuilder",
13331445
"MembersFromList",
1446+
"RelationBuilder",
13341447
]

0 commit comments

Comments
 (0)