opencloning 0.3.6__tar.gz → 0.3.7__tar.gz
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-0.3.6 → opencloning-0.3.7}/PKG-INFO +1 -1
- {opencloning-0.3.6 → opencloning-0.3.7}/pyproject.toml +1 -1
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/assembly2.py +127 -6
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/dna_utils.py +13 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/endpoints/assembly.py +9 -2
- {opencloning-0.3.6 → opencloning-0.3.7}/LICENSE +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/README.md +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/__init__.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/_version.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/api_config_utils.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/app_settings.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/__init__.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/barcode.gb +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/common_plasmid.gb +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/example.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/primer_design_settings.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/__init__.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/index.html +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/__init__.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/index.html +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_all.sh +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_clone.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_gather.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_get_primers.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_summary.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/ziqiang_et_al2024/__init__.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/ziqiang_et_al2024/index.html +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/ziqiang_et_al2024/ziqiang_et_al2024.json +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/bug_fixing/README.md +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/bug_fixing/__init__.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/bug_fixing/backend_v0_3.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/cre_lox.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/dna_functions.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/ebic/__init__.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/ebic/primer_design.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/ebic/primer_design_settings.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/endpoints/annotation.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/endpoints/external_import.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/endpoints/no_assembly.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/endpoints/no_input.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/endpoints/other.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/endpoints/primer_design.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/gateway.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/get_router.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/http_client.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/main.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/ncbi_requests.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/primer_design.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/pydantic_models.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/request_examples.py +0 -0
- {opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/utils.py +0 -0
|
@@ -8,7 +8,7 @@ authors = ["Manuel Lera-Ramirez <manulera14@gmail.com>"]
|
|
|
8
8
|
description = "Backend of OpenCloning, a web application to generate molecular cloning strategies in json format, and share them with others."
|
|
9
9
|
license = "MIT"
|
|
10
10
|
name = "opencloning"
|
|
11
|
-
version = "0.3.
|
|
11
|
+
version = "0.3.7"
|
|
12
12
|
package-mode = true
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
repository = "https://github.com/manulera/OpenCloning_backend"
|
|
@@ -15,7 +15,7 @@ from pydna.seqrecord import SeqRecord as _SeqRecord
|
|
|
15
15
|
import networkx as _nx
|
|
16
16
|
import itertools as _itertools
|
|
17
17
|
from Bio.SeqFeature import SimpleLocation, Location
|
|
18
|
-
from .dna_utils import sum_is_sticky
|
|
18
|
+
from .dna_utils import sum_is_sticky, create_location
|
|
19
19
|
from Bio.Seq import reverse_complement
|
|
20
20
|
from Bio.Restriction.Restriction import RestrictionBatch, AbstractCut
|
|
21
21
|
import regex
|
|
@@ -156,7 +156,55 @@ def blunt_overlap(seqx: _Dseqrecord, seqy: _Dseqrecord, limit=None):
|
|
|
156
156
|
|
|
157
157
|
|
|
158
158
|
def common_sub_strings(seqx: _Dseqrecord, seqy: _Dseqrecord, limit=25):
|
|
159
|
-
|
|
159
|
+
query_seqx = str(seqx.seq).upper()
|
|
160
|
+
query_seqy = str(seqy.seq).upper()
|
|
161
|
+
if seqx.circular:
|
|
162
|
+
query_seqx = query_seqx * 2
|
|
163
|
+
if seqy.circular:
|
|
164
|
+
query_seqy = query_seqy * 2
|
|
165
|
+
results = common_sub_strings_str(query_seqx, query_seqy, limit)
|
|
166
|
+
|
|
167
|
+
if not seqx.circular and not seqy.circular:
|
|
168
|
+
return results
|
|
169
|
+
|
|
170
|
+
# Remove matches that start on the second copy of the sequence
|
|
171
|
+
if seqx.circular:
|
|
172
|
+
results = [r for r in results if r[0] < len(seqx)]
|
|
173
|
+
if seqy.circular:
|
|
174
|
+
results = [r for r in results if r[1] < len(seqy)]
|
|
175
|
+
|
|
176
|
+
# Trim lengths that span more than the sequence
|
|
177
|
+
if seqx.circular or seqy.circular:
|
|
178
|
+
max_match_length = min(len(seqx), len(seqy))
|
|
179
|
+
results = [(r[0], r[1], min(r[2], max_match_length)) for r in results]
|
|
180
|
+
|
|
181
|
+
# Edge case where the sequences are identical
|
|
182
|
+
if len(seqx.seq) == len(seqy.seq):
|
|
183
|
+
full_match = next((r for r in results if r[2] == len(seqx.seq)), None)
|
|
184
|
+
if full_match is not None:
|
|
185
|
+
return [full_match]
|
|
186
|
+
|
|
187
|
+
# Remove duplicate matches, see example below
|
|
188
|
+
# Let's imagine the following two sequences, where either seqy or both are circular
|
|
189
|
+
# seqx: 01234
|
|
190
|
+
# seqy: 123450, circular
|
|
191
|
+
#
|
|
192
|
+
# common_sub_strings would return [(0, 5, 5), (1, 0, 4)]
|
|
193
|
+
# Actually, (1, 0, 4) is a subset of (0, 5, 5), the part
|
|
194
|
+
# that does not span the origin. To remove matches like this,
|
|
195
|
+
# We find matches where the origin is spanned in one of the sequences
|
|
196
|
+
# only, and then remove the subset of that match that does not span the origin.
|
|
197
|
+
shifted_matches = set()
|
|
198
|
+
for x, y, length in results:
|
|
199
|
+
x_span_origin = seqx.circular and x + length > len(seqx)
|
|
200
|
+
y_span_origin = seqy.circular and y + length > len(seqy)
|
|
201
|
+
if x_span_origin and not y_span_origin:
|
|
202
|
+
shift = len(seqx) - x
|
|
203
|
+
shifted_matches.add((0, y + shift, length - shift))
|
|
204
|
+
elif not x_span_origin and y_span_origin:
|
|
205
|
+
shift = len(seqy) - y
|
|
206
|
+
shifted_matches.add((x + shift, 0, length - shift))
|
|
207
|
+
return [r for r in results if r not in shifted_matches]
|
|
160
208
|
|
|
161
209
|
|
|
162
210
|
def gibson_overlap(seqx: _Dseqrecord, seqy: _Dseqrecord, limit=25):
|
|
@@ -1042,16 +1090,25 @@ class Assembly:
|
|
|
1042
1090
|
edge_pair_index = list()
|
|
1043
1091
|
|
|
1044
1092
|
# Pair edges with one another
|
|
1045
|
-
for i, ((_u1, v1, _,
|
|
1093
|
+
for i, ((_u1, v1, _, end_location), (_u2, _v2, start_location, _)) in enumerate(
|
|
1046
1094
|
zip(assembly, assembly[1:] + assembly[:1])
|
|
1047
1095
|
):
|
|
1048
1096
|
fragment = self.fragments[abs(v1) - 1]
|
|
1049
1097
|
# Find the pair of edges that should be last and first ((3, 1, [8:10], [9:11)]), (1, 2, [4:6], [0:2]) in
|
|
1050
1098
|
# the example above. Only one of the pairs of edges should satisfy this condition for the topology to make sense.
|
|
1051
|
-
|
|
1052
|
-
|
|
1099
|
+
left_of_insertion = _location_boundaries(start_location)[0]
|
|
1100
|
+
right_of_insertion = _location_boundaries(end_location)[0]
|
|
1053
1101
|
if not fragment.circular and (
|
|
1054
|
-
right_of_insertion
|
|
1102
|
+
right_of_insertion >= left_of_insertion
|
|
1103
|
+
# The below condition is for single-site integration.
|
|
1104
|
+
# The reason to use locations_overlap instead of equality is because the location might extend
|
|
1105
|
+
# left of right. For example, let's take ACCGGTTT as homology arm for an integration:
|
|
1106
|
+
#
|
|
1107
|
+
# insert aaACCGGTTTccACCGGTTTtt
|
|
1108
|
+
# genome aaACCGGTTTtt
|
|
1109
|
+
#
|
|
1110
|
+
# The locations of homology on the genome are [0:10] and [2:12], so not identical
|
|
1111
|
+
# but they overlap.
|
|
1055
1112
|
or _locations_overlap(start_location, end_location, len(fragment))
|
|
1056
1113
|
):
|
|
1057
1114
|
edge_pair_index.append(i)
|
|
@@ -1062,6 +1119,68 @@ class Assembly:
|
|
|
1062
1119
|
shift_by = (edge_pair_index[0] + 1) % len(assembly)
|
|
1063
1120
|
return assembly[shift_by:] + assembly[:shift_by]
|
|
1064
1121
|
|
|
1122
|
+
def format_insertion_assembly_edge_case(self, assembly):
|
|
1123
|
+
"""
|
|
1124
|
+
Edge case from https://github.com/manulera/OpenCloning_backend/issues/329
|
|
1125
|
+
"""
|
|
1126
|
+
same_assembly = assembly[:]
|
|
1127
|
+
|
|
1128
|
+
if len(assembly) != 2:
|
|
1129
|
+
return same_assembly
|
|
1130
|
+
((f1, f2, loc_f1_1, loc_f2_1), (_f2, _f1, loc_f2_2, loc_f1_2)) = assembly
|
|
1131
|
+
|
|
1132
|
+
if f1 != _f1 or _f2 != f2:
|
|
1133
|
+
return same_assembly
|
|
1134
|
+
|
|
1135
|
+
if loc_f2_1 == loc_f2_2 or loc_f1_2 == loc_f1_1:
|
|
1136
|
+
return same_assembly
|
|
1137
|
+
|
|
1138
|
+
fragment1 = self.fragments[abs(f1) - 1]
|
|
1139
|
+
fragment2 = self.fragments[abs(f2) - 1]
|
|
1140
|
+
|
|
1141
|
+
if not _locations_overlap(loc_f1_1, loc_f1_2, len(fragment1)) or not _locations_overlap(
|
|
1142
|
+
loc_f2_2, loc_f2_1, len(fragment2)
|
|
1143
|
+
):
|
|
1144
|
+
return same_assembly
|
|
1145
|
+
|
|
1146
|
+
# Sort to make compatible with insertion assembly
|
|
1147
|
+
if _location_boundaries(loc_f1_1)[0] > _location_boundaries(loc_f1_2)[0]:
|
|
1148
|
+
new_assembly = same_assembly[::-1]
|
|
1149
|
+
else:
|
|
1150
|
+
new_assembly = same_assembly[:]
|
|
1151
|
+
|
|
1152
|
+
((f1, f2, loc_f1_1, loc_f2_1), (_f2, _f1, loc_f2_2, loc_f1_2)) = new_assembly
|
|
1153
|
+
|
|
1154
|
+
fragment1 = self.fragments[abs(f1) - 1]
|
|
1155
|
+
if fragment1.circular:
|
|
1156
|
+
return same_assembly
|
|
1157
|
+
fragment2 = self.fragments[abs(f2) - 1]
|
|
1158
|
+
|
|
1159
|
+
# Extract boundaries
|
|
1160
|
+
f2_1_start, _ = _location_boundaries(loc_f2_1)
|
|
1161
|
+
f2_2_start, f2_2_end = _location_boundaries(loc_f2_2)
|
|
1162
|
+
f1_1_start, _ = _location_boundaries(loc_f1_1)
|
|
1163
|
+
f1_2_start, f1_2_end = _location_boundaries(loc_f1_2)
|
|
1164
|
+
|
|
1165
|
+
overlap_diff = len(fragment1[f1_1_start:f1_2_end]) - len(fragment2[f2_1_start:f2_2_end])
|
|
1166
|
+
|
|
1167
|
+
if overlap_diff == 0:
|
|
1168
|
+
assert False, 'Overlap is 0'
|
|
1169
|
+
|
|
1170
|
+
if overlap_diff > 0:
|
|
1171
|
+
new_loc_f1_1 = create_location(f1_1_start, f1_2_start - overlap_diff, len(fragment1))
|
|
1172
|
+
new_loc_f2_1 = create_location(f2_1_start, f2_2_start, len(fragment2))
|
|
1173
|
+
else:
|
|
1174
|
+
new_loc_f2_1 = create_location(f2_1_start, f2_2_start + overlap_diff, len(fragment2))
|
|
1175
|
+
new_loc_f1_1 = create_location(f1_1_start, f1_2_start, len(fragment1))
|
|
1176
|
+
|
|
1177
|
+
new_assembly = [
|
|
1178
|
+
(f1, f2, new_loc_f1_1, new_loc_f2_1),
|
|
1179
|
+
new_assembly[1],
|
|
1180
|
+
]
|
|
1181
|
+
|
|
1182
|
+
return new_assembly
|
|
1183
|
+
|
|
1065
1184
|
def get_insertion_assemblies(self, only_adjacent_edges: bool = False, max_assemblies: int = 50):
|
|
1066
1185
|
"""Assemblies that represent the insertion of a fragment or series of fragment inside a linear construct. For instance,
|
|
1067
1186
|
digesting CCCCGAATTCCCCGAATTC with EcoRI and inserting the fragment with two overhangs into the EcoRI site of AAAGAATTCAAA.
|
|
@@ -1088,6 +1207,8 @@ class Assembly:
|
|
|
1088
1207
|
# We find cycles first
|
|
1089
1208
|
iterator = limit_iterator(_nx.cycles.simple_cycles(self.G), 10000)
|
|
1090
1209
|
assemblies = sum(map(lambda x: self.node_path2assembly_list(x, True), iterator), [])
|
|
1210
|
+
# We format the edge case
|
|
1211
|
+
assemblies = [self.format_insertion_assembly_edge_case(a) for a in assemblies]
|
|
1091
1212
|
# We select those that contain exactly only one suitable edge
|
|
1092
1213
|
assemblies = [b for a in assemblies if (b := self.format_insertion_assembly(a)) is not None]
|
|
1093
1214
|
# First fragment should be in the + orientation
|
|
@@ -13,6 +13,8 @@ from pydna.parsers import parse
|
|
|
13
13
|
from Bio.Align import PairwiseAligner
|
|
14
14
|
from Bio.Data.IUPACData import ambiguous_dna_values as _ambiguous_dna_values
|
|
15
15
|
import re
|
|
16
|
+
from Bio.SeqFeature import Location, SimpleLocation
|
|
17
|
+
from pydna.utils import shift_location
|
|
16
18
|
|
|
17
19
|
aligner = PairwiseAligner(scoring='blastn')
|
|
18
20
|
|
|
@@ -167,3 +169,14 @@ def dseqrecord_finditer(pattern: str, seq: Dseqrecord) -> list[re.Match]:
|
|
|
167
169
|
query = str(seq.seq) if not seq.circular else str(seq.seq) * 2
|
|
168
170
|
matches = re.finditer(pattern, query)
|
|
169
171
|
return (m for m in matches if m.start() <= len(seq))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def create_location(start: int, end: int, lim: int) -> Location:
|
|
175
|
+
while start < 0:
|
|
176
|
+
start += lim
|
|
177
|
+
while end < 0:
|
|
178
|
+
end += lim
|
|
179
|
+
if end > start:
|
|
180
|
+
return SimpleLocation(start, end)
|
|
181
|
+
else:
|
|
182
|
+
return shift_location(SimpleLocation(start, end + lim), 0, lim)
|
|
@@ -90,6 +90,9 @@ async def crispr(
|
|
|
90
90
|
"""
|
|
91
91
|
template, insert = [read_dsrecord_from_json(seq) for seq in sequences]
|
|
92
92
|
|
|
93
|
+
if template.circular:
|
|
94
|
+
raise HTTPException(400, 'Circular DNA targets are not supported for CRISPR editing.')
|
|
95
|
+
|
|
93
96
|
# TODO: check input method for guide (currently as a primer)
|
|
94
97
|
# TODO: support user input PAM
|
|
95
98
|
|
|
@@ -150,7 +153,8 @@ async def crispr(
|
|
|
150
153
|
return format_known_assembly_response(source, out_sources, [template, insert])
|
|
151
154
|
|
|
152
155
|
out_sequences = [
|
|
153
|
-
format_sequence_genbank(assemble([template, insert], a), source.output_name)
|
|
156
|
+
format_sequence_genbank(assemble([template, insert], a, is_insertion=True), source.output_name)
|
|
157
|
+
for a in valid_assemblies
|
|
154
158
|
]
|
|
155
159
|
return {'sources': out_sources, 'sequences': out_sequences}
|
|
156
160
|
|
|
@@ -386,7 +390,10 @@ async def homologous_recombination(
|
|
|
386
390
|
return format_known_assembly_response(source, out_sources, [template, insert])
|
|
387
391
|
|
|
388
392
|
out_sequences = [
|
|
389
|
-
format_sequence_genbank(
|
|
393
|
+
format_sequence_genbank(
|
|
394
|
+
assemble([template, insert], a, is_insertion=not template.circular), source.output_name
|
|
395
|
+
)
|
|
396
|
+
for a in possible_assemblies
|
|
390
397
|
]
|
|
391
398
|
|
|
392
399
|
return {'sources': out_sources, 'sequences': out_sequences}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/common_plasmid.gb
RENAMED
|
File without changes
|
|
File without changes
|
{opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/primer_design_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_get_primers.py
RENAMED
|
File without changes
|
{opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_summary.py
RENAMED
|
File without changes
|
{opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/ziqiang_et_al2024/__init__.py
RENAMED
|
File without changes
|
{opencloning-0.3.6 → opencloning-0.3.7}/src/opencloning/batch_cloning/ziqiang_et_al2024/index.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|