Repository: MoseleyBioinformaticsLab/kegg_pull (language: python)

Issue:
Adds link to GitHub issues page to README



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 25716b7..547ea0b 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -4,9 +4,13 @@ name: build
 
 on:
   push:
-    branches: [ "main" ]
+    branches:
+      - main
+      - dev
   pull_request:
-    branches: [ "main" ]
+    branches:
+      - main
+      - dev
   workflow_dispatch:
 
 jobs:
diff --git a/README.rst b/README.rst
index 5eb3c9e..ada3cbc 100644
--- a/README.rst
+++ b/README.rst
@@ -26,6 +26,10 @@ Install on Windows
 
 See our PyPi page `here <https://pypi.org/project/kegg-pull/>`__.
 
+Questions, Feature Requests, and Bug Reports
+--------------------------------------------
+Please submit any questions or feature requests you may have and report any potential bugs/errors you observe on `our GitHub issues page <https://github.com/MoseleyBioinformaticsLab/kegg_pull/issues>`__.
+
 Dependencies
 ------------
 Note, the ``pip`` command will install dependencies automatically.
diff --git a/src/kegg_pull/entry_ids.py b/src/kegg_pull/entry_ids.py
index 8e283ee..88b1475 100644
--- a/src/kegg_pull/entry_ids.py
+++ b/src/kegg_pull/entry_ids.py
@@ -6,43 +6,31 @@
 import typing as t
 
 from . import rest as r
+from . import kegg_url as ku
 
 
 def from_database(database_name: str, kegg_rest: r.KEGGrest = None) -> list:
     """ Pulls the KEGG entry IDs of a given database.
 
     :param database_name: The KEGG database to pull the entry IDs from.
-    :param kegg_rest: Optional KEGGrest object. If None, one is created with the default parameters.
+    :param kegg_rest: The KEGGrest object to request the entry IDs. If None, one is created with the default parameters.
     :return: The list of resulting entry IDs.
     :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
     """
-    return _process_response(method='list', kegg_rest=kegg_rest, database_name=database_name)
+    return _process_response(KEGGurl=ku.ListKEGGurl, kegg_rest=kegg_rest, database_name=database_name)
 
 
-def _process_response(method: str, kegg_rest: t.Union[r.KEGGrest, None], **kwargs) -> list:
-    """ Extracts the entry IDs from a KEGG response if successful, else raises an exception. The KEGG response arrives from calling
-    an entry IDs related method on a KEGGrest object.
+def _process_response(KEGGurl: type, kegg_rest: t.Union[r.KEGGrest, None], **kwargs) -> list:
+    """ Extracts the entry IDs from a KEGG response if successful, else raises an exception. The KEGG response arrives from making
+    an entry IDs related request with a KEGGrest object.
 
-    :param method: The method to call on the KEGGrest object.
-    :param kegg_rest: The KEGGrest object to call the method on. If None, one is created with the default parameters.
+    :param KEGGurl: The URL class for the request.
+    :param kegg_rest: The KEGGrest object to make the request with. If None, one is created with the default parameters.
     :param kwargs: The arguments to pass into the KEGGrest method.
     :return: The list of KEGG entry IDs.
     :raises RuntimeError: Raised if the KEGG response indicates a failure or time out.
     """
-    kegg_rest = kegg_rest if kegg_rest is not None else r.KEGGrest()
-    method: t.Callable = kegg_rest.__getattribute__(method)
-    kegg_response: r.KEGGresponse = method(**kwargs)
-
-    if kegg_response.status == r.KEGGresponse.Status.FAILED:
-        raise RuntimeError(
-            f'The KEGG request failed to pull the entry IDs from the following URL: {kegg_response.kegg_url.url}'
-        )
-    elif kegg_response.status == r.KEGGresponse.Status.TIMEOUT:
-        raise RuntimeError(
-            f'The KEGG request timed out while trying to pull the entry IDs from the following URL: '
-            f'{kegg_response.kegg_url.url}'
-        )
-
+    kegg_response: r.KEGGresponse = r.request_and_check_error(kegg_rest=kegg_rest, KEGGurl=KEGGurl, **kwargs)
     entry_ids: list = _parse_entry_ids_string(entry_ids_string=kegg_response.text_body)
 
     return entry_ids
@@ -83,11 +71,11 @@ def from_keywords(database_name: str, keywords: list, kegg_rest: r.KEGGrest = No
 
     :param database_name: The name of the database to pull entry IDs from.
     :param keywords: The keywords to search entries in the database with.
-    :param kegg_rest: Optional KEGGrest object. If None, one is created with the default parameters.
+    :param kegg_rest: The KEGGrest object to request the entry IDs. If None, one is created with the default parameters.
     :return: The list of entry IDs.
     :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
     """
-    return _process_response(method='keywords_find', kegg_rest=kegg_rest, database_name=database_name, keywords=keywords)
+    return _process_response(KEGGurl=ku.KeywordsFindKEGGurl, kegg_rest=kegg_rest, database_name=database_name, keywords=keywords)
 
 
 def from_molecular_attribute(
@@ -100,11 +88,11 @@ def from_molecular_attribute(
     :param formula: The chemical formula to search for.
     :param exact_mass: The exact mass of the compound to search for (a single value or a range).
     :param molecular_weight: The molecular weight of the compound to search for (a single value or a range).
-    :param kegg_rest: Optional KEGGrest object. If None, one is created with the default parameters.
+    :param kegg_rest: The KEGGrest object to request the entry IDs. If None, one is created with the default parameters.
     :return: The list of entry IDs.
     :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
     """
     return _process_response(
-        method='molecular_find', kegg_rest=kegg_rest, database_name=database_name, formula=formula, exact_mass=exact_mass,
+        KEGGurl=ku.MolecularFindKEGGurl, kegg_rest=kegg_rest, database_name=database_name, formula=formula, exact_mass=exact_mass,
         molecular_weight=molecular_weight
     )
diff --git a/src/kegg_pull/link_to_dict.py b/src/kegg_pull/link_to_dict.py
index 35f230b..0558c99 100644
--- a/src/kegg_pull/link_to_dict.py
+++ b/src/kegg_pull/link_to_dict.py
@@ -1,291 +1,273 @@
 """
 Creating Dictionaries From KEGG Link Requests
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Contains methods for converting the output from the KEGG "link" REST operation into dictionaries linking the entry IDs from one database to the IDs of related entries.
+Functionality for converting the output from the KEGG "link" REST operation into dictionaries linking the entry IDs from one database to the IDs of related entries.
 """
+import typing as t
+
 from . import rest as r
+from . import kegg_url as ku
+
+
+def database_link(target_database_name: str, source_database_name: str, kegg_rest: r.KEGGrest = None) -> dict:
+    """ Converts the output of the KEGG "link" operation (of the form that links the entry IDs of one database to the entry IDs of
+    another) into a dictionary.
+
+    :param target_database_name: One of the two KEGG databases to pull linked entries from.
+    :param source_database_name: The other KEGG database to link entries from the target database.
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return _to_dict(
+        kegg_rest=kegg_rest, KEGGurl=ku.DatabaseLinkKEGGurl, target_database_name=target_database_name,
+        source_database_name=source_database_name
+    )
+
+
+def _to_dict(kegg_rest: t.Union[r.KEGGrest, None], KEGGurl: type, **kwargs) -> dict:
+    """ Converts output from the KEGG "link" operation into a dictionary.
+
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :param KEGGurl: The class extending AbstractKEGGurl used to form the URL for the link operation.
+    :param kwargs: The keyword arguments for constructing the URL.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    kegg_response: r.KEGGresponse = r.request_and_check_error(kegg_rest=kegg_rest, KEGGurl=KEGGurl, **kwargs)
+    linked_ids = {}
+
+    for link in kegg_response.text_body.strip().split('\n'):
+        [link_from_id, link_to_id] = link.split('\t')
+
+        if link_from_id in linked_ids.keys():
+            linked_ids[link_from_id].add(link_to_id)
+        else:
+            linked_ids[link_from_id] = {link_to_id}
+
+    return linked_ids
+
+
+def entries_link(target_database_name: str, entry_ids: list, kegg_rest: r.KEGGrest = None) -> dict:
+    """ Converts the output of the KEGG "link" operation (of the form that links the entry IDs of a database to specific provided
+    entry IDs) to a dictionary.
+
+    :param target_database_name: The KEGG database to find links to the provided entries.
+    :param entry_ids: The IDs of the entries to link to entries in the target database.
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return _to_dict(
+        kegg_rest=kegg_rest, KEGGurl=ku.EntriesLinkKEGGurl, target_database_name=target_database_name, entry_ids=entry_ids
+    )
+
+
+def compound_to_pathway(add_glycans: bool, kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps compound entry IDs to related pathway IDs.
+
+    :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return reverse_mapping(mapping=pathway_to_compound(add_glycans=add_glycans, kegg_rest=kegg_rest))
+
+
+def reverse_mapping(mapping: dict) -> dict:
+    """ Reverses the dictionary mapping entry IDs of one database to IDs of related entries, turning keys into values and values into keys.
+
+    :param mapping: The dictionary, of entry IDs (strings) to sets of entry IDs, to reverse.
+    :return: The reversed mapping.
+    """
+    reversed_mapping: dict = {}
+
+    for key, values in mapping.items():
+        for value in values:
+            _add_to_dict(dictionary=reversed_mapping, key=value, values={key})
+
+    return reversed_mapping
+
+
+def _add_to_dict(dictionary: dict, key: str, values: set) -> None:
+    """ Adds a set of values to a set mapped from a given key in a dictionary.
+
+    :param dictionary: The dictionary mapping to sets, one of which the values will be added to.
+    :param key: The key in the dictionary mapping to the set that the values will be added to.
+    :param values: The values to add to the set mapped to by the key.
+    """
+    if key in dictionary.keys():
+        dictionary[key].update(values)
+    else:
+        dictionary[key] = values
+
+
+def pathway_to_compound(add_glycans: bool, kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps pathway entry IDs to related compound IDs.
+
+    :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return _database_to_compound(database='pathway', add_glycans=add_glycans, kegg_rest=kegg_rest)
+
+
+def _database_to_compound(database: str, add_glycans, kegg_rest: t.Union[r.KEGGrest, None]) -> dict:
+    """ Creates a dictionary that maps the entry IDs of a given KEGG database to related compound IDs.
+
+    :param database: The database with IDs to map to compound IDs.
+    :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    database_to_compound: dict = database_link(target_database_name='compound', source_database_name=database, kegg_rest=kegg_rest)
+
+    if add_glycans:
+        database_to_glycan: dict = database_link(target_database_name='glycan', source_database_name=database, kegg_rest=kegg_rest)
+        glycan_to_compound: dict = database_link(target_database_name='compound', source_database_name='glycan', kegg_rest=kegg_rest)
 
-import logging as l
-import jsonschema as js
-import json as j
-
-class LinkToDict:
-    """Class containing methods for creating dictionaries from the output of the "link" KEGG REST operation, both general methods and methods for creating specific mappings."""
-    def __init__(self, kegg_rest: r.KEGGrest = None) -> None:
-        """
-        :param kegg_rest: Optional KEGGrest object for making the "link" operation requests. A new KEGGrest object is created by default.
-        """
-        self._kegg_rest = kegg_rest if kegg_rest is not None else r.KEGGrest()
+        for entry_id, glycan_ids in database_to_glycan.items():
+            for glycan_id in glycan_ids:
+                if glycan_id in glycan_to_compound.keys():
+                    glycan_compound_ids: set = glycan_to_compound[glycan_id]
+                    _add_to_dict(dictionary=database_to_compound, key=entry_id, values=glycan_compound_ids)
 
-    def database_link(self, target_database_name: str, source_database_name: str) -> dict:
-        """ Converts the output of the KEGGrest database_link method into a dictionary.
-
-        :param target_database_name: One of the two KEGG databases to pull linked entries from.
-        :param source_database_name: The other KEGG database to link entries from the target database.
-        :return: The dictionary.
-        """
-        kegg_response: r.KEGGresponse = self._kegg_rest.database_link(
-            target_database_name=target_database_name, source_database_name=source_database_name
-        )
-
-        return self._to_dict(kegg_response=kegg_response)
-
-    def entries_link(self, target_database_name: str, entry_ids: list) -> dict:
-        """ Converts the output of the KEGGrest entries_link method into a dictionary.
-
-        :param target_database_name: The KEGG database to find links to the provided entries.
-        :param entry_ids: The IDs of the entries to link to entries in the target database.
-        :return: The dictionary.
-        """
-        kegg_response: r.KEGGresponse = self._kegg_rest.entries_link(target_database_name=target_database_name, entry_ids=entry_ids)
-
-        return self._to_dict(kegg_response=kegg_response)
-
-    @staticmethod
-    def reverse_mapping(mapping: dict) -> dict:
-        """ Reverses the dictionary mapping entry IDs of one database to IDs of related entries, turning keys into values and values into keys.
-
-        :param mapping: The dictionary, of entry IDs (strings) to sets of entry IDs, to reverse.
-        :return: The reversed mapping.
-        """
-        reverse_mapping: dict = {}
-
-        for key, values in mapping.items():
-            for value in values:
-                LinkToDict._add_to_dict(dictionary=reverse_mapping, key=value, values={key})
-
-        return reverse_mapping
-
-    @staticmethod
-    def _to_dict(kegg_response: r.KEGGresponse) -> dict:
-        """ Converts output from a "link" KEGG REST operation into a dictionary.
-
-        :param kegg_response: The KEGGresponse object from the "link" operation.
-        :return: The dictionary.
-        """
-        linked_ids = {}
-
-        for link in kegg_response.text_body.strip().split('\n'):
-            [link_from_id, link_to_id] = link.split('\t')
-
-            if link_from_id in linked_ids.keys():
-                linked_ids[link_from_id].add(link_to_id)
-            else:
-                linked_ids[link_from_id] = {link_to_id}
-
-        return linked_ids
-
-    def compound_to_pathway(self, add_glycans: bool) -> dict:
-        """ Creates a mapping of compound entry IDs to related pathway IDs.
-
-        :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
-        :return: The mapping.
-        """
-        return LinkToDict.reverse_mapping(mapping=self.pathway_to_compound(add_glycans=add_glycans))
-
-    def pathway_to_compound(self, add_glycans: bool) -> dict:
-        """ Creates a mapping of pathway entry IDs to related compound IDs.
-
-        :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
-        :return: The mapping.
-        """
-        return self._database_to_compound(database='pathway', add_glycans=add_glycans)
-
-    def _database_to_compound(self, database: str, add_glycans):
-        """ Creates a mapping of a given KEGG database to related compound IDs.
-
-        :param database: The database with IDs to map to compound IDs.
-        :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
-        :return: The mapping.
-        """
-        database_to_compound: dict = self.database_link(target_database_name='compound', source_database_name=database)
-
-        if add_glycans:
-            database_to_glycan: dict = self.database_link(target_database_name='glycan', source_database_name=database)
-            glycan_to_compound: dict = self.database_link(target_database_name='compound', source_database_name='glycan')
-
-            for entry_id, glycan_ids in database_to_glycan.items():
-                for glycan_id in glycan_ids:
-                    if glycan_id in glycan_to_compound.keys():
-                        glycan_compound_ids: set = glycan_to_compound[glycan_id]
-                        LinkToDict._add_to_dict(dictionary=database_to_compound, key=entry_id, values=glycan_compound_ids)
-
-        return database_to_compound
-
-    @staticmethod
-    def _add_to_dict(dictionary: dict, key: str, values: set) -> None:
-        """ Adds a set of values to a set mapped from a given key in a dictionary.
+    return database_to_compound
 
-        :param dictionary: The dictionary mapping to sets, one of which the value will be added to.
-        :param key: The key in the dictionary mapping to the set that the values will be added to.
-        :param values: The values to add to the set mapped to by the key.
-        """
-        if key in dictionary.keys():
-            dictionary[key].update(values)
+
+def compound_to_reaction(add_glycans: bool, kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps compound entry IDs to related reaction IDs.
+
+    :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return reverse_mapping(mapping=reaction_to_compound(add_glycans=add_glycans, kegg_rest=kegg_rest))
+
+
+def reaction_to_compound(add_glycans: bool, kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps reaction entry IDs to related compound IDs.
+
+    :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return _database_to_compound(database='reaction', add_glycans=add_glycans, kegg_rest=kegg_rest)
+
+def compound_to_gene(add_glycans: bool, kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps compound entry IDs to related gene IDs (from the KEGG ko database).
+
+    :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return reverse_mapping(mapping=gene_to_compound(add_glycans=add_glycans, kegg_rest=kegg_rest))
+
+def gene_to_compound(add_glycans: bool, kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps gene entry IDs (from the KEGG ko database) to related compound IDs.
+
+    :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    gene_to_reaction_: dict = database_link(target_database_name='reaction', source_database_name='ko', kegg_rest=kegg_rest)
+    reaction_to_compound_: dict = reaction_to_compound(add_glycans=add_glycans, kegg_rest=kegg_rest)
+    gene_to_compound_: dict = {}
+
+    for gene_id, reaction_ids in gene_to_reaction_.items():
+        for reaction_id in reaction_ids:
+            if reaction_id in reaction_to_compound_.keys():
+                compound_ids: set = reaction_to_compound_[reaction_id]
+                _add_to_dict(dictionary=gene_to_compound_, key=gene_id, values=compound_ids)
+
+    return gene_to_compound_
+
+
+def reaction_to_pathway(kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps reaction entry IDs to related pathway IDs.
+
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return reverse_mapping(mapping=pathway_to_reaction(kegg_rest=kegg_rest))
+
+
+def pathway_to_reaction(kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps pathway entry IDs to related reaction IDs.
+
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return _pathway_to_database(database='reaction', duplicate_prefix='path:rn', kegg_rest=kegg_rest)
+
+
+def _pathway_to_database(database: str, duplicate_prefix: str, kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps pathway entry IDs to IDs of a given KEGG database.
+
+    :param database: The database with IDs to be mapped to from pathway IDs.
+    :param duplicate_prefix: The prefix of duplicate pathway IDs to remove from the mapping.
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    pathway_to_database: dict = database_link(target_database_name=database, source_database_name='pathway', kegg_rest=kegg_rest)
+
+    for pathway_id in list(pathway_to_database.keys()):
+        if pathway_id.startswith(duplicate_prefix):
+            equivalent: str = pathway_id.replace(duplicate_prefix, 'path:map')
+            are_equivalent = pathway_to_database[equivalent] == pathway_to_database[pathway_id]
+
+            assert are_equivalent, f'Pathway {equivalent} is not equivalent to {pathway_id}'
+            del pathway_to_database[pathway_id]
         else:
-            dictionary[key] = values
+            assert pathway_id.startswith('path:map'), f'Unknown path map: {pathway_id}'
+
+    return pathway_to_database
+
+
+def gene_to_pathway(kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps gene entry IDs (from the KEGG ko database) to related pathway IDs.
+
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return reverse_mapping(mapping=pathway_to_gene(kegg_rest=kegg_rest))
 
-    def compound_to_reaction(self, add_glycans: bool) -> dict:
-        """ Creates a mapping of compound entry IDs to related reaction IDs.
 
-        :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
-        :return: The mapping.
-        """
-        return LinkToDict.reverse_mapping(mapping=self.reaction_to_compound(add_glycans=add_glycans))
+def pathway_to_gene(kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps pathway entry IDs to related gene IDs (from the KEGG ko database).
 
-    def reaction_to_compound(self, add_glycans: bool) -> dict:
-        """ Creates a mapping of reaction entry IDs to related compound IDs.
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return _pathway_to_database(database='ko', duplicate_prefix='path:ko', kegg_rest=kegg_rest)
 
-        :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
-        :return: The mapping.
-        """
-        return self._database_to_compound(database='reaction', add_glycans=add_glycans)
 
-    def compound_to_gene(self, add_glycans: bool) -> dict:
-        """ Creates a mapping of compound entry IDs to related gene IDs (from the KEGG ko database).
+def reaction_to_gene(kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps reaction entry IDs to related gene IDs (from the KEGG ko database).
 
-        :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
-        :return: The mapping.
-        """
-        return LinkToDict.reverse_mapping(mapping=self.gene_to_compound(add_glycans=add_glycans))
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return database_link(target_database_name='ko', source_database_name='reaction', kegg_rest=kegg_rest)
 
-    def gene_to_compound(self, add_glycans: bool) -> dict:
-        """ Creates a mapping of gene entry IDs (from the KEGG ko database) to related compound IDs.
 
-        :param add_glycans: Whether to add the corresponding compound IDs of KEGG glycan entries.
-        :return: The mapping.
-        """
-        gene_to_reaction: dict = self.database_link(target_database_name='reaction', source_database_name='ko')
-        reaction_to_compound: dict = self.reaction_to_compound(add_glycans=add_glycans)
-        gene_to_compound: dict = {}
-
-        for gene_id, reaction_ids in gene_to_reaction.items():
-            for reaction_id in reaction_ids:
-                if reaction_id in reaction_to_compound.keys():
-                    compound_ids: set = reaction_to_compound[reaction_id]
-                    LinkToDict._add_to_dict(dictionary=gene_to_compound, key=gene_id, values=compound_ids)
-
-        return gene_to_compound
-
-    def reaction_to_pathway(self) -> dict:
-        """ Creates a mapping of reaction entry IDs to related pathway IDs.
-
-        :return: The mapping.
-        """
-        return LinkToDict.reverse_mapping(mapping=self.pathway_to_reaction())
-
-    def pathway_to_reaction(self) -> dict:
-        """ Creates a mapping of pathway entry IDs to related reaction IDs.
-
-        :return: The mapping.
-        """
-        return self._pathway_to_database(database='reaction', duplicate_prefix='path:rn')
-
-    def _pathway_to_database(self, database: str, duplicate_prefix: str) -> dict:
-        """ Creates a mapping of a pathway entry IDs to a IDs of a given KEGG database.
-
-        :param database: The database with IDs to be mapped to from pathway IDs.
-        :param duplicate_prefix: The prefix of duplicate pathway IDs to remove from the mapping.
-        :return: The mapping.
-        """
-        pathway_to_database: dict = self.database_link(target_database_name=database, source_database_name='pathway')
-
-        for pathway_id in list(pathway_to_database.keys()):
-            if pathway_id.startswith(duplicate_prefix):
-                equivalent: str = pathway_id.replace(duplicate_prefix, 'path:map')
-                are_equivalent = pathway_to_database[equivalent] == pathway_to_database[pathway_id]
-
-                assert are_equivalent, f'Pathway {equivalent} is not equivalent to {pathway_id}'
-                del pathway_to_database[pathway_id]
-            else:
-                assert pathway_id.startswith('path:map'), f'Unknown path map: {pathway_id}'
-
-        return pathway_to_database
-
-    def gene_to_pathway(self) -> dict:
-        """ Creates a mapping of gene entry IDs (from the KEGG ko database) to related pathway IDs.
-
-        :return: The mapping.
-        """
-        return LinkToDict.reverse_mapping(mapping=self.pathway_to_gene())
-
-    def pathway_to_gene(self) -> dict:
-        """ Creates a mapping of pathway entry IDs to related gene IDs (from the KEGG ko database).
-
-        :return: The mapping.
-        """
-        return self._pathway_to_database(database='ko', duplicate_prefix='path:ko')
-
-    def reaction_to_gene(self) -> dict:
-        """ Creates a mapping of reaction entry IDs to related gene IDs (from the KEGG ko database).
-
-        :return: The mapping.
-        """
-        return self.database_link(target_database_name='ko', source_database_name='reaction')
-
-    def gene_to_reaction(self) -> dict:
-        """ Creates a mapping of gene entry IDs (from the KEGG ko database) to related reaction IDs.
-
-        :return: The mapping.
-        """
-        return self.database_link(target_database_name='reaction', source_database_name='ko')
-
-    @staticmethod
-    def save_json(mapping: dict, file_path: str) -> None:
-        """ Saves a KEGG mapping to a JSON file.
-
-        :param mapping: The mapping of KEGG entry IDs to sets of IDs.
-        :param file_path: The path to the JSON file.
-        :raises ValidationError: Raised if the mapping does not follow the correct JSON schema.
-        """
-        for entry_id, entry_ids in mapping.items():
-            mapping[entry_id] = sorted(entry_ids)
-
-        LinkToDict._validate_json(mapping=mapping)
-
-        with open(file_path, 'w') as file:
-            j.dump(mapping, file)
-
-    @staticmethod
-    def _validate_json(mapping: dict) -> None:
-        """ Ensures a mapping follows the correct schema.
-
-        :param mapping: The mapping to validate.
-        :raises ValueError: Raised if the mapping does not follow the correct JSON schema.
-        """
-        mapping_schema = {
-            'type': 'object',
-            'patternProperties': {
-                '^.*$': {
-                    'type': 'array',
-                    'items': {
-                        'type': 'string'
-                    }
-                }
-            }
-        }
+def gene_to_reaction(kegg_rest: r.KEGGrest = None) -> dict:
+    """ Creates a dictionary that maps gene entry IDs (from the KEGG ko database) to related reaction IDs.
 
-        try:
-            js.validate(instance=mapping, schema=mapping_schema)
-        except js.exceptions.ValidationError as e:
-            l.error('The mapping must be a dictionary of KEGG entry IDs (strings) mapped to a set of KEGG entry IDs')
-
-            raise e
-
-    @staticmethod
-    def load_json(file_path: str) -> dict:
-        """ Loads a KEGG mapping from a JSON file.
-
-        :param file_path: The path to the JSON file.
-        :return: The mapping.
-        :raises ValidationError: Raised if the JSON file does not follow the correct schema.
-        """
-        with open(file_path, 'r') as file:
-            mapping: dict = j.load(file)
-
-        LinkToDict._validate_json(mapping=mapping)
-
-        for entry_id, entry_ids in mapping.items():
-            mapping[entry_id] = set(entry_ids)
-
-        return mapping
+    :param kegg_rest: The KEGGrest object to perform the "link" operation. If None, one is created with the default parameters.
+    :return: The dictionary.
+    :raises RuntimeError: Raised if the request to the KEGG REST API fails or times out.
+    """
+    return database_link(target_database_name='reaction', source_database_name='ko', kegg_rest=kegg_rest)
diff --git a/src/kegg_pull/link_to_dict_cli.py b/src/kegg_pull/link_to_dict_cli.py
index 1601303..7427492 100644
--- a/src/kegg_pull/link_to_dict_cli.py
+++ b/src/kegg_pull/link_to_dict_cli.py
@@ -1,29 +1,28 @@
 """
 Usage:
     kegg_pull link-to-dict -h | --help
-    kegg_pull link-to-dict <target-database-name> <source-database-name> [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict --link-target=<target-database-name> <entry-ids> [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict pathway-to-compound [--add-glycans] [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict reaction-to-compound [--add-glycans] [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict gene-to-compound [--add-glycans] [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict compound-to-pathway [--add-glycans] [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict compound-to-reaction [--add-glycans] [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict compound-to-gene [--add-glycans] [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict pathway-to-gene [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict pathway-to-reaction [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict gene-to-pathway [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict reaction-to-pathway [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict reaction-to-gene [--output=<output-location>] [--file=<file-name>]
-    kegg_pull link-to-dict gene-to-reaction [--output=<output-location>] [--file=<file-name>]
+    kegg_pull link-to-dict <target-database-name> <source-database-name> [--output=<output>]
+    kegg_pull link-to-dict --link-target=<target-database-name> <entry-ids> [--output=<output>]
+    kegg_pull link-to-dict pathway-to-compound [--add-glycans] [--output=<output>]
+    kegg_pull link-to-dict reaction-to-compound [--add-glycans] [--output=<output>]
+    kegg_pull link-to-dict gene-to-compound [--add-glycans] [--output=<output>]
+    kegg_pull link-to-dict compound-to-pathway [--add-glycans] [--output=<output>]
+    kegg_pull link-to-dict compound-to-reaction [--add-glycans] [--output=<output>]
+    kegg_pull link-to-dict compound-to-gene [--add-glycans] [--output=<output>]
+    kegg_pull link-to-dict pathway-to-gene [--output=<output>]
+    kegg_pull link-to-dict pathway-to-reaction [--output=<output>]
+    kegg_pull link-to-dict gene-to-pathway [--output=<output>]
+    kegg_pull link-to-dict reaction-to-pathway [--output=<output>]
+    kegg_pull link-to-dict reaction-to-gene [--output=<output>]
+    kegg_pull link-to-dict gene-to-reaction [--output=<output>]
 
 Options:
     -h --help                               Show this help message.
     <target-database-name>                  The name of the database to find cross-references in the source database.
     <source-database-name>                  The name of the database from which cross-references are found in the target database.
-    --output=<output-location>              The location (either a directory or ZIP archive if ends in '.zip') of the file to store the mapping. Prints to the console if not set.
-    --file=<file-name>                      The name of the JSON file to store in the output location (specified by --output). If --output is set but --file is not set, defaults to file name of "mapping.json".
+    --output=<output>                       The location (either a directory or ZIP archive) of the JSON file to store the mapping. If not set, prints a JSON representation of the mapping to the console. If a ZIP archive, the file path must be in the form of /path/to/zip-archive.zip:/path/to/file (e.g. ./archive.zip:mapping.json).
     --link-target=<target-database-name>    The name of the database to find cross-references in the provided entry IDs.
-    <entry-ids>                             Comma separated list of entry IDs.
+    <entry-ids>                             Comma separated list of entry IDs (e.g. Id1,Id2,Id3 etc.). Or if equal to "-", entry IDs are read from standard input, one entry ID per line; Press CTRL+D to finalize input or pipe (e.g. cat file.txt | kegg_pull link-to-dict --link-target=drug - ...).
     pathway-to-compound                     Creates a specific mapping of KEGG entry IDs.
     --add-glycans                           Whether to add the compound IDs corresponding to KEGG glycan entries.
     reaction-to-compound                    Creates a specific mapping of KEGG entry IDs.
@@ -48,27 +47,38 @@
 def main():
     args: dict = d.docopt(__doc__)
     add_glycans: bool = args['--add-glycans']
-    link_to_dict = ltd.LinkToDict()
 
     if args['pathway-to-compound']:
-        mapping: dict = link_to_dict.pathway_to_compound(add_glycans=add_glycans)
+        mapping: dict = ltd.pathway_to_compound(add_glycans=add_glycans)
     elif args['reaction-to-compound']:
-        mapping: dict = link_to_dict.reaction_to_compound(add_glycans=add_glycans)
+        mapping: dict = ltd.reaction_to_compound(add_glycans=add_glycans)
     elif args['gene-to-compound']:
-        mapping: dict = link_to_dict.gene_to_compound(add_glycans=add_glycans)
+        mapping: dict = ltd.gene_to_compound(add_glycans=add_glycans)
     elif args['compound-to-pathway']:
-        mapping: dict = link_to_dict.compound_to_pathway(add_glycans=add_glycans)
-    # TODO Fill in the rest of the specific mappings
+        mapping: dict = ltd.compound_to_pathway(add_glycans=add_glycans)
+    elif args['compound-to-reaction']:
+        mapping: dict = ltd.compound_to_reaction(add_glycans=add_glycans)
+    elif args['compound-to-gene']:
+        mapping: dict = ltd.compound_to_gene(add_glycans=add_glycans)
+    elif args['pathway-to-gene']:
+        mapping: dict = ltd.pathway_to_gene()
+    elif args['pathway-to-reaction']:
+        mapping: dict = ltd.pathway_to_reaction()
+    elif args['gene-to-pathway']:
+        mapping: dict = ltd.gene_to_pathway()
+    elif args['reaction-to-pathway']:
+        mapping: dict = ltd.reaction_to_pathway()
+    elif args['reaction-to-gene']:
+        mapping: dict = ltd.reaction_to_gene()
+    elif args['gene-to-reaction']:
+        mapping: dict = ltd.gene_to_reaction()
     elif args['<target-database-name']:
-        mapping: dict = link_to_dict.database_link(
+        mapping: dict = ltd.database_link(
             target_database_name=args['<target-database-name>'], source_database_name=args['source-database-name']
         )
     else:
-        entry_ids: list = u.split_comma_separated_list(list_string=args['<entry-ids>'])
-        mapping: dict = link_to_dict.entries_link(target_database_name=args['--link-target'], entry_ids=entry_ids)
+        entry_ids: list = u.handle_cli_input(input_source=args['<entry-ids>'])
+        mapping: dict = ltd.entries_link(target_database_name=args['--link-target'], entry_ids=entry_ids)
 
-    mapping: str = j.dumps(obj=mapping, indent=1)
-    output_location: str = args['--output']
-    file_name: str = args['--file-name']
-    file_name: str = 'entry-ids.txt' if file_name is None and output_location is not None else file_name
-    u.handle_cli_output(output_location=output_location, output_content=mapping, file_name=file_name, save_type='w')
+    mapping: str = j.dumps(obj=mapping, indent=2)
+    u.handle_cli_output(output_target=args['--output'], output_content=mapping)
diff --git a/src/kegg_pull/rest.py b/src/kegg_pull/rest.py
index b289d74..1f577e7 100644
--- a/src/kegg_pull/rest.py
+++ b/src/kegg_pull/rest.py
@@ -264,3 +264,25 @@ def ddi(self, drug_entry_ids: list) -> KEGGresponse:
         :return: The KEGG response
         """
         return self.request(KEGGurl=ku.DdiKEGGurl, drug_entry_ids=drug_entry_ids)
+
+
+def request_and_check_error(kegg_rest: KEGGrest = None, KEGGurl: type = None, kegg_url: ku.AbstractKEGGurl = None, **kwargs) -> KEGGresponse:
+    """ Makes a general request to the KEGG REST API using a KEGGrest object. Creates the KEGGrest object if one is not provided.
+    Additionally, raises an exception if the request is not successful, specifying the URL that was unsuccessful.
+
+    :param kegg_rest: The KEGGrest object to perform the request. If None, one is created with the default parameters.
+    :param KEGGurl: Optional KEGG URL class (extended from AbstractKEGGurl) that's instantiated with provided keyword arguments.
+    :param kegg_url: Optional KEGGurl object that's already instantiated (used if KEGGurl class is not provided).
+    :param kwargs: The keyword arguments used to instantiate the KEGGurl class, if provided.
+    :return: The KEGG response
+    :raises RuntimeError: Raised if the request fails or times out.
+    """
+    kegg_rest = kegg_rest if kegg_rest is not None else KEGGrest()
+    kegg_response: KEGGresponse = kegg_rest.request(KEGGurl=KEGGurl, kegg_url=kegg_url, **kwargs)
+
+    if kegg_response.status == KEGGresponse.Status.FAILED:
+        raise RuntimeError(f'The KEGG request failed with the following URL: {kegg_response.kegg_url.url}')
+    elif kegg_response.status == KEGGresponse.Status.TIMEOUT:
+        raise RuntimeError(f'The KEGG request timed out with the following URL: {kegg_response.kegg_url.url}')
+
+    return kegg_response


Test patch:
diff --git a/dev/test_entry_ids.py b/dev/test_entry_ids.py
index dc44d49..80c61d1 100644
--- a/dev/test_entry_ids.py
+++ b/dev/test_entry_ids.py
@@ -4,37 +4,21 @@
 
 import kegg_pull.rest as r
 import kegg_pull.entry_ids as ei
+import kegg_pull.kegg_url as ku
 import dev.utils as u
 
 
-test_process_response_exception_data = [
-    ('The KEGG request failed to pull the entry IDs from the following URL: url/mock', r.KEGGresponse.Status.FAILED),
-    (
-        'The KEGG request timed out while trying to pull the entry IDs from the following URL: url/mock',
-        r.KEGGresponse.Status.TIMEOUT
-    )
-]
-@pt.mark.parametrize('expected_message,status', test_process_response_exception_data)
-def test_process_response_exception(mocker, expected_message: str, status: r.KEGGresponse.Status):
-    kegg_response_mock = mocker.MagicMock(kegg_url=mocker.MagicMock(url='url/mock'), status=status)
-    kegg_rest_mock = mocker.MagicMock(method=mocker.MagicMock(return_value=kegg_response_mock))
-
-    with pt.raises(RuntimeError) as error:
-        ei._process_response(method='method', kegg_rest=kegg_rest_mock)
-
-    u.assert_expected_error_message(expected_message=expected_message, error=error)
-
-
 test_from_kegg_rest_data = [
-    (ei.from_database, 'list', {'database_name': 'compound'}),
-    (ei.from_keywords, 'keywords_find', {'database_name': 'compound', 'keywords': ['kw1', 'kw2']}),
+    (ei.from_database, ku.ListKEGGurl, {'database_name': 'compound'}, 'list/compound'),
+    (ei.from_keywords, ku.KeywordsFindKEGGurl, {'database_name': 'compound', 'keywords': ['kw1', 'kw2']}, 'find/compound/kw1+kw2'),
     (
-        ei.from_molecular_attribute, 'molecular_find',
-        {'database_name': 'compound', 'formula': 'M4O3C2K1', 'exact_mass': None, 'molecular_weight': None}
+        ei.from_molecular_attribute, ku.MolecularFindKEGGurl,
+        {'database_name': 'compound', 'formula': 'M4O3C2K1', 'exact_mass': None, 'molecular_weight': None},
+        'find/compound/M4O3C2K1/formula'
     )
 ]
-@pt.mark.parametrize('get_entry_ids,rest_method,kwargs', test_from_kegg_rest_data)
-def test_from_kegg_rest(mocker, get_entry_ids: t.Callable, rest_method: str, kwargs: dict):
+@pt.mark.parametrize('get_entry_ids,KEGGurl,kwargs,url', test_from_kegg_rest_data)
+def test_from_kegg_rest(mocker, get_entry_ids: t.Callable, KEGGurl: type, kwargs: dict, url: str):
     text_body_mock = '''
     cpd:C22501	alpha-D-Xylulofuranose
     cpd:C22502	alpha-D-Fructofuranose; alpha-D-Fructose
@@ -50,16 +34,16 @@ def test_from_kegg_rest(mocker, get_entry_ids: t.Callable, rest_method: str, kwa
     cpd:C22514	2,3-Bis-O-(geranylfarnesyl)-sn-glycerol 1-phosphate
     '''
 
-    rest_method_mock = mocker.MagicMock(
-        return_value=mocker.MagicMock(text_body=text_body_mock, status=r.KEGGresponse.Status.SUCCESS)
+    get_mock: mocker.MagicMock = mocker.patch(
+        'kegg_pull.rest.rq.get',
+        return_value=mocker.MagicMock(text=text_body_mock, status_code=200)
     )
 
-    kegg_rest_mock = mocker.MagicMock()
-    kegg_rest_mock.__setattr__(rest_method, rest_method_mock)
-    KEGGrestMock = mocker.patch('kegg_pull.entry_ids.r.KEGGrest', return_value=kegg_rest_mock)
+    request_and_check_error_spy: mocker.MagicMock = mocker.spy(r, 'request_and_check_error')
     actual_entry_ids: list = get_entry_ids(**kwargs)
-    rest_method_mock.assert_called_once_with(**kwargs)
-    KEGGrestMock.assert_called_once_with()
+    request_and_check_error_spy.assert_called_once_with(kegg_rest=None, KEGGurl=KEGGurl, **kwargs)
+    url = f'{ku.BASE_URL}/{url}'
+    get_mock.assert_called_once_with(url=url, timeout=60)
 
     expected_entry_ids = [
         'cpd:C22501', 'cpd:C22502', 'cpd:C22500', 'cpd:C22504', 'cpd:C22506', 'cpd:C22507', 'cpd:C22509', 'cpd:C22510',
diff --git a/dev/test_rest.py b/dev/test_rest.py
index b5ed2bd..cf0d45f 100644
--- a/dev/test_rest.py
+++ b/dev/test_rest.py
@@ -183,3 +183,20 @@ def test_get_kegg_url_warning(mocker, caplog):
     )
 
     assert kegg_url == kegg_url_mock
+
+
+test_request_and_check_error_data = [
+    ('The KEGG request failed with the following URL: url/mock', r.KEGGresponse.Status.FAILED),
+    ('The KEGG request timed out with the following URL: url/mock', r.KEGGresponse.Status.TIMEOUT)
+]
+@pt.mark.parametrize('expected_message,status', test_request_and_check_error_data)
+def test_request_and_check_error(mocker, expected_message: str, status: r.KEGGresponse.Status):
+    kegg_url_mock = mocker.MagicMock(url='url/mock')
+    kegg_response_mock = mocker.MagicMock(kegg_url=kegg_url_mock, status=status)
+    request_mock: mocker.MagicMock = mocker.patch('kegg_pull.rest.KEGGrest.request', return_value=kegg_response_mock)
+
+    with pt.raises(RuntimeError) as error:
+        r.request_and_check_error(kegg_url=kegg_url_mock, kwarg1='val1', kwarg2='val2')
+
+    request_mock.assert_called_once_with(KEGGurl=None, kegg_url=kegg_url_mock, kwarg1='val1', kwarg2='val2')
+    u.assert_expected_error_message(expected_message=expected_message, error=error)