Skip to content

Commit 8d14037

Browse files
Basic implementation of apollo export gff3 (#511)
* Basic implementation of `apollo export gff3` To be fixed?: Output gff3 includes fasta sequence *only* if the assembly is editable (see tests). * Handle invalid assembly * Optional export of fasta in `apollo export gff3` For UI, always include fasta when downloading gff3 * Export fasta by streaing original file Also re-enable cypress test to download annotation * Fix lint * Fix import * Include comments here #511 (review) Use includeFASTA instead of withFasta and --include-fasta instead of --with-fasta in cli * Tiny docs fix skip-checks:true --------- Co-authored-by: Garrett Stevens <stevens.garrett.j@gmail.com>
1 parent e1fea5a commit 8d14037

File tree

12 files changed

+358
-27
lines changed

12 files changed

+358
-27
lines changed

.github/workflows/pull_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
yarn --cwd packages/apollo-shared start &
4343
ALLOW_ROOT_USER=true ROOT_USER_PASSWORD=pass yarn --cwd packages/apollo-collaboration-server start &
4444
- name: Run CLI tests
45-
run: yarn tsx src/test/test.ts
45+
run: yarn test:cli
4646
working-directory: packages/apollo-cli
4747
# - name: Run docker tests
4848
# working-directory: packages/apollo-cli

packages/apollo-cli/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ USAGE
3737
- [`apollo assembly sequence`](#apollo-assembly-sequence)
3838
- [`apollo change get`](#apollo-change-get)
3939
- [`apollo config [KEY] [VALUE]`](#apollo-config-key-value)
40+
- [`apollo export gff3 ASSEMBLY`](#apollo-export-gff3-assembly)
4041
- [`apollo feature add-child`](#apollo-feature-add-child)
4142
- [`apollo feature check`](#apollo-feature-check)
4243
- [`apollo feature copy`](#apollo-feature-copy)
@@ -354,6 +355,34 @@ EXAMPLES
354355
_See code:
355356
[src/commands/config.ts](https://github.com/GMOD/Apollo3/blob/v0.3.1/packages/apollo-cli/src/commands/config.ts)_
356357

358+
## `apollo export gff3 ASSEMBLY`
359+
360+
Export the annotations for an assembly to stdout as gff3
361+
362+
```
363+
USAGE
364+
$ apollo export gff3 ASSEMBLY [--profile <value>] [--config-file <value>] [--include-fasta]
365+
366+
ARGUMENTS
367+
ASSEMBLY Export annotations for this assembly name or id
368+
369+
FLAGS
370+
--config-file=<value> Use this config file (mostly for testing)
371+
--include-fasta Include fasta sequence in output
372+
--profile=<value> Use credentials from this profile
373+
374+
DESCRIPTION
375+
Export the annotations for an assembly to stdout as gff3
376+
377+
EXAMPLES
378+
Export annotations for myAssembly:
379+
380+
$ apollo export gff3 myAssembly > out.gff3
381+
```
382+
383+
_See code:
384+
[src/commands/export/gff3.ts](https://github.com/GMOD/Apollo3/blob/v0.3.1/packages/apollo-cli/src/commands/export/gff3.ts)_
385+
357386
## `apollo feature add-child`
358387

359388
Add a child feature (e.g. add an exon to an mRNA)

packages/apollo-cli/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"test": "mocha --require src/test/fixtures.ts 'src/**/*.test.ts'",
3030
"posttest": "yarn lint",
3131
"test:ci": "nyc mocha 'src/**/*.test.ts'",
32+
"test:cli": "yarn tsx src/test/test.ts",
3233
"version": "oclif readme --multi --dir ../website/docs/admin/cli/ && oclif readme && git add README.md"
3334
},
3435
"oclif": {
@@ -51,6 +52,9 @@
5152
"change": {
5253
"description": "Commands to manage the change log"
5354
},
55+
"export": {
56+
"description": "Commands to export data"
57+
},
5458
"feature": {
5559
"description": "Commands to manage features"
5660
},
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { Args, Flags } from '@oclif/core'
2+
import { fetch } from 'undici'
3+
4+
import { BaseCommand } from '../../baseCommand.js'
5+
import {
6+
convertAssemblyNameToId,
7+
createFetchErrorMessage,
8+
idReader,
9+
localhostToAddress,
10+
} from '../../utils.js'
11+
12+
import { Readable } from 'node:stream'
13+
14+
export default class Get extends BaseCommand<typeof Get> {
15+
static description =
16+
'Export the annotations for an assembly to stdout as gff3'
17+
18+
static examples = [
19+
{
20+
description: 'Export annotations for myAssembly:',
21+
command: '<%= config.bin %> <%= command.id %> myAssembly > out.gff3',
22+
},
23+
]
24+
25+
static args = {
26+
assembly: Args.string({
27+
description: 'Export annotations for this assembly name or id',
28+
required: true,
29+
}),
30+
}
31+
32+
static flags = {
33+
'include-fasta': Flags.boolean({
34+
description: 'Include fasta sequence in output',
35+
default: false,
36+
}),
37+
}
38+
39+
public async run(): Promise<void> {
40+
const { args } = await this.parse(Get)
41+
42+
const access = await this.getAccess()
43+
44+
const assembly = await idReader([args.assembly])
45+
const [assemblyId] = await convertAssemblyNameToId(
46+
access.address,
47+
access.accessToken,
48+
assembly,
49+
)
50+
if (!assemblyId) {
51+
this.error(`Invalid assembly name or id: ${args.assembly}`)
52+
}
53+
54+
const url = new URL(localhostToAddress(`${access.address}/export/getID`))
55+
const searchParams = new URLSearchParams({
56+
assembly: assemblyId,
57+
})
58+
url.search = searchParams.toString()
59+
const uri = url.toString()
60+
const auth = {
61+
headers: {
62+
authorization: `Bearer ${access.accessToken}`,
63+
},
64+
}
65+
const response = await fetch(uri, auth)
66+
if (!response.ok) {
67+
const newErrorMessage = await createFetchErrorMessage(
68+
response,
69+
'Error when exporting ID',
70+
)
71+
throw new Error(newErrorMessage)
72+
}
73+
74+
const { exportID } = (await response.json()) as { exportID: string }
75+
76+
const exportURL = new URL(localhostToAddress(`${access.address}/export`))
77+
78+
const params: Record<string, string> = {
79+
exportID,
80+
assemblyId,
81+
includeFASTA: this.flags['include-fasta'] ? 'true' : 'false',
82+
}
83+
const exportSearchParams = new URLSearchParams(params)
84+
exportURL.search = exportSearchParams.toString()
85+
const exportUri = exportURL.toString()
86+
87+
const responseExport = await fetch(exportUri, auth)
88+
if (!responseExport.ok) {
89+
const newErrorMessage = await createFetchErrorMessage(
90+
responseExport,
91+
'Error when exporting gff',
92+
)
93+
throw new Error(newErrorMessage)
94+
}
95+
const { body } = responseExport
96+
if (body) {
97+
const readable = Readable.from(body)
98+
readable.pipe(process.stdout)
99+
} else {
100+
this.error('Failed to export gff3')
101+
}
102+
}
103+
}

packages/apollo-cli/src/test/test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
* USAGE
1010
* From package root directory (`packages/apollo-cli`). Run all tests:
1111
*
12-
* yarn tsx src/test/test.ts
12+
* yarn test:cli
1313
*
1414
* Run only matching pattern:
1515
*
16-
* yarn tsx --test-name-pattern='Print help|Feature get' src/test/test.ts
16+
* yarn test:cli --test-name-pattern='Print help|Feature get'
1717
*/
1818

1919
import assert from 'node:assert'
@@ -1388,4 +1388,68 @@ void describe('Test CLI', () => {
13881388
out = JSON.parse(p.stdout)
13891389
assert.strictEqual(out.length, 0)
13901390
})
1391+
1392+
void globalThis.itName('Export gff3 from editable assembly', () => {
1393+
new Shell(
1394+
`${apollo} assembly add-from-fasta ${P} test_data/tiny.fasta.gz -a vv1 -f --editable`,
1395+
)
1396+
new Shell(`${apollo} feature import ${P} test_data/tiny.fasta.gff3 -a vv1`)
1397+
let p = new Shell(`${apollo} export gff3 ${P} vv1 --include-fasta`)
1398+
let gff = p.stdout
1399+
assert.ok(gff.startsWith('##gff-version 3'))
1400+
assert.ok(gff.includes('multivalue=val1,val2,val3'))
1401+
assert.ok(gff.includes('##FASTA\n'))
1402+
assert.deepStrictEqual(gff.slice(-6, gff.length), 'taccc\n')
1403+
1404+
p = new Shell(`${apollo} export gff3 ${P} vv1`)
1405+
gff = p.stdout
1406+
assert.ok(gff.startsWith('##gff-version 3'))
1407+
assert.ok(gff.includes('multivalue=val1,val2,val3'))
1408+
assert.ok(!gff.includes('##FASTA\n'))
1409+
1410+
// Invalid assembly
1411+
p = new Shell(`${apollo} export gff3 ${P} foobar`, false)
1412+
assert.ok(p.returncode != 0)
1413+
assert.ok(p.stderr.includes('foobar'))
1414+
})
1415+
1416+
void globalThis.itName('Export gff3 from non-editable assembly', () => {
1417+
new Shell(
1418+
`${apollo} assembly add-from-fasta ${P} test_data/tiny.fasta.gz -a vv1 -f`,
1419+
)
1420+
new Shell(`${apollo} feature import ${P} test_data/tiny.fasta.gff3 -a vv1`)
1421+
let p = new Shell(`${apollo} export gff3 ${P} vv1 --include-fasta`)
1422+
let gff = p.stdout
1423+
assert.ok(gff.startsWith('##gff-version 3'))
1424+
assert.ok(gff.includes('multivalue=val1,val2,val3'))
1425+
assert.ok(gff.includes('##FASTA\n'))
1426+
// We end with two newlines because the test data does have an extra newline at the end.
1427+
assert.deepStrictEqual(gff.slice(-7, gff.length), 'taccc\n\n')
1428+
1429+
p = new Shell(`${apollo} export gff3 ${P} vv1`)
1430+
gff = p.stdout
1431+
assert.ok(gff.startsWith('##gff-version 3'))
1432+
assert.ok(gff.includes('multivalue=val1,val2,val3'))
1433+
assert.ok(!gff.includes('##FASTA\n'))
1434+
})
1435+
1436+
void globalThis.itName('Export gff3 from external assembly', () => {
1437+
new Shell(
1438+
`${apollo} assembly add-from-fasta ${P} https://raw.githubusercontent.com/GMOD/Apollo3/refs/heads/main/packages/apollo-cli/test_data/tiny.fasta.gz -a vv1 -f`,
1439+
)
1440+
new Shell(`${apollo} feature import ${P} test_data/tiny.fasta.gff3 -a vv1`)
1441+
let p = new Shell(`${apollo} export gff3 ${P} vv1 --include-fasta`)
1442+
let gff = p.stdout
1443+
assert.ok(gff.startsWith('##gff-version 3'))
1444+
assert.ok(gff.includes('multivalue=val1,val2,val3'))
1445+
assert.ok(gff.includes('##FASTA\n'))
1446+
// We end with two newlines because the test data does have an extra newline at the end.
1447+
assert.deepStrictEqual(gff.slice(-7, gff.length), 'taccc\n\n')
1448+
1449+
p = new Shell(`${apollo} export gff3 ${P} vv1`)
1450+
gff = p.stdout
1451+
assert.ok(gff.startsWith('##gff-version 3'))
1452+
assert.ok(gff.includes('multivalue=val1,val2,val3'))
1453+
assert.ok(!gff.includes('##FASTA\n'))
1454+
})
13911455
})

packages/apollo-collaboration-server/src/export/export.controller.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
22
import {
33
Controller,
4+
DefaultValuePipe,
45
Get,
56
Logger,
7+
ParseBoolPipe,
8+
ParseIntPipe,
69
Query,
710
Response,
811
StreamableFile,
@@ -41,12 +44,17 @@ export class ExportController {
4144
@Validations(Role.None)
4245
@Get()
4346
async exportGFF3(
44-
@Query() request: { exportID: string; fastaWidth?: number },
47+
// @Query()
48+
@Query('exportID') exportID: string,
49+
@Query('includeFASTA', new DefaultValuePipe(false), ParseBoolPipe)
50+
includeFASTA: boolean,
51+
@Query('fastaWidth', new DefaultValuePipe(80), ParseIntPipe)
52+
fastaWidth: number,
4553
@Response({ passthrough: true }) res: ExpressResponse,
4654
) {
47-
const { exportID, ...rest } = request
4855
const [stream, assembly] = await this.exportService.exportGFF3(exportID, {
49-
...rest,
56+
includeFASTA,
57+
fastaWidth,
5058
})
5159
const assemblyName = await this.exportService.getAssemblyName(assembly)
5260
res.set({

packages/apollo-collaboration-server/src/export/export.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import { RefSeqChunksModule } from '../refSeqChunks/refSeqChunks.module'
88
import { RefSeqsModule } from '../refSeqs/refSeqs.module'
99
import { ExportController } from './export.controller'
1010
import { ExportService } from './export.service'
11+
import { FilesModule } from '../files/files.module'
1112

1213
@Module({
1314
imports: [
1415
AssembliesModule,
1516
FeaturesModule,
17+
FilesModule,
1618
MongooseModule.forFeature([{ name: Export.name, schema: ExportSchema }]),
1719
RefSeqsModule,
1820
RefSeqChunksModule,

0 commit comments

Comments
 (0)