XspecT 0.2.5__py3-none-any.whl → 0.2.7__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.

Potentially problematic release.


This version of XspecT might be problematic. Click here for more details.

xspect/definitions.py CHANGED
@@ -40,3 +40,10 @@ def get_xspect_runs_path():
40
40
  runs_path = get_xspect_root_path() / "runs"
41
41
  runs_path.mkdir(exist_ok=True, parents=True)
42
42
  return runs_path
43
+
44
+
45
+ def get_xspect_mlst_path():
46
+ """Return the path to the XspecT runs directory."""
47
+ mlst_path = get_xspect_root_path() / "mlst"
48
+ mlst_path.mkdir(exist_ok=True, parents=True)
49
+ return mlst_path
@@ -7,8 +7,8 @@ import requests
7
7
  from xspect.definitions import get_xspect_model_path, get_xspect_tmp_path
8
8
 
9
9
 
10
- def download_test_filters(url):
11
- """Download filters."""
10
+ def download_test_models(url):
11
+ """Download models."""
12
12
 
13
13
  download_path = get_xspect_tmp_path() / "models.zip"
14
14
  extract_path = get_xspect_tmp_path() / "extracted_models"
xspect/fastapi.py CHANGED
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  from shutil import copyfileobj
6
6
  from fastapi import FastAPI, UploadFile, BackgroundTasks
7
7
  from xspect.definitions import get_xspect_runs_path, get_xspect_upload_path
8
- from xspect.download_filters import download_test_filters
8
+ from xspect.download_models import download_test_models
9
9
  import xspect.model_management as mm
10
10
  from xspect.models.result import StepType
11
11
  from xspect.pipeline import ModelExecution, Pipeline, PipelineStep
@@ -17,7 +17,7 @@ app = FastAPI()
17
17
  @app.get("/download-filters")
18
18
  def download_filters():
19
19
  """Download filters."""
20
- download_test_filters("https://xspect2.s3.eu-central-1.amazonaws.com/models.zip")
20
+ download_test_models("https://xspect2.s3.eu-central-1.amazonaws.com/models.zip")
21
21
 
22
22
 
23
23
  @app.get("/classify")
xspect/main.py CHANGED
@@ -6,13 +6,23 @@ import uuid
6
6
  import click
7
7
  import uvicorn
8
8
  from xspect import fastapi
9
- from xspect.download_filters import download_test_filters
9
+ from xspect.download_models import download_test_models
10
10
  from xspect.train import train_ncbi
11
11
  from xspect.models.result import (
12
12
  StepType,
13
13
  )
14
- from xspect.definitions import get_xspect_runs_path, fasta_endings, fastq_endings
14
+ from xspect.definitions import (
15
+ get_xspect_runs_path,
16
+ fasta_endings,
17
+ fastq_endings,
18
+ get_xspect_model_path,
19
+ )
15
20
  from xspect.pipeline import ModelExecution, Pipeline, PipelineStep
21
+ from xspect.mlst_feature.mlst_helper import pick_scheme, pick_scheme_from_models_dir
22
+ from xspect.mlst_feature.pub_mlst_handler import PubMLSTHandler
23
+ from xspect.models.probabilistic_filter_mlst_model import (
24
+ ProbabilisticFilterMlstSchemeModel,
25
+ )
16
26
 
17
27
 
18
28
  @click.group()
@@ -22,10 +32,10 @@ def cli():
22
32
 
23
33
 
24
34
  @cli.command()
25
- def download_filters():
26
- """Download filters."""
27
- click.echo("Downloading filters, this may take a while...")
28
- download_test_filters("https://xspect2.s3.eu-central-1.amazonaws.com/models.zip")
35
+ def download_models():
36
+ """Download models."""
37
+ click.echo("Downloading models, this may take a while...")
38
+ download_test_models("https://xspect2.s3.eu-central-1.amazonaws.com/models.zip")
29
39
 
30
40
 
31
41
  @cli.command()
@@ -43,7 +53,7 @@ def download_filters():
43
53
  help="Sparse sampling step size (e. g. only every 500th kmer for step=500).",
44
54
  default=1,
45
55
  )
46
- def classify(genus, path, meta, step):
56
+ def classify_species(genus, path, meta, step):
47
57
  """Classify sample(s) from file or directory PATH."""
48
58
  click.echo("Classifying...")
49
59
  click.echo(f"Step: {step}")
@@ -105,7 +115,7 @@ def classify(genus, path, meta, step):
105
115
  help="SVM Sparse sampling step size (e. g. only every 500th kmer for step=500).",
106
116
  default=1,
107
117
  )
108
- def train(genus, bf_assembly_path, svm_assembly_path, svm_step):
118
+ def train_species(genus, bf_assembly_path, svm_assembly_path, svm_step):
109
119
  """Train model."""
110
120
 
111
121
  if bf_assembly_path or svm_assembly_path:
@@ -118,6 +128,49 @@ def train(genus, bf_assembly_path, svm_assembly_path, svm_step):
118
128
  raise click.ClickException(str(e)) from e
119
129
 
120
130
 
131
+ @cli.command()
132
+ @click.option(
133
+ "-c",
134
+ "--choose_schemes",
135
+ is_flag=True,
136
+ help="Choose your own schemes."
137
+ "Default setting is Oxford and Pasteur scheme of A.baumannii.",
138
+ )
139
+ def train_mlst(choose_schemes):
140
+ """Download alleles and train bloom filters."""
141
+ click.echo("Updating alleles")
142
+ handler = PubMLSTHandler()
143
+ handler.download_alleles(choose_schemes)
144
+ click.echo("Download finished")
145
+ scheme_path = pick_scheme(handler.get_scheme_paths())
146
+ species_name = str(scheme_path).split("/")[-2]
147
+ scheme_name = str(scheme_path).split("/")[-1]
148
+ model = ProbabilisticFilterMlstSchemeModel(
149
+ 31, f"{species_name}:{scheme_name}", get_xspect_model_path()
150
+ )
151
+ click.echo("Creating mlst model")
152
+ model.fit(scheme_path)
153
+ model.save()
154
+ click.echo(f"Saved at {model.cobs_path}")
155
+
156
+
157
+ @cli.command()
158
+ @click.option(
159
+ "-p",
160
+ "--path",
161
+ help="Path to FASTA-file for mlst identification.",
162
+ type=click.Path(exists=True, dir_okay=True, file_okay=True),
163
+ )
164
+ def classify_mlst(path):
165
+ """MLST classify a sample."""
166
+ click.echo("Classifying...")
167
+ path = Path(path)
168
+ scheme_path = pick_scheme_from_models_dir()
169
+ model = ProbabilisticFilterMlstSchemeModel.load(scheme_path)
170
+ model.predict(scheme_path, path).save(model.model_display_name, path)
171
+ click.echo(f"Run saved at {get_xspect_runs_path()}.")
172
+
173
+
121
174
  @cli.command()
122
175
  def api():
123
176
  """Open the XspecT FastAPI."""
File without changes
@@ -0,0 +1,155 @@
1
+ """Module for utility functions used in other modules regarding MLST."""
2
+
3
+ __author__ = "Cetin, Oemer"
4
+
5
+ import requests
6
+ import json
7
+ from io import StringIO
8
+ from pathlib import Path
9
+ from Bio import SeqIO
10
+ from xspect.definitions import get_xspect_model_path, get_xspect_runs_path
11
+
12
+
13
+ def create_fasta_files(locus_path: Path, fasta_batch: str):
14
+ """Create Fasta-Files for every allele of a locus."""
15
+ # fasta_batch = full string of a fasta file containing every allele sequence of a locus
16
+ for record in SeqIO.parse(StringIO(fasta_batch), "fasta"):
17
+ number = record.id.split("_")[-1] # example id = Oxf_cpn60_263
18
+ output_fasta_file = locus_path / f"Allele_ID_{number}.fasta"
19
+ if output_fasta_file.exists():
20
+ continue # Ignore existing ones
21
+ with open(output_fasta_file, "w") as allele:
22
+ SeqIO.write(record, allele, "fasta")
23
+
24
+
25
+ def pick_species_number_from_db(available_species: dict) -> str:
26
+ """Returns the chosen species from all available ones in the database."""
27
+ # The "database" string can look like this: pubmlst_abaumannii_seqdef
28
+ for counter, database in available_species.items():
29
+ print(str(counter) + ":" + database.split("_")[1])
30
+ print("\nPick one of the above databases")
31
+ while True:
32
+ try:
33
+ choice = input("Choose a species by selecting the corresponding number:")
34
+ if int(choice) in available_species.keys():
35
+ chosen_species = available_species.get(int(choice))
36
+ return chosen_species
37
+ else:
38
+ print(
39
+ "Wrong input! Try again with a number that is available in the list above."
40
+ )
41
+ except ValueError:
42
+ print(
43
+ "Wrong input! Try again with a number that is available in the list above."
44
+ )
45
+
46
+
47
+ def pick_scheme_number_from_db(available_schemes: dict) -> str:
48
+ """Returns the chosen schemes from all available ones of a species."""
49
+ # List all available schemes of a species database
50
+ for counter, scheme in available_schemes.items():
51
+ print(str(counter) + ":" + scheme[0])
52
+ print("\nPick any available scheme that is listed for download")
53
+ while True:
54
+ try:
55
+ choice = input("Choose a scheme by selecting the corresponding number:")
56
+ if int(choice) in available_schemes.keys():
57
+ chosen_scheme = available_schemes.get(int(choice))[1]
58
+ return chosen_scheme
59
+ else:
60
+ print(
61
+ "Wrong input! Try again with a number that is available in the above list."
62
+ )
63
+ except ValueError:
64
+ print(
65
+ "Wrong input! Try again with a number that is available in the above list."
66
+ )
67
+
68
+
69
+ def scheme_list_to_dict(scheme_list: list[str]):
70
+ """Converts the scheme list attribute into a dictionary with a number as the key."""
71
+ return dict(zip(range(1, len(scheme_list) + 1), scheme_list))
72
+
73
+
74
+ def pick_scheme_from_models_dir() -> Path:
75
+ """Returns the chosen scheme from models that have been fitted prior."""
76
+ schemes = {}
77
+ counter = 1
78
+ for entry in sorted((get_xspect_model_path() / "MLST").iterdir()):
79
+ schemes[counter] = entry
80
+ counter += 1
81
+ return pick_scheme(schemes)
82
+
83
+
84
+ def pick_scheme(available_schemes: dict) -> Path:
85
+ """Returns the chosen scheme from the scheme list."""
86
+ if not available_schemes:
87
+ raise ValueError("No scheme has been chosen for download yet!")
88
+
89
+ if len(available_schemes.items()) == 1:
90
+ return next(iter(available_schemes.values()))
91
+
92
+ # List available schemes
93
+ for counter, scheme in available_schemes.items():
94
+ # For Strain Typing with an API-POST Request to the db
95
+ if str(scheme).startswith("http"):
96
+ scheme_json = requests.get(scheme).json()
97
+ print(str(counter) + ":" + scheme_json["description"])
98
+
99
+ # To pick a scheme after download for fitting
100
+ else:
101
+ print(str(counter) + ":" + str(scheme).split("/")[-1])
102
+
103
+ print("\nPick a scheme for strain type prediction")
104
+ while True:
105
+ try:
106
+ choice = input("Choose a scheme by selecting the corresponding number:")
107
+ if int(choice) in available_schemes.keys():
108
+ chosen_scheme = available_schemes.get(int(choice))
109
+ return chosen_scheme
110
+ else:
111
+ print(
112
+ "Wrong input! Try again with a number that is available in the above list."
113
+ )
114
+ except ValueError:
115
+ print(
116
+ "Wrong input! Try again with a number that is available in the above list."
117
+ )
118
+
119
+
120
+ class MlstResult:
121
+ """Class for storing mlst results."""
122
+
123
+ def __init__(
124
+ self,
125
+ scheme_model: str,
126
+ steps: int,
127
+ hits: dict[str, list[dict]],
128
+ ):
129
+ self.scheme_model = scheme_model
130
+ self.steps = steps
131
+ self.hits = hits
132
+
133
+ def get_results(self) -> dict:
134
+ """Stores the result of a prediction in a dictionary."""
135
+ results = {seq_id: result for seq_id, result in self.hits.items()}
136
+ return results
137
+
138
+ def to_dict(self) -> dict:
139
+ """Converts all attributes into one dictionary."""
140
+ result = {
141
+ "Scheme": self.scheme_model,
142
+ "Steps": self.steps,
143
+ "Results": self.get_results(),
144
+ }
145
+ return result
146
+
147
+ def save(self, display: str, file_path: Path) -> None:
148
+ """Saves the result inside the "runs" directory"""
149
+ file_name = str(file_path).split("/")[-1]
150
+ json_path = get_xspect_runs_path() / "MLST" / f"{file_name}-{display}.json"
151
+ json_path.parent.mkdir(exist_ok=True, parents=True)
152
+ json_object = json.dumps(self.to_dict(), indent=4)
153
+
154
+ with open(json_path, "w", encoding="utf-8") as file:
155
+ file.write(json_object)
@@ -0,0 +1,119 @@
1
+ """Module for connecting with the PubMLST database via API requests and downloading allele files."""
2
+
3
+ __author__ = "Cetin, Oemer"
4
+
5
+ import requests
6
+ import json
7
+ from xspect.mlst_feature.mlst_helper import (
8
+ create_fasta_files,
9
+ pick_species_number_from_db,
10
+ pick_scheme_number_from_db,
11
+ pick_scheme,
12
+ scheme_list_to_dict,
13
+ )
14
+ from xspect.definitions import get_xspect_mlst_path, get_xspect_upload_path
15
+
16
+
17
+ class PubMLSTHandler:
18
+ """Class for communicating with PubMLST and downloading alleles (FASTA-Format) from all loci."""
19
+
20
+ base_url = "http://rest.pubmlst.org/db"
21
+
22
+ def __init__(self):
23
+ # Default values: Oxford (1) and Pasteur (2) schemes of A.baumannii species
24
+ self.scheme_list = [
25
+ self.base_url + "/pubmlst_abaumannii_seqdef/schemes/1",
26
+ self.base_url + "/pubmlst_abaumannii_seqdef/schemes/2",
27
+ ]
28
+ self.scheme_paths = []
29
+
30
+ def get_scheme_paths(self) -> dict:
31
+ """Returns the scheme paths in a dictionary"""
32
+ return scheme_list_to_dict(self.scheme_paths)
33
+
34
+ def choose_schemes(self) -> None:
35
+ """Changes the scheme list attribute to feature other schemes from some species"""
36
+ available_species = {}
37
+ available_schemes = {}
38
+ chosen_schemes = []
39
+ counter = 1
40
+ # retrieve all available species
41
+ species_url = PubMLSTHandler.base_url
42
+ for species_databases in requests.get(species_url).json():
43
+ for database in species_databases["databases"]:
44
+ if database["name"].endswith("seqdef"):
45
+ available_species[counter] = database["name"]
46
+ counter += 1
47
+ # pick a species out of the available ones
48
+ chosen_species = pick_species_number_from_db(available_species)
49
+
50
+ counter = 1
51
+ scheme_url = f"{species_url}/{chosen_species}/schemes"
52
+ for scheme in requests.get(scheme_url).json()["schemes"]:
53
+ # scheme["description"] stores the name of a scheme.
54
+ # scheme["scheme"] stores the URL that is needed for downloading all loci.
55
+ available_schemes[counter] = [scheme["description"], scheme["scheme"]]
56
+ counter += 1
57
+
58
+ # Selection process of available scheme from a species for download (doubles are caught!)
59
+ while True:
60
+ chosen_scheme = pick_scheme_number_from_db(available_schemes)
61
+ (
62
+ chosen_schemes.append(chosen_scheme)
63
+ if chosen_scheme not in chosen_schemes
64
+ else None
65
+ )
66
+ choice = input(
67
+ "Do you want to pick another scheme to download? (y/n):"
68
+ ).lower()
69
+ if choice != "y":
70
+ break
71
+ self.scheme_list = chosen_schemes
72
+
73
+ def download_alleles(self, choice: False):
74
+ """Downloads every allele FASTA-file from all loci of the scheme list attribute"""
75
+ if choice: # pick an own scheme if not Oxford or Pasteur
76
+ self.choose_schemes() # changes the scheme_list attribute
77
+
78
+ for scheme in self.scheme_list:
79
+ scheme_json = requests.get(scheme).json()
80
+ # We only want the name and the respective featured loci of a scheme
81
+ scheme_name = scheme_json["description"]
82
+ locus_list = scheme_json["loci"]
83
+
84
+ species_name = scheme.split("_")[1] # name = pubmlst_abaumannii_seqdef
85
+ scheme_path = get_xspect_mlst_path() / species_name / scheme_name
86
+ self.scheme_paths.append(scheme_path)
87
+
88
+ for locus_url in locus_list:
89
+ # After using split the last part ([-1]) of the url is the locus name
90
+ locus_name = locus_url.split("/")[-1]
91
+ locus_path = (
92
+ get_xspect_mlst_path() / species_name / scheme_name / locus_name
93
+ )
94
+
95
+ if not locus_path.exists():
96
+ locus_path.mkdir(exist_ok=True, parents=True)
97
+
98
+ alleles = requests.get(f"{locus_url}/alleles_fasta").text
99
+ create_fasta_files(locus_path, alleles)
100
+
101
+ def assign_strain_type_by_db(self):
102
+ """Sends an API-POST-Request to the database for MLST without bloom filters"""
103
+ scheme_url = (
104
+ str(pick_scheme(scheme_list_to_dict(self.scheme_list))) + "/sequence"
105
+ )
106
+ fasta_file = get_xspect_upload_path() / "Test.fna"
107
+ with open(fasta_file, "r") as file:
108
+ data = file.read()
109
+ payload = { # Essential API-POST-Body
110
+ "sequence": data,
111
+ "filetype": "fasta",
112
+ }
113
+ response = requests.post(scheme_url, data=json.dumps(payload)).json()
114
+
115
+ for locus, meta_data in response["exact_matches"].items():
116
+ # meta_data is a list containing a dictionary, therefore [0] and then key value.
117
+ # Example: 'Pas_fusA': [{'href': some URL, 'allele_id': '2'}]
118
+ print(locus + ":" + meta_data[0]["allele_id"], end="; ")
119
+ print("\nStrain Type:", response["fields"])
@@ -30,12 +30,11 @@ def get_model_by_slug(model_slug: str):
30
30
  model_metadata = get_model_metadata(model_path)
31
31
  if model_metadata["model_class"] == "ProbabilisticSingleFilterModel":
32
32
  return ProbabilisticSingleFilterModel.load(model_path)
33
- elif model_metadata["model_class"] == "ProbabilisticFilterSVMModel":
33
+ if model_metadata["model_class"] == "ProbabilisticFilterSVMModel":
34
34
  return ProbabilisticFilterSVMModel.load(model_path)
35
- elif model_metadata["model_class"] == "ProbabilisticFilterModel":
35
+ if model_metadata["model_class"] == "ProbabilisticFilterModel":
36
36
  return ProbabilisticFilterModel.load(model_path)
37
- else:
38
- raise ValueError(f"Model class {model_metadata['model_class']} not recognized.")
37
+ raise ValueError(f"Model class {model_metadata['model_class']} not recognized.")
39
38
 
40
39
 
41
40
  def get_model_metadata(model: str | Path):