From 14034e0d32ffcd2b0ad802eb16204789b308dd6c Mon Sep 17 00:00:00 2001
From: Manuel Lera-Ramirez
Date: Thu, 11 Jun 2026 09:44:04 +0100
Subject: [PATCH 01/21] version working for deletion
---
.../batch_cloning/pombe/__init__.py | 78 ++++++-----
.../batch_cloning/pombe/index.html | 61 +++++----
.../batch_cloning/pombe/pombe_clone.py | 123 +++++++-----------
.../batch_cloning/pombe/pombe_get_primers.py | 114 ----------------
.../src/opencloning/primer_design.py | 78 ++++++-----
pombe_all.sh | 9 --
uv.lock | 6 +-
7 files changed, 171 insertions(+), 298 deletions(-)
delete mode 100644 packages/opencloning/src/opencloning/batch_cloning/pombe/pombe_get_primers.py
delete mode 100644 pombe_all.sh
diff --git a/packages/opencloning/src/opencloning/batch_cloning/pombe/__init__.py b/packages/opencloning/src/opencloning/batch_cloning/pombe/__init__.py
index f1779344..75087440 100644
--- a/packages/opencloning/src/opencloning/batch_cloning/pombe/__init__.py
+++ b/packages/opencloning/src/opencloning/batch_cloning/pombe/__init__.py
@@ -1,16 +1,19 @@
from fastapi import Form, File, UploadFile, HTTPException
from typing import Annotated, Literal
-from tempfile import TemporaryDirectory
import os
+import tempfile
from fastapi.responses import FileResponse
-from .pombe_get_primers import main as pombe_primers
from .pombe_clone import main as pombe_clone
from .pombe_summary import main as pombe_summary
from .pombe_gather import main as pombe_gather
import shutil
-import traceback
from ...get_router import get_router
from fastapi import Request
+from opencloning.dna_functions import request_from_addgene, request_from_snapgene
+from pydna.primer import Primer
+from pydna.opencloning_models import UploadedFileSource
+from pydna.parsers import parse as pydna_parse
+from pydna.parsers import parse_snapgene
router = get_router()
@@ -20,53 +23,60 @@ async def get_batch_cloning_page(request: Request):
return FileResponse(os.path.join(os.path.dirname(__file__), 'index.html'))
+ASSEMBLY_ACCESSION = 'GCF_000002945.2'
+
+
@router.post('/batch_cloning/pombe')
async def post_batch_cloning(
gene_list: str = Form(...),
plasmid_file: UploadFile | None = File(None),
addgene_id: str | None = Form(None),
- plasmid_option: Annotated[Literal['addgene', 'file'], Form(...)] = None,
+ plasmid_option: Annotated[Literal['addgene', 'file', 'default'], Form(...)] = None,
checking_primer_forward: str = Form(..., pattern=r'^[ACGTacgt]+$', min_length=1),
checking_primer_reverse: str = Form(..., pattern=r'^[ACGTacgt]+$', min_length=1),
+ resistance_marker: Annotated[Literal['kanmx6', 'natmx6', 'hphmx6', 'other'], Form(...)] = None,
):
-
- plasmid = plasmid_file if plasmid_option == 'file' else addgene_id
- if plasmid is None:
- raise HTTPException(status_code=400, detail='No plasmid provided')
-
genes = [gene.strip() for gene in gene_list.split() if gene.strip()]
if not genes:
raise HTTPException(status_code=400, detail='No valid genes provided')
- with TemporaryDirectory() as temp_dir:
- if plasmid_option == 'file':
- # Write the plasmid to the temp dir
- with open(os.path.join(temp_dir, plasmid_file.filename), 'wb') as f:
- shutil.copyfileobj(plasmid_file.file, f)
+ common_primers = [
+ Primer(checking_primer_forward, name='common_insert_fwd'),
+ Primer(checking_primer_reverse, name='common_insert_rvs'),
+ ]
+
+ if plasmid_option == 'default':
+ if resistance_marker == 'kanmx6':
+ addgene_id = '39296'
+ elif resistance_marker == 'natmx6':
+ plasmid = await request_from_snapgene('yeast_plasmids', 'pFA6a-natMX6')
+ elif resistance_marker == 'hphmx6':
+ plasmid = await request_from_snapgene('yeast_plasmids', 'pFA6a-hphMX6')
+ else:
+ raise HTTPException(status_code=400, detail='resistance_marker other is not supported for default plasmid')
- # Write the checking primers
- with open(os.path.join(temp_dir, 'checking_primers.fa'), 'w') as f:
- f.write(f'>common_insert_fwd\n{checking_primer_forward}\n>common_insert_rvs\n{checking_primer_reverse}')
+ elif plasmid_option == 'file':
+ assert plasmid_file is not None
+ assert plasmid_file.filename is not None
+ file_content = await plasmid_file.read()
+ if plasmid_file.filename.endswith('.dna'):
+ plasmid = parse_snapgene(file_content)[0]
+ else:
+ plasmid = pydna_parse(file_content)[0]
+ plasmid.source = UploadedFileSource(
+ file_name=plasmid_file.filename,
+ sequence_file_format=plasmid.annotations['pydna_parse_sequence_file_format'],
+ index_in_file=0,
+ )
+ if plasmid_option == 'addgene':
+ assert addgene_id is not None
+ plasmid = await request_from_addgene(addgene_id)
+ with tempfile.TemporaryDirectory() as temp_dir:
for gene in genes:
- try:
- await pombe_primers(gene, temp_dir)
- except Exception:
- raise HTTPException(status_code=404, detail=f'Primers for {gene} not found')
- try:
- if plasmid_option == 'file':
- with open(os.path.join(temp_dir, plasmid_file.filename), 'rb') as f:
- await pombe_clone(
- gene, 'GCF_000002945.2', temp_dir, UploadFile(file=f, filename=plasmid_file.filename)
- )
- else:
- await pombe_clone(gene, 'GCF_000002945.2', temp_dir, addgene_id)
- except Exception:
- # Show the stack trace in console
- print(f'Error occurred while cloning {gene}:')
- traceback.print_exc()
- raise HTTPException(status_code=400, detail=f'Clone for {gene} failed')
+ await pombe_clone(gene, ASSEMBLY_ACCESSION, temp_dir, plasmid, common_primers)
+
try:
pombe_summary(temp_dir)
pombe_gather(temp_dir)
diff --git a/packages/opencloning/src/opencloning/batch_cloning/pombe/index.html b/packages/opencloning/src/opencloning/batch_cloning/pombe/index.html
index a3373e9d..73587de4 100644
--- a/packages/opencloning/src/opencloning/batch_cloning/pombe/index.html
+++ b/packages/opencloning/src/opencloning/batch_cloning/pombe/index.html
@@ -103,15 +103,16 @@ Best way to get started
gene locus via homologous recombination.
@@ -126,25 +127,29 @@
Best way to get started
@@ -183,19 +188,29 @@
Best way to get started
}
function togglePrimerCheck() {
- var selectedOption = document.querySelector('input[name="primer_check"]:checked').value;
+ var selectedOption = document.querySelector('input[name="resistance_marker"]:checked').value;
+ var defaultPlasmid = document.getElementById("plasmid_default");
if (selectedOption === "other") {
- // Clear the content of primer inputs
document.getElementById("checking-primer-forward").value = "";
document.getElementById("checking-primer-reverse").value = "";
+ defaultPlasmid.disabled = true;
+ if (defaultPlasmid.checked) {
+ document.getElementById("plasmid_addgene").checked = true;
+ }
+ } else {
+ defaultPlasmid.disabled = false;
+ if (selectedOption === "kanmx6") {
+ document.getElementById("checking-primer-forward").value = "CGCTATACTGCTGTCGATTCG";
+ document.getElementById("checking-primer-reverse").value = "CGGATGTGATGTGAGAACTGTATCCTAGC";
+ } else if (selectedOption === "hphmx6") {
+ document.getElementById("checking-primer-forward").value = "GCGCAGAAAGTAATATCATGCG";
+ document.getElementById("checking-primer-reverse").value = "GCTCCATACAAGCCAACCACG";
+ } else if (selectedOption === "natmx6") {
+ document.getElementById("checking-primer-forward").value = "GTACCACTCTTGACGACACG";
+ document.getElementById("checking-primer-reverse").value = "GCATGCTCATGTAGAGCGCC";
+ }
}
- else if (selectedOption === "kanmx6") {
- document.getElementById("checking-primer-forward").value = "CGCTATACTGCTGTCGATTCG";
- document.getElementById("checking-primer-reverse").value = "CGGATGTGATGTGAGAACTGTATCCTAGC";
- } else if (selectedOption === "natmx6") {
- document.getElementById("checking-primer-forward").value = "GTACCACTCTTGACGACACG";
- document.getElementById("checking-primer-reverse").value = "GCATGCTCATGTAGAGCGCC";
- }
+ togglePlasmidInput();
}
window.onload = () => {
diff --git a/packages/opencloning/src/opencloning/batch_cloning/pombe/pombe_clone.py b/packages/opencloning/src/opencloning/batch_cloning/pombe/pombe_clone.py
index cdf60835..be23139e 100644
--- a/packages/opencloning/src/opencloning/batch_cloning/pombe/pombe_clone.py
+++ b/packages/opencloning/src/opencloning/batch_cloning/pombe/pombe_clone.py
@@ -1,40 +1,18 @@
import os
+from pydna.dseqrecord import Dseqrecord
from pydna.assembly2 import homologous_recombination_integration, pcr_assembly
-from opencloning.dna_functions import request_from_addgene
from opencloning.ncbi_requests import get_annotations_from_query, get_genome_region_from_annotation
-import asyncio
-from Bio import SeqIO
from pydna.primer import Primer
from pydna.opencloning_models import CloningStrategy
-from fastapi.datastructures import UploadFile
-from pydna.parsers import parse as pydna_parse
-from opencloning.dna_functions import custom_file_parser
-import io
+from pydna.utils import location_boundaries
+from opencloning.primer_design import primer_to_amplify_fragment_of_given_size_knowing_other_primer
-async def main(
- gene: str,
- assembly_accession: str,
- output_dir: str,
- plasmid_input: UploadFile | str = '19343',
- padding: int = 1000,
-):
+
+async def main(gene: str, assembly_accession: str, output_dir: str, plasmid: Dseqrecord, common_primers: list[Primer]):
print(f"\033[92mCloning {gene}\033[0m")
# Parse primers =================================================================================
- primers = [Primer(p) for p in SeqIO.parse(os.path.join(output_dir, gene, 'primers.fa'), 'fasta')]
- common_primers = [Primer(p) for p in SeqIO.parse(os.path.join(output_dir, 'checking_primers.fa'), 'fasta')]
-
- # Get plasmid sequence =================================================================================
- if isinstance(plasmid_input, UploadFile):
- file_content = await plasmid_input.read()
- if plasmid_input.filename.endswith('.dna'):
- file_streamer = io.BytesIO(file_content)
- plasmid = custom_file_parser(file_streamer, 'snapgene')[0]
- else:
- plasmid = pydna_parse(file_content)[0]
-
- else:
- plasmid = await request_from_addgene(plasmid_input)
+ # Primers have to be: clone_fwd, clone_rvs, check_fwd, check_rvs
# Get genome region =====================================================================
annotations = await get_annotations_from_query(gene, assembly_accession)
@@ -42,20 +20,50 @@ async def main(
raise ValueError(f'No annotations found for {gene}')
annotations = [a for a in annotations if gene.upper() in a['locus_tag'].upper()]
- if len(annotations) != 1:
+ if len(annotations) == 0:
raise ValueError(f'No right annotation found for {gene}')
-
- locus = await get_genome_region_from_annotation(annotations[0], 1000, 1000)
-
+ if len(annotations) > 1:
+ raise ValueError(f'Multiple annotations found for {gene}')
+
+ annotation = annotations[0]
+ if annotation['gene_type'] != 'protein-coding':
+ raise ValueError(f'{gene} is not a protein-coding gene')
+ if 'gene_id' not in annotation or not annotation['gene_id']:
+ raise ValueError(f'{gene} has no gene_id')
+
+ # Get homology arms ================================================================================
+
+ locus = await get_genome_region_from_annotation(annotation, 1000, 1000)
+ feature = next(
+ f
+ for f in locus.features
+ if (f.type == 'CDS') and (f"GeneID:{annotation['gene_id']}" in f.qualifiers['db_xref'])
+ )
+ start, end = (int(i) for i in location_boundaries(feature.location))
+ left_homology_arm = str(locus.seq[start - 80 : start]).lower()
+ right_homology_arm = str(locus.seq[end : end + 80].reverse_complement()).lower()
+ left_seq = 'CGGATCCCCGGGTTAATTAA'
+ right_seq = 'GAATTCGAGCTCGTTTAAAC'
+ left_primer = Primer(left_homology_arm + left_seq, name=f'{gene}_deletion_fwd')
+ right_primer = Primer(right_homology_arm + right_seq, name=f'{gene}_deletion_rvs')
# PCR ================================================================================================
- pcr_products = pcr_assembly(plasmid, primers[0], primers[1], limit=14, mismatches=0)
+ pcr_products = pcr_assembly(plasmid, left_primer, right_primer, limit=14, mismatches=0)
pcr_products[0].name = 'amplified_marker'
alleles = homologous_recombination_integration(locus, [pcr_products[0]], 40)
- pcr_check1 = pcr_assembly(alleles[0], primers[2], common_primers[1], limit=14, mismatches=0)[0]
+ alleles[0].name = f'{gene}Δ'
+ # Check PCR ======================================================================================
+ right_check_primer = primer_to_amplify_fragment_of_given_size_knowing_other_primer(
+ alleles[0], common_primers[0], True, [900, 1200]
+ )
+ right_check_primer.name = f'{gene}_check_pcr_right'
+ left_check_primer = primer_to_amplify_fragment_of_given_size_knowing_other_primer(
+ alleles[0], common_primers[1], False, [1100, 1200]
+ )
+ left_check_primer.name = f'{gene}_check_pcr_left'
+ pcr_check1 = pcr_assembly(alleles[0], left_check_primer, common_primers[1], limit=14, mismatches=0)[0]
pcr_check1.name = 'check_pcr_left'
- pcr_check2 = pcr_assembly(alleles[0], primers[3], common_primers[0], limit=14, mismatches=0)[0]
+ pcr_check2 = pcr_assembly(alleles[0], common_primers[0], right_check_primer, limit=14, mismatches=0)[0]
pcr_check2.name = 'check_pcr_right'
- alleles[0].name = 'deletion_allele'
cs = CloningStrategy.from_dseqrecords([pcr_check1, pcr_check2])
@@ -64,46 +72,3 @@ async def main(
with open(os.path.join(output_dir, gene, 'cloning_strategy.json'), 'w') as f:
f.write(cs.model_dump_json(indent=2))
-
-
-if __name__ == '__main__':
- import argparse
-
- parser = argparse.ArgumentParser(description='List of genes to delete from S. pombe')
- parser.add_argument(
- '--genes', type=str, required=True, help='Path to a file containing a list of genes, one per line'
- )
- args = parser.parse_args()
-
- parser.add_argument(
- '--assembly_accession',
- type=str,
- default='GCF_000002945.2',
- help='Assembly accession for S. pombe genome (default: GCF_000002945.2)',
- )
-
- parser.add_argument(
- '--output_dir',
- type=str,
- default='batch_cloning_output',
- help='Directory to save the output files (default: batch_cloning_output)',
- )
-
- parser.add_argument(
- '--plasmid',
- type=str,
- default='19343',
- help='Addgene ID for the plasmid (default: 19343)',
- )
-
- args = parser.parse_args()
- assembly_accession = args.assembly_accession
-
- with open(args.genes, 'r') as f:
- genes = [line.strip() for line in f if line.strip()]
-
- if not os.path.exists(args.output_dir):
- os.makedirs(args.output_dir)
-
- for gene in genes:
- asyncio.run(main(gene, assembly_accession, args.output_dir, args.plasmid))
diff --git a/packages/opencloning/src/opencloning/batch_cloning/pombe/pombe_get_primers.py b/packages/opencloning/src/opencloning/batch_cloning/pombe/pombe_get_primers.py
deleted file mode 100644
index 51b2a7d8..00000000
--- a/packages/opencloning/src/opencloning/batch_cloning/pombe/pombe_get_primers.py
+++ /dev/null
@@ -1,114 +0,0 @@
-from bs4 import BeautifulSoup
-import asyncio
-import re
-import os
-import argparse
-from Bio import SeqIO
-from Bio.Seq import Seq
-from Bio.SeqRecord import SeqRecord
-from ...http_client import get_http_client, Response
-
-default_settings_primer_design = {
- 'length': 80,
- 'increment': 40,
- 'add_seq': 400,
- 'opt_len': 22,
- 'min_len': 20,
- 'max_len': 28,
- 'opt_tm': 60.0,
- 'min_tm': 57.0,
- 'max_tm': 63.0,
- 'min_gc': 30,
- 'max_gc': 60,
- '.submit': 'Submit',
-}
-
-
-async def async_post(url, headers, data, params=None) -> Response:
- async with get_http_client() as client:
- return await client.post(url, headers=headers, data=data, params=params, timeout=20.0)
-
-
-async def get_primers(gene):
- print(f"\033[92mGetting primers for {gene}\033[0m")
- # A first request to access the primers
- data = {
- 'gene': gene,
- 'length': 80,
- 'plasmid': 'pFA6a',
- 'increment': 40,
- '.submit': 'Submit',
- '.cgifields': 'plasmid',
- }
-
- url = 'http://bahlerweb.cs.ucl.ac.uk/cgi-bin/PPPP/pppp_deletion.pl'
- headers = None
- resp = await async_post(url, headers, data)
- # Parse with BeautifulSoup
- soup = BeautifulSoup(resp.text, 'html.parser')
- # select forward and reverse primers by default
- forward_primer = soup.find('input', {'name': 'for_sel', 'checked': True})
- reverse_primer = soup.find('input', {'name': 'rev_sel', 'checked': True})
- forward_primer_seq = re.sub(r'[^a-zA-Z]', '', forward_primer['value'])
- reverse_primer_seq = re.sub(r'[^a-zA-Z]', '', reverse_primer['value'])
- # Make a second request to get the checking primers
- data = {
- 'gene': gene,
- 'for_sel': forward_primer['value'],
- 'rev_sel': reverse_primer['value'],
- }
- data.update(default_settings_primer_design)
-
- url = 'http://bahlerweb.cs.ucl.ac.uk/cgi-bin/PPPP/pppp_checking.pl'
- resp = await async_post(url, headers, data)
- # Parse with BeautifulSoup
- soup = BeautifulSoup(resp.text, 'html.parser')
- # Find a pre tag with the text "Left Primer:"
- left_check_primer = soup.find('pre', string=re.compile(r'Left Primer:\s+Sequence:')).get_text().strip()
- right_check_primer = soup.find('pre', string=re.compile(r'Right Primer:\s+Sequence:')).get_text().strip()
-
- pattern = r'Sequence:\s+(\S+)'
- left_check_primer_seq = re.search(pattern, left_check_primer).group(1)
- right_check_primer_seq = re.search(pattern, right_check_primer).group(1)
- return forward_primer_seq, reverse_primer_seq, left_check_primer_seq, right_check_primer_seq
-
-
-async def main(gene: str, output_dir: str):
- gene_dir = os.path.join(output_dir, gene)
- os.makedirs(gene_dir, exist_ok=True)
-
- forward_primer, reverse_primer, forward_check, reverse_check = await get_primers(gene)
-
- primers = [
- SeqRecord(Seq(forward_primer), id=f"{gene}_fwd", description=''),
- SeqRecord(Seq(reverse_primer), id=f"{gene}_rvs", description=''),
- SeqRecord(Seq(forward_check), id=f"{gene}_fwd_check", description=''),
- SeqRecord(Seq(reverse_check), id=f"{gene}_rvs_check", description=''),
- ]
-
- with open(os.path.join(gene_dir, 'primers.fa'), 'w') as f:
- SeqIO.write(primers, f, 'fasta')
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser(description='Get primers for S. pombe genes')
- parser.add_argument(
- '--genes', type=str, required=True, help='Path to a file containing a list of genes, one per line'
- )
- parser.add_argument(
- '--output_dir',
- type=str,
- default='batch_cloning_output',
- help='Directory to save the output files (default: batch_cloning_output)',
- )
-
- args = parser.parse_args()
-
- with open(args.genes, 'r') as f:
- genes = [line.strip() for line in f if line.strip()]
-
- if not os.path.exists(args.output_dir):
- os.makedirs(args.output_dir)
-
- for gene in genes:
- asyncio.run(main(gene, args.output_dir))
diff --git a/packages/opencloning/src/opencloning/primer_design.py b/packages/opencloning/src/opencloning/primer_design.py
index fe4feb99..d7b6c656 100644
--- a/packages/opencloning/src/opencloning/primer_design.py
+++ b/packages/opencloning/src/opencloning/primer_design.py
@@ -8,7 +8,7 @@
from Bio.Restriction.Restriction import RestrictionType
from Bio.Data.IUPACData import ambiguous_dna_values as _ambiguous_dna_values
from typing import Callable
-from .primer3_functions import primer3_calc_tm, PrimerDesignSettings
+from .primer3_functions import primer3_calc_tm, PrimerDesignSettings, primer3_design_primers
ambiguous_dna_values = _ambiguous_dna_values.copy()
# Remove acgt
@@ -246,38 +246,44 @@ def simple_pair_primers(
return (Primer(fwd_primer_seq, name=fwd_primer_name), Primer(rvs_primer_seq, name=rvs_primer_name))
-# def gateway_attB_primers(
-# template: Dseqrecord,
-# minimal_hybridization_length: int,
-# target_tm: float,
-# sites: tuple[str, str],
-# spacers: tuple[str, str],
-# filler_bases: str = 'GGGG',
-# ) -> tuple[PrimerModel, PrimerModel]:
-# if spacers is None:
-# spacers = ['', '']
-
-# if len(spacers) != 2:
-# raise ValueError("The 'spacers' list must contain exactly two elements.")
-
-# if sites[0] not in primer_design_attB or sites[1] not in primer_design_attB:
-# raise ValueError('Invalid attB site.')
-
-# amplicon = primer_design(template, limit=minimal_hybridization_length, target_tm=target_tm)
-# fwd_primer, rvs_primer = amplicon.primers()
-
-# if fwd_primer is None or rvs_primer is None:
-# raise ValueError('Primers could not be designed, try changing settings.')
-
-# template_name = template.name if template.name != 'name' else f'seq_{template.id}'
-
-# left_site = primer_design_attB[sites[0]]
-# right_site = primer_design_attB[sites[1]]
-
-# fwd_primer_seq = filler_bases + left_site + spacers[0] + fwd_primer.seq
-# rvs_primer_seq = filler_bases + right_site + reverse_complement(spacers[1]) + rvs_primer.seq
-
-# return (
-# PrimerModel(id=0, name=f'{template_name}_{sites[0]}_fwd', sequence=str(fwd_primer_seq)),
-# PrimerModel(id=0, name=f'{template_name}_{sites[1]}_rvs', sequence=str(rvs_primer_seq)),
-# )
+def primer_to_amplify_fragment_of_given_size_knowing_other_primer(
+ template: Dseqrecord,
+ known_primer: Primer,
+ known_is_forward: bool,
+ fragment_size_range: list[int, int],
+) -> Primer:
+ """
+ Design primers to amplify a DNA fragment of a given size, knowing one primer.
+ """
+ other_primer_tm = primer3_calc_tm(str(known_primer.seq), PrimerDesignSettings())
+ if known_is_forward:
+ seq_args = {
+ 'SEQUENCE_PRIMER': str(known_primer.seq),
+ }
+ else:
+ seq_args = {
+ 'SEQUENCE_PRIMER_REVCOMP': str(known_primer.seq),
+ }
+
+ result = primer3_design_primers(
+ str(template.seq),
+ seq_args=seq_args,
+ global_args={
+ 'PRIMER_PRODUCT_SIZE_RANGE': [fragment_size_range], # ~500 bp band
+ 'PRIMER_OPT_SIZE': 20,
+ 'PRIMER_MIN_SIZE': 18,
+ 'PRIMER_MAX_SIZE': 30,
+ 'PRIMER_OPT_TM': other_primer_tm,
+ 'PRIMER_MIN_TM': other_primer_tm - 2,
+ 'PRIMER_MAX_TM': other_primer_tm + 2,
+ 'PRIMER_PICK_ANYWAY': 1,
+ },
+ )
+ try:
+ if known_is_forward:
+ return Primer(result['PRIMER_RIGHT'][0]['SEQUENCE'], name=f'{template.name}_fwd')
+ else:
+ return Primer(result['PRIMER_LEFT'][0]['SEQUENCE'], name=f'{template.name}_rvs')
+ except IndexError:
+ print(result)
+ raise ValueError(f'Primers for checking could not be designed: {result}')
diff --git a/pombe_all.sh b/pombe_all.sh
deleted file mode 100644
index e396ba45..00000000
--- a/pombe_all.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-set -e
-# Clear the output directory except for the gene_list.txt
-find batch_cloning_output -type f ! -name 'gene_list.txt' -delete
-find batch_cloning_output -type d -empty -delete
-
-python -m opencloning.batch_cloning.pombe.pombe_get_primers --genes batch_cloning_output/gene_list.txt
-python -m opencloning.batch_cloning.pombe.pombe_clone --genes batch_cloning_output/gene_list.txt
-python -m opencloning.batch_cloning.pombe.pombe_summary
-python -m opencloning.batch_cloning.pombe.pombe_gather
diff --git a/uv.lock b/uv.lock
index 5d9f6fbc..bbba585d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1185,7 +1185,7 @@ wheels = [
[[package]]
name = "opencloning"
-version = "1.9.3"
+version = "1.9.4"
source = { editable = "packages/opencloning" }
dependencies = [
{ name = "beautifulsoup4" },
@@ -1236,7 +1236,7 @@ requires-dist = [
[[package]]
name = "opencloning-cli"
-version = "1.9.3"
+version = "1.9.4"
source = { editable = "packages/opencloning-cli" }
dependencies = [
{ name = "opencloning-db" },
@@ -1251,7 +1251,7 @@ requires-dist = [
[[package]]
name = "opencloning-db"
-version = "1.9.3"
+version = "1.9.4"
source = { editable = "packages/opencloning-db" }
dependencies = [
{ name = "alembic" },
From 7725060d9cdb1eb866d222fdf8f65ca127e57410 Mon Sep 17 00:00:00 2001
From: Manuel Lera-Ramirez
Date: Thu, 11 Jun 2026 11:37:59 +0100
Subject: [PATCH 02/21] improve styling of form and better primer design
---
.../batch_cloning/pombe/__init__.py | 2 +
.../batch_cloning/pombe/index.html | 335 +++++++++++++-----
.../batch_cloning/pombe/pombe_clone.py | 2 +-
.../src/opencloning/primer_design.py | 12 +-
4 files changed, 257 insertions(+), 94 deletions(-)
diff --git a/packages/opencloning/src/opencloning/batch_cloning/pombe/__init__.py b/packages/opencloning/src/opencloning/batch_cloning/pombe/__init__.py
index 75087440..9d05b129 100644
--- a/packages/opencloning/src/opencloning/batch_cloning/pombe/__init__.py
+++ b/packages/opencloning/src/opencloning/batch_cloning/pombe/__init__.py
@@ -28,6 +28,7 @@ async def get_batch_cloning_page(request: Request):
@router.post('/batch_cloning/pombe')
async def post_batch_cloning(
+ cloning_type: Annotated[Literal['gene_deletion', 'gene_cterm_tagging'], Form(...)],
gene_list: str = Form(...),
plasmid_file: UploadFile | None = File(None),
addgene_id: str | None = Form(None),
@@ -49,6 +50,7 @@ async def post_batch_cloning(
if plasmid_option == 'default':
if resistance_marker == 'kanmx6':
addgene_id = '39296'
+ plasmid_option = 'addgene'
elif resistance_marker == 'natmx6':
plasmid = await request_from_snapgene('yeast_plasmids', 'pFA6a-natMX6')
elif resistance_marker == 'hphmx6':
diff --git a/packages/opencloning/src/opencloning/batch_cloning/pombe/index.html b/packages/opencloning/src/opencloning/batch_cloning/pombe/index.html
index 73587de4..d23a133a 100644
--- a/packages/opencloning/src/opencloning/batch_cloning/pombe/index.html
+++ b/packages/opencloning/src/opencloning/batch_cloning/pombe/index.html
@@ -13,20 +13,95 @@
padding: 20px;
}
- textarea {
+ form {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ }
+
+ .form-section {
+ border: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .form-section legend,
+ .field-label {
+ font-weight: bold;
+ padding: 0;
+ }
+
+ .nested-section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding-left: 16px;
+ border-left: 2px solid #e8e8e8;
+ }
+
+ .section-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+ }
+
+ .radio-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+
+ .radio-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: normal;
+ cursor: pointer;
+ }
+
+ .primer_pair_row {
+ display: flex;
+ gap: 16px;
+ }
+
+ .primer_pair_field {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .primer_pair_field .field-label {
+ font-weight: bold;
+ }
+
+ textarea,
+ input[type="text"],
+ input[type="file"] {
width: 100%;
+ box-sizing: border-box;
+ }
+
+ textarea {
height: 200px;
- margin-bottom: 10px;
}
- #checking_primers input {
+ .primer_pair_field input[type="text"] {
font-family: monospace;
- width: 100%;
+ }
+
+ button {
+ cursor: pointer;
}
p.explanation {
font-size: 0.9em;
- margin: 5px 0;
+ margin: 0;
color: #666;
}
@@ -34,28 +109,43 @@
color: #0066cc;
}
+ .form-footer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ }
+
input[type="submit"] {
- display: block;
- margin: 20px auto 40px;
padding: 12px 30px;
font-size: 1.1em;
cursor: pointer;
}
+
+ #loading-message {
+ color: #666;
+ }
Batch Cloning Gene List
-
Design gene-deletion clones for Schizosaccharomyces pombe in batch.
+ Design PCR-based cloning strategies for Schizosaccharomyces pombe in batch.
How to use
+ - Select the cloning type
- Enter S. pombe systematic gene IDs (e.g. SPBC15D4.01c).
- Select a marker plasmid.
- - Specify verification primers (optional).
- - The pipeline fetches cloning primers from the Pombe Primer Project (PPPP).
- - Designs homologous-recombination strategies for each gene.
+ - Specify verification primers.
+ - The pipeline designs primers for integration and checking.
+ - Note: Integration primers assume the same binding sequence on the plasmid always:
+
+ CGGATCCCCGGGTTAATTAA for forward primer
+ GAATTCGAGCTCGTTTAAAC for reverse primer
+
+
What it returns
@@ -89,98 +179,167 @@ Best way to get started