diff --git a/.idea/runConfigurations/LapisV2Open.xml b/.idea/runConfigurations/LapisV2Open.xml index e633be40..ee0c00c5 100644 --- a/.idea/runConfigurations/LapisV2Open.xml +++ b/.idea/runConfigurations/LapisV2Open.xml @@ -1,11 +1,11 @@ - - - - + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/LapisV2Protected.xml b/.idea/runConfigurations/LapisV2Protected.xml index 7822c70b..adfe5de2 100644 --- a/.idea/runConfigurations/LapisV2Protected.xml +++ b/.idea/runConfigurations/LapisV2Protected.xml @@ -1,11 +1,11 @@ - - - - + + + + \ No newline at end of file diff --git a/lapis2/build.gradle b/lapis2/build.gradle index 82cd2441..5bd2ab09 100644 --- a/lapis2/build.gradle +++ b/lapis2/build.gradle @@ -79,8 +79,9 @@ openApi { apiDocsUrl.set("http://localhost:8080/api-docs") customBootRun { args.set([ - "--silo.url=does.not.matter.here", - "--lapis.databaseConfig.path=../siloLapisTests/testData/testDatabaseConfig.yaml" + "--silo.url=does.not.matter.here", + "--lapis.databaseConfig.path=../siloLapisTests/testData/testDatabaseConfig.yaml", + "--referenceGenomeFilename=../siloLapisTests/testData/reference-genomes.json" ]) } } diff --git a/lapis2/docker-compose.yml b/lapis2/docker-compose.yml index 1ceb9567..62fe23d8 100644 --- a/lapis2/docker-compose.yml +++ b/lapis2/docker-compose.yml @@ -4,12 +4,16 @@ services: image: ghcr.io/genspectrum/lapis-v2:${LAPIS_TAG} ports: - "8080:8080" - command: --silo.url=http://silo:8081 --lapis.databaseConfig.path=databaseConfig.yaml + command: --silo.url=http://silo:8081 --lapis.databaseConfig.path=databaseConfig.yaml --referenceGenomeFilename=reference-genomes.json volumes: - type: bind source: ../siloLapisTests/testData/testDatabaseConfig.yaml target: /workspace/databaseConfig.yaml read_only: true + - type: bind + source: ../siloLapisTests/testData/reference-genomes.json + target: /workspace/reference-genomes.json + read_only: true silo: image: ghcr.io/genspectrum/lapis-silo:${SILO_TAG} diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisApplication.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisApplication.kt index 9a254ed0..2096785d 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisApplication.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisApplication.kt @@ -1,5 +1,6 @@ package org.genspectrum.lapis +import org.genspectrum.lapis.config.ReferenceGenome import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @@ -7,5 +8,7 @@ import org.springframework.boot.runApplication class Lapisv2Application fun main(args: Array) { - runApplication(*args) + val referenceGenomeArgs = ReferenceGenome.readFromFileFromProgramArgs(args).toSpringApplicationArgs() + + runApplication(*(args + referenceGenomeArgs)) } diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt index 50714d59..7c7e355b 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/LapisSpringConfig.kt @@ -4,6 +4,9 @@ import com.fasterxml.jackson.module.kotlin.readValue import mu.KotlinLogging import org.genspectrum.lapis.auth.DataOpennessAuthorizationFilterFactory import org.genspectrum.lapis.config.DatabaseConfig +import org.genspectrum.lapis.config.NucleotideSequence +import org.genspectrum.lapis.config.REFERENCE_GENOME_APPLICATION_ARG_PREFIX +import org.genspectrum.lapis.config.ReferenceGenome import org.genspectrum.lapis.config.SequenceFilterFields import org.genspectrum.lapis.logging.RequestContext import org.genspectrum.lapis.logging.RequestContextLogger @@ -60,4 +63,11 @@ class LapisSpringConfig { fun dataOpennessAuthorizationFilter( dataOpennessAuthorizationFilterFactory: DataOpennessAuthorizationFilterFactory, ) = dataOpennessAuthorizationFilterFactory.create() + + @Bean + fun referenceGenome( + @Value("\${$REFERENCE_GENOME_APPLICATION_ARG_PREFIX}") nucleotideSegments: List, + ): ReferenceGenome { + return ReferenceGenome(nucleotideSegments.map { NucleotideSequence(it) }) + } } diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/OpenApiDocs.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/OpenApiDocs.kt index cc7d0d49..441ae9d7 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/OpenApiDocs.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/OpenApiDocs.kt @@ -42,6 +42,7 @@ import org.genspectrum.lapis.controller.ORDER_BY_FIELDS_SCHEMA import org.genspectrum.lapis.controller.ORDER_BY_PROPERTY import org.genspectrum.lapis.controller.REQUEST_SCHEMA_WITH_MIN_PROPORTION import org.genspectrum.lapis.controller.SEQUENCE_FILTERS_SCHEMA +import org.genspectrum.lapis.controller.SEQUENCE_REQUEST_SCHEMA import org.genspectrum.lapis.request.AminoAcidInsertion import org.genspectrum.lapis.request.AminoAcidMutation import org.genspectrum.lapis.request.NucleotideInsertion @@ -64,8 +65,9 @@ fun buildOpenApiSchema(sequenceFilterFields: SequenceFilterFields, databaseConfi Pair(AMINO_ACID_INSERTIONS_PROPERTY, aminoAcidInsertions()) + Pair(ORDER_BY_PROPERTY, orderByPostSchema()) + Pair(LIMIT_PROPERTY, limitSchema()) + - Pair(OFFSET_PROPERTY, offsetSchema()) + - Pair(FORMAT_PROPERTY, formatSchema()) + Pair(OFFSET_PROPERTY, offsetSchema()) + + val sequenceFiltersWithFormat = sequenceFilters + Pair(FORMAT_PROPERTY, formatSchema()) return OpenAPI() .components( @@ -82,18 +84,24 @@ fun buildOpenApiSchema(sequenceFilterFields: SequenceFilterFields, databaseConfi Schema() .type("object") .description("valid filters for sequence data") - .properties(sequenceFilters + Pair(MIN_PROPORTION_PROPERTY, Schema().type("number"))), + .properties( + sequenceFiltersWithFormat + Pair(MIN_PROPORTION_PROPERTY, Schema().type("number")), + ), ) .addSchemas( AGGREGATED_REQUEST_SCHEMA, - requestSchemaWithFields(sequenceFilters, AGGREGATED_GROUP_BY_FIELDS_DESCRIPTION), + requestSchemaWithFields(sequenceFiltersWithFormat, AGGREGATED_GROUP_BY_FIELDS_DESCRIPTION), ) .addSchemas( DETAILS_REQUEST_SCHEMA, - requestSchemaWithFields(sequenceFilters, DETAILS_FIELDS_DESCRIPTION), + requestSchemaWithFields(sequenceFiltersWithFormat, DETAILS_FIELDS_DESCRIPTION), ) .addSchemas( INSERTIONS_REQUEST_SCHEMA, + requestSchemaForCommonSequenceFilters(sequenceFiltersWithFormat), + ) + .addSchemas( + SEQUENCE_REQUEST_SCHEMA, requestSchemaForCommonSequenceFilters(sequenceFilters), ) .addSchemas( diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt index c2b5a937..994305ea 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/auth/DataOpennessAuthorizationFilter.kt @@ -9,8 +9,13 @@ import org.genspectrum.lapis.config.AccessKeysReader import org.genspectrum.lapis.config.DatabaseConfig import org.genspectrum.lapis.config.OpennessLevel import org.genspectrum.lapis.controller.ACCESS_KEY_PROPERTY +import org.genspectrum.lapis.controller.AGGREGATED_ROUTE +import org.genspectrum.lapis.controller.AMINO_ACID_INSERTIONS_ROUTE +import org.genspectrum.lapis.controller.AMINO_ACID_MUTATIONS_ROUTE import org.genspectrum.lapis.controller.LapisError import org.genspectrum.lapis.controller.LapisErrorResponse +import org.genspectrum.lapis.controller.NUCLEOTIDE_INSERTIONS_ROUTE +import org.genspectrum.lapis.controller.NUCLEOTIDE_MUTATIONS_ROUTE import org.genspectrum.lapis.util.CachedBodyHttpServletRequest import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -93,7 +98,13 @@ private class ProtectedDataAuthorizationFilter( companion object { private val WHITELISTED_PATHS = listOf("/swagger-ui", "/api-docs") - private val ENDPOINTS_THAT_SERVE_AGGREGATED_DATA = listOf("/aggregated", "/nucleotideMutations") + private val ENDPOINTS_THAT_SERVE_AGGREGATED_DATA = listOf( + AGGREGATED_ROUTE, + NUCLEOTIDE_MUTATIONS_ROUTE, + AMINO_ACID_MUTATIONS_ROUTE, + NUCLEOTIDE_INSERTIONS_ROUTE, + AMINO_ACID_INSERTIONS_ROUTE, + ) } override fun isAuthorizedForEndpoint(request: CachedBodyHttpServletRequest): AuthorizationResult { diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/config/ReferenceGenome.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/config/ReferenceGenome.kt new file mode 100644 index 00000000..eb05b82d --- /dev/null +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/config/ReferenceGenome.kt @@ -0,0 +1,53 @@ +package org.genspectrum.lapis.config + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import java.io.File + +const val REFERENCE_GENOME_APPLICATION_ARG_PREFIX = "referenceGenome.nucleotideSequences" + +@JsonIgnoreProperties(ignoreUnknown = true) +class ReferenceGenome( + @JsonProperty("nucleotide_sequences") + val nucleotideSequences: List, +) { + fun isSingleSegmented(): Boolean { + return nucleotideSequences.size == 1 + } + + companion object { + fun readFromFile(filename: String): ReferenceGenome { + return jacksonObjectMapper().readValue(File(filename)) + } + + private fun readFilenameFromProgramArgs(args: Array): String { + val referenceGenomeArg = args.find { it.startsWith("--referenceGenomeFilename=") } + return referenceGenomeArg?.substringAfter("=") ?: throw IllegalArgumentException( + "No reference genome filename specified. Please specify a reference genome filename using the " + + "--referenceGenomeFilename argument.", + ) + } + + fun readFromFileFromProgramArgs(args: Array): ReferenceGenome { + return readFromFile(readFilenameFromProgramArgs(args)) + } + } + + fun toSpringApplicationArgs(): Array { + val nucleotideSequenceArgs = + "--$REFERENCE_GENOME_APPLICATION_ARG_PREFIX=" + this.nucleotideSequences.joinToString( + separator = ",", + ) { + it.name + } + + return arrayOf(nucleotideSequenceArgs) + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class NucleotideSequence( + val name: String, +) diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/config/SingleSegmentedSequenceFeature.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/config/SingleSegmentedSequenceFeature.kt deleted file mode 100644 index a02427bf..00000000 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/config/SingleSegmentedSequenceFeature.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.genspectrum.lapis.config - -import org.springframework.stereotype.Component - -const val IS_SEQUENCE_SINGLE_SEGMENTED_FEATURE = "isSingleSegmentedSequence" - -@Component -class SingleSegmentedSequenceFeature(private val databaseConfig: DatabaseConfig) { - fun isEnabled() = - databaseConfig.schema.features.any { it.name == IS_SEQUENCE_SINGLE_SEGMENTED_FEATURE } -} diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt index d26389e6..d736c1f7 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt @@ -15,11 +15,11 @@ import org.genspectrum.lapis.model.SiloQueryModel import org.genspectrum.lapis.request.AminoAcidInsertion import org.genspectrum.lapis.request.AminoAcidMutation import org.genspectrum.lapis.request.CommonSequenceFilters -import org.genspectrum.lapis.request.InsertionsRequest import org.genspectrum.lapis.request.MutationProportionsRequest import org.genspectrum.lapis.request.NucleotideInsertion import org.genspectrum.lapis.request.NucleotideMutation import org.genspectrum.lapis.request.OrderByField +import org.genspectrum.lapis.request.SequenceFiltersRequest import org.genspectrum.lapis.request.SequenceFiltersRequestWithFields import org.genspectrum.lapis.response.AggregationData import org.genspectrum.lapis.response.AminoAcidInsertionResponse @@ -27,8 +27,10 @@ import org.genspectrum.lapis.response.AminoAcidMutationResponse import org.genspectrum.lapis.response.DetailsData import org.genspectrum.lapis.response.NucleotideInsertionResponse import org.genspectrum.lapis.response.NucleotideMutationResponse +import org.genspectrum.lapis.silo.SequenceType import org.springframework.http.MediaType import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestParam @@ -39,6 +41,7 @@ const val REQUEST_SCHEMA_WITH_MIN_PROPORTION = "SequenceFiltersWithMinProportion const val AGGREGATED_REQUEST_SCHEMA = "AggregatedPostRequest" const val DETAILS_REQUEST_SCHEMA = "DetailsPostRequest" const val INSERTIONS_REQUEST_SCHEMA = "InsertionsRequest" +const val SEQUENCE_REQUEST_SCHEMA = "SequenceRequest" const val AGGREGATED_RESPONSE_SCHEMA = "AggregatedResponse" const val DETAILS_RESPONSE_SCHEMA = "DetailsResponse" @@ -72,6 +75,10 @@ const val AMINO_ACID_INSERTIONS_ENDPOINT_DESCRIPTION = "Returns a list of mutations along with the counts and proportions whose proportions are greater " + "than or equal to the specified minProportion. Only sequences matching the specified " + "sequence filters are considered." + +const val AMINO_ACID_SEQUENCE_ENDPOINT_DESCRIPTION = + "Returns a string of fasta formated amino acid sequences. Only sequences matching the specified " + + "sequence filters are considered." const val AGGREGATED_GROUP_BY_FIELDS_DESCRIPTION = "The fields to stratify by. If empty, only the overall count is returned" const val AGGREGATED_ORDER_BY_FIELDS_DESCRIPTION = @@ -88,13 +95,22 @@ const val FORMAT_DESCRIPTION = "The data format of the response. " + "Alternatively, the data format can be specified by setting the \"Accept\"-header. When both are specified, " + "this parameter takes precedence." +const val AGGREGATED_ROUTE = "/aggregated" +const val DETAILS_ROUTE = "/details" +const val NUCLEOTIDE_MUTATIONS_ROUTE = "/nucleotideMutations" +const val AMINO_ACID_MUTATIONS_ROUTE = "/aminoAcidMutations" +const val NUCLEOTIDE_INSERTIONS_ROUTE = "/nucleotideInsertions" +const val AMINO_ACID_INSERTIONS_ROUTE = "/aminoAcidInsertions" +const val ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE = "/alignedNucleotideSequences" +const val AMINO_ACID_SEQUENCES_ROUTE = "/aminoAcidSequences" + @RestController class LapisController( private val siloQueryModel: SiloQueryModel, private val requestContext: RequestContext, private val csvWriter: CsvWriter, ) { - @GetMapping("/aggregated", produces = [MediaType.APPLICATION_JSON_VALUE]) + @GetMapping(AGGREGATED_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisAggregatedResponse fun aggregated( @SequenceFilters @@ -160,7 +176,7 @@ class LapisController( return LapisResponse(siloQueryModel.getAggregated(request)) } - @GetMapping("/aggregated", produces = [TEXT_CSV_HEADER]) + @GetMapping(AGGREGATED_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = AGGREGATED_ENDPOINT_DESCRIPTION, operationId = "getAggregatedAsCsv", @@ -228,7 +244,7 @@ class LapisController( return getResponseAsCsv(request, COMMA, siloQueryModel::getAggregated) } - @GetMapping("/aggregated", produces = [TEXT_TSV_HEADER]) + @GetMapping(AGGREGATED_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = AGGREGATED_ENDPOINT_DESCRIPTION, operationId = "getAggregatedAsTsv", @@ -296,7 +312,7 @@ class LapisController( return getResponseAsCsv(request, TAB, siloQueryModel::getAggregated) } - @PostMapping("/aggregated", produces = [MediaType.APPLICATION_JSON_VALUE]) + @PostMapping(AGGREGATED_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisAggregatedResponse @Operation( operationId = "postAggregated", @@ -311,7 +327,7 @@ class LapisController( return LapisResponse(siloQueryModel.getAggregated(request)) } - @PostMapping("/aggregated", produces = [TEXT_CSV_HEADER]) + @PostMapping(AGGREGATED_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = AGGREGATED_ENDPOINT_DESCRIPTION, operationId = "postAggregatedAsCsv", @@ -325,7 +341,7 @@ class LapisController( return getResponseAsCsv(request, COMMA, siloQueryModel::getAggregated) } - @PostMapping("/aggregated", produces = [TEXT_TSV_HEADER]) + @PostMapping(AGGREGATED_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = AGGREGATED_ENDPOINT_DESCRIPTION, operationId = "postAggregatedAsTsv", @@ -339,7 +355,7 @@ class LapisController( return getResponseAsCsv(request, TAB, siloQueryModel::getAggregated) } - @GetMapping("/nucleotideMutations", produces = [MediaType.APPLICATION_JSON_VALUE]) + @GetMapping(NUCLEOTIDE_MUTATIONS_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisNucleotideMutationsResponse fun getNucleotideMutations( @Parameter( @@ -406,7 +422,7 @@ class LapisController( return LapisResponse(result) } - @GetMapping("/nucleotideMutations", produces = [TEXT_CSV_HEADER]) + @GetMapping(NUCLEOTIDE_MUTATIONS_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = NUCLEOTIDE_MUTATION_ENDPOINT_DESCRIPTION, operationId = "getNucleotideMutationsAsCsv", @@ -468,7 +484,7 @@ class LapisController( return getResponseAsCsv(request, COMMA, siloQueryModel::computeNucleotideMutationProportions) } - @GetMapping("/nucleotideMutations", produces = [TEXT_TSV_HEADER]) + @GetMapping(NUCLEOTIDE_MUTATIONS_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = NUCLEOTIDE_MUTATION_ENDPOINT_DESCRIPTION, operationId = "getNucleotideMutationsAsTsv", @@ -530,7 +546,7 @@ class LapisController( return getResponseAsCsv(request, TAB, siloQueryModel::computeNucleotideMutationProportions) } - @PostMapping("/nucleotideMutations", produces = [MediaType.APPLICATION_JSON_VALUE]) + @PostMapping(NUCLEOTIDE_MUTATIONS_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisNucleotideMutationsResponse @Operation( operationId = "postNucleotideMutations", @@ -546,7 +562,7 @@ class LapisController( return LapisResponse(result) } - @PostMapping("/nucleotideMutations", produces = [TEXT_CSV_HEADER]) + @PostMapping(NUCLEOTIDE_MUTATIONS_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = NUCLEOTIDE_MUTATION_ENDPOINT_DESCRIPTION, operationId = "postNucleotideMutationsAsCsv", @@ -560,7 +576,7 @@ class LapisController( return getResponseAsCsv(mutationProportionsRequest, COMMA, siloQueryModel::computeNucleotideMutationProportions) } - @PostMapping("/nucleotideMutations", produces = [TEXT_TSV_HEADER]) + @PostMapping(NUCLEOTIDE_MUTATIONS_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = NUCLEOTIDE_MUTATION_ENDPOINT_DESCRIPTION, operationId = "postNucleotideMutationsAsTsv", @@ -574,7 +590,7 @@ class LapisController( return getResponseAsCsv(mutationProportionsRequest, TAB, siloQueryModel::computeNucleotideMutationProportions) } - @GetMapping("/aminoAcidMutations", produces = [MediaType.APPLICATION_JSON_VALUE]) + @GetMapping(AMINO_ACID_MUTATIONS_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisAminoAcidMutationsResponse fun getAminoAcidMutations( @Parameter( @@ -634,7 +650,7 @@ class LapisController( return LapisResponse(result) } - @GetMapping("/aminoAcidMutations", produces = [TEXT_CSV_HEADER]) + @GetMapping(AMINO_ACID_MUTATIONS_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = AMINO_ACID_MUTATIONS_ENDPOINT_DESCRIPTION, operationId = "getAminoAcidMutationsAsCsv", @@ -696,7 +712,7 @@ class LapisController( return getResponseAsCsv(mutationProportionsRequest, COMMA, siloQueryModel::computeAminoAcidMutationProportions) } - @GetMapping("/aminoAcidMutations", produces = [TEXT_TSV_HEADER]) + @GetMapping(AMINO_ACID_MUTATIONS_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = AMINO_ACID_MUTATIONS_ENDPOINT_DESCRIPTION, operationId = "getAminoAcidMutationsAsTsv", @@ -762,7 +778,7 @@ class LapisController( ) } - @PostMapping("/aminoAcidMutations", produces = [MediaType.APPLICATION_JSON_VALUE]) + @PostMapping(AMINO_ACID_MUTATIONS_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisAminoAcidMutationsResponse @Operation( operationId = "postAminoAcidMutations", @@ -778,7 +794,7 @@ class LapisController( return LapisResponse(result) } - @PostMapping("/aminoAcidMutations", produces = [TEXT_CSV_HEADER]) + @PostMapping(AMINO_ACID_MUTATIONS_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = AMINO_ACID_MUTATIONS_ENDPOINT_DESCRIPTION, operationId = "postAminoAcidMutationsAsCsv", @@ -798,7 +814,7 @@ class LapisController( ) } - @PostMapping("/aminoAcidMutations", produces = [TEXT_TSV_HEADER]) + @PostMapping(AMINO_ACID_MUTATIONS_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = AMINO_ACID_MUTATIONS_ENDPOINT_DESCRIPTION, operationId = "postAminoAcidMutationsAsCsv", @@ -818,7 +834,7 @@ class LapisController( ) } - @GetMapping("/details", produces = [MediaType.APPLICATION_JSON_VALUE]) + @GetMapping(DETAILS_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisDetailsResponse fun getDetailsAsJson( @SequenceFilters @@ -881,7 +897,7 @@ class LapisController( return LapisResponse(siloQueryModel.getDetails(request)) } - @GetMapping("/details", produces = [TEXT_CSV_HEADER]) + @GetMapping(DETAILS_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = DETAILS_ENDPOINT_DESCRIPTION, operationId = "getDetailsAsCsv", @@ -940,7 +956,7 @@ class LapisController( return getResponseAsCsv(request, COMMA, siloQueryModel::getDetails) } - @GetMapping("/details", produces = [TEXT_TSV_HEADER]) + @GetMapping(DETAILS_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = DETAILS_ENDPOINT_DESCRIPTION, operationId = "getDetailsAsTsv", @@ -999,7 +1015,7 @@ class LapisController( return getResponseAsCsv(request, TAB, siloQueryModel::getDetails) } - @PostMapping("/details", produces = [MediaType.APPLICATION_JSON_VALUE]) + @PostMapping(DETAILS_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisDetailsResponse @Operation( operationId = "postDetails", @@ -1014,7 +1030,7 @@ class LapisController( return LapisResponse(siloQueryModel.getDetails(request)) } - @PostMapping("/details", produces = [TEXT_CSV_HEADER]) + @PostMapping(DETAILS_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = DETAILS_ENDPOINT_DESCRIPTION, operationId = "postDetailsAsCsv", @@ -1028,7 +1044,7 @@ class LapisController( return getResponseAsCsv(request, COMMA, siloQueryModel::getDetails) } - @PostMapping("/details", produces = [TEXT_TSV_HEADER]) + @PostMapping(DETAILS_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = DETAILS_ENDPOINT_DESCRIPTION, operationId = "postDetailsAsTsv", @@ -1042,7 +1058,7 @@ class LapisController( return getResponseAsCsv(request, TAB, siloQueryModel::getDetails) } - @GetMapping("/nucleotideInsertions", produces = [MediaType.APPLICATION_JSON_VALUE]) + @GetMapping(NUCLEOTIDE_INSERTIONS_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisNucleotideInsertionsResponse fun getNucleotideInsertions( @SequenceFilters @@ -1064,8 +1080,10 @@ class LapisController( @RequestParam aminoAcidMutations: List?, @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_INSERTIONS_SCHEMA")) nucleotideInsertions: List?, @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_INSERTIONS_SCHEMA")) aminoAcidInsertions: List?, @Parameter( schema = Schema(ref = "#/components/schemas/$LIMIT_SCHEMA"), @@ -1086,7 +1104,7 @@ class LapisController( @RequestParam dataFormat: String? = null, ): LapisResponse> { - val insertionRequest = InsertionsRequest( + val request = SequenceFiltersRequest( sequenceFilters?.filter { !SPECIAL_REQUEST_PROPERTIES.contains(it.key) } ?: emptyMap(), nucleotideMutations ?: emptyList(), aminoAcidMutations ?: emptyList(), @@ -1097,13 +1115,13 @@ class LapisController( offset, ) - requestContext.filter = insertionRequest + requestContext.filter = request - val result = siloQueryModel.getNucleotideInsertions(insertionRequest) + val result = siloQueryModel.getNucleotideInsertions(request) return LapisResponse(result) } - @GetMapping("/nucleotideInsertions", produces = [TEXT_CSV_HEADER]) + @GetMapping(NUCLEOTIDE_INSERTIONS_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = NUCLEOTIDE_INSERTIONS_ENDPOINT_DESCRIPTION, operationId = "getNucleotideInsertionsAsCsv", @@ -1129,6 +1147,7 @@ class LapisController( @RequestParam aminoAcidMutations: List?, @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_INSERTIONS_SCHEMA")) nucleotideInsertions: List?, @RequestParam aminoAcidInsertions: List?, @@ -1151,7 +1170,7 @@ class LapisController( @RequestParam dataFormat: String? = null, ): String { - val insertionRequest = InsertionsRequest( + val request = SequenceFiltersRequest( sequenceFilters?.filter { !SPECIAL_REQUEST_PROPERTIES.contains(it.key) } ?: emptyMap(), nucleotideMutations ?: emptyList(), aminoAcidMutations ?: emptyList(), @@ -1162,12 +1181,12 @@ class LapisController( offset, ) - requestContext.filter = insertionRequest + requestContext.filter = request - return getResponseAsCsv(insertionRequest, COMMA, siloQueryModel::getNucleotideInsertions) + return getResponseAsCsv(request, COMMA, siloQueryModel::getNucleotideInsertions) } - @GetMapping("/nucleotideInsertions", produces = [TEXT_TSV_HEADER]) + @GetMapping(NUCLEOTIDE_INSERTIONS_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = NUCLEOTIDE_INSERTIONS_ENDPOINT_DESCRIPTION, operationId = "getNucleotideInsertionsAsTsv", @@ -1193,8 +1212,10 @@ class LapisController( @RequestParam aminoAcidMutations: List?, @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_INSERTIONS_SCHEMA")) nucleotideInsertions: List?, @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_INSERTIONS_SCHEMA")) aminoAcidInsertions: List?, @Parameter( schema = Schema(ref = "#/components/schemas/$LIMIT_SCHEMA"), @@ -1215,7 +1236,7 @@ class LapisController( @RequestParam dataFormat: String? = null, ): String { - val insertionRequest = InsertionsRequest( + val request = SequenceFiltersRequest( sequenceFilters?.filter { !SPECIAL_REQUEST_PROPERTIES.contains(it.key) } ?: emptyMap(), nucleotideMutations ?: emptyList(), aminoAcidMutations ?: emptyList(), @@ -1226,12 +1247,12 @@ class LapisController( offset, ) - requestContext.filter = insertionRequest + requestContext.filter = request - return getResponseAsCsv(insertionRequest, TAB, siloQueryModel::getNucleotideInsertions) + return getResponseAsCsv(request, TAB, siloQueryModel::getNucleotideInsertions) } - @PostMapping("/nucleotideInsertions", produces = [MediaType.APPLICATION_JSON_VALUE]) + @PostMapping(NUCLEOTIDE_INSERTIONS_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisNucleotideInsertionsResponse @Operation( operationId = "postNucleotideInsertions", @@ -1239,7 +1260,7 @@ class LapisController( fun postNucleotideInsertions( @Parameter(schema = Schema(ref = "#/components/schemas/$INSERTIONS_REQUEST_SCHEMA")) @RequestBody - request: InsertionsRequest, + request: SequenceFiltersRequest, ): LapisResponse> { requestContext.filter = request @@ -1247,7 +1268,7 @@ class LapisController( return LapisResponse(result) } - @PostMapping("/nucleotideInsertions", produces = [TEXT_CSV_HEADER]) + @PostMapping(NUCLEOTIDE_INSERTIONS_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = NUCLEOTIDE_INSERTIONS_ENDPOINT_DESCRIPTION, operationId = "postNucleotideInsertionsAsCsv", @@ -1256,14 +1277,14 @@ class LapisController( fun postNucleotideInsertionsAsCsv( @Parameter(schema = Schema(ref = "#/components/schemas/$INSERTIONS_REQUEST_SCHEMA")) @RequestBody - request: InsertionsRequest, + request: SequenceFiltersRequest, ): String { requestContext.filter = request return getResponseAsCsv(request, COMMA, siloQueryModel::getNucleotideInsertions) } - @PostMapping("/nucleotideInsertions", produces = [TEXT_TSV_HEADER]) + @PostMapping(NUCLEOTIDE_INSERTIONS_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = NUCLEOTIDE_INSERTIONS_ENDPOINT_DESCRIPTION, operationId = "postNucleotideInsertionsAsTsv", @@ -1272,14 +1293,14 @@ class LapisController( fun postNucleotideInsertionsAsTsv( @Parameter(schema = Schema(ref = "#/components/schemas/$INSERTIONS_REQUEST_SCHEMA")) @RequestBody - request: InsertionsRequest, + request: SequenceFiltersRequest, ): String { requestContext.filter = request return getResponseAsCsv(request, TAB, siloQueryModel::getNucleotideInsertions) } - @GetMapping("/aminoAcidInsertions", produces = [MediaType.APPLICATION_JSON_VALUE]) + @GetMapping(AMINO_ACID_INSERTIONS_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisAminoAcidInsertionsResponse fun getAminoAcidInsertions( @SequenceFilters @@ -1301,8 +1322,10 @@ class LapisController( @RequestParam aminoAcidMutations: List?, @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_INSERTIONS_SCHEMA")) nucleotideInsertions: List?, @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_INSERTIONS_SCHEMA")) aminoAcidInsertions: List?, @Parameter( schema = Schema(ref = "#/components/schemas/$LIMIT_SCHEMA"), @@ -1323,7 +1346,7 @@ class LapisController( @RequestParam dataFormat: String? = null, ): LapisResponse> { - val insertionRequest = InsertionsRequest( + val request = SequenceFiltersRequest( sequenceFilters?.filter { !SPECIAL_REQUEST_PROPERTIES.contains(it.key) } ?: emptyMap(), nucleotideMutations ?: emptyList(), aminoAcidMutations ?: emptyList(), @@ -1334,13 +1357,13 @@ class LapisController( offset, ) - requestContext.filter = insertionRequest + requestContext.filter = request - val result = siloQueryModel.getAminoAcidInsertions(insertionRequest) + val result = siloQueryModel.getAminoAcidInsertions(request) return LapisResponse(result) } - @GetMapping("/aminoAcidInsertions", produces = [TEXT_CSV_HEADER]) + @GetMapping(AMINO_ACID_INSERTIONS_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = AMINO_ACID_INSERTIONS_ENDPOINT_DESCRIPTION, operationId = "getAminoAcidInsertionsAsCsv", @@ -1366,8 +1389,10 @@ class LapisController( @RequestParam aminoAcidMutations: List?, @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_INSERTIONS_SCHEMA")) nucleotideInsertions: List?, @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_INSERTIONS_SCHEMA")) aminoAcidInsertions: List?, @Parameter( schema = Schema(ref = "#/components/schemas/$LIMIT_SCHEMA"), @@ -1388,7 +1413,7 @@ class LapisController( @RequestParam dataFormat: String? = null, ): String { - val insertionRequest = InsertionsRequest( + val request = SequenceFiltersRequest( sequenceFilters?.filter { !SPECIAL_REQUEST_PROPERTIES.contains(it.key) } ?: emptyMap(), nucleotideMutations ?: emptyList(), aminoAcidMutations ?: emptyList(), @@ -1399,12 +1424,12 @@ class LapisController( offset, ) - requestContext.filter = insertionRequest + requestContext.filter = request - return getResponseAsCsv(insertionRequest, COMMA, siloQueryModel::getAminoAcidInsertions) + return getResponseAsCsv(request, COMMA, siloQueryModel::getAminoAcidInsertions) } - @GetMapping("/aminoAcidInsertions", produces = [TEXT_TSV_HEADER]) + @GetMapping(AMINO_ACID_INSERTIONS_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = AMINO_ACID_INSERTIONS_ENDPOINT_DESCRIPTION, operationId = "getAminoAcidInsertionsAsTsv", @@ -1430,8 +1455,10 @@ class LapisController( @RequestParam aminoAcidMutations: List?, @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_INSERTIONS_SCHEMA")) nucleotideInsertions: List?, @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_INSERTIONS_SCHEMA")) aminoAcidInsertions: List?, @Parameter( schema = Schema(ref = "#/components/schemas/$LIMIT_SCHEMA"), @@ -1452,7 +1479,7 @@ class LapisController( @RequestParam dataFormat: String? = null, ): String { - val insertionRequest = InsertionsRequest( + val request = SequenceFiltersRequest( sequenceFilters?.filter { !SPECIAL_REQUEST_PROPERTIES.contains(it.key) } ?: emptyMap(), nucleotideMutations ?: emptyList(), aminoAcidMutations ?: emptyList(), @@ -1463,12 +1490,12 @@ class LapisController( offset, ) - requestContext.filter = insertionRequest + requestContext.filter = request - return getResponseAsCsv(insertionRequest, TAB, siloQueryModel::getAminoAcidInsertions) + return getResponseAsCsv(request, TAB, siloQueryModel::getAminoAcidInsertions) } - @PostMapping("/aminoAcidInsertions", produces = [MediaType.APPLICATION_JSON_VALUE]) + @PostMapping(AMINO_ACID_INSERTIONS_ROUTE, produces = [MediaType.APPLICATION_JSON_VALUE]) @LapisAminoAcidInsertionsResponse @Operation( operationId = "postAminoAcidInsertions", @@ -1476,7 +1503,7 @@ class LapisController( fun postAminoAcidInsertions( @Parameter(schema = Schema(ref = "#/components/schemas/$INSERTIONS_REQUEST_SCHEMA")) @RequestBody - request: InsertionsRequest, + request: SequenceFiltersRequest, ): LapisResponse> { requestContext.filter = request @@ -1484,7 +1511,7 @@ class LapisController( return LapisResponse(result) } - @PostMapping("/aminoAcidInsertions", produces = [TEXT_CSV_HEADER]) + @PostMapping(AMINO_ACID_INSERTIONS_ROUTE, produces = [TEXT_CSV_HEADER]) @Operation( description = AMINO_ACID_INSERTIONS_ENDPOINT_DESCRIPTION, operationId = "postAminoAcidInsertionsAsCsv", @@ -1493,14 +1520,14 @@ class LapisController( fun postAminoAcidInsertionsAsCsv( @Parameter(schema = Schema(ref = "#/components/schemas/$INSERTIONS_REQUEST_SCHEMA")) @RequestBody - request: InsertionsRequest, + request: SequenceFiltersRequest, ): String { requestContext.filter = request return getResponseAsCsv(request, COMMA, siloQueryModel::getAminoAcidInsertions) } - @PostMapping("/aminoAcidInsertions", produces = [TEXT_TSV_HEADER]) + @PostMapping(AMINO_ACID_INSERTIONS_ROUTE, produces = [TEXT_TSV_HEADER]) @Operation( description = AMINO_ACID_INSERTIONS_ENDPOINT_DESCRIPTION, operationId = "postAminoAcidInsertionsAsTsv", @@ -1509,13 +1536,83 @@ class LapisController( fun postAminoAcidInsertionsAsTsv( @Parameter(schema = Schema(ref = "#/components/schemas/$INSERTIONS_REQUEST_SCHEMA")) @RequestBody - request: InsertionsRequest, + request: SequenceFiltersRequest, ): String { requestContext.filter = request return getResponseAsCsv(request, TAB, siloQueryModel::getAminoAcidInsertions) } + @GetMapping("$AMINO_ACID_SEQUENCES_ROUTE/{gene}", produces = ["text/x-fasta"]) + @LapisAminoAcidSequenceResponse + fun getAminoAcidSequence( + @PathVariable(name = "gene", required = true) gene: String, + @SequenceFilters + @RequestParam + sequenceFilters: Map?, + @Parameter( + schema = Schema(ref = "#/components/schemas/$ORDER_BY_FIELDS_SCHEMA"), + description = AGGREGATED_ORDER_BY_FIELDS_DESCRIPTION, + ) + @RequestParam + orderBy: List?, + @Parameter( + schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_MUTATIONS_SCHEMA"), + explode = Explode.TRUE, + ) + @RequestParam + nucleotideMutations: List?, + @Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_MUTATIONS_SCHEMA")) + @RequestParam + aminoAcidMutations: List?, + @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_INSERTIONS_SCHEMA")) + nucleotideInsertions: List?, + @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_INSERTIONS_SCHEMA")) + aminoAcidInsertions: List?, + @Parameter( + schema = Schema(ref = "#/components/schemas/$LIMIT_SCHEMA"), + description = LIMIT_DESCRIPTION, + ) + @RequestParam + limit: Int? = null, + @Parameter( + schema = Schema(ref = "#/components/schemas/$OFFSET_SCHEMA"), + description = OFFSET_DESCRIPTION, + ) + @RequestParam + offset: Int? = null, + ): String { + val request = SequenceFiltersRequest( + sequenceFilters?.filter { !SPECIAL_REQUEST_PROPERTIES.contains(it.key) } ?: emptyMap(), + nucleotideMutations ?: emptyList(), + aminoAcidMutations ?: emptyList(), + nucleotideInsertions ?: emptyList(), + aminoAcidInsertions ?: emptyList(), + orderBy ?: emptyList(), + limit, + offset, + ) + + requestContext.filter = request + + return siloQueryModel.getGenomicSequence(request, SequenceType.ALIGNED, gene) + } + + @PostMapping("$AMINO_ACID_SEQUENCES_ROUTE/{gene}", produces = ["text/x-fasta"]) + @LapisAminoAcidSequenceResponse + fun postAminoAcidSequence( + @PathVariable(name = "gene", required = true) gene: String, + @Parameter(schema = Schema(ref = "#/components/schemas/$SEQUENCE_REQUEST_SCHEMA")) + @RequestBody + request: SequenceFiltersRequest, + ): String { + requestContext.filter = request + + return siloQueryModel.getGenomicSequence(request, SequenceType.ALIGNED, gene) + } + private fun getResponseAsCsv( request: Request, delimiter: Delimiter, @@ -1612,6 +1709,17 @@ private annotation class LapisNucleotideInsertionsResponse ) private annotation class LapisAminoAcidInsertionsResponse +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@Operation( + description = AMINO_ACID_SEQUENCE_ENDPOINT_DESCRIPTION, +) +@ApiResponse( + responseCode = "200", + description = "OK", +) +private annotation class LapisAminoAcidSequenceResponse + @Target(AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) @Parameter( @@ -1620,4 +1728,4 @@ private annotation class LapisAminoAcidInsertionsResponse explode = Explode.TRUE, style = ParameterStyle.FORM, ) -private annotation class SequenceFilters +annotation class SequenceFilters diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/MultiSegmentedSequenceController.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/MultiSegmentedSequenceController.kt new file mode 100644 index 00000000..0465edcf --- /dev/null +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/MultiSegmentedSequenceController.kt @@ -0,0 +1,126 @@ +package org.genspectrum.lapis.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.enums.Explode +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import org.genspectrum.lapis.config.REFERENCE_GENOME_APPLICATION_ARG_PREFIX +import org.genspectrum.lapis.logging.RequestContext +import org.genspectrum.lapis.model.SiloQueryModel +import org.genspectrum.lapis.request.AminoAcidInsertion +import org.genspectrum.lapis.request.AminoAcidMutation +import org.genspectrum.lapis.request.NucleotideInsertion +import org.genspectrum.lapis.request.NucleotideMutation +import org.genspectrum.lapis.request.OrderByField +import org.genspectrum.lapis.request.SequenceFiltersRequest +import org.genspectrum.lapis.silo.SequenceType +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +private const val ALIGNED_MULTI_SEGMENTED_NUCLEOTIDE_SEQUENCE_ENDPOINT_DESCRIPTION = + "Returns a string of fasta formatted aligned nucleotide sequences of the requested segment. " + + "Only sequences matching the specified sequence filters are considered." + +const val isMultiSegmentSequenceExpression = "#{'\${$REFERENCE_GENOME_APPLICATION_ARG_PREFIX}'.split(',').length > 1}" + +@RestController +@ConditionalOnExpression(isMultiSegmentSequenceExpression) +class MultiSegmentedSequenceController( + private val siloQueryModel: SiloQueryModel, + private val requestContext: RequestContext, +) { + @GetMapping("$ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE/{segment}", produces = ["text/x-fasta"]) + @LapisAlignedMultiSegmentedNucleotideSequenceResponse + fun getAlignedNucleotideSequence( + @PathVariable(name = "segment", required = true) segment: String, + @SequenceFilters + @RequestParam + sequenceFilters: Map?, + @Parameter( + schema = Schema(ref = "#/components/schemas/$ORDER_BY_FIELDS_SCHEMA"), + description = AGGREGATED_ORDER_BY_FIELDS_DESCRIPTION, + ) + @RequestParam + orderBy: List?, + @Parameter( + schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_MUTATIONS_SCHEMA"), + explode = Explode.TRUE, + ) + @RequestParam + nucleotideMutations: List?, + @Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_MUTATIONS_SCHEMA")) + @RequestParam + aminoAcidMutations: List?, + @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_INSERTIONS_SCHEMA")) + nucleotideInsertions: List?, + @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_INSERTIONS_SCHEMA")) + aminoAcidInsertions: List?, + @Parameter( + schema = Schema(ref = "#/components/schemas/$LIMIT_SCHEMA"), + description = LIMIT_DESCRIPTION, + ) + @RequestParam + limit: Int? = null, + @Parameter( + schema = Schema(ref = "#/components/schemas/$OFFSET_SCHEMA"), + description = OFFSET_DESCRIPTION, + ) + @RequestParam + offset: Int? = null, + ): String { + val request = SequenceFiltersRequest( + sequenceFilters?.filter { !SPECIAL_REQUEST_PROPERTIES.contains(it.key) } ?: emptyMap(), + nucleotideMutations ?: emptyList(), + aminoAcidMutations ?: emptyList(), + nucleotideInsertions ?: emptyList(), + aminoAcidInsertions ?: emptyList(), + orderBy ?: emptyList(), + limit, + offset, + ) + + requestContext.filter = request + + return siloQueryModel.getGenomicSequence( + request, + SequenceType.ALIGNED, + segment, + ) + } + + @PostMapping("$ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE/{segment}", produces = ["text/x-fasta"]) + @LapisAlignedMultiSegmentedNucleotideSequenceResponse + fun postAlignedNucleotideSequence( + @PathVariable(name = "segment", required = true) segment: String, + @Parameter(schema = Schema(ref = "#/components/schemas/$SEQUENCE_REQUEST_SCHEMA")) + @RequestBody + request: SequenceFiltersRequest, + ): String { + requestContext.filter = request + + return siloQueryModel.getGenomicSequence( + request, + SequenceType.ALIGNED, + segment, + ) + } +} + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@Operation( + description = ALIGNED_MULTI_SEGMENTED_NUCLEOTIDE_SEQUENCE_ENDPOINT_DESCRIPTION, +) +@ApiResponse( + responseCode = "200", + description = "OK", +) +private annotation class LapisAlignedMultiSegmentedNucleotideSequenceResponse diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/SingleSegmentedSequenceController.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/SingleSegmentedSequenceController.kt new file mode 100644 index 00000000..4f6c75b4 --- /dev/null +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/controller/SingleSegmentedSequenceController.kt @@ -0,0 +1,126 @@ +package org.genspectrum.lapis.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.enums.Explode +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import org.genspectrum.lapis.config.REFERENCE_GENOME_APPLICATION_ARG_PREFIX +import org.genspectrum.lapis.config.ReferenceGenome +import org.genspectrum.lapis.logging.RequestContext +import org.genspectrum.lapis.model.SiloQueryModel +import org.genspectrum.lapis.request.AminoAcidInsertion +import org.genspectrum.lapis.request.AminoAcidMutation +import org.genspectrum.lapis.request.NucleotideInsertion +import org.genspectrum.lapis.request.NucleotideMutation +import org.genspectrum.lapis.request.OrderByField +import org.genspectrum.lapis.request.SequenceFiltersRequest +import org.genspectrum.lapis.silo.SequenceType +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +private const val ALIGNED_SINGLE_SEGMENTED_NUCLEOTIDE_SEQUENCE_ENDPOINT_DESCRIPTION = + "Returns a string of fasta formatted aligned nucleotide sequences. Only sequences matching the " + + "specified sequence filters are considered." + +const val isSingleSegmentSequenceExpression = "#{'\${$REFERENCE_GENOME_APPLICATION_ARG_PREFIX}'.split(',').length == 1}" + +@RestController +@ConditionalOnExpression(isSingleSegmentSequenceExpression) +class SingleSegmentedSequenceController( + private val siloQueryModel: SiloQueryModel, + private val requestContext: RequestContext, + private val referenceGenome: ReferenceGenome, +) { + + @GetMapping(ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE, produces = ["text/x-fasta"]) + @LapisAlignedSingleSegmentedNucleotideSequenceResponse + fun getAlignedNucleotideSequences( + @SequenceFilters + @RequestParam + sequenceFilters: Map?, + @Parameter( + schema = Schema(ref = "#/components/schemas/$ORDER_BY_FIELDS_SCHEMA"), + description = AGGREGATED_ORDER_BY_FIELDS_DESCRIPTION, + ) + @RequestParam + orderBy: List?, + @Parameter( + schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_MUTATIONS_SCHEMA"), + explode = Explode.TRUE, + ) + @RequestParam + nucleotideMutations: List?, + @Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_MUTATIONS_SCHEMA")) + @RequestParam + aminoAcidMutations: List?, + @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$NUCLEOTIDE_INSERTIONS_SCHEMA")) + nucleotideInsertions: List?, + @RequestParam + @Parameter(schema = Schema(ref = "#/components/schemas/$AMINO_ACID_INSERTIONS_SCHEMA")) + aminoAcidInsertions: List?, + @Parameter( + schema = Schema(ref = "#/components/schemas/$LIMIT_SCHEMA"), + description = LIMIT_DESCRIPTION, + ) + @RequestParam + limit: Int? = null, + @Parameter( + schema = Schema(ref = "#/components/schemas/$OFFSET_SCHEMA"), + description = OFFSET_DESCRIPTION, + ) + @RequestParam + offset: Int? = null, + ): String { + val request = SequenceFiltersRequest( + sequenceFilters?.filter { !SPECIAL_REQUEST_PROPERTIES.contains(it.key) } ?: emptyMap(), + nucleotideMutations ?: emptyList(), + aminoAcidMutations ?: emptyList(), + nucleotideInsertions ?: emptyList(), + aminoAcidInsertions ?: emptyList(), + orderBy ?: emptyList(), + limit, + offset, + ) + + requestContext.filter = request + + return siloQueryModel.getGenomicSequence( + request, + SequenceType.ALIGNED, + referenceGenome.nucleotideSequences[0].name, + ) + } + + @PostMapping(ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE, produces = ["text/x-fasta"]) + @LapisAlignedSingleSegmentedNucleotideSequenceResponse + fun postAlignedNucleotideSequence( + @Parameter(schema = Schema(ref = "#/components/schemas/$SEQUENCE_REQUEST_SCHEMA")) + @RequestBody + request: SequenceFiltersRequest, + ): String { + requestContext.filter = request + + return siloQueryModel.getGenomicSequence( + request, + SequenceType.ALIGNED, + referenceGenome.nucleotideSequences[0].name, + ) + } +} + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@Operation( + description = ALIGNED_SINGLE_SEGMENTED_NUCLEOTIDE_SEQUENCE_ENDPOINT_DESCRIPTION, +) +@ApiResponse( + responseCode = "200", + description = "OK", +) +private annotation class LapisAlignedSingleSegmentedNucleotideSequenceResponse diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt index 1abb1249..15f08881 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt @@ -1,14 +1,15 @@ package org.genspectrum.lapis.model -import org.genspectrum.lapis.config.SingleSegmentedSequenceFeature -import org.genspectrum.lapis.request.InsertionsRequest +import org.genspectrum.lapis.config.ReferenceGenome import org.genspectrum.lapis.request.MutationProportionsRequest +import org.genspectrum.lapis.request.SequenceFiltersRequest import org.genspectrum.lapis.request.SequenceFiltersRequestWithFields import org.genspectrum.lapis.response.AminoAcidInsertionResponse import org.genspectrum.lapis.response.AminoAcidMutationResponse import org.genspectrum.lapis.response.DetailsData import org.genspectrum.lapis.response.NucleotideInsertionResponse import org.genspectrum.lapis.response.NucleotideMutationResponse +import org.genspectrum.lapis.silo.SequenceType import org.genspectrum.lapis.silo.SiloAction import org.genspectrum.lapis.silo.SiloClient import org.genspectrum.lapis.silo.SiloQuery @@ -18,7 +19,7 @@ import org.springframework.stereotype.Component class SiloQueryModel( private val siloClient: SiloClient, private val siloFilterExpressionMapper: SiloFilterExpressionMapper, - private val singleSegmentedSequenceFeature: SingleSegmentedSequenceFeature, + private val referenceGenome: ReferenceGenome, ) { fun getAggregated(sequenceFilters: SequenceFiltersRequestWithFields) = siloClient.sendQuery( @@ -49,7 +50,7 @@ class SiloQueryModel( ) return data.map { it -> val sequenceName = - if (singleSegmentedSequenceFeature.isEnabled()) it.mutation else "${it.sequenceName}:${it.mutation}" + if (referenceGenome.isSingleSegmented()) it.mutation else "${it.sequenceName}:${it.mutation}" NucleotideMutationResponse( sequenceName, @@ -95,7 +96,7 @@ class SiloQueryModel( ), ) - fun getNucleotideInsertions(sequenceFilters: InsertionsRequest): List { + fun getNucleotideInsertions(sequenceFilters: SequenceFiltersRequest): List { val data = siloClient.sendQuery( SiloQuery( SiloAction.nucleotideInsertions( @@ -108,7 +109,7 @@ class SiloQueryModel( ) return data.map { it -> - val sequenceName = if (singleSegmentedSequenceFeature.isEnabled()) "" else "${it.sequenceName}:" + val sequenceName = if (referenceGenome.isSingleSegmented()) "" else "${it.sequenceName}:" NucleotideInsertionResponse( "ins_${sequenceName}${it.position}:${it.insertions}", @@ -117,7 +118,7 @@ class SiloQueryModel( } } - fun getAminoAcidInsertions(sequenceFilters: InsertionsRequest): List { + fun getAminoAcidInsertions(sequenceFilters: SequenceFiltersRequest): List { val data = siloClient.sendQuery( SiloQuery( SiloAction.aminoAcidInsertions( @@ -136,4 +137,23 @@ class SiloQueryModel( ) } } + + fun getGenomicSequence( + sequenceFilters: SequenceFiltersRequest, + sequenceType: SequenceType, + sequenceName: String, + ): String { + return siloClient.sendQuery( + SiloQuery( + SiloAction.genomicSequence( + sequenceType, + sequenceName, + sequenceFilters.orderByFields, + sequenceFilters.limit, + sequenceFilters.offset, + ), + siloFilterExpressionMapper.map(sequenceFilters), + ), + ).joinToString("\n") { ">${it.sequenceKey}\n${it.sequence}" } + } } diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/request/InsertionsRequest.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequest.kt similarity index 87% rename from lapis2/src/main/kotlin/org/genspectrum/lapis/request/InsertionsRequest.kt rename to lapis2/src/main/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequest.kt index 4c28389e..5ee5e336 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/request/InsertionsRequest.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequest.kt @@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonNode import org.springframework.boot.jackson.JsonComponent -data class InsertionsRequest( +data class SequenceFiltersRequest( override val sequenceFilters: Map, override val nucleotideMutations: List, override val aaMutations: List, @@ -18,14 +18,14 @@ data class InsertionsRequest( ) : CommonSequenceFilters @JsonComponent -class InsertionRequestDeserializer : JsonDeserializer() { - override fun deserialize(jsonParser: JsonParser, ctxt: DeserializationContext): InsertionsRequest { +class SequenceFiltersRequestDeserializer : JsonDeserializer() { + override fun deserialize(jsonParser: JsonParser, ctxt: DeserializationContext): SequenceFiltersRequest { val node = jsonParser.readValueAsTree() val codec = jsonParser.codec val parsedCommonFields = parseCommonFields(node, codec) - return InsertionsRequest( + return SequenceFiltersRequest( parsedCommonFields.sequenceFilters, parsedCommonFields.nucleotideMutations, parsedCommonFields.aminoAcidMutations, diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/response/SiloResponse.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/response/SiloResponse.kt index 86d08f30..d0edb1ab 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/response/SiloResponse.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/response/SiloResponse.kt @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.SerializerProvider import io.swagger.v3.oas.annotations.media.Schema +import org.genspectrum.lapis.config.DatabaseConfig import org.genspectrum.lapis.controller.CsvRecord import org.springframework.boot.jackson.JsonComponent @@ -64,3 +65,19 @@ data class InsertionData( val position: Int, val sequenceName: String, ) + +data class SequenceData( + val sequenceKey: String, + val sequence: String, +) + +@JsonComponent +class SequenceDataDeserializer(val databaseConfig: DatabaseConfig) : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): SequenceData { + val node = p.readValueAsTree() + val sequenceKey = node.get(databaseConfig.schema.primaryKey).asText() + val sequence = + node.fields().asSequence().first { it.key != databaseConfig.schema.primaryKey }.value.asText() + return SequenceData(sequenceKey, sequence) + } +} diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/silo/SiloQuery.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/silo/SiloQuery.kt index 7365f285..ad497938 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/silo/SiloQuery.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/silo/SiloQuery.kt @@ -2,12 +2,14 @@ package org.genspectrum.lapis.silo import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.core.type.TypeReference import org.genspectrum.lapis.request.OrderByField import org.genspectrum.lapis.response.AggregationData import org.genspectrum.lapis.response.DetailsData import org.genspectrum.lapis.response.InsertionData import org.genspectrum.lapis.response.MutationData +import org.genspectrum.lapis.response.SequenceData import java.time.LocalDate data class SiloQuery(val action: SiloAction, val filterExpression: SiloFilterExpression) @@ -17,6 +19,7 @@ class MutationDataTypeReference : TypeReference>>() class DetailsDataTypeReference : TypeReference>>() class InsertionDataTypeReference : TypeReference>>() +class SequenceDataTypeReference : TypeReference>>() interface CommonActionFields { val orderByFields: List @@ -78,6 +81,14 @@ sealed class SiloAction( limit: Int? = null, offset: Int? = null, ): SiloAction> = AminoAcidInsertionsAction(orderByFields, limit, offset) + + fun genomicSequence( + type: SequenceType, + sequenceName: String, + orderByFields: List = emptyList(), + limit: Int? = null, + offset: Int? = null, + ): SiloAction> = SequenceAction(orderByFields, limit, offset, type, sequenceName) } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -131,6 +142,15 @@ sealed class SiloAction( override val offset: Int? = null, val type: String = "AminoAcidInsertions", ) : SiloAction>(InsertionDataTypeReference()) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private data class SequenceAction( + override val orderByFields: List = emptyList(), + override val limit: Int? = null, + override val offset: Int? = null, + val type: SequenceType, + val sequenceName: String, + ) : SiloAction>(SequenceDataTypeReference()) } sealed class SiloFilterExpression(val type: String) @@ -184,3 +204,11 @@ data class IntBetween(val column: String, val from: Int?, val to: Int?) : SiloFi data class FloatEquals(val column: String, val value: Double) : SiloFilterExpression("FloatEquals") data class FloatBetween(val column: String, val from: Double?, val to: Double?) : SiloFilterExpression("FloatBetween") + +enum class SequenceType { + @JsonProperty("Fasta") + UNALIGNED, + + @JsonProperty("FastaAligned") + ALIGNED, +} diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/auth/ProtectedDataAuthorizationTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/auth/ProtectedDataAuthorizationTest.kt index 97941942..a560a53e 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/auth/ProtectedDataAuthorizationTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/auth/ProtectedDataAuthorizationTest.kt @@ -4,6 +4,7 @@ import com.ninjasquad.springmockk.MockkBean import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.verify +import org.genspectrum.lapis.controller.AGGREGATED_ROUTE import org.genspectrum.lapis.model.SiloQueryModel import org.genspectrum.lapis.request.SequenceFiltersRequestWithFields import org.junit.jupiter.api.BeforeEach @@ -34,7 +35,7 @@ class ProtectedDataAuthorizationTest(@Autowired val mockMvc: MockMvc) { @MockkBean lateinit var siloQueryModelMock: SiloQueryModel - private val validRoute = "/aggregated" + private val validRoute = AGGREGATED_ROUTE @BeforeEach fun setUp() { diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/config/DatabaseConfigTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/config/DatabaseConfigTest.kt index 24c5baeb..e0e17ef7 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/config/DatabaseConfigTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/config/DatabaseConfigTest.kt @@ -34,7 +34,6 @@ class DatabaseConfigTest { underTest.schema.features, containsInAnyOrder( DatabaseFeature(name = SARS_COV2_VARIANT_QUERY_FEATURE), - DatabaseFeature(name = IS_SEQUENCE_SINGLE_SEGMENTED_FEATURE), ), ) } diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/config/ReferenceGenomeTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/config/ReferenceGenomeTest.kt new file mode 100644 index 00000000..7f3cd88d --- /dev/null +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/config/ReferenceGenomeTest.kt @@ -0,0 +1,54 @@ +package org.genspectrum.lapis.config + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.lang.IllegalArgumentException + +private const val REFERENCE_GENOME_DEFAULT_FILENAME = "src/test/resources/config/reference-genomes.json" + +class ReferenceGenomeTest { + @Test + fun `should read from file`() { + val referenceGenome = ReferenceGenome.readFromFile(REFERENCE_GENOME_DEFAULT_FILENAME) + assertThat(referenceGenome.nucleotideSequences.size, equalTo(1)) + assertThat(referenceGenome.nucleotideSequences[0].name, equalTo("main")) + } + + @Test + fun `should read from file through program args`() { + val args = arrayOf("--referenceGenomeFilename=$REFERENCE_GENOME_DEFAULT_FILENAME") + val referenceGenome = ReferenceGenome.readFromFileFromProgramArgs(args) + assertThat(referenceGenome.nucleotideSequences.size, equalTo(1)) + assertThat(referenceGenome.nucleotideSequences[0].name, equalTo("main")) + } + + @Test + fun `should throw if no reference genome filename is given in the args`() { + val args = emptyArray() + assertThrows { ReferenceGenome.readFromFileFromProgramArgs(args) } + } + + @Test + fun `should generate spring application arguments`() { + val referenceGenome = ReferenceGenome( + listOf(NucleotideSequence("main"), NucleotideSequence("other_segment")), + ) + val springArgs = referenceGenome.toSpringApplicationArgs() + assertThat(springArgs[0], equalTo("--$REFERENCE_GENOME_APPLICATION_ARG_PREFIX=main,other_segment")) + } + + @Test + fun `should detect single segmented sequence`() { + val singleSegmented = ReferenceGenome( + listOf(NucleotideSequence("main")), + ) + assertThat(singleSegmented.isSingleSegmented(), equalTo(true)) + + val multiSegmented = ReferenceGenome( + listOf(NucleotideSequence("main"), NucleotideSequence("other_segment")), + ) + assertThat(multiSegmented.isSingleSegmented(), equalTo(false)) + } +} diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/config/SingleSegmentedSequenceFeatureTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/config/SingleSegmentedSequenceFeatureTest.kt deleted file mode 100644 index c7448293..00000000 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/config/SingleSegmentedSequenceFeatureTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.genspectrum.lapis.config - -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class SingleSegmentedSequenceFeatureTest { - @Test - fun `given a databaseConfig with a feature named isSingleSegmentedSequence then isEnabled returns true`() { - val input = databaseConfigWithFeatures(listOf(DatabaseFeature(IS_SEQUENCE_SINGLE_SEGMENTED_FEATURE))) - - val underTest = SingleSegmentedSequenceFeature(input) - - assertTrue(underTest.isEnabled()) - } - - @Test - fun `given a databaseConfig without a feature named isSingleSegmentedSequence then isEnabled returns false`() { - val input = databaseConfigWithFeatures(listOf(DatabaseFeature("notTheRightFeature"))) - - val underTest = SingleSegmentedSequenceFeature(input) - - assertFalse(underTest.isEnabled()) - } - - private fun databaseConfigWithFeatures( - databaseFeatures: List = emptyList(), - ) = DatabaseConfig( - DatabaseSchema( - "test config", - OpennessLevel.OPEN, - emptyList(), - "test primary key", - databaseFeatures, - ), - ) -} diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCommonFieldsTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCommonFieldsTest.kt index 58bf7ee0..d0bf0938 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCommonFieldsTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCommonFieldsTest.kt @@ -27,7 +27,9 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @SpringBootTest @AutoConfigureMockMvc -class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { +class LapisControllerCommonFieldsTest( + @Autowired val mockMvc: MockMvc, +) { @MockkBean lateinit var siloQueryModelMock: SiloQueryModel @@ -47,7 +49,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(AggregationData(0, mapOf("country" to TextNode("Switzerland")))) - mockMvc.perform(get("/aggregated?orderBy=country")) + mockMvc.perform(get("$AGGREGATED_ROUTE?orderBy=country")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(0)) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) @@ -69,7 +71,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(AggregationData(0, mapOf("country" to TextNode("Switzerland")))) - mockMvc.perform(get("/aggregated?orderBy=country,date")) + mockMvc.perform(get("$AGGREGATED_ROUTE?orderBy=country,date")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(0)) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) @@ -91,7 +93,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(AggregationData(0, mapOf("country" to TextNode("Switzerland")))) - val request = post("/aggregated") + val request = post(AGGREGATED_ROUTE) .content("""{"orderBy": ["country", "date"]}""") .contentType(MediaType.APPLICATION_JSON) @@ -120,7 +122,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(AggregationData(0, mapOf("country" to TextNode("Switzerland")))) - val request = post("/aggregated") + val request = post(AGGREGATED_ROUTE) .content( """ { @@ -141,7 +143,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { @Test fun `POST aggregated with invalid orderBy fields`() { - val request = post("/aggregated") + val request = post("$AGGREGATED_ROUTE") .content("""{"orderBy": [ { "field": ["this is an array, not a string"] } ]}""") .contentType(MediaType.APPLICATION_JSON) @@ -167,7 +169,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(AggregationData(0, mapOf("country" to TextNode("Switzerland")))) - mockMvc.perform(get("/aggregated?limit=100")) + mockMvc.perform(get("$AGGREGATED_ROUTE?limit=100")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(0)) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) @@ -190,7 +192,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(AggregationData(0, mapOf("country" to TextNode("Switzerland")))) - val request = post("/aggregated") + val request = post(AGGREGATED_ROUTE) .content("""{"limit": 100}""") .contentType(MediaType.APPLICATION_JSON) @@ -201,7 +203,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { @Test fun `POST aggregated with invalid limit`() { - val request = post("/aggregated") + val request = post(AGGREGATED_ROUTE) .content("""{"limit": "this is not a number"}""") .contentType(MediaType.APPLICATION_JSON) @@ -228,7 +230,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(AggregationData(0, mapOf("country" to TextNode("Switzerland")))) - mockMvc.perform(get("/aggregated?offset=5")) + mockMvc.perform(get("$AGGREGATED_ROUTE?offset=5")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(0)) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) @@ -252,7 +254,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(AggregationData(0, mapOf("country" to TextNode("Switzerland")))) - val request = post("/aggregated") + val request = post(AGGREGATED_ROUTE) .content("""{"offset": 5}""") .contentType(MediaType.APPLICATION_JSON) @@ -263,7 +265,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { @Test fun `POST aggregated with invalid offset`() { - val request = post("/aggregated") + val request = post(AGGREGATED_ROUTE) .content("""{"offset": "this is not a number"}""") .contentType(MediaType.APPLICATION_JSON) @@ -287,7 +289,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(AggregationData(5, emptyMap())) - mockMvc.perform(get("/aggregated?nucleotideInsertions=ins_123:ABC,ins_segment:124:DEF")) + mockMvc.perform(get("$AGGREGATED_ROUTE?nucleotideInsertions=ins_123:ABC,ins_segment:124:DEF")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(5)) } @@ -307,7 +309,7 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(AggregationData(5, emptyMap())) - mockMvc.perform(get("/aggregated?aminoAcidInsertions=ins_S:123:ABC,ins_ORF1:124:DEF")) + mockMvc.perform(get("$AGGREGATED_ROUTE?aminoAcidInsertions=ins_S:123:ABC,ins_ORF1:124:DEF")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(5)) } @@ -351,20 +353,23 @@ class LapisControllerCommonFieldsTest(@Autowired val mockMvc: MockMvc) { } private companion object { - fun allEndpoints() = listOf( - Arguments.of("/nucleotideMutations"), - Arguments.of("/aminoAcidMutations"), - Arguments.of("/aggregated"), - Arguments.of("/details"), + fun endpointsOfController() = listOf( + Arguments.of(NUCLEOTIDE_MUTATIONS_ROUTE), + Arguments.of(AMINO_ACID_MUTATIONS_ROUTE), + Arguments.of(AGGREGATED_ROUTE), + Arguments.of(DETAILS_ROUTE), + Arguments.of(NUCLEOTIDE_INSERTIONS_ROUTE), + Arguments.of(AMINO_ACID_INSERTIONS_ROUTE), + Arguments.of("$AMINO_ACID_SEQUENCES_ROUTE/S"), ) @JvmStatic - fun getEndpointsWithInsertionFilter() = allEndpoints() + fun getEndpointsWithInsertionFilter() = endpointsOfController() @JvmStatic - fun getEndpointsWithNucleotideMutationFilter() = allEndpoints() + fun getEndpointsWithNucleotideMutationFilter() = endpointsOfController() @JvmStatic - fun getEndpointsWithAminoAcidMutationFilter() = allEndpoints() + fun getEndpointsWithAminoAcidMutationFilter() = endpointsOfController() } } diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCsvTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCsvTest.kt index 051d23b1..09026c63 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCsvTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCsvTest.kt @@ -221,34 +221,34 @@ class LapisControllerCsvTest(@Autowired val mockMvc: MockMvc) { fun mockEndpointReturnEmptyList(endpoint: String) = when (endpoint) { - "/details" -> + DETAILS_ROUTE -> every { siloQueryModelMock.getDetails(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland"))) } returns emptyList() - "/aggregated" -> + AGGREGATED_ROUTE -> every { siloQueryModelMock.getAggregated( sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland")), ) } returns emptyList() - "/nucleotideMutations" -> + NUCLEOTIDE_MUTATIONS_ROUTE -> every { siloQueryModelMock.computeNucleotideMutationProportions(any()) } returns emptyList() - "/aminoAcidMutations" -> + AMINO_ACID_MUTATIONS_ROUTE -> every { siloQueryModelMock.computeAminoAcidMutationProportions(any()) } returns emptyList() - "/nucleotideInsertions" -> + NUCLEOTIDE_INSERTIONS_ROUTE -> every { siloQueryModelMock.getNucleotideInsertions(any()) } returns emptyList() - "/aminoAcidInsertions" -> + AMINO_ACID_INSERTIONS_ROUTE -> every { siloQueryModelMock.getAminoAcidInsertions(any()) } returns emptyList() @@ -257,32 +257,32 @@ class LapisControllerCsvTest(@Autowired val mockMvc: MockMvc) { } fun mockEndpointReturnData(endpoint: String) = when (endpoint) { - "/details" -> + DETAILS_ROUTE -> every { siloQueryModelMock.getDetails(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland"))) } returns detailsData - "/aggregated" -> + AGGREGATED_ROUTE -> every { siloQueryModelMock.getAggregated(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland"))) } returns aggregationData - "/nucleotideMutations" -> + NUCLEOTIDE_MUTATIONS_ROUTE -> every { siloQueryModelMock.computeNucleotideMutationProportions(any()) } returns nucleotideMutationData - "/aminoAcidMutations" -> + AMINO_ACID_MUTATIONS_ROUTE -> every { siloQueryModelMock.computeAminoAcidMutationProportions(any()) } returns aminoAcidMutationData - "/nucleotideInsertions" -> + NUCLEOTIDE_INSERTIONS_ROUTE -> every { siloQueryModelMock.getNucleotideInsertions(any()) } returns nucleotideInsertionData - "/aminoAcidInsertions" -> + AMINO_ACID_INSERTIONS_ROUTE -> every { siloQueryModelMock.getAminoAcidInsertions(any()) } returns aminoAcidInsertionData @@ -291,22 +291,22 @@ class LapisControllerCsvTest(@Autowired val mockMvc: MockMvc) { } fun returnedCsvData(endpoint: String) = when (endpoint) { - "/details" -> detailsDataCsv - "/aggregated" -> aggregationDataCsv - "/nucleotideMutations" -> mutationDataCsv - "/aminoAcidMutations" -> mutationDataCsv - "/nucleotideInsertions" -> nucleotideInsertionDataCsv - "/aminoAcidInsertions" -> aminoAcidInsertionDataCsv + DETAILS_ROUTE -> detailsDataCsv + AGGREGATED_ROUTE -> aggregationDataCsv + NUCLEOTIDE_MUTATIONS_ROUTE -> mutationDataCsv + AMINO_ACID_MUTATIONS_ROUTE -> mutationDataCsv + NUCLEOTIDE_INSERTIONS_ROUTE -> nucleotideInsertionDataCsv + AMINO_ACID_INSERTIONS_ROUTE -> aminoAcidInsertionDataCsv else -> throw IllegalArgumentException("Unknown endpoint: $endpoint") } fun returnedTsvData(endpoint: String) = when (endpoint) { - "/details" -> detailsDataTsv - "/aggregated" -> aggregationDataTsv - "/nucleotideMutations" -> mutationDataTsv - "/aminoAcidMutations" -> mutationDataTsv - "/nucleotideInsertions" -> nucleotideInsertionDataTsv - "/aminoAcidInsertions" -> aminoAcidInsertionDataTsv + DETAILS_ROUTE -> detailsDataTsv + AGGREGATED_ROUTE -> aggregationDataTsv + NUCLEOTIDE_MUTATIONS_ROUTE -> mutationDataTsv + AMINO_ACID_MUTATIONS_ROUTE -> mutationDataTsv + NUCLEOTIDE_INSERTIONS_ROUTE -> nucleotideInsertionDataTsv + AMINO_ACID_INSERTIONS_ROUTE -> aminoAcidInsertionDataTsv else -> throw IllegalArgumentException("Unknown endpoint: $endpoint") } @@ -419,12 +419,12 @@ class LapisControllerCsvTest(@Autowired val mockMvc: MockMvc) { private companion object { @JvmStatic fun getEndpoints() = listOf( - Arguments.of("/details"), - Arguments.of("/aggregated"), - Arguments.of("/nucleotideMutations"), - Arguments.of("/aminoAcidMutations"), - Arguments.of("/nucleotideInsertions"), - Arguments.of("/aminoAcidInsertions"), + Arguments.of(DETAILS_ROUTE), + Arguments.of(AGGREGATED_ROUTE), + Arguments.of(NUCLEOTIDE_MUTATIONS_ROUTE), + Arguments.of(AMINO_ACID_MUTATIONS_ROUTE), + Arguments.of(NUCLEOTIDE_INSERTIONS_ROUTE), + Arguments.of(AMINO_ACID_INSERTIONS_ROUTE), ) } diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerTest.kt index 98c4afec..e65fd7a2 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerTest.kt @@ -6,9 +6,9 @@ import com.ninjasquad.springmockk.MockkBean import io.mockk.every import org.genspectrum.lapis.model.SiloQueryModel import org.genspectrum.lapis.request.DataVersion -import org.genspectrum.lapis.request.InsertionsRequest import org.genspectrum.lapis.request.MutationProportionsRequest import org.genspectrum.lapis.request.NucleotideMutation +import org.genspectrum.lapis.request.SequenceFiltersRequest import org.genspectrum.lapis.request.SequenceFiltersRequestWithFields import org.genspectrum.lapis.response.AggregationData import org.genspectrum.lapis.response.AminoAcidInsertionResponse @@ -59,7 +59,7 @@ class LapisControllerTest(@Autowired val mockMvc: MockMvc) { ), ) - mockMvc.perform(get("/aggregated?country=Switzerland")) + mockMvc.perform(get("$AGGREGATED_ROUTE?country=Switzerland")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(0)) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) @@ -103,7 +103,7 @@ class LapisControllerTest(@Autowired val mockMvc: MockMvc) { ), ) - mockMvc.perform(get("/aggregated?country=Switzerland&fields=country,age")) + mockMvc.perform(get("$AGGREGATED_ROUTE?country=Switzerland&fields=country,age")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(0)) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) @@ -125,7 +125,7 @@ class LapisControllerTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(AggregationData(5, emptyMap())) - mockMvc.perform(get("/aggregated?nucleotideMutations=123A,124B")) + mockMvc.perform(get("$AGGREGATED_ROUTE?nucleotideMutations=123A,124B")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].count").value(5)) } @@ -146,7 +146,7 @@ class LapisControllerTest(@Autowired val mockMvc: MockMvc) { ), ) - val request = post("/aggregated") + val request = post(AGGREGATED_ROUTE) .content("""{"country": "Switzerland", "fields": ["country","age"]}""") .contentType(MediaType.APPLICATION_JSON) @@ -299,15 +299,17 @@ class LapisControllerTest(@Autowired val mockMvc: MockMvc) { private fun setupInsertionMock(endpoint: String) { when (endpoint) { - "/nucleotideInsertions" -> { + NUCLEOTIDE_INSERTIONS_ROUTE -> { every { - siloQueryModelMock.getNucleotideInsertions(insertionRequest(mapOf("country" to "Switzerland"))) + siloQueryModelMock.getNucleotideInsertions( + sequenceFiltersRequest(mapOf("country" to "Switzerland")), + ) } returns listOf(someNucleotideInsertion()) } - "/aminoAcidInsertions" -> { + AMINO_ACID_INSERTIONS_ROUTE -> { every { - siloQueryModelMock.getAminoAcidInsertions(insertionRequest(mapOf("country" to "Switzerland"))) + siloQueryModelMock.getAminoAcidInsertions(sequenceFiltersRequest(mapOf("country" to "Switzerland"))) } returns listOf(someAminoAcidInsertion()) } @@ -318,14 +320,14 @@ class LapisControllerTest(@Autowired val mockMvc: MockMvc) { private companion object { @JvmStatic fun getMutationEndpoints() = listOf( - Arguments.of("/nucleotideMutations"), - Arguments.of("/aminoAcidMutations"), + Arguments.of(NUCLEOTIDE_MUTATIONS_ROUTE), + Arguments.of(AMINO_ACID_MUTATIONS_ROUTE), ) @JvmStatic fun getInsertionEndpoints() = listOf( - Arguments.of("/nucleotideInsertions"), - Arguments.of("/aminoAcidInsertions"), + Arguments.of(NUCLEOTIDE_INSERTIONS_ROUTE), + Arguments.of(AMINO_ACID_INSERTIONS_ROUTE), ) } @@ -335,7 +337,7 @@ class LapisControllerTest(@Autowired val mockMvc: MockMvc) { siloQueryModelMock.getDetails(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland"))) } returns listOf(DetailsData(mapOf("country" to TextNode("Switzerland"), "age" to IntNode(42)))) - mockMvc.perform(get("/details?country=Switzerland")) + mockMvc.perform(get("$DETAILS_ROUTE?country=Switzerland")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) .andExpect(jsonPath("\$.data[0].age").value(42)) @@ -353,7 +355,7 @@ class LapisControllerTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(DetailsData(mapOf("country" to TextNode("Switzerland"), "age" to IntNode(42)))) - mockMvc.perform(get("/details?country=Switzerland&fields=country&fields=age")) + mockMvc.perform(get("$DETAILS_ROUTE?country=Switzerland&fields=country&fields=age")) .andExpect(status().isOk) .andExpect(jsonPath("\$.data[0].country").value("Switzerland")) .andExpect(jsonPath("\$.data[0].age").value(42)) @@ -365,7 +367,7 @@ class LapisControllerTest(@Autowired val mockMvc: MockMvc) { siloQueryModelMock.getDetails(sequenceFiltersRequestWithFields(mapOf("country" to "Switzerland"))) } returns listOf(DetailsData(mapOf("country" to TextNode("Switzerland"), "age" to IntNode(42)))) - val request = post("/details") + val request = post(DETAILS_ROUTE) .content("""{"country": "Switzerland"}""") .contentType(MediaType.APPLICATION_JSON) @@ -387,7 +389,7 @@ class LapisControllerTest(@Autowired val mockMvc: MockMvc) { ) } returns listOf(DetailsData(mapOf("country" to TextNode("Switzerland"), "age" to IntNode(42)))) - val request = post("/details") + val request = post(DETAILS_ROUTE) .content("""{"country": "Switzerland", "fields": ["country", "age"]}""") .contentType(MediaType.APPLICATION_JSON) @@ -410,9 +412,9 @@ class LapisControllerTest(@Autowired val mockMvc: MockMvc) { emptyList(), ) - private fun insertionRequest( + private fun sequenceFiltersRequest( sequenceFilters: Map, - ) = InsertionsRequest( + ) = SequenceFiltersRequest( sequenceFilters, emptyList(), emptyList(), diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/MultiSegmentedSequenceControllerTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/MultiSegmentedSequenceControllerTest.kt new file mode 100644 index 00000000..d4255003 --- /dev/null +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/MultiSegmentedSequenceControllerTest.kt @@ -0,0 +1,161 @@ +package org.genspectrum.lapis.controller + +import com.ninjasquad.springmockk.MockkBean +import io.mockk.every +import org.genspectrum.lapis.config.REFERENCE_GENOME_APPLICATION_ARG_PREFIX +import org.genspectrum.lapis.model.SiloQueryModel +import org.genspectrum.lapis.request.DataVersion +import org.genspectrum.lapis.request.SequenceFiltersRequest +import org.genspectrum.lapis.silo.SequenceType +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@SpringBootTest( + properties = ["$REFERENCE_GENOME_APPLICATION_ARG_PREFIX=someSegment,otherSegment"], +) +@AutoConfigureMockMvc +class MultiSegmentedSequenceControllerTest(@Autowired val mockMvc: MockMvc) { + @MockkBean + lateinit var siloQueryModelMock: SiloQueryModel + + @MockkBean + lateinit var dataVersion: DataVersion + + @BeforeEach + fun setup() { + every { + dataVersion.dataVersion + } returns "1234" + } + + @Test + fun `should GET alignedNucleotideSequences with empty filter`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(emptyMap()), + SequenceType.ALIGNED, + "otherSegment", + ) + } returns returnedValue + + mockMvc.perform(get("$ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE/otherSegment")) + .andExpect(status().isOk) + .andExpect(content().string(returnedValue)) + .andExpect(header().stringValues("Lapis-Data-Version", "1234")) + } + + @Test + fun `should GET alignedNucleotideSequences with filter`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(mapOf("country" to "Switzerland")), + SequenceType.ALIGNED, + "otherSegment", + ) + } returns returnedValue + + mockMvc.perform(get("$ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE/otherSegment?country=Switzerland")) + .andExpect(status().isOk) + .andExpect(content().string(returnedValue)) + .andExpect(header().stringValues("Lapis-Data-Version", "1234")) + } + + @Test + fun `should POST alignedNucleotideSequences with empty filter`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(emptyMap()), + SequenceType.ALIGNED, + "otherSegment", + ) + } returns returnedValue + + val request = post("$ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE/otherSegment") + .content("""{}""") + .contentType(MediaType.APPLICATION_JSON) + + mockMvc.perform(request) + .andExpect(status().isOk) + .andExpect(content().string(returnedValue)) + .andExpect(header().stringValues("Lapis-Data-Version", "1234")) + } + + @Test + fun `should POST alignedNucleotideSequences with filter`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(mapOf("country" to "Switzerland")), + SequenceType.ALIGNED, + "otherSegment", + ) + } returns returnedValue + + val request = post("$ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE/otherSegment") + .content("""{"country":"Switzerland"}""") + .contentType(MediaType.APPLICATION_JSON) + + mockMvc.perform(request) + .andExpect(status().isOk) + .andExpect(content().string(returnedValue)) + .andExpect(header().stringValues("Lapis-Data-Version", "1234")) + } + + @Test + fun `should not GET alignedNucleotideSequence without segment`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(emptyMap()), + SequenceType.ALIGNED, + "someSegment", + ) + } returns returnedValue + + mockMvc.perform(get(ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE)) + .andExpect(status().isNotFound) + } + + @Test + fun `should not POST alignedNucleotideSequences without segment`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(emptyMap()), + SequenceType.ALIGNED, + "otherSegment", + ) + } returns returnedValue + + val request = post(ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE) + .content("""{}""") + .contentType(MediaType.APPLICATION_JSON) + + mockMvc.perform(request) + .andExpect(status().isNotFound) + } + + private fun sequenceFiltersRequest( + sequenceFilters: Map, + ) = SequenceFiltersRequest( + sequenceFilters, + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + ) +} diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/SingleSegmentedSequenceControllerTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/SingleSegmentedSequenceControllerTest.kt new file mode 100644 index 00000000..93c66dde --- /dev/null +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/controller/SingleSegmentedSequenceControllerTest.kt @@ -0,0 +1,161 @@ +package org.genspectrum.lapis.controller + +import com.ninjasquad.springmockk.MockkBean +import io.mockk.every +import org.genspectrum.lapis.config.REFERENCE_GENOME_APPLICATION_ARG_PREFIX +import org.genspectrum.lapis.model.SiloQueryModel +import org.genspectrum.lapis.request.DataVersion +import org.genspectrum.lapis.request.SequenceFiltersRequest +import org.genspectrum.lapis.silo.SequenceType +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@SpringBootTest( + properties = ["$REFERENCE_GENOME_APPLICATION_ARG_PREFIX=someSegment"], +) +@AutoConfigureMockMvc +class SingleSegmentedSequenceControllerTest(@Autowired val mockMvc: MockMvc) { + @MockkBean + lateinit var siloQueryModelMock: SiloQueryModel + + @MockkBean + lateinit var dataVersion: DataVersion + + @BeforeEach + fun setup() { + every { + dataVersion.dataVersion + } returns "1234" + } + + @Test + fun `should GET alignedNucleotideSequences with empty filter`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(emptyMap()), + SequenceType.ALIGNED, + "someSegment", + ) + } returns returnedValue + + mockMvc.perform(get(ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE)) + .andExpect(status().isOk) + .andExpect(content().string(returnedValue)) + .andExpect(header().stringValues("Lapis-Data-Version", "1234")) + } + + @Test + fun `should GET alignedNucleotideSequences with filter`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(mapOf("country" to "Switzerland")), + SequenceType.ALIGNED, + "someSegment", + ) + } returns returnedValue + + mockMvc.perform(get("$ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE?country=Switzerland")) + .andExpect(status().isOk) + .andExpect(content().string(returnedValue)) + .andExpect(header().stringValues("Lapis-Data-Version", "1234")) + } + + @Test + fun `should POST alignedNucleotideSequences with empty filter`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(emptyMap()), + SequenceType.ALIGNED, + "someSegment", + ) + } returns returnedValue + + val request = MockMvcRequestBuilders.post(ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE) + .content("""{}""") + .contentType(MediaType.APPLICATION_JSON) + + mockMvc.perform(request) + .andExpect(status().isOk) + .andExpect(content().string(returnedValue)) + .andExpect(header().stringValues("Lapis-Data-Version", "1234")) + } + + @Test + fun `should POST alignedNucleotideSequences with filter`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(mapOf("country" to "Switzerland")), + SequenceType.ALIGNED, + "someSegment", + ) + } returns returnedValue + + val request = MockMvcRequestBuilders.post(ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE) + .content("""{"country": "Switzerland"}""") + .contentType(MediaType.APPLICATION_JSON) + + mockMvc.perform(request) + .andExpect(status().isOk) + .andExpect(content().string(returnedValue)) + .andExpect(header().stringValues("Lapis-Data-Version", "1234")) + } + + @Test + fun `should not GET alignedNucleotideSequence with segment`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(emptyMap()), + SequenceType.ALIGNED, + "someSegment", + ) + } returns returnedValue + + mockMvc.perform(get("$ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE/someSegment")) + .andExpect(status().isNotFound) + } + + @Test + fun `should not POST alignedNucleotideSequences with segment`() { + val returnedValue = "TestSequenceContent" + every { + siloQueryModelMock.getGenomicSequence( + sequenceFiltersRequest(emptyMap()), + SequenceType.ALIGNED, + "someSegment", + ) + } returns returnedValue + + val request = MockMvcRequestBuilders.post("$ALIGNED_NUCLEOTIDE_SEQUENCES_ROUTE/someSegment") + .content("""{}""") + .contentType(MediaType.APPLICATION_JSON) + + mockMvc.perform(request) + .andExpect(status().isNotFound) + } + + private fun sequenceFiltersRequest( + sequenceFilters: Map, + ) = SequenceFiltersRequest( + sequenceFilters, + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + ) +} diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/model/SiloQueryModelTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/model/SiloQueryModelTest.kt index 6b58cfc9..a96ae3d4 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/model/SiloQueryModelTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/model/SiloQueryModelTest.kt @@ -4,10 +4,10 @@ import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.verify -import org.genspectrum.lapis.config.SingleSegmentedSequenceFeature +import org.genspectrum.lapis.config.ReferenceGenome import org.genspectrum.lapis.request.CommonSequenceFilters -import org.genspectrum.lapis.request.InsertionsRequest import org.genspectrum.lapis.request.MutationProportionsRequest +import org.genspectrum.lapis.request.SequenceFiltersRequest import org.genspectrum.lapis.request.SequenceFiltersRequestWithFields import org.genspectrum.lapis.response.AggregationData import org.genspectrum.lapis.response.AminoAcidInsertionResponse @@ -16,6 +16,8 @@ import org.genspectrum.lapis.response.InsertionData import org.genspectrum.lapis.response.MutationData import org.genspectrum.lapis.response.NucleotideInsertionResponse import org.genspectrum.lapis.response.NucleotideMutationResponse +import org.genspectrum.lapis.response.SequenceData +import org.genspectrum.lapis.silo.SequenceType import org.genspectrum.lapis.silo.SiloAction import org.genspectrum.lapis.silo.SiloClient import org.genspectrum.lapis.silo.SiloQuery @@ -30,7 +32,7 @@ class SiloQueryModelTest { lateinit var siloClientMock: SiloClient @MockK - lateinit var singleSegmentedSequenceFeatureMock: SingleSegmentedSequenceFeature + lateinit var referenceGenomeMock: ReferenceGenome @MockK lateinit var siloFilterExpressionMapperMock: SiloFilterExpressionMapper @@ -40,14 +42,14 @@ class SiloQueryModelTest { @BeforeEach fun setup() { MockKAnnotations.init(this) - underTest = SiloQueryModel(siloClientMock, siloFilterExpressionMapperMock, singleSegmentedSequenceFeatureMock) + underTest = SiloQueryModel(siloClientMock, siloFilterExpressionMapperMock, referenceGenomeMock) } @Test fun `aggregate calls the SILO client with an aggregated action`() { every { siloClientMock.sendQuery(any>>()) } returns emptyList() every { siloFilterExpressionMapperMock.map(any()) } returns True - every { singleSegmentedSequenceFeatureMock.isEnabled() } returns true + every { referenceGenomeMock.isSingleSegmented() } returns true underTest.getAggregated( SequenceFiltersRequestWithFields( @@ -71,7 +73,7 @@ class SiloQueryModelTest { fun `computeNucleotideMutationProportions calls the SILO client with a mutations action`() { every { siloClientMock.sendQuery(any>>()) } returns emptyList() every { siloFilterExpressionMapperMock.map(any()) } returns True - every { singleSegmentedSequenceFeatureMock.isEnabled() } returns true + every { referenceGenomeMock.isSingleSegmented() } returns true underTest.computeNucleotideMutationProportions( MutationProportionsRequest(emptyMap(), emptyList(), emptyList(), emptyList(), emptyList(), 0.5), @@ -90,7 +92,7 @@ class SiloQueryModelTest { MutationData("A1234B", 1234, 0.1234, "someSequenceName"), ) every { siloFilterExpressionMapperMock.map(any()) } returns True - every { singleSegmentedSequenceFeatureMock.isEnabled() } returns true + every { referenceGenomeMock.isSingleSegmented() } returns true val result = underTest.computeNucleotideMutationProportions( MutationProportionsRequest(emptyMap(), emptyList(), emptyList(), emptyList(), emptyList()), @@ -105,7 +107,7 @@ class SiloQueryModelTest { MutationData("A1234B", 1234, 0.1234, "someSegmentName"), ) every { siloFilterExpressionMapperMock.map(any()) } returns True - every { singleSegmentedSequenceFeatureMock.isEnabled() } returns false + every { referenceGenomeMock.isSingleSegmented() } returns false val result = underTest.computeNucleotideMutationProportions( MutationProportionsRequest(emptyMap(), emptyList(), emptyList(), emptyList(), emptyList()), @@ -129,15 +131,15 @@ class SiloQueryModelTest { } @Test - fun `getNucleotideInsertions ignores the field sequenceName if if singleSegmentedSequenceFeature is enabled`() { + fun `getNucleotideInsertions ignores the field sequenceName if the nucleotide sequence has one segment`() { every { siloClientMock.sendQuery(any>>()) } returns listOf( InsertionData(42, "ABCD", 1234, "someSequenceName"), ) every { siloFilterExpressionMapperMock.map(any()) } returns True - every { singleSegmentedSequenceFeatureMock.isEnabled() } returns true + every { referenceGenomeMock.isSingleSegmented() } returns true val result = underTest.getNucleotideInsertions( - InsertionsRequest( + SequenceFiltersRequest( emptyMap(), emptyList(), emptyList(), @@ -151,15 +153,15 @@ class SiloQueryModelTest { } @Test - fun `getNucleotideInsertions includes the field sequenceName if singleSegmentedSequenceFeature is not enabled`() { + fun `getNucleotideInsertions includes the field sequenceName if the nucleotide sequence has multiple segments`() { every { siloClientMock.sendQuery(any>>()) } returns listOf( InsertionData(42, "ABCD", 1234, "someSequenceName"), ) every { siloFilterExpressionMapperMock.map(any()) } returns True - every { singleSegmentedSequenceFeatureMock.isEnabled() } returns false + every { referenceGenomeMock.isSingleSegmented() } returns false val result = underTest.getNucleotideInsertions( - InsertionsRequest( + SequenceFiltersRequest( emptyMap(), emptyList(), emptyList(), @@ -180,7 +182,7 @@ class SiloQueryModelTest { every { siloFilterExpressionMapperMock.map(any()) } returns True val result = underTest.getAminoAcidInsertions( - InsertionsRequest( + SequenceFiltersRequest( emptyMap(), emptyList(), emptyList(), @@ -192,4 +194,29 @@ class SiloQueryModelTest { assertThat(result, equalTo(listOf(AminoAcidInsertionResponse("ins_someGene:1234:ABCD", 42)))) } + + @Test + fun `getGenomicSequence calls the SILO client with a sequence action`() { + every { siloClientMock.sendQuery(any>>()) } returns emptyList() + every { siloFilterExpressionMapperMock.map(any()) } returns True + + underTest.getGenomicSequence( + SequenceFiltersRequest( + emptyMap(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + ), + SequenceType.ALIGNED, + "someSequenceName", + ) + + verify { + siloClientMock.sendQuery( + SiloQuery(SiloAction.genomicSequence(SequenceType.ALIGNED, "someSequenceName"), True), + ) + } + } } diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt index 89e80ce6..f8f948af 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.TextNode import org.genspectrum.lapis.response.AggregationData import org.genspectrum.lapis.response.DetailsData import org.genspectrum.lapis.response.MutationData +import org.genspectrum.lapis.response.SequenceData import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.containsString @@ -154,6 +155,43 @@ class SiloClientTest { ) } + @Test + fun `given server returns sequence data then response can be deserialized`() { + expectQueryRequestAndRespondWith( + response() + .withContentType(MediaType.APPLICATION_JSON_UTF_8) + .withBody( + """{ + "queryResult": [ + { + "gisaid_epi_isl": "key1", + "someSequenceName": "ABCD" + }, + { + "gisaid_epi_isl": "key2", + "someSequenceName": "DEFG" + } + ] + }""", + ), + ) + + val query = SiloQuery( + SiloAction.genomicSequence(SequenceType.ALIGNED, "someSequenceName"), + StringEquals("theColumn", "theValue"), + ) + val result = underTest.sendQuery(query) + + assertThat(result, hasSize(2)) + assertThat( + result, + containsInAnyOrder( + SequenceData("key1", "ABCD"), + SequenceData("key2", "DEFG"), + ), + ) + } + @Test fun `given server returns details response then response can be deserialized`() { expectQueryRequestAndRespondWith( diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloQueryTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloQueryTest.kt index 3b1ee3fc..3136fb11 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloQueryTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloQueryTest.kt @@ -196,6 +196,66 @@ class SiloQueryTest { } """, ), + Arguments.of( + SiloAction.genomicSequence(SequenceType.ALIGNED, "someSequenceName"), + """ + { + "type": "FastaAligned", + "sequenceName": "someSequenceName" + } + """, + ), + Arguments.of( + SiloAction.genomicSequence( + SequenceType.ALIGNED, + "someSequenceName", + listOf(OrderByField("field3", Order.ASCENDING), OrderByField("field4", Order.DESCENDING)), + 100, + 50, + ), + """ + { + "type": "FastaAligned", + "sequenceName": "someSequenceName", + "orderByFields": [ + {"field": "field3", "order": "ascending"}, + {"field": "field4", "order": "descending"} + ], + "limit": 100, + "offset": 50 + } + """, + ), + Arguments.of( + SiloAction.genomicSequence(SequenceType.UNALIGNED, "someSequenceName"), + """ + { + "type": "Fasta", + "sequenceName": "someSequenceName" + } + """, + ), + Arguments.of( + SiloAction.genomicSequence( + SequenceType.UNALIGNED, + "someSequenceName", + listOf(OrderByField("field3", Order.ASCENDING), OrderByField("field4", Order.DESCENDING)), + 100, + 50, + ), + """ + { + "type": "Fasta", + "sequenceName": "someSequenceName", + "orderByFields": [ + {"field": "field3", "order": "ascending"}, + {"field": "field4", "order": "descending"} + ], + "limit": 100, + "offset": 50 + } + """, + ), ) @JvmStatic diff --git a/lapis2/src/test/resources/application-test.properties b/lapis2/src/test/resources/application-test.properties index 9665721e..cb4d3d20 100644 --- a/lapis2/src/test/resources/application-test.properties +++ b/lapis2/src/test/resources/application-test.properties @@ -1,3 +1,4 @@ silo.url=http://url.to.silo lapis.databaseConfig.path=src/test/resources/config/testDatabaseConfig.yaml lapis.accessKeys.path=src/test/resources/config/testAccessKeys.yaml +referenceGenome.nucleotideSequences=main,other_segment diff --git a/lapis2/src/test/resources/application-testWithoutAccessKeys.properties b/lapis2/src/test/resources/application-testWithoutAccessKeys.properties index fd626d23..272aa699 100644 --- a/lapis2/src/test/resources/application-testWithoutAccessKeys.properties +++ b/lapis2/src/test/resources/application-testWithoutAccessKeys.properties @@ -1,4 +1,4 @@ spring.config.import=file:src/main/resources/application.properties - silo.url=http://url.to.silo lapis.databaseConfig.path=src/test/resources/config/testDatabaseConfig.yaml +referenceGenome.nucleotideSequences=main,other_segment diff --git a/lapis2/src/test/resources/config/reference-genomes.json b/lapis2/src/test/resources/config/reference-genomes.json new file mode 100644 index 00000000..aef3f62d --- /dev/null +++ b/lapis2/src/test/resources/config/reference-genomes.json @@ -0,0 +1,58 @@ +{ + "nucleotide_sequences": [ + { + "name": "main", + "sequence": "ATTAAAGGTTTATACCTTCCCAGGTAACAAACCAACCAACTTTCGATCTCTTGTAGATCTGTTCTCTAAACGAACTTTAAAATCTGTGTGGCTGTCACTCGGCTGCATGCTTAGTGCACTCACGCAGTATAATTAATAACTAATTACTGTCGTTGACAGGACACGAGTAACTCGTCTATCTTCTGCAGGCTGCTTACGGTTTCGTCCGTGTTGCAGCCGATCATCAGCACATCTAGGTTTCGTCCGGGTGTGACCGAAAGGTAAGATGGAGAGCCTTGTCCCTGGTTTCAACGAGAAAACACACGTCCAACTCAGTTTGCCTGTTTTACAGGTTCGCGACGTGCTCGTACGTGGCTTTGGAGACTCCGTGGAGGAGGTCTTATCAGAGGCACGTCAACATCTTAAAGATGGCACTTGTGGCTTAGTAGAAGTTGAAAAAGGCGTTTTGCCTCAACTTGAACAGCCCTATGTGTTCATCAAACGTTCGGATGCTCGAACTGCACCTCATGGTCATGTTATGGTTGAGCTGGTAGCAGAACTCGAAGGCATTCAGTACGGTCGTAGTGGTGAGACACTTGGTGTCCTTGTCCCTCATGTGGGCGAAATACCAGTGGCTTACCGCAAGGTTCTTCTTCGTAAGAACGGTAATAAAGGAGCTGGTGGCCATAGTTACGGCGCCGATCTAAAGTCATTTGACTTAGGCGACGAGCTTGGCACTGATCCTTATGAAGATTTTCAAGAAAACTGGAACACTAAACATAGCAGTGGTGTTACCCGTGAACTCATGCGTGAGCTTAACGGAGGGGCATACACTCGCTATGTCGATAACAACTTCTGTGGCCCTGATGGCTACCCTCTTGAGTGCATTAAAGACCTTCTAGCACGTGCTGGTAAAGCTTCATGCACTTTGTCCGAACAACTGGACTTTATTGACACTAAGAGGGGTGTATACTGCTGCCGTGAACATGAGCATGAAATTGCTTGGTACACGGAACGTTCTGAAAAGAGCTATGAATTGCAGACACCTTTTGAAATTAAATTGGCAAAGAAATTTGACACCTTCAATGGGGAATGTCCAAATTTTGTATTTCCCTTAAATTCCATAATCAAGACTATTCAACCAAGGGTTGAAAAGAAAAAGCTTGATGGCTTTATGGGTAGAATTCGATCTGTCTATCCAGTTGCGTCACCAAATGAATGCAACCAAATGTGCCTTTCAACTCTCATGAAGTGTGATCATTGTGGTGAAACTTCATGGCAGACGGGCGATTTTGTTAAAGCCACTTGCGAATTTTGTGGCACTGAGAATTTGACTAAAGAAGGTGCCACTACTTGTGGTTACTTACCCCAAAATGCTGTTGTTAAAATTTATTGTCCAGCATGTCACAATTCAGAAGTAGGACCTGAGCATAGTCTTGCCGAATACCATAATGAATCTGGCTTGAAAACCATTCTTCGTAAGGGTGGTCGCACTATTGCCTTTGGAGGCTGTGTGTTCTCTTATGTTGGTTGCCATAACAAGTGTGCCTATTGGGTTCCACGTGCTAGCGCTAACATAGGTTGTAACCATACAGGTGTTGTTGGAGAAGGTTCCGAAGGTCTTAATGACAACCTTCTTGAAATACTCCAAAAAGAGAAAGTCAACATCAATATTGTTGGTGACTTTAAACTTAATGAAGAGATCGCCATTATTTTGGCATCTTTTTCTGCTTCCACAAGTGCTTTTGTGGAAACTGTGAAAGGTTTGGATTATAAAGCATTCAAACAAATTGTTGAATCCTGTGGTAATTTTAAAGTTACAAAAGGAAAAGCTAAAAAAGGTGCCTGGAATATTGGTGAACAGAAATCAATACTGAGTCCTCTTTATGCATTTGCATCAGAGGCTGCTCGTGTTGTACGATCAATTTTCTCCCGCACTCTTGAAACTGCTCAAAATTCTGTGCGTGTTTTACAGAAGGCCGCTATAACAATACTAGATGGAATTTCACAGTATTCACTGAGACTCATTGATGCTATGATGTTCACATCTGATTTGGCTACTAACAATCTAGTTGTAATGGCCTACATTACAGGTGGTGTTGTTCAGTTGACTTCGCAGTGGCTAACTAACATCTTTGGCACTGTTTATGAAAAACTCAAACCCGTCCTTGATTGGCTTGAAGAGAAGTTTAAGGAAGGTGTAGAGTTTCTTAGAGACGGTTGGGAAATTGTTAAATTTATCTCAACCTGTGCTTGTGAAATTGTCGGTGGACAAATTGTCACCTGTGCAAAGGAAATTAAGGAGAGTGTTCAGACATTCTTTAAGCTTGTAAATAAATTTTTGGCTTTGTGTGCTGACTCTATCATTATTGGTGGAGCTAAACTTAAAGCCTTGAATTTAGGTGAAACATTTGTCACGCACTCAAAGGGATTGTACAGAAAGTGTGTTAAATCCAGAGAAGAAACTGGCCTACTCATGCCTCTAAAAGCCCCAAAAGAAATTATCTTCTTAGAGGGAGAAACACTTCCCACAGAAGTGTTAACAGAGGAAGTTGTCTTGAAAACTGGTGATTTACAACCATTAGAACAACCTACTAGTGAAGCTGTTGAAGCTCCATTGGTTGGTACACCAGTTTGTATTAACGGGCTTATGTTGCTCGAAATCAAAGACACAGAAAAGTACTGTGCCCTTGCACCTAATATGATGGTAACAAACAATACCTTCACACTCAAAGGCGGTGCACCAACAAAGGTTACTTTTGGTGATGACACTGTGATAGAAGTGCAAGGTTACAAGAGTGTGAATATCACTTTTGAACTTGATGAAAGGATTGATAAAGTACTTAATGAGAAGTGCTCTGCCTATACAGTTGAACTCGGTACAGAAGTAAATGAGTTCGCCTGTGTTGTGGCAGATGCTGTCATAAAAACTTTGCAACCAGTATCTGAATTACTTACACCACTGGGCATTGATTTAGATGAGTGGAGTATGGCTACATACTACTTATTTGATGAGTCTGGTGAGTTTAAATTGGCTTCACATATGTATTGTTCTTTCTACCCTCCAGATGAGGATGAAGAAGAAGGTGATTGTGAAGAAGAAGAGTTTGAGCCATCAACTCAATATGAGTATGGTACTGAAGATGATTACCAAGGTAAACCTTTGGAATTTGGTGCCACTTCTGCTGCTCTTCAACCTGAAGAAGAGCAAGAAGAAGATTGGTTAGATGATGATAGTCAACAAACTGTTGGTCAACAAGACGGCAGTGAGGACAATCAGACAACTACTATTCAAACAATTGTTGAGGTTCAACCTCAATTAGAGATGGAACTTACACCAGTTGTTCAGACTATTGAAGTGAATAGTTTTAGTGGTTATTTAAAACTTACTGACAATGTATACATTAAAAATGCAGACATTGTGGAAGAAGCTAAAAAGGTAAAACCAACAGTGGTTGTTAATGCAGCCAATGTTTACCTTAAACATGGAGGAGGTGTTGCAGGAGCCTTAAATAAGGCTACTAACAATGCCATGCAAGTTGAATCTGATGATTACATAGCTACTAATGGACCACTTAAAGTGGGTGGTAGTTGTGTTTTAAGCGGACACAATCTTGCTAAACACTGTCTTCATGTTGTCGGCCCAAATGTTAACAAAGGTGAAGACATTCAACTTCTTAAGAGTGCTTATGAAAATTTTAATCAGCACGAAGTTCTACTTGCACCATTATTATCAGCTGGTATTTTTGGTGCTGACCCTATACATTCTTTAAGAGTTTGTGTAGATACTGTTCGCACAAATGTCTACTTAGCTGTCTTTGATAAAAATCTCTATGACAAACTTGTTTCAAGCTTTTTGGAAATGAAGAGTGAAAAGCAAGTTGAACAAAAGATCGCTGAGATTCCTAAAGAGGAAGTTAAGCCATTTATAACTGAAAGTAAACCTTCAGTTGAACAGAGAAAACAAGATGATAAGAAAATCAAAGCTTGTGTTGAAGAAGTTACAACAACTCTGGAAGAAACTAAGTTCCTCACAGAAAACTTGTTACTTTATATTGACATTAATGGCAATCTTCATCCAGATTCTGCCACTCTTGTTAGTGACATTGACATCACTTTCTTAAAGAAAGATGCTCCATATATAGTGGGTGATGTTGTTCAAGAGGGTGTTTTAACTGCTGTGGTTATACCTACTAAAAAGGCTGGTGGCACTACTGAAATGCTAGCGAAAGCTTTGAGAAAAGTGCCAACAGACAATTATATAACCACTTACCCGGGTCAGGGTTTAAATGGTTACACTGTAGAGGAGGCAAAGACAGTGCTTAAAAAGTGTAAAAGTGCCTTTTACATTCTACCATCTATTATCTCTAATGAGAAGCAAGAAATTCTTGGAACTGTTTCTTGGAATTTGCGAGAAATGCTTGCACATGCAGAAGAAACACGCAAATTAATGCCTGTCTGTGTGGAAACTAAAGCCATAGTTTCAACTATACAGCGTAAATATAAGGGTATTAAAATACAAGAGGGTGTGGTTGATTATGGTGCTAGATTTTACTTTTACACCAGTAAAACAACTGTAGCGTCACTTATCAACACACTTAACGATCTAAATGAAACTCTTGTTACAATGCCACTTGGCTATGTAACACATGGCTTAAATTTGGAAGAAGCTGCTCGGTATATGAGATCTCTCAAAGTGCCAGCTACAGTTTCTGTTTCTTCACCTGATGCTGTTACAGCGTATAATGGTTATCTTACTTCTTCTTCTAAAACACCTGAAGAACATTTTATTGAAACCATCTCACTTGCTGGTTCCTATAAAGATTGGTCCTATTCTGGACAATCTACACAACTAGGTATAGAATTTCTTAAGAGAGGTGATAAAAGTGTATATTACACTAGTAATCCTACCACATTCCACCTAGATGGTGAAGTTATCACCTTTGACAATCTTAAGACACTTCTTTCTTTGAGAGAAGTGAGGACTATTAAGGTGTTTACAACAGTAGACAACATTAACCTCCACACGCAAGTTGTGGACATGTCAATGACATATGGACAACAGTTTGGTCCAACTTATTTGGATGGAGCTGATGTTACTAAAATAAAACCTCATAATTCACATGAAGGTAAAACATTTTATGTTTTACCTAATGATGACACTCTACGTGTTGAGGCTTTTGAGTACTACCACACAACTGATCCTAGTTTTCTGGGTAGGTACATGTCAGCATTAAATCACACTAAAAAGTGGAAATACCCACAAGTTAATGGTTTAACTTCTATTAAATGGGCAGATAACAACTGTTATCTTGCCACTGCATTGTTAACACTCCAACAAATAGAGTTGAAGTTTAATCCACCTGCTCTACAAGATGCTTATTACAGAGCAAGGGCTGGTGAAGCTGCTAACTTTTGTGCACTTATCTTAGCCTACTGTAATAAGACAGTAGGTGAGTTAGGTGATGTTAGAGAAACAATGAGTTACTTGTTTCAACATGCCAATTTAGATTCTTGCAAAAGAGTCTTGAACGTGGTGTGTAAAACTTGTGGACAACAGCAGACAACCCTTAAGGGTGTAGAAGCTGTTATGTACATGGGCACACTTTCTTATGAACAATTTAAGAAAGGTGTTCAGATACCTTGTACGTGTGGTAAACAAGCTACAAAATATCTAGTACAACAGGAGTCACCTTTTGTTATGATGTCAGCACCACCTGCTCAGTATGAACTTAAGCATGGTACATTTACTTGTGCTAGTGAGTACACTGGTAATTACCAGTGTGGTCACTATAAACATATAACTTCTAAAGAAACTTTGTATTGCATAGACGGTGCTTTACTTACAAAGTCCTCAGAATACAAAGGTCCTATTACGGATGTTTTCTACAAAGAAAACAGTTACACAACAACCATAAAACCAGTTACTTATAAATTGGATGGTGTTGTTTGTACAGAAATTGACCCTAAGTTGGACAATTATTATAAGAAAGACAATTCTTATTTCACAGAGCAACCAATTGATCTTGTACCAAACCAACCATATCCAAACGCAAGCTTCGATAATTTTAAGTTTGTATGTGATAATATCAAATTTGCTGATGATTTAAACCAGTTAACTGGTTATAAGAAACCTGCTTCAAGAGAGCTTAAAGTTACATTTTTCCCTGACTTAAATGGTGATGTGGTGGCTATTGATTATAAACACTACACACCCTCTTTTAAGAAAGGAGCTAAATTGTTACATAAACCTATTGTTTGGCATGTTAACAATGCAACTAATAAAGCCACGTATAAACCAAATACCTGGTGTATACGTTGTCTTTGGAGCACAAAACCAGTTGAAACATCAAATTCGTTTGATGTACTGAAGTCAGAGGACGCGCAGGGAATGGATAATCTTGCCTGCGAAGATCTAAAACCAGTCTCTGAAGAAGTAGTGGAAAATCCTACCATACAGAAAGACGTTCTTGAGTGTAATGTGAAAACTACCGAAGTTGTAGGAGACATTATACTTAAACCAGCAAATAATAGTTTAAAAATTACAGAAGAGGTTGGCCACACAGATCTAATGGCTGCTTATGTAGACAATTCTAGTCTTACTATTAAGAAACCTAATGAATTATCTAGAGTATTAGGTTTGAAAACCCTTGCTACTCATGGTTTAGCTGCTGTTAATAGTGTCCCTTGGGATACTATAGCTAATTATGCTAAGCCTTTTCTTAACAAAGTTGTTAGTACAACTACTAACATAGTTACACGGTGTTTAAACCGTGTTTGTACTAATTATATGCCTTATTTCTTTACTTTATTGCTACAATTGTGTACTTTTACTAGAAGTACAAATTCTAGAATTAAAGCATCTATGCCGACTACTATAGCAAAGAATACTGTTAAGAGTGTCGGTAAATTTTGTCTAGAGGCTTCATTTAATTATTTGAAGTCACCTAATTTTTCTAAACTGATAAATATTATAATTTGGTTTTTACTATTAAGTGTTTGCCTAGGTTCTTTAATCTACTCAACCGCTGCTTTAGGTGTTTTAATGTCTAATTTAGGCATGCCTTCTTACTGTACTGGTTACAGAGAAGGCTATTTGAACTCTACTAATGTCACTATTGCAACCTACTGTACTGGTTCTATACCTTGTAGTGTTTGTCTTAGTGGTTTAGATTCTTTAGACACCTATCCTTCTTTAGAAACTATACAAATTACCATTTCATCTTTTAAATGGGATTTAACTGCTTTTGGCTTAGTTGCAGAGTGGTTTTTGGCATATATTCTTTTCACTAGGTTTTTCTATGTACTTGGATTGGCTGCAATCATGCAATTGTTTTTCAGCTATTTTGCAGTACATTTTATTAGTAATTCTTGGCTTATGTGGTTAATAATTAATCTTGTACAAATGGCCCCGATTTCAGCTATGGTTAGAATGTACATCTTCTTTGCATCATTTTATTATGTATGGAAAAGTTATGTGCATGTTGTAGACGGTTGTAATTCATCAACTTGTATGATGTGTTACAAACGTAATAGAGCAACAAGAGTCGAATGTACAACTATTGTTAATGGTGTTAGAAGGTCCTTTTATGTCTATGCTAATGGAGGTAAAGGCTTTTGCAAACTACACAATTGGAATTGTGTTAATTGTGATACATTCTGTGCTGGTAGTACATTTATTAGTGATGAAGTTGCGAGAGACTTGTCACTACAGTTTAAAAGACCAATAAATCCTACTGACCAGTCTTCTTACATCGTTGATAGTGTTACAGTGAAGAATGGTTCCATCCATCTTTACTTTGATAAAGCTGGTCAAAAGACTTATGAAAGACATTCTCTCTCTCATTTTGTTAACTTAGACAACCTGAGAGCTAATAACACTAAAGGTTCATTGCCTATTAATGTTATAGTTTTTGATGGTAAATCAAAATGTGAAGAATCATCTGCAAAATCAGCGTCTGTTTACTACAGTCAGCTTATGTGTCAACCTATACTGTTACTAGATCAGGCATTAGTGTCTGATGTTGGTGATAGTGCGGAAGTTGCAGTTAAAATGTTTGATGCTTACGTTAATACGTTTTCATCAACTTTTAACGTACCAATGGAAAAACTCAAAACACTAGTTGCAACTGCAGAAGCTGAACTTGCAAAGAATGTGTCCTTAGACAATGTCTTATCTACTTTTATTTCAGCAGCTCGGCAAGGGTTTGTTGATTCAGATGTAGAAACTAAAGATGTTGTTGAATGTCTTAAATTGTCACATCAATCTGACATAGAAGTTACTGGCGATAGTTGTAATAACTATATGCTCACCTATAACAAAGTTGAAAACATGACACCCCGTGACCTTGGTGCTTGTATTGACTGTAGTGCGCGTCATATTAATGCGCAGGTAGCAAAAAGTCACAACATTGCTTTGATATGGAACGTTAAAGATTTCATGTCATTGTCTGAACAACTACGAAAACAAATACGTAGTGCTGCTAAAAAGAATAACTTACCTTTTAAGTTGACATGTGCAACTACTAGACAAGTTGTTAATGTTGTAACAACAAAGATAGCACTTAAGGGTGGTAAAATTGTTAATAATTGGTTGAAGCAGTTAATTAAAGTTACACTTGTGTTCCTTTTTGTTGCTGCTATTTTCTATTTAATAACACCTGTTCATGTCATGTCTAAACATACTGACTTTTCAAGTGAAATCATAGGATACAAGGCTATTGATGGTGGTGTCACTCGTGACATAGCATCTACAGATACTTGTTTTGCTAACAAACATGCTGATTTTGACACATGGTTTAGCCAGCGTGGTGGTAGTTATACTAATGACAAAGCTTGCCCATTGATTGCTGCAGTCATAACAAGAGAAGTGGGTTTTGTCGTGCCTGGTTTGCCTGGCACGATATTACGCACAACTAATGGTGACTTTTTGCATTTCTTACCTAGAGTTTTTAGTGCAGTTGGTAACATCTGTTACACACCATCAAAACTTATAGAGTACACTGACTTTGCAACATCAGCTTGTGTTTTGGCTGCTGAATGTACAATTTTTAAAGATGCTTCTGGTAAGCCAGTACCATATTGTTATGATACCAATGTACTAGAAGGTTCTGTTGCTTATGAAAGTTTACGCCCTGACACACGTTATGTGCTCATGGATGGCTCTATTATTCAATTTCCTAACACCTACCTTGAAGGTTCTGTTAGAGTGGTAACAACTTTTGATTCTGAGTACTGTAGGCACGGCACTTGTGAAAGATCAGAAGCTGGTGTTTGTGTATCTACTAGTGGTAGATGGGTACTTAACAATGATTATTACAGATCTTTACCAGGAGTTTTCTGTGGTGTAGATGCTGTAAATTTACTTACTAATATGTTTACACCACTAATTCAACCTATTGGTGCTTTGGACATATCAGCATCTATAGTAGCTGGTGGTATTGTAGCTATCGTAGTAACATGCCTTGCCTACTATTTTATGAGGTTTAGAAGAGCTTTTGGTGAATACAGTCATGTAGTTGCCTTTAATACTTTACTATTCCTTATGTCATTCACTGTACTCTGTTTAACACCAGTTTACTCATTCTTACCTGGTGTTTATTCTGTTATTTACTTGTACTTGACATTTTATCTTACTAATGATGTTTCTTTTTTAGCACATATTCAGTGGATGGTTATGTTCACACCTTTAGTACCTTTCTGGATAACAATTGCTTATATCATTTGTATTTCCACAAAGCATTTCTATTGGTTCTTTAGTAATTACCTAAAGAGACGTGTAGTCTTTAATGGTGTTTCCTTTAGTACTTTTGAAGAAGCTGCGCTGTGCACCTTTTTGTTAAATAAAGAAATGTATCTAAAGTTGCGTAGTGATGTGCTATTACCTCTTACGCAATATAATAGATACTTAGCTCTTTATAATAAGTACAAGTATTTTAGTGGAGCAATGGATACAACTAGCTACAGAGAAGCTGCTTGTTGTCATCTCGCAAAGGCTCTCAATGACTTCAGTAACTCAGGTTCTGATGTTCTTTACCAACCACCACAAACCTCTATCACCTCAGCTGTTTTGCAGAGTGGTTTTAGAAAAATGGCATTCCCATCTGGTAAAGTTGAGGGTTGTATGGTACAAGTAACTTGTGGTACAACTACACTTAACGGTCTTTGGCTTGATGACGTAGTTTACTGTCCAAGACATGTGATCTGCACCTCTGAAGACATGCTTAACCCTAATTATGAAGATTTACTCATTCGTAAGTCTAATCATAATTTCTTGGTACAGGCTGGTAATGTTCAACTCAGGGTTATTGGACATTCTATGCAAAATTGTGTACTTAAGCTTAAGGTTGATACAGCCAATCCTAAGACACCTAAGTATAAGTTTGTTCGCATTCAACCAGGACAGACTTTTTCAGTGTTAGCTTGTTACAATGGTTCACCATCTGGTGTTTACCAATGTGCTATGAGGCCCAATTTCACTATTAAGGGTTCATTCCTTAATGGTTCATGTGGTAGTGTTGGTTTTAACATAGATTATGACTGTGTCTCTTTTTGTTACATGCACCATATGGAATTACCAACTGGAGTTCATGCTGGCACAGACTTAGAAGGTAACTTTTATGGACCTTTTGTTGACAGGCAAACAGCACAAGCAGCTGGTACGGACACAACTATTACAGTTAATGTTTTAGCTTGGTTGTACGCTGCTGTTATAAATGGAGACAGGTGGTTTCTCAATCGATTTACCACAACTCTTAATGACTTTAACCTTGTGGCTATGAAGTACAATTATGAACCTCTAACACAAGACCATGTTGACATACTAGGACCTCTTTCTGCTCAAACTGGAATTGCCGTTTTAGATATGTGTGCTTCATTAAAAGAATTACTGCAAAATGGTATGAATGGACGTACCATATTGGGTAGTGCTTTATTAGAAGATGAATTTACACCTTTTGATGTTGTTAGACAATGCTCAGGTGTTACTTTCCAAAGTGCAGTGAAAAGAACAATCAAGGGTACACACCACTGGTTGTTACTCACAATTTTGACTTCACTTTTAGTTTTAGTCCAGAGTACTCAATGGTCTTTGTTCTTTTTTTTGTATGAAAATGCCTTTTTACCTTTTGCTATGGGTATTATTGCTATGTCTGCTTTTGCAATGATGTTTGTCAAACATAAGCATGCATTTCTCTGTTTGTTTTTGTTACCTTCTCTTGCCACTGTAGCTTATTTTAATATGGTCTATATGCCTGCTAGTTGGGTGATGCGTATTATGACATGGTTGGATATGGTTGATACTAGTTTGTCTGGTTTTAAGCTAAAAGACTGTGTTATGTATGCATCAGCTGTAGTGTTACTAATCCTTATGACAGCAAGAACTGTGTATGATGATGGTGCTAGGAGAGTGTGGACACTTATGAATGTCTTGACACTCGTTTATAAAGTTTATTATGGTAATGCTTTAGATCAAGCCATTTCCATGTGGGCTCTTATAATCTCTGTTACTTCTAACTACTCAGGTGTAGTTACAACTGTCATGTTTTTGGCCAGAGGTATTGTTTTTATGTGTGTTGAGTATTGCCCTATTTTCTTCATAACTGGTAATACACTTCAGTGTATAATGCTAGTTTATTGTTTCTTAGGCTATTTTTGTACTTGTTACTTTGGCCTCTTTTGTTTACTCAACCGCTACTTTAGACTGACTCTTGGTGTTTATGATTACTTAGTTTCTACACAGGAGTTTAGATATATGAATTCACAGGGACTACTCCCACCCAAGAATAGCATAGATGCCTTCAAACTCAACATTAAATTGTTGGGTGTTGGTGGCAAACCTTGTATCAAAGTAGCCACTGTACAGTCTAAAATGTCAGATGTAAAGTGCACATCAGTAGTCTTACTCTCAGTTTTGCAACAACTCAGAGTAGAATCATCATCTAAATTGTGGGCTCAATGTGTCCAGTTACACAATGACATTCTCTTAGCTAAAGATACTACTGAAGCCTTTGAAAAAATGGTTTCACTACTTTCTGTTTTGCTTTCCATGCAGGGTGCTGTAGACATAAACAAGCTTTGTGAAGAAATGCTGGACAACAGGGCAACCTTACAAGCTATAGCCTCAGAGTTTAGTTCCCTTCCATCATATGCAGCTTTTGCTACTGCTCAAGAAGCTTATGAGCAGGCTGTTGCTAATGGTGATTCTGAAGTTGTTCTTAAAAAGTTGAAGAAGTCTTTGAATGTGGCTAAATCTGAATTTGACCGTGATGCAGCCATGCAACGTAAGTTGGAAAAGATGGCTGATCAAGCTATGACCCAAATGTATAAACAGGCTAGATCTGAGGACAAGAGGGCAAAAGTTACTAGTGCTATGCAGACAATGCTTTTCACTATGCTTAGAAAGTTGGATAATGATGCACTCAACAACATTATCAACAATGCAAGAGATGGTTGTGTTCCCTTGAACATAATACCTCTTACAACAGCAGCCAAACTAATGGTTGTCATACCAGACTATAACACATATAAAAATACGTGTGATGGTACAACATTTACTTATGCATCAGCATTGTGGGAAATCCAACAGGTTGTAGATGCAGATAGTAAAATTGTTCAACTTAGTGAAATTAGTATGGACAATTCACCTAATTTAGCATGGCCTCTTATTGTAACAGCTTTAAGGGCCAATTCTGCTGTCAAATTACAGAATAATGAGCTTAGTCCTGTTGCACTACGACAGATGTCTTGTGCTGCCGGTACTACACAAACTGCTTGCACTGATGACAATGCGTTAGCTTACTACAACACAACAAAGGGAGGTAGGTTTGTACTTGCACTGTTATCCGATTTACAGGATTTGAAATGGGCTAGATTCCCTAAGAGTGATGGAACTGGTACTATCTATACAGAACTGGAACCACCTTGTAGGTTTGTTACAGACACACCTAAAGGTCCTAAAGTGAAGTATTTATACTTTATTAAAGGATTAAACAACCTAAATAGAGGTATGGTACTTGGTAGTTTAGCTGCCACAGTACGTCTACAAGCTGGTAATGCAACAGAAGTGCCTGCCAATTCAACTGTATTATCTTTCTGTGCTTTTGCTGTAGATGCTGCTAAAGCTTACAAAGATTATCTAGCTAGTGGGGGACAACCAATCACTAATTGTGTTAAGATGTTGTGTACACACACTGGTACTGGTCAGGCAATAACAGTTACACCGGAAGCCAATATGGATCAAGAATCCTTTGGTGGTGCATCGTGTTGTCTGTACTGCCGTTGCCACATAGATCATCCAAATCCTAAAGGATTTTGTGACTTAAAAGGTAAGTATGTACAAATACCTACAACTTGTGCTAATGACCCTGTGGGTTTTACACTTAAAAACACAGTCTGTACCGTCTGCGGTATGTGGAAAGGTTATGGCTGTAGTTGTGATCAACTCCGCGAACCCATGCTTCAGTCAGCTGATGCACAATCGTTTTTAAACGGGTTTGCGGTGTAAGTGCAGCCCGTCTTACACCGTGCGGCACAGGCACTAGTACTGATGTCGTATACAGGGCTTTTGACATCTACAATGATAAAGTAGCTGGTTTTGCTAAATTCCTAAAAACTAATTGTTGTCGCTTCCAAGAAAAGGACGAAGATGACAATTTAATTGATTCTTACTTTGTAGTTAAGAGACACACTTTCTCTAACTACCAACATGAAGAAACAATTTATAATTTACTTAAGGATTGTCCAGCTGTTGCTAAACATGACTTCTTTAAGTTTAGAATAGACGGTGACATGGTACCACATATATCACGTCAACGTCTTACTAAATACACAATGGCAGACCTCGTCTATGCTTTAAGGCATTTTGATGAAGGTAATTGTGACACATTAAAAGAAATACTTGTCACATACAATTGTTGTGATGATGATTATTTCAATAAAAAGGACTGGTATGATTTTGTAGAAAACCCAGATATATTACGCGTATACGCCAACTTAGGTGAACGTGTACGCCAAGCTTTGTTAAAAACAGTACAATTCTGTGATGCCATGCGAAATGCTGGTATTGTTGGTGTACTGACATTAGATAATCAAGATCTCAATGGTAACTGGTATGATTTCGGTGATTTCATACAAACCACGCCAGGTAGTGGAGTTCCTGTTGTAGATTCTTATTATTCATTGTTAATGCCTATATTAACCTTGACCAGGGCTTTAACTGCAGAGTCACATGTTGACACTGACTTAACAAAGCCTTACATTAAGTGGGATTTGTTAAAATATGACTTCACGGAAGAGAGGTTAAAACTCTTTGACCGTTATTTTAAATATTGGGATCAGACATACCACCCAAATTGTGTTAACTGTTTGGATGACAGATGCATTCTGCATTGTGCAAACTTTAATGTTTTATTCTCTACAGTGTTCCCACCTACAAGTTTTGGACCACTAGTGAGAAAAATATTTGTTGATGGTGTTCCATTTGTAGTTTCAACTGGATACCACTTCAGAGAGCTAGGTGTTGTACATAATCAGGATGTAAACTTACATAGCTCTAGACTTAGTTTTAAGGAATTACTTGTGTATGCTGCTGACCCTGCTATGCACGCTGCTTCTGGTAATCTATTACTAGATAAACGCACTACGTGCTTTTCAGTAGCTGCACTTACTAACAATGTTGCTTTTCAAACTGTCAAACCCGGTAATTTTAACAAAGACTTCTATGACTTTGCTGTGTCTAAGGGTTTCTTTAAGGAAGGAAGTTCTGTTGAATTAAAACACTTCTTCTTTGCTCAGGATGGTAATGCTGCTATCAGCGATTATGACTACTATCGTTATAATCTACCAACAATGTGTGATATCAGACAACTACTATTTGTAGTTGAAGTTGTTGATAAGTACTTTGATTGTTACGATGGTGGCTGTATTAATGCTAACCAAGTCATCGTCAACAACCTAGACAAATCAGCTGGTTTTCCATTTAATAAATGGGGTAAGGCTAGACTTTATTATGATTCAATGAGTTATGAGGATCAAGATGCACTTTTCGCATATACAAAACGTAATGTCATCCCTACTATAACTCAAATGAATCTTAAGTATGCCATTAGTGCAAAGAATAGAGCTCGCACCGTAGCTGGTGTCTCTATCTGTAGTACTATGACCAATAGACAGTTTCATCAAAAATTATTGAAATCAATAGCCGCCACTAGAGGAGCTACTGTAGTAATTGGAACAAGCAAATTCTATGGTGGTTGGCACAACATGTTAAAAACTGTTTATAGTGATGTAGAAAACCCTCACCTTATGGGTTGGGATTATCCTAAATGTGATAGAGCCATGCCTAACATGCTTAGAATTATGGCCTCACTTGTTCTTGCTCGCAAACATACAACGTGTTGTAGCTTGTCACACCGTTTCTATAGATTAGCTAATGAGTGTGCTCAAGTATTGAGTGAAATGGTCATGTGTGGCGGTTCACTATATGTTAAACCAGGTGGAACCTCATCAGGAGATGCCACAACTGCTTATGCTAATAGTGTTTTTAACATTTGTCAAGCTGTCACGGCCAATGTTAATGCACTTTTATCTACTGATGGTAACAAAATTGCCGATAAGTATGTCCGCAATTTACAACACAGACTTTATGAGTGTCTCTATAGAAATAGAGATGTTGACACAGACTTTGTGAATGAGTTTTACGCATATTTGCGTAAACATTTCTCAATGATGATACTCTCTGACGATGCTGTTGTGTGTTTCAATAGCACTTATGCATCTCAAGGTCTAGTGGCTAGCATAAAGAACTTTAAGTCAGTTCTTTATTATCAAAACAATGTTTTTATGTCTGAAGCAAAATGTTGGACTGAGACTGACCTTACTAAAGGACCTCATGAATTTTGCTCTCAACATACAATGCTAGTTAAACAGGGTGATGATTATGTGTACCTTCCTTACCCAGATCCATCAAGAATCCTAGGGGCCGGCTGTTTTGTAGATGATATCGTAAAAACAGATGGTACACTTATGATTGAACGGTTCGTGTCTTTAGCTATAGATGCTTACCCACTTACTAAACATCCTAATCAGGAGTATGCTGATGTCTTTCATTTGTACTTACAATACATAAGAAAGCTACATGATGAGTTAACAGGACACATGTTAGACATGTATTCTGTTATGCTTACTAATGATAACACTTCAAGGTATTGGGAACCTGAGTTTTATGAGGCTATGTACACACCGCATACAGTCTTACAGGCTGTTGGGGCTTGTGTTCTTTGCAATTCACAGACTTCATTAAGATGTGGTGCTTGCATACGTAGACCATTCTTATGTTGTAAATGCTGTTACGACCATGTCATATCAACATCACATAAATTAGTCTTGTCTGTTAATCCGTATGTTTGCAATGCTCCAGGTTGTGATGTCACAGATGTGACTCAACTTTACTTAGGAGGTATGAGCTATTATTGTAAATCACATAAACCACCCATTAGTTTTCCATTGTGTGCTAATGGACAAGTTTTTGGTTTATATAAAAATACATGTGTTGGTAGCGATAATGTTACTGACTTTAATGCAATTGCAACATGTGACTGGACAAATGCTGGTGATTACATTTTAGCTAACACCTGTACTGAAAGACTCAAGCTTTTTGCAGCAGAAACGCTCAAAGCTACTGAGGAGACATTTAAACTGTCTTATGGTATTGCTACTGTACGTGAAGTGCTGTCTGACAGAGAATTACATCTTTCATGGGAAGTTGGTAAACCTAGACCACCACTTAACCGAAATTATGTCTTTACTGGTTATCGTGTAACTAAAAACAGTAAAGTACAAATAGGAGAGTACACCTTTGAAAAAGGTGACTATGGTGATGCTGTTGTTTACCGAGGTACAACAACTTACAAATTAAATGTTGGTGATTATTTTGTGCTGACATCACATACAGTAATGCCATTAAGTGCACCTACACTAGTGCCACAAGAGCACTATGTTAGAATTACTGGCTTATACCCAACACTCAATATCTCAGATGAGTTTTCTAGCAATGTTGCAAATTATCAAAAGGTTGGTATGCAAAAGTATTCTACACTCCAGGGACCACCTGGTACTGGTAAGAGTCATTTTGCTATTGGCCTAGCTCTCTACTACCCTTCTGCTCGCATAGTGTATACAGCTTGCTCTCATGCCGCTGTTGATGCACTATGTGAGAAGGCATTAAAATATTTGCCTATAGATAAATGTAGTAGAATTATACCTGCACGTGCTCGTGTAGAGTGTTTTGATAAATTCAAAGTGAATTCAACATTAGAACAGTATGTCTTTTGTACTGTAAATGCATTGCCTGAGACGACAGCAGATATAGTTGTCTTTGATGAAATTTCAATGGCCACAAATTATGATTTGAGTGTTGTCAATGCCAGATTACGTGCTAAGCACTATGTGTACATTGGCGACCCTGCTCAATTACCTGCACCACGCACATTGCTAACTAAGGGCACACTAGAACCAGAATATTTCAATTCAGTGTGTAGACTTATGAAAACTATAGGTCCAGACATGTTCCTCGGAACTTGTCGGCGTTGTCCTGCTGAAATTGTTGACACTGTGAGTGCTTTGGTTTATGATAATAAGCTTAAAGCACATAAAGACAAATCAGCTCAATGCTTTAAAATGTTTTATAAGGGTGTTATCACGCATGATGTTTCATCTGCAATTAACAGGCCACAAATAGGCGTGGTAAGAGAATTCCTTACACGTAACCCTGCTTGGAGAAAAGCTGTCTTTATTTCACCTTATAATTCACAGAATGCTGTAGCCTCAAAGATTTTGGGACTACCAACTCAAACTGTTGATTCATCACAGGGCTCAGAATATGACTATGTCATATTCACTCAAACCACTGAAACAGCTCACTCTTGTAATGTAAACAGATTTAATGTTGCTATTACCAGAGCAAAAGTAGGCATACTTTGCATAATGTCTGATAGAGACCTTTATGACAAGTTGCAATTTACAAGTCTTGAAATTCCACGTAGGAATGTGGCAACTTTACAAGCTGAAAATGTAACAGGACTCTTTAAAGATTGTAGTAAGGTAATCACTGGGTTACATCCTACACAGGCACCTACACACCTCAGTGTTGACACTAAATTCAAAACTGAAGGTTTATGTGTTGACATACCTGGCATACCTAAGGACATGACCTATAGAAGACTCATCTCTATGATGGGTTTTAAAATGAATTATCAAGTTAATGGTTACCCTAACATGTTTATCACCCGCGAAGAAGCTATAAGACATGTACGTGCATGGATTGGCTTCGATGTCGAGGGGTGTCATGCTACTAGAGAAGCTGTTGGTACCAATTTACCTTTACAGCTAGGTTTTTCTACAGGTGTTAACCTAGTTGCTGTACCTACAGGTTATGTTGATACACCTAATAATACAGATTTTTCCAGAGTTAGTGCTAAACCACCGCCTGGAGATCAATTTAAACACCTCATACCACTTATGTACAAAGGACTTCCTTGGAATGTAGTGCGTATAAAGATTGTACAAATGTTAAGTGACACACTTAAAAATCTCTCTGACAGAGTCGTATTTGTCTTATGGGCACATGGCTTTGAGTTGACATCTATGAAGTATTTTGTGAAAATAGGACCTGAGCGCACCTGTTGTCTATGTGATAGACGTGCCACATGCTTTTCCACTGCTTCAGACACTTATGCCTGTTGGCATCATTCTATTGGATTTGATTACGTCTATAATCCGTTTATGATTGATGTTCAACAATGGGGTTTTACAGGTAACCTACAAAGCAACCATGATCTGTATTGTCAAGTCCATGGTAATGCACATGTAGCTAGTTGTGATGCAATCATGACTAGGTGTCTAGCTGTCCACGAGTGCTTTGTTAAGCGTGTTGACTGGACTATTGAATATCCTATAATTGGTGATGAACTGAAGATTAATGCGGCTTGTAGAAAGGTTCAACACATGGTTGTTAAAGCTGCATTATTAGCAGACAAATTCCCAGTTCTTCACGACATTGGTAACCCTAAAGCTATTAAGTGTGTACCTCAAGCTGATGTAGAATGGAAGTTCTATGATGCACAGCCTTGTAGTGACAAAGCTTATAAAATAGAAGAATTATTCTATTCTTATGCCACACATTCTGACAAATTCACAGATGGTGTATGCCTATTTTGGAATTGCAATGTCGATAGATATCCTGCTAATTCCATTGTTTGTAGATTTGACACTAGAGTGCTATCTAACCTTAACTTGCCTGGTTGTGATGGTGGCAGTTTGTATGTAAATAAACATGCATTCCACACACCAGCTTTTGATAAAAGTGCTTTTGTTAATTTAAAACAATTACCATTTTTCTATTACTCTGACAGTCCATGTGAGTCTCATGGAAAACAAGTAGTGTCAGATATAGATTATGTACCACTAAAGTCTGCTACGTGTATAACACGTTGCAATTTAGGTGGTGCTGTCTGTAGACATCATGCTAATGAGTACAGATTGTATCTCGATGCTTATAACATGATGATCTCAGCTGGCTTTAGCTTGTGGGTTTACAAACAATTTGATACTTATAACCTCTGGAACACTTTTACAAGACTTCAGAGTTTAGAAAATGTGGCTTTTAATGTTGTAAATAAGGGACACTTTGATGGACAACAGGGTGAAGTACCAGTTTCTATCATTAATAACACTGTTTACACAAAAGTTGATGGTGTTGATGTAGAATTGTTTGAAAATAAAACAACATTACCTGTTAATGTAGCATTTGAGCTTTGGGCTAAGCGCAACATTAAACCAGTACCAGAGGTGAAAATACTCAATAATTTGGGTGTGGACATTGCTGCTAATACTGTGATCTGGGACTACAAAAGAGATGCTCCAGCACATATATCTACTATTGGTGTTTGTTCTATGACTGACATAGCCAAGAAACCAACTGAAACGATTTGTGCACCACTCACTGTCTTTTTTGATGGTAGAGTTGATGGTCAAGTAGACTTATTTAGAAATGCCCGTAATGGTGTTCTTATTACAGAAGGTAGTGTTAAAGGTTTACAACCATCTGTAGGTCCCAAACAAGCTAGTCTTAATGGAGTCACATTAATTGGAGAAGCCGTAAAAACACAGTTCAATTATTATAAGAAAGTTGATGGTGTTGTCCAACAATTACCTGAAACTTACTTTACTCAGAGTAGAAATTTACAAGAATTTAAACCCAGGAGTCAAATGGAAATTGATTTCTTAGAATTAGCTATGGATGAATTCATTGAACGGTATAAATTAGAAGGCTATGCCTTCGAACATATCGTTTATGGAGATTTTAGTCATAGTCAGTTAGGTGGTTTACATCTACTGATTGGACTAGCTAAACGTTTTAAGGAATCACCTTTTGAATTAGAAGATTTTATTCCTATGGACAGTACAGTTAAAAACTATTTCATAACAGATGCGCAAACAGGTTCATCTAAGTGTGTGTGTTCTGTTATTGATTTATTACTTGATGATTTTGTTGAAATAATAAAATCCCAAGATTTATCTGTAGTTTCTAAGGTTGTCAAAGTGACTATTGACTATACAGAAATTTCATTTATGCTTTGGTGTAAAGATGGCCATGTAGAAACATTTTACCCAAAATTACAATCTAGTCAAGCGTGGCAACCGGGTGTTGCTATGCCTAATCTTTACAAAATGCAAAGAATGCTATTAGAAAAGTGTGACCTTCAAAATTATGGTGATAGTGCAACATTACCTAAAGGCATAATGATGAATGTCGCAAAATATACTCAACTGTGTCAATATTTAAACACATTAACATTAGCTGTACCCTATAATATGAGAGTTATACATTTTGGTGCTGGTTCTGATAAAGGAGTTGCACCAGGTACAGCTGTTTTAAGACAGTGGTTGCCTACGGGTACGCTGCTTGTCGATTCAGATCTTAATGACTTTGTCTCTGATGCAGATTCAACTTTGATTGGTGATTGTGCAACTGTACATACAGCTAATAAATGGGATCTCATTATTAGTGATATGTACGACCCTAAGACTAAAAATGTTACAAAAGAAAATGACTCTAAAGAGGGTTTTTTCACTTACATTTGTGGGTTTATACAACAAAAGCTAGCTCTTGGAGGTTCCGTGGCTATAAAGATAACAGAACATTCTTGGAATGCTGATCTTTATAAGCTCATGGGACACTTCGCATGGTGGACAGCCTTTGTTACTAATGTGAATGCGTCATCATCTGAAGCATTTTTAATTGGATGTAATTATCTTGGCAAACCACGCGAACAAATAGATGGTTATGTCATGCATGCAAATTACATATTTTGGAGGAATACAAATCCAATTCAGTTGTCTTCCTATTCTTTATTTGACATGAGTAAATTTCCCCTTAAATTAAGGGGTACTGCTGTTATGTCTTTAAAAGAAGGTCAAATCAATGATATGATTTTATCTCTTCTTAGTAAAGGTAGACTTATAATTAGAGAAAACAACAGAGTTGTTATTTCTAGTGATGTTCTTGTTAACAACTAAACGAACAATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTTAATCTTACAACCAGAACTCAATTACCCCCTGCATACACTAATTCTTTCACACGTGGTGTTTATTACCCTGACAAAGTTTTCAGATCCTCAGTTTTACATTCAACTCAGGACTTGTTCTTACCTTTCTTTTCCAATGTTACTTGGTTCCATGCTATACATGTCTCTGGGACCAATGGTACTAAGAGGTTTGATAACCCTGTCCTACCATTTAATGATGGTGTTTATTTTGCTTCCACTGAGAAGTCTAACATAATAAGAGGCTGGATTTTTGGTACTACTTTAGATTCGAAGACCCAGTCCCTACTTATTGTTAATAACGCTACTAATGTTGTTATTAAAGTCTGTGAATTTCAATTTTGTAATGATCCATTTTTGGGTGTTTATTACCACAAAAACAACAAAAGTTGGATGGAAAGTGAGTTCAGAGTTTATTCTAGTGCGAATAATTGCACTTTTGAATATGTCTCTCAGCCTTTTCTTATGGACCTTGAAGGAAAACAGGGTAATTTCAAAAATCTTAGGGAATTTGTGTTTAAGAATATTGATGGTTATTTTAAAATATATTCTAAGCACACGCCTATTAATTTAGTGCGTGATCTCCCTCAGGGTTTTTCGGCTTTAGAACCATTGGTAGATTTGCCAATAGGTATTAACATCACTAGGTTTCAAACTTTACTTGCTTTACATAGAAGTTATTTGACTCCTGGTGATTCTTCTTCAGGTTGGACAGCTGGTGCTGCAGCTTATTATGTGGGTTATCTTCAACCTAGGACTTTTCTATTAAAATATAATGAAAATGGAACCATTACAGATGCTGTAGACTGTGCACTTGACCCTCTCTCAGAAACAAAGTGTACGTTGAAATCCTTCACTGTAGAAAAAGGAATCTATCAAACTTCTAACTTTAGAGTCCAACCAACAGAATCTATTGTTAGATTTCCTAATATTACAAACTTGTGCCCTTTTGGTGAAGTTTTTAACGCCACCAGATTTGCATCTGTTTATGCTTGGAACAGGAAGAGAATCAGCAACTGTGTTGCTGATTATTCTGTCCTATATAATTCCGCATCATTTTCCACTTTTAAGTGTTATGGAGTGTCTCCTACTAAATTAAATGATCTCTGCTTTACTAATGTCTATGCAGATTCATTTGTAATTAGAGGTGATGAAGTCAGACAAATCGCTCCAGGGCAAACTGGAAAGATTGCTGATTATAATTATAAATTACCAGATGATTTTACAGGCTGCGTTATAGCTTGGAATTCTAACAATCTTGATTCTAAGGTTGGTGGTAATTATAATTACCTGTATAGATTGTTTAGGAAGTCTAATCTCAAACCTTTTGAGAGAGATATTTCAACTGAAATCTATCAGGCCGGTAGCACACCTTGTAATGGTGTTGAAGGTTTTAATTGTTACTTTCCTTTACAATCATATGGTTTCCAACCCACTAATGGTGTTGGTTACCAACCATACAGAGTAGTAGTACTTTCTTTTGAACTTCTACATGCACCAGCAACTGTTTGTGGACCTAAAAAGTCTACTAATTTGGTTAAAAACAAATGTGTCAATTTCAACTTCAATGGTTTAACAGGCACAGGTGTTCTTACTGAGTCTAACAAAAAGTTTCTGCCTTTCCAACAATTTGGCAGAGACATTGCTGACACTACTGATGCTGTCCGTGATCCACAGACACTTGAGATTCTTGACATTACACCATGTTCTTTTGGTGGTGTCAGTGTTATAACACCAGGAACAAATACTTCTAACCAGGTTGCTGTTCTTTATCAGGATGTTAACTGCACAGAAGTCCCTGTTGCTATTCATGCAGATCAACTTACTCCTACTTGGCGTGTTTATTCTACAGGTTCTAATGTTTTTCAAACACGTGCAGGCTGTTTAATAGGGGCTGAACATGTCAACAACTCATATGAGTGTGACATACCCATTGGTGCAGGTATATGCGCTAGTTATCAGACTCAGACTAATTCTCCTCGGCGGGCACGTAGTGTAGCTAGTCAATCCATCATTGCCTACACTATGTCACTTGGTGCAGAAAATTCAGTTGCTTACTCTAATAACTCTATTGCCATACCCACAAATTTTACTATTAGTGTTACCACAGAAATTCTACCAGTGTCTATGACCAAGACATCAGTAGATTGTACAATGTACATTTGTGGTGATTCAACTGAATGCAGCAATCTTTTGTTGCAATATGGCAGTTTTTGTACACAATTAAACCGTGCTTTAACTGGAATAGCTGTTGAACAAGACAAAAACACCCAAGAAGTTTTTGCACAAGTCAAACAAATTTACAAAACACCACCAATTAAAGATTTTGGTGGTTTTAATTTTTCACAAATATTACCAGATCCATCAAAACCAAGCAAGAGGTCATTTATTGAAGATCTACTTTTCAACAAAGTGACACTTGCAGATGCTGGCTTCATCAAACAATATGGTGATTGCCTTGGTGATATTGCTGCTAGAGACCTCATTTGTGCACAAAAGTTTAACGGCCTTACTGTTTTGCCACCTTTGCTCACAGATGAAATGATTGCTCAATACACTTCTGCACTGTTAGCGGGTACAATCACTTCTGGTTGGACCTTTGGTGCAGGTGCTGCATTACAAATACCATTTGCTATGCAAATGGCTTATAGGTTTAATGGTATTGGAGTTACACAGAATGTTCTCTATGAGAACCAAAAATTGATTGCCAACCAATTTAATAGTGCTATTGGCAAAATTCAAGACTCACTTTCTTCCACAGCAAGTGCACTTGGAAAACTTCAAGATGTGGTCAACCAAAATGCACAAGCTTTAAACACGCTTGTTAAACAACTTAGCTCCAATTTTGGTGCAATTTCAAGTGTTTTAAATGATATCCTTTCACGTCTTGACAAAGTTGAGGCTGAAGTGCAAATTGATAGGTTGATCACAGGCAGACTTCAAAGTTTGCAGACATATGTGACTCAACAATTAATTAGAGCTGCAGAAATCAGAGCTTCTGCTAATCTTGCTGCTACTAAAATGTCAGAGTGTGTACTTGGACAATCAAAAAGAGTTGATTTTTGTGGAAAGGGCTATCATCTTATGTCCTTCCCTCAGTCAGCACCTCATGGTGTAGTCTTCTTGCATGTGACTTATGTCCCTGCACAAGAAAAGAACTTCACAACTGCTCCTGCCATTTGTCATGATGGAAAAGCACACTTTCCTCGTGAAGGTGTCTTTGTTTCAAATGGCACACACTGGTTTGTAACACAAAGGAATTTTTATGAACCACAAATCATTACTACAGACAACACATTTGTGTCTGGTAACTGTGATGTTGTAATAGGAATTGTCAACAACACAGTTTATGATCCTTTGCAACCTGAATTAGACTCATTCAAGGAGGAGTTAGATAAATATTTTAAGAATCATACATCACCAGATGTTGATTTAGGTGACATCTCTGGCATTAATGCTTCAGTTGTAAACATTCAAAAAGAAATTGACCGCCTCAATGAGGTTGCCAAGAATTTAAATGAATCTCTCATCGATCTCCAAGAACTTGGAAAGTATGAGCAGTATATAAAATGGCCATGGTACATTTGGCTAGGTTTTATAGCTGGCTTGATTGCCATAGTAATGGTGACAATTATGCTTTGCTGTATGACCAGTTGCTGTAGTTGTCTCAAGGGCTGTTGTTCTTGTGGATCCTGCTGCAAATTTGATGAAGACGACTCTGAGCCAGTGCTCAAAGGAGTCAAATTACATTACACATAAACGAACTTATGGATTTGTTTATGAGAATCTTCACAATTGGAACTGTAACTTTGAAGCAAGGTGAAATCAAGGATGCTACTCCTTCAGATTTTGTTCGCGCTACTGCAACGATACCGATACAAGCCTCACTCCCTTTCGGATGGCTTATTGTTGGCGTTGCACTTCTTGCTGTTTTTCAGAGCGCTTCCAAAATCATAACCCTCAAAAAGAGATGGCAACTAGCACTCTCCAAGGGTGTTCACTTTGTTTGCAACTTGCTGTTGTTGTTTGTAACAGTTTACTCACACCTTTTGCTCGTTGCTGCTGGCCTTGAAGCCCCTTTTCTCTATCTTTATGCTTTAGTCTACTTCTTGCAGAGTATAAACTTTGTAAGAATAATAATGAGGCTTTGGCTTTGCTGGAAATGCCGTTCCAAAAACCCATTACTTTATGATGCCAACTATTTTCTTTGCTGGCATACTAATTGTTACGACTATTGTATACCTTACAATAGTGTAACTTCTTCAATTGTCATTACTTCAGGTGATGGCACAACAAGTCCTATTTCTGAACATGACTACCAGATTGGTGGTTATACTGAAAAATGGGAATCTGGAGTAAAAGACTGTGTTGTATTACACAGTTACTTCACTTCAGACTATTACCAGCTGTACTCAACTCAATTGAGTACAGACACTGGTGTTGAACATGTTACCTTCTTCATCTACAATAAAATTGTTGATGAGCCTGAAGAACATGTCCAAATTCACACAATCGACGGTTCATCCGGAGTTGTTAATCCAGTAATGGAACCAATTTATGATGAACCGACGACGACTACTAGCGTGCCTTTGTAAGCACAAGCTGATGAGTACGAACTTATGTACTCATTCGTTTCGGAAGAGACAGGTACGTTAATAGTTAATAGCGTACTTCTTTTTCTTGCTTTCGTGGTATTCTTGCTAGTTACACTAGCCATCCTTACTGCGCTTCGATTGTGTGCGTACTGCTGCAATATTGTTAACGTGAGTCTTGTAAAACCTTCTTTTTACGTTTACTCTCGTGTTAAAAATCTGAATTCTTCTAGAGTTCCTGATCTTCTGGTCTAAACGAACTAAATATTATATTAGTTTTTCTGTTTGGAACTTTAATTTTAGCCATGGCAGATTCCAACGGTACTATTACCGTTGAAGAGCTTAAAAAGCTCCTTGAACAATGGAACCTAGTAATAGGTTTCCTATTCCTTACATGGATTTGTCTTCTACAATTTGCCTATGCCAACAGGAATAGGTTTTTGTATATAATTAAGTTAATTTTCCTCTGGCTGTTATGGCCAGTAACTTTAGCTTGTTTTGTGCTTGCTGCTGTTTACAGAATAAATTGGATCACCGGTGGAATTGCTATCGCAATGGCTTGTCTTGTAGGCTTGATGTGGCTCAGCTACTTCATTGCTTCTTTCAGACTGTTTGCGCGTACGCGTTCCATGTGGTCATTCAATCCAGAAACTAACATTCTTCTCAACGTGCCACTCCATGGCACTATTCTGACCAGACCGCTTCTAGAAAGTGAACTCGTAATCGGAGCTGTGATCCTTCGTGGACATCTTCGTATTGCTGGACACCATCTAGGACGCTGTGACATCAAGGACCTGCCTAAAGAAATCACTGTTGCTACATCACGAACGCTTTCTTATTACAAATTGGGAGCTTCGCAGCGTGTAGCAGGTGACTCAGGTTTTGCTGCATACAGTCGCTACAGGATTGGCAACTATAAATTAAACACAGACCATTCCAGTAGCAGTGACAATATTGCTTTGCTTGTACAGTAAGTGACAACAGATGTTTCATCTCGTTGACTTTCAGGTTACTATAGCAGAGATATTACTAATTATTATGAGGACTTTTAAAGTTTCCATTTGGAATCTTGATTACATCATAAACCTCATAATTAAAAATTTATCTAAGTCACTAACTGAGAATAAATATTCTCAATTAGATGAAGAGCAACCAATGGAGATTGATTAAACGAACATGAAAATTATTCTTTTCTTGGCACTGATAACACTCGCTACTTGTGAGCTTTATCACTACCAAGAGTGTGTTAGAGGTACAACAGTACTTTTAAAAGAACCTTGCTCTTCTGGAACATACGAGGGCAATTCACCATTTCATCCTCTAGCTGATAACAAATTTGCACTGACTTGCTTTAGCACTCAATTTGCTTTTGCTTGTCCTGACGGCGTAAAACACGTCTATCAGTTACGTGCCAGATCAGTTTCACCTAAACTGTTCATCAGACAAGAGGAAGTTCAAGAACTTTACTCTCCAATTTTTCTTATTGTTGCGGCAATAGTGTTTATAACACTTTGCTTCACACTCAAAAGAAAGACAGAATGATTGAACTTTCATTAATTGACTTCTATTTGTGCTTTTTAGCCTTTCTGCTATTCCTTGTTTTAATTATGCTTATTATCTTTTGGTTCTCACTTGAACTGCAAGATCATAATGAAACTTGTCACGCCTAAACGAACATGAAATTTCTTGTTTTCTTAGGAATCATCACAACTGTAGCTGCATTTCACCAAGAATGTAGTTTACAGTCATGTACTCAACATCAACCATATGTAGTTGATGACCCGTGTCCTATTCACTTCTATTCTAAATGGTATATTAGAGTAGGAGCTAGAAAATCAGCACCTTTAATTGAATTGTGCGTGGATGAGGCTGGTTCTAAATCACCCATTCAGTACATCGATATCGGTAATTATACAGTTTCCTGTTTACCTTTTACAATTAATTGCCAGGAACCTAAATTGGGTAGTCTTGTAGTGCGTTGTTCGTTCTATGAAGACTTTTTAGAGTATCATGACGTTCGTGTTGTTTTAGATTTCATCTAAACGAACAAACTAAAATGTCTGATAATGGACCCCAAAATCAGCGAAATGCACCCCGCATTACGTTTGGTGGACCCTCAGATTCAACTGGCAGTAACCAGAATGGAGAACGCAGTGGGGCGCGATCAAAACAACGTCGGCCCCAAGGTTTACCCAATAATACTGCGTCTTGGTTCACCGCTCTCACTCAACATGGCAAGGAAGACCTTAAATTCCCTCGAGGACAAGGCGTTCCAATTAACACCAATAGCAGTCCAGATGACCAAATTGGCTACTACCGAAGAGCTACCAGACGAATTCGTGGTGGTGACGGTAAAATGAAAGATCTCAGTCCAAGATGGTATTTCTACTACCTAGGAACTGGGCCAGAAGCTGGACTTCCCTATGGTGCTAACAAAGACGGCATCATATGGGTTGCAACTGAGGGAGCCTTGAATACACCAAAAGATCACATTGGCACCCGCAATCCTGCTAACAATGCTGCAATCGTGCTACAACTTCCTCAAGGAACAACATTGCCAAAAGGCTTCTACGCAGAAGGGAGCAGAGGCGGCAGTCAAGCCTCTTCTCGTTCCTCATCACGTAGTCGCAACAGTTCAAGAAATTCAACTCCAGGCAGCAGTAGGGGAACTTCTCCTGCTAGAATGGCTGGCAATGGCGGTGATGCTGCTCTTGCTTTGCTGCTGCTTGACAGATTGAACCAGCTTGAGAGCAAAATGTCTGGTAAAGGCCAACAACAACAAGGCCAAACTGTCACTAAGAAATCTGCTGCTGAGGCTTCTAAGAAGCCTCGGCAAAAACGTACTGCCACTAAAGCATACAATGTAACACAAGCTTTCGGCAGACGTGGTCCAGAACAAACCCAAGGAAATTTTGGGGACCAGGAACTAATCAGACAAGGAACTGATTACAAACATTGGCCGCAAATTGCACAATTTGCCCCCAGCGCTTCAGCGTTCTTCGGAATGTCGCGCATTGGCATGGAAGTCACACCTTCGGGAACGTGGTTGACCTACACAGGTGCCATCAAATTGGATGACAAAGATCCAAATTTCAAAGATCAAGTCATTTTGCTGAATAAGCATATTGACGCATACAAAACATTCCCACCAACAGAGCCTAAAAAGGACAAAAAGAAGAAGGCTGATGAAACTCAAGCCTTACCGCAGAGACAGAAGAAACAGCAAACTGTGACTCTTCTTCCTGCTGCAGATTTGGATGATTTCTCCAAACAATTGCAACAATCCATGAGCAGTGCTGACTCAACTCAGGCCTAAACTCATGCAGACCACACAAGGCAGATGGGCTATATAAACGTTTTCGCTTTTCCGTTTACGATATATAGTCTACTCTTGTGCAGAATGAATTCTCGTAACTACATAGCACAAGTAGATGTAGTTAACTTTAATCTCACATAGCAATCTTTAATCAGTGTGTAACATTAGGGAGGACTTGAAAGAGCCACCACATTTTCACCGAGGCCACGCGGAGTACGATCGAGTGTACAGTGAACAATGCTAGGGAGAGCTGCCTATATGGAAGAGCCCTAATGTGTAAAATTAATTTTAGTAGTGCTATCCCCATGTGATTTTAATAGCTTCTTAGGAGAATGACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + } + ], + "genes": [ + { + "name": "E", + "sequence": "MYSFVSEETGTLIVNSVLLFLAFVVFLLVTLAILTALRLCAYCCNIVNVSLVKPSFYVYSRVKNLNSSRVPDLLV*" + }, + { + "name": "M", + "sequence": "MADSNGTITVEELKKLLEQWNLVIGFLFLTWICLLQFAYANRNRFLYIIKLIFLWLLWPVTLACFVLAAVYRINWITGGIAIAMACLVGLMWLSYFIASFRLFARTRSMWSFNPETNILLNVPLHGTILTRPLLESELVIGAVILRGHLRIAGHHLGRCDIKDLPKEITVATSRTLSYYKLGASQRVAGDSGFAAYSRYRIGNYKLNTDHSSSSDNIALLVQ*" + }, + { + "name": "N", + "sequence": "MSDNGPQNQRNAPRITFGGPSDSTGSNQNGERSGARSKQRRPQGLPNNTASWFTALTQHGKEDLKFPRGQGVPINTNSSPDDQIGYYRRATRRIRGGDGKMKDLSPRWYFYYLGTGPEAGLPYGANKDGIIWVATEGALNTPKDHIGTRNPANNAAIVLQLPQGTTLPKGFYAEGSRGGSQASSRSSSRSRNSSRNSTPGSSRGTSPARMAGNGGDAALALLLLDRLNQLESKMSGKGQQQQGQTVTKKSAAEASKKPRQKRTATKAYNVTQAFGRRGPEQTQGNFGDQELIRQGTDYKHWPQIAQFAPSASAFFGMSRIGMEVTPSGTWLTYTGAIKLDDKDPNFKDQVILLNKHIDAYKTFPPTEPKKDKKKKADETQALPQRQKKQQTVTLLPAADLDDFSKQLQQSMSSADSTQA*" + }, + { + "name": "ORF1a", + "sequence": "MESLVPGFNEKTHVQLSLPVLQVRDVLVRGFGDSVEEVLSEARQHLKDGTCGLVEVEKGVLPQLEQPYVFIKRSDARTAPHGHVMVELVAELEGIQYGRSGETLGVLVPHVGEIPVAYRKVLLRKNGNKGAGGHSYGADLKSFDLGDELGTDPYEDFQENWNTKHSSGVTRELMRELNGGAYTRYVDNNFCGPDGYPLECIKDLLARAGKASCTLSEQLDFIDTKRGVYCCREHEHEIAWYTERSEKSYELQTPFEIKLAKKFDTFNGECPNFVFPLNSIIKTIQPRVEKKKLDGFMGRIRSVYPVASPNECNQMCLSTLMKCDHCGETSWQTGDFVKATCEFCGTENLTKEGATTCGYLPQNAVVKIYCPACHNSEVGPEHSLAEYHNESGLKTILRKGGRTIAFGGCVFSYVGCHNKCAYWVPRASANIGCNHTGVVGEGSEGLNDNLLEILQKEKVNINIVGDFKLNEEIAIILASFSASTSAFVETVKGLDYKAFKQIVESCGNFKVTKGKAKKGAWNIGEQKSILSPLYAFASEAARVVRSIFSRTLETAQNSVRVLQKAAITILDGISQYSLRLIDAMMFTSDLATNNLVVMAYITGGVVQLTSQWLTNIFGTVYEKLKPVLDWLEEKFKEGVEFLRDGWEIVKFISTCACEIVGGQIVTCAKEIKESVQTFFKLVNKFLALCADSIIIGGAKLKALNLGETFVTHSKGLYRKCVKSREETGLLMPLKAPKEIIFLEGETLPTEVLTEEVVLKTGDLQPLEQPTSEAVEAPLVGTPVCINGLMLLEIKDTEKYCALAPNMMVTNNTFTLKGGAPTKVTFGDDTVIEVQGYKSVNITFELDERIDKVLNEKCSAYTVELGTEVNEFACVVADAVIKTLQPVSELLTPLGIDLDEWSMATYYLFDESGEFKLASHMYCSFYPPDEDEEEGDCEEEEFEPSTQYEYGTEDDYQGKPLEFGATSAALQPEEEQEEDWLDDDSQQTVGQQDGSEDNQTTTIQTIVEVQPQLEMELTPVVQTIEVNSFSGYLKLTDNVYIKNADIVEEAKKVKPTVVVNAANVYLKHGGGVAGALNKATNNAMQVESDDYIATNGPLKVGGSCVLSGHNLAKHCLHVVGPNVNKGEDIQLLKSAYENFNQHEVLLAPLLSAGIFGADPIHSLRVCVDTVRTNVYLAVFDKNLYDKLVSSFLEMKSEKQVEQKIAEIPKEEVKPFITESKPSVEQRKQDDKKIKACVEEVTTTLEETKFLTENLLLYIDINGNLHPDSATLVSDIDITFLKKDAPYIVGDVVQEGVLTAVVIPTKKAGGTTEMLAKALRKVPTDNYITTYPGQGLNGYTVEEAKTVLKKCKSAFYILPSIISNEKQEILGTVSWNLREMLAHAEETRKLMPVCVETKAIVSTIQRKYKGIKIQEGVVDYGARFYFYTSKTTVASLINTLNDLNETLVTMPLGYVTHGLNLEEAARYMRSLKVPATVSVSSPDAVTAYNGYLTSSSKTPEEHFIETISLAGSYKDWSYSGQSTQLGIEFLKRGDKSVYYTSNPTTFHLDGEVITFDNLKTLLSLREVRTIKVFTTVDNINLHTQVVDMSMTYGQQFGPTYLDGADVTKIKPHNSHEGKTFYVLPNDDTLRVEAFEYYHTTDPSFLGRYMSALNHTKKWKYPQVNGLTSIKWADNNCYLATALLTLQQIELKFNPPALQDAYYRARAGEAANFCALILAYCNKTVGELGDVRETMSYLFQHANLDSCKRVLNVVCKTCGQQQTTLKGVEAVMYMGTLSYEQFKKGVQIPCTCGKQATKYLVQQESPFVMMSAPPAQYELKHGTFTCASEYTGNYQCGHYKHITSKETLYCIDGALLTKSSEYKGPITDVFYKENSYTTTIKPVTYKLDGVVCTEIDPKLDNYYKKDNSYFTEQPIDLVPNQPYPNASFDNFKFVCDNIKFADDLNQLTGYKKPASRELKVTFFPDLNGDVVAIDYKHYTPSFKKGAKLLHKPIVWHVNNATNKATYKPNTWCIRCLWSTKPVETSNSFDVLKSEDAQGMDNLACEDLKPVSEEVVENPTIQKDVLECNVKTTEVVGDIILKPANNSLKITEEVGHTDLMAAYVDNSSLTIKKPNELSRVLGLKTLATHGLAAVNSVPWDTIANYAKPFLNKVVSTTTNIVTRCLNRVCTNYMPYFFTLLLQLCTFTRSTNSRIKASMPTTIAKNTVKSVGKFCLEASFNYLKSPNFSKLINIIIWFLLLSVCLGSLIYSTAALGVLMSNLGMPSYCTGYREGYLNSTNVTIATYCTGSIPCSVCLSGLDSLDTYPSLETIQITISSFKWDLTAFGLVAEWFLAYILFTRFFYVLGLAAIMQLFFSYFAVHFISNSWLMWLIINLVQMAPISAMVRMYIFFASFYYVWKSYVHVVDGCNSSTCMMCYKRNRATRVECTTIVNGVRRSFYVYANGGKGFCKLHNWNCVNCDTFCAGSTFISDEVARDLSLQFKRPINPTDQSSYIVDSVTVKNGSIHLYFDKAGQKTYERHSLSHFVNLDNLRANNTKGSLPINVIVFDGKSKCEESSAKSASVYYSQLMCQPILLLDQALVSDVGDSAEVAVKMFDAYVNTFSSTFNVPMEKLKTLVATAEAELAKNVSLDNVLSTFISAARQGFVDSDVETKDVVECLKLSHQSDIEVTGDSCNNYMLTYNKVENMTPRDLGACIDCSARHINAQVAKSHNIALIWNVKDFMSLSEQLRKQIRSAAKKNNLPFKLTCATTRQVVNVVTTKIALKGGKIVNNWLKQLIKVTLVFLFVAAIFYLITPVHVMSKHTDFSSEIIGYKAIDGGVTRDIASTDTCFANKHADFDTWFSQRGGSYTNDKACPLIAAVITREVGFVVPGLPGTILRTTNGDFLHFLPRVFSAVGNICYTPSKLIEYTDFATSACVLAAECTIFKDASGKPVPYCYDTNVLEGSVAYESLRPDTRYVLMDGSIIQFPNTYLEGSVRVVTTFDSEYCRHGTCERSEAGVCVSTSGRWVLNNDYYRSLPGVFCGVDAVNLLTNMFTPLIQPIGALDISASIVAGGIVAIVVTCLAYYFMRFRRAFGEYSHVVAFNTLLFLMSFTVLCLTPVYSFLPGVYSVIYLYLTFYLTNDVSFLAHIQWMVMFTPLVPFWITIAYIICISTKHFYWFFSNYLKRRVVFNGVSFSTFEEAALCTFLLNKEMYLKLRSDVLLPLTQYNRYLALYNKYKYFSGAMDTTSYREAACCHLAKALNDFSNSGSDVLYQPPQTSITSAVLQSGFRKMAFPSGKVEGCMVQVTCGTTTLNGLWLDDVVYCPRHVICTSEDMLNPNYEDLLIRKSNHNFLVQAGNVQLRVIGHSMQNCVLKLKVDTANPKTPKYKFVRIQPGQTFSVLACYNGSPSGVYQCAMRPNFTIKGSFLNGSCGSVGFNIDYDCVSFCYMHHMELPTGVHAGTDLEGNFYGPFVDRQTAQAAGTDTTITVNVLAWLYAAVINGDRWFLNRFTTTLNDFNLVAMKYNYEPLTQDHVDILGPLSAQTGIAVLDMCASLKELLQNGMNGRTILGSALLEDEFTPFDVVRQCSGVTFQSAVKRTIKGTHHWLLLTILTSLLVLVQSTQWSLFFFLYENAFLPFAMGIIAMSAFAMMFVKHKHAFLCLFLLPSLATVAYFNMVYMPASWVMRIMTWLDMVDTSLSGFKLKDCVMYASAVVLLILMTARTVYDDGARRVWTLMNVLTLVYKVYYGNALDQAISMWALIISVTSNYSGVVTTVMFLARGIVFMCVEYCPIFFITGNTLQCIMLVYCFLGYFCTCYFGLFCLLNRYFRLTLGVYDYLVSTQEFRYMNSQGLLPPKNSIDAFKLNIKLLGVGGKPCIKVATVQSKMSDVKCTSVVLLSVLQQLRVESSSKLWAQCVQLHNDILLAKDTTEAFEKMVSLLSVLLSMQGAVDINKLCEEMLDNRATLQAIASEFSSLPSYAAFATAQEAYEQAVANGDSEVVLKKLKKSLNVAKSEFDRDAAMQRKLEKMADQAMTQMYKQARSEDKRAKVTSAMQTMLFTMLRKLDNDALNNIINNARDGCVPLNIIPLTTAAKLMVVIPDYNTYKNTCDGTTFTYASALWEIQQVVDADSKIVQLSEISMDNSPNLAWPLIVTALRANSAVKLQNNELSPVALRQMSCAAGTTQTACTDDNALAYYNTTKGGRFVLALLSDLQDLKWARFPKSDGTGTIYTELEPPCRFVTDTPKGPKVKYLYFIKGLNNLNRGMVLGSLAATVRLQAGNATEVPANSTVLSFCAFAVDAAKAYKDYLASGGQPITNCVKMLCTHTGTGQAITVTPEANMDQESFGGASCCLYCRCHIDHPNPKGFCDLKGKYVQIPTTCANDPVGFTLKNTVCTVCGMWKGYGCSCDQLREPMLQSADAQSFLN" + }, + { + "name": "ORF1b", + "sequence": "RVCGVSAARLTPCGTGTSTDVVYRAFDIYNDKVAGFAKFLKTNCCRFQEKDEDDNLIDSYFVVKRHTFSNYQHEETIYNLLKDCPAVAKHDFFKFRIDGDMVPHISRQRLTKYTMADLVYALRHFDEGNCDTLKEILVTYNCCDDDYFNKKDWYDFVENPDILRVYANLGERVRQALLKTVQFCDAMRNAGIVGVLTLDNQDLNGNWYDFGDFIQTTPGSGVPVVDSYYSLLMPILTLTRALTAESHVDTDLTKPYIKWDLLKYDFTEERLKLFDRYFKYWDQTYHPNCVNCLDDRCILHCANFNVLFSTVFPPTSFGPLVRKIFVDGVPFVVSTGYHFRELGVVHNQDVNLHSSRLSFKELLVYAADPAMHAASGNLLLDKRTTCFSVAALTNNVAFQTVKPGNFNKDFYDFAVSKGFFKEGSSVELKHFFFAQDGNAAISDYDYYRYNLPTMCDIRQLLFVVEVVDKYFDCYDGGCINANQVIVNNLDKSAGFPFNKWGKARLYYDSMSYEDQDALFAYTKRNVIPTITQMNLKYAISAKNRARTVAGVSICSTMTNRQFHQKLLKSIAATRGATVVIGTSKFYGGWHNMLKTVYSDVENPHLMGWDYPKCDRAMPNMLRIMASLVLARKHTTCCSLSHRFYRLANECAQVLSEMVMCGGSLYVKPGGTSSGDATTAYANSVFNICQAVTANVNALLSTDGNKIADKYVRNLQHRLYECLYRNRDVDTDFVNEFYAYLRKHFSMMILSDDAVVCFNSTYASQGLVASIKNFKSVLYYQNNVFMSEAKCWTETDLTKGPHEFCSQHTMLVKQGDDYVYLPYPDPSRILGAGCFVDDIVKTDGTLMIERFVSLAIDAYPLTKHPNQEYADVFHLYLQYIRKLHDELTGHMLDMYSVMLTNDNTSRYWEPEFYEAMYTPHTVLQAVGACVLCNSQTSLRCGACIRRPFLCCKCCYDHVISTSHKLVLSVNPYVCNAPGCDVTDVTQLYLGGMSYYCKSHKPPISFPLCANGQVFGLYKNTCVGSDNVTDFNAIATCDWTNAGDYILANTCTERLKLFAAETLKATEETFKLSYGIATVREVLSDRELHLSWEVGKPRPPLNRNYVFTGYRVTKNSKVQIGEYTFEKGDYGDAVVYRGTTTYKLNVGDYFVLTSHTVMPLSAPTLVPQEHYVRITGLYPTLNISDEFSSNVANYQKVGMQKYSTLQGPPGTGKSHFAIGLALYYPSARIVYTACSHAAVDALCEKALKYLPIDKCSRIIPARARVECFDKFKVNSTLEQYVFCTVNALPETTADIVVFDEISMATNYDLSVVNARLRAKHYVYIGDPAQLPAPRTLLTKGTLEPEYFNSVCRLMKTIGPDMFLGTCRRCPAEIVDTVSALVYDNKLKAHKDKSAQCFKMFYKGVITHDVSSAINRPQIGVVREFLTRNPAWRKAVFISPYNSQNAVASKILGLPTQTVDSSQGSEYDYVIFTQTTETAHSCNVNRFNVAITRAKVGILCIMSDRDLYDKLQFTSLEIPRRNVATLQAENVTGLFKDCSKVITGLHPTQAPTHLSVDTKFKTEGLCVDIPGIPKDMTYRRLISMMGFKMNYQVNGYPNMFITREEAIRHVRAWIGFDVEGCHATREAVGTNLPLQLGFSTGVNLVAVPTGYVDTPNNTDFSRVSAKPPPGDQFKHLIPLMYKGLPWNVVRIKIVQMLSDTLKNLSDRVVFVLWAHGFELTSMKYFVKIGPERTCCLCDRRATCFSTASDTYACWHHSIGFDYVYNPFMIDVQQWGFTGNLQSNHDLYCQVHGNAHVASCDAIMTRCLAVHECFVKRVDWTIEYPIIGDELKINAACRKVQHMVVKAALLADKFPVLHDIGNPKAIKCVPQADVEWKFYDAQPCSDKAYKIEELFYSYATHSDKFTDGVCLFWNCNVDRYPANSIVCRFDTRVLSNLNLPGCDGGSLYVNKHAFHTPAFDKSAFVNLKQLPFFYYSDSPCESHGKQVVSDIDYVPLKSATCITRCNLGGAVCRHHANEYRLYLDAYNMMISAGFSLWVYKQFDTYNLWNTFTRLQSLENVAFNVVNKGHFDGQQGEVPVSIINNTVYTKVDGVDVELFENKTTLPVNVAFELWAKRNIKPVPEVKILNNLGVDIAANTVIWDYKRDAPAHISTIGVCSMTDIAKKPTETICAPLTVFFDGRVDGQVDLFRNARNGVLITEGSVKGLQPSVGPKQASLNGVTLIGEAVKTQFNYYKKVDGVVQQLPETYFTQSRNLQEFKPRSQMEIDFLELAMDEFIERYKLEGYAFEHIVYGDFSHSQLGGLHLLIGLAKRFKESPFELEDFIPMDSTVKNYFITDAQTGSSKCVCSVIDLLLDDFVEIIKSQDLSVVSKVVKVTIDYTEISFMLWCKDGHVETFYPKLQSSQAWQPGVAMPNLYKMQRMLLEKCDLQNYGDSATLPKGIMMNVAKYTQLCQYLNTLTLAVPYNMRVIHFGAGSDKGVAPGTAVLRQWLPTGTLLVDSDLNDFVSDADSTLIGDCATVHTANKWDLIISDMYDPKTKNVTKENDSKEGFFTYICGFIQQKLALGGSVAIKITEHSWNADLYKLMGHFAWWTAFVTNVNASSSEAFLIGCNYLGKPREQIDGYVMHANYIFWRNTNPIQLSSYSLFDMSKFPLKLRGTAVMSLKEGQINDMILSLLSKGRLIIRENNRVVISSDVLVNN*" + }, + { + "name": "ORF3a", + "sequence": "MDLFMRIFTIGTVTLKQGEIKDATPSDFVRATATIPIQASLPFGWLIVGVALLAVFQSASKIITLKKRWQLALSKGVHFVCNLLLLFVTVYSHLLLVAAGLEAPFLYLYALVYFLQSINFVRIIMRLWLCWKCRSKNPLLYDANYFLCWHTNCYDYCIPYNSVTSSIVITSGDGTTSPISEHDYQIGGYTEKWESGVKDCVVLHSYFTSDYYQLYSTQLSTDTGVEHVTFFIYNKIVDEPEEHVQIHTIDGSSGVVNPVMEPIYDEPTTTTSVPL*" + }, + { + "name": "ORF6", + "sequence": "MFHLVDFQVTIAEILLIIMRTFKVSIWNLDYIINLIIKNLSKSLTENKYSQLDEEQPMEID*" + }, + { + "name": "ORF7a", + "sequence": "MKIILFLALITLATCELYHYQECVRGTTVLLKEPCSSGTYEGNSPFHPLADNKFALTCFSTQFAFACPDGVKHVYQLRARSVSPKLFIRQEEVQELYSPIFLIVAAIVFITLCFTLKRKTE*" + }, + { + "name": "ORF7b", + "sequence": "MIELSLIDFYLCFLAFLLFLVLIMLIIFWFSLELQDHNETCHA*" + }, + { + "name": "ORF8", + "sequence": "MKFLVFLGIITTVAAFHQECSLQSCTQHQPYVVDDPCPIHFYSKWYIRVGARKSAPLIELCVDEAGSKSPIQYIDIGNYTVSCLPFTINCQEPKLGSLVVRCSFYEDFLEYHDVRVVLDFI*" + }, + { + "name": "ORF9b", + "sequence": "MDPKISEMHPALRLVDPQIQLAVTRMENAVGRDQNNVGPKVYPIILRLGSPLSLNMARKTLNSLEDKAFQLTPIAVQMTKLATTEELPDEFVVVTVK*" + }, + { + "name": "S", + "sequence": "MFVFLVLLPLVSSQCVNLTTRTQLPPAYTNSFTRGVYYPDKVFRSSVLHSTQDLFLPFFSNVTWFHAIHVSGTNGTKRFDNPVLPFNDGVYFASTEKSNIIRGWIFGTTLDSKTQSLLIVNNATNVVIKVCEFQFCNDPFLGVYYHKNNKSWMESEFRVYSSANNCTFEYVSQPFLMDLEGKQGNFKNLREFVFKNIDGYFKIYSKHTPINLVRDLPQGFSALEPLVDLPIGINITRFQTLLALHRSYLTPGDSSSGWTAGAAAYYVGYLQPRTFLLKYNENGTITDAVDCALDPLSETKCTLKSFTVEKGIYQTSNFRVQPTESIVRFPNITNLCPFGEVFNATRFASVYAWNRKRISNCVADYSVLYNSASFSTFKCYGVSPTKLNDLCFTNVYADSFVIRGDEVRQIAPGQTGKIADYNYKLPDDFTGCVIAWNSNNLDSKVGGNYNYLYRLFRKSNLKPFERDISTEIYQAGSTPCNGVEGFNCYFPLQSYGFQPTNGVGYQPYRVVVLSFELLHAPATVCGPKKSTNLVKNKCVNFNFNGLTGTGVLTESNKKFLPFQQFGRDIADTTDAVRDPQTLEILDITPCSFGGVSVITPGTNTSNQVAVLYQDVNCTEVPVAIHADQLTPTWRVYSTGSNVFQTRAGCLIGAEHVNNSYECDIPIGAGICASYQTQTNSPRRARSVASQSIIAYTMSLGAENSVAYSNNSIAIPTNFTISVTTEILPVSMTKTSVDCTMYICGDSTECSNLLLQYGSFCTQLNRALTGIAVEQDKNTQEVFAQVKQIYKTPPIKDFGGFNFSQILPDPSKPSKRSFIEDLLFNKVTLADAGFIKQYGDCLGDIAARDLICAQKFNGLTVLPPLLTDEMIAQYTSALLAGTITSGWTFGAGAALQIPFAMQMAYRFNGIGVTQNVLYENQKLIANQFNSAIGKIQDSLSSTASALGKLQDVVNQNAQALNTLVKQLSSNFGAISSVLNDILSRLDKVEAEVQIDRLITGRLQSLQTYVTQQLIRAAEIRASANLAATKMSECVLGQSKRVDFCGKGYHLMSFPQSAPHGVVFLHVTYVPAQEKNFTTAPAICHDGKAHFPREGVFVSNGTHWFVTQRNFYEPQIITTDNTFVSGNCDVVIGIVNNTVYDPLQPELDSFKEELDKYFKNHTSPDVDLGDISGINASVVNIQKEIDRLNEVAKNLNESLIDLQELGKYEQYIKWPWYIWLGFIAGLIAIVMVTIMLCCMTSCCSCLKGCCSCGSCCKFDEDDSEPVLKGVKLHYT*" + } + ] +} diff --git a/lapis2/src/test/resources/config/testDatabaseConfig.yaml b/lapis2/src/test/resources/config/testDatabaseConfig.yaml index 7cd74046..aa56983f 100644 --- a/lapis2/src/test/resources/config/testDatabaseConfig.yaml +++ b/lapis2/src/test/resources/config/testDatabaseConfig.yaml @@ -18,5 +18,4 @@ schema: type: aaInsertion features: - name: sarsCoV2VariantQuery - - name: isSingleSegmentedSequence primaryKey: gisaid_epi_isl diff --git a/siloLapisTests/test/aggregated.spec.ts b/siloLapisTests/test/aggregated.spec.ts index 6491d0e1..4742e0eb 100644 --- a/siloLapisTests/test/aggregated.spec.ts +++ b/siloLapisTests/test/aggregated.spec.ts @@ -1,8 +1,8 @@ import { expect } from 'chai'; -import { lapisClient, basePath } from './common'; +import { basePath, lapisClient } from './common'; import fs from 'fs'; -import { AggregatedPostRequest } from './lapisClient/models/AggregatedPostRequest'; -import { AggregatedResponse } from './lapisClient/models/AggregatedResponse'; +import { AggregatedPostRequest } from './lapisClient'; +import { AggregatedResponse } from './lapisClient'; const queriesPath = __dirname + '/aggregatedQueries'; const aggregatedQueryFiles = fs.readdirSync(queriesPath); diff --git a/siloLapisTests/test/alignedNucleotideSequence.spec.ts b/siloLapisTests/test/alignedNucleotideSequence.spec.ts new file mode 100644 index 00000000..0ae9052b --- /dev/null +++ b/siloLapisTests/test/alignedNucleotideSequence.spec.ts @@ -0,0 +1,112 @@ +import { expect } from 'chai'; +import { basePath, lapisSingleSegmentedSequenceController, sequenceData } from './common'; + +describe('The /alignedNucleotideSequence endpoint', () => { + it('should return aligned nucleotide sequences for Switzerland', async () => { + const result = await lapisSingleSegmentedSequenceController.postAlignedNucleotideSequence({ + sequenceRequest: { country: 'Switzerland' }, + }); + + const { primaryKeys, sequences } = sequenceData(result); + + expect(primaryKeys).to.have.length(100); + expect(sequences).to.have.length(100); + expect(primaryKeys[0]).to.equal('>EPI_ISL_3247294'); + expect(sequences[0]).to.have.length(29903); + }); + + it('should order ascending by specified fields', async () => { + const result = await lapisSingleSegmentedSequenceController.postAlignedNucleotideSequence({ + sequenceRequest: { orderBy: [{ field: 'gisaid_epi_isl', type: 'ascending' }] }, + }); + + const { primaryKeys, sequences } = sequenceData(result); + + expect(primaryKeys).to.have.length(100); + expect(sequences).to.have.length(100); + expect(primaryKeys[0]).to.equal('>EPI_ISL_1001493'); + expect(sequences[0]).to.have.length(29903); + }); + + it('should order descending by specified fields', async () => { + const result = await lapisSingleSegmentedSequenceController.postAlignedNucleotideSequence({ + sequenceRequest: { orderBy: [{ field: 'gisaid_epi_isl', type: 'descending' }] }, + }); + + const { primaryKeys, sequences } = sequenceData(result); + + expect(primaryKeys).to.have.length(100); + expect(sequences).to.have.length(100); + expect(primaryKeys[0]).to.equal('>EPI_ISL_931279'); + expect(sequences[0]).to.have.length(29903); + }); + + it('should apply limit and offset', async () => { + const resultWithLimit = await lapisSingleSegmentedSequenceController.postAlignedNucleotideSequence({ + sequenceRequest: { + orderBy: [{ field: 'gisaid_epi_isl', type: 'ascending' }], + limit: 2, + }, + }); + + const { primaryKeys: primaryKeysWithLimit, sequences: sequencesWithLimit } = + sequenceData(resultWithLimit); + + expect(primaryKeysWithLimit).to.have.length(2); + expect(sequencesWithLimit).to.have.length(2); + expect(primaryKeysWithLimit[0]).to.equal('>EPI_ISL_1001493'); + expect(sequencesWithLimit[0]).to.have.length(29903); + + const resultWithLimitAndOffset = + await lapisSingleSegmentedSequenceController.postAlignedNucleotideSequence({ + sequenceRequest: { + orderBy: [{ field: 'gisaid_epi_isl', type: 'ascending' }], + limit: 2, + offset: 1, + }, + }); + + const { primaryKeys: primaryKeysWithLimitAndOffset, sequences: sequencesWithLimitAndOffset } = + sequenceData(resultWithLimitAndOffset); + + expect(primaryKeysWithLimitAndOffset).to.have.length(2); + expect(sequencesWithLimitAndOffset).to.have.length(2); + expect(primaryKeysWithLimitAndOffset[0]).to.equal(primaryKeysWithLimit[1]); + expect(sequencesWithLimitAndOffset[0]).to.equal(sequencesWithLimit[1]); + }); + + it('should correctly handle nucleotide insertion requests', async () => { + const result = await lapisSingleSegmentedSequenceController.postAlignedNucleotideSequence({ + sequenceRequest: { + nucleotideInsertions: ['ins_25701:CC?', 'ins_5959:?AT'], + }, + }); + + const { primaryKeys, sequences } = sequenceData(result); + + expect(primaryKeys).to.have.length(1); + expect(sequences).to.have.length(1); + expect(primaryKeys[0]).to.equal('>EPI_ISL_3578231'); + }); + + it('should correctly handle amino acid insertion requests', async () => { + const result = await lapisSingleSegmentedSequenceController.postAlignedNucleotideSequence({ + sequenceRequest: { + aminoAcidInsertions: ['ins_S:143:T', 'ins_ORF1a:3602:F?P'], + }, + }); + + const { primaryKeys, sequences } = sequenceData(result); + + expect(primaryKeys).to.have.length(1); + expect(sequences).to.have.length(1); + expect(primaryKeys[0]).to.equal('>EPI_ISL_3259931'); + }); + + it('should return the lapis data version in the response', async () => { + const result = await fetch(basePath + '/alignedNucleotideSequences'); + + expect(result.status).equals(200); + expect(result.headers.get('lapis-data-version')).to.match(/\d{10}/); + }); +}); diff --git a/siloLapisTests/test/aminoAcidSequence.spec.ts b/siloLapisTests/test/aminoAcidSequence.spec.ts new file mode 100644 index 00000000..2f2d0739 --- /dev/null +++ b/siloLapisTests/test/aminoAcidSequence.spec.ts @@ -0,0 +1,119 @@ +import { expect } from 'chai'; +import { basePath, lapisClient, sequenceData } from './common'; + +describe('The /aminoAcidSequence endpoint', () => { + it('should return amino acid sequences for Switzerland', async () => { + const result = await lapisClient.postAminoAcidSequence({ + gene: 'S', + sequenceRequest: { country: 'Switzerland' }, + }); + + const { primaryKeys, sequences } = sequenceData(result); + + expect(primaryKeys).to.have.length(100); + expect(sequences).to.have.length(100); + expect(primaryKeys[0]).to.equal('>EPI_ISL_3247294'); + expect(sequences[0]).to.have.length(1274); + }); + + it('should order ascending by specified fields', async () => { + const result = await lapisClient.postAminoAcidSequence({ + gene: 'S', + sequenceRequest: { orderBy: [{ field: 'gisaid_epi_isl', type: 'ascending' }] }, + }); + + const { primaryKeys, sequences } = sequenceData(result); + + expect(primaryKeys).to.have.length(100); + expect(sequences).to.have.length(100); + expect(primaryKeys[0]).to.equal('>EPI_ISL_1001493'); + expect(sequences[0]).to.have.length(1274); + }); + + it('should order descending by specified fields', async () => { + const result = await lapisClient.postAminoAcidSequence({ + gene: 'S', + sequenceRequest: { orderBy: [{ field: 'gisaid_epi_isl', type: 'descending' }] }, + }); + + const { primaryKeys, sequences } = sequenceData(result); + + expect(primaryKeys).to.have.length(100); + expect(sequences).to.have.length(100); + expect(primaryKeys[0]).to.equal('>EPI_ISL_931279'); + expect(sequences[0]).to.have.length(1274); + }); + + it('should apply limit and offset', async () => { + const resultWithLimit = await lapisClient.postAminoAcidSequence({ + gene: 'S', + sequenceRequest: { + orderBy: [{ field: 'gisaid_epi_isl', type: 'ascending' }], + limit: 2, + }, + }); + + const { primaryKeys: primaryKeysWithLimit, sequences: sequencesWithLimit } = + sequenceData(resultWithLimit); + + expect(primaryKeysWithLimit).to.have.length(2); + expect(sequencesWithLimit).to.have.length(2); + expect(primaryKeysWithLimit[0]).to.equal('>EPI_ISL_1001493'); + expect(sequencesWithLimit[0]).to.have.length(1274); + + const resultWithLimitAndOffset = await lapisClient.postAminoAcidSequence({ + gene: 'S', + sequenceRequest: { + orderBy: [{ field: 'gisaid_epi_isl', type: 'ascending' }], + limit: 2, + offset: 1, + }, + }); + + const { primaryKeys: primaryKeysWithLimitAndOffset, sequences: sequencesWithLimitAndOffset } = + sequenceData(resultWithLimitAndOffset); + + expect(primaryKeysWithLimitAndOffset).to.have.length(2); + expect(sequencesWithLimitAndOffset).to.have.length(2); + expect(primaryKeysWithLimitAndOffset[0]).to.equal(primaryKeysWithLimit[1]); + expect(sequencesWithLimitAndOffset[0]).to.equal(sequencesWithLimit[1]); + }); + + it('should correctly handle nucleotide insertion requests', async () => { + const result = await lapisClient.postAminoAcidSequence({ + gene: 'S', + sequenceRequest: { + nucleotideInsertions: ['ins_25701:CC?', 'ins_5959:?AT'], + }, + }); + + const { primaryKeys, sequences } = sequenceData(result); + + expect(primaryKeys).to.have.length(1); + expect(sequences).to.have.length(1); + expect(primaryKeys[0]).to.equal('>EPI_ISL_3578231'); + expect(sequences[0]).to.have.length(1274); + }); + + it('should correctly handle amino acid insertion requests', async () => { + const result = await lapisClient.postAminoAcidSequence({ + gene: 'S', + sequenceRequest: { + aminoAcidInsertions: ['ins_S:143:T', 'ins_ORF1a:3602:F?P'], + }, + }); + + const { primaryKeys, sequences } = sequenceData(result); + + expect(primaryKeys).to.have.length(1); + expect(sequences).to.have.length(1); + expect(primaryKeys[0]).to.equal('>EPI_ISL_3259931'); + }); + + it('should return the lapis data version in the response', async () => { + const result = await fetch(basePath + '/aminoAcidSequences/S'); + + expect(result.status).equals(200); + expect(result.headers.get('lapis-data-version')).to.match(/\d{10}/); + }); +}); diff --git a/siloLapisTests/test/common.ts b/siloLapisTests/test/common.ts index dd492b25..9e33583c 100644 --- a/siloLapisTests/test/common.ts +++ b/siloLapisTests/test/common.ts @@ -1,8 +1,13 @@ -import { Configuration, LapisControllerApi } from './lapisClient'; +import { + Configuration, + LapisControllerApi, + Middleware, + SingleSegmentedSequenceControllerApi, +} from './lapisClient'; export const basePath = 'http://localhost:8080'; -export const lapisClient = new LapisControllerApi(new Configuration({ basePath })).withMiddleware({ +const middleware: Middleware = { onError: errorContext => { if (errorContext.response) { console.log('Response status code: ', errorContext.response.status); @@ -22,4 +27,21 @@ export const lapisClient = new LapisControllerApi(new Configuration({ basePath } } return Promise.resolve(responseContext.response); }, -}); +}; + +export const lapisClient = new LapisControllerApi(new Configuration({ basePath })).withMiddleware(middleware); + +export const lapisSingleSegmentedSequenceController = new SingleSegmentedSequenceControllerApi( + new Configuration({ basePath }) +).withMiddleware(middleware); + +export function sequenceData(serverResponse: string) { + const lines = serverResponse.split('\n'); + const primaryKeys = lines.filter(line => line.startsWith('>')); + const sequences = lines.filter(line => !line.startsWith('>')); + + return { + primaryKeys, + sequences, + }; +} diff --git a/siloLapisTests/testData/testDatabaseConfig.yaml b/siloLapisTests/testData/testDatabaseConfig.yaml index c9729d21..dd23ce92 100644 --- a/siloLapisTests/testData/testDatabaseConfig.yaml +++ b/siloLapisTests/testData/testDatabaseConfig.yaml @@ -27,7 +27,6 @@ schema: type: aaInsertion features: - name: sarsCoV2VariantQuery - - name: isSingleSegmentedSequence primaryKey: gisaid_epi_isl dateToSortBy: date partitionBy: pango_lineage