opencloning 0.4.8__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.
Files changed (37) hide show
  1. opencloning/app_settings.py +7 -0
  2. opencloning/batch_cloning/pombe/__init__.py +2 -2
  3. opencloning/batch_cloning/pombe/pombe_clone.py +31 -112
  4. opencloning/batch_cloning/pombe/pombe_summary.py +20 -8
  5. opencloning/batch_cloning/ziqiang_et_al2024/__init__.py +8 -8
  6. opencloning/batch_cloning/ziqiang_et_al2024/ziqiang_et_al2024.json +2 -9
  7. opencloning/bug_fixing/backend_v0_3.py +13 -5
  8. opencloning/catalogs/__init__.py +36 -0
  9. opencloning/catalogs/igem2024.yaml +2172 -0
  10. opencloning/catalogs/openDNA_collections.yaml +1161 -0
  11. opencloning/catalogs/readme.txt +1 -0
  12. opencloning/catalogs/seva.tsv +231 -0
  13. opencloning/catalogs/snapgene.yaml +2837 -0
  14. opencloning/dna_functions.py +155 -158
  15. opencloning/dna_utils.py +45 -62
  16. opencloning/ebic/primer_design.py +1 -1
  17. opencloning/endpoints/annotation.py +9 -13
  18. opencloning/endpoints/assembly.py +157 -378
  19. opencloning/endpoints/endpoint_utils.py +52 -0
  20. opencloning/endpoints/external_import.py +169 -124
  21. opencloning/endpoints/no_assembly.py +23 -39
  22. opencloning/endpoints/no_input.py +32 -47
  23. opencloning/endpoints/other.py +1 -1
  24. opencloning/endpoints/primer_design.py +2 -1
  25. opencloning/http_client.py +2 -2
  26. opencloning/ncbi_requests.py +113 -47
  27. opencloning/primer_design.py +1 -1
  28. opencloning/pydantic_models.py +10 -510
  29. opencloning/request_examples.py +10 -22
  30. opencloning/temp_functions.py +50 -0
  31. {opencloning-0.4.8.dist-info → opencloning-0.5.dist-info}/METADATA +18 -8
  32. opencloning-0.5.dist-info/RECORD +51 -0
  33. {opencloning-0.4.8.dist-info → opencloning-0.5.dist-info}/WHEEL +1 -1
  34. opencloning/cre_lox.py +0 -116
  35. opencloning/gateway.py +0 -154
  36. opencloning-0.4.8.dist-info/RECORD +0 -45
  37. {opencloning-0.4.8.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, Literal, Callable
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 Bio.Restriction.Restriction import RestrictionBatch
9
- from opencloning.cre_lox import cre_loxP_overlap, annotate_loxP_sites
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
- from ..pydantic_models import (
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
- RestrictionAndLigationSource,
25
- AssemblySource,
21
+ InVivoAssemblySource,
26
22
  OverlapExtensionPCRLigationSource,
23
+ HomologousRecombinationSource,
24
+ RestrictionAndLigationSource,
27
25
  GatewaySource,
28
- CreLoxRecombinationSource,
29
- InVivoAssemblySource,
26
+ Primer as PrimerModel,
27
+ TextFileSequence,
30
28
  )
29
+
31
30
  from pydna.assembly2 import (
32
- Assembly,
33
- assemble,
34
- sticky_end_sub_strings,
35
- PCRAssembly,
36
- gibson_overlap,
37
- filter_linear_subassemblies,
38
- restriction_ligation_overlap,
39
- SingleFragmentAssembly,
40
- blunt_overlap,
41
- combine_algorithms,
42
- annotate_primer_binding_sites,
43
- common_sub_strings,
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 ..gateway import gateway_overlap, find_gateway_sites, annotate_gateway_sites
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 template.circular:
95
- raise HTTPException(400, 'Circular DNA targets are not supported for CRISPR editing.')
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
- out_sources = []
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
- # If a specific assembly is requested
209
- if source.is_assembly_complete():
210
- return format_known_assembly_response(source, out_sources, fragments, product_callback)
211
-
212
- out_sequences = [
213
- format_sequence_genbank(
214
- product_callback(assemble(fragments, s.get_assembly_plan(fragments))), source.output_name
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 features without length)
244
- if source.is_assembly_complete():
245
- asm = source.get_assembly_plan(fragments)
246
- blunt = len(asm[0][2]) == 0
247
-
248
- algo = combine_algorithms(blunt_overlap, sticky_end_sub_strings) if blunt else sticky_end_sub_strings
249
- resp = generate_assemblies(
250
- source, create_source, fragments, circular_only, algo, False, {'limit': allow_partial_overlap}
251
- )
252
- if len(resp['sources']) == 0:
253
- raise HTTPException(400, 'No ligations were found.')
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 resp
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=1, max_length=2)],
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 source.is_assembly_complete():
287
- minimal_annealing = source.minimal_overlap()
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
- possible_assemblies = asm.get_linear_assemblies()
304
- except ValueError as e:
305
- raise HTTPException(400, *e.args)
306
-
307
- # Edge case: where both primers are identical, remove
308
- # duplicate assemblies that represent just reverse complement
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
- for a in possible_assemblies
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 {'sources': out_sources, 'sequences': out_sequences}
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 source.is_assembly_complete():
370
- minimal_homology = source.minimal_overlap()
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 not template.circular:
377
- possible_assemblies = [a for a in asm.get_insertion_assemblies() if a[0][0] == 1]
204
+ if template.circular:
205
+ products = _in_vivo_assembly([template, insert], minimal_homology, circular_only=True)
378
206
  else:
379
- possible_assemblies = [a for a in asm.get_circular_assemblies()]
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
- if len(possible_assemblies) == 0:
385
- raise HTTPException(400, 'No homologous recombination was found.')
386
-
387
- out_sources = [
388
- HomologousRecombinationSource.from_assembly(
389
- id=source.id, assembly=a, circular=False, fragments=[template, insert]
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
- # Lambda function for code clarity
431
- def create_source(a, is_circular):
432
- return source.__class__.from_assembly(assembly=a, circular=is_circular, id=source.id, fragments=fragments)
255
+ try:
256
+ products = function2use(fragments, minimal_homology, circular_only)
257
+ except ValueError as e:
258
+ raise HTTPException(400, *e.args)
433
259
 
434
- algo = gibson_overlap if not isinstance(source, InVivoAssemblySource) else common_sub_strings
435
- resp = generate_assemblies(
436
- source, create_source, fragments, circular_only, algo, False, {'limit': minimal_homology}
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
- invalid_enzymes = get_invalid_enzyme_names(source.restriction_enzymes)
466
- if len(invalid_enzymes):
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
- if len(resp['sources']) == 0:
488
- raise HTTPException(400, 'No compatible restriction-ligation was found.')
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 resp
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
- greedy = source.greedy
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
- # Algorithm used by assembly class
522
- def algo(x, y, _l):
523
- # By default, we allow blunt ends
524
- return gateway_overlap(x, y, source.reaction_type, greedy)
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
- if only_multi_site:
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 resp
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
- # Lambda function for code clarity
569
- def create_source(a, is_circular):
570
- return CreLoxRecombinationSource.from_assembly(
571
- assembly=a, circular=is_circular, id=source.id, fragments=fragments
572
- )
573
-
574
- resp = generate_assemblies(
575
- source,
576
- create_source,
577
- fragments,
578
- False,
579
- cre_loxP_overlap,
580
- True,
581
- recombination_mode=True,
582
- product_callback=annotate_loxP_sites,
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