Skip to content

Commit

Permalink
Merge pull request #209 from SynBioDex/develop
Browse files Browse the repository at this point in the history
Prepare 1.0a17 release
  • Loading branch information
jakebeal authored Apr 14, 2023
2 parents b3ab7cc + 2b8d628 commit 1984cf5
Show file tree
Hide file tree
Showing 50 changed files with 4,313 additions and 160 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
matrix:
# Default builds are on Ubuntu
os: [ubuntu-latest]
python-version: ['3.7', '3.8', '3.9', '3.10']
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
include:
# Also test on macOS and Windows using the latest Python 3
- os: macos-latest
Expand All @@ -38,6 +38,9 @@ jobs:
python -m pip install interrogate
- name: Setup Graphviz
uses: ts-graphviz/setup-graphviz@v1
- name: Show Node.js version
run: |
node --version
- name: Test with pytest
run: |
pip install .
Expand Down
45 changes: 45 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## How to contribute to SBOL Utilities

We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:

- Reporting a bug
- Proposing new features
- Contributing bug fixes, features, and new utilities

## Asking questions

* SBOL utilities has [online documentation available](https://sbol-utilities.readthedocs.io/en/latest/).
* If you need help and do not easily find an answer in the documentation, [ask your question in a new issue](https://github.com/SynBioDex/SBOL-utilities/issues/new).
* Any question that you have likely indicates a shortcoming in the documentation, which we will want to fix as well.


## Reporting a bug / proposing a feature

* **Check if the bug / feature request was not already reported** by searching on GitHub under [Issues](https://github.com/SynBioDex/SBOL-utilities/issues).

* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/SynBioDex/SBOL-utilities/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible.

* If you are reporting a bug, it will be much easier to fix if you include a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.

## Development Guidelines

We use GitHub to host code and documentation, to track issues and feature requests, as well as accept pull requests.

### All Changes Happen Through Pull Requests

Pull requests are the best way to propose changes to the codebase (we use the [Github Flow](https://guides.github.com/introduction/flow/) contribution models). We actively welcome your pull requests:

1. [Fork the GitHub repository](https://guides.github.com/activities/forking/) and create your branch from `develop`.
2. If you've added code that should be tested, add tests.
3. If you've added new features, update the documentation.
4. Ensure the test suite passes (_make sure you enable GitHub actions in your fork!_)
5. Make that pull request! Please ensure the pull request description clearly describes the problem and solution. Include the relevant issue number if applicable.

### All contributions are under the MIT Software License
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.

_No proprietary or copylefted code will be accepted!_

### **Do you want to add a new feature or change an existing one?**

We strongly suggest that you propose your change in [a new issues](https://github.com/SynBioDex/SBOL-utilities/issues/new) and collect feedback before you start writing code or open a pull request.
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# SBOL-utilities

SBOL-utilities is a collection of scripts and functions for manipulating SBOL 3 data that can be run from the command line or as functions in Python.
[Additional documentation is online at readthedocs.io](https://sbol-utilities.readthedocs.io/en/latest/).

[![Documentation Status](https://readthedocs.org/projects/sbol-utilities/badge/?version=latest)](http://sbol-utilities.readthedocs.io/)
[![Docstrings Coverage](https://github.com/SynBioDex/SBOL-utilities/actions/workflows/docstr-coverage.yml/badge.svg)](https://github.com/SynBioDex/SBOL-utilities/actions/workflows/docstr-coverage.yml)
Expand All @@ -22,6 +24,7 @@ Certain utilities also have non-Python dependencies, which must be installed sep
- `graph-sbol` requires [Graphviz](https://graphviz.org/) to be able to render diagrams.
- `sbol-converter` requires [node.js](https://nodejs.org/en/) to be able to locally run Javascript.


## Utilities

### Graph SBOL Documents
Expand Down Expand Up @@ -54,9 +57,9 @@ The `excel-to-sbol` utility reads an Excel file specifying a library of basic an
The `sbol-converter` utility converts between any of the SBOL3, SBOL2, GenBank, and FASTA formats.

Additional "macro" utilities convert specifically between SBOL3 and one of the other formats:
- `sbol2fasta` and `fasta2sbol` convert from SBOL3 to FASTA and vice versa
- `sbol2genbank` and `genbank2sbol` convert from SBOL3 to GenBank and vice versa
- `sbol3to2` and `sbol2to3` convert to and from SBOL2
- `sbol-to-fasta` and `fasta-to-sbol` convert from SBOL3 to FASTA and vice versa
- `sbol-to-genbank` and `genbank-to-sbol` convert from SBOL3 to GenBank and vice versa
- `sbol3-to-sbol2` and `sbol2-to-sbol3` convert to and from SBOL2

### Expand the combinatorial derivations in an SBOL file

Expand All @@ -69,3 +72,9 @@ The `sbol-calculate-sequences` utility attempts to calculate the sequence of any
### Compute the difference between two SBOL3 documents
The `sbol-diff` utility computes the difference between two SBOL3 documents
and reports the differences.


## Contributing

We welcome contributions that patch bugs, improve existing utilities or documentation, or add new utilities!
For guidance on how to contribute effectively to this project, see [CONTRIBUTING.md](CONTRIBUTING.md).
130 changes: 127 additions & 3 deletions sbol_utilities/component.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from __future__ import annotations

from typing import Dict, Iterable, List, Union, Optional, Tuple
from typing import Callable, Dict, Iterable, List, Union, Set, Optional, Tuple

import sbol3
import tyto

from sbol_utilities.helper_functions import id_sort, find_child, find_top_level, SBOL3PassiveVisitor, cached_references
from sbol_utilities.helper_functions import id_sort, find_child, find_top_level, SBOL3PassiveVisitor, cached_references, is_plasmid
from sbol_utilities.workarounds import get_parent

from Bio import Restriction


# TODO: consider allowing return of LocalSubComponent and ExternallyDefined
def contained_components(roots: Union[sbol3.TopLevel, Iterable[sbol3.TopLevel]]) -> set[sbol3.Component]:
Expand Down Expand Up @@ -86,13 +88,34 @@ def has_dna_type(component: sbol3.Component) -> bool:

# there must be atleast 1 SO role, among others
def check_roles(component: sbol3.Component) -> bool:
return any(tyto.SO.get_term_by_uri(role) for role in component.roles)
try:
return any(tyto.SO.get_term_by_uri(role) for role in component.roles)
except LookupError:
return False

# check all conditions
return isinstance(obj, sbol3.Component) and check_roles(obj) \
and has_dna_type(obj) and len(obj.sequences) == 1


def by_roles(required_role: str) -> Callable[[sbol3.TopLevel], bool]:
"""Given an object and a role, check if it is one of the roles of the object.
:param required_role: the role which must be present in given object
:return: lambda function taking an obj to check roles in, returns bool
"""
return lambda obj: isinstance(obj, sbol3.Component) and required_role in obj.roles


def by_types(required_type: str) -> Callable[[sbol3.TopLevel], bool]:
"""Given an object and a type, check if it is one of the types of the object.
:param required_type: the type which must be present in given object
:return: lambda function taking an obj to check types in, returns bool
"""
return lambda obj: isinstance(obj, sbol3.Component) and required_type in obj.types


def ensure_singleton_feature(system: sbol3.Component, target: Union[sbol3.Feature, sbol3.Component]):
"""Return a feature associated with the target, i.e., the target itself if a feature, or a SubComponent.
If the target is not already in the system, add it.
Expand Down Expand Up @@ -519,3 +542,104 @@ def ed_protein(definition: str, **kwargs) -> sbol3.ExternallyDefined:
:return: A Component object.
"""
return sbol3.ExternallyDefined([sbol3.SBO_PROTEIN], definition, **kwargs)

def ed_restriction_enzyme(name:str, **kwargs) -> sbol3.ExternallyDefined:
"""Creates an ExternallyDefined Restriction Enzyme Component from rebase.
:param name: Name of the SBOL ExternallyDefined, used by PyDNA. Case sensitive, follow standard restriction enzyme nomenclature, i.e. 'BsaI'
:param kwargs: Keyword arguments of any other ExternallyDefined attribute.
:return: An ExternallyDefined object.
"""
check_enzyme = Restriction.__dict__[name]
definition=f'http://rebase.neb.com/rebase/enz/{name}.html' # TODO: replace with getting the URI from Enzyme when REBASE identifiers become available in biopython 1.8
return sbol3.ExternallyDefined([sbol3.SBO_PROTEIN], definition=definition, name=name, **kwargs)

def backbone(identity: str, sequence: str, dropout_location: List[int], fusion_site_length:int, linear:bool, **kwargs) -> Tuple[sbol3.Component, sbol3.Sequence]:
"""Creates a Backbone Component and its Sequence.
:param identity: The identity of the Component. The identity of Sequence is also identity with the suffix '_seq'.
:param sequence: The DNA sequence of the Component encoded in IUPAC.
:param dropout_location: List of 2 integers that indicates the start and the end of the dropout sequence including overhangs. Note that the index of the first location is 1, as is typical practice in biology, rather than 0, as is typical practice in computer science.
:param fusion_site_length: Integer of the lenght of the fusion sites (eg. BsaI fusion site lenght is 4, SapI fusion site lenght is 3)
:param linear: Boolean than indicates if the backbone is linear, by default it is seted to Flase which means that it has a circular topology.
:param kwargs: Keyword arguments of any other Component attribute.
:return: A tuple of Component and Sequence.
"""
if len(dropout_location) != 2:
raise ValueError('The dropout_location only accepts 2 int values in a list.')
backbone_component, backbone_seq = dna_component_with_sequence(identity, sequence, **kwargs)
backbone_component.roles.append(sbol3.SO_DOUBLE_STRANDED)
dropout_location_comp = sbol3.Range(sequence=backbone_seq, start=dropout_location[0], end=dropout_location[1])
insertion_site_location1 = sbol3.Range(sequence=backbone_seq, start=dropout_location[0], end=dropout_location[0]+fusion_site_length, order=1)
insertion_site_location2 = sbol3.Range(sequence=backbone_seq, start=dropout_location[1]-fusion_site_length, end=dropout_location[1], order=3)
dropout_sequence_feature = sbol3.SequenceFeature(locations=[dropout_location_comp], roles=[tyto.SO.deletion])
insertion_sites_feature = sbol3.SequenceFeature(locations=[insertion_site_location1, insertion_site_location2], roles=[tyto.SO.insertion_site])
if linear:
backbone_component.types.append(sbol3.SO_LINEAR)
backbone_component.roles.append(sbol3.SO_ENGINEERED_REGION)
open_backbone_location1 = sbol3.Range(sequence=backbone_seq, start=1, end=dropout_location[0]+fusion_site_length-1, order=1)
open_backbone_location2 = sbol3.Range(sequence=backbone_seq, start=dropout_location[1]-fusion_site_length, end=len(sequence), order=3)
open_backbone_feature = sbol3.SequenceFeature(locations=[open_backbone_location1, open_backbone_location2])
else:
backbone_component.types.append(sbol3.SO_CIRCULAR)
backbone_component.roles.append(tyto.SO.plasmid_vector)
open_backbone_location1 = sbol3.Range(sequence=backbone_seq, start=1, end=dropout_location[0]+fusion_site_length-1, order=2)
open_backbone_location2 = sbol3.Range(sequence=backbone_seq, start=dropout_location[1]-fusion_site_length, end=len(sequence), order=1)
open_backbone_feature = sbol3.SequenceFeature(locations=[open_backbone_location1, open_backbone_location2])
backbone_component.features.append(dropout_sequence_feature)
backbone_component.features.append(insertion_sites_feature)
backbone_component.features.append(open_backbone_feature)
backbone_dropout_meets = sbol3.Constraint(restriction='http://sbols.org/v3#meets', subject=dropout_sequence_feature, object=open_backbone_feature)
backbone_component.constraints.append(backbone_dropout_meets)
return backbone_component, backbone_seq

def part_in_backbone(identity: str, part: sbol3.Component, backbone: sbol3.Component, linear:bool=False, **kwargs) -> Tuple[sbol3.Component, sbol3.Sequence]:
"""Creates a Part in Backbone Component and its Sequence.
:param identity: The identity of the Component. The identity of Sequence is also identity with the suffix '_seq'.
:param part: Part to be located in the backbone as SBOL Component.
:param backbone: Backbone in wich the part is located as SBOL Component.
:param linear: Boolean than indicates if the backbone is linear, by default it is seted to Flase which means that it has a circular topology.
:param kwargs: Keyword arguments of any other Component attribute.
:return: A tuple of Component and Sequence.
"""
# check that backbone has a plasmid vector or child ontology term
if is_plasmid(backbone)==False:
raise TypeError('The backbone has no valid plasmid vector or child role')
# check that the backbone and part has one sequence
if len(backbone.sequences)!=1:
raise ValueError(f'The backbone should have only one sequence, found {len(backbone.sequences)} sequences')
if len(part.sequences)!=1:
raise ValueError(f'The part should have only one sequence, found{len(part.sequences)} sequences')
# check that the the last feature of backbone has 2 locations
if len(backbone.features[-1].locations)!=2:
raise ValueError(f'The backbone last feature should be the open backbone and should contain 2 Locations, found {len(backbone.features[-1].locations)} Locations')
# get backbone sequence
backbone_sequence = backbone.sequences[0].lookup().elements
# compute open backbone sequences
open_backbone_sequence_from_location1=backbone_sequence[backbone.features[-1].locations[0].start -1 : backbone.features[-1].locations[0].end -1]
open_backbone_sequence_from_location2=backbone_sequence[backbone.features[-1].locations[1].start -1 : backbone.features[-1].locations[1].end-1]
# extract part sequence
part_sequence = part.sequences[0].lookup().elements
# make new component sequence
if linear:
part_in_backbone_seq_str = open_backbone_sequence_from_location1 + part_sequence + open_backbone_sequence_from_location2
topology_type = sbol3.SO_LINEAR
else:
part_in_backbone_seq_str = part_sequence + open_backbone_sequence_from_location2 + open_backbone_sequence_from_location1
topology_type = sbol3.SO_CIRCULAR
# part in backbone Component
part_in_backbone_component, part_in_backbone_seq = dna_component_with_sequence(identity, part_in_backbone_seq_str, **kwargs)
part_in_backbone_component.roles.append(tyto.SO.plasmid_vector) #review
# defining Location
part_subcomponent_location = sbol3.Range(sequence=part_in_backbone_seq, start=1, end=len(part_sequence))
backbone_subcomponent_location = sbol3.Range(sequence=part_in_backbone_seq, start=len(part_sequence)+1, end=len(part_in_backbone_seq_str))
source_location = sbol3.Range(sequence=backbone_sequence, start=backbone.features[-1].locations[0].start, end=backbone.features[-1].locations[0].end) # review
# creating and adding features
part_subcomponent = sbol3.SubComponent(part, roles=[tyto.SO.engineered_insert], locations=[part_subcomponent_location], role_integration='http://sbols.org/v3#mergeRoles')
backbone_subcomponent = sbol3.SubComponent(backbone, locations=[backbone_subcomponent_location], source_locations=[source_location]) #[backbone.features[2].locations[0]]) #generalize source location
part_in_backbone_component.features.append(part_subcomponent)
part_in_backbone_component.features.append(backbone_subcomponent)
# adding topology
part_in_backbone_component.types.append(topology_type)
return part_in_backbone_component, part_in_backbone_seq
Loading

0 comments on commit 1984cf5

Please sign in to comment.