Skip to content

Commit c702fe4

Browse files
authored
PMID:39007160 experiment design (#235)
* experiment desing from PMID:39007160 (Ziqiang et al. 2024) * update html form
1 parent 2c58e55 commit c702fe4

File tree

5 files changed

+562
-0
lines changed

5 files changed

+562
-0
lines changed

batch_cloning/__init__.py

Whitespace-only changes.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<title>Enter DNA Sequences</title>
6+
<style>
7+
body {
8+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
9+
max-width: 800px;
10+
margin: 0 auto;
11+
padding: 20px;
12+
text-align: center;
13+
}
14+
15+
textarea {
16+
width: 400px;
17+
height: 300px;
18+
margin: 20px auto;
19+
display: block;
20+
font-family: monospace;
21+
}
22+
23+
.error {
24+
color: red;
25+
margin: 10px auto;
26+
}
27+
28+
input[type="submit"] {
29+
background-color: #4CAF50;
30+
border: none;
31+
color: white;
32+
padding: 15px 32px;
33+
text-align: center;
34+
text-decoration: none;
35+
display: inline-block;
36+
font-size: 1.4em;
37+
margin: 4px 2px;
38+
cursor: pointer;
39+
border-radius: 4px;
40+
transition: background-color 0.3s;
41+
}
42+
43+
input[type="submit"]:hover {
44+
background-color: #45a049;
45+
}
46+
</style>
47+
</head>
48+
49+
<body>
50+
<h1>Versatile Cloning Strategy for Efficient Multigene Editing in Arabidopsis</h1>
51+
<p>Ziqiang P. Li, Jennifer Huard, Emmanuelle M. Bayer and Valérie Wattelet-Boyer</p>
52+
<p><a href="https://doi.org/10.21769/BioProtoc.5029">doi:10.21769/BioProtoc.5029</a></p>
53+
<p>Enter protospacer sequences (20 bases each) separated by line breaks.</p>
54+
<p>Below is the example protospacer sequences from the paper.</p>
55+
56+
<form id="sequenceForm">
57+
<textarea id="sequences" name="sequences">
58+
GCTGGCTAACCGTGAGGGGA
59+
CCGTGTACTGTAGTTACAGT
60+
TGTGGTTCCCCGGCCGTCTT
61+
ATACTCTAGTCCTCAACGCC</textarea>
62+
<br>
63+
<div id="error" class="error">
64+
</div>
65+
<input type="submit" value="Submit">
66+
</form>
67+
68+
<script>
69+
document.getElementById('sequenceForm').addEventListener('submit', async function (e) {
70+
e.preventDefault();
71+
const sequences = document.getElementById('sequences').value;
72+
const lines = sequences.split('\n').filter(line => line.trim().length > 0);
73+
const dnaRegex = /^[ACGTacgt]{20}$/;
74+
75+
const valid = lines.every(line => dnaRegex.test(line.trim()));
76+
77+
const errorDiv = document.getElementById('error');
78+
errorDiv.textContent = valid ? '' : 'Each sequence must be exactly 20 bases long and contain only A, C, G, or T';
79+
80+
if (valid) {
81+
// Submit the form data via fetch
82+
try {
83+
const response = await fetch(window.location.href, {
84+
method: 'POST',
85+
headers: {
86+
'Content-Type': 'application/json',
87+
},
88+
body: JSON.stringify(lines)
89+
});
90+
91+
if (!response.ok) {
92+
if (response.status === 400) {
93+
const error = await response.json();
94+
errorDiv.textContent = error.detail;
95+
} else {
96+
throw new Error(`HTTP error! status: ${response.status}`);
97+
}
98+
}
99+
100+
const data = await response.json();
101+
102+
// Create a blob with the JSON data
103+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
104+
105+
// Create a temporary link element
106+
const downloadLink = document.createElement('a');
107+
downloadLink.href = URL.createObjectURL(blob);
108+
downloadLink.download = 'cloning_strategy.json';
109+
110+
// Trigger the download
111+
document.body.appendChild(downloadLink);
112+
downloadLink.click();
113+
114+
// Clean up
115+
document.body.removeChild(downloadLink);
116+
URL.revokeObjectURL(downloadLink.href);
117+
} catch (error) {
118+
console.error('Error:', error);
119+
}
120+
}
121+
});
122+
</script>
123+
</body>
124+
125+
</html>

batch_cloning/ziqiang_et_al2024.json

Lines changed: 280 additions & 0 deletions
Large diffs are not rendered by default.

batch_cloning/ziqiang_et_al2024.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from Bio.Seq import reverse_complement
2+
from fastapi import APIRouter
3+
from fastapi.responses import FileResponse
4+
from pathlib import Path
5+
6+
7+
router = APIRouter()
8+
9+
10+
def validate_protospacers(protospacers):
11+
"""
12+
Ensure that the protospacers are compatible for the golden gate assembly, by checking that the central 4
13+
bps are different in all of them, and also different from the other assembly joints.
14+
"""
15+
forbidden_joints = ['AACA', 'GCTT']
16+
17+
for i, ps in enumerate(protospacers):
18+
# Must be only ACGT
19+
if not set(ps).issubset({'A', 'C', 'G', 'T'}):
20+
raise ValueError(f"Protospacer {i} {ps} contains invalid bases")
21+
if len(ps) != 20:
22+
raise ValueError(f"Protospacer {i} {ps} is not 20 bp long")
23+
if ps[8:12] in forbidden_joints:
24+
raise ValueError(
25+
f"Protospacer {i} has a forbidden joint {ps[8:12]}, which is used for the constant parts of the assembly"
26+
)
27+
# Find any other protospacers with the same joint sequence
28+
same_joint_indexes = [
29+
j + 1 for j, other_ps in enumerate(protospacers) if i != j and ps[8:12] == other_ps[8:12]
30+
]
31+
if same_joint_indexes:
32+
raise ValueError(
33+
f"Protospacer {i + 1} has the same joint as protospacers {', '.join(map(str, same_joint_indexes))}"
34+
)
35+
36+
37+
def design_primers(protospacers):
38+
39+
fwd_prefix = 'taggtctcc'
40+
rvs_prefix = 'atggtctca'
41+
fwd_suffix = 'gttttagagctagaa'
42+
rvs_suffix = 'tgcaccagccgggaa'
43+
44+
primers = list()
45+
for protospacer in protospacers:
46+
primers.append(rvs_prefix + reverse_complement(protospacer[:12]) + rvs_suffix)
47+
primers.append(fwd_prefix + protospacer[8:] + fwd_suffix)
48+
return primers
49+
50+
51+
@router.get('/batch_cloning/ziqiang_et_al2024')
52+
def ziqiang_et_al2024():
53+
return FileResponse(Path(__file__).parent / 'ziqiang_et_al2024.html')

main.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@
8484
import io
8585
from Bio import BiopythonParserWarning
8686
from gateway import gateway_overlap, find_gateway_sites, annotate_gateway_sites
87+
from batch_cloning.ziqiang_et_al2024 import (
88+
router as ziqiang_et_al2024_router,
89+
validate_protospacers,
90+
design_primers as design_primers_ziqiang_et_al2024,
91+
)
92+
import json
8793

8894
# ENV variables ========================================
8995
RECORD_STUBS = os.environ['RECORD_STUBS'] == '1' if 'RECORD_STUBS' in os.environ else False
@@ -1546,4 +1552,102 @@ async def get_other_frontend_files(name: str):
15461552
raise HTTPException(404)
15471553

15481554

1555+
@router.post('/batch_cloning/ziqiang_et_al2024', response_model=BaseCloningStrategy)
1556+
async def ziqiang_et_al2024_post(protospacers: list[str]):
1557+
try:
1558+
validate_protospacers(protospacers)
1559+
except ValueError as e:
1560+
raise HTTPException(400, str(e))
1561+
primers = design_primers_ziqiang_et_al2024(protospacers)
1562+
1563+
with open('batch_cloning/ziqiang_et_al2024.json', 'r') as f:
1564+
template = BaseCloningStrategy.model_validate(json.load(f))
1565+
1566+
max_primer_id = max([primer.id for primer in template.primers], default=0)
1567+
1568+
for i, primer in enumerate(primers):
1569+
max_primer_id += 1
1570+
orientation = 'rvs' if i % 2 == 0 else 'fwd'
1571+
template.primers.append(
1572+
PrimerModel(id=max_primer_id, name=f"protospacer_{i // 2 + 1}_{orientation}", sequence=primer)
1573+
)
1574+
1575+
primer_ids_for_pcrs = [3, *[p.id for p in template.primers[-len(primers) :]], 12]
1576+
next_node_id = max([s.id for s in template.sequences] + [s.id for s in template.sources]) + 1
1577+
1578+
template_sequence = next(s for s in template.sequences if s.id == 18)
1579+
for i, (fwd_primer_id, rvs_primer_id) in enumerate(zip(primer_ids_for_pcrs[::2], primer_ids_for_pcrs[1::2])):
1580+
pcr_source = PCRSource(id=next_node_id, output_name=f"pcr_protospacer_{i + 1}")
1581+
fwd_primer = next(p for p in template.primers if p.id == fwd_primer_id)
1582+
rvs_primer = next(p for p in template.primers if p.id == rvs_primer_id)
1583+
1584+
next_node_id += 1
1585+
resp = await pcr(pcr_source, [template_sequence], [fwd_primer, rvs_primer], 14, 0)
1586+
pcr_product: TextFileSequence = TextFileSequence.model_validate(resp['sequences'][0])
1587+
pcr_product.id = next_node_id
1588+
pcr_source: PCRSource = PCRSource.model_validate(resp['sources'][0])
1589+
pcr_source.output = next_node_id
1590+
1591+
template.sequences.append(pcr_product)
1592+
template.sources.append(pcr_source)
1593+
1594+
next_node_id += 1
1595+
1596+
# Find all PCR products
1597+
# (we use type instead of isinstance because the BaseCloningStrategy does not
1598+
# have the newer source models with extra methods)
1599+
pcr_product_ids = [s.output for s in template.sources if s.type == 'PCRSource']
1600+
1601+
# Make all input of a Golden gate assembly
1602+
golden_gate_source = RestrictionAndLigationSource(
1603+
id=next_node_id, output_name='golden_gate_assembly', restriction_enzymes=['BsaI'], input=pcr_product_ids
1604+
)
1605+
1606+
next_node_id += 1
1607+
# Make them
1608+
input_sequences = [next(s for s in template.sequences if s.id == p) for p in pcr_product_ids]
1609+
resp = await restriction_and_ligation(golden_gate_source, input_sequences, False, False)
1610+
golden_gate_product: TextFileSequence = TextFileSequence.model_validate(resp['sequences'][0])
1611+
golden_gate_product.id = next_node_id
1612+
golden_gate_source: RestrictionAndLigationSource = RestrictionAndLigationSource.model_validate(resp['sources'][0])
1613+
golden_gate_source.output = next_node_id
1614+
next_node_id += 1
1615+
1616+
template.sequences.append(golden_gate_product)
1617+
template.sources.append(golden_gate_source)
1618+
1619+
bp_target = next(s for s in template.sequences if s.id == 12)
1620+
gateway_source = GatewaySource(id=next_node_id, output_name='entry_clone', reaction_type='BP', greedy=False)
1621+
next_node_id += 1
1622+
resp = await gateway(gateway_source, [golden_gate_product, bp_target], circular_only=True, only_multi_site=True)
1623+
gateway_product: TextFileSequence = TextFileSequence.model_validate(resp['sequences'][0])
1624+
gateway_product.id = next_node_id
1625+
gateway_source: GatewaySource = GatewaySource.model_validate(resp['sources'][0])
1626+
gateway_source.output = next_node_id
1627+
next_node_id += 1
1628+
1629+
template.sequences.append(gateway_product)
1630+
template.sources.append(gateway_source)
1631+
1632+
# Now we want to do a Gateway with everything, so we need to find all sequences that are not input of anything
1633+
all_input_ids = sum([s.input for s in template.sources], [])
1634+
sequences_to_clone = [s for s in template.sequences if s.id not in all_input_ids]
1635+
1636+
gateway_source = GatewaySource(id=next_node_id, output_name='expression_clone', reaction_type='LR', greedy=False)
1637+
next_node_id += 1
1638+
resp = await gateway(gateway_source, sequences_to_clone, circular_only=True, only_multi_site=True)
1639+
index_of_product = next(i for i, s in enumerate(resp['sequences']) if '/label="Cas9"' in s.file_content)
1640+
expression_clone: TextFileSequence = TextFileSequence.model_validate(resp['sequences'][index_of_product])
1641+
expression_clone.id = next_node_id
1642+
gateway_source: GatewaySource = GatewaySource.model_validate(resp['sources'][index_of_product])
1643+
gateway_source.output = next_node_id
1644+
next_node_id += 1
1645+
1646+
template.sequences.append(expression_clone)
1647+
template.sources.append(gateway_source)
1648+
1649+
return template
1650+
1651+
15491652
app.include_router(router)
1653+
app.include_router(ziqiang_et_al2024_router)

0 commit comments

Comments
 (0)