def join(self, target_table: Table, query: Query) -> Query: if target_table == Table.metaproteomic_analysis: return query.join( models.MetaPGeneFunctionAggregation, models.MetaPGeneFunctionAggregation.metaproteomic_analysis_id == models.MetaproteomicAnalysis.id, ).join( MetaPGeneFunction, MetaPGeneFunction.id == models.MetaPGeneFunctionAggregation.gene_function_id, ) query = super().join(target_table, query) return (query.join( models.MetaproteomicAnalysis, models.MetaproteomicAnalysis.omics_processing_id == models.OmicsProcessing.id, ).join( models.MetaPGeneFunctionAggregation, models.MetaPGeneFunctionAggregation.metaproteomic_analysis_id == models.MetaproteomicAnalysis.id, ).join( MetaPGeneFunction, MetaPGeneFunction.id == models.MetaPGeneFunctionAggregation.gene_function_id, ))
def join(self, target_table: Table, query: Query) -> Query: if target_table == Table.metagenome_annotation: return query.join( models.MGAGeneFunctionAggregation, models.MGAGeneFunctionAggregation.metagenome_annotation_id == models.MetagenomeAnnotation.id, ).join( models.GeneFunction, models.GeneFunction.id == models.MGAGeneFunctionAggregation.gene_function_id, ) query = super().join(target_table, query) return (query.join( models.MetagenomeAnnotation, models.MetagenomeAnnotation.omics_processing_id == models.OmicsProcessing.id, ).join( models.MGAGeneFunctionAggregation, models.MGAGeneFunctionAggregation.metagenome_annotation_id == models.MetagenomeAnnotation.id, ).join( models.GeneFunction, models.GeneFunction.id == models.MGAGeneFunctionAggregation.gene_function_id, ))
def join_envo(self, table: Table, query: Query) -> Query: """Join with an envo table. Assumes the query is already joined with the biosample table. """ if table == Table.env_broad_scale: return query.join( EnvBroadScaleAncestor, models.Biosample.env_broad_scale_id == EnvBroadScaleAncestor.id, ).join( EnvBroadScaleTerm, EnvBroadScaleAncestor.ancestor_id == EnvBroadScaleTerm.id, ) if table == Table.env_local_scale: return query.join( EnvLocalScaleAncestor, models.Biosample.env_local_scale_id == EnvLocalScaleAncestor.id, ).join( EnvLocalScaleTerm, EnvLocalScaleAncestor.ancestor_id == EnvLocalScaleTerm.id, ) if table == Table.env_medium: return query.join( EnvMediumAncestor, models.Biosample.env_medium_id == EnvMediumAncestor.id, ).join( EnvMediumTerm, EnvMediumAncestor.ancestor_id == EnvMediumTerm.id, ) return query
def join_biosample(self, query: Query) -> Query: return self.join_self( query.join( models.OmicsProcessing, models.Biosample.id == models.OmicsProcessing.biosample_id), Table.omics_processing, )
def __apply_filters_to_query(self, query: Query, filters: Union[Dict[str, List[str]]]): query = query.join(MetaORM) for key, values in filters.items(): query = query.filter(MetaORM.name == key, MetaORM.value.in_(values)) return query
def create_multi_join_view(cls, db, view_name, selectable, joins, order_by): with db.managed_session() as session: query = Query(selectable, session=session) for (join_table, join_clause) in joins: query = query.join(join_table, join_clause) query = query.order_by(order_by) cls.__create_view_if_not_exists(session, view_name, str(query))
def _filter_storages(self, ctx: RunContext, sess: Session, query: Query) -> Query: if not self.storages: return query return query.join(StoredDataBlockMetadata).filter( StoredDataBlockMetadata.storage_url.in_( [s.url for s in self.storages]) # type: ignore )
def data_layer_get_object_update_query(self, *args, query: Query = None, qs: QueryStringManager = None, view_kwargs=None, self_json_api=None, **kwargs) -> Query: """ Во время создания запроса к БД на выгрузку объекта. Тут можно пропатчить запрос к БД. Навешиваем ограничения на запрос, чтобы не тянулись поля из БД, которые данному пользователю не доступны. Также навешиваем фильтры, чтобы пользователь не смог увидеть записи, которые ему не доступны :param args: :param Query query: Сформированный запрос к БД :param QueryStringManager qs: список параметров для запроса :param view_kwargs: список фильтров для запроса :param self_json_api: :param kwargs: :return: возвращает пропатченный запрос к бд """ permission: PermissionUser = self._get_permission_user(view_kwargs) permission_for_get: PermissionForGet = permission.permission_for_get( self_json_api.model) # Навешиваем фильтры (например пользователь не должен видеть некоторые поля) for i_join in permission_for_get.joins: query = query.join(*i_join) query = query.filter(*permission_for_get.filters) # Навешиваем ограничения по атрибутам (которые доступны & которые запросил пользователь) name_columns = permission_for_get.columns if qs: user_requested_columns = qs.fields.get( self_json_api.resource.schema.Meta.type_) if user_requested_columns: name_columns = list( set(name_columns) & set(user_requested_columns)) # Убираем relationship поля name_columns = [ i_name for i_name in name_columns if i_name in self_json_api.model.__table__.columns.keys() ] required_columns_names = [] for i_name in name_columns: required_columns_names.extend( get_required_fields(i_name, self_json_api.model)) name_columns = list(set(name_columns) | set(required_columns_names)) query = query.options(load_only(*name_columns)) if qs: query = self._eagerload_includes(query, qs, permission, self_json_api=self_json_api) # Запретим использовать стандартную функцию eagerload_includes для присоединения сторонних молелей self_json_api.eagerload_includes = lambda x, y: x return query
def _inject_omics_data_summary(self, db: Session, query: Query) -> Query: aggs = [] for omics_class in workflow_search_classes: pipeline_model = omics_class().table.model table_name = pipeline_model.__tablename__ # type: ignore filter_conditions = [ c for c in self.conditions if c.table.value in {"omics_processing", table_name, "biosample"} ] query_schema = omics_class(conditions=filter_conditions) omics_subquery = self._count_omics_data_query( db, query_schema).subquery() study_id = getattr(omics_subquery.c, f"{table_name}_study_id") query = query.join( omics_subquery, self.table.model.id == study_id, # type: ignore isouter=True, ) aggs.append( func.json_build_object( "type", table_name, "count", getattr(omics_subquery.c, f"{table_name}_count"))) op_filter_conditions = [ c for c in self.conditions if c.table.value in {"omics_processing", "biosample"} ] op_summary_subquery = self._count_omics_processing_summary( db, op_filter_conditions).subquery() query = query.join( op_summary_subquery, op_summary_subquery.c.omics_processing_study_id == models.Study.id, isouter=True, ) aggregation = func.json_build_array(*aggs) return query.populate_existing().options( with_expression(models.Study.omics_counts, aggregation), with_expression( models.Study.omics_processing_counts, op_summary_subquery.c.omics_processing_summary, ), )
def filter_role(db: Session, query: Query, zones: List[GeoZone], role: str): all_zones = [[zone.id] + [child.id for child in get_child(db, zone)] for zone in zones] all_zones = [item for sublist in all_zones for item in sublist] if role == 'referent': return query.join(Adherents.managed_area) \ .join(ReferentManagedAreasTags.referent_tag) \ .join(ReferentTags.zone.and_(GeoZone.id.in_(all_zones))) if role in ['deputy', 'senator']: return query.join(AdherentMessages.filter) \ .join(AdherentMessageFilters.referent_tag) \ .join(ReferentTags.zone.and_(GeoZone.id.in_(all_zones))) if role == 'candidate': return query.join(AdherentMessages.filter) \ .join(AdherentMessageFilters.zone.and_(GeoZone.id.in_(all_zones))) return query
def restricted_incident_filter(query: orm.Query, current_user: DispatchUser): """Adds additional incident filters to query (usually for permissions).""" query = (query.join( Participant, Incident.id == Participant.incident_id).join(IndividualContact).filter( or_( Incident.visibility == Visibility.open.value, IndividualContact.email == current_user.email, )).distinct()) return query
def data_layer_get_collection_update_query(self, *args, query: Query = None, qs: QueryStringManager = None, view_kwargs=None, self_json_api=None, **kwargs) -> Query: """ Во время создания запроса к БД на выгрузку объектов. Тут можно пропатчить запрос к БД :param args: :param Query query: Сформированный запрос к БД :param QueryStringManager qs: список параметров для запроса :param view_kwargs: список фильтров для запроса :param self_json_api: :param kwargs: :return: возвращает пропатченный запрос к бд """ permission: PermissionUser = self._get_permission_user(view_kwargs) permission_for_get: PermissionForGet = permission.permission_for_get( self_json_api.model) # Навешиваем фильтры (например пользователь не должен видеть некоторые поля) for i_join in permission_for_get.joins: query = query.join(*i_join) query = query.filter(*permission_for_get.filters) # Навешиваем ограничения по атрибутам (которые доступны & которые запросил пользователь) name_columns = permission_for_get.columns user_requested_columns = qs.fields.get( self_json_api.resource.schema.Meta.type_) if user_requested_columns: name_columns = list( set(name_columns) & set(user_requested_columns)) # required fields (from Meta.required_fields) required_columns_names = [] for i_name in name_columns: required_columns_names.extend( get_required_fields(i_name, self_json_api.model)) # remove relationship fields name_columns = list( set(name_columns) & set(get_columns_for_query(self_json_api.model))) name_columns = list(set(name_columns) | set(required_columns_names)) query = query.options(load_only(*name_columns)) # Запретим использовать стандартную функцию eagerload_includes для присоединения сторонних молелей setattr(self_json_api, "eagerload_includes", False) query = self._eagerload_includes(query, qs, permission, self_json_api=self_json_api) return query
def _join_biosample_related_tables(self, target_table: Table, query: Query) -> Query: if target_table != Table.biosample: query = query.join(models.Biosample) query = self.join_biosample(query) if target_table == Table.biosample: return query if target_table in envo_tables: return self.join_envo(target_table, query) return query
def apply_joins(self, query: Query, joins: List[SqlJoin]) -> Query: """ Augment the sql alchemy query with joins from the analysis. """ for join in joins: foreign_table = self.get_table(join.right) query = query.join( foreign_table, self.get_column(join.right) == self.get_column(join.left), isouter=True, ) return query
def _join_envo_facet(query: Query, attribute: str) -> Query: if attribute == "env_broad_scale": return query.join( EnvBroadScaleAncestor, EnvBroadScaleTerm.id == EnvBroadScaleAncestor.ancestor_id).join( models.Biosample, models.Biosample.env_broad_scale_id == EnvBroadScaleAncestor.id) elif attribute == "env_local_scale": return query.join( EnvLocalScaleAncestor, EnvLocalScaleTerm.id == EnvLocalScaleAncestor.ancestor_id).join( models.Biosample, models.Biosample.env_local_scale_id == EnvLocalScaleAncestor.id) elif attribute == "env_medium": return query.join( EnvMediumAncestor, EnvMediumTerm.id == EnvMediumAncestor.ancestor_id).join( models.Biosample, models.Biosample.env_medium_id == EnvMediumAncestor.id) else: raise Exception("Unknown envo attribute")
def restricted_incident_filter(query: orm.Query, current_user: DispatchUser, role: UserRoles): """Adds additional incident filters to query (usually for permissions).""" if role != UserRoles.owner: # We don't allow users that are not owners to see restricted incidents query = (query.join( Participant, Incident.id == Participant.incident_id).join( IndividualContact).filter( or_( Incident.visibility == Visibility.open, IndividualContact.email == current_user.email, ))) return query.distinct()
def restricted_incident_filter(query: orm.Query, current_user: DispatchUser, role: UserRoles): """Adds additional incident filters to query (usually for permissions).""" if role == UserRoles.member: # We filter out resticted incidents for users with a member role if the user is not an incident participant query = (query.join( Participant, Incident.id == Participant.incident_id).join( IndividualContact).filter( or_( Incident.visibility == Visibility.open, IndividualContact.email == current_user.email, ))) return query.distinct()
def query(self, base_query: Query, field_value: Any) -> Query: """ Get the current field query Args: base_query: base query used for building the query field_value: value of the field filter Returns: field query """ return base_query.join(getattr(New, self.new_join_field)).filter( getattr(self.destination_entity, self.destination_field) == field_value)
def create_multi_join_view(cls, db, view_name, selectable, joins, order_by=None): """Create a database view named view_name if it doesn't already exist.""" with db.managed_session() as session: query = Query(selectable, session=session) for (join_table, join_clause) in joins: query = query.join(join_table, join_clause) if order_by is not None: query = query.order_by(order_by) cls.__create_view_if_not_exists(session, view_name, str(query))
def restricted_incident_filter(query: orm.Query, current_user: DispatchUser): """Adds additional incident filters to query (usually for permissions)""" return ( query.join(Participant, Incident.id == Participant.incident_id) .join(IndividualContact) .filter( not_( and_( Incident.visibility == Visibility.restricted.value, IndividualContact.email != current_user.email, ) ) ) )
def get_scenario_view_states(self, states_filter=None, pagination=None): """Search for scenario view_states by filter. :param states_filter: instance of :class:`ScenarioStateFilter <autostorage.core.scenario.param_spec.ScenarioStateFilter>`. :param pagination: instance of `Pagination <autostorage.core.param_spec.Pagination>`. :returns: list with instances of :class:`ScenarioState <autostorage.core.scenario.scenario.ScenarioState>`. """ ids_query = Query(ScenarioViewStateRecord) subquery = Query([ ScenarioViewStateRecord.scenario_id, func.max(ScenarioViewStateRecord.changed).label('newest_change_date') ]) if states_filter and states_filter.date: subquery = subquery.filter(ScenarioViewStateRecord.changed <= states_filter.date) subquery = subquery.group_by(ScenarioViewStateRecord.scenario_id).subquery() ids_query = ids_query.join( subquery, and_( ScenarioViewStateRecord.scenario_id == subquery.columns.scenario_id, ScenarioViewStateRecord.changed == subquery.columns.newest_change_date ) ) if pagination: offset = pagination.page_index * pagination.items_per_page ids_query = ids_query.offset(offset).limit(pagination.items_per_page) with self.base.get_session() as session: bound_query = ids_query.with_session(session) states = [] for state_record in bound_query: scenario = ScenarioEntity(self.base, state_record.scenario_id) state = ScenarioViewStateEntity( scenario=scenario, name=state_record.name, description=state_record.description, date=state_record.changed ) states.append(state) return states
def join_to_clusters(base_citation_query: Query) -> Tuple[Query, Alias, Alias]: citing_opinion, cited_opinion = aliased(Opinion), aliased(Opinion) citing_cluster, cited_cluster = aliased(Cluster), aliased(Cluster) return ( ( base_citation_query.join( citing_opinion, Citation.citing_opinion_id == citing_opinion.resource_id, ) .join( cited_opinion, Citation.cited_opinion_id == cited_opinion.resource_id, ) .join( citing_cluster, citing_opinion.cluster_id == citing_cluster.resource_id, ) .join( cited_cluster, cited_opinion.cluster_id == cited_cluster.resource_id ) ), citing_cluster, cited_cluster, )
def restricted_job_filter(query: orm.Query, current_user: DispatchUser, role: UserRoles): """Adds additional incident type filters to query (usually for permissions).""" if current_user: if role == UserRoles.WORKER: query = (query.join( Worker, Worker.id == Job.scheduled_primary_worker_id).join( DispatchUser, DispatchUser.id == Worker.dispatch_user_id).filter( DispatchUser.email == current_user.email)) query.distinct() elif role == UserRoles.PLANNER: team_list = [i.id for i in current_user.managed_teams] team_list.append(current_user.default_team_id) query = query.filter(Job.team_id.in_(set(team_list))) elif role == UserRoles.CUSTOMER: # team_list = [i.id for i in current_user.managed_teams] locs = location_service.get_by_auth_email(db_session=query.session, email=current_user.email) loc_id_list = [i.id for i in locs] query = query.filter(Job.location_id.in_(set(loc_id_list))) return query
def query_select(session, columns, join, tool_metric_filters, multiqc): # Add the Sqlalchemy class columns needed for the given column selection. select_cols = [] # Order of the column output is the order of user's --select input. for col in columns: if col == 'sample': select_cols.extend([ Sample.id, Sample.sample_name, Sample.flowcell_lane, Sample.library_id, Sample.platform, Sample.centre, Sample.reference_genome, Sample.description ]) if col == 'cohort': select_cols.extend([ Cohort.id, Cohort.description, Cohort.sample_count, Cohort.batch_count ]) if col == 'batch': select_cols.extend( [Batch.batch_name, Batch.description, Batch.sample_count]) if col == 'tool-metric': # If filtering on tool, we need to select for the tool metrics. if tool_metric_filters: for tm in tool_metric_filters: # Use the metric name as this column's alias. c = func.max(RawData.metrics[tm[1]].astext).label(tm[1]) c.quote = True select_cols.append(c) else: select_cols.append(RawData.qc_tool) if multiqc: # Need to add batch paths. select_cols.append(Batch.path) query = Query(select_cols, session=session) join['joined'].add( columns[0] ) # The first item in select query doesn't need to be explicitly joined ### ================================= JOIN ==========================================#### # Add the table joins needed for the given column selection or filtering. if 'sample' in join['joins'] and 'sample' not in join['joined']: if 'tool-metric' in join['joined']: query = query.join(Sample, Sample.id == RawData.sample_id) elif 'batch' in join['joined']: query = query.join(Sample, Sample.batch_id == Batch.id) elif 'cohort' in join['joined']: query = query.join(Sample, Sample.cohort_id == Cohort.id) join['joined'].add('sample') # For multiqc we need Batch for batch.path. if (multiqc or 'batch' in join['joins']) and 'batch' not in join['joined']: if 'tool-metric' in join['joined'] and 'sample' not in join['joined']: query = query.join(Sample, Sample.id == RawData.sample_id) join['joined'].add('sample') if 'sample' in join['joined']: query = query.join(Batch, Batch.id == Sample.batch_id) elif 'cohort' in join['joined']: query = query.join(Batch, Batch.cohort_id == Cohort.id) join['joined'].add('batch') if 'cohort' in join['joins'] and 'cohort' not in join['joined']: if 'tool-metric' in join['joined'] and 'sample' not in join['joined']: query = query.join(Sample, Sample.id == RawData.sample_id) join['joined'].add('sample') if 'sample' in join['joined']: query = query.join(Cohort, Cohort.id == Sample.cohort_id) elif 'batch' in join['joined']: query = query.join(Cohort, Cohort.id == Batch.cohort_id) join['joined'].add('cohort') if 'tool-metric' in join['joins'] and 'tool-metric' not in join['joined']: if 'batch' in join['joined'] and 'sample' not in join['joined']: query = query.join(Sample, Sample.batch_id == Batch.id) elif 'cohort' in join['joined'] and 'sample' not in join['joined']: query = query.join(Sample, Sample.cohort_id == Cohort.id) query = query.join(RawData, RawData.sample_id == Sample.id) join['joined'].add('tool-metric') return query
def filter_users_with_at_least_one_not_validated_offerer_validated_user_offerer( query: Query) -> Query: return (query.join(UserOfferer).join( Offerer).filter((Offerer.validationToken != None) & (UserOfferer.validationToken == None)))
class QueryMaker(object): def __init__( self, # An optional `Declarative class <http://docs.sqlalchemy.org/en/latest/orm/tutorial.html#declare-a-mapping>`_ to query. declarative_class=None, # Optionally, begin with an existing query_. query=None): if declarative_class: assert _is_mapped_class(declarative_class) # If a query is provided, try to infer the declarative_class. if query is not None: assert isinstance(query, Query) self._query = query try: self._select = self._get_joinpoint_zero_class() except: # We can't infer it. Use what's provided instead, and add this to the query. assert declarative_class self._select = declarative_class self._query = self._query.select_from(declarative_class) else: # If a declarative_class was provided, make sure it's consistent with the inferred class. if declarative_class: assert declarative_class is self._select else: # The declarative class must be provided if the query wasn't. assert declarative_class # Since a query was not provied, create an empty `query <http://docs.sqlalchemy.org/en/latest/orm/query.html>`_; ``to_query`` will fill in the missing information. self._query = Query([]).select_from(declarative_class) # Keep track of the last selectable construct, to generate the select in ``to_query``. self._select = declarative_class # Copied verbatim from ``sqlalchemy.orm.query.Query._clone``. This adds the support needed for the _`generative` interface. (Mostly) quoting from query_, "QueryMaker_ features a generative interface whereby successive calls return a new QueryMaker_ object, a copy of the former with additional criteria and options associated with it." def _clone(self): cls = self.__class__ q = cls.__new__(cls) q.__dict__ = self.__dict__.copy() return q # Looking up a class's `Column <http://docs.sqlalchemy.org/en/latest/core/metadata.html#sqlalchemy.schema.Column>`_ or `relationship <http://docs.sqlalchemy.org/en/latest/orm/relationship_api.html#sqlalchemy.orm.relationship>`_ generates the matching query. @_generative() def __getattr__(self, name): # Find the Column_ or relationship_ in the join point class we're querying. attr = getattr(self._get_joinpoint_zero_class(), name) # If the attribute refers to a column, save this as a possible select statement. Note that a Column_ gets replaced with an `InstrumentedAttribute <http://docs.sqlalchemy.org/en/latest/orm/internals.html?highlight=instrumentedattribute#sqlalchemy.orm.attributes.InstrumentedAttribute>`_; see `QueryableAttribute <http://docs.sqlalchemy.org/en/latest/orm/internals.html?highlight=instrumentedattribute#sqlalchemy.orm.attributes.QueryableAttribute.property>`_. if isinstance(attr.property, ColumnProperty): self._select = attr elif isinstance(attr.property, RelationshipProperty): # Figure out what class this relationship refers to. See `mapper.params.class_ <http://docs.sqlalchemy.org/en/latest/orm/mapping_api.html?highlight=mapper#sqlalchemy.orm.mapper.params.class_>`_. declarative_class = attr.property.mapper.class_ # Update the query by performing the implied join. self._query = self._query.join(declarative_class) # Save this relationship as a possible select statement. self._select = declarative_class else: # This isn't a Column_ or a relationship_. assert False # Indexing the object performs the implied filter. For example, ``session(User)['jack']`` implies ``session.query(User).filter(User.name == 'jack')``. @_generative() def __getitem__( self, # Most often, this is a key which will be filtered by the ``default_query`` method of the currently-active `Declarative class`_. In the example above, the ``User`` class must define a ``default_query`` to operate on strings. However, it may also be a filter criterion, such as ``session(User)[User.name == 'jack']``. key): # See if this is a filter criterion; if not, rely in the ``default_query`` defined by the `Declarative class`_ or fall back to the first primary key. criteria = None jp0_class = self._get_joinpoint_zero_class() if isinstance(key, ClauseElement): criteria = key elif hasattr(jp0_class, 'default_query'): criteria = jp0_class.default_query(key) if criteria is None: pks = inspect(jp0_class).primary_key criteria = pks[0] == key self._query = self._query.filter(criteria) # Support common syntax: ``for x in query_maker:`` converts this to a query and returns results. The session must already have been set. def __iter__(self): return self.to_query().__iter__() # This property returns a `_QueryWrapper`_, a query-like object which transforms returned Query_ values back into this class while leaving other return values unchanged. @property def q(self): return _QueryWrapper(self) # Transform this object into a Query_. def to_query( self, # Optionally, the `Session <http://docs.sqlalchemy.org/en/latest/orm/session_api.html?highlight=session#sqlalchemy.orm.session.Session>`_ to run this query in. session=None): # If a session was specified, use it to produce the query_; otherwise, use the existing query_. query = self._query.with_session(session) if session else self._query # Choose the correct method to select either a column or a class (e.g. an entity). As noted earlier, a Column_ becomes and InstrumentedAttribute_. if isinstance(self._select, InstrumentedAttribute): return query.add_columns(self._select) else: return query.add_entity(self._select) # Get the right-most join point in the current query. def _get_joinpoint_zero_class(self): jp0 = self._query._joinpoint_zero() # If the join point was returned as a `Mapper <http://docs.sqlalchemy.org/en/latest/orm/mapping_api.html#sqlalchemy.orm.mapper.Mapper>`_, get the underlying class. if isinstance(jp0, Mapper): jp0 = jp0.class_ return jp0
def _join_omics_processing_related_tables(self, target_table: Table, query: Query) -> Query: if target_table != Table.omics_processing: query = query.join(models.OmicsProcessing) return self.join_omics_processing(query)
def _join_study_related_tables(self, target_table: Table, query: Query) -> Query: if target_table != Table.study: query = query.join(models.Study) return self.join_study(query)
def join_self(self, query: Query, parent: Table) -> Query: if self.table == parent: return query return query.join(self.table.model)
def join_study(self, query: Query) -> Query: return self.join_self( query.join(models.OmicsProcessing, models.Study.id == models.OmicsProcessing.study_id), Table.omics_processing, )
def join_study(self, query: Query) -> Query: return self.join_self( query.join(models.Biosample, models.Study.id == models.Biosample.study_id), Table.biosample, )