opencloning 0.2.8.2__py3-none-any.whl → 0.3.0__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.
@@ -1,11 +1,14 @@
1
- from pydantic import BaseModel, Field, model_validator
1
+ from pydantic import BaseModel, Field, model_validator, field_validator
2
2
  from typing import Optional, List
3
+ from pydantic_core import core_schema
4
+ from ._version import __version__
3
5
 
4
6
  from Bio.SeqFeature import (
5
7
  SeqFeature,
6
8
  Location,
7
- SimpleLocation as BioSimpleLocation,
9
+ SimpleLocation,
8
10
  FeatureLocation as BioFeatureLocation,
11
+ LocationParserError,
9
12
  )
10
13
  from Bio.SeqIO.InsdcIO import _insdc_location_string as format_feature_location
11
14
  from Bio.Restriction.Restriction import RestrictionType, RestrictionBatch
@@ -31,7 +34,6 @@ from opencloning_linkml.datamodel import (
31
34
  CRISPRSource as _CRISPRSource,
32
35
  Primer as _Primer,
33
36
  AssemblyFragment as _AssemblyFragment,
34
- SimpleSequenceLocation as _SimpleSequenceLocation,
35
37
  AddgeneIdSource as _AddgeneIdSource,
36
38
  WekWikGeneIdSource as _WekWikGeneIdSource,
37
39
  BenchlingUrlSource as _BenchlingUrlSource,
@@ -48,8 +50,11 @@ from opencloning_linkml.datamodel import (
48
50
  CreLoxRecombinationSource as _CreLoxRecombinationSource,
49
51
  InVivoAssemblySource as _InVivoAssemblySource,
50
52
  )
51
- from pydna.utils import shift_location as _shift_location
52
- from .assembly2 import edge_representation2subfragment_representation, subfragment_representation2edge_representation
53
+ from .assembly2 import (
54
+ edge_representation2subfragment_representation,
55
+ subfragment_representation2edge_representation,
56
+ )
57
+ from pydna.utils import location_boundaries, shift_location
53
58
 
54
59
 
55
60
  SequenceFileFormat = _SequenceFileFormat
@@ -110,7 +115,17 @@ class ManuallyTypedSource(SourceCommonClass, _ManuallyTypedSource):
110
115
 
111
116
 
112
117
  class UploadedFileSource(SourceCommonClass, _UploadedFileSource):
113
- pass
118
+ coordinates: Optional['SequenceLocationStr'] = Field(
119
+ default=None,
120
+ description="""If provided, coordinates within the sequence of the file to extract a subsequence""",
121
+ json_schema_extra={'linkml_meta': {'alias': 'coordinates', 'domain_of': ['UploadedFileSource']}},
122
+ )
123
+
124
+ @field_validator('coordinates', mode='before')
125
+ def parse_coordinates(cls, v):
126
+ if v is None:
127
+ return None
128
+ return SequenceLocationStr.field_validator(v)
114
129
 
115
130
 
116
131
  class RepositoryIdSource(SourceCommonClass, _RepositoryIdSource):
@@ -218,36 +233,91 @@ class RestrictionEnzymeDigestionSource(SourceCommonClass, _RestrictionEnzymeDige
218
233
  return sorted(list(set(out)), key=out.index)
219
234
 
220
235
 
221
- class SimpleSequenceLocation(_SimpleSequenceLocation):
236
+ class SequenceLocationStr(str):
237
+ """A string representation of a sequence location, genbank-like."""
238
+
222
239
  # TODO: this should handle origin-spanning simple locations (splitted)
223
240
  @classmethod
224
- def from_simple_location(cls, location: BioSimpleLocation):
225
- return cls(
226
- start=location.start,
227
- end=location.end,
228
- strand=location.strand,
241
+ def from_biopython_location(cls, location: Location):
242
+ return cls(format_feature_location(location, None))
243
+
244
+ @classmethod
245
+ def from_start_and_end(cls, start: int, end: int, seq_len: int | None = None, strand: int | None = 1):
246
+ if end >= start:
247
+ return cls.from_biopython_location(SimpleLocation(start, end, strand=strand))
248
+ else:
249
+ if seq_len is None:
250
+ raise ValueError('Sequence length is required to handle origin-spanning simple locations')
251
+ unwrapped_location = SimpleLocation(start, end + seq_len, strand=strand)
252
+ wrapped_location = shift_location(unwrapped_location, 0, seq_len)
253
+ return cls.from_biopython_location(wrapped_location)
254
+
255
+ def to_biopython_location(self) -> BioFeatureLocation:
256
+ return Location.fromstring(self)
257
+
258
+ @classmethod
259
+ def field_validator(cls, v):
260
+ if isinstance(v, str):
261
+ value = cls(v)
262
+ try:
263
+ value.to_biopython_location()
264
+ except LocationParserError:
265
+ raise ValueError(f'Location "{v}" is not a valid location')
266
+ return value
267
+ raise ValueError(f'Location must be a string or a {cls.__name__}')
268
+
269
+ @property
270
+ def start(self) -> int:
271
+ return location_boundaries(self.to_biopython_location())[0]
272
+
273
+ @property
274
+ def end(self) -> int:
275
+ return location_boundaries(self.to_biopython_location())[1]
276
+
277
+ @classmethod
278
+ def __get_pydantic_core_schema__(
279
+ cls,
280
+ source_type,
281
+ handler,
282
+ ) -> core_schema.CoreSchema:
283
+ """Generate Pydantic core schema for SequenceLocationStr."""
284
+ return core_schema.with_info_after_validator_function(
285
+ cls._validate,
286
+ core_schema.str_schema(),
229
287
  )
230
288
 
231
- def to_biopython_location(self, circular: bool = False, seq_len: int = None) -> BioFeatureLocation:
232
- if circular and self.start > self.end and seq_len is not None:
233
- unwrapped_location = BioSimpleLocation(self.start, self.end + seq_len, self.strand)
234
- return _shift_location(unwrapped_location, 0, seq_len)
235
- return BioSimpleLocation(self.start, self.end, self.strand)
289
+ @classmethod
290
+ def _validate(cls, value: str, info):
291
+ """Validate and create SequenceLocationStr instance."""
292
+ return cls.field_validator(value)
236
293
 
237
294
 
238
295
  class AssemblyFragment(_AssemblyFragment):
239
- left_location: Optional[SimpleSequenceLocation] = None
240
- right_location: Optional[SimpleSequenceLocation] = None
296
+ left_location: Optional[SequenceLocationStr] = None
297
+ right_location: Optional[SequenceLocationStr] = None
241
298
 
242
- def to_fragment_tuple(self, fragments) -> tuple[int, BioSimpleLocation, BioSimpleLocation]:
299
+ def to_fragment_tuple(self, fragments) -> tuple[int, Location, Location]:
243
300
  fragment_ids = [int(f.id) for f in fragments]
301
+ # By convention, these have no strand
302
+ left_loc = None if self.left_location is None else self.left_location.to_biopython_location()
303
+ right_loc = None if self.right_location is None else self.right_location.to_biopython_location()
304
+ if left_loc is not None:
305
+ left_loc.strand = None
306
+ if right_loc is not None:
307
+ right_loc.strand = None
244
308
 
245
309
  return (
246
310
  (fragment_ids.index(self.sequence) + 1) * (-1 if self.reverse_complemented else 1),
247
- None if self.left_location is None else self.left_location.to_biopython_location(),
248
- None if self.right_location is None else self.right_location.to_biopython_location(),
311
+ left_loc,
312
+ right_loc,
249
313
  )
250
314
 
315
+ @field_validator('left_location', 'right_location', mode='before')
316
+ def parse_location(cls, v):
317
+ if v is None:
318
+ return None
319
+ return SequenceLocationStr.field_validator(v)
320
+
251
321
 
252
322
  class AssemblySourceCommonClass(SourceCommonClass):
253
323
  # TODO: This is different in the LinkML model, because there it is not required,
@@ -290,8 +360,8 @@ class AssemblySourceCommonClass(SourceCommonClass):
290
360
  assembly_fragments = [
291
361
  AssemblyFragment(
292
362
  sequence=fragment_ids[abs(pos) - 1],
293
- left_location=None if left_loc is None else SimpleSequenceLocation.from_simple_location(left_loc),
294
- right_location=None if right_loc is None else SimpleSequenceLocation.from_simple_location(right_loc),
363
+ left_location=None if left_loc is None else SequenceLocationStr.from_biopython_location(left_loc),
364
+ right_location=None if right_loc is None else SequenceLocationStr.from_biopython_location(right_loc),
295
365
  reverse_complemented=pos < 0,
296
366
  )
297
367
  for pos, left_loc, right_loc in fragment_assembly_positions
@@ -410,6 +480,11 @@ class BaseCloningStrategy(_CloningStrategy):
410
480
  description="""The primers that are used in the cloning strategy""",
411
481
  json_schema_extra={'linkml_meta': {'alias': 'primers', 'domain_of': ['CloningStrategy']}},
412
482
  )
483
+ backend_version: Optional[str] = Field(
484
+ default=__version__,
485
+ description="""The version of the backend that was used to generate this cloning strategy""",
486
+ json_schema_extra={'linkml_meta': {'alias': 'backend_version', 'domain_of': ['CloningStrategy']}},
487
+ )
413
488
 
414
489
  def next_primer_id(self):
415
490
  return max([p.id for p in self.primers], default=0) + 1
@@ -436,8 +511,27 @@ class BaseCloningStrategy(_CloningStrategy):
436
511
  self.sequences.append(sequence)
437
512
  source.output = sequence.id
438
513
 
514
+ def all_children_source_ids(self, source_id: int, source_children: list | None = None) -> list[int]:
515
+ """Returns the ids of all source children ids of a source"""
516
+ source = next(s for s in self.sources if s.id == source_id)
517
+ if source_children is None:
518
+ source_children = []
519
+
520
+ sources_that_take_output_as_input = [s for s in self.sources if source.output in s.input]
521
+ new_source_ids = [s.id for s in sources_that_take_output_as_input]
522
+
523
+ source_children.extend(new_source_ids)
524
+ for new_source_id in new_source_ids:
525
+ self.all_children_source_ids(new_source_id, source_children)
526
+ return source_children
527
+
439
528
 
440
529
  class PrimerDesignQuery(BaseModel):
530
+ model_config = {'arbitrary_types_allowed': True}
441
531
  sequence: TextFileSequence
442
- location: SimpleSequenceLocation
532
+ location: SequenceLocationStr
443
533
  forward_orientation: bool = True
534
+
535
+ @field_validator('location', mode='before')
536
+ def parse_location(cls, v):
537
+ return SequenceLocationStr.field_validator(v)
opencloning/utils.py CHANGED
@@ -43,15 +43,3 @@ class TemporaryFolderOverride:
43
43
  if self.target_folder_exists:
44
44
  os.mkdir(self.target_folder)
45
45
  move_all_contents(self.backup_folder, self.target_folder)
46
-
47
-
48
- def api_version() -> dict[str, str | None]:
49
- version = None
50
- commit_sha = None
51
- if os.path.exists('version.txt'):
52
- with open('version.txt', 'r') as f:
53
- version = f.read().strip()
54
- if os.path.exists('commit_sha.txt'):
55
- with open('commit_sha.txt', 'r') as f:
56
- commit_sha = f.read().strip()
57
- return {'version': version, 'commit_sha': commit_sha}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: opencloning
3
- Version: 0.2.8.2
3
+ Version: 0.3.0
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,10 +15,11 @@ 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.6.1a0)
18
+ Requires-Dist: opencloning-linkml (==0.3.0a0)
19
19
  Requires-Dist: openpyxl (>=3.1.5,<4.0.0)
20
+ Requires-Dist: packaging (>=25.0,<26.0)
20
21
  Requires-Dist: pandas (>=2.2.3,<3.0.0)
21
- Requires-Dist: primer3-py (>=2.0.3,<3.0.0)
22
+ Requires-Dist: primer3-py (==2.0.3)
22
23
  Requires-Dist: pydantic (>=2.7.1,<3.0.0)
23
24
  Requires-Dist: pydna (==5.5.0)
24
25
  Requires-Dist: python-multipart
@@ -48,6 +49,12 @@ This API provides a series of entry points. The API documentation can be accesse
48
49
 
49
50
  The API functions can also be used to write python scripts to automate cloning. See the [scripting examples](examples/scripting) for more information.
50
51
 
52
+ ## Migrating between model versions and fixing model bugs
53
+
54
+ * The data model changes, so the json files you created may not be compatible with the newest version of the library, which uses the latest data mode. You can easily fix this using `python -m opencloning_linkml.migrations.migrate file.json
55
+ ` see [full documentation](https://github.com/OpenCloning/OpenCloning_LinkML?tab=readme-ov-file#migration-from-previous-versions-of-the-schema).
56
+ * Before version 0.3, there was a bug for assembly fields that included locations spanning the origin. See the details and how to fix it in the documentation of [this file](./src/opencloning/bug_fixing/README.md).
57
+
51
58
  ## Getting started
52
59
 
53
60
  If you want to quickly set up a local instance of the frontend and backend of the application, check [getting started in 5 minutes](https://github.com/manulera/OpenCloning#timer_clock-getting-started-in-5-minutes) in the main repository.
@@ -61,7 +68,7 @@ You can install this as a python package:
61
68
  python -m venv .venv
62
69
  # Activate the virtual environment
63
70
  source .venv/bin/activate
64
- # Install the package from github (will be in pypi at some point)
71
+ # Install the package from pypi
65
72
  pip install opencloning
66
73
  # Run the API (uvicorn should be installed in the virtual environment)
67
74
  uvicorn opencloning.main:app
@@ -1,7 +1,8 @@
1
1
  opencloning/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ opencloning/_version.py,sha256=6QbWXLSZypjtWL_CwJFHH4dzMRK3AUH4B0YudzvGz9s,200
2
3
  opencloning/api_config_utils.py,sha256=inAXPGYNDz-DuEoSqitImj0Vv5TpQSbMZH9D3dQb5P0,4319
3
4
  opencloning/app_settings.py,sha256=x5ddkaoyWE76fa4CdwIv-aDfg1eyZr6qTQwfKJB4mCo,1785
4
- opencloning/assembly2.py,sha256=WeNV0AK_MVzF_mdXaG3scNlNgwy-iJY63mvqBMfLAhI,56600
5
+ opencloning/assembly2.py,sha256=_UdQCnjc2wmWoeVT9NKwXcYUAEBrBT0OSBNMbGYAIL8,57041
5
6
  opencloning/batch_cloning/EBIC/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
7
  opencloning/batch_cloning/EBIC/barcode.gb,sha256=G6kP6MuY23S-n3xg16LQaTasFtYFqik5eEgcooZ9ATM,815
7
8
  opencloning/batch_cloning/EBIC/common_plasmid.gb,sha256=At1HJjqJ2MsLMEx6W3MihJy7tgdtDu3fhwF4YGuw8Dk,13068
@@ -9,7 +10,7 @@ opencloning/batch_cloning/EBIC/example.py,sha256=FWjROXWsgM-gz2oYnKiywUVKa9uoEe9
9
10
  opencloning/batch_cloning/EBIC/primer_design_settings.py,sha256=MVML1r1ciJYMFUJoqZVcGLoPM-f28oBN1wSDzlD0y64,1896
10
11
  opencloning/batch_cloning/__init__.py,sha256=uDxAa45g30_S6dJScNMlIxubQXlLRUsWoLX4S4y-l88,244
11
12
  opencloning/batch_cloning/index.html,sha256=HDqPHrJxrrKfGmy_dwYHhOsdUgZHHIch7Z0ey8qyvZI,1332
12
- opencloning/batch_cloning/pombe/__init__.py,sha256=nL4ZHdyEr_J0xzPFCYjujlwx81RcWYxeR_ax2V4-vuM,3532
13
+ opencloning/batch_cloning/pombe/__init__.py,sha256=Fq7SroO0Fer5CtFBRWdduIvzp1_dTUZwBb8IjBtRQO0,3332
13
14
  opencloning/batch_cloning/pombe/index.html,sha256=3YchoKGpcKDfvTOW1Rdih4PkbZIkMjKIQ0PaVXfV3e8,8348
14
15
  opencloning/batch_cloning/pombe/pombe_all.sh,sha256=0yvDdBaIdt2RsIrvnjgn5L3KtYBToq3Rl8-X8RFHibE,364
15
16
  opencloning/batch_cloning/pombe/pombe_clone.py,sha256=OY6yOlBK-9OAmHu3HUhP50mIXvyR0HJg1_2OjBFifV8,8123
@@ -18,29 +19,32 @@ opencloning/batch_cloning/pombe/pombe_get_primers.py,sha256=1RbR_8YGhSrmeIVDOpUp
18
19
  opencloning/batch_cloning/pombe/pombe_summary.py,sha256=W9DLpnCuwK7w2DhHLu60N7L6jquuYubD3ZRFwdhNPVw,4033
19
20
  opencloning/batch_cloning/ziqiang_et_al2024/__init__.py,sha256=zZUbj3uMzd9rKMXi5s9LQ1yUg7sccdS0f_4kpw7SQlk,7584
20
21
  opencloning/batch_cloning/ziqiang_et_al2024/index.html,sha256=EDncANDhhQkhi5FjnnAP6liHkG5srf4_Y46IrnMUG5g,4607
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
+ opencloning/batch_cloning/ziqiang_et_al2024/ziqiang_et_al2024.json,sha256=d-7oXbxoKhMKLz4FJ2OGDdedWTisoaRqLAh1NZnScBg,157189
23
+ opencloning/bug_fixing/README.md,sha256=9EMhP_ibl_HDt745dz1Cw_Pl2kTMVrNJQW5ysCBcJoE,4231
24
+ opencloning/bug_fixing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ opencloning/bug_fixing/backend_v0_3.py,sha256=ubfDN-IEwj-dW1Qgy3ozrkXcUIXwXvZcxJHrVanR_Ws,4213
26
+ opencloning/cre_lox.py,sha256=x_OVYzfaLJH5eVyp05_I9YNycT606UL683AswhQ-gjU,4294
23
27
  opencloning/dna_functions.py,sha256=ivepJM2wRTIW0ArSiQ5s-XuqBd69giEQijaWXXGT64E,16536
24
28
  opencloning/dna_utils.py,sha256=uv97aO04dbk3NnqbN6GlnwOu0MOpK88rl2np2QcEQ4Y,6301
25
29
  opencloning/ebic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
30
  opencloning/ebic/primer_design.py,sha256=gPZTF9w5SV7WGgnefp_HBM831y0z73M1Kb0QUPnbfIM,2270
27
31
  opencloning/ebic/primer_design_settings.py,sha256=OnFsuh0QCvplUEPXLZouzRo9R7rm4nLbcd2LkDCiIDM,1896
28
32
  opencloning/endpoints/annotation.py,sha256=3rlIXeNQzoqPD9lJUEBGLGxvlhUCTcfkqno814A8P0U,2283
29
- opencloning/endpoints/assembly.py,sha256=H1b7CRx1JZ5pcUGd3uyJG2syYugkXiIo8HRCA11TQfE,20704
30
- opencloning/endpoints/external_import.py,sha256=DG8WSvyvr-9xy-odEwLHHA4FWiIh8sw4DvTblw5NCYc,18179
33
+ opencloning/endpoints/assembly.py,sha256=MMwvlyM2NHnT04Pp7LdKQRvahq0itdqTtZx3OQhOswc,20980
34
+ opencloning/endpoints/external_import.py,sha256=xU-ZL503pJW1M08gxXTULaLgp9jHc_dBVrcyMRjNmow,18178
31
35
  opencloning/endpoints/no_assembly.py,sha256=NY6rhEDCNoZVn6Xk81cen2n-FkMr7ierfxM8G0npbQs,4722
32
36
  opencloning/endpoints/no_input.py,sha256=DuqKD3Ph3a44ZxPMEzZv1nwD5xlxYsN7YyxXcfjSUFc,3844
33
- opencloning/endpoints/other.py,sha256=TzfCJLDmZFWeKYxKhEfXOvlQrWWyBIGJ5FR0yA7tuvI,1673
34
- opencloning/endpoints/primer_design.py,sha256=ItPUa7bBW9JOOfTuLj0yNnF9UmQ-I_0l3i8wHpnUc6k,12854
35
- opencloning/gateway.py,sha256=jzpLbB8UCSlks0S6Qe9PXJ7CdzHiH2ko_O7MzYQLR14,8435
37
+ opencloning/endpoints/other.py,sha256=7YBXU5UrVCjEjOjdYWw-0sASXn3MhWVZYwDYSZD4C9E,3452
38
+ opencloning/endpoints/primer_design.py,sha256=3eiQ7MwgeLoAuXFUMNF-DzjzwH_eJGCjd4s32CjxIic,12717
39
+ opencloning/gateway.py,sha256=pFB3gsCQL715kOFOP1NQOOsQqrkWuQe5qXk4IunF5SA,8486
36
40
  opencloning/get_router.py,sha256=l2DXaTbeL2tDqlnVMlcewutzt1sjaHlxku1X9HVUwJk,252
37
41
  opencloning/main.py,sha256=l9PrPBMtGMEWxAPiPWR15Qv2oDNnRoNd8H8E3bZW6Do,3750
38
42
  opencloning/ncbi_requests.py,sha256=JrFc-Ugr1r1F4LqsdpJZEiERj7ZemvZSgiIltl2Chx8,5547
39
43
  opencloning/primer_design.py,sha256=nqCmYIZ7UvU4CQwVGJwX7T5LTHwt3-51_ZcTZZAgT_Y,9175
40
- opencloning/pydantic_models.py,sha256=gsipVXhjQOXVz2NL-MiNpLuOZYDVo2Pli9F--bp6tjs,15345
44
+ opencloning/pydantic_models.py,sha256=lMO78M4MwDgzTEGz9qzsaADwAFXagWK4qGsF1K1hLZw,18865
41
45
  opencloning/request_examples.py,sha256=QAsJxVaq5tHwlPB404IiJ9WC6SA7iNY7XnJm63BWT_E,2944
42
- opencloning/utils.py,sha256=wsdTJYliap-t3oa7yQE3pWDa1CR19mr5lUQfocp4hoM,1875
43
- opencloning-0.2.8.2.dist-info/LICENSE,sha256=VSdVE1f8axjIh6gvo9ZZygJdTVkRFMcwCW_hvjOHC_w,1058
44
- opencloning-0.2.8.2.dist-info/METADATA,sha256=OrcZ2VMjjkWI31tB4B1tEa6GgIxBFlOiGWxmA_8eK6A,8429
45
- opencloning-0.2.8.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
46
- opencloning-0.2.8.2.dist-info/RECORD,,
46
+ opencloning/utils.py,sha256=0Lvw1h1AsUJTK2b9mNzYVi_DBeWmWCFA5dIPl_gERcI,1479
47
+ opencloning-0.3.0.dist-info/LICENSE,sha256=VSdVE1f8axjIh6gvo9ZZygJdTVkRFMcwCW_hvjOHC_w,1058
48
+ opencloning-0.3.0.dist-info/METADATA,sha256=fqz_qzNi4Q4eHOzzx4B6iMUMf7WMLFOu5EY4x0vthek,9083
49
+ opencloning-0.3.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
50
+ opencloning-0.3.0.dist-info/RECORD,,