From 49462393274a5aa788a732e7bf3ba57ed7d24a8a Mon Sep 17 00:00:00 2001 From: dpopleton Date: Mon, 19 Sep 2022 15:59:17 -0400 Subject: [PATCH 01/14] Added models.py --- requirements.in | 1 + requirements.txt | 2 + src/ensembl/production/metadata/api.py | 487 +++++++++++++--------- src/ensembl/production/metadata/models.py | 217 ++++++++++ 4 files changed, 513 insertions(+), 194 deletions(-) create mode 100644 src/ensembl/production/metadata/models.py diff --git a/requirements.in b/requirements.in index af1aca55..0b9c3329 100644 --- a/requirements.in +++ b/requirements.in @@ -2,3 +2,4 @@ mysqlclient pymysql sqlalchemy types-pymysql +fastapi \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f233661a..5cb401dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,5 @@ sqlalchemy==1.4.21 # via -r requirements.in types-pymysql==1.0.0 # via -r requirements.in +fastapi==0.83.0 + # via -r requirements.in \ No newline at end of file diff --git a/src/ensembl/production/metadata/api.py b/src/ensembl/production/metadata/api.py index 55c4e5fe..9d30134e 100644 --- a/src/ensembl/production/metadata/api.py +++ b/src/ensembl/production/metadata/api.py @@ -19,21 +19,29 @@ config = MetadataConfig() + + +#Looks good right here. def load_database(uri): try: engine = db.create_engine(uri) except AttributeError as err: - raise ValueError(f'Could not connect to database {uri}: {err}.') from err + raise ValueError(f"Could not connect to database {uri}: {err}.") from err try: connection = engine.connect() except db.exc.OperationalError as err: - raise ValueError(f'Could not connect to database {uri}: {err}.') from err + raise ValueError(f"Could not connect to database {uri}: {err}.") from err connection.close() return engine + + +#Not sure why all of this is here, but it is not being removed until we are certain. + + def check_parameter(param): if param is not None and not isinstance(param, list): param = [param] @@ -52,45 +60,50 @@ def __init__(self, metadata_uri=None): class ReleaseAdaptor(BaseAdaptor): - def fetch_releases(self, - release_id=None, - release_version=None, - current_only=True, - release_type=None, - site_name=None - ): + def fetch_releases( + self, + release_id=None, + release_version=None, + current_only=True, + release_type=None, + site_name=None, + ): release_id = check_parameter(release_id) release_version = check_parameter(release_version) release_type = check_parameter(release_type) site_name = check_parameter(site_name) # Reflect existing tables, letting sqlalchemy load linked tables where possible. - release = db.Table('ensembl_release', self.md, autoload_with=self.metadata_db) - site = self.md.tables['ensembl_site'] + release = db.Table("ensembl_release", self.md, autoload_with=self.metadata_db) + site = self.md.tables["ensembl_site"] release_select = db.select( - release.c.release_id, - release.c.version.label('release_version'), - db.cast(release.c.release_date, db.String), - release.c.label.label('release_label'), - release.c.is_current, - release.c.release_type, - site.c.name.label('site_name'), - site.c.label.label('site_label'), - site.c.uri.label('site_uri') - ).select_from(release) + release.c.release_id, + release.c.version.label("release_version"), + db.cast(release.c.release_date, db.String), + release.c.label.label("release_label"), + release.c.is_current, + release.c.release_type, + site.c.name.label("site_name"), + site.c.label.label("site_label"), + site.c.uri.label("site_uri"), + ).select_from(release) # These options are in order of decreasing specificity, # and thus the ones later in the list can be redundant. if release_id is not None: release_select = release_select.filter(release.c.release_id.in_(release_id)) elif release_version is not None: - release_select = release_select.filter(release.c.version.in_(release_version)) + release_select = release_select.filter( + release.c.version.in_(release_version) + ) elif current_only: release_select = release_select.filter_by(is_current=1) if release_type is not None: - release_select = release_select.filter(release.c.release_type.in_(release_type)) + release_select = release_select.filter( + release.c.release_type.in_(release_type) + ) release_select = release_select.join(site) if site_name is not None: @@ -99,30 +112,40 @@ def fetch_releases(self, return self.metadata_db_session.execute(release_select).all() def fetch_releases_for_genome(self, genome_uuid, site_name=None): - genome = db.Table('genome', self.md, autoload_with=self.metadata_db) - genome_release = db.Table('genome_release', self.md, autoload_with=self.metadata_db) + genome = db.Table("genome", self.md, autoload_with=self.metadata_db) + genome_release = db.Table( + "genome_release", self.md, autoload_with=self.metadata_db + ) - release_id_select = db.select( - genome_release.c.release_id - ).select_from(genome).filter_by( - genome_uuid=genome_uuid - ).join(genome_release) + release_id_select = ( + db.select(genome_release.c.release_id) + .select_from(genome) + .filter_by(genome_uuid=genome_uuid) + .join(genome_release) + ) - release_ids = [rid for (rid,) in self.metadata_db_session.execute(release_id_select)] + release_ids = [ + rid for (rid,) in self.metadata_db_session.execute(release_id_select) + ] return self.fetch_releases(release_id=release_ids, site_name=site_name) def fetch_releases_for_dataset(self, dataset_uuid, site_name=None): - dataset = db.Table('dataset', self.md, autoload_with=self.metadata_db) - genome_dataset = db.Table('genome_dataset', self.md, autoload_with=self.metadata_db) + dataset = db.Table("dataset", self.md, autoload_with=self.metadata_db) + genome_dataset = db.Table( + "genome_dataset", self.md, autoload_with=self.metadata_db + ) - release_id_select = db.select( - genome_dataset.c.release_id - ).select_from(dataset).filter_by( - dataset_uuid=dataset_uuid - ).join(genome_dataset) + release_id_select = ( + db.select(genome_dataset.c.release_id) + .select_from(dataset) + .filter_by(dataset_uuid=dataset_uuid) + .join(genome_dataset) + ) - release_ids = [rid for (rid,) in self.metadata_db_session.execute(release_id_select)] + release_ids = [ + rid for (rid,) in self.metadata_db_session.execute(release_id_select) + ] return self.fetch_releases(release_id=release_ids, site_name=site_name) @@ -144,69 +167,73 @@ def __init__(self, metadata_uri=None, taxonomy_uri=None): self.taxon_names = self.fetch_taxonomy_names(taxonomy_ids) def fetch_taxonomy_ids(self): - organism = db.Table('organism', self.md, autoload_with=self.metadata_db) + organism = db.Table("organism", self.md, autoload_with=self.metadata_db) taxonomy_id_select = db.select(organism.c.taxonomy_id.distinct()) taxonomy_ids = [tid for (tid,) in self.metadata_db.execute(taxonomy_id_select)] return taxonomy_ids def fetch_taxonomy_names(self, taxonomy_id): - ncbi_taxa_name = db.Table('ncbi_taxa_name', self.md, autoload_with=self.taxonomy_db) + ncbi_taxa_name = db.Table( + "ncbi_taxa_name", self.md, autoload_with=self.taxonomy_db + ) taxons = {} for tid in taxonomy_id: - names = { - 'scientific_name': None, - 'synonym': [] - } + names = {"scientific_name": None, "synonym": []} taxons[tid] = names sci_name_select = db.select( - ncbi_taxa_name.c.taxon_id, - ncbi_taxa_name.c.name + ncbi_taxa_name.c.taxon_id, ncbi_taxa_name.c.name ).filter( ncbi_taxa_name.c.taxon_id.in_(taxonomy_id), - ncbi_taxa_name.c.name_class == 'scientific name' + ncbi_taxa_name.c.name_class == "scientific name", ) for x in self.taxonomy_db.execute(sci_name_select): - taxons[x.taxon_id]['scientific_name'] = x.name + taxons[x.taxon_id]["scientific_name"] = x.name synonym_class = [ - 'common name', - 'equivalent name', - 'genbank common name', - 'genbank synonym', - 'synonym' + "common name", + "equivalent name", + "genbank common name", + "genbank synonym", + "synonym", ] synonyms_select = db.select( - ncbi_taxa_name.c.taxon_id, - ncbi_taxa_name.c.name + ncbi_taxa_name.c.taxon_id, ncbi_taxa_name.c.name ).filter( ncbi_taxa_name.c.taxon_id.in_(taxonomy_id), - ncbi_taxa_name.c.name_class.in_(synonym_class) + ncbi_taxa_name.c.name_class.in_(synonym_class), ) for x in self.taxonomy_db.execute(synonyms_select): - taxons[x.taxon_id]['synonym'].append(x.name) + taxons[x.taxon_id]["synonym"].append(x.name) return taxons - def fetch_genomes(self, - genome_id=None, genome_uuid=None, - assembly_accession=None, - ensembl_name=None, taxonomy_id=None, - unreleased_only=False, - site_name=None, release_type=None, release_version=None, current_only=True - ): + def fetch_genomes( + self, + genome_id=None, + genome_uuid=None, + assembly_accession=None, + ensembl_name=None, + taxonomy_id=None, + unreleased_only=False, + site_name=None, + release_type=None, + release_version=None, + current_only=True, + ): genome_id = check_parameter(genome_id) genome_uuid = check_parameter(genome_uuid) assembly_accession = check_parameter(assembly_accession) ensembl_name = check_parameter(ensembl_name) taxonomy_id = check_parameter(taxonomy_id) - genome = db.Table('genome', self.md, autoload_with=self.metadata_db) - assembly = self.md.tables['assembly'] - organism = self.md.tables['organism'] + genome = db.Table("genome", self.md, autoload_with=self.metadata_db) + assembly = self.md.tables["assembly"] + organism = self.md.tables["organism"] - genome_select = db.select( + genome_select = ( + db.select( genome.c.genome_id, genome.c.genome_uuid, organism.c.ensembl_name, @@ -214,35 +241,51 @@ def fetch_genomes(self, organism.c.display_name, organism.c.strain, organism.c.taxonomy_id, - assembly.c.accession.label('assembly_accession'), - assembly.c.name.label('assembly_name'), - assembly.c.ucsc_name.label('assembly_ucsc_name'), - assembly.c.level.label('assembly_level') - ).select_from(genome).join(assembly).join(organism) + assembly.c.accession.label("assembly_accession"), + assembly.c.name.label("assembly_name"), + assembly.c.ucsc_name.label("assembly_ucsc_name"), + assembly.c.level.label("assembly_level"), + ) + .select_from(genome) + .join(assembly) + .join(organism) + ) if unreleased_only: - genome_release = db.Table('genome_release', self.md, autoload_with=self.metadata_db) + genome_release = db.Table( + "genome_release", self.md, autoload_with=self.metadata_db + ) - genome_select = genome_select.outerjoin(genome_release).filter_by(genome_id=None) + genome_select = genome_select.outerjoin(genome_release).filter_by( + genome_id=None + ) elif site_name is not None: - genome_release = db.Table('genome_release', self.md, autoload_with=self.metadata_db) - release = self.md.tables['ensembl_release'] - site = self.md.tables['ensembl_site'] - - genome_select = genome_select.join( - genome_release).join( - release).join( - site).filter_by(name=site_name) + genome_release = db.Table( + "genome_release", self.md, autoload_with=self.metadata_db + ) + release = self.md.tables["ensembl_release"] + site = self.md.tables["ensembl_site"] + + genome_select = ( + genome_select.join(genome_release) + .join(release) + .join(site) + .filter_by(name=site_name) + ) if release_type is not None: - genome_select = genome_select.filter(release.c.release_type == release_type) + genome_select = genome_select.filter( + release.c.release_type == release_type + ) if current_only: genome_select = genome_select.filter(genome_release.c.is_current == 1) if release_version is not None: - genome_select = genome_select.filter(release.c.version <= release_version) + genome_select = genome_select.filter( + release.c.version <= release_version + ) # These options are in order of decreasing specificity, # and thus the ones later in the list can be redundant. @@ -251,11 +294,17 @@ def fetch_genomes(self, elif genome_uuid is not None: genome_select = genome_select.filter(genome.c.genome_uuid.in_(genome_uuid)) elif assembly_accession is not None: - genome_select = genome_select.filter(assembly.c.accession.in_(assembly_accession)) + genome_select = genome_select.filter( + assembly.c.accession.in_(assembly_accession) + ) elif ensembl_name is not None: - genome_select = genome_select.filter(organism.c.ensembl_name.in_(ensembl_name)) + genome_select = genome_select.filter( + organism.c.ensembl_name.in_(ensembl_name) + ) elif taxonomy_id is not None: - genome_select = genome_select.filter(organism.c.taxonomy_id.in_(taxonomy_id)) + genome_select = genome_select.filter( + organism.c.taxonomy_id.in_(taxonomy_id) + ) for result in self.metadata_db_session.execute(genome_select): taxon_names = self.taxon_names[result.taxonomy_id] @@ -263,128 +312,174 @@ def fetch_genomes(self, result_dict.update(taxon_names) yield result_dict - def fetch_genomes_by_genome_uuid(self, - genome_uuid, - unreleased_only=False, - site_name=None, release_type=None, - release_version=None, current_only=True - ): - return self.fetch_genomes(genome_uuid=genome_uuid, - unreleased_only=unreleased_only, - site_name=site_name, - release_type=release_type, - release_version=release_version, - current_only=current_only) - - def fetch_genomes_by_assembly_accession(self, - assembly_accession, - unreleased_only=False, - site_name=None, release_type=None, - release_version=None, current_only=True - ): - return self.fetch_genomes(assembly_accession=assembly_accession, - unreleased_only=unreleased_only, - site_name=site_name, - release_type=release_type, - release_version=release_version, - current_only=current_only) - - def fetch_genomes_by_ensembl_name(self, - ensembl_name, - unreleased_only=False, - site_name=None, release_type=None, - release_version=None, current_only=True - ): - return self.fetch_genomes(ensembl_name=ensembl_name, - unreleased_only=unreleased_only, - site_name=site_name, - release_type=release_type, - release_version=release_version, - current_only=current_only) - - def fetch_genomes_by_taxonomy_id(self, - taxonomy_id, - unreleased_only=False, - site_name=None, release_type=None, - release_version=None, current_only=True - ): - return self.fetch_genomes(taxonomy_id=taxonomy_id, - unreleased_only=unreleased_only, - site_name=site_name, - release_type=release_type, - release_version=release_version, - current_only=current_only) - - def fetch_genomes_by_scientific_name(self, - scientific_name, - unreleased_only=False, - site_name=None, release_type=None, - release_version=None, current_only=True - ): - taxonomy_ids = [t_id for t_id in self.taxon_names - if self.taxon_names[t_id]['scientific_name'] == scientific_name] - - return self.fetch_genomes_by_taxonomy_id(taxonomy_ids, - unreleased_only=unreleased_only, - site_name=site_name, - release_type=release_type, - release_version=release_version, - current_only=current_only) - - def fetch_genomes_by_synonym(self, - synonym, - unreleased_only=False, - site_name=None, release_type=None, - release_version=None, current_only=True - ): + def fetch_genomes_by_genome_uuid( + self, + genome_uuid, + unreleased_only=False, + site_name=None, + release_type=None, + release_version=None, + current_only=True, + ): + return self.fetch_genomes( + genome_uuid=genome_uuid, + unreleased_only=unreleased_only, + site_name=site_name, + release_type=release_type, + release_version=release_version, + current_only=current_only, + ) + + def fetch_genomes_by_assembly_accession( + self, + assembly_accession, + unreleased_only=False, + site_name=None, + release_type=None, + release_version=None, + current_only=True, + ): + return self.fetch_genomes( + assembly_accession=assembly_accession, + unreleased_only=unreleased_only, + site_name=site_name, + release_type=release_type, + release_version=release_version, + current_only=current_only, + ) + + def fetch_genomes_by_ensembl_name( + self, + ensembl_name, + unreleased_only=False, + site_name=None, + release_type=None, + release_version=None, + current_only=True, + ): + return self.fetch_genomes( + ensembl_name=ensembl_name, + unreleased_only=unreleased_only, + site_name=site_name, + release_type=release_type, + release_version=release_version, + current_only=current_only, + ) + + def fetch_genomes_by_taxonomy_id( + self, + taxonomy_id, + unreleased_only=False, + site_name=None, + release_type=None, + release_version=None, + current_only=True, + ): + return self.fetch_genomes( + taxonomy_id=taxonomy_id, + unreleased_only=unreleased_only, + site_name=site_name, + release_type=release_type, + release_version=release_version, + current_only=current_only, + ) + + def fetch_genomes_by_scientific_name( + self, + scientific_name, + unreleased_only=False, + site_name=None, + release_type=None, + release_version=None, + current_only=True, + ): + taxonomy_ids = [ + t_id + for t_id in self.taxon_names + if self.taxon_names[t_id]["scientific_name"] == scientific_name + ] + + return self.fetch_genomes_by_taxonomy_id( + taxonomy_ids, + unreleased_only=unreleased_only, + site_name=site_name, + release_type=release_type, + release_version=release_version, + current_only=current_only, + ) + + def fetch_genomes_by_synonym( + self, + synonym, + unreleased_only=False, + site_name=None, + release_type=None, + release_version=None, + current_only=True, + ): taxonomy_ids = [] for taxon_id in self.taxon_names: - if synonym.casefold() in [x.casefold() for x in self.taxon_names[taxon_id]['synonym']]: + if synonym.casefold() in [ + x.casefold() for x in self.taxon_names[taxon_id]["synonym"] + ]: taxonomy_ids.append(taxon_id) - return self.fetch_genomes_by_taxonomy_id(taxonomy_ids, - unreleased_only=unreleased_only, - site_name=site_name, - release_type=release_type, - release_version=release_version, - current_only=current_only) - - def fetch_sequences(self, - genome_id=None, genome_uuid=None, - assembly_accession=None, - chromosomal_only=False - ): + return self.fetch_genomes_by_taxonomy_id( + taxonomy_ids, + unreleased_only=unreleased_only, + site_name=site_name, + release_type=release_type, + release_version=release_version, + current_only=current_only, + ) + + def fetch_sequences( + self, + genome_id=None, + genome_uuid=None, + assembly_accession=None, + chromosomal_only=False, + ): genome_id = check_parameter(genome_id) genome_uuid = check_parameter(genome_uuid) assembly_accession = check_parameter(assembly_accession) - assembly = db.Table('assembly', self.md, autoload_with=self.metadata_db) - assembly_sequence = db.Table('assembly_sequence', self.md, autoload_with=self.metadata_db) + assembly = db.Table("assembly", self.md, autoload_with=self.metadata_db) + assembly_sequence = db.Table( + "assembly_sequence", self.md, autoload_with=self.metadata_db + ) - seq_select = db.select( + seq_select = ( + db.select( assembly_sequence.c.accession, assembly_sequence.c.name, assembly_sequence.c.sequence_location, assembly_sequence.c.length, assembly_sequence.c.chromosomal, assembly_sequence.c.sequence_checksum, - assembly_sequence.c.ga4gh_identifier - ).select_from( - assembly).join( - assembly_sequence, assembly.c.assembly_id == assembly_sequence.c.assembly_id) + assembly_sequence.c.ga4gh_identifier, + ) + .select_from(assembly) + .join( + assembly_sequence, + assembly.c.assembly_id == assembly_sequence.c.assembly_id, + ) + ) if chromosomal_only: seq_select = seq_select.filter_by(chromosomal=1) # These options are in order of decreasing specificity, # and thus the ones later in the list can be redundant. if genome_id is not None: - genome = db.Table('genome', self.md, autoload_with=self.metadata_db) - seq_select = seq_select.join( - genome).filter(genome.c.genome_id.in_(genome_id)) + genome = db.Table("genome", self.md, autoload_with=self.metadata_db) + seq_select = seq_select.join(genome).filter( + genome.c.genome_id.in_(genome_id) + ) elif genome_uuid is not None: - genome = db.Table('genome', self.md, autoload_with=self.metadata_db) - seq_select = seq_select.join( - genome).filter(genome.c.genome_uuid.in_(genome_uuid)) + genome = db.Table("genome", self.md, autoload_with=self.metadata_db) + seq_select = seq_select.join(genome).filter( + genome.c.genome_uuid.in_(genome_uuid) + ) elif assembly_accession is not None: seq_select = seq_select.filter(assembly.c.accession.in_(assembly_accession)) @@ -392,9 +487,13 @@ def fetch_sequences(self, yield dict(result) def fetch_sequences_by_genome_uuid(self, genome_uuid, chromosomal_only=False): - return self.fetch_sequences(genome_uuid=genome_uuid, - chromosomal_only=chromosomal_only) + return self.fetch_sequences( + genome_uuid=genome_uuid, chromosomal_only=chromosomal_only + ) - def fetch_sequences_by_assembly_accession(self, assembly_accession, chromosomal_only=False): - return self.fetch_sequences(assembly_accession=assembly_accession, - chromosomal_only=chromosomal_only) + def fetch_sequences_by_assembly_accession( + self, assembly_accession, chromosomal_only=False + ): + return self.fetch_sequences( + assembly_accession=assembly_accession, chromosomal_only=chromosomal_only + ) diff --git a/src/ensembl/production/metadata/models.py b/src/ensembl/production/metadata/models.py new file mode 100644 index 00000000..eb6bb9a0 --- /dev/null +++ b/src/ensembl/production/metadata/models.py @@ -0,0 +1,217 @@ +# coding: utf-8 +from sqlalchemy import Column, DECIMAL, Date, DateTime, ForeignKey, Index, Integer, String +from sqlalchemy.dialects.mysql import DATETIME, TINYINT +from sqlalchemy.orm import relationship +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() +metadata = Base.metadata + + +class Assembly(Base): + __tablename__ = 'assembly' + + assembly_id = Column(Integer, primary_key=True) + ucsc_name = Column(String(16)) + accession = Column(String(16), nullable=False, unique=True) + level = Column(String(32), nullable=False) + name = Column(String(128), nullable=False) + accession_body = Column(String(32)) + assembly_default = Column(String(32)) + tolid = Column(String(32), unique=True) + created = Column(DateTime) + ensembl_name = Column(String(255), unique=True) + + +class Attribute(Base): + __tablename__ = 'attribute' + + attribute_id = Column(Integer, primary_key=True) + name = Column(String(128), nullable=False) + label = Column(String(128), nullable=False) + description = Column(String(255)) + + +class DatasetSource(Base): + __tablename__ = 'dataset_source' + + dataset_source_id = Column(Integer, primary_key=True) + type = Column(String(32), nullable=False) + name = Column(String(255), nullable=False, unique=True) + + +class DatasetType(Base): + __tablename__ = 'dataset_type' + + dataset_type_id = Column(Integer, primary_key=True) + name = Column(String(32), nullable=False) + label = Column(String(128), nullable=False) + topic = Column(String(32), nullable=False) + description = Column(String(255)) + details_uri = Column(String(255)) + + +class DjangoMigration(Base): + __tablename__ = 'django_migrations' + + id = Column(Integer, primary_key=True) + app = Column(String(255), nullable=False) + name = Column(String(255), nullable=False) + applied = Column(DATETIME(fsp=6), nullable=False) + + +class EnsemblSite(Base): + __tablename__ = 'ensembl_site' + + site_id = Column(Integer, primary_key=True) + name = Column(String(64), nullable=False) + label = Column(String(64), nullable=False) + uri = Column(String(64), nullable=False) + + +class Organism(Base): + __tablename__ = 'organism' + + organism_id = Column(Integer, primary_key=True) + taxonomy_id = Column(Integer, nullable=False) + species_taxonomy_id = Column(Integer) + display_name = Column(String(128), nullable=False) + strain = Column(String(128)) + scientific_name = Column(String(128)) + url_name = Column(String(128), nullable=False) + ensembl_name = Column(String(128), nullable=False, unique=True) + scientific_parlance_name = Column(String(255)) + + +class OrganismGroup(Base): + __tablename__ = 'organism_group' + __table_args__ = ( + Index('group_type_name_63c2f6ac_uniq', 'type', 'name', unique=True), + ) + + organism_group_id = Column(Integer, primary_key=True) + type = Column(String(32), nullable=False) + name = Column(String(255), nullable=False) + code = Column(String(48), unique=True) + + +class AssemblySequence(Base): + __tablename__ = 'assembly_sequence' + __table_args__ = ( + Index('assembly_sequence_assembly_id_accession_5f3e5119_uniq', 'assembly_id', 'accession', unique=True), + ) + + assembly_sequence_id = Column(Integer, primary_key=True) + name = Column(String(128)) + assembly_id = Column(ForeignKey('assembly.assembly_id'), nullable=False, index=True) + accession = Column(String(32), nullable=False) + chromosomal = Column(TINYINT(1), nullable=False) + length = Column(Integer, nullable=False) + sequence_location = Column(String(10)) + sequence_checksum = Column(String(32)) + ga4gh_identifier = Column(String(32)) + + assembly = relationship('Assembly') + + +class Dataset(Base): + __tablename__ = 'dataset' + + dataset_id = Column(Integer, primary_key=True) + dataset_uuid = Column(String(128), nullable=False, unique=True) + dataset_type_id = Column(ForeignKey('dataset_type.dataset_type_id'), nullable=False, index=True) + name = Column(String(128), nullable=False) + version = Column(String(128)) + created = Column(DATETIME(fsp=6), nullable=False) + dataset_source_id = Column(ForeignKey('dataset_source.dataset_source_id'), nullable=False, index=True) + label = Column(String(128), nullable=False) + + dataset_source = relationship('DatasetSource') + dataset_type = relationship('DatasetType') + + +class EnsemblRelease(Base): + __tablename__ = 'ensembl_release' + __table_args__ = ( + Index('ensembl_release_version_site_id_b743399a_uniq', 'version', 'site_id', unique=True), + ) + + release_id = Column(Integer, primary_key=True) + version = Column(DECIMAL(10, 1), nullable=False) + release_date = Column(Date, nullable=False) + label = Column(String(64)) + is_current = Column(TINYINT(1), nullable=False) + site_id = Column(ForeignKey('ensembl_site.site_id'), index=True) + release_type = Column(String(16), nullable=False) + + site = relationship('EnsemblSite') + + +class Genome(Base): + __tablename__ = 'genome' + + genome_id = Column(Integer, primary_key=True) + genome_uuid = Column(String(128), nullable=False, unique=True) + assembly_id = Column(ForeignKey('assembly.assembly_id'), nullable=False, index=True) + organism_id = Column(ForeignKey('organism.organism_id'), nullable=False, index=True) + created = Column(DATETIME(fsp=6), nullable=False) + + assembly = relationship('Assembly') + organism = relationship('Organism') + + +class OrganismGroupMember(Base): + __tablename__ = 'organism_group_member' + __table_args__ = ( + Index('organism_group_member_organism_id_organism_gro_fe8f49ac_uniq', 'organism_id', 'organism_group_id', unique=True), + ) + + organism_group_member_id = Column(Integer, primary_key=True) + is_reference = Column(TINYINT(1), nullable=False) + organism_id = Column(ForeignKey('organism.organism_id'), nullable=False) + organism_group_id = Column(ForeignKey('organism_group.organism_group_id'), nullable=False, index=True) + + organism_group = relationship('OrganismGroup') + organism = relationship('Organism') + + +class DatasetAttribute(Base): + __tablename__ = 'dataset_attribute' + __table_args__ = ( + Index('dataset_attribute_dataset_id_attribute_id__d3b34d8c_uniq', 'dataset_id', 'attribute_id', 'type', 'value', unique=True), + ) + + dataset_attribute_id = Column(Integer, primary_key=True) + type = Column(String(32), nullable=False) + value = Column(String(128), nullable=False) + attribute_id = Column(ForeignKey('attribute.attribute_id'), nullable=False, index=True) + dataset_id = Column(ForeignKey('dataset.dataset_id'), nullable=False, index=True) + + attribute = relationship('Attribute') + dataset = relationship('Dataset') + + +class GenomeDataset(Base): + __tablename__ = 'genome_dataset' + + genome_dataset_id = Column(Integer, primary_key=True) + dataset_id = Column(ForeignKey('dataset.dataset_id'), nullable=False, index=True) + genome_id = Column(ForeignKey('genome.genome_id'), nullable=False, index=True) + release_id = Column(ForeignKey('ensembl_release.release_id'), nullable=False, index=True) + is_current = Column(TINYINT(1), nullable=False) + + dataset = relationship('Dataset') + genome = relationship('Genome') + release = relationship('EnsemblRelease') + + +class GenomeRelease(Base): + __tablename__ = 'genome_release' + + genome_release_id = Column(Integer, primary_key=True) + genome_id = Column(ForeignKey('genome.genome_id'), nullable=False, index=True) + release_id = Column(ForeignKey('ensembl_release.release_id'), nullable=False, index=True) + is_current = Column(TINYINT(1), nullable=False) + + genome = relationship('Genome') + release = relationship('EnsemblRelease') \ No newline at end of file From 64a40f8206ab2b79411584cd16dc8a6140d91e0b Mon Sep 17 00:00:00 2001 From: dpopleton Date: Fri, 23 Sep 2022 08:56:38 -0400 Subject: [PATCH 02/14] Rearanged Models, added relationships, added taxonomy. NOT TESTED. Do not merge with master. --- src/ensembl/production/metadata/models.py | 245 +++++++++++++--------- 1 file changed, 144 insertions(+), 101 deletions(-) diff --git a/src/ensembl/production/metadata/models.py b/src/ensembl/production/metadata/models.py index eb6bb9a0..5ad33f83 100644 --- a/src/ensembl/production/metadata/models.py +++ b/src/ensembl/production/metadata/models.py @@ -1,12 +1,14 @@ # coding: utf-8 from sqlalchemy import Column, DECIMAL, Date, DateTime, ForeignKey, Index, Integer, String from sqlalchemy.dialects.mysql import DATETIME, TINYINT -from sqlalchemy.orm import relationship +from sqlalchemy.orm import relationship, sessionmaker, backref from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import create_engine, MetaData, inspect Base = declarative_base() metadata = Base.metadata +# Currently the backreference is a plural of the class Assembly(Base): __tablename__ = 'assembly' @@ -23,6 +25,24 @@ class Assembly(Base): ensembl_name = Column(String(255), unique=True) +class AssemblySequence(Base): + __tablename__ = 'assembly_sequence' + __table_args__ = ( + Index('assembly_sequence_assembly_id_accession_5f3e5119_uniq', 'assembly_id', 'accession', unique=True), + ) + + assembly_sequence_id = Column(Integer, primary_key=True) + name = Column(String(128)) + assembly_id = Column(ForeignKey('assembly.assembly_id'), nullable=False, index=True) + accession = Column(String(32), nullable=False) + chromosomal = Column(TINYINT(1), nullable=False) + length = Column(Integer, nullable=False) + sequence_location = Column(String(10)) + sequence_checksum = Column(String(32)) + ga4gh_identifier = Column(String(32)) + assembly = relationship('Assembly', backref="assembly") + + class Attribute(Base): __tablename__ = 'attribute' @@ -32,6 +52,39 @@ class Attribute(Base): description = Column(String(255)) + +class Dataset(Base): + __tablename__ = 'dataset' + + dataset_id = Column(Integer, primary_key=True) + dataset_uuid = Column(String(128), nullable=False, unique=True) + dataset_type_id = Column(ForeignKey('dataset_type.dataset_type_id'), nullable=False, index=True) + name = Column(String(128), nullable=False) + version = Column(String(128)) + created = Column(DATETIME(fsp=6), nullable=False) + dataset_source_id = Column(ForeignKey('dataset_source.dataset_source_id'), nullable=False, index=True) + label = Column(String(128), nullable=False) + + dataset_source = relationship('DatasetSource', backref="dataset") + dataset_type = relationship('DatasetType', backref="dataset") + + +class DatasetAttribute(Base): + __tablename__ = 'dataset_attribute' + __table_args__ = ( + Index('dataset_attribute_dataset_id_attribute_id__d3b34d8c_uniq', 'dataset_id', 'attribute_id', 'type', 'value', unique=True), + ) + + dataset_attribute_id = Column(Integer, primary_key=True) + type = Column(String(32), nullable=False) + value = Column(String(128), nullable=False) + attribute_id = Column(ForeignKey('attribute.attribute_id'), nullable=False, index=True) + dataset_id = Column(ForeignKey('dataset.dataset_id'), nullable=False, index=True) + + attribute = relationship('Attribute', backref="dataset_attribute") + dataset = relationship('Dataset', backref="dataset_attribute") + + class DatasetSource(Base): __tablename__ = 'dataset_source' @@ -69,67 +122,6 @@ class EnsemblSite(Base): uri = Column(String(64), nullable=False) -class Organism(Base): - __tablename__ = 'organism' - - organism_id = Column(Integer, primary_key=True) - taxonomy_id = Column(Integer, nullable=False) - species_taxonomy_id = Column(Integer) - display_name = Column(String(128), nullable=False) - strain = Column(String(128)) - scientific_name = Column(String(128)) - url_name = Column(String(128), nullable=False) - ensembl_name = Column(String(128), nullable=False, unique=True) - scientific_parlance_name = Column(String(255)) - - -class OrganismGroup(Base): - __tablename__ = 'organism_group' - __table_args__ = ( - Index('group_type_name_63c2f6ac_uniq', 'type', 'name', unique=True), - ) - - organism_group_id = Column(Integer, primary_key=True) - type = Column(String(32), nullable=False) - name = Column(String(255), nullable=False) - code = Column(String(48), unique=True) - - -class AssemblySequence(Base): - __tablename__ = 'assembly_sequence' - __table_args__ = ( - Index('assembly_sequence_assembly_id_accession_5f3e5119_uniq', 'assembly_id', 'accession', unique=True), - ) - - assembly_sequence_id = Column(Integer, primary_key=True) - name = Column(String(128)) - assembly_id = Column(ForeignKey('assembly.assembly_id'), nullable=False, index=True) - accession = Column(String(32), nullable=False) - chromosomal = Column(TINYINT(1), nullable=False) - length = Column(Integer, nullable=False) - sequence_location = Column(String(10)) - sequence_checksum = Column(String(32)) - ga4gh_identifier = Column(String(32)) - - assembly = relationship('Assembly') - - -class Dataset(Base): - __tablename__ = 'dataset' - - dataset_id = Column(Integer, primary_key=True) - dataset_uuid = Column(String(128), nullable=False, unique=True) - dataset_type_id = Column(ForeignKey('dataset_type.dataset_type_id'), nullable=False, index=True) - name = Column(String(128), nullable=False) - version = Column(String(128)) - created = Column(DATETIME(fsp=6), nullable=False) - dataset_source_id = Column(ForeignKey('dataset_source.dataset_source_id'), nullable=False, index=True) - label = Column(String(128), nullable=False) - - dataset_source = relationship('DatasetSource') - dataset_type = relationship('DatasetType') - - class EnsemblRelease(Base): __tablename__ = 'ensembl_release' __table_args__ = ( @@ -144,7 +136,7 @@ class EnsemblRelease(Base): site_id = Column(ForeignKey('ensembl_site.site_id'), index=True) release_type = Column(String(16), nullable=False) - site = relationship('EnsemblSite') + site = relationship('EnsemblSite', backref='ensembl_release') class Genome(Base): @@ -156,39 +148,8 @@ class Genome(Base): organism_id = Column(ForeignKey('organism.organism_id'), nullable=False, index=True) created = Column(DATETIME(fsp=6), nullable=False) - assembly = relationship('Assembly') - organism = relationship('Organism') - - -class OrganismGroupMember(Base): - __tablename__ = 'organism_group_member' - __table_args__ = ( - Index('organism_group_member_organism_id_organism_gro_fe8f49ac_uniq', 'organism_id', 'organism_group_id', unique=True), - ) - - organism_group_member_id = Column(Integer, primary_key=True) - is_reference = Column(TINYINT(1), nullable=False) - organism_id = Column(ForeignKey('organism.organism_id'), nullable=False) - organism_group_id = Column(ForeignKey('organism_group.organism_group_id'), nullable=False, index=True) - - organism_group = relationship('OrganismGroup') - organism = relationship('Organism') - - -class DatasetAttribute(Base): - __tablename__ = 'dataset_attribute' - __table_args__ = ( - Index('dataset_attribute_dataset_id_attribute_id__d3b34d8c_uniq', 'dataset_id', 'attribute_id', 'type', 'value', unique=True), - ) - - dataset_attribute_id = Column(Integer, primary_key=True) - type = Column(String(32), nullable=False) - value = Column(String(128), nullable=False) - attribute_id = Column(ForeignKey('attribute.attribute_id'), nullable=False, index=True) - dataset_id = Column(ForeignKey('dataset.dataset_id'), nullable=False, index=True) - - attribute = relationship('Attribute') - dataset = relationship('Dataset') + assembly = relationship('Assembly', backref="genome") + organism = relationship('Organism', backref="genome") class GenomeDataset(Base): @@ -200,9 +161,9 @@ class GenomeDataset(Base): release_id = Column(ForeignKey('ensembl_release.release_id'), nullable=False, index=True) is_current = Column(TINYINT(1), nullable=False) - dataset = relationship('Dataset') - genome = relationship('Genome') - release = relationship('EnsemblRelease') + dataset = relationship('Dataset', backref="genome_dataset") + genome = relationship('Genome', backref="genome_dataset") + release = relationship('EnsemblRelease', backref="genome_dataset") class GenomeRelease(Base): @@ -213,5 +174,87 @@ class GenomeRelease(Base): release_id = Column(ForeignKey('ensembl_release.release_id'), nullable=False, index=True) is_current = Column(TINYINT(1), nullable=False) - genome = relationship('Genome') - release = relationship('EnsemblRelease') \ No newline at end of file + genome = relationship('Genome', backref='genome_release') + release = relationship('EnsemblRelease', backref='genome_release') + + + +class Organism(Base): + __tablename__ = 'organism' + + organism_id = Column(Integer, primary_key=True) + #taxonomy_id = Column(Integer, nullable=False) + #species_taxonomy_id = Column(Integer) + display_name = Column(String(128), nullable=False) + strain = Column(String(128)) + scientific_name = Column(String(128)) + url_name = Column(String(128), nullable=False) + ensembl_name = Column(String(128), nullable=False, unique=True) + scientific_parlance_name = Column(String(255)) + + # These are for the taxonomy that is in this document. Commented out fields are for if we remove it. + taxonomy_id = Column(ForeignKey('taxonomy_node.taxon_id'), nullable=False) + species_taxonomy_id = Column(ForeignKey('taxonomy_node.taxon_id')) + taxnode = relationship('TaxonomyNode', backref='organism') #no idea whether these will break in the future 2 to 1 relationship seems wrong. + + +#Taxonomy relationships. Currently these were added as Ensembl/ensembl-metadata-admin/blob/main/ncbi_taxonomy/models.py +# works on the djano implementation of sqlalchemy rather than the sqlalchemy ORM that is in use here. +class TaxonomyNode(Base): + __tablename__ = 'taxonomy_node' + # Not sure what we are doing here, as the db doesn't have it. + # Could check from the regular metadata.... + # Also check for the unique constraints and whether they are nullable. + # __table_args__ = (???) + taxon_id = Column(Integer, primary_key=True) + parent = Column(Integer) + rank = Column(String(255)) + genbank_hidden_flag = Column(Integer) + left_index = Column(Integer) + right_index = Column(Integer) + root_id = Column(Integer) + #Not including the relationship for taxon_id to parent as I think the schema is wrong. + + +class TaxonomyName(Base): + __tablename__ = 'taxonomy_name' + # Not sure what we are doing here, as the db doesn't have it. + # Could check from the regular metadata.... + # Also check for the unique constraints and whether they are nullable. + # __table_args__ = (???) + name_id = Column(Integer, primary_key=True) + parent = Column(ForeignKey('taxonomy_node.taxon_id')) + name = Column(String(255)) + name_class = Column(String(255)) + taxnode = relationship('TaxonomyNode', backref='taxonomy_name') + + + + +class OrganismGroup(Base): + __tablename__ = 'organism_group' + __table_args__ = ( + Index('group_type_name_63c2f6ac_uniq', 'type', 'name', unique=True), + ) + + organism_group_id = Column(Integer, primary_key=True) + type = Column(String(32), nullable=False) + name = Column(String(255), nullable=False) + code = Column(String(48), unique=True) + + +class OrganismGroupMember(Base): + __tablename__ = 'organism_group_member' + __table_args__ = ( + Index('organism_group_member_organism_id_organism_gro_fe8f49ac_uniq', 'organism_id', 'organism_group_id', unique=True), + ) + + organism_group_member_id = Column(Integer, primary_key=True) + is_reference = Column(TINYINT(1), nullable=False) + organism_id = Column(ForeignKey('organism.organism_id'), nullable=False) + organism_group_id = Column(ForeignKey('organism_group.organism_group_id'), nullable=False, index=True) + + organism_group = relationship('OrganismGroup', backref='organism_group_member') + organism = relationship('Organism', backref='organism_group_member') + + From a4806f7053e92a36cf685bf7a2d71f11eb37f15c Mon Sep 17 00:00:00 2001 From: dpopleton Date: Mon, 26 Sep 2022 04:41:51 -0400 Subject: [PATCH 03/14] Added ensembly-py dependency, cleaned models --- requirements.in | 3 +- requirements.txt | 3 +- setup.py | 3 + src/ensembl/production/metadata/api.py | 31 +++++----- src/ensembl/production/metadata/models.py | 73 ++++++----------------- 5 files changed, 41 insertions(+), 72 deletions(-) diff --git a/requirements.in b/requirements.in index 0b9c3329..d4903edf 100644 --- a/requirements.in +++ b/requirements.in @@ -2,4 +2,5 @@ mysqlclient pymysql sqlalchemy types-pymysql -fastapi \ No newline at end of file +fastapi +ensembl-py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5cb401dc..5ca9f31f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,5 @@ sqlalchemy==1.4.21 types-pymysql==1.0.0 # via -r requirements.in fastapi==0.83.0 - # via -r requirements.in \ No newline at end of file + # via -r requirements.in +git+https://github.com/Ensembl/ensembl-py.git@main \ No newline at end of file diff --git a/setup.py b/setup.py index e39fc5d5..fa46d1fe 100644 --- a/setup.py +++ b/setup.py @@ -41,5 +41,8 @@ 'Programming Language :: Python', 'Programming Language :: Python :: 3', "Programming Language :: Python :: 3.8", + ], + install_requires=[ + 'ensembl-py @ git+https://github.com/Ensembl/ensembl-py.git@main', ] ) diff --git a/src/ensembl/production/metadata/api.py b/src/ensembl/production/metadata/api.py index 9d30134e..b4bf03cf 100644 --- a/src/ensembl/production/metadata/api.py +++ b/src/ensembl/production/metadata/api.py @@ -12,7 +12,7 @@ import sqlalchemy as db from sqlalchemy.orm import Session import pymysql - +import models from ensembl.production.metadata.config import MetadataConfig pymysql.install_as_MySQLdb() @@ -21,20 +21,21 @@ -#Looks good right here. -def load_database(uri): - try: - engine = db.create_engine(uri) - except AttributeError as err: - raise ValueError(f"Could not connect to database {uri}: {err}.") from err - - try: - connection = engine.connect() - except db.exc.OperationalError as err: - raise ValueError(f"Could not connect to database {uri}: {err}.") from err - - connection.close() - return engine +#Replace with the DBconnection interface from ensembl-py.database.dbconnection.py +#Remove after tests are acceptable. +#def load_database(uri): +# try: +# engine = db.create_engine(uri) +# except AttributeError as err: +# raise ValueError(f"Could not connect to database {uri}: {err}.") from err +# +# try: +# connection = engine.connect() +# except db.exc.OperationalError as err: +# raise ValueError(f"Could not connect to database {uri}: {err}.") from err +# +# connection.close() +# return engine diff --git a/src/ensembl/production/metadata/models.py b/src/ensembl/production/metadata/models.py index 5ad33f83..7b7f83f2 100644 --- a/src/ensembl/production/metadata/models.py +++ b/src/ensembl/production/metadata/models.py @@ -1,14 +1,28 @@ -# coding: utf-8 +# See the NOTICE file distributed with this work for additional information +# regarding copyright ownership. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + from sqlalchemy import Column, DECIMAL, Date, DateTime, ForeignKey, Index, Integer, String from sqlalchemy.dialects.mysql import DATETIME, TINYINT from sqlalchemy.orm import relationship, sessionmaker, backref from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import create_engine, MetaData, inspect + Base = declarative_base() metadata = Base.metadata -# Currently the backreference is a plural of the class Assembly(Base): __tablename__ = 'assembly' @@ -52,7 +66,6 @@ class Attribute(Base): description = Column(String(255)) - class Dataset(Base): __tablename__ = 'dataset' @@ -104,15 +117,6 @@ class DatasetType(Base): details_uri = Column(String(255)) -class DjangoMigration(Base): - __tablename__ = 'django_migrations' - - id = Column(Integer, primary_key=True) - app = Column(String(255), nullable=False) - name = Column(String(255), nullable=False) - applied = Column(DATETIME(fsp=6), nullable=False) - - class EnsemblSite(Base): __tablename__ = 'ensembl_site' @@ -178,13 +182,12 @@ class GenomeRelease(Base): release = relationship('EnsemblRelease', backref='genome_release') - class Organism(Base): __tablename__ = 'organism' organism_id = Column(Integer, primary_key=True) - #taxonomy_id = Column(Integer, nullable=False) - #species_taxonomy_id = Column(Integer) + taxonomy_id = Column(Integer, nullable=False) + species_taxonomy_id = Column(Integer) display_name = Column(String(128), nullable=False) strain = Column(String(128)) scientific_name = Column(String(128)) @@ -192,44 +195,6 @@ class Organism(Base): ensembl_name = Column(String(128), nullable=False, unique=True) scientific_parlance_name = Column(String(255)) - # These are for the taxonomy that is in this document. Commented out fields are for if we remove it. - taxonomy_id = Column(ForeignKey('taxonomy_node.taxon_id'), nullable=False) - species_taxonomy_id = Column(ForeignKey('taxonomy_node.taxon_id')) - taxnode = relationship('TaxonomyNode', backref='organism') #no idea whether these will break in the future 2 to 1 relationship seems wrong. - - -#Taxonomy relationships. Currently these were added as Ensembl/ensembl-metadata-admin/blob/main/ncbi_taxonomy/models.py -# works on the djano implementation of sqlalchemy rather than the sqlalchemy ORM that is in use here. -class TaxonomyNode(Base): - __tablename__ = 'taxonomy_node' - # Not sure what we are doing here, as the db doesn't have it. - # Could check from the regular metadata.... - # Also check for the unique constraints and whether they are nullable. - # __table_args__ = (???) - taxon_id = Column(Integer, primary_key=True) - parent = Column(Integer) - rank = Column(String(255)) - genbank_hidden_flag = Column(Integer) - left_index = Column(Integer) - right_index = Column(Integer) - root_id = Column(Integer) - #Not including the relationship for taxon_id to parent as I think the schema is wrong. - - -class TaxonomyName(Base): - __tablename__ = 'taxonomy_name' - # Not sure what we are doing here, as the db doesn't have it. - # Could check from the regular metadata.... - # Also check for the unique constraints and whether they are nullable. - # __table_args__ = (???) - name_id = Column(Integer, primary_key=True) - parent = Column(ForeignKey('taxonomy_node.taxon_id')) - name = Column(String(255)) - name_class = Column(String(255)) - taxnode = relationship('TaxonomyNode', backref='taxonomy_name') - - - class OrganismGroup(Base): __tablename__ = 'organism_group' @@ -256,5 +221,3 @@ class OrganismGroupMember(Base): organism_group = relationship('OrganismGroup', backref='organism_group_member') organism = relationship('Organism', backref='organism_group_member') - - From a486862efd451a7a0e9cab1d62d4ffc4fd7c424a Mon Sep 17 00:00:00 2001 From: dpopleton Date: Mon, 26 Sep 2022 07:12:08 -0400 Subject: [PATCH 04/14] Implemented Vinay's PR comments --- requirements.in | 1 - requirements.txt | 2 -- src/ensembl/production/metadata/api.py | 3 +-- tests/test_api.py | 6 ++++-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/requirements.in b/requirements.in index d4903edf..25419910 100644 --- a/requirements.in +++ b/requirements.in @@ -2,5 +2,4 @@ mysqlclient pymysql sqlalchemy types-pymysql -fastapi ensembl-py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5ca9f31f..2c2dbcfb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,4 @@ sqlalchemy==1.4.21 # via -r requirements.in types-pymysql==1.0.0 # via -r requirements.in -fastapi==0.83.0 - # via -r requirements.in git+https://github.com/Ensembl/ensembl-py.git@main \ No newline at end of file diff --git a/src/ensembl/production/metadata/api.py b/src/ensembl/production/metadata/api.py index b4bf03cf..68bfbee4 100644 --- a/src/ensembl/production/metadata/api.py +++ b/src/ensembl/production/metadata/api.py @@ -12,9 +12,8 @@ import sqlalchemy as db from sqlalchemy.orm import Session import pymysql -import models from ensembl.production.metadata.config import MetadataConfig - +import src.ensembl.production.metadata.models pymysql.install_as_MySQLdb() config = MetadataConfig() diff --git a/tests/test_api.py b/tests/test_api.py index ad2db065..bc37303e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -13,9 +13,11 @@ Unit tests for api module """ -from ensembl.production.metadata.api import load_database - def test_load_database(): """Test api.load_database function""" + #load_database('XXX') pass + + + From 018d5f57585337bd357caf1f85adf78fae7aaec8 Mon Sep 17 00:00:00 2001 From: dpopleton Date: Wed, 28 Sep 2022 09:50:22 -0400 Subject: [PATCH 05/14] Updated load_database for ORM --- setup.py | 2 +- src/ensembl/production/metadata/api.py | 48 +++++++++++--------------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/setup.py b/setup.py index fa46d1fe..957b67cc 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,6 @@ "Programming Language :: Python :: 3.8", ], install_requires=[ - 'ensembl-py @ git+https://github.com/Ensembl/ensembl-py.git@main', + 'ensembl-py @ git+https://github.com/Ensembl/ensembl-py.git', ] ) diff --git a/src/ensembl/production/metadata/api.py b/src/ensembl/production/metadata/api.py index 68bfbee4..e36e12fa 100644 --- a/src/ensembl/production/metadata/api.py +++ b/src/ensembl/production/metadata/api.py @@ -10,36 +10,34 @@ # See the License for the specific language governing permissions and # limitations under the License. import sqlalchemy as db -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker import pymysql from ensembl.production.metadata.config import MetadataConfig -import src.ensembl.production.metadata.models -pymysql.install_as_MySQLdb() -config = MetadataConfig() +from ensembl.database.dbconnection import DBConnection +from ensembl.production.metadata.models import * +#Database ORM connection. +class load_database(DBConnection): + """ + Load a database and directly create a session for ORM interaction with the database + """ + def create_session(self, engine): + self._session = Session(engine, future=True) + def __init__(self, url): + super().__init__(url) + self.create_session(self._engine) -#Replace with the DBconnection interface from ensembl-py.database.dbconnection.py -#Remove after tests are acceptable. -#def load_database(uri): -# try: -# engine = db.create_engine(uri) -# except AttributeError as err: -# raise ValueError(f"Could not connect to database {uri}: {err}.") from err -# -# try: -# connection = engine.connect() -# except db.exc.OperationalError as err: -# raise ValueError(f"Could not connect to database {uri}: {err}.") from err -# -# connection.close() -# return engine + #Commit any changes to the database and create a new session instance. + def commit(self): + self._session.commit() + self._session.close() + self.create_session(self._engine) - - - -#Not sure why all of this is here, but it is not being removed until we are certain. + #rollback any changes made before commiting the session instance. + def rollback(self): + self._session.rollback() def check_parameter(param): @@ -50,13 +48,9 @@ def check_parameter(param): class BaseAdaptor: def __init__(self, metadata_uri=None): - # This is sqlalchemy's metadata, not Ensembl's! - self.md = db.MetaData() - if metadata_uri is None: metadata_uri = config.METADATA_URI self.metadata_db = load_database(metadata_uri) - self.metadata_db_session = Session(self.metadata_db, future=True) class ReleaseAdaptor(BaseAdaptor): From e6d02d19ab0bec9619c373b5b10614b4029be06d Mon Sep 17 00:00:00 2001 From: dpopleton Date: Thu, 29 Sep 2022 19:15:34 -0400 Subject: [PATCH 06/14] Converted Release adapter and Base adapter to ORM --- requirements.in | 3 +- requirements.txt | 3 +- src/ensembl/production/metadata/api.py | 70 ++++++++++++++--------- src/ensembl/production/metadata/config.py | 8 ++- 4 files changed, 53 insertions(+), 31 deletions(-) diff --git a/requirements.in b/requirements.in index 25419910..0bc65d52 100644 --- a/requirements.in +++ b/requirements.in @@ -2,4 +2,5 @@ mysqlclient pymysql sqlalchemy types-pymysql -ensembl-py \ No newline at end of file +ensembl-py +mysql \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2c2dbcfb..5dd67e9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,5 @@ sqlalchemy==1.4.21 # via -r requirements.in types-pymysql==1.0.0 # via -r requirements.in -git+https://github.com/Ensembl/ensembl-py.git@main \ No newline at end of file +git+https://github.com/Ensembl/ensembl-py.git@main +mysql \ No newline at end of file diff --git a/src/ensembl/production/metadata/api.py b/src/ensembl/production/metadata/api.py index e36e12fa..5233cc29 100644 --- a/src/ensembl/production/metadata/api.py +++ b/src/ensembl/production/metadata/api.py @@ -10,9 +10,10 @@ # See the License for the specific language governing permissions and # limitations under the License. import sqlalchemy as db +from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker import pymysql -from ensembl.production.metadata.config import MetadataConfig +from ensembl.production.metadata.config import get_metadata_uri, get_taxonomy_uri from ensembl.database.dbconnection import DBConnection from ensembl.production.metadata.models import * @@ -49,7 +50,7 @@ def check_parameter(param): class BaseAdaptor: def __init__(self, metadata_uri=None): if metadata_uri is None: - metadata_uri = config.METADATA_URI + metadata_uri = get_metadata_uri() self.metadata_db = load_database(metadata_uri) @@ -67,43 +68,49 @@ def fetch_releases( release_type = check_parameter(release_type) site_name = check_parameter(site_name) - # Reflect existing tables, letting sqlalchemy load linked tables where possible. - release = db.Table("ensembl_release", self.md, autoload_with=self.metadata_db) - site = self.md.tables["ensembl_site"] + #SELECT ensembl_release.release_id, ensembl_release.version AS release_version, ensembl_release.release_date, ensembl_release.label AS release_label, ensembl_release.is_current, ensembl_release.release_type, ensembl_site.name AS site_name, ensembl_site.label AS site_label, ensembl_site.uri AS site_uri + #FROM ensembl_release JOIN ensembl_site ON ensembl_site.site_id = ensembl_release.site_id release_select = db.select( - release.c.release_id, - release.c.version.label("release_version"), - db.cast(release.c.release_date, db.String), - release.c.label.label("release_label"), - release.c.is_current, - release.c.release_type, - site.c.name.label("site_name"), - site.c.label.label("site_label"), - site.c.uri.label("site_uri"), - ).select_from(release) - - # These options are in order of decreasing specificity, - # and thus the ones later in the list can be redundant. + EnsemblRelease.release_id, + EnsemblRelease.version.label("release_version"), + EnsemblRelease.release_date, + EnsemblRelease.label.label("release_label"), + EnsemblRelease.is_current, + EnsemblRelease.release_type, + EnsemblSite.name.label("site_name"), + EnsemblSite.label.label("site_label"), + EnsemblSite.uri.label("site_uri") + ).join(EnsemblRelease.site) + + #WHERE ensembl_release.release_id = :release_id_1 if release_id is not None: - release_select = release_select.filter(release.c.release_id.in_(release_id)) + release_select = release_select.filter( + EnsemblRelease.release_id == release_id + ) + #WHERE ensembl_release.version = :version_1 elif release_version is not None: release_select = release_select.filter( - release.c.version.in_(release_version) + EnsemblRelease.version == release_version ) + #WHERE ensembl_release.is_current =:is_current_1 elif current_only: - release_select = release_select.filter_by(is_current=1) + release_select = release_select.filter( + EnsemblRelease.is_current == 1 + ) + #WHERE ensembl_release.release_type = :release_type_1 if release_type is not None: release_select = release_select.filter( - release.c.release_type.in_(release_type) + EnsemblRelease.release_type == release_type ) - release_select = release_select.join(site) + #WHERE ensembl_site.name = :name_1 if site_name is not None: - release_select = release_select.filter(site.c.name.in_(site_name)) - - return self.metadata_db_session.execute(release_select).all() + release_select = release_select.filter( + EnsemblSite.name == site_name + ) + return self.metadata_db._session.execute(release_select) def fetch_releases_for_genome(self, genome_uuid, site_name=None): genome = db.Table("genome", self.md, autoload_with=self.metadata_db) @@ -144,6 +151,17 @@ def fetch_releases_for_dataset(self, dataset_uuid, site_name=None): return self.fetch_releases(release_id=release_ids, site_name=site_name) +TEST = ReleaseAdaptor('mysql://danielp:Killadam69@localhost/ensembl_metadata_2020') +TEST2 = TEST.fetch_releases() +for i in TEST2: + print (i) + + + + + + + class GenomeAdaptor(BaseAdaptor): taxon_names = {} diff --git a/src/ensembl/production/metadata/config.py b/src/ensembl/production/metadata/config.py index 3d980ddf..ad6cb833 100755 --- a/src/ensembl/production/metadata/config.py +++ b/src/ensembl/production/metadata/config.py @@ -12,6 +12,8 @@ import os -class MetadataConfig: - METADATA_URI = os.environ.get("METADATA_URI", None) - TAXONOMY_URI = os.environ.get("TAXONOMY_URI", None) +def get_metadata_uri(): + return os.environ.get("METADATA_URI", None) + +def get_taxonomy_uri(): + return os.environ.get("TAXONOMY_URI", None) From 87acb30f306cd8585c1d6e24430f1c6368b5d7b2 Mon Sep 17 00:00:00 2001 From: dpopleton Date: Thu, 29 Sep 2022 19:44:13 -0400 Subject: [PATCH 07/14] fetch_releases_for_genome converted to ORM --- src/ensembl/production/metadata/api.py | 33 ++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/ensembl/production/metadata/api.py b/src/ensembl/production/metadata/api.py index 5233cc29..20abc0fc 100644 --- a/src/ensembl/production/metadata/api.py +++ b/src/ensembl/production/metadata/api.py @@ -112,24 +112,27 @@ def fetch_releases( ) return self.metadata_db._session.execute(release_select) + def fetch_releases_for_genome(self, genome_uuid, site_name=None): - genome = db.Table("genome", self.md, autoload_with=self.metadata_db) - genome_release = db.Table( - "genome_release", self.md, autoload_with=self.metadata_db - ) - release_id_select = ( - db.select(genome_release.c.release_id) - .select_from(genome) - .filter_by(genome_uuid=genome_uuid) - .join(genome_release) + # SELECT genome_release.release_id + # FROM genome_release + # JOIN genome ON genome.genome_id = genome_release.genome_id + # WHERE genome.genome_uuid =:genome_uuid_1 + release_id_select = db.select( + GenomeRelease.release_id + ).filter( + Genome.genome_uuid == genome_uuid + ).join( + GenomeRelease.genome ) + #Don't really like this section. Refactor later. + release_ids = [] + release_objects = self.metadata_db._session.execute(release_id_select) + for rid in release_objects: + release_ids.append(rid[0]) - release_ids = [ - rid for (rid,) in self.metadata_db_session.execute(release_id_select) - ] - - return self.fetch_releases(release_id=release_ids, site_name=site_name) + return release_ids def fetch_releases_for_dataset(self, dataset_uuid, site_name=None): dataset = db.Table("dataset", self.md, autoload_with=self.metadata_db) @@ -152,7 +155,7 @@ def fetch_releases_for_dataset(self, dataset_uuid, site_name=None): TEST = ReleaseAdaptor('mysql://danielp:Killadam69@localhost/ensembl_metadata_2020') -TEST2 = TEST.fetch_releases() +TEST2 = TEST.fetch_releases_for_genome('3704ceb1-948d-11ec-a39d-005056b38ce3') for i in TEST2: print (i) From 940ec636c1ca41838f7775c09ff8f611a6d7dcb4 Mon Sep 17 00:00:00 2001 From: dpopleton Date: Thu, 29 Sep 2022 19:47:03 -0400 Subject: [PATCH 08/14] fetch_releases_for_genome converted to ORM properly --- src/ensembl/production/metadata/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ensembl/production/metadata/api.py b/src/ensembl/production/metadata/api.py index 20abc0fc..8c07cfc1 100644 --- a/src/ensembl/production/metadata/api.py +++ b/src/ensembl/production/metadata/api.py @@ -132,7 +132,7 @@ def fetch_releases_for_genome(self, genome_uuid, site_name=None): for rid in release_objects: release_ids.append(rid[0]) - return release_ids + return self.fetch_releases(release_id=release_ids, site_name=site_name) def fetch_releases_for_dataset(self, dataset_uuid, site_name=None): dataset = db.Table("dataset", self.md, autoload_with=self.metadata_db) From 72e86c5b3534ca6e626e052613ef8e71abcef743 Mon Sep 17 00:00:00 2001 From: dpopleton Date: Thu, 29 Sep 2022 19:56:12 -0400 Subject: [PATCH 09/14] fetch_releases_for_dataset converted to ORM --- src/ensembl/production/metadata/api.py | 39 +++++++++++--------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/ensembl/production/metadata/api.py b/src/ensembl/production/metadata/api.py index 8c07cfc1..75a7a5b7 100644 --- a/src/ensembl/production/metadata/api.py +++ b/src/ensembl/production/metadata/api.py @@ -127,6 +127,7 @@ def fetch_releases_for_genome(self, genome_uuid, site_name=None): GenomeRelease.genome ) #Don't really like this section. Refactor later. + # It is also used twice. Function maybe? release_ids = [] release_objects = self.metadata_db._session.execute(release_id_select) for rid in release_objects: @@ -135,35 +136,29 @@ def fetch_releases_for_genome(self, genome_uuid, site_name=None): return self.fetch_releases(release_id=release_ids, site_name=site_name) def fetch_releases_for_dataset(self, dataset_uuid, site_name=None): - dataset = db.Table("dataset", self.md, autoload_with=self.metadata_db) - genome_dataset = db.Table( - "genome_dataset", self.md, autoload_with=self.metadata_db - ) - release_id_select = ( - db.select(genome_dataset.c.release_id) - .select_from(dataset) - .filter_by(dataset_uuid=dataset_uuid) - .join(genome_dataset) + # SELECT genome_release.release_id + # FROM genome_dataset + # JOIN dataset ON dataset.dataset_id = genome_dataset.dataset_id + # WHERE dataset.dataset_uuid = :dataset_uuid_1 + release_id_select = db.select( + GenomeDataset.release_id + ).filter( + Dataset.dataset_uuid == dataset_uuid + ).join( + GenomeDataset.dataset ) - release_ids = [ - rid for (rid,) in self.metadata_db_session.execute(release_id_select) - ] + #Don't really like this section. Refactor later. + # It is also used twice. Function maybe? + release_ids = [] + release_objects = self.metadata_db._session.execute(release_id_select) + for rid in release_objects: + release_ids.append(rid[0]) return self.fetch_releases(release_id=release_ids, site_name=site_name) -TEST = ReleaseAdaptor('mysql://danielp:Killadam69@localhost/ensembl_metadata_2020') -TEST2 = TEST.fetch_releases_for_genome('3704ceb1-948d-11ec-a39d-005056b38ce3') -for i in TEST2: - print (i) - - - - - - class GenomeAdaptor(BaseAdaptor): taxon_names = {} From 1c527c75447358b6a343b27b5086822f7e929b27 Mon Sep 17 00:00:00 2001 From: dpopleton Date: Thu, 29 Sep 2022 19:56:39 -0400 Subject: [PATCH 10/14] Added Tests --- src/ensembl/production/metadata/api.py | 1 - tests/test_api.py | 16 ++++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/ensembl/production/metadata/api.py b/src/ensembl/production/metadata/api.py index 75a7a5b7..02a0ae96 100644 --- a/src/ensembl/production/metadata/api.py +++ b/src/ensembl/production/metadata/api.py @@ -159,7 +159,6 @@ def fetch_releases_for_dataset(self, dataset_uuid, site_name=None): return self.fetch_releases(release_id=release_ids, site_name=site_name) - class GenomeAdaptor(BaseAdaptor): taxon_names = {} diff --git a/tests/test_api.py b/tests/test_api.py index bc37303e..755679fc 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -12,12 +12,16 @@ """ Unit tests for api module """ - +from ensembl.production.metadata.api import * def test_load_database(): - """Test api.load_database function""" - #load_database('XXX') - pass - - + DB_TEST = ReleaseAdaptor('sqlite:///TEST.db') + assert DB_TEST, "DB should not be empty" +def test_Release_adaptor(): + conn = ReleaseAdaptor('sqlite:///TEST.db') + TEST2 = conn.fetch_releases().one() + #Test the one to many connection + assert TEST2[6] == '2020-map' + #Test the direct access. + assert TEST2[3] == '2020 MAP 7 species' From 1cf99528edb9642ba6e525bc7de7badf27a4fbee Mon Sep 17 00:00:00 2001 From: dpopleton Date: Thu, 29 Sep 2022 19:58:51 -0400 Subject: [PATCH 11/14] Added Tests --- .python-version | 1 + __init__.py | 0 tests/TEST.db | Bin 0 -> 376832 bytes 3 files changed, 1 insertion(+) create mode 100644 .python-version create mode 100644 __init__.py create mode 100644 tests/TEST.db diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..db0033b7 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +ensembl_metadata_api diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/TEST.db b/tests/TEST.db new file mode 100644 index 0000000000000000000000000000000000000000..dfd297ad3cc0be2a6eb81658125145cbbc13f20c GIT binary patch literal 376832 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 Date: Thu, 6 Oct 2022 17:31:51 -0400 Subject: [PATCH 12/14] Added Tests, Refactored models.py, improved fetch_releases() --- requirements.in | 2 +- requirements.txt | 58 +++++++++- src/ensembl/production/metadata/api.py | 30 ++---- src/ensembl/production/metadata/models.py | 126 +++++++++++++++++----- tests/test_api.py | 19 +++- 5 files changed, 176 insertions(+), 59 deletions(-) diff --git a/requirements.in b/requirements.in index 0bc65d52..ebd9816c 100644 --- a/requirements.in +++ b/requirements.in @@ -2,5 +2,5 @@ mysqlclient pymysql sqlalchemy types-pymysql -ensembl-py +git+https://github.com/Ensembl/ensembl-py.git#egg=ensembl-py mysql \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5dd67e9b..5d140a62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,66 @@ # -# This file is autogenerated by pip-compile with python 3.8 +# This file is autogenerated by pip-compile with python 3.10 # To update, run: # # pip-compile --output-file=requirements.txt requirements.in # +attrs==22.1.0 + # via pytest +certifi==2022.9.24 + # via requests +charset-normalizer==2.1.1 + # via requests +ensembl-hive @ git+https://github.com/Ensembl/ensembl-hive.git@main + # via ensembl-py +ensembl-py @ git+https://github.com/Ensembl/ensembl-py.git + # via -r requirements.in greenlet==1.1.0 # via sqlalchemy -mysqlclient==2.0.3 +idna==3.4 + # via requests +iniconfig==1.1.1 + # via pytest +mysql==0.0.3 # via -r requirements.in +mysqlclient==2.0.3 + # via + # -r requirements.in + # ensembl-py + # mysql +packaging==21.3 + # via pytest +pluggy==1.0.0 + # via pytest +py==1.11.0 + # via pytest pymysql==1.0.2 # via -r requirements.in +pyparsing==3.0.9 + # via packaging +pytest==7.1.3 + # via + # ensembl-py + # pytest-dependency +pytest-dependency==0.5.1 + # via ensembl-py +python-dotenv==0.19.2 + # via ensembl-py +pyyaml==6.0 + # via ensembl-py +requests==2.28.1 + # via ensembl-py +six==1.16.0 + # via sqlalchemy-utils sqlalchemy==1.4.21 - # via -r requirements.in + # via + # -r requirements.in + # ensembl-py + # sqlalchemy-utils +sqlalchemy-utils==0.37.9 + # via ensembl-py +tomli==2.0.1 + # via pytest types-pymysql==1.0.0 # via -r requirements.in -git+https://github.com/Ensembl/ensembl-py.git@main -mysql \ No newline at end of file +urllib3==1.26.12 + # via requests diff --git a/src/ensembl/production/metadata/api.py b/src/ensembl/production/metadata/api.py index 02a0ae96..1598d8a3 100644 --- a/src/ensembl/production/metadata/api.py +++ b/src/ensembl/production/metadata/api.py @@ -69,29 +69,19 @@ def fetch_releases( site_name = check_parameter(site_name) - #SELECT ensembl_release.release_id, ensembl_release.version AS release_version, ensembl_release.release_date, ensembl_release.label AS release_label, ensembl_release.is_current, ensembl_release.release_type, ensembl_site.name AS site_name, ensembl_site.label AS site_label, ensembl_site.uri AS site_uri - #FROM ensembl_release JOIN ensembl_site ON ensembl_site.site_id = ensembl_release.site_id release_select = db.select( - EnsemblRelease.release_id, - EnsemblRelease.version.label("release_version"), - EnsemblRelease.release_date, - EnsemblRelease.label.label("release_label"), - EnsemblRelease.is_current, - EnsemblRelease.release_type, - EnsemblSite.name.label("site_name"), - EnsemblSite.label.label("site_label"), - EnsemblSite.uri.label("site_uri") - ).join(EnsemblRelease.site) + EnsemblRelease,EnsemblSite + ).join(EnsemblRelease.ensembl_site) #WHERE ensembl_release.release_id = :release_id_1 if release_id is not None: release_select = release_select.filter( - EnsemblRelease.release_id == release_id + EnsemblRelease.release_id.in_(release_id) ) #WHERE ensembl_release.version = :version_1 elif release_version is not None: release_select = release_select.filter( - EnsemblRelease.version == release_version + EnsemblRelease.version.in_(release_version) ) #WHERE ensembl_release.is_current =:is_current_1 elif current_only: @@ -102,13 +92,13 @@ def fetch_releases( #WHERE ensembl_release.release_type = :release_type_1 if release_type is not None: release_select = release_select.filter( - EnsemblRelease.release_type == release_type + EnsemblRelease.release_type.in_(release_type) ) #WHERE ensembl_site.name = :name_1 if site_name is not None: release_select = release_select.filter( - EnsemblSite.name == site_name + EnsemblSite.name.in_(site_name) ) return self.metadata_db._session.execute(release_select) @@ -126,13 +116,12 @@ def fetch_releases_for_genome(self, genome_uuid, site_name=None): ).join( GenomeRelease.genome ) - #Don't really like this section. Refactor later. - # It is also used twice. Function maybe? + release_ids = [] release_objects = self.metadata_db._session.execute(release_id_select) for rid in release_objects: release_ids.append(rid[0]) - + release_ids = list(dict.fromkeys(release_ids)) return self.fetch_releases(release_id=release_ids, site_name=site_name) def fetch_releases_for_dataset(self, dataset_uuid, site_name=None): @@ -149,12 +138,11 @@ def fetch_releases_for_dataset(self, dataset_uuid, site_name=None): GenomeDataset.dataset ) - #Don't really like this section. Refactor later. - # It is also used twice. Function maybe? release_ids = [] release_objects = self.metadata_db._session.execute(release_id_select) for rid in release_objects: release_ids.append(rid[0]) + release_ids = list(dict.fromkeys(release_ids)) return self.fetch_releases(release_id=release_ids, site_name=site_name) diff --git a/src/ensembl/production/metadata/models.py b/src/ensembl/production/metadata/models.py index 7b7f83f2..336d5e24 100644 --- a/src/ensembl/production/metadata/models.py +++ b/src/ensembl/production/metadata/models.py @@ -37,7 +37,13 @@ class Assembly(Base): tolid = Column(String(32), unique=True) created = Column(DateTime) ensembl_name = Column(String(255), unique=True) - +#One to many relationships +#assembly_id within assembly_sequence + assembly_sequences = relationship("AssemblySequence", back_populates="assembly") +#assembly_id within genome + genomes = relationship("Genome", back_populates="assembly") +#many to one relationships +#none class AssemblySequence(Base): __tablename__ = 'assembly_sequence' @@ -54,7 +60,11 @@ class AssemblySequence(Base): sequence_location = Column(String(10)) sequence_checksum = Column(String(32)) ga4gh_identifier = Column(String(32)) - assembly = relationship('Assembly', backref="assembly") + #One to many relationships + #none + #many to one relationships + #assembly_id within assembly + assembly = relationship('Assembly', back_populates="assembly_sequences") class Attribute(Base): @@ -64,7 +74,11 @@ class Attribute(Base): name = Column(String(128), nullable=False) label = Column(String(128), nullable=False) description = Column(String(255)) - + #One to many relationships + #attribute_id within dataset attribute + dataset_attributes = relationship("DatasetAttribute", back_populates='attribute') + #many to one relationships + #none class Dataset(Base): __tablename__ = 'dataset' @@ -78,8 +92,15 @@ class Dataset(Base): dataset_source_id = Column(ForeignKey('dataset_source.dataset_source_id'), nullable=False, index=True) label = Column(String(128), nullable=False) - dataset_source = relationship('DatasetSource', backref="dataset") - dataset_type = relationship('DatasetType', backref="dataset") + #One to many relationships + #dataset_id to dataset attribute and genome dataset + dataset_attributes = relationship("DatasetAttribute", back_populates='dataset') + genome_datasets = relationship("GenomeDataset", back_populates='dataset') + #many to one relationships + #dataset_type_id to dataset_type + dataset_type = relationship('DatasetType', back_populates="datasets") + #dataset_source_id to dataset source + dataset_source = relationship('DatasetSource', back_populates="datasets") class DatasetAttribute(Base): @@ -93,9 +114,13 @@ class DatasetAttribute(Base): value = Column(String(128), nullable=False) attribute_id = Column(ForeignKey('attribute.attribute_id'), nullable=False, index=True) dataset_id = Column(ForeignKey('dataset.dataset_id'), nullable=False, index=True) - - attribute = relationship('Attribute', backref="dataset_attribute") - dataset = relationship('Dataset', backref="dataset_attribute") + #One to many relationships + #none + #many to one relationships + #dataset_attribute_id to dataset + attribute = relationship('Attribute', back_populates="dataset_attributes") + #attribute_id to attribute + dataset = relationship('Dataset', back_populates="dataset_attributes") class DatasetSource(Base): @@ -104,7 +129,11 @@ class DatasetSource(Base): dataset_source_id = Column(Integer, primary_key=True) type = Column(String(32), nullable=False) name = Column(String(255), nullable=False, unique=True) - + #One to many relationships + #dataset_source_id to dataset + datasets = relationship('Dataset', back_populates='dataset_source') + #many to one relationships + #none class DatasetType(Base): __tablename__ = 'dataset_type' @@ -115,7 +144,11 @@ class DatasetType(Base): topic = Column(String(32), nullable=False) description = Column(String(255)) details_uri = Column(String(255)) - + #One to many relationships + #dataset_type_id to dataset + datasets = relationship('Dataset', back_populates='dataset_type') + #many to one relationships + #none class EnsemblSite(Base): __tablename__ = 'ensembl_site' @@ -124,7 +157,11 @@ class EnsemblSite(Base): name = Column(String(64), nullable=False) label = Column(String(64), nullable=False) uri = Column(String(64), nullable=False) - + #One to many relationships + #site_id to ensembl_release + ensembl_releases = relationship('EnsemblRelease', back_populates='ensembl_site') + #many to one relationships + #none class EnsemblRelease(Base): __tablename__ = 'ensembl_release' @@ -139,8 +176,13 @@ class EnsemblRelease(Base): is_current = Column(TINYINT(1), nullable=False) site_id = Column(ForeignKey('ensembl_site.site_id'), index=True) release_type = Column(String(16), nullable=False) - - site = relationship('EnsemblSite', backref='ensembl_release') + #One to many relationships + #release_id to genome dataset and genome release + genome_datasets = relationship('GenomeDataset', back_populates='ensembl_release') + genome_releases = relationship('GenomeRelease', back_populates='ensembl_release') + #many to one relationships + #site_id to ensembl_site + ensembl_site = relationship('EnsemblSite', back_populates='ensembl_releases') class Genome(Base): @@ -151,9 +193,15 @@ class Genome(Base): assembly_id = Column(ForeignKey('assembly.assembly_id'), nullable=False, index=True) organism_id = Column(ForeignKey('organism.organism_id'), nullable=False, index=True) created = Column(DATETIME(fsp=6), nullable=False) - - assembly = relationship('Assembly', backref="genome") - organism = relationship('Organism', backref="genome") + # One to many relationships + # genome_id to genome_dataset and genome release + genome_datasets = relationship('GenomeDataset', back_populates='genome') + genome_releases = relationship('GenomeRelease', back_populates='genome') + # many to one relationships + # assembly_id to assembly + assembly = relationship('Assembly', back_populates="genomes") + # organism_id to organism + organism = relationship('Organism', back_populates="genomes") class GenomeDataset(Base): @@ -164,10 +212,15 @@ class GenomeDataset(Base): genome_id = Column(ForeignKey('genome.genome_id'), nullable=False, index=True) release_id = Column(ForeignKey('ensembl_release.release_id'), nullable=False, index=True) is_current = Column(TINYINT(1), nullable=False) - - dataset = relationship('Dataset', backref="genome_dataset") - genome = relationship('Genome', backref="genome_dataset") - release = relationship('EnsemblRelease', backref="genome_dataset") + #One to many relationships + #none + #many to one relationships + #genome_dataset_id to genome + dataset = relationship('Dataset', back_populates="genome_datasets") + #genome_id to genome + genome = relationship('Genome', back_populates="genome_datasets") + #release_id to release + ensembl_release = relationship('EnsemblRelease', back_populates="genome_datasets") class GenomeRelease(Base): @@ -177,9 +230,13 @@ class GenomeRelease(Base): genome_id = Column(ForeignKey('genome.genome_id'), nullable=False, index=True) release_id = Column(ForeignKey('ensembl_release.release_id'), nullable=False, index=True) is_current = Column(TINYINT(1), nullable=False) - - genome = relationship('Genome', backref='genome_release') - release = relationship('EnsemblRelease', backref='genome_release') + #One to many relationships + #none + #many to one relationships + #genome_release_id to genome_release + genome = relationship('Genome', back_populates='genome_releases') + #release_id to ensembl release + ensembl_release = relationship('EnsemblRelease', back_populates='genome_releases') class Organism(Base): @@ -194,7 +251,12 @@ class Organism(Base): url_name = Column(String(128), nullable=False) ensembl_name = Column(String(128), nullable=False, unique=True) scientific_parlance_name = Column(String(255)) - + #One to many relationships + #Organism_id to organism_group_member and genome + genomes = relationship('Genome', back_populates='organism') + organism_group_members = relationship('OrganismGroupMember', back_populates='organism') + #many to one relationships + #organim_id and taxonomy_id to taxonomy_node #DIFFERENT DATABASE class OrganismGroup(Base): __tablename__ = 'organism_group' @@ -206,7 +268,11 @@ class OrganismGroup(Base): type = Column(String(32), nullable=False) name = Column(String(255), nullable=False) code = Column(String(48), unique=True) - + #One to many relationships + #Organism_group_id to organism_group_member + organism_group_members = relationship('OrganismGroupMember', back_populates='organism_group') + #many to one relationships + #none class OrganismGroupMember(Base): __tablename__ = 'organism_group_member' @@ -218,6 +284,10 @@ class OrganismGroupMember(Base): is_reference = Column(TINYINT(1), nullable=False) organism_id = Column(ForeignKey('organism.organism_id'), nullable=False) organism_group_id = Column(ForeignKey('organism_group.organism_group_id'), nullable=False, index=True) - - organism_group = relationship('OrganismGroup', backref='organism_group_member') - organism = relationship('Organism', backref='organism_group_member') + #One to many relationships + #none + #many to one relationships + #Organism_group_id to organism_group_member + #organism_id to organism + organism_group = relationship('OrganismGroup', back_populates='organism_group_members') + organism = relationship('Organism', back_populates='organism_group_members') diff --git a/tests/test_api.py b/tests/test_api.py index 755679fc..2417e80b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -18,10 +18,21 @@ def test_load_database(): DB_TEST = ReleaseAdaptor('sqlite:///TEST.db') assert DB_TEST, "DB should not be empty" -def test_Release_adaptor(): +def test_fetch_releases(): conn = ReleaseAdaptor('sqlite:///TEST.db') - TEST2 = conn.fetch_releases().one() + TEST = conn.fetch_releases(release_id=1).one() #Test the one to many connection - assert TEST2[6] == '2020-map' + assert TEST.EnsemblSite.name == '2020-map' #Test the direct access. - assert TEST2[3] == '2020 MAP 7 species' + assert TEST.EnsemblRelease.label == '2020 MAP 7 species' + +#currently only have one release, so the testing is not comprehensive +def test_fetch_releases_for_genome(): + conn = ReleaseAdaptor('sqlite:///TEST.db') + TEST = conn.fetch_releases_for_genome('a733574a-93e7-11ec-a39d-005056b38ce3').one() + assert TEST.EnsemblSite.name == '2020-map' + +def test_fetch_releases_for_dataset(): + conn = ReleaseAdaptor('sqlite:///TEST.db') + TEST = conn.fetch_releases_for_dataset('76ffa505-948d-11ec-a39d-005056b38ce3').one() + assert TEST.EnsemblSite.name == '2020-map' \ No newline at end of file From a13387d53e5c4fe016ca2603490baa2418506ce5 Mon Sep 17 00:00:00 2001 From: Marc Chakiachvili Date: Fri, 7 Oct 2022 14:24:52 +0100 Subject: [PATCH 13/14] Fixed DB path to be located in the right place. Initially was created directly under root path, so empty on travis. --- .travis.yml | 5 ++++- __init__.py | 0 tests/test_api.py | 11 +++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) delete mode 100644 __init__.py diff --git a/.travis.yml b/.travis.yml index 66cb7350..d3526357 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,13 +3,16 @@ os: linux python: - "3.8" + - "3.9" + - "3.10" env: - TESTENV=test -install: +before_script: - pip install -r requirements-test.txt - pip install . + - export PYTHONPATH=$PYTHONPATH:$PWD/src script: - if [[ "$TESTENV" == "test" ]]; then coverage run -m pytest; fi diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_api.py b/tests/test_api.py index 2417e80b..c708bba7 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -12,14 +12,17 @@ """ Unit tests for api module """ +from os.path import dirname from ensembl.production.metadata.api import * +DB_NAME = 'sqlite:///' + dirname(__file__) + '/TEST.db' + def test_load_database(): - DB_TEST = ReleaseAdaptor('sqlite:///TEST.db') + DB_TEST = ReleaseAdaptor(DB_NAME) assert DB_TEST, "DB should not be empty" def test_fetch_releases(): - conn = ReleaseAdaptor('sqlite:///TEST.db') + conn = ReleaseAdaptor(DB_NAME) TEST = conn.fetch_releases(release_id=1).one() #Test the one to many connection assert TEST.EnsemblSite.name == '2020-map' @@ -28,11 +31,11 @@ def test_fetch_releases(): #currently only have one release, so the testing is not comprehensive def test_fetch_releases_for_genome(): - conn = ReleaseAdaptor('sqlite:///TEST.db') + conn = ReleaseAdaptor(DB_NAME) TEST = conn.fetch_releases_for_genome('a733574a-93e7-11ec-a39d-005056b38ce3').one() assert TEST.EnsemblSite.name == '2020-map' def test_fetch_releases_for_dataset(): - conn = ReleaseAdaptor('sqlite:///TEST.db') + conn = ReleaseAdaptor(DB_NAME) TEST = conn.fetch_releases_for_dataset('76ffa505-948d-11ec-a39d-005056b38ce3').one() assert TEST.EnsemblSite.name == '2020-map' \ No newline at end of file From 4b7c1130f019b3e99e8d4cc2bf5096c4d0fa6a95 Mon Sep 17 00:00:00 2001 From: Marc Chakiachvili Date: Fri, 7 Oct 2022 14:43:13 +0100 Subject: [PATCH 14/14] Removed 3.10 from the list, not targeted anyway for now. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d3526357..b1177099 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ os: linux python: - "3.8" - "3.9" - - "3.10" env: - TESTENV=test