diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 02496f9..93eb72e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -27,7 +27,7 @@ jobs: run: uv sync --all-extras - name: Run tests with coverage - run: uv run coverage run -m pytest tests/ -v + run: uv run coverage run -m pytest src/tests/ -v - name: Generate coverage report run: uv run coverage report -m @@ -36,8 +36,6 @@ jobs: run: uv run coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: - files: ./coverage.xml - fail_ci_if_error: false - verbose: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f697c33..6837e25 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,4 +27,4 @@ jobs: run: uv sync --all-extras - name: Run tests - run: uv run pytest tests/ -v + run: uv run pytest src/tests/ -v diff --git a/pyproject.toml b/pyproject.toml index 70ca703..e5e31aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,9 +26,20 @@ dev = [ "ipykernel>=6.29.5", ] +[project.scripts] +prepare-data = "genome_plotter.prepare_data:main" +plot-chromosome = "genome_plotter.plot_chromosome:main" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["functions", "input_parsers"] +packages = ["src/genome_plotter"] + +[tool.hatch.build.targets.sdist] +include = ["src/genome_plotter"] + +[tool.pytest.ini_options] +testpaths = ["src/tests"] +pythonpath = ["src"] diff --git a/src/genome_plotter/__init__.py b/src/genome_plotter/__init__.py new file mode 100644 index 0000000..66e028c --- /dev/null +++ b/src/genome_plotter/__init__.py @@ -0,0 +1,5 @@ +"""GenomePlotter - A tool for creating accurate visualizations of the human genome.""" + +from __future__ import annotations + +__version__ = "0.1.0" diff --git a/config.json b/src/genome_plotter/config.json similarity index 100% rename from config.json rename to src/genome_plotter/config.json diff --git a/functions/ChromosomePlotter.py b/src/genome_plotter/functions/ChromosomePlotter.py similarity index 100% rename from functions/ChromosomePlotter.py rename to src/genome_plotter/functions/ChromosomePlotter.py diff --git a/functions/ColorFunctions.py b/src/genome_plotter/functions/ColorFunctions.py similarity index 100% rename from functions/ColorFunctions.py rename to src/genome_plotter/functions/ColorFunctions.py diff --git a/functions/ConfigManager.py b/src/genome_plotter/functions/ConfigManager.py similarity index 100% rename from functions/ConfigManager.py rename to src/genome_plotter/functions/ConfigManager.py diff --git a/functions/CustomGenePlotter.py b/src/genome_plotter/functions/CustomGenePlotter.py similarity index 98% rename from functions/CustomGenePlotter.py rename to src/genome_plotter/functions/CustomGenePlotter.py index 07eb25e..a0eba44 100644 --- a/functions/CustomGenePlotter.py +++ b/src/genome_plotter/functions/CustomGenePlotter.py @@ -7,11 +7,11 @@ import pandas as pd -from input_parsers.data_integrator import DataIntegrator +from genome_plotter.input_parsers.data_integrator import DataIntegrator if TYPE_CHECKING: - from functions.ColorFunctions import ColorPicker - from functions.ConfigManager import Config + from genome_plotter.functions.ColorFunctions import ColorPicker + from genome_plotter.functions.ConfigManager import Config class CustomGeneIntegrator: diff --git a/functions/CytobandAnnotator.py b/src/genome_plotter/functions/CytobandAnnotator.py similarity index 100% rename from functions/CytobandAnnotator.py rename to src/genome_plotter/functions/CytobandAnnotator.py diff --git a/functions/DataIntegrator.py b/src/genome_plotter/functions/DataIntegrator.py similarity index 100% rename from functions/DataIntegrator.py rename to src/genome_plotter/functions/DataIntegrator.py diff --git a/functions/DrawLegend.py b/src/genome_plotter/functions/DrawLegend.py similarity index 94% rename from functions/DrawLegend.py rename to src/genome_plotter/functions/DrawLegend.py index 9ae78cf..ab3ddad 100644 --- a/functions/DrawLegend.py +++ b/src/genome_plotter/functions/DrawLegend.py @@ -7,11 +7,11 @@ from dataclasses import asdict from typing import TYPE_CHECKING -from ColorFunctions import linear_gradient -from svg_handler import svg_handler +from genome_plotter.functions.ColorFunctions import linear_gradient +from genome_plotter.functions.svg_handler import svg_handler if TYPE_CHECKING: - from functions.ConfigManager import Config + from genome_plotter.functions.ConfigManager import Config class DrawLegend: diff --git a/functions/FetchFromFtp.py b/src/genome_plotter/functions/FetchFromFtp.py similarity index 100% rename from functions/FetchFromFtp.py rename to src/genome_plotter/functions/FetchFromFtp.py diff --git a/functions/GeneAnnotator.py b/src/genome_plotter/functions/GeneAnnotator.py similarity index 100% rename from functions/GeneAnnotator.py rename to src/genome_plotter/functions/GeneAnnotator.py diff --git a/functions/GwasAnnotator.py b/src/genome_plotter/functions/GwasAnnotator.py similarity index 100% rename from functions/GwasAnnotator.py rename to src/genome_plotter/functions/GwasAnnotator.py diff --git a/functions/__init__.py b/src/genome_plotter/functions/__init__.py similarity index 100% rename from functions/__init__.py rename to src/genome_plotter/functions/__init__.py diff --git a/functions/chrom_GWAS_plotter.py b/src/genome_plotter/functions/chrom_GWAS_plotter.py similarity index 100% rename from functions/chrom_GWAS_plotter.py rename to src/genome_plotter/functions/chrom_GWAS_plotter.py diff --git a/functions/svg_handler.py b/src/genome_plotter/functions/svg_handler.py similarity index 98% rename from functions/svg_handler.py rename to src/genome_plotter/functions/svg_handler.py index 5dd8788..e0611ab 100644 --- a/functions/svg_handler.py +++ b/src/genome_plotter/functions/svg_handler.py @@ -188,7 +188,7 @@ def add_text( x: float, y: float, text: str, - size: int = 10, + size: int | float = 10, fill: str = "#000000", anchor: str = "start", ) -> None: @@ -198,7 +198,7 @@ def add_text( x (float): X coordinate. y (float): Y coordinate. text (str): Text content. - size (int): Font size. + size (int | float): Font size. fill (str): Text color. anchor (str): Text anchor position. """ diff --git a/input_parsers/__init__.py b/src/genome_plotter/input_parsers/__init__.py similarity index 100% rename from input_parsers/__init__.py rename to src/genome_plotter/input_parsers/__init__.py diff --git a/src/genome_plotter/input_parsers/data_integrator.py b/src/genome_plotter/input_parsers/data_integrator.py new file mode 100644 index 0000000..9cf92d0 --- /dev/null +++ b/src/genome_plotter/input_parsers/data_integrator.py @@ -0,0 +1,288 @@ +"""A set of functions to integrate parsed input data.""" + +from __future__ import annotations + +import logging +import pickle +from typing import TYPE_CHECKING, Any + +import pandas as pd +import pybedtools + +if TYPE_CHECKING: + from genome_plotter.functions.ColorFunctions import ColorPicker + + +logger = logging.getLogger(__name__) + + +def read_data(file: str, types: dict[str, Any]) -> pd.DataFrame: + """Read the input data. + + Args: + file (str): The file to read. + types (typedict): The types of the columns. + + Returns: + pd.DataFrame: The data. + """ + return pd.read_csv(file, compression="gzip", sep="\t", header=0, dtype=types) + + +def integrate_data( + output_dir: str, + chromosomes: list[str], + cytoband_file: str, + gencode_file: str, + dummy: bool = False, +) -> None: + """Integrate the parsed input data. + + Args: + output_dir (str): The directory to save the data to. + chromosomes (list[str]): The chromosomes. + cytoband_file (str): The cytobands file. + gencode_file (str): The gencode file. + dummy (bool, optional): Whether to use dummy data. Defaults to False. + """ + logger.info("Integrating parsed data.") + # Read cytobands: + gencode_df = read_data( + gencode_file, {"chr": str, "start": int, "end": int, "type": str} + ) + cyb_df = read_data( + cytoband_file, + {"chr": str, "start": int, "end": int, "name": str, "type": str}, + ) + logger.info(f"Number of GENCODE annotations in the genome: {len(gencode_df):,}") + logger.info(f"Number of cytological bands in the genome: {len(cyb_df):,}") + + # Iterate over chromosomes: + for chromosome in chromosomes: + logger.info(f"Integrating data for chromosome: {chromosome}") + # Reading chromosome data: + chr_df = read_data( + f"{output_dir}/processed_chr{chromosome}.bed.gz", + {"chr": str, "start": int, "end": int, "GC_ratio": float}, + ) + + # Initialize data integrator: + integrator = DataIntegrator(chr_df) + + # Downstream processing depends on dummy status: + if dummy: + # Adding cytological band information to the data: + integrator.add_centromere(cyb_df) + + # Adding dummy GENCODE annotation to genomic data: + integrator.add_dummy() + + else: + # Adding GENCODE annotation to genomic data: + integrator.add_genes(gencode_df) + + # Adding cytological band information to the data: + integrator.add_centromere(cyb_df) + + # Assigning heterocromatic regions: + integrator.assign_hetero() + + # Integration is complete: + integrator.save_table(f"{output_dir}/integrated_chr{chromosome}.bed.gz") + logger.info(f"Integration for chromosome {chromosome} is complete.") + + logger.info("Integration complete.") + + +class DataIntegrator: + """Assign color for each chunk based on genomic features. + + This class assigns color for each chunk in the chromosome + based on GC content + GENCODE annotation + darkened fraction. + """ + + __required_columns = ["chr", "start", "end"] + + def __init__(self: DataIntegrator, genome_df: pd.DataFrame) -> None: + """Initialize the data integrator.""" + self.__genome__ = genome_df.copy() + self.chromosome_name = genome_df.iloc[0]["chr"] + + logger.info(f"Integrating data on chromosome: {self.chromosome_name}") + logger.info( + f"Number of chunks on chromosome {self.chromosome_name}: {len(self.__genome__):,}" + ) + + # Testing columns: + for col in self.__required_columns: + if col not in genome_df.columns: + raise ValueError( + f"Manadatory colum: {col} is not found in the provided dataframe." + ) + + def add_xy_coordinates(self: DataIntegrator, width: int) -> None: + """Calculate chunk coordinates. + + Args: + width (int): Number of chunks in one row. + """ + # If the data is already there, we might re-compute: + if "x" in self.__genome__.columns: + self.__genome__.drop(columns=["x", "y"], inplace=True) + + # By default, all chunks are written into the same row: + if not width: + width = len(self.__genome__) + self.__genome__.reset_index(drop=True, inplace=True) + + self.__width__ = width + + self.__genome__ = ( + self.__genome__.assign( + # Get x position of the chunk: + x=self.__genome__.index.astype(int) % width, + # Set y position of the chunk: + y=self.__genome__.index.astype(int) / width, + ) + # Set proper type: + .astype({"y": "int32"}) + ) + logger.info(f"Number of chunks in one row: {width:,}") + logger.info(f"Number of rows: {self.__genome__.y.max():,}") + + def add_genes(self: DataIntegrator, gencode_df: pd.DataFrame) -> None: + """Add GENCODE annotation for each chunk in the genome. + + Args: + gencode_df (pd.DataFrame): GENCODE annotation data. + """ + # If the dataset has already been annotated, we return: + if "GENCODE" in self.__genome__.columns: + return None + + # Creating bedtools objects: + gencode_bed = pybedtools.bedtool.BedTool.from_dataframe( + ( + gencode_df.loc[gencode_df.chr == self.chromosome_name].rename( + columns={"chr": "chrom"} + ) + ) + ) + chrom_bed = pybedtools.bedtool.BedTool.from_dataframe( + self.__genome__.rename(columns={"chr": "chrom"}) + ) + + # Run intersectbed and extract result as dataframe: + try: + gencode_intersect = chrom_bed.intersect(gencode_bed, wa=True, wb=True) + intersect_df = gencode_intersect.to_dataframe( + header=None, + names=[ + "chr", + "start", + "end", + "GC_ratio", + "x", + "y", + "chr_2", + "start_2", + "end_2", + "gene_id", + "gene_name", + "transcript_id", + "type", + ], + ) + # At this point we should have a dataframe with all the data: + except Exception as e: + logger.error("Gencode data:") + logger.error(gencode_bed.head()) + logger.error("Chromosome data:") + logger.error(chrom_bed.head()) + + raise e + + # Parse out results: + gencode_chunks = intersect_df.groupby("start").apply( + lambda x: "exon" if "exon" in x.type.unique() else "gene" + ) + gencode_chunks.name = "GENCODE" + + # Updating index: + genome_df = self.__genome__.merge( + gencode_chunks, left_on="start", right_index=True, how="left" + ).fillna({"GENCODE": "intergenic"}) + + # Adding annotation to df: + self.__genome__ = genome_df + + def get_data(self: DataIntegrator) -> pd.DataFrame: + """Get a copy of the integrated data. + + Returns: + pd.DataFrame: The integrated data. + """ + return self.__genome__.copy() + + def add_centromere(self: DataIntegrator, cytoband_df: pd.DataFrame) -> None: + """Add centromere annotation to the genome. + + Args: + cytoband_df (pd.DataFrame): Cytoband data. + """ + chromosome = self.__genome__.chr[1] + centromer_loc = cytoband_df.loc[ + (cytoband_df.chr == str(chromosome)) & (cytoband_df.type == "acen"), + ["start", "end"], + ] + centromer_loc = (int(centromer_loc.start.min()), int(centromer_loc.end.max())) + + # If GENCODE column is missing, let's initialize: + if "GENCODE" not in self.__genome__.columns: + self.__genome__["GENCODE"] = None + + # Assigning centromere: + self.__genome__.loc[ + (self.__genome__.end > centromer_loc[0]) + & (self.__genome__.start < centromer_loc[1]), + "GENCODE", + ] = "centromere" + + def assign_hetero(self: DataIntegrator) -> None: + """Assign heterochromatic regions.""" + self.__genome__.loc[self.__genome__.GC_ratio.isnull(), "GENCODE"] = ( + "heterochromatin" + ) + + def add_colors(self: DataIntegrator, color_picker: ColorPicker) -> None: + """Assigned color to each chunk in the genome. + + Args: + color_picker (ColorPicker): The color picker. + """ + self.__genome__["color"] = self.__genome__.apply( + color_picker.pick_color, axis=1 + ) + + def save_pkl(self: DataIntegrator, file_name: str) -> None: + """Save the integrated data as a pickle file. + + Args: + file_name (str): The file name. + """ + pickle.dump(self.__genome__, open(file_name, "wb")) + + def save_table(self: DataIntegrator, file_name: str) -> None: + """Save the integrated data as a table. + + Args: + file_name (str): The file name. + """ + self.__genome__.to_csv(file_name, sep="\t", index=False, compression="infer") + + def add_dummy(self: DataIntegrator) -> None: + """Assume GENCODE annotation is dummy for all chunks.""" + if "GENCODE" not in self.__genome__.columns: + self.__genome__ = self.__genome__.assign("GENCODE", "dummy") + else: + self.__genome__["GENCODE"] = self.__genome__.GENCODE.fillna("dummy") diff --git a/input_parsers/fetch_cytobands.py b/src/genome_plotter/input_parsers/fetch_cytobands.py similarity index 100% rename from input_parsers/fetch_cytobands.py rename to src/genome_plotter/input_parsers/fetch_cytobands.py diff --git a/input_parsers/fetch_ensembl.py b/src/genome_plotter/input_parsers/fetch_ensembl.py similarity index 97% rename from input_parsers/fetch_ensembl.py rename to src/genome_plotter/input_parsers/fetch_ensembl.py index 41a56d3..d8364a9 100644 --- a/input_parsers/fetch_ensembl.py +++ b/src/genome_plotter/input_parsers/fetch_ensembl.py @@ -9,10 +9,10 @@ import pandas as pd import requests -from functions.FetchFromFtp import FetchFromFtp +from genome_plotter.functions.FetchFromFtp import FetchFromFtp if TYPE_CHECKING: - from functions.ConfigManager import SourcePrototype + from genome_plotter.functions.ConfigManager import SourcePrototype logger = logging.getLogger(__name__) diff --git a/input_parsers/fetch_gencode.py b/src/genome_plotter/input_parsers/fetch_gencode.py similarity index 99% rename from input_parsers/fetch_gencode.py rename to src/genome_plotter/input_parsers/fetch_gencode.py index 2f24b8e..4a09932 100644 --- a/input_parsers/fetch_gencode.py +++ b/src/genome_plotter/input_parsers/fetch_gencode.py @@ -8,10 +8,10 @@ import pandas as pd -from functions.FetchFromFtp import FetchFromFtp +from genome_plotter.functions.FetchFromFtp import FetchFromFtp if TYPE_CHECKING: - from functions.ConfigManager import SourcePrototype + from genome_plotter.functions.ConfigManager import SourcePrototype logger = logging.getLogger(__name__) diff --git a/input_parsers/fetch_gwas_catalog.py b/src/genome_plotter/input_parsers/fetch_gwas_catalog.py similarity index 96% rename from input_parsers/fetch_gwas_catalog.py rename to src/genome_plotter/input_parsers/fetch_gwas_catalog.py index 1b45cb9..849712a 100644 --- a/input_parsers/fetch_gwas_catalog.py +++ b/src/genome_plotter/input_parsers/fetch_gwas_catalog.py @@ -5,10 +5,10 @@ import logging from typing import TYPE_CHECKING -from functions.FetchFromFtp import FetchFromFtp +from genome_plotter.functions.FetchFromFtp import FetchFromFtp if TYPE_CHECKING: - from functions.ConfigManager import SourcePrototype + from genome_plotter.functions.ConfigManager import SourcePrototype logger = logging.getLogger(__name__) diff --git a/logger_config.yaml b/src/genome_plotter/logger_config.yaml similarity index 100% rename from logger_config.yaml rename to src/genome_plotter/logger_config.yaml diff --git a/plot_chromosome.py b/src/genome_plotter/plot_chromosome.py similarity index 93% rename from plot_chromosome.py rename to src/genome_plotter/plot_chromosome.py index 7e59f94..7c2c286 100644 --- a/plot_chromosome.py +++ b/src/genome_plotter/plot_chromosome.py @@ -11,16 +11,21 @@ import pandas as pd import yaml -from functions.ChromosomePlotter import ChromosomePlotter +from genome_plotter.functions.ChromosomePlotter import ChromosomePlotter # Importing custom functions: -from functions.ColorFunctions import ColorPicker -from functions.ConfigManager import Config -from functions.CytobandAnnotator import CytobandAnnotator, get_centromere_position -from functions.GeneAnnotator import GeneAnnotator -from functions.GwasAnnotator import gwas_annotator -from functions.svg_handler import svg_handler -from input_parsers.data_integrator import DataIntegrator +from genome_plotter.functions.ColorFunctions import ColorPicker +from genome_plotter.functions.ConfigManager import Config +from genome_plotter.functions.CytobandAnnotator import ( + CytobandAnnotator, + get_centromere_position, +) +from genome_plotter.functions.GeneAnnotator import GeneAnnotator +from genome_plotter.functions.GwasAnnotator import gwas_annotator +from genome_plotter.functions.svg_handler import svg_handler +from genome_plotter.input_parsers.data_integrator import DataIntegrator + +logger = logging.getLogger(__name__) def genes_annotation_wrapper( @@ -297,7 +302,8 @@ def parse_arguments() -> argparse.Namespace: return parser.parse_args() -if __name__ == "__main__": +def main() -> None: + """Entry point for the plot-chromosome CLI command.""" # Extracting submitted options: args = parse_arguments() @@ -311,21 +317,13 @@ def parse_arguments() -> argparse.Namespace: plot_folder = os.path.abspath(args.folder) # Initialise logger: - with open("logger_config.yaml", "r") as stream: + logger_config_path = os.path.join(os.path.dirname(__file__), "logger_config.yaml") + with open(logger_config_path, "r") as stream: logger_config = yaml.safe_load(stream) logging.config.dictConfig(logger_config) logger = logging.getLogger(__name__) - # Loading config: - with open(args.config) as f: - try: - configuration = Config(**json.load(f)) - except json.decoder.JSONDecodeError: - raise ValueError( - f"The provided config file ({args.config}) is not a valid JSON file." - ) - # Reporting parameters: logger.info(f"Generating plot for chromosome: {chromosome}") logger.info("Processing parameters.") @@ -399,11 +397,6 @@ def parse_arguments() -> argparse.Namespace: chromosomeSvgObject.mergeSvg(cyb_svg) if args.geneFile: - # Get centromere position: - centromerePos = get_centromere_position( - config_manager.get_cytoband_file(), chromosome - ) - # Get plot dimension: plot_height = chromosomeSvgObject.getHeight() @@ -441,3 +434,7 @@ def parse_arguments() -> argparse.Namespace: chromosomeSvgObject.saveSvg(output_filename.replace("png", "svg")) logger.info("All done.") + + +if __name__ == "__main__": + main() diff --git a/Prepare_data.py b/src/genome_plotter/prepare_data.py similarity index 91% rename from Prepare_data.py rename to src/genome_plotter/prepare_data.py index d0b1ca2..3f89734 100644 --- a/Prepare_data.py +++ b/src/genome_plotter/prepare_data.py @@ -9,9 +9,11 @@ import yaml -from functions.ConfigManager import Config -from input_parsers.fetch_cytobands import FetchCytobands -from input_parsers.fetch_gencode import FetchGencode +from genome_plotter.functions.ConfigManager import Config +from genome_plotter.input_parsers.fetch_cytobands import FetchCytobands +from genome_plotter.input_parsers.fetch_gencode import FetchGencode + +logger = logging.getLogger(__name__) def parse_args() -> argparse.Namespace: @@ -70,8 +72,8 @@ def get_cytoband_data(cytoband_url: str, cytoband_output_file: str) -> str: return cytoband_retrieve.get_assembly_build() -def main(configuration: Config) -> None: - """Main function to fetch and prepare the input data for the genome plotter project. +def run(configuration: Config) -> None: + """Run the data preparation pipeline. Args: configuration (Config): The configuration object containing the input data. @@ -165,12 +167,14 @@ def validate_input(data_dir: str, config_file: str) -> None: raise ValueError(f"The provided config file ({config_file}) does not exist.") -if __name__ == "__main__": +def main() -> None: + """Entry point for the prepare-data CLI command.""" # Parse command line arguments args = parse_args() # Initialise logger: - with open("logger_config.yaml", "r") as stream: + logger_config_path = os.path.join(os.path.dirname(__file__), "logger_config.yaml") + with open(logger_config_path, "r") as stream: logger_config = yaml.safe_load(stream) logging.config.dictConfig(logger_config) @@ -199,4 +203,8 @@ def validate_input(data_dir: str, config_file: str) -> None: missing_tolerance=args.tolerance, ) - main(configuration) + run(configuration) + + +if __name__ == "__main__": + main() diff --git a/src/tests/__init__.py b/src/tests/__init__.py new file mode 100644 index 0000000..2d95d19 --- /dev/null +++ b/src/tests/__init__.py @@ -0,0 +1,3 @@ +"""Tests for the GenomePlotter package.""" + +from __future__ import annotations diff --git a/tests/test_color_fun.py b/src/tests/test_color_fun.py similarity index 99% rename from tests/test_color_fun.py rename to src/tests/test_color_fun.py index 33826db..2e051a6 100644 --- a/tests/test_color_fun.py +++ b/src/tests/test_color_fun.py @@ -7,7 +7,7 @@ import pandas as pd -from functions.ColorFunctions import ( +from genome_plotter.functions.ColorFunctions import ( ColorPicker, color_darkener, hex_to_rgb, diff --git a/tests/test_config_manager.py b/src/tests/test_config_manager.py similarity index 94% rename from tests/test_config_manager.py rename to src/tests/test_config_manager.py index 37012c8..58fcc86 100644 --- a/tests/test_config_manager.py +++ b/src/tests/test_config_manager.py @@ -3,16 +3,19 @@ from __future__ import annotations import json +import os import re import unittest -from functions.ConfigManager import Config +from genome_plotter.functions.ConfigManager import Config class TestConfigManager(unittest.TestCase): """Test cases for Config manager.""" - CONFIG_JSON = "config.json" + CONFIG_JSON = os.path.join( + os.path.dirname(__file__), "..", "genome_plotter", "config.json" + ) with open(CONFIG_JSON) as f: config_obj = Config(**json.load(f))