napistu 0.2.5.dev7__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. napistu/__main__.py +126 -96
  2. napistu/constants.py +35 -41
  3. napistu/context/__init__.py +10 -0
  4. napistu/context/discretize.py +462 -0
  5. napistu/context/filtering.py +387 -0
  6. napistu/gcs/__init__.py +1 -1
  7. napistu/identifiers.py +74 -15
  8. napistu/indices.py +68 -0
  9. napistu/ingestion/__init__.py +1 -1
  10. napistu/ingestion/bigg.py +47 -62
  11. napistu/ingestion/constants.py +18 -133
  12. napistu/ingestion/gtex.py +113 -0
  13. napistu/ingestion/hpa.py +147 -0
  14. napistu/ingestion/sbml.py +0 -97
  15. napistu/ingestion/string.py +2 -2
  16. napistu/matching/__init__.py +10 -0
  17. napistu/matching/constants.py +18 -0
  18. napistu/matching/interactions.py +518 -0
  19. napistu/matching/mount.py +529 -0
  20. napistu/matching/species.py +510 -0
  21. napistu/mcp/__init__.py +7 -4
  22. napistu/mcp/__main__.py +128 -72
  23. napistu/mcp/client.py +16 -25
  24. napistu/mcp/codebase.py +201 -145
  25. napistu/mcp/component_base.py +170 -0
  26. napistu/mcp/config.py +223 -0
  27. napistu/mcp/constants.py +45 -2
  28. napistu/mcp/documentation.py +253 -136
  29. napistu/mcp/documentation_utils.py +13 -48
  30. napistu/mcp/execution.py +372 -305
  31. napistu/mcp/health.py +47 -65
  32. napistu/mcp/profiles.py +10 -6
  33. napistu/mcp/server.py +161 -80
  34. napistu/mcp/tutorials.py +139 -87
  35. napistu/modify/__init__.py +1 -1
  36. napistu/modify/gaps.py +1 -1
  37. napistu/network/__init__.py +1 -1
  38. napistu/network/constants.py +101 -34
  39. napistu/network/data_handling.py +388 -0
  40. napistu/network/ig_utils.py +351 -0
  41. napistu/network/napistu_graph_core.py +354 -0
  42. napistu/network/neighborhoods.py +40 -40
  43. napistu/network/net_create.py +373 -309
  44. napistu/network/net_propagation.py +47 -19
  45. napistu/network/{net_utils.py → ng_utils.py} +124 -272
  46. napistu/network/paths.py +67 -51
  47. napistu/network/precompute.py +11 -11
  48. napistu/ontologies/__init__.py +10 -0
  49. napistu/ontologies/constants.py +129 -0
  50. napistu/ontologies/dogma.py +243 -0
  51. napistu/ontologies/genodexito.py +649 -0
  52. napistu/ontologies/mygene.py +369 -0
  53. napistu/ontologies/renaming.py +198 -0
  54. napistu/rpy2/__init__.py +229 -86
  55. napistu/rpy2/callr.py +47 -77
  56. napistu/rpy2/constants.py +24 -23
  57. napistu/rpy2/rids.py +61 -648
  58. napistu/sbml_dfs_core.py +587 -222
  59. napistu/scverse/__init__.py +15 -0
  60. napistu/scverse/constants.py +28 -0
  61. napistu/scverse/loading.py +727 -0
  62. napistu/utils.py +118 -10
  63. {napistu-0.2.5.dev7.dist-info → napistu-0.3.1.dist-info}/METADATA +8 -3
  64. napistu-0.3.1.dist-info/RECORD +133 -0
  65. tests/conftest.py +22 -0
  66. tests/test_context_discretize.py +56 -0
  67. tests/test_context_filtering.py +267 -0
  68. tests/test_identifiers.py +100 -0
  69. tests/test_indices.py +65 -0
  70. tests/{test_edgelist.py → test_ingestion_napistu_edgelist.py} +2 -2
  71. tests/test_matching_interactions.py +108 -0
  72. tests/test_matching_mount.py +305 -0
  73. tests/test_matching_species.py +394 -0
  74. tests/test_mcp_config.py +193 -0
  75. tests/test_mcp_documentation_utils.py +12 -3
  76. tests/test_mcp_server.py +156 -19
  77. tests/test_network_data_handling.py +397 -0
  78. tests/test_network_ig_utils.py +23 -0
  79. tests/test_network_neighborhoods.py +19 -0
  80. tests/test_network_net_create.py +459 -0
  81. tests/test_network_ng_utils.py +30 -0
  82. tests/test_network_paths.py +56 -0
  83. tests/{test_precomputed_distances.py → test_network_precompute.py} +8 -6
  84. tests/test_ontologies_genodexito.py +58 -0
  85. tests/test_ontologies_mygene.py +39 -0
  86. tests/test_ontologies_renaming.py +110 -0
  87. tests/test_rpy2_callr.py +79 -0
  88. tests/test_rpy2_init.py +151 -0
  89. tests/test_sbml.py +0 -31
  90. tests/test_sbml_dfs_core.py +134 -10
  91. tests/test_scverse_loading.py +778 -0
  92. tests/test_set_coverage.py +2 -2
  93. tests/test_utils.py +121 -1
  94. napistu/mechanism_matching.py +0 -1353
  95. napistu/rpy2/netcontextr.py +0 -467
  96. napistu-0.2.5.dev7.dist-info/RECORD +0 -98
  97. tests/test_igraph.py +0 -367
  98. tests/test_mechanism_matching.py +0 -784
  99. tests/test_net_utils.py +0 -149
  100. tests/test_netcontextr.py +0 -105
  101. tests/test_rpy2.py +0 -61
  102. /napistu/ingestion/{cpr_edgelist.py → napistu_edgelist.py} +0 -0
  103. {napistu-0.2.5.dev7.dist-info → napistu-0.3.1.dist-info}/WHEEL +0 -0
  104. {napistu-0.2.5.dev7.dist-info → napistu-0.3.1.dist-info}/entry_points.txt +0 -0
  105. {napistu-0.2.5.dev7.dist-info → napistu-0.3.1.dist-info}/licenses/LICENSE +0 -0
  106. {napistu-0.2.5.dev7.dist-info → napistu-0.3.1.dist-info}/top_level.txt +0 -0
  107. /tests/{test_obo.py → test_ingestion_obo.py} +0 -0
@@ -0,0 +1,39 @@
1
+ from napistu.ontologies.mygene import create_python_mapping_tables
2
+ from napistu.ontologies.constants import INTERCONVERTIBLE_GENIC_ONTOLOGIES
3
+
4
+
5
+ def test_create_python_mapping_tables_yeast():
6
+ """Test create_python_mapping_tables with yeast species."""
7
+ # Test with a subset of mappings to keep test runtime reasonable
8
+ test_mappings = {"ensembl_gene", "symbol", "uniprot"}
9
+
10
+ # Verify test mappings are valid
11
+ assert test_mappings.issubset(
12
+ INTERCONVERTIBLE_GENIC_ONTOLOGIES
13
+ ), "Test mappings must be valid ontologies"
14
+
15
+ # Call function with yeast species
16
+ mapping_tables = create_python_mapping_tables(
17
+ mappings=test_mappings,
18
+ species="Saccharomyces cerevisiae",
19
+ test_mode=True, # Limit to 1000 genes for faster testing
20
+ )
21
+
22
+ # Basic validation of results
23
+ assert isinstance(mapping_tables, dict), "Should return a dictionary"
24
+
25
+ # Check that all requested mappings are present (ignoring extras like ncbi_entrez_gene)
26
+ assert test_mappings.issubset(
27
+ set(mapping_tables.keys())
28
+ ), "All requested mappings should be present"
29
+
30
+ # Check each mapping table
31
+ for ontology in test_mappings:
32
+ df = mapping_tables[ontology]
33
+ assert not df.empty, f"Mapping table for {ontology} should not be empty"
34
+ assert (
35
+ df.index.name == "ncbi_entrez_gene"
36
+ ), f"Index should be entrez gene IDs for {ontology}"
37
+ assert (
38
+ not df.index.duplicated().any()
39
+ ), f"Should not have duplicate indices in {ontology}"
@@ -0,0 +1,110 @@
1
+ """Tests for the ontology aliases module."""
2
+
3
+ import pytest
4
+ import pandas as pd
5
+ from napistu import identifiers
6
+ from napistu.constants import IDENTIFIERS, SBML_DFS
7
+ from napistu.ontologies import renaming
8
+
9
+
10
+ @pytest.fixture
11
+ def mock_sbml_dfs():
12
+ """Create a mock SBML_dfs object for testing."""
13
+ # Create a simple species DataFrame with identifiers
14
+ s1_ids = identifiers.Identifiers(
15
+ [
16
+ {
17
+ IDENTIFIERS.ONTOLOGY: "ncbigene",
18
+ IDENTIFIERS.IDENTIFIER: "123",
19
+ IDENTIFIERS.URL: "http://ncbi/123",
20
+ IDENTIFIERS.BQB: "is",
21
+ },
22
+ {
23
+ IDENTIFIERS.ONTOLOGY: "uniprot_id",
24
+ IDENTIFIERS.IDENTIFIER: "P12345",
25
+ IDENTIFIERS.URL: "http://uniprot/P12345",
26
+ IDENTIFIERS.BQB: "is",
27
+ },
28
+ ]
29
+ )
30
+
31
+ s2_ids = identifiers.Identifiers(
32
+ [
33
+ {
34
+ IDENTIFIERS.ONTOLOGY: "ncbigene",
35
+ IDENTIFIERS.IDENTIFIER: "456",
36
+ IDENTIFIERS.URL: "http://ncbi/456",
37
+ IDENTIFIERS.BQB: "is",
38
+ }
39
+ ]
40
+ )
41
+
42
+ species_df = pd.DataFrame(
43
+ {"s_name": ["gene1", "gene2"], SBML_DFS.S_IDENTIFIERS: [s1_ids, s2_ids]}
44
+ )
45
+
46
+ # Create mock SBML_dfs object
47
+ class MockSBMLDfs:
48
+ def __init__(self):
49
+ self.species = species_df
50
+ self.schema = {"species": {"pk": "s_id", "id": SBML_DFS.S_IDENTIFIERS}}
51
+
52
+ def get_identifiers(self, table_name):
53
+ if table_name == SBML_DFS.SPECIES:
54
+ all_ids = []
55
+ for idx, row in self.species.iterrows():
56
+ for id_dict in row[SBML_DFS.S_IDENTIFIERS].ids:
57
+ all_ids.append({"s_id": idx, **id_dict})
58
+ return pd.DataFrame(all_ids)
59
+ return pd.DataFrame()
60
+
61
+ return MockSBMLDfs()
62
+
63
+
64
+ def test_rename_species_ontologies_basic(mock_sbml_dfs):
65
+ """Test basic alias updating functionality."""
66
+ # Define test aliases
67
+ test_aliases = {"ncbi_entrez_gene": {"ncbigene"}, "uniprot": {"uniprot_id"}}
68
+
69
+ # Update aliases
70
+ renaming.rename_species_ontologies(mock_sbml_dfs, test_aliases)
71
+
72
+ # Get updated identifiers
73
+ updated_ids = mock_sbml_dfs.get_identifiers(SBML_DFS.SPECIES)
74
+
75
+ # Check that ontologies were updated correctly
76
+ assert "ncbi_entrez_gene" in set(updated_ids[IDENTIFIERS.ONTOLOGY])
77
+ assert "uniprot" in set(updated_ids[IDENTIFIERS.ONTOLOGY])
78
+ assert "ncbigene" not in set(updated_ids[IDENTIFIERS.ONTOLOGY])
79
+ assert "uniprot_id" not in set(updated_ids[IDENTIFIERS.ONTOLOGY])
80
+
81
+
82
+ def test_rename_species_ontologies_no_overlap(mock_sbml_dfs):
83
+ """Test that error is raised when no aliases overlap with data."""
84
+ # Define aliases that don't match any existing ontologies
85
+ test_aliases = {"ensembl_gene": {"ensembl"}}
86
+
87
+ # Should raise ValueError due to no overlap
88
+ with pytest.raises(ValueError, match="do not overlap"):
89
+ renaming.rename_species_ontologies(mock_sbml_dfs, test_aliases)
90
+
91
+
92
+ def test_rename_species_ontologies_partial_update(mock_sbml_dfs):
93
+ """Test that partial updates work correctly."""
94
+ # Define aliases that only update some ontologies
95
+ test_aliases = {
96
+ "ncbi_entrez_gene": {"ncbigene"}
97
+ # Don't include uniprot_id mapping
98
+ }
99
+
100
+ # Update aliases
101
+ renaming.rename_species_ontologies(mock_sbml_dfs, test_aliases)
102
+
103
+ # Get updated identifiers
104
+ updated_ids = mock_sbml_dfs.get_identifiers(SBML_DFS.SPECIES)
105
+
106
+ # Check that only ncbigene was updated
107
+ assert "ncbi_entrez_gene" in set(updated_ids[IDENTIFIERS.ONTOLOGY])
108
+ assert "uniprot_id" in set(
109
+ updated_ids[IDENTIFIERS.ONTOLOGY]
110
+ ) # Should remain unchanged
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from unittest.mock import Mock, patch
5
+ import pandas as pd
6
+ import pytest
7
+
8
+
9
+ # Mock all rpy2 dependencies before any imports to prevent ImportErrors
10
+ sys.modules["rpy2"] = Mock()
11
+ sys.modules["rpy2.robjects"] = Mock()
12
+ sys.modules["rpy2.robjects.conversion"] = Mock()
13
+ sys.modules["rpy2.robjects.default_converter"] = Mock()
14
+ sys.modules["rpy2.robjects.packages"] = Mock()
15
+ sys.modules["rpy2.robjects.pandas2ri"] = Mock()
16
+ sys.modules["rpy2.rinterface"] = Mock()
17
+ sys.modules["rpy2_arrow"] = Mock()
18
+ sys.modules["rpy2_arrow.arrow"] = Mock()
19
+ sys.modules["pyarrow"] = Mock()
20
+
21
+ import napistu.rpy2.callr # noqa: E402
22
+
23
+
24
+ def test_get_napistu_r_without_rpy2():
25
+ """Test get_napistu_r when rpy2 not available."""
26
+ with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
27
+ with pytest.raises(ImportError, match="requires `rpy2`"):
28
+ napistu.rpy2.callr.get_napistu_r()
29
+
30
+
31
+ def test_bioconductor_org_r_function_without_rpy2():
32
+ """Test bioconductor function when rpy2 not available."""
33
+ with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
34
+ with pytest.raises(ImportError):
35
+ napistu.rpy2.callr.bioconductor_org_r_function("test", "Homo sapiens")
36
+
37
+
38
+ def test_get_rbase_without_rpy2():
39
+ """Test get_rbase when rpy2 not available."""
40
+ with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
41
+ with pytest.raises(ImportError):
42
+ napistu.rpy2.callr.get_rbase()
43
+
44
+
45
+ def test_pandas_conversion_functions_without_rpy2():
46
+ """Test pandas conversion functions when rpy2 not available."""
47
+ with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
48
+ # Test pandas to R conversion
49
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
50
+ with pytest.raises(ImportError):
51
+ napistu.rpy2.callr.pandas_to_r_dataframe(df)
52
+
53
+ # Test R to pandas conversion
54
+ mock_rdf = Mock()
55
+ with pytest.raises(ImportError):
56
+ napistu.rpy2.callr.r_dataframe_to_pandas(mock_rdf)
57
+
58
+ # Test internal converter function
59
+ with pytest.raises(ImportError):
60
+ napistu.rpy2.callr._get_py2rpy_pandas_conv()
61
+
62
+
63
+ def test_callr_module_import_safety():
64
+ """Test that callr module can be imported safely without triggering R initialization."""
65
+ import importlib
66
+
67
+ # Test module can be imported without rpy2
68
+ with patch.dict("sys.modules", {"rpy2": None}):
69
+ # This should not raise ImportError during import
70
+ importlib.reload(napistu.rpy2.callr)
71
+
72
+ # Test that function calls fail appropriately but imports don't
73
+ with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
74
+ # Functions should fail with ImportError, not other errors
75
+ with pytest.raises(ImportError):
76
+ napistu.rpy2.callr.get_napistu_r()
77
+
78
+ with pytest.raises(ImportError):
79
+ napistu.rpy2.callr.get_rbase()
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from unittest.mock import Mock, patch
5
+ import pytest
6
+
7
+
8
+ # Mock all rpy2 dependencies before any imports to prevent ImportErrors
9
+ sys.modules["rpy2"] = Mock()
10
+ sys.modules["rpy2.robjects"] = Mock()
11
+ sys.modules["rpy2.robjects.conversion"] = Mock()
12
+ sys.modules["rpy2.robjects.default_converter"] = Mock()
13
+ sys.modules["rpy2.robjects.packages"] = Mock()
14
+ sys.modules["rpy2.robjects.pandas2ri"] = Mock()
15
+ sys.modules["rpy2.rinterface"] = Mock()
16
+ sys.modules["rpy2_arrow"] = Mock()
17
+ sys.modules["rpy2_arrow.arrow"] = Mock()
18
+ sys.modules["pyarrow"] = Mock()
19
+
20
+ import napistu.rpy2 # noqa: E402
21
+
22
+
23
+ def test_rpy2_availability_detection():
24
+ """Test rpy2 availability detection in various scenarios."""
25
+ # Test ImportError case
26
+ with patch.dict("sys.modules", {"rpy2": None}):
27
+ if hasattr(napistu.rpy2.get_rpy2_availability, "cache_clear"):
28
+ napistu.rpy2.get_rpy2_availability.cache_clear()
29
+ assert napistu.rpy2.get_rpy2_availability() is False
30
+
31
+ # Test other exception case during import
32
+ with patch("builtins.__import__") as mock_import:
33
+ mock_import.side_effect = RuntimeError("R installation broken")
34
+ if hasattr(napistu.rpy2.get_rpy2_availability, "cache_clear"):
35
+ napistu.rpy2.get_rpy2_availability.cache_clear()
36
+ assert napistu.rpy2.get_rpy2_availability() is False
37
+
38
+ # Test success case
39
+ mock_rpy2 = Mock()
40
+ with patch.dict("sys.modules", {"rpy2": mock_rpy2}):
41
+ if hasattr(napistu.rpy2.get_rpy2_availability, "cache_clear"):
42
+ napistu.rpy2.get_rpy2_availability.cache_clear()
43
+ assert napistu.rpy2.get_rpy2_availability() is True
44
+
45
+
46
+ def test_caching_behavior():
47
+ """Test that lazy loading functions are properly cached."""
48
+ # Test availability caching
49
+ with patch("builtins.__import__") as mock_import:
50
+ mock_import.return_value = Mock()
51
+ if hasattr(napistu.rpy2.get_rpy2_availability, "cache_clear"):
52
+ napistu.rpy2.get_rpy2_availability.cache_clear()
53
+
54
+ result1 = napistu.rpy2.get_rpy2_availability()
55
+ result2 = napistu.rpy2.get_rpy2_availability()
56
+
57
+ assert result1 == result2
58
+ # Only test call count if caching is enabled
59
+ if hasattr(napistu.rpy2.get_rpy2_availability, "cache_clear"):
60
+ assert mock_import.call_count == 1 # Should only be called once
61
+
62
+ # Test core modules caching
63
+ with patch("napistu.rpy2.get_rpy2_availability", return_value=True):
64
+ with patch("rpy2.robjects.conversion"), patch(
65
+ "rpy2.robjects.default_converter"
66
+ ), patch("rpy2.robjects.packages.importr"):
67
+ if hasattr(napistu.rpy2.get_rpy2_core_modules, "cache_clear"):
68
+ napistu.rpy2.get_rpy2_core_modules.cache_clear()
69
+
70
+ result1 = napistu.rpy2.get_rpy2_core_modules()
71
+ result2 = napistu.rpy2.get_rpy2_core_modules()
72
+
73
+ # Only test object identity if caching is enabled
74
+ if hasattr(napistu.rpy2.get_rpy2_core_modules, "cache_clear"):
75
+ assert result1 is result2 # Same object due to caching
76
+
77
+
78
+ def test_lazy_import_functions_without_rpy2():
79
+ """Test that lazy import functions fail appropriately when rpy2 unavailable."""
80
+ with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
81
+ # Clear all caches if they exist
82
+ if hasattr(napistu.rpy2.get_rpy2_core_modules, "cache_clear"):
83
+ napistu.rpy2.get_rpy2_core_modules.cache_clear()
84
+ if hasattr(napistu.rpy2.get_rpy2_extended_modules, "cache_clear"):
85
+ napistu.rpy2.get_rpy2_extended_modules.cache_clear()
86
+ if hasattr(napistu.rpy2.get_napistu_r_package, "cache_clear"):
87
+ napistu.rpy2.get_napistu_r_package.cache_clear()
88
+
89
+ # All should raise ImportError
90
+ with pytest.raises(ImportError, match="requires `rpy2`"):
91
+ napistu.rpy2.get_rpy2_core_modules()
92
+
93
+ with pytest.raises(ImportError, match="requires `rpy2`"):
94
+ napistu.rpy2.get_rpy2_extended_modules()
95
+
96
+ with pytest.raises(ImportError, match="requires `rpy2`"):
97
+ napistu.rpy2.get_napistu_r_package()
98
+
99
+
100
+ def test_decorators():
101
+ """Test require_rpy2 and report_r_exceptions decorators."""
102
+ # Test require_rpy2 with rpy2 available
103
+ with patch("napistu.rpy2.get_rpy2_availability", return_value=True):
104
+
105
+ @napistu.rpy2.require_rpy2
106
+ def test_func_success():
107
+ return "success"
108
+
109
+ assert test_func_success() == "success"
110
+
111
+ # Test require_rpy2 without rpy2
112
+ with patch("napistu.rpy2.get_rpy2_availability", return_value=False):
113
+
114
+ @napistu.rpy2.require_rpy2
115
+ def test_func_fail():
116
+ return "success"
117
+
118
+ with pytest.raises(ImportError, match="test_func_fail.*requires `rpy2`"):
119
+ test_func_fail()
120
+
121
+ # Test report_r_exceptions with success
122
+ with patch("napistu.rpy2.get_rpy2_availability", return_value=True):
123
+
124
+ @napistu.rpy2.report_r_exceptions
125
+ def test_func_report_success():
126
+ return "success"
127
+
128
+ assert test_func_report_success() == "success"
129
+
130
+ # Test report_r_exceptions with failure
131
+ with patch("napistu.rpy2.get_rpy2_availability", return_value=True):
132
+ with patch("napistu.rpy2.rsession_info") as mock_rsession:
133
+
134
+ @napistu.rpy2.report_r_exceptions
135
+ def test_func_report_fail():
136
+ raise ValueError("R function failed")
137
+
138
+ with pytest.raises(ValueError, match="R function failed"):
139
+ test_func_report_fail()
140
+
141
+ mock_rsession.assert_called_once()
142
+
143
+
144
+ def test_module_import_safety():
145
+ """Test that modules can be imported safely without triggering R initialization."""
146
+ import importlib
147
+
148
+ # Test modules can be imported without rpy2
149
+ with patch.dict("sys.modules", {"rpy2": None}):
150
+ # These should not raise ImportError during import
151
+ importlib.reload(napistu.rpy2)
tests/test_sbml.py CHANGED
@@ -1,8 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
- import pytest
5
-
6
3
  import pandas as pd
7
4
  from napistu import sbml_dfs_core
8
5
  from napistu.ingestion import sbml
@@ -18,31 +15,3 @@ def test_sbml_dfs(sbml_path):
18
15
  assert type(dfs.get_cspecies_features()) is pd.DataFrame
19
16
  assert type(dfs.get_species_features()) is pd.DataFrame
20
17
  assert type(dfs.get_identifiers("species")) is pd.DataFrame
21
-
22
-
23
- @pytest.mark.skip_on_windows
24
- def test_adding_sbml_annotations(sbml_model):
25
- annotations = pd.DataFrame(
26
- [
27
- {
28
- "id": "compartment_12045",
29
- "type": "compartment",
30
- "uri": "http://identifiers.org/chebi/CHEBI:00000",
31
- },
32
- {
33
- "id": "species_9033251",
34
- "type": "species",
35
- "uri": "http://identifiers.org/bigg.metabolite/fakemet",
36
- },
37
- {
38
- "id": "species_9033251",
39
- "type": "species",
40
- "uri": "http://identifiers.org/chebi/CHEBI:00000",
41
- },
42
- ]
43
- )
44
-
45
- outpath = "/tmp/tmp_write_test.sbml"
46
-
47
- sbml.add_sbml_annotations(sbml_model, annotations, save_path=outpath)
48
- assert os.path.isfile(outpath) is True
@@ -10,7 +10,7 @@ from napistu.ingestion import sbml
10
10
  from napistu.modify import pathwayannot
11
11
 
12
12
  from napistu import identifiers as napistu_identifiers
13
- from napistu.constants import SBML_DFS
13
+ from napistu.constants import SBML_DFS, SBOTERM_NAMES
14
14
  from napistu.sbml_dfs_core import SBML_dfs
15
15
 
16
16
 
@@ -196,17 +196,13 @@ def test_sbml_dfs_remove_reactions_check_species(sbml_dfs):
196
196
  # find all r_ids for a species and check if
197
197
  # removing all these reactions also removes the species
198
198
  s_id = sbml_dfs.species.index[0]
199
- dat = (
200
- sbml_dfs.compartmentalized_species.query("s_id == @s_id").merge(
201
- sbml_dfs.reaction_species, on="sc_id"
202
- )
203
- )[["r_id", "sc_id"]]
204
- r_ids = dat["r_id"]
205
- sc_ids = dat["sc_id"]
199
+ dat = sbml_dfs.compartmentalized_species.query("s_id == @s_id").merge(
200
+ sbml_dfs.reaction_species, left_index=True, right_on="sc_id"
201
+ )
202
+ r_ids = dat["r_id"].unique()
206
203
  sbml_dfs.remove_reactions(r_ids, remove_species=True)
207
- for sc_id in sc_ids:
208
- assert sc_id not in sbml_dfs.compartmentalized_species.index
209
204
  assert s_id not in sbml_dfs.species.index
205
+ sbml_dfs.validate()
210
206
 
211
207
 
212
208
  def test_formula(sbml_dfs):
@@ -369,3 +365,131 @@ def test_get_identifiers_handles_missing_values():
369
365
  assert result.shape[0] == 0 or all(
370
366
  result[SBML_DFS.S_ID] == "s1"
371
367
  ), "Only Identifiers objects should be returned."
368
+
369
+
370
+ def test_find_underspecified_reactions():
371
+
372
+ reaction_w_regulators = pd.DataFrame(
373
+ {
374
+ SBML_DFS.SC_ID: ["A", "B", "C", "D", "E", "F", "G"],
375
+ SBML_DFS.STOICHIOMETRY: [-1, -1, 1, 1, 0, 0, 0],
376
+ SBML_DFS.SBO_TERM: [
377
+ SBOTERM_NAMES.REACTANT,
378
+ SBOTERM_NAMES.REACTANT,
379
+ SBOTERM_NAMES.PRODUCT,
380
+ SBOTERM_NAMES.PRODUCT,
381
+ SBOTERM_NAMES.CATALYST,
382
+ SBOTERM_NAMES.CATALYST,
383
+ SBOTERM_NAMES.STIMULATOR,
384
+ ],
385
+ }
386
+ ).assign(r_id="bar")
387
+ reaction_w_regulators[SBML_DFS.RSC_ID] = [
388
+ f"rsc_{i}" for i in range(len(reaction_w_regulators))
389
+ ]
390
+ reaction_w_regulators.set_index(SBML_DFS.RSC_ID, inplace=True)
391
+ reaction_w_regulators = sbml_dfs_core.add_sbo_role(reaction_w_regulators)
392
+
393
+ reaction_w_interactors = pd.DataFrame(
394
+ {
395
+ SBML_DFS.SC_ID: ["A", "B"],
396
+ SBML_DFS.STOICHIOMETRY: [-1, 1],
397
+ SBML_DFS.SBO_TERM: [SBOTERM_NAMES.REACTANT, SBOTERM_NAMES.REACTANT],
398
+ }
399
+ ).assign(r_id="baz")
400
+ reaction_w_interactors[SBML_DFS.RSC_ID] = [
401
+ f"rsc_{i}" for i in range(len(reaction_w_interactors))
402
+ ]
403
+ reaction_w_interactors.set_index(SBML_DFS.RSC_ID, inplace=True)
404
+ reaction_w_interactors = sbml_dfs_core.add_sbo_role(reaction_w_interactors)
405
+
406
+ working_reactions = reaction_w_regulators.copy()
407
+ working_reactions["new"] = True
408
+ working_reactions.loc["rsc_0", "new"] = False
409
+ working_reactions
410
+ result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
411
+ assert result == {"bar"}
412
+
413
+ # missing one enzyme -> operable
414
+ working_reactions = reaction_w_regulators.copy()
415
+ working_reactions["new"] = True
416
+ working_reactions.loc["rsc_4", "new"] = False
417
+ working_reactions
418
+ result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
419
+ assert result == set()
420
+
421
+ # missing one product -> inoperable
422
+ working_reactions = reaction_w_regulators.copy()
423
+ working_reactions["new"] = True
424
+ working_reactions.loc["rsc_2", "new"] = False
425
+ working_reactions
426
+ result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
427
+ assert result == {"bar"}
428
+
429
+ # missing all enzymes -> inoperable
430
+ working_reactions = reaction_w_regulators.copy()
431
+ working_reactions["new"] = True
432
+ working_reactions.loc["rsc_4", "new"] = False
433
+ working_reactions.loc["rsc_5", "new"] = False
434
+ working_reactions
435
+ result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
436
+ assert result == {"bar"}
437
+
438
+ # missing regulators -> operable
439
+ working_reactions = reaction_w_regulators.copy()
440
+ working_reactions["new"] = True
441
+ working_reactions.loc["rsc_6", "new"] = False
442
+ working_reactions
443
+ result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
444
+ assert result == set()
445
+
446
+ # remove an interactor
447
+ working_reactions = reaction_w_interactors.copy()
448
+ working_reactions["new"] = True
449
+ working_reactions.loc["rsc_0", "new"] = False
450
+ working_reactions
451
+ result = sbml_dfs_core.find_underspecified_reactions(working_reactions)
452
+ assert result == {"baz"}
453
+
454
+
455
+ def test_remove_entity_data_success(sbml_dfs_w_data):
456
+ """Test successful removal of entity data."""
457
+ # Get initial data
458
+ initial_species_data_keys = set(sbml_dfs_w_data.species_data.keys())
459
+ initial_reactions_data_keys = set(sbml_dfs_w_data.reactions_data.keys())
460
+
461
+ # Remove species data
462
+ sbml_dfs_w_data._remove_entity_data(SBML_DFS.SPECIES, "test_species")
463
+ assert "test_species" not in sbml_dfs_w_data.species_data
464
+ assert set(sbml_dfs_w_data.species_data.keys()) == initial_species_data_keys - {
465
+ "test_species"
466
+ }
467
+
468
+ # Remove reactions data
469
+ sbml_dfs_w_data._remove_entity_data(SBML_DFS.REACTIONS, "test_reactions")
470
+ assert "test_reactions" not in sbml_dfs_w_data.reactions_data
471
+ assert set(sbml_dfs_w_data.reactions_data.keys()) == initial_reactions_data_keys - {
472
+ "test_reactions"
473
+ }
474
+
475
+ # Validate the model is still valid after removals
476
+ sbml_dfs_w_data.validate()
477
+
478
+
479
+ def test_remove_entity_data_nonexistent(sbml_dfs_w_data, caplog):
480
+ """Test warning when trying to remove nonexistent entity data."""
481
+ # Try to remove nonexistent species data
482
+ sbml_dfs_w_data._remove_entity_data(SBML_DFS.SPECIES, "nonexistent_label")
483
+ assert "Label 'nonexistent_label' not found in species_data" in caplog.text
484
+ assert set(sbml_dfs_w_data.species_data.keys()) == {"test_species"}
485
+
486
+ # Clear the log
487
+ caplog.clear()
488
+
489
+ # Try to remove nonexistent reactions data
490
+ sbml_dfs_w_data._remove_entity_data(SBML_DFS.REACTIONS, "nonexistent_label")
491
+ assert "Label 'nonexistent_label' not found in reactions_data" in caplog.text
492
+ assert set(sbml_dfs_w_data.reactions_data.keys()) == {"test_reactions"}
493
+
494
+ # Validate the model is still valid
495
+ sbml_dfs_w_data.validate()