Skip to content

Commit

Permalink
Add ability to define custom aliases for refSeq names (#419)
Browse files Browse the repository at this point in the history
* refname alias UI

* refname alias UI

* refname alias UI

* refname alias UI

* cleanup

* refname alias cli

* update alias on client session

* update alias on client session

* refname alias editable row

* Standardize AddRefSeqAliasesChange

---------

Co-authored-by: Garrett Stevens <stevens.garrett.j@gmail.com>
  • Loading branch information
shashankbrgowda and garrettjstevens authored Aug 20, 2024
1 parent 3c101e1 commit 59969bf
Show file tree
Hide file tree
Showing 13 changed files with 565 additions and 12 deletions.
33 changes: 32 additions & 1 deletion packages/apollo-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ $ npm install -g @apollo-annotation/cli
$ apollo COMMAND
running command...
$ apollo (--version)
@apollo-annotation/cli/0.1.18 linux-x64 node-v20.14.0
@apollo-annotation/cli/0.1.18 darwin-x64 node-v20.10.0
$ apollo --help [COMMAND]
USAGE
$ apollo COMMAND
Expand Down Expand Up @@ -52,6 +52,7 @@ USAGE
- [`apollo help [COMMANDS]`](#apollo-help-commands)
- [`apollo login`](#apollo-login)
- [`apollo logout`](#apollo-logout)
- [`apollo refseq add-alias`](#apollo-refseq-add-alias)
- [`apollo refseq get`](#apollo-refseq-get)
- [`apollo status`](#apollo-status)
- [`apollo user get`](#apollo-user-get)
Expand Down Expand Up @@ -835,6 +836,36 @@ EXAMPLES
_See code:
[src/commands/logout.ts](https://github.com/GMOD/Apollo3/blob/v0.1.18/packages/apollo-cli/src/commands/logout.ts)_

## `apollo refseq add-alias`

Add reference name aliases from a file

```
USAGE
$ apollo refseq add-alias -i <value> -a <value> [--profile <value>] [--config-file <value>]
FLAGS
-a, --assembly=<value> (required) Name for this assembly.
-i, --input-file=<value> (required) Input refname alias file
--config-file=<value> Use this config file (mostly for testing)
--profile=<value> Use credentials from this profile
DESCRIPTION
Add reference name aliases from a file
Reference name aliasing is a process to make chromosomes that are named slightly
differently but which refer to the same thing render properly. This command
reads a file with reference name aliases and adds them to the database.
EXAMPLES
Add reference name aliases:
$ apollo refseq add-alias -i alias.txt -a myAssembly
```

_See code:
[src/commands/refseq/add-alias.ts](https://github.com/GMOD/Apollo3/blob/v0.1.18/packages/apollo-cli/src/commands/refseq/add-alias.ts)_

## `apollo refseq get`

Get reference sequences
Expand Down
102 changes: 102 additions & 0 deletions packages/apollo-cli/src/commands/refseq/add-alias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as fs from 'node:fs'
import { Agent, RequestInit, Response, fetch } from 'undici'
import { Flags } from '@oclif/core'
import { BaseCommand } from '../../baseCommand.js'
import {
createFetchErrorMessage,
localhostToAddress,
queryApollo,
wrapLines,
} from '../../utils.js'
import { ConfigError } from '../../ApolloConf.js'

export default class AddRefNameAlias extends BaseCommand<
typeof AddRefNameAlias
> {
static summary = 'Add reference name aliases from a file'
static description = wrapLines(
'Reference name aliasing is a process to make chromosomes that are named slightly differently but which refer to the same thing render properly. This command reads a file with reference name aliases and adds them to the database.',
)

static examples = [
{
description: 'Add reference name aliases:',
command: '<%= config.bin %> <%= command.id %> -i alias.txt -a myAssembly',
},
]

static flags = {
'input-file': Flags.string({
char: 'i',
description: 'Input refname alias file',
required: true,
}),
assembly: Flags.string({
char: 'a',
description: 'Name for this assembly.',
required: true,
}),
}

async run(): Promise<void> {
const { flags } = await this.parse(AddRefNameAlias)

if (!fs.existsSync(flags['input-file'])) {
this.error(`File ${flags['input-file']} does not exist`)
}

const access: { address: string; accessToken: string } =
await this.getAccess(flags['config-file'], flags.profile)
const filehandle = await fs.promises.open(flags['input-file'])
const fileContent = await filehandle.readFile({ encoding: 'utf8' })
await filehandle.close()
const lines = fileContent.split('\n')

const refNameAliases = []
for (const line of lines) {
const [refName, ...aliases] = line.split('\t')
refNameAliases.push({ refName, aliases })
}

const assemblies: Response = await queryApollo(
access.address,
access.accessToken,
'assemblies',
)
const json = (await assemblies.json()) as object[]
const assembly = json.find((x) => 'name' in x && x.name === flags.assembly)
const assemblyId = assembly && '_id' in assembly ? assembly._id : undefined

if (!assemblyId) {
this.error(`Assembly ${flags.assembly} not found`)
}

const change = {
typeName: 'AddRefSeqAliasesChange',
assembly: assemblyId,
refSeqAliases: refNameAliases,
}

const auth: RequestInit = {
method: 'POST',
body: JSON.stringify(change),
headers: {
Authorization: `Bearer ${access.accessToken}`,
'Content-Type': 'application/json',
},
dispatcher: new Agent({ headersTimeout: 60 * 60 * 1000 }),
}
const url = new URL(localhostToAddress(`${access.address}/changes`))
const response = await fetch(url, auth)
if (!response.ok) {
const errorMessage = await createFetchErrorMessage(
response,
'Failed to add reference name aliases',
)
throw new ConfigError(errorMessage)
}
this.log(
`Reference name aliases added successfully to assembly ${flags.assembly}`,
)
}
}
22 changes: 22 additions & 0 deletions packages/apollo-cli/test/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
./test/test.py TestCLI.testAddAssemblyFromGff # Run only this test
"""

import argparse
import json
import os
import sys
Expand Down Expand Up @@ -910,6 +911,27 @@ def testInvalidAccess(self):
self.assertEqual(1, p.returncode)
self.assertTrue('Profile "foo" does not exist' in p.stderr)

def testRefNameAliasConfiguration(self):
shell(f"{apollo} assembly add-gff {P} -i test_data/tiny.fasta.gff3 -a asm1 -f")

p = shell(f"{apollo} assembly get {P} -a asm1")
self.assertTrue("asm1" in p.stdout)
self.assertTrue("asm2" not in p.stdout)
asm_id = json.loads(p.stdout)[0]["_id"]

p = shell(f"{apollo} refseq add-alias {P} -i test_data/alias.txt -a asm2", strict=False)
self.assertTrue("Assembly asm2 not found" in p.stderr)

p = shell(f"{apollo} refseq add-alias {P} -i test_data/alias.txt -a asm1", strict=False)
self.assertTrue("Reference name aliases added successfully to assembly asm1" in p.stdout)

p = shell(f"{apollo} refseq get {P}")
refseq = json.loads(p.stdout.strip())
vv1ref = [x for x in refseq if x["assembly"] == asm_id]
refname_aliases = {x["name"]: x["aliases"] for x in vv1ref}
self.assertTrue(all(alias in refname_aliases.get("ctgA", []) for alias in ["ctga", "CTGA"]))
self.assertTrue(all(alias in refname_aliases.get("ctgB", []) for alias in ["ctgb", "CTGB"]))
self.assertTrue(all(alias in refname_aliases.get("ctgC", []) for alias in ["ctgc", "CTGC"]))

if __name__ == "__main__":
unittest.main()
3 changes: 3 additions & 0 deletions packages/apollo-cli/test_data/alias.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ctgA ctga CTGA
ctgB ctgb CTGB
ctgC ctgc CTGC
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,8 @@ export class JBrowseService {
assembly: assemblyId,
})
const ids: Record<string, string> = {}
const refNameAliasesFeatures = refSeqs.map((refSeq) => {
const refSeqId = (refSeq._id as Types.ObjectId).toHexString()
ids[refSeq.name] = refSeqId
return {
refName: refSeq.name,
aliases: [refSeqId],
uniqueId: `alias-${refSeqId}`,
}
refSeqs.map((refSeq) => {
ids[refSeq.name] = (refSeq._id as Types.ObjectId).toHexString()
})
this.logger.debug(`generating assembly ${assemblyId}`)
return {
Expand All @@ -137,8 +131,9 @@ export class JBrowseService {
},
refNameAliases: {
adapter: {
type: 'FromConfigAdapter',
features: refNameAliasesFeatures,
type: 'ApolloRefNameAliasAdapter',
assemblyId,
baseURL: { uri: url, locationType: 'UriLocation' },
},
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function getRequiredRoleForChange(changeName: string) {
'AddAssemblyFromFileChange',
'AddAssemblyAndFeaturesFromFileChange',
'AddFeaturesFromFileChange',
'AddRefSeqAliasesChange',
'DeleteAssemblyChange',
'UserChange',
'DeleteUserChange',
Expand Down
3 changes: 3 additions & 0 deletions packages/apollo-schemas/src/refSeq.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export class RefSeq {
@Prop()
description: string

@Prop()
aliases: string[]

@Prop({ required: true })
length: number

Expand Down
88 changes: 88 additions & 0 deletions packages/apollo-shared/src/Changes/AddRefSeqAliasesChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
AssemblySpecificChange,
Change,
ChangeOptions,
ClientDataStore,
LocalGFF3DataStore,
SerializedAssemblySpecificChange,
ServerDataStore,
} from '@apollo-annotation/common'
import { getSession } from '@jbrowse/core/util'

export interface SerializedRefSeqAliases {
refName: string
aliases: string[]
}

export interface SerializedRefSeqAliasesChange
extends SerializedAssemblySpecificChange {
typeName: 'AddRefSeqAliasesChange'
refSeqAliases: SerializedRefSeqAliases[]
}

export class AddRefSeqAliasesChange extends AssemblySpecificChange {
typeName = 'AddRefSeqAliasesChange' as const
refSeqAliases: SerializedRefSeqAliases[]

constructor(json: SerializedRefSeqAliasesChange, options?: ChangeOptions) {
super(json, options)
this.refSeqAliases = json.refSeqAliases
}

executeOnClient(clientDataStore: ClientDataStore) {
const { assemblyManager } = getSession(clientDataStore)
const assembly = assemblyManager.get(this.assembly)
if (!assembly) {
throw new Error(`assembly ${this.assembly} not found`)
}
const sessionAliases = assembly.refNameAliases
const sessionLCAliases = assembly.lowerCaseRefNameAliases

if (!sessionAliases || !sessionLCAliases) {
throw new Error('Session refNameAliases not found in assembly')
}

for (const refSeqAlias of this.refSeqAliases) {
const { aliases, refName } = refSeqAlias
for (const alias of aliases) {
sessionAliases[alias] = refName
sessionLCAliases[alias.toLowerCase()] = refName
}
}
assembly.setRefNameAliases(sessionAliases, sessionLCAliases)
return Promise.resolve()
}

getInverse(): Change {
throw new Error('Method not implemented.')
}

toJSON(): SerializedRefSeqAliasesChange {
const { assembly, refSeqAliases, typeName } = this
return { assembly, typeName, refSeqAliases }
}

async executeOnServer(backend: ServerDataStore) {
const { refSeqModel, session } = backend
const { assembly, logger, refSeqAliases } = this

for (const refSeqAlias of refSeqAliases) {
logger.debug?.(
`Updating Refname alias for assembly: ${assembly}, refSeqAlias: ${JSON.stringify(refSeqAlias)}`,
)
const { aliases, refName } = refSeqAlias
await refSeqModel
.updateOne({ assembly, name: refName }, { $set: { aliases } })
.session(session)
}
}

executeOnLocalGFF3(_backend: LocalGFF3DataStore): Promise<unknown> {
throw new Error('Method not implemented.')
}

// eslint-disable-next-line @typescript-eslint/class-literal-property-style
get notification(): string {
return 'RefSeq aliases have been added.'
}
}
3 changes: 3 additions & 0 deletions packages/apollo-shared/src/Changes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AddAssemblyFromExternalChange } from './AddAssemblyFromExternalChange'
import { AddAssemblyFromFileChange } from './AddAssemblyFromFileChange'
import { AddFeatureChange } from './AddFeatureChange'
import { AddFeaturesFromFileChange } from './AddFeaturesFromFileChange'
import { AddRefSeqAliasesChange } from './AddRefSeqAliasesChange'
import { DeleteAssemblyChange } from './DeleteAssemblyChange'
import { DeleteFeatureChange } from './DeleteFeatureChange'
import { DeleteUserChange } from './DeleteUserChange'
Expand Down Expand Up @@ -30,6 +31,7 @@ export const changes = {
StrandChange,
TypeChange,
UserChange,
AddRefSeqAliasesChange,
}

export * from './AddAssemblyAndFeaturesFromFileChange'
Expand All @@ -47,3 +49,4 @@ export * from './LocationStartChange'
export * from './StrandChange'
export * from './TypeChange'
export * from './UserChange'
export * from './AddRefSeqAliasesChange'
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AbstractMenuManager, AbstractSessionModel } from '@jbrowse/core/util'

import {
AddAssembly,
AddRefSeqAliases,
DeleteAssembly,
ImportFeatures,
ManageUsers,
Expand Down Expand Up @@ -60,6 +61,23 @@ export function addMenuItems(rootModel: AbstractMenuManager) {
)
},
})
rootModel.appendToMenu('Apollo', {
label: 'Add reference sequence aliases',
onClick: (session: ApolloSessionModel) => {
;(session as unknown as AbstractSessionModel).queueDialog(
(doneCallback) => [
AddRefSeqAliases,
{
session,
handleClose: () => {
doneCallback()
},
changeManager: session.apolloDataStore.changeManager,
},
],
)
},
})
rootModel.appendToMenu('Apollo', {
label: 'Manage Users',
onClick: (session: ApolloSessionModel) => {
Expand Down
Loading

0 comments on commit 59969bf

Please sign in to comment.