opencloning 0.3.5__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.
Files changed (51) hide show
  1. {opencloning-0.3.5 → opencloning-0.3.7}/PKG-INFO +1 -1
  2. {opencloning-0.3.5 → opencloning-0.3.7}/pyproject.toml +1 -1
  3. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/assembly2.py +127 -6
  4. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/dna_utils.py +13 -0
  5. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/endpoints/assembly.py +9 -2
  6. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/http_client.py +1 -1
  7. {opencloning-0.3.5 → opencloning-0.3.7}/LICENSE +0 -0
  8. {opencloning-0.3.5 → opencloning-0.3.7}/README.md +0 -0
  9. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/__init__.py +0 -0
  10. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/_version.py +0 -0
  11. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/api_config_utils.py +0 -0
  12. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/app_settings.py +0 -0
  13. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/__init__.py +0 -0
  14. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/barcode.gb +0 -0
  15. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/common_plasmid.gb +0 -0
  16. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/example.py +0 -0
  17. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/EBIC/primer_design_settings.py +0 -0
  18. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/__init__.py +0 -0
  19. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/index.html +0 -0
  20. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/__init__.py +0 -0
  21. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/index.html +0 -0
  22. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_all.sh +0 -0
  23. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_clone.py +0 -0
  24. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_gather.py +0 -0
  25. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_get_primers.py +0 -0
  26. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/pombe/pombe_summary.py +0 -0
  27. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/ziqiang_et_al2024/__init__.py +0 -0
  28. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/ziqiang_et_al2024/index.html +0 -0
  29. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/batch_cloning/ziqiang_et_al2024/ziqiang_et_al2024.json +0 -0
  30. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/bug_fixing/README.md +0 -0
  31. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/bug_fixing/__init__.py +0 -0
  32. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/bug_fixing/backend_v0_3.py +0 -0
  33. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/cre_lox.py +0 -0
  34. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/dna_functions.py +0 -0
  35. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/ebic/__init__.py +0 -0
  36. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/ebic/primer_design.py +0 -0
  37. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/ebic/primer_design_settings.py +0 -0
  38. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/endpoints/annotation.py +0 -0
  39. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/endpoints/external_import.py +0 -0
  40. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/endpoints/no_assembly.py +0 -0
  41. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/endpoints/no_input.py +0 -0
  42. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/endpoints/other.py +0 -0
  43. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/endpoints/primer_design.py +0 -0
  44. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/gateway.py +0 -0
  45. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/get_router.py +0 -0
  46. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/main.py +0 -0
  47. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/ncbi_requests.py +0 -0
  48. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/primer_design.py +0 -0
  49. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/pydantic_models.py +0 -0
  50. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/request_examples.py +0 -0
  51. {opencloning-0.3.5 → opencloning-0.3.7}/src/opencloning/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: opencloning
3
- Version: 0.3.5
3
+ Version: 0.3.7
4
4
  Summary: Backend of OpenCloning, a web application to generate molecular cloning strategies in json format, and share them with others.
5
5
  License: MIT
6
6
  Author: Manuel Lera-Ramirez
@@ -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.5"
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
- return common_sub_strings_str(str(seqx.seq).upper(), str(seqy.seq).upper(), limit)
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, _, start_location), (_u2, _v2, end_location, _)) in enumerate(
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
- right_of_insertion = _location_boundaries(start_location)[0]
1052
- left_of_insertion = _location_boundaries(end_location)[0]
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 > left_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) for a in valid_assemblies
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(assemble([template, insert], a), source.output_name) for a in possible_assemblies
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}
@@ -22,7 +22,7 @@ white_listed_urls = {
22
22
  r'^https://eutils.ncbi.nlm.nih.gov/entrez/eutils/',
23
23
  r'^https://www.snapgene.com/local/fetch.php',
24
24
  r'^https://benchling.com/',
25
- r'^https://raw.githubusercontent.com/manulera/annotated-igem-distribution',
25
+ r'^https://assets.opencloning.org/annotated-igem-distribution',
26
26
  r'^http://www.euroscarf.de/',
27
27
  }
28
28
 
File without changes
File without changes