opencloning 0.4.7__py3-none-any.whl → 0.5__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.
- opencloning/app_settings.py +7 -0
- opencloning/batch_cloning/pombe/__init__.py +2 -2
- opencloning/batch_cloning/pombe/pombe_clone.py +31 -112
- opencloning/batch_cloning/pombe/pombe_summary.py +20 -8
- opencloning/batch_cloning/ziqiang_et_al2024/__init__.py +8 -8
- opencloning/batch_cloning/ziqiang_et_al2024/ziqiang_et_al2024.json +2 -9
- opencloning/bug_fixing/backend_v0_3.py +13 -5
- opencloning/catalogs/__init__.py +36 -0
- opencloning/catalogs/igem2024.yaml +2172 -0
- opencloning/catalogs/openDNA_collections.yaml +1161 -0
- opencloning/catalogs/readme.txt +1 -0
- opencloning/catalogs/seva.tsv +231 -0
- opencloning/catalogs/snapgene.yaml +2837 -0
- opencloning/dna_functions.py +155 -158
- opencloning/dna_utils.py +45 -62
- opencloning/ebic/primer_design.py +24 -14
- opencloning/endpoints/annotation.py +9 -13
- opencloning/endpoints/assembly.py +157 -378
- opencloning/endpoints/endpoint_utils.py +52 -0
- opencloning/endpoints/external_import.py +169 -124
- opencloning/endpoints/no_assembly.py +23 -39
- opencloning/endpoints/no_input.py +32 -47
- opencloning/endpoints/other.py +1 -1
- opencloning/endpoints/primer_design.py +23 -17
- opencloning/http_client.py +2 -2
- opencloning/ncbi_requests.py +113 -47
- opencloning/primer3_functions.py +3 -3
- opencloning/primer_design.py +1 -1
- opencloning/pydantic_models.py +10 -510
- opencloning/request_examples.py +10 -22
- opencloning/temp_functions.py +50 -0
- {opencloning-0.4.7.dist-info → opencloning-0.5.dist-info}/METADATA +18 -8
- opencloning-0.5.dist-info/RECORD +51 -0
- {opencloning-0.4.7.dist-info → opencloning-0.5.dist-info}/WHEEL +1 -1
- opencloning/cre_lox.py +0 -116
- opencloning/gateway.py +0 -154
- opencloning-0.4.7.dist-info/RECORD +0 -45
- {opencloning-0.4.7.dist-info → opencloning-0.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,76 +1,54 @@
|
|
|
1
1
|
from fastapi import Query, HTTPException
|
|
2
|
-
from typing import Union
|
|
2
|
+
from typing import Union
|
|
3
3
|
from pydna.dseqrecord import Dseqrecord
|
|
4
4
|
from pydna.primer import Primer as PydnaPrimer
|
|
5
|
-
from pydna.crispr import cas9
|
|
6
5
|
from pydantic import create_model, Field
|
|
7
6
|
from typing import Annotated
|
|
8
|
-
from
|
|
9
|
-
from opencloning.
|
|
7
|
+
from opencloning.endpoints.endpoint_utils import format_products, parse_restriction_enzymes
|
|
8
|
+
from opencloning.temp_functions import is_assembly_complete, minimal_assembly_overlap
|
|
10
9
|
from ..dna_functions import (
|
|
11
|
-
get_invalid_enzyme_names,
|
|
12
|
-
format_sequence_genbank,
|
|
13
10
|
read_dsrecord_from_json,
|
|
14
11
|
)
|
|
15
|
-
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from opencloning_linkml.datamodel import (
|
|
15
|
+
CRISPRSource,
|
|
16
|
+
CreLoxRecombinationSource,
|
|
16
17
|
PCRSource,
|
|
17
|
-
PrimerModel,
|
|
18
|
-
TextFileSequence,
|
|
19
18
|
LigationSource,
|
|
20
|
-
HomologousRecombinationSource,
|
|
21
|
-
CRISPRSource,
|
|
22
19
|
GibsonAssemblySource,
|
|
23
20
|
InFusionSource,
|
|
24
|
-
|
|
25
|
-
AssemblySource,
|
|
21
|
+
InVivoAssemblySource,
|
|
26
22
|
OverlapExtensionPCRLigationSource,
|
|
23
|
+
HomologousRecombinationSource,
|
|
24
|
+
RestrictionAndLigationSource,
|
|
27
25
|
GatewaySource,
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
Primer as PrimerModel,
|
|
27
|
+
TextFileSequence,
|
|
30
28
|
)
|
|
29
|
+
|
|
31
30
|
from pydna.assembly2 import (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
pcr_assembly as _pcr_assembly,
|
|
32
|
+
ligation_assembly as _ligation_assembly,
|
|
33
|
+
gibson_assembly as _gibson_assembly,
|
|
34
|
+
in_fusion_assembly as _in_fusion_assembly,
|
|
35
|
+
in_vivo_assembly as _in_vivo_assembly,
|
|
36
|
+
fusion_pcr_assembly as _fusion_pcr_assembly,
|
|
37
|
+
restriction_ligation_assembly as _restriction_ligation_assembly,
|
|
38
|
+
homologous_recombination_integration as _homologous_recombination_integration,
|
|
39
|
+
gateway_assembly as _gateway_assembly,
|
|
40
|
+
crispr_integration as _crispr_integration,
|
|
41
|
+
cre_lox_integration as _cre_lox_integration,
|
|
42
|
+
cre_lox_excision as _cre_lox_excision,
|
|
44
43
|
)
|
|
44
|
+
from pydna.cre_lox import annotate_loxP_sites
|
|
45
45
|
|
|
46
|
-
from
|
|
46
|
+
from pydna.gateway import annotate_gateway_sites
|
|
47
47
|
from ..get_router import get_router
|
|
48
48
|
|
|
49
49
|
router = get_router()
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def format_known_assembly_response(
|
|
53
|
-
source: AssemblySource,
|
|
54
|
-
out_sources: list[AssemblySource],
|
|
55
|
-
fragments: list[Dseqrecord],
|
|
56
|
-
product_callback: Callable[[Dseqrecord], Dseqrecord] = lambda x: x,
|
|
57
|
-
):
|
|
58
|
-
"""Common function for assembly sources, when assembly is known"""
|
|
59
|
-
# If a specific assembly is requested
|
|
60
|
-
assembly_plan = source.get_assembly_plan(fragments)
|
|
61
|
-
for s in out_sources:
|
|
62
|
-
# TODO: it seems that assemble() is not getting is_insertion ever
|
|
63
|
-
other_assembly_plan = s.get_assembly_plan(fragments)
|
|
64
|
-
if assembly_plan == other_assembly_plan:
|
|
65
|
-
return {
|
|
66
|
-
'sequences': [
|
|
67
|
-
format_sequence_genbank(product_callback(assemble(fragments, assembly_plan)), s.output_name)
|
|
68
|
-
],
|
|
69
|
-
'sources': [s],
|
|
70
|
-
}
|
|
71
|
-
raise HTTPException(400, 'The provided assembly is not valid.')
|
|
72
|
-
|
|
73
|
-
|
|
74
52
|
@router.post(
|
|
75
53
|
'/crispr',
|
|
76
54
|
response_model=create_model(
|
|
@@ -90,133 +68,24 @@ async def crispr(
|
|
|
90
68
|
TODO: Check support for circular DNA targets
|
|
91
69
|
"""
|
|
92
70
|
template, insert = [read_dsrecord_from_json(seq) for seq in sequences]
|
|
71
|
+
guides = [PydnaPrimer(guide.sequence, id=str(guide.id), name=guide.name) for guide in guides]
|
|
93
72
|
|
|
94
|
-
if
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# TODO: check input method for guide (currently as a primer)
|
|
98
|
-
# TODO: support user input PAM
|
|
99
|
-
|
|
100
|
-
# Check cutsites from guide provided by user
|
|
101
|
-
guide_cuts = []
|
|
102
|
-
for guide in guides:
|
|
103
|
-
enzyme = cas9(guide.sequence)
|
|
104
|
-
possible_cuts = template.seq.get_cutsites(enzyme)
|
|
105
|
-
if len(possible_cuts) == 0:
|
|
106
|
-
raise HTTPException(
|
|
107
|
-
400, f'Could not find Cas9 cutsite in the target sequence using the guide: {guide.name}'
|
|
108
|
-
)
|
|
109
|
-
guide_cuts.append(possible_cuts)
|
|
110
|
-
sorted_guide_ids = list(sorted([guide.id for guide in guides]))
|
|
111
|
-
|
|
112
|
-
# Check if homologous recombination is possible
|
|
113
|
-
fragments = [template, insert]
|
|
114
|
-
asm = Assembly(fragments, minimal_homology, use_all_fragments=True)
|
|
115
|
-
try:
|
|
116
|
-
possible_assemblies = [a for a in asm.get_insertion_assemblies() if a[0][0] == 1]
|
|
117
|
-
except ValueError as e:
|
|
118
|
-
raise HTTPException(400, *e.args)
|
|
73
|
+
completed_source = source if is_assembly_complete(source) else None
|
|
74
|
+
if completed_source is not None:
|
|
75
|
+
minimal_homology = minimal_assembly_overlap(source)
|
|
119
76
|
|
|
120
|
-
if not possible_assemblies:
|
|
121
|
-
raise HTTPException(400, 'Repair fragment cannot be inserted in the target sequence through homology')
|
|
122
|
-
|
|
123
|
-
valid_assemblies = []
|
|
124
|
-
# Check if Cas9 cut is within the homologous recombination region
|
|
125
|
-
for a in possible_assemblies:
|
|
126
|
-
hr_start = int(a[0][2].start)
|
|
127
|
-
hr_end = int(a[1][3].end)
|
|
128
|
-
|
|
129
|
-
for cuts in guide_cuts:
|
|
130
|
-
reparable_cuts = [c for c in cuts if c[0][0] > hr_start and c[0][0] <= hr_end]
|
|
131
|
-
if len(reparable_cuts):
|
|
132
|
-
valid_assemblies.append(a)
|
|
133
|
-
if len(reparable_cuts) != len(cuts):
|
|
134
|
-
# TODO: warning a cutsite falls outside
|
|
135
|
-
pass
|
|
136
|
-
|
|
137
|
-
if len(valid_assemblies) == 0:
|
|
138
|
-
raise HTTPException(
|
|
139
|
-
400, 'A Cas9 cutsite was found, and a homologous recombination region, but they do not overlap.'
|
|
140
|
-
)
|
|
141
|
-
# elif len(valid_assemblies) != len(possible_assemblies):
|
|
142
|
-
# # TODO: warning that some assemblies were discarded
|
|
143
|
-
# pass
|
|
144
|
-
|
|
145
|
-
# TODO: double check that this works for circular DNA -> for now get_insertion_assemblies() is only
|
|
146
|
-
# meant for linear DNA
|
|
147
|
-
|
|
148
|
-
out_sources = [
|
|
149
|
-
CRISPRSource.from_assembly(id=source.id, assembly=a, guides=sorted_guide_ids, fragments=fragments)
|
|
150
|
-
for a in valid_assemblies
|
|
151
|
-
]
|
|
152
|
-
|
|
153
|
-
# If a specific assembly is requested
|
|
154
|
-
if source.is_assembly_complete():
|
|
155
|
-
return format_known_assembly_response(source, out_sources, [template, insert])
|
|
156
|
-
|
|
157
|
-
out_sequences = [
|
|
158
|
-
format_sequence_genbank(assemble([template, insert], a, is_insertion=True), source.output_name)
|
|
159
|
-
for a in valid_assemblies
|
|
160
|
-
]
|
|
161
|
-
return {'sources': out_sources, 'sequences': out_sequences}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def generate_assemblies(
|
|
165
|
-
source: AssemblySource,
|
|
166
|
-
create_source: Callable[[list, bool], AssemblySource],
|
|
167
|
-
fragments: list[TextFileSequence],
|
|
168
|
-
circular_only: bool,
|
|
169
|
-
algo: Callable,
|
|
170
|
-
allow_insertion_assemblies: bool,
|
|
171
|
-
assembly_kwargs: dict | None = None,
|
|
172
|
-
product_callback: Callable[[Dseqrecord], Dseqrecord] = lambda x: x,
|
|
173
|
-
recombination_mode: bool = False,
|
|
174
|
-
) -> dict[Literal['sources', 'sequences'], list[AssemblySource] | list[TextFileSequence]]:
|
|
175
|
-
if assembly_kwargs is None:
|
|
176
|
-
assembly_kwargs = {}
|
|
177
77
|
try:
|
|
178
|
-
|
|
179
|
-
if len(fragments) > 1:
|
|
180
|
-
asm = Assembly(
|
|
181
|
-
fragments,
|
|
182
|
-
algorithm=algo,
|
|
183
|
-
use_all_fragments=True,
|
|
184
|
-
use_fragment_order=False,
|
|
185
|
-
**assembly_kwargs,
|
|
186
|
-
)
|
|
187
|
-
circular_assemblies = asm.get_circular_assemblies()
|
|
188
|
-
out_sources += [create_source(a, True) for a in circular_assemblies]
|
|
189
|
-
if not circular_only:
|
|
190
|
-
if not recombination_mode:
|
|
191
|
-
out_sources += [
|
|
192
|
-
create_source(a, False)
|
|
193
|
-
for a in filter_linear_subassemblies(
|
|
194
|
-
asm.get_linear_assemblies(), circular_assemblies, fragments
|
|
195
|
-
)
|
|
196
|
-
]
|
|
197
|
-
else:
|
|
198
|
-
out_sources += [create_source(a, False) for a in asm.get_insertion_assemblies()]
|
|
199
|
-
else:
|
|
200
|
-
asm = SingleFragmentAssembly(fragments, algorithm=algo, **assembly_kwargs)
|
|
201
|
-
out_sources.extend(create_source(a, True) for a in asm.get_circular_assemblies())
|
|
202
|
-
if not circular_only and allow_insertion_assemblies:
|
|
203
|
-
out_sources.extend(create_source(a, False) for a in asm.get_insertion_assemblies())
|
|
204
|
-
|
|
78
|
+
products = _crispr_integration(template, [insert], guides, minimal_homology)
|
|
205
79
|
except ValueError as e:
|
|
206
80
|
raise HTTPException(400, *e.args)
|
|
207
81
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
216
|
-
for s in out_sources
|
|
217
|
-
]
|
|
218
|
-
|
|
219
|
-
return {'sources': out_sources, 'sequences': out_sequences}
|
|
82
|
+
return format_products(
|
|
83
|
+
source.id,
|
|
84
|
+
products,
|
|
85
|
+
completed_source,
|
|
86
|
+
source.output_name,
|
|
87
|
+
no_products_error_message=f'No suitable products produced with provided primers and {minimal_homology} bps of homology',
|
|
88
|
+
)
|
|
220
89
|
|
|
221
90
|
|
|
222
91
|
@router.post(
|
|
@@ -235,24 +104,23 @@ async def ligation(
|
|
|
235
104
|
|
|
236
105
|
fragments = [read_dsrecord_from_json(seq) for seq in sequences]
|
|
237
106
|
|
|
238
|
-
# Lambda function for code clarity
|
|
239
|
-
def create_source(a, is_circular):
|
|
240
|
-
return LigationSource.from_assembly(assembly=a, circular=is_circular, id=source.id, fragments=fragments)
|
|
241
|
-
|
|
242
107
|
# If the assembly is known, the blunt parameter is ignored, and we set the algorithm type from the assembly
|
|
243
|
-
# (blunt ligations have
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
108
|
+
# (blunt ligations have locations of length zero)
|
|
109
|
+
# Also, we allow partial overlap to be more permissive
|
|
110
|
+
completed_source = source if is_assembly_complete(source) else None
|
|
111
|
+
if completed_source:
|
|
112
|
+
blunt = minimal_assembly_overlap(source) == 0
|
|
113
|
+
allow_partial_overlap = True
|
|
114
|
+
try:
|
|
115
|
+
products = _ligation_assembly(
|
|
116
|
+
fragments, allow_blunt=blunt, allow_partial_overlap=allow_partial_overlap, circular_only=circular_only
|
|
117
|
+
)
|
|
118
|
+
except ValueError as e:
|
|
119
|
+
raise HTTPException(400, *e.args)
|
|
254
120
|
|
|
255
|
-
return
|
|
121
|
+
return format_products(
|
|
122
|
+
source.id, products, completed_source, source.output_name, no_products_error_message='No ligations were found.'
|
|
123
|
+
)
|
|
256
124
|
|
|
257
125
|
|
|
258
126
|
@router.post(
|
|
@@ -264,17 +132,14 @@ async def ligation(
|
|
|
264
132
|
async def pcr(
|
|
265
133
|
source: PCRSource,
|
|
266
134
|
sequences: Annotated[list[TextFileSequence], Field(min_length=1, max_length=1)],
|
|
267
|
-
primers: Annotated[list[PrimerModel], Field(min_length=
|
|
135
|
+
primers: Annotated[list[PrimerModel], Field(min_length=2, max_length=2)],
|
|
268
136
|
minimal_annealing: int = Query(
|
|
269
137
|
14,
|
|
270
138
|
description='The minimal amount of bases that must match between the primer and the sequence, excluding mismatches.',
|
|
271
139
|
),
|
|
272
140
|
allowed_mismatches: int = Query(0, description='The number of mismatches allowed'),
|
|
273
141
|
):
|
|
274
|
-
if len(primers) != len(sequences) * 2:
|
|
275
|
-
raise HTTPException(400, 'The number of primers should be twice the number of sequences.')
|
|
276
142
|
|
|
277
|
-
minimal_annealing = minimal_annealing + allowed_mismatches
|
|
278
143
|
pydna_sequences = [read_dsrecord_from_json(s) for s in sequences]
|
|
279
144
|
pydna_primers = [PydnaPrimer(p.sequence, id=str(p.id), name=p.name) for p in primers]
|
|
280
145
|
|
|
@@ -283,77 +148,42 @@ async def pcr(
|
|
|
283
148
|
# What happens if annealing is zero? That would mean
|
|
284
149
|
# mismatch in the 3' of the primer, which maybe should
|
|
285
150
|
# not be allowed.
|
|
286
|
-
if
|
|
287
|
-
|
|
151
|
+
completed_source = source if is_assembly_complete(source) else None
|
|
152
|
+
if completed_source is not None:
|
|
153
|
+
minimal_annealing = minimal_assembly_overlap(source)
|
|
288
154
|
# Only the ones that match are included in the output assembly
|
|
289
155
|
# location, so the submitted assembly should be returned without
|
|
290
156
|
# allowed mistmatches
|
|
291
157
|
# TODO: tests for this
|
|
292
158
|
allowed_mismatches = 0
|
|
293
159
|
|
|
294
|
-
# Arrange the fragments in the order primer, sequence, primer
|
|
295
|
-
fragments = list()
|
|
296
|
-
while len(pydna_primers):
|
|
297
|
-
fragments.append(pydna_primers.pop(0))
|
|
298
|
-
fragments.append(pydna_sequences.pop(0))
|
|
299
|
-
fragments.append(pydna_primers.pop(0))
|
|
300
|
-
|
|
301
|
-
asm = PCRAssembly(fragments, limit=minimal_annealing, mismatches=allowed_mismatches)
|
|
302
160
|
try:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if len(sequences) == 1 and primers[0].id == primers[1].id:
|
|
310
|
-
possible_assemblies = [a for a in possible_assemblies if (a[0][0] == 1 and a[0][1] == 2)]
|
|
311
|
-
|
|
312
|
-
out_sources = [
|
|
313
|
-
PCRSource.from_assembly(
|
|
314
|
-
id=source.id,
|
|
315
|
-
assembly=a,
|
|
316
|
-
circular=False,
|
|
317
|
-
fragments=fragments,
|
|
161
|
+
products: list[Dseqrecord] = _pcr_assembly(
|
|
162
|
+
pydna_sequences[0],
|
|
163
|
+
pydna_primers[0],
|
|
164
|
+
pydna_primers[1],
|
|
165
|
+
limit=minimal_annealing,
|
|
166
|
+
mismatches=allowed_mismatches,
|
|
318
167
|
add_primer_features=source.add_primer_features,
|
|
319
168
|
)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
# If a specific assembly is requested
|
|
324
|
-
if source.is_assembly_complete():
|
|
325
|
-
|
|
326
|
-
def callback(x):
|
|
327
|
-
if source.add_primer_features:
|
|
328
|
-
return annotate_primer_binding_sites(x, fragments)
|
|
329
|
-
else:
|
|
330
|
-
return x
|
|
331
|
-
|
|
332
|
-
return format_known_assembly_response(source, out_sources, fragments, callback)
|
|
333
|
-
|
|
334
|
-
if len(possible_assemblies) == 0:
|
|
335
|
-
raise HTTPException(400, 'No pair of annealing primers was found. Try changing the annealing settings.')
|
|
336
|
-
|
|
337
|
-
def callback(fragments, a):
|
|
338
|
-
out_seq = assemble(fragments, a)
|
|
339
|
-
if source.add_primer_features:
|
|
340
|
-
return annotate_primer_binding_sites(out_seq, fragments)
|
|
341
|
-
else:
|
|
342
|
-
return out_seq
|
|
343
|
-
|
|
344
|
-
out_sequences = [
|
|
345
|
-
format_sequence_genbank(callback(fragments, a), source.output_name)
|
|
346
|
-
for s, a in zip(out_sources, possible_assemblies)
|
|
347
|
-
]
|
|
169
|
+
except ValueError as e:
|
|
170
|
+
# This catches the too many assemblies error
|
|
171
|
+
raise HTTPException(400, *e.args)
|
|
348
172
|
|
|
349
|
-
return
|
|
173
|
+
return format_products(
|
|
174
|
+
source.id,
|
|
175
|
+
products,
|
|
176
|
+
completed_source,
|
|
177
|
+
source.output_name,
|
|
178
|
+
no_products_error_message='No pair of annealing primers was found. Try changing the annealing settings.',
|
|
179
|
+
)
|
|
350
180
|
|
|
351
181
|
|
|
352
182
|
@router.post(
|
|
353
183
|
'/homologous_recombination',
|
|
354
184
|
response_model=create_model(
|
|
355
185
|
'HomologousRecombinationResponse',
|
|
356
|
-
sources=(list[HomologousRecombinationSource], ...),
|
|
186
|
+
sources=(list[Union[HomologousRecombinationSource, InVivoAssemblySource]], ...),
|
|
357
187
|
sequences=(list[TextFileSequence], ...),
|
|
358
188
|
),
|
|
359
189
|
)
|
|
@@ -366,43 +196,25 @@ async def homologous_recombination(
|
|
|
366
196
|
template, insert = [read_dsrecord_from_json(seq) for seq in sequences]
|
|
367
197
|
|
|
368
198
|
# If an assembly is provided, we ignore minimal_homology
|
|
369
|
-
if
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
asm = Assembly((template, insert), limit=minimal_homology, use_all_fragments=True)
|
|
199
|
+
completed_source = source if is_assembly_complete(source) else None
|
|
200
|
+
if completed_source is not None:
|
|
201
|
+
minimal_homology = minimal_assembly_overlap(source)
|
|
373
202
|
|
|
374
|
-
# The condition is that the first and last fragments are the template
|
|
375
203
|
try:
|
|
376
|
-
if
|
|
377
|
-
|
|
204
|
+
if template.circular:
|
|
205
|
+
products = _in_vivo_assembly([template, insert], minimal_homology, circular_only=True)
|
|
378
206
|
else:
|
|
379
|
-
|
|
380
|
-
|
|
207
|
+
products = _homologous_recombination_integration(template, [insert], minimal_homology)
|
|
381
208
|
except ValueError as e:
|
|
382
209
|
raise HTTPException(400, *e.args)
|
|
383
210
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
for a in possible_assemblies
|
|
392
|
-
]
|
|
393
|
-
|
|
394
|
-
# If a specific assembly is requested
|
|
395
|
-
if source.is_assembly_complete():
|
|
396
|
-
return format_known_assembly_response(source, out_sources, [template, insert])
|
|
397
|
-
|
|
398
|
-
out_sequences = [
|
|
399
|
-
format_sequence_genbank(
|
|
400
|
-
assemble([template, insert], a, is_insertion=not template.circular), source.output_name
|
|
401
|
-
)
|
|
402
|
-
for a in possible_assemblies
|
|
403
|
-
]
|
|
404
|
-
|
|
405
|
-
return {'sources': out_sources, 'sequences': out_sequences}
|
|
211
|
+
return format_products(
|
|
212
|
+
source.id,
|
|
213
|
+
products,
|
|
214
|
+
completed_source,
|
|
215
|
+
source.output_name,
|
|
216
|
+
no_products_error_message=f'No homologous recombination with at least {minimal_homology} bps of homology was found.',
|
|
217
|
+
)
|
|
406
218
|
|
|
407
219
|
|
|
408
220
|
@router.post(
|
|
@@ -426,24 +238,33 @@ async def gibson_assembly(
|
|
|
426
238
|
):
|
|
427
239
|
|
|
428
240
|
fragments = [read_dsrecord_from_json(seq) for seq in sequences]
|
|
241
|
+
completed_source = source if is_assembly_complete(source) else None
|
|
242
|
+
if completed_source:
|
|
243
|
+
minimal_homology = minimal_assembly_overlap(completed_source)
|
|
244
|
+
|
|
245
|
+
function2use = None
|
|
246
|
+
if isinstance(source, GibsonAssemblySource):
|
|
247
|
+
function2use = _gibson_assembly
|
|
248
|
+
elif isinstance(source, OverlapExtensionPCRLigationSource):
|
|
249
|
+
function2use = _fusion_pcr_assembly
|
|
250
|
+
elif isinstance(source, InFusionSource):
|
|
251
|
+
function2use = _in_fusion_assembly
|
|
252
|
+
else:
|
|
253
|
+
function2use = _in_vivo_assembly
|
|
429
254
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
255
|
+
try:
|
|
256
|
+
products = function2use(fragments, minimal_homology, circular_only)
|
|
257
|
+
except ValueError as e:
|
|
258
|
+
raise HTTPException(400, *e.args)
|
|
433
259
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
260
|
+
return format_products(
|
|
261
|
+
source.id,
|
|
262
|
+
products,
|
|
263
|
+
completed_source,
|
|
264
|
+
source.output_name,
|
|
265
|
+
no_products_error_message=f'No {"circular " if circular_only else ""}assembly with at least {minimal_homology} bps of homology was found.',
|
|
437
266
|
)
|
|
438
267
|
|
|
439
|
-
if len(resp['sources']) == 0:
|
|
440
|
-
raise HTTPException(
|
|
441
|
-
400,
|
|
442
|
-
f'No {"circular " if circular_only else ""}assembly with at least {minimal_homology} bps of homology was found.',
|
|
443
|
-
)
|
|
444
|
-
|
|
445
|
-
return resp
|
|
446
|
-
|
|
447
268
|
|
|
448
269
|
@router.post(
|
|
449
270
|
'/restriction_and_ligation',
|
|
@@ -457,37 +278,25 @@ async def gibson_assembly(
|
|
|
457
278
|
async def restriction_and_ligation(
|
|
458
279
|
source: RestrictionAndLigationSource,
|
|
459
280
|
sequences: Annotated[list[TextFileSequence], Field(min_length=1)],
|
|
460
|
-
allow_partial_overlap: bool = Query(False, description='Allow for partially overlapping sticky ends.'),
|
|
461
281
|
circular_only: bool = Query(False, description='Only return circular assemblies.'),
|
|
462
282
|
):
|
|
463
283
|
|
|
464
284
|
fragments = [read_dsrecord_from_json(seq) for seq in sequences]
|
|
465
|
-
|
|
466
|
-
if
|
|
467
|
-
raise HTTPException(404, 'These enzymes do not exist: ' + ', '.join(invalid_enzymes))
|
|
468
|
-
enzymes = RestrictionBatch(first=[e for e in source.restriction_enzymes if e is not None])
|
|
469
|
-
|
|
470
|
-
# Lambda function for code clarity
|
|
471
|
-
def create_source(a, is_circular):
|
|
472
|
-
return RestrictionAndLigationSource.from_assembly(
|
|
473
|
-
assembly=a,
|
|
474
|
-
circular=is_circular,
|
|
475
|
-
id=source.id,
|
|
476
|
-
restriction_enzymes=source.restriction_enzymes,
|
|
477
|
-
fragments=fragments,
|
|
478
|
-
)
|
|
479
|
-
|
|
480
|
-
# Algorithm used by assembly class
|
|
481
|
-
def algo(x, y, _l):
|
|
482
|
-
# By default, we allow blunt ends
|
|
483
|
-
return restriction_ligation_overlap(x, y, enzymes, allow_partial_overlap, True)
|
|
484
|
-
|
|
485
|
-
resp = generate_assemblies(source, create_source, fragments, circular_only, algo, True)
|
|
285
|
+
enzymes = parse_restriction_enzymes(source.restriction_enzymes)
|
|
286
|
+
completed_source = source if is_assembly_complete(source) else None
|
|
486
287
|
|
|
487
|
-
|
|
488
|
-
|
|
288
|
+
try:
|
|
289
|
+
products = _restriction_ligation_assembly(fragments, enzymes, circular_only=circular_only)
|
|
290
|
+
except ValueError as e:
|
|
291
|
+
raise HTTPException(400, *e.args)
|
|
489
292
|
|
|
490
|
-
return
|
|
293
|
+
return format_products(
|
|
294
|
+
source.id,
|
|
295
|
+
products,
|
|
296
|
+
completed_source,
|
|
297
|
+
source.output_name,
|
|
298
|
+
no_products_error_message='No compatible restriction-ligation was found.',
|
|
299
|
+
)
|
|
491
300
|
|
|
492
301
|
|
|
493
302
|
@router.post(
|
|
@@ -506,50 +315,22 @@ async def gateway(
|
|
|
506
315
|
):
|
|
507
316
|
|
|
508
317
|
fragments = [read_dsrecord_from_json(seq) for seq in sequences]
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
# Lambda function for code clarity
|
|
512
|
-
def create_source(a, is_circular):
|
|
513
|
-
return GatewaySource.from_assembly(
|
|
514
|
-
assembly=a,
|
|
515
|
-
circular=is_circular,
|
|
516
|
-
id=source.id,
|
|
517
|
-
reaction_type=source.reaction_type,
|
|
518
|
-
fragments=fragments,
|
|
519
|
-
)
|
|
318
|
+
completed_source = source if is_assembly_complete(source) else None
|
|
520
319
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
def annotate(x):
|
|
527
|
-
return annotate_gateway_sites(x, greedy)
|
|
528
|
-
|
|
529
|
-
resp = generate_assemblies(source, create_source, fragments, circular_only, algo, False, product_callback=annotate)
|
|
530
|
-
|
|
531
|
-
if len(resp['sources']) == 0:
|
|
532
|
-
# Build a list of all the sites in the fragments
|
|
533
|
-
sites_in_fragments = list()
|
|
534
|
-
for frag in fragments:
|
|
535
|
-
sites_in_fragments.append(list(find_gateway_sites(frag, greedy).keys()))
|
|
536
|
-
formatted_strings = [f'fragment {i + 1}: {", ".join(sites)}' for i, sites in enumerate(sites_in_fragments)]
|
|
537
|
-
raise HTTPException(
|
|
538
|
-
400,
|
|
539
|
-
f'Inputs are not compatible for {source.reaction_type} reaction.\n\n' + '\n'.join(formatted_strings),
|
|
540
|
-
)
|
|
320
|
+
try:
|
|
321
|
+
products = _gateway_assembly(fragments, source.reaction_type, source.greedy, circular_only, only_multi_site)
|
|
322
|
+
except ValueError as e:
|
|
323
|
+
raise HTTPException(400, *e.args)
|
|
541
324
|
|
|
542
|
-
|
|
543
|
-
multi_site_sources = [
|
|
544
|
-
i
|
|
545
|
-
for i, s in enumerate(resp['sources'])
|
|
546
|
-
if all(join.left_location != join.right_location for join in s.input)
|
|
547
|
-
]
|
|
548
|
-
sources = [resp['sources'][i] for i in multi_site_sources]
|
|
549
|
-
sequences = [resp['sequences'][i] for i in multi_site_sources]
|
|
550
|
-
return {'sources': sources, 'sequences': sequences}
|
|
325
|
+
products = [annotate_gateway_sites(p, source.greedy) for p in products]
|
|
551
326
|
|
|
552
|
-
return
|
|
327
|
+
return format_products(
|
|
328
|
+
source.id,
|
|
329
|
+
products,
|
|
330
|
+
completed_source,
|
|
331
|
+
source.output_name,
|
|
332
|
+
no_products_error_message=None, # Already handled by the _gateway_assembly function
|
|
333
|
+
)
|
|
553
334
|
|
|
554
335
|
|
|
555
336
|
@router.post(
|
|
@@ -564,25 +345,23 @@ async def cre_lox_recombination(
|
|
|
564
345
|
source: CreLoxRecombinationSource, sequences: Annotated[list[TextFileSequence], Field(min_length=1)]
|
|
565
346
|
):
|
|
566
347
|
fragments = [read_dsrecord_from_json(seq) for seq in sequences]
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
348
|
+
completed_source = source if is_assembly_complete(source) else None
|
|
349
|
+
|
|
350
|
+
if len(fragments) == 1:
|
|
351
|
+
products = _cre_lox_excision(fragments[0])
|
|
352
|
+
else:
|
|
353
|
+
products = []
|
|
354
|
+
if not fragments[0].circular:
|
|
355
|
+
products.extend(_cre_lox_integration(fragments[0], fragments[1:]))
|
|
356
|
+
if not fragments[1].circular:
|
|
357
|
+
products.extend(_cre_lox_integration(fragments[1], fragments[:1]))
|
|
358
|
+
|
|
359
|
+
products = [annotate_loxP_sites(p) for p in products]
|
|
360
|
+
|
|
361
|
+
return format_products(
|
|
362
|
+
source.id,
|
|
363
|
+
products,
|
|
364
|
+
completed_source,
|
|
365
|
+
source.output_name,
|
|
366
|
+
no_products_error_message='No compatible Cre/Lox recombination was found.',
|
|
583
367
|
)
|
|
584
|
-
|
|
585
|
-
if len(resp['sources']) == 0:
|
|
586
|
-
raise HTTPException(400, 'No compatible Cre/Lox recombination was found.')
|
|
587
|
-
|
|
588
|
-
return resp
|