opencloning 0.2.7.3__py3-none-any.whl → 0.2.8.1__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/cre_lox.py ADDED
@@ -0,0 +1,58 @@
1
+ from itertools import product
2
+ from pydna.dseqrecord import Dseqrecord
3
+ from Bio.Data.IUPACData import ambiguous_dna_values
4
+ from Bio.Seq import reverse_complement
5
+ from .dna_utils import compute_regex_site, dseqrecord_finditer
6
+
7
+ # We create a dictionary to map ambiguous bases to their consensus base
8
+ # For example, ambigous_base_dict['ACGT'] -> 'N'
9
+ ambiguous_base_dict = {}
10
+ for ambiguous, bases in ambiguous_dna_values.items():
11
+ ambiguous_base_dict[''.join(sorted(bases))] = ambiguous
12
+
13
+ # To handle N values
14
+ ambiguous_base_dict['N'] = 'N'
15
+
16
+ # This is the original loxP sequence, here for reference
17
+ LOXP_SEQUENCE = 'ATAACTTCGTATAGCATACATTATACGAAGTTAT'
18
+
19
+ loxP_sequences = [
20
+ # https://blog.addgene.org/plasmids-101-cre-lox
21
+ # loxP
22
+ 'ATAACTTCGTATANNNTANNNTATACGAAGTTAT',
23
+ # PMID:12202778
24
+ # lox66
25
+ 'ATAACTTCGTATANNNTANNNTATACGAACGGTA',
26
+ # lox71
27
+ 'TACCGTTCGTATANNNTANNNTATACGAAGTTAT',
28
+ ]
29
+
30
+ loxP_consensus = ''
31
+
32
+ for pos in range(len(LOXP_SEQUENCE)):
33
+ all_letters = set(seq[pos] for seq in loxP_sequences)
34
+ key = ''.join(sorted(all_letters))
35
+ loxP_consensus += ambiguous_base_dict[key]
36
+
37
+ # We compute the regex for the forward and reverse loxP sequences
38
+ loxP_regex = (compute_regex_site(loxP_consensus), compute_regex_site(reverse_complement(loxP_consensus)))
39
+
40
+
41
+ def cre_loxP_overlap(x: Dseqrecord, y: Dseqrecord, _l: None = None) -> list[tuple[int, int, int]]:
42
+ """Find matching loxP sites between two sequences."""
43
+ out = list()
44
+ for pattern in loxP_regex:
45
+ matches_x = dseqrecord_finditer(pattern, x)
46
+ matches_y = dseqrecord_finditer(pattern, y)
47
+
48
+ for match_x, match_y in product(matches_x, matches_y):
49
+ value_x = match_x.group()
50
+ value_y = match_y.group()
51
+ if value_x[13:21] == value_y[13:21]:
52
+ out.append((match_x.start() + 13, match_y.start() + 13, 8))
53
+ # Unique values (keeping the order)
54
+ unique_out = []
55
+ for item in out:
56
+ if item not in unique_out:
57
+ unique_out.append(item)
58
+ return unique_out
opencloning/dna_utils.py CHANGED
@@ -11,9 +11,15 @@ import os
11
11
  import shutil
12
12
  from pydna.parsers import parse
13
13
  from Bio.Align import PairwiseAligner
14
+ from Bio.Data.IUPACData import ambiguous_dna_values as _ambiguous_dna_values
15
+ import re
14
16
 
15
17
  aligner = PairwiseAligner(scoring='blastn')
16
18
 
19
+ ambiguous_only_dna_values = {**_ambiguous_dna_values}
20
+ for normal_base in 'ACGT':
21
+ del ambiguous_only_dna_values[normal_base]
22
+
17
23
 
18
24
  def sum_is_sticky(three_prime_end: tuple[str, str], five_prime_end: tuple[str, str], partial: bool = False) -> int:
19
25
  """Return the overlap length if the 3' end of seq1 and 5' end of seq2 ends are sticky and compatible for ligation.
@@ -144,3 +150,20 @@ def align_sanger_traces(dseqr: Dseqrecord, sanger_traces: list[str]) -> list[str
144
150
  sanger_traces = traces_oriented
145
151
 
146
152
  return align_with_mafft([query_str, *sanger_traces], True)
153
+
154
+
155
+ def compute_regex_site(site: str) -> str:
156
+ upper_site = site.upper()
157
+ for k, v in ambiguous_only_dna_values.items():
158
+ if len(v) > 1:
159
+ upper_site = upper_site.replace(k, f"[{''.join(v)}]")
160
+
161
+ # Make case insensitive
162
+ upper_site = f'(?i){upper_site}'
163
+ return upper_site
164
+
165
+
166
+ def dseqrecord_finditer(pattern: str, seq: Dseqrecord) -> list[re.Match]:
167
+ query = str(seq.seq) if not seq.circular else str(seq.seq) * 2
168
+ matches = re.finditer(pattern, query)
169
+ return (m for m in matches if m.start() <= len(seq))
@@ -5,7 +5,7 @@ from pydna.primer import Primer as PydnaPrimer
5
5
  from pydna.crispr import cas9
6
6
  from pydantic import conlist, create_model
7
7
  from Bio.Restriction.Restriction import RestrictionBatch
8
-
8
+ from opencloning.cre_lox import cre_loxP_overlap
9
9
  from ..dna_functions import (
10
10
  get_invalid_enzyme_names,
11
11
  format_sequence_genbank,
@@ -24,6 +24,8 @@ from ..pydantic_models import (
24
24
  AssemblySource,
25
25
  OverlapExtensionPCRLigationSource,
26
26
  GatewaySource,
27
+ CreLoxRecombinationSource,
28
+ InVivoAssemblySource,
27
29
  )
28
30
  from ..assembly2 import (
29
31
  Assembly,
@@ -159,6 +161,7 @@ def generate_assemblies(
159
161
  allow_insertion_assemblies: bool,
160
162
  assembly_kwargs: dict | None = None,
161
163
  product_callback: Callable[[Dseqrecord], Dseqrecord] = lambda x: x,
164
+ recombination_mode: bool = False,
162
165
  ) -> dict[Literal['sources', 'sequences'], list[AssemblySource] | list[TextFileSequence]]:
163
166
  if assembly_kwargs is None:
164
167
  assembly_kwargs = {}
@@ -175,10 +178,15 @@ def generate_assemblies(
175
178
  circular_assemblies = asm.get_circular_assemblies()
176
179
  out_sources += [create_source(a, True) for a in circular_assemblies]
177
180
  if not circular_only:
178
- out_sources += [
179
- create_source(a, False)
180
- for a in filter_linear_subassemblies(asm.get_linear_assemblies(), circular_assemblies, fragments)
181
- ]
181
+ if not recombination_mode:
182
+ out_sources += [
183
+ create_source(a, False)
184
+ for a in filter_linear_subassemblies(
185
+ asm.get_linear_assemblies(), circular_assemblies, fragments
186
+ )
187
+ ]
188
+ else:
189
+ out_sources += [create_source(a, False) for a in asm.get_insertion_assemblies()]
182
190
  else:
183
191
  asm = SingleFragmentAssembly(fragments, algorithm=algo, **assembly_kwargs)
184
192
  out_sources.extend(create_source(a, True) for a in asm.get_circular_assemblies())
@@ -385,13 +393,16 @@ async def homologous_recombination(
385
393
  '/gibson_assembly',
386
394
  response_model=create_model(
387
395
  'GibsonAssemblyResponse',
388
- sources=(list[Union[GibsonAssemblySource, OverlapExtensionPCRLigationSource, InFusionSource]], ...),
396
+ sources=(
397
+ list[Union[GibsonAssemblySource, OverlapExtensionPCRLigationSource, InFusionSource, InVivoAssemblySource]],
398
+ ...,
399
+ ),
389
400
  sequences=(list[TextFileSequence], ...),
390
401
  ),
391
402
  )
392
403
  async def gibson_assembly(
393
404
  sequences: conlist(TextFileSequence, min_length=1),
394
- source: Union[GibsonAssemblySource, OverlapExtensionPCRLigationSource, InFusionSource],
405
+ source: Union[GibsonAssemblySource, OverlapExtensionPCRLigationSource, InFusionSource, InVivoAssemblySource],
395
406
  minimal_homology: int = Query(
396
407
  40, description='The minimum homology between consecutive fragments in the assembly.'
397
408
  ),
@@ -522,3 +533,30 @@ async def gateway(
522
533
  return {'sources': sources, 'sequences': sequences}
523
534
 
524
535
  return resp
536
+
537
+
538
+ @router.post(
539
+ '/cre_lox_recombination',
540
+ response_model=create_model(
541
+ 'CreLoxRecombinationResponse',
542
+ sources=(list[CreLoxRecombinationSource], ...),
543
+ sequences=(list[TextFileSequence], ...),
544
+ ),
545
+ )
546
+ async def cre_lox_recombination(source: CreLoxRecombinationSource, sequences: conlist(TextFileSequence, min_length=1)):
547
+ fragments = [read_dsrecord_from_json(seq) for seq in sequences]
548
+
549
+ # Lambda function for code clarity
550
+ def create_source(a, is_circular):
551
+ return CreLoxRecombinationSource.from_assembly(
552
+ assembly=a, circular=is_circular, id=source.id, fragments=fragments
553
+ )
554
+
555
+ resp = generate_assemblies(
556
+ source, create_source, fragments, False, cre_loxP_overlap, True, recombination_mode=True
557
+ )
558
+
559
+ if len(resp['sources']) == 0:
560
+ raise HTTPException(400, 'No compatible Cre/Lox recombination was found.')
561
+
562
+ return resp
@@ -8,6 +8,8 @@ from starlette.responses import RedirectResponse
8
8
  from Bio import BiopythonParserWarning
9
9
  from typing import Annotated
10
10
  from urllib.error import HTTPError
11
+ from pydna.utils import location_boundaries
12
+
11
13
  from ..get_router import get_router
12
14
  from ..pydantic_models import (
13
15
  TextFileSequence,
@@ -22,6 +24,7 @@ from ..pydantic_models import (
22
24
  GenomeCoordinatesSource,
23
25
  SequenceFileFormat,
24
26
  SEVASource,
27
+ SimpleSequenceLocation,
25
28
  )
26
29
  from ..dna_functions import (
27
30
  format_sequence_genbank,
@@ -51,13 +54,13 @@ router = get_router()
51
54
  'description': 'The sequence was successfully parsed',
52
55
  'headers': {
53
56
  'x-warning': {
54
- 'description': 'A warning returned if the file can be read but is not in the expected format',
57
+ 'description': 'A warning returned if the file can be read but is not in the expected format or if some sequences were not extracted because they are incompatible with the provided coordinates',
55
58
  'schema': {'type': 'string'},
56
59
  },
57
60
  },
58
61
  },
59
62
  422: {
60
- 'description': 'Biopython cannot process this file.',
63
+ 'description': 'Biopython cannot process this file or provided coordinates are invalid.',
61
64
  },
62
65
  404: {
63
66
  'description': 'The index_in_file is out of range.',
@@ -83,6 +86,12 @@ async def read_from_file(
83
86
  None,
84
87
  description='Name of the output sequence',
85
88
  ),
89
+ start: int | None = Query(None, description='Start position of the sequence to read (0-based)', ge=0),
90
+ end: int | None = Query(
91
+ None,
92
+ description='End position of the sequence to read (0-based)',
93
+ ge=0,
94
+ ),
86
95
  ):
87
96
  """Return a json sequence from a sequence file"""
88
97
 
@@ -107,6 +116,7 @@ async def read_from_file(
107
116
  sequence_file_format = SequenceFileFormat(extension_dict[extension])
108
117
 
109
118
  dseqs = list()
119
+ warning_messages = list()
110
120
 
111
121
  file_content = await file.read()
112
122
  if sequence_file_format == 'snapgene':
@@ -124,7 +134,6 @@ async def read_from_file(
124
134
 
125
135
  if warnings_captured:
126
136
  warning_messages = [str(w.message) for w in warnings_captured]
127
- response.headers['x-warning'] = '; '.join(warning_messages)
128
137
 
129
138
  except ValueError as e:
130
139
  raise HTTPException(422, f'Biopython cannot process this file: {e}.')
@@ -134,25 +143,62 @@ async def read_from_file(
134
143
  if len(dseqs) == 0:
135
144
  raise HTTPException(422, 'Biopython cannot process this file.')
136
145
 
146
+ if index_in_file is not None:
147
+ if index_in_file >= len(dseqs):
148
+ raise HTTPException(404, 'The index_in_file is out of range.')
149
+ dseqs = [dseqs[index_in_file]]
150
+
151
+ seq_feature = None
152
+ if start is not None and end is not None:
153
+ seq_feature = SimpleSequenceLocation(start=start, end=end)
154
+ extracted_sequences = list()
155
+ for dseq in dseqs:
156
+ try:
157
+ # TODO: We could use extract when this is addressed: https://github.com/biopython/biopython/issues/4989
158
+ location = seq_feature.to_biopython_location(circular=dseq.circular, seq_len=len(dseq))
159
+ i, j = location_boundaries(location)
160
+ extracted_sequence = dseq[i:j]
161
+ # Only add the sequence if the interval is not out of bounds
162
+ if len(extracted_sequence) == len(location):
163
+ extracted_sequences.append(extracted_sequence)
164
+ else:
165
+ extracted_sequences.append(None)
166
+ except Exception:
167
+ extracted_sequences.append(None)
168
+ dseqs = extracted_sequences
169
+
137
170
  # The common part
138
- # TODO: using id=0 is not great
139
171
  parent_source = UploadedFileSource(
140
- id=0, sequence_file_format=sequence_file_format, file_name=file.filename, circularize=circularize
172
+ id=0,
173
+ sequence_file_format=sequence_file_format,
174
+ file_name=file.filename,
175
+ circularize=circularize,
176
+ coordinates=seq_feature,
141
177
  )
178
+
179
+ # If coordinates are provided, we only keep the sequences compatible with those coordinates
142
180
  out_sources = list()
181
+ out_sequences = list()
143
182
  for i in range(len(dseqs)):
183
+ if dseqs[i] is None:
184
+ continue
144
185
  new_source = parent_source.model_copy()
145
- new_source.index_in_file = i
186
+ new_source.index_in_file = index_in_file if index_in_file is not None else i
146
187
  out_sources.append(new_source)
188
+ out_sequences.append(format_sequence_genbank(dseqs[i], output_name))
147
189
 
148
- out_sequences = [format_sequence_genbank(s, output_name) for s in dseqs]
190
+ if len(out_sequences) == 0:
191
+ raise HTTPException(422, 'Provided coordinates are incompatible with sequences in the file.')
149
192
 
150
- if index_in_file is not None:
151
- if index_in_file >= len(out_sources):
152
- raise HTTPException(404, 'The index_in_file is out of range.')
153
- return {'sequences': [out_sequences[index_in_file]], 'sources': [out_sources[index_in_file]]}
154
- else:
155
- return {'sequences': out_sequences, 'sources': out_sources}
193
+ if len(out_sequences) < len(dseqs):
194
+ warning_messages.append(
195
+ 'Some sequences were not extracted because they are incompatible with the provided coordinates.'
196
+ )
197
+
198
+ if len(warning_messages) > 0:
199
+ response.headers['x-warning'] = '; '.join(warning_messages)
200
+
201
+ return {'sequences': out_sequences, 'sources': out_sources}
156
202
 
157
203
 
158
204
  # TODO: a bit inconsistent that here you don't put {source: {...}} in the request, but
opencloning/gateway.py CHANGED
@@ -1,31 +1,10 @@
1
- from Bio.Data.IUPACData import ambiguous_dna_values as _ambiguous_dna_values
2
1
  from Bio.Seq import reverse_complement
3
2
  from pydna.dseqrecord import Dseqrecord as _Dseqrecord
4
3
  import re
5
4
  import itertools as _itertools
6
5
  from Bio.SeqFeature import SimpleLocation, SeqFeature
7
6
  from pydna.utils import shift_location
8
-
9
- ambiguous_only_dna_values = {**_ambiguous_dna_values}
10
- for normal_base in 'ACGT':
11
- del ambiguous_only_dna_values[normal_base]
12
-
13
-
14
- def compute_regex_site(site: str) -> str:
15
- upper_site = site.upper()
16
- for k, v in ambiguous_only_dna_values.items():
17
- if len(v) > 1:
18
- upper_site = upper_site.replace(k, f"[{''.join(v)}]")
19
-
20
- # Make case insensitive
21
- upper_site = f'(?i){upper_site}'
22
- return upper_site
23
-
24
-
25
- def dseqrecord_finditer(pattern: str, seq: _Dseqrecord) -> list[re.Match]:
26
- query = str(seq.seq) if not seq.circular else str(seq.seq) * 2
27
- matches = re.finditer(pattern, query)
28
- return (m for m in matches if m.start() <= len(seq))
7
+ from .dna_utils import compute_regex_site, dseqrecord_finditer
29
8
 
30
9
 
31
10
  raw_gateway_common = {
@@ -45,6 +45,8 @@ from opencloning_linkml.datamodel import (
45
45
  IGEMSource as _IGEMSource,
46
46
  ReverseComplementSource as _ReverseComplementSource,
47
47
  SEVASource as _SEVASource,
48
+ CreLoxRecombinationSource as _CreLoxRecombinationSource,
49
+ InVivoAssemblySource as _InVivoAssemblySource,
48
50
  )
49
51
  from pydna.utils import shift_location as _shift_location
50
52
  from .assembly2 import edge_representation2subfragment_representation, subfragment_representation2edge_representation
@@ -338,6 +340,10 @@ class InFusionSource(AssemblySourceCommonClass, _InFusionSource):
338
340
  pass
339
341
 
340
342
 
343
+ class InVivoAssemblySource(AssemblySourceCommonClass, _InVivoAssemblySource):
344
+ pass
345
+
346
+
341
347
  class CRISPRSource(AssemblySourceCommonClass, _CRISPRSource):
342
348
 
343
349
  # TODO
@@ -384,6 +390,10 @@ class GatewaySource(AssemblySourceCommonClass, _GatewaySource):
384
390
  return super().from_assembly(assembly, id, circular, fragments, reaction_type=reaction_type)
385
391
 
386
392
 
393
+ class CreLoxRecombinationSource(AssemblySourceCommonClass, _CreLoxRecombinationSource):
394
+ pass
395
+
396
+
387
397
  class OligoHybridizationSource(SourceCommonClass, _OligoHybridizationSource):
388
398
  pass
389
399
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: opencloning
3
- Version: 0.2.7.3
3
+ Version: 0.2.8.1
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
@@ -15,7 +15,7 @@ Requires-Dist: beautifulsoup4 (>=4.11.1,<5.0.0)
15
15
  Requires-Dist: biopython (==1.84)
16
16
  Requires-Dist: fastapi
17
17
  Requires-Dist: httpx (>=0.25.0,<0.26.0)
18
- Requires-Dist: opencloning-linkml (==0.2.5.2a0)
18
+ Requires-Dist: opencloning-linkml (==0.2.6.1a0)
19
19
  Requires-Dist: openpyxl (>=3.1.5,<4.0.0)
20
20
  Requires-Dist: pandas (>=2.2.3,<3.0.0)
21
21
  Requires-Dist: primer3-py (>=2.0.3,<3.0.0)
@@ -19,27 +19,28 @@ opencloning/batch_cloning/pombe/pombe_summary.py,sha256=W9DLpnCuwK7w2DhHLu60N7L6
19
19
  opencloning/batch_cloning/ziqiang_et_al2024/__init__.py,sha256=zZUbj3uMzd9rKMXi5s9LQ1yUg7sccdS0f_4kpw7SQlk,7584
20
20
  opencloning/batch_cloning/ziqiang_et_al2024/index.html,sha256=EDncANDhhQkhi5FjnnAP6liHkG5srf4_Y46IrnMUG5g,4607
21
21
  opencloning/batch_cloning/ziqiang_et_al2024/ziqiang_et_al2024.json,sha256=mB81j2qWam7uRc-980YFjfqq2CiWTXJYfKFAoKuGtRw,157148
22
+ opencloning/cre_lox.py,sha256=mb2ZddjrPIrUBT3xxMub5-c97WkKZ4Z-HkGFVzuR8pQ,2031
22
23
  opencloning/dna_functions.py,sha256=W-SxEfvYpN1JVZbTeCNitpQXkazEHvFyqZBUndd-jpY,16329
23
- opencloning/dna_utils.py,sha256=emms1omBhQKuVNEv6YXcHReP69tvUU1iRpLgjXn5p9o,5541
24
+ opencloning/dna_utils.py,sha256=uv97aO04dbk3NnqbN6GlnwOu0MOpK88rl2np2QcEQ4Y,6301
24
25
  opencloning/ebic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
26
  opencloning/ebic/primer_design.py,sha256=gPZTF9w5SV7WGgnefp_HBM831y0z73M1Kb0QUPnbfIM,2270
26
27
  opencloning/ebic/primer_design_settings.py,sha256=OnFsuh0QCvplUEPXLZouzRo9R7rm4nLbcd2LkDCiIDM,1896
27
28
  opencloning/endpoints/annotation.py,sha256=3rlIXeNQzoqPD9lJUEBGLGxvlhUCTcfkqno814A8P0U,2283
28
- opencloning/endpoints/assembly.py,sha256=0kcWchgN5ulj_I7ZOpFhsgq74SBT_7A7xbZz7s5-1C0,19330
29
- opencloning/endpoints/external_import.py,sha256=dDG7DiNb8WYE46nLGnkyRbGVVNUDXp3h0_1ixsJAh5o,16242
29
+ opencloning/endpoints/assembly.py,sha256=H1b7CRx1JZ5pcUGd3uyJG2syYugkXiIo8HRCA11TQfE,20704
30
+ opencloning/endpoints/external_import.py,sha256=DG8WSvyvr-9xy-odEwLHHA4FWiIh8sw4DvTblw5NCYc,18179
30
31
  opencloning/endpoints/no_assembly.py,sha256=NY6rhEDCNoZVn6Xk81cen2n-FkMr7ierfxM8G0npbQs,4722
31
32
  opencloning/endpoints/no_input.py,sha256=DuqKD3Ph3a44ZxPMEzZv1nwD5xlxYsN7YyxXcfjSUFc,3844
32
33
  opencloning/endpoints/other.py,sha256=TzfCJLDmZFWeKYxKhEfXOvlQrWWyBIGJ5FR0yA7tuvI,1673
33
34
  opencloning/endpoints/primer_design.py,sha256=ItPUa7bBW9JOOfTuLj0yNnF9UmQ-I_0l3i8wHpnUc6k,12854
34
- opencloning/gateway.py,sha256=qaefWKjfASuVU_nXnCCoDepQq1jhNINNW0VifkCLVC0,9123
35
+ opencloning/gateway.py,sha256=jzpLbB8UCSlks0S6Qe9PXJ7CdzHiH2ko_O7MzYQLR14,8435
35
36
  opencloning/get_router.py,sha256=l2DXaTbeL2tDqlnVMlcewutzt1sjaHlxku1X9HVUwJk,252
36
37
  opencloning/main.py,sha256=l9PrPBMtGMEWxAPiPWR15Qv2oDNnRoNd8H8E3bZW6Do,3750
37
38
  opencloning/ncbi_requests.py,sha256=JrFc-Ugr1r1F4LqsdpJZEiERj7ZemvZSgiIltl2Chx8,5547
38
39
  opencloning/primer_design.py,sha256=nqCmYIZ7UvU4CQwVGJwX7T5LTHwt3-51_ZcTZZAgT_Y,9175
39
- opencloning/pydantic_models.py,sha256=S3ehXeynVlYJQK-G96D0jApMp7dn-eRg1di9d0DbsEc,15045
40
+ opencloning/pydantic_models.py,sha256=gsipVXhjQOXVz2NL-MiNpLuOZYDVo2Pli9F--bp6tjs,15345
40
41
  opencloning/request_examples.py,sha256=QAsJxVaq5tHwlPB404IiJ9WC6SA7iNY7XnJm63BWT_E,2944
41
42
  opencloning/utils.py,sha256=wsdTJYliap-t3oa7yQE3pWDa1CR19mr5lUQfocp4hoM,1875
42
- opencloning-0.2.7.3.dist-info/LICENSE,sha256=VSdVE1f8axjIh6gvo9ZZygJdTVkRFMcwCW_hvjOHC_w,1058
43
- opencloning-0.2.7.3.dist-info/METADATA,sha256=SFtmhPNvDi61Qp9JYNcsiQop9I_zYOPyGfeus-Etbf4,8429
44
- opencloning-0.2.7.3.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
45
- opencloning-0.2.7.3.dist-info/RECORD,,
43
+ opencloning-0.2.8.1.dist-info/LICENSE,sha256=VSdVE1f8axjIh6gvo9ZZygJdTVkRFMcwCW_hvjOHC_w,1058
44
+ opencloning-0.2.8.1.dist-info/METADATA,sha256=rp3mHAG3x49YfumIjM5teZL6iAtRbhQG1tl64bOjPfI,8429
45
+ opencloning-0.2.8.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
46
+ opencloning-0.2.8.1.dist-info/RECORD,,