Skip to content

Commit

Permalink
Major upgrade
Browse files Browse the repository at this point in the history
Added example output
Parsing all data from fusion detection tools
Working installation
Added some basic testing
Added new databases: COSMIC and Mitelman
  • Loading branch information
matq007 committed Mar 15, 2019
1 parent 69c3570 commit ffcf5ea
Show file tree
Hide file tree
Showing 36 changed files with 22,409 additions and 108 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.vscode
db/
*.db
build/
dist/
*.egg-info
Expand Down
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ python:
- "3.6"

install:
- pip install pylint
- pip install Sphinx sphinxcontrib-napoleon
- pip install -r requirements.txt
- python setup.py install

script:
- fusion_report --help
- pylint fusion_report/
- pytest
157 changes: 90 additions & 67 deletions bin/fusion_report
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ from fusion_report.lib.section import Section
from fusion_report.lib.graph import Graph
from fusion_report.helpers.tool_parser import ToolParser
from fusion_report.helpers.core import tool_detection_chart, known_vs_unknown_chart, \
distribution_chart, create_fusions_table, create_ppi_graph, print_progress_bar
distribution_chart, create_fusions_table, create_ppi_graph, print_progress_bar, \
get_db_fusions

# Minimum number of tools that have to detect a fusion, used as a filter in Dashboard
TOOL_DETECTION_CUTOFF = 2
SUPPORTED_TOOLS = ['ericscript', 'starfusion', 'fusioncatcher', 'pizzly', 'squid']
DEFAULT_TOOL_WEIGHT = float(100/len(SUPPORTED_TOOLS))

def parse(params):
"""
Expand All @@ -22,34 +25,28 @@ def parse(params):
params (ArgumentParser):
"""
tools = ToolParser()
tools.parse('ericscript', params.ericscript)
tools.parse('starfusion', params.starfusion)
tools.parse('fusioncatcher', params.fusioncatcher)
tools.parse('pizzly', params.pizzly)
tools.parse('squid', params.squid)
for tool in SUPPORTED_TOOLS:
tools.parse(tool, params.__dict__[tool])

return tools

def generate_index(params, parser, known_fusions, unknown_fusions):
def generate_index(params, parser):
"""
Helper function for generating index.html page.
Args:
params (ArgumentParser)
parser (ToolParser)
known_fusions (list): List of known fusions
unknown_fusions (list): List of unknown fusions
Returns:
Page: Returns final Page object for index.html page.
"""
known_sum = len(known_fusions)
unknown_sum = len(unknown_fusions)
known_fusions, unknown_fusions = parser.get_db_counts()
index_page = Page(
title='index',
page_variables={
'sample': params.sample,
'fusions_sum': int(unknown_sum + known_sum),
'known_fusion_sum': known_sum,
'fusions_sum': int(unknown_fusions + known_fusions),
'known_fusion_sum': known_fusions,
'fusion_tools': parser.get_tools()
},
partial_template='index'
Expand All @@ -72,7 +69,7 @@ def generate_index(params, parser, known_fusions, unknown_fusions):
'known_vs_unknown_chart',
'Known Vs Unknown',
'Shows the ration between found and unknown missing fusions in the local database.',
known_vs_unknown_chart(known_sum, unknown_sum)
known_vs_unknown_chart(known_fusions, unknown_fusions)
)
)
dashboard_section.add_graph(
Expand All @@ -91,13 +88,13 @@ def generate_index(params, parser, known_fusions, unknown_fusions):
content='''
Filters fusions found by at least {tool} tools. If number of chosen tools is less
than {tool} the filter is disabled. The whole list can be found in
<code>results/Report-{sample}/fusions.txt</code>.
<code>./Report-{sample}/fusions.json</code>.
'''.format(tool=str(params.tool_num), sample=str(params.sample))
)
fusion_list_section.data = create_fusions_table(
parser.get_fusions(),
parser.get_tools(),
known_fusions, params.tool_num
params
)
index_page.add_section(fusion_list_section)

Expand All @@ -115,6 +112,7 @@ def generate_fusion_page(params, parser, fusion, db):
Returns:
Page: Returns final Page object for <fusion>.html page.
"""
db.connect('fusiongdb')
fusion_page = Page(
title=fusion,
page_variables={
Expand All @@ -128,7 +126,7 @@ def generate_fusion_page(params, parser, fusion, db):
title='Detail results from fusion detection tools',
content='Some description for each tool?'
)
detail_section.data = parser.get_fusion(fusion)
detail_section.data = parser.get_fusion(fusion).__dict__
fusion_page.add_section(detail_section)
# Variations section
variations_section = Section(
Expand Down Expand Up @@ -232,102 +230,127 @@ def generate_report(params):
Args:
params (ArgumentParser)
"""
i = 0
parser = parse(params)
db = Db(params.database)
known_fusions = []
unknown_fusions = []
report = Report(params.config, params.output)

# Get all fusions from DB
db.connect('fusiongdb')
db_fusions = db.select('''
SELECT DISTINCT (h_gene || "--" || t_gene) as fusion_pair
FROM TCGA_ChiTaRS_combined_fusion_information_on_hg19
''')
db_fusions = [x['fusion_pair'] for x in db_fusions]

db = Db(params.db_path)
fusions = parser.get_fusions()
db_fusions = get_db_fusions(db)

report = Report(params.config, params.output)
print_progress_bar(0, len(fusions), 50)
for i, fusion in enumerate(fusions):

if fusion not in db_fusions:
unknown_fusions.append(fusion)
# progress bar
sleep(0.1)
print_progress_bar(i + 1, len(fusions), 50)
continue # go to next fusion
for fusion, detail in fusions:
i += 1
for db_name, db_list in db_fusions.items():
if fusion in db_list:
detail.add_db(db_name)

if 'FusionGDB' in detail.dbs:
fusion_page = generate_fusion_page(params, parser, fusion, db)
report.add_page(fusion_page)

known_fusions.append(fusion)
fusion_page = generate_fusion_page(params, parser, fusion, db)
report.add_page(fusion_page)
# progress bar
sleep(0.1)
print_progress_bar(i + 1, len(fusions), 50)
print_progress_bar(i, len(fusions), 50)

index_page = generate_index(params, parser, known_fusions, unknown_fusions)
index_page = generate_index(params, parser)
report.add_page(index_page)
parser.save(params.output, 'fusions')
print(f'The report for `sample` {params.sample} was generated in {params.output}.')

def main():
"""Main function for processing command line arguments"""
parser = argparse.ArgumentParser(
description='Tool for generating friendly UI custom report'
description='''
Tool for generating friendly UI custom report.
Supported tools are: {0} and {1}.
'''.format(', '.join(SUPPORTED_TOOLS[:-1]), SUPPORTED_TOOLS[-1])
)
mandatory = parser.add_argument_group('Mandatory arguments', 'Required arguments to run app.')
mandatory.add_argument(
'sample',
help='Sample name',
type=str
)
mandatory.add_argument(
'output',
help='Output directory',
type=str
)
parser.add_argument(
mandatory.add_argument(
'db_path',
help='Path to folder where all databases are stored.',
type=str
)
tools = parser.add_argument_group('Tools', 'List of all supported tools with their weights.')
tools.add_argument(
'--ericscript',
help='EricScript output file',
type=str
)
parser.add_argument(
tools.add_argument(
'--ericscript_weight',
help='EricScript weight',
type=float,
default=DEFAULT_TOOL_WEIGHT
)
tools.add_argument(
'--fusioncatcher',
help='FusionCatcher output file',
type=str
)
parser.add_argument(
tools.add_argument(
'--fusioncatcher_weight',
help='FusionCatcher weight',
type=float,
default=DEFAULT_TOOL_WEIGHT
)
tools.add_argument(
'--starfusion',
help='STAR-Fusion output file',
type=str
)
parser.add_argument(
tools.add_argument(
'--starfusion_weight',
help='STAR-Fusion weight',
type=float,
default=DEFAULT_TOOL_WEIGHT
)
tools.add_argument(
'--pizzly',
help='Pizzly output file',
type=str
)
parser.add_argument(
tools.add_argument(
'--pizzly_weight',
help='Pizzly weight',
type=float,
default=DEFAULT_TOOL_WEIGHT
)
tools.add_argument(
'--squid',
help='Squid output file',
type=str
)
parser.add_argument(
'-s', '--sample',
help='Sample name',
type=str,
required=True
)
parser.add_argument(
'-o', '--output',
help='Output directory',
type=str,
required=True
tools.add_argument(
'--squid_weight',
help='Squid weight',
type=float,
default=DEFAULT_TOOL_WEIGHT
)
parser.add_argument(
optional = parser.add_argument_group('Optional', 'List of additional configuration parameters.')
optional.add_argument(
'-c', '--config',
help='Input config file',
type=str,
required=False
)
parser.add_argument(
optional.add_argument(
'-t', '--tool_num',
help='Number of tools required to detect a fusion',
type=int,
default=TOOL_DETECTION_CUTOFF
)
parser.add_argument(
'-db', '--database',
help='Path to database file fusions.db (for local development)',
type=str,
required=False
)
generate_report(parser.parse_args())

if __name__ == "__main__":
Expand Down
1,704 changes: 1,704 additions & 0 deletions docs/example/AKAP9_BRAF.html

Large diffs are not rendered by default.

1,704 changes: 1,704 additions & 0 deletions docs/example/BRD4_NUTM1.html

Large diffs are not rendered by default.

1,704 changes: 1,704 additions & 0 deletions docs/example/CD74_ROS1.html

Large diffs are not rendered by default.

1,704 changes: 1,704 additions & 0 deletions docs/example/EML4_ALK.html

Large diffs are not rendered by default.

1,704 changes: 1,704 additions & 0 deletions docs/example/ETV6_NTRK3.html

Large diffs are not rendered by default.

1,704 changes: 1,704 additions & 0 deletions docs/example/EWSR1_ATF1.html

Large diffs are not rendered by default.

1,704 changes: 1,704 additions & 0 deletions docs/example/EWSR1_FLI1.html

Large diffs are not rendered by default.

1,704 changes: 1,704 additions & 0 deletions docs/example/FGFR3_TACC3.html

Large diffs are not rendered by default.

1,704 changes: 1,704 additions & 0 deletions docs/example/FIP1L1_PDGFRA.html

Large diffs are not rendered by default.

1,704 changes: 1,704 additions & 0 deletions docs/example/HOOK3_RET.html

Large diffs are not rendered by default.

1,704 changes: 1,704 additions & 0 deletions docs/example/NPM1_ALK.html

Large diffs are not rendered by default.

1,704 changes: 1,704 additions & 0 deletions docs/example/TMPRSS2_ETV1.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/example/fusions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"AKAP9--BRAF":{"score":0.0,"tools":{"ericscript":[{"position":"7:92003235:+#7:140787584:-","discordant_reads":11,"junction_reads":24,"fusion_type":"intra-chromosomal","gene_expr1":9.56,"gene_expr2":9.72,"gene_expr_fusion":37.9}],"fusioncatcher":[{"position":"7:92003235:+#7:140787584:-","common_mapping_reads":0,"spanning_pairs":10,"spanning_unique_reads":11,"longest_anchor":29,"fusion_type":"in-frame"}],"pizzly":[{"pair_count":4,"split_count":5}],"squid":[{"position":"7:92002951-92003235:+#7:140783020-140787584:+","score":8}]},"dbs":["FusionGDB","Mitelman","COSMIC"]},"GOPC--ROS1":{"score":0.0,"tools":{"ericscript":[{"position":"6:117566854:-#6:117321394:-","discordant_reads":140,"junction_reads":73,"fusion_type":"intra-chromosomal","gene_expr1":0.0,"gene_expr2":2.11,"gene_expr_fusion":26.21}],"fusioncatcher":[{"position":"6:117566854:-#6:117321394:-","common_mapping_reads":0,"spanning_pairs":73,"spanning_unique_reads":22,"longest_anchor":30,"fusion_type":"in-frame"},{"position":"6:117566854:-#6:117320030:-","common_mapping_reads":0,"spanning_pairs":73,"spanning_unique_reads":2,"longest_anchor":26,"fusion_type":"out-of-frame"}]},"dbs":["Mitelman","COSMIC"]},"FGFR3--TACC3":{"score":0.0,"tools":{"ericscript":[{"position":"4:1806934:+#4:1727977:+","discordant_reads":978,"junction_reads":378,"fusion_type":"Read-Through","gene_expr1":0.0,"gene_expr2":1.58,"gene_expr_fusion":101.36}],"starfusion":[{"position":"4:1806934:+#4:1727977:+","junction_reads":235,"spanning_reads":637,"ffmp":69504.2245}],"fusioncatcher":[{"position":"4:1806934:+#4:1727977:+","common_mapping_reads":0,"spanning_pairs":820,"spanning_unique_reads":66,"longest_anchor":43,"fusion_type":"in-frame"},{"position":"4:1807033:+#4:1727832:+","common_mapping_reads":0,"spanning_pairs":820,"spanning_unique_reads":9,"longest_anchor":43,"fusion_type":"intronic/CDS(truncated)"}],"pizzly":[{"pair_count":487,"split_count":231}],"squid":[{"position":"4:1806048-1806934:+#4:1727976-1728733:-","score":785},{"position":"4:1806048-1807038:+#4:1727836-1728733:-","score":785}]},"dbs":["FusionGDB","Mitelman","COSMIC"]},"FIP1L1--PDGFRA":{"score":0.0,"tools":{"ericscript":[{"position":"4:53425965:+#4:54274925:+","discordant_reads":68,"junction_reads":39,"fusion_type":"intra-chromosomal","gene_expr1":0.0,"gene_expr2":0.01,"gene_expr_fusion":29.15}],"fusioncatcher":[{"position":"4:53425965:+#4:54274925:+","common_mapping_reads":0,"spanning_pairs":50,"spanning_unique_reads":42,"longest_anchor":45,"fusion_type":"in-frame"}]},"dbs":["FusionGDB","Mitelman"]},"BRD4--NUTM1":{"score":0.0,"tools":{"ericscript":[{"position":"19:15254152:-#15:34347969:+","discordant_reads":7,"junction_reads":13,"fusion_type":"inter-chromosomal","gene_expr1":4.19,"gene_expr2":17.98,"gene_expr_fusion":83.9}],"starfusion":[{"position":"19:15254152:-#15:34347969:+","junction_reads":7,"spanning_reads":2,"ffmp":717.3602}],"fusioncatcher":[{"position":"19:15254152:-#15:34347969:+","common_mapping_reads":0,"spanning_pairs":5,"spanning_unique_reads":9,"longest_anchor":30,"fusion_type":"in-frame"}],"pizzly":[{"pair_count":1,"split_count":6}],"squid":[{"position":"19:15254151-15254264:-#15:34347968-34348134:-","score":9}]},"dbs":["FusionGDB","Mitelman"]},"HOOK3--RET":{"score":0.0,"tools":{"ericscript":[{"position":"8:42968214:+#10:43116584:+","discordant_reads":12,"junction_reads":21,"fusion_type":"inter-chromosomal","gene_expr1":2.44,"gene_expr2":3.31,"gene_expr_fusion":31.82}],"starfusion":[{"position":"8:42968214:+#10:43116584:+","junction_reads":10,"spanning_reads":2,"ffmp":956.4802}],"fusioncatcher":[{"position":"8:42968214:+#10:43116584:+","common_mapping_reads":0,"spanning_pairs":10,"spanning_unique_reads":10,"longest_anchor":29,"fusion_type":"in-frame"}],"pizzly":[{"pair_count":2,"split_count":8}],"squid":[{"position":"8:42968010-42968214:+#10:43116583-43116731:-","score":11}]},"dbs":["FusionGDB","Mitelman","COSMIC"]},"TMPRSS2--ETV1":{"score":0.0,"tools":{"ericscript":[{"position":"21:41494380:-#7:13935843:-","discordant_reads":9,"junction_reads":16,"fusion_type":"inter-chromosomal","gene_expr1":0.0,"gene_expr2":17.69,"gene_expr_fusion":18.49}],"starfusion":[{"position":"21:41494375:-#7:13935838:-","junction_reads":10,"spanning_reads":3,"ffmp":1036.1868}],"fusioncatcher":[{"position":"21:41494381:-#7:13935844:-","common_mapping_reads":0,"spanning_pairs":7,"spanning_unique_reads":10,"longest_anchor":38,"fusion_type":"in-frame"}],"squid":[{"position":"7:13935707-13935838:+#21:41494374-41494578:-","score":12}]},"dbs":["FusionGDB","Mitelman","COSMIC"]},"CD74--ROS1":{"score":0.0,"tools":{"ericscript":[{"position":"5:150404680:-#6:117324415:-","discordant_reads":3,"junction_reads":14,"fusion_type":"inter-chromosomal","gene_expr1":2.44,"gene_expr2":2.11,"gene_expr_fusion":19.87}],"starfusion":[{"position":"5:150404680:-#6:117324415:-","junction_reads":5,"spanning_reads":0,"ffmp":398.5334}],"fusioncatcher":[{"position":"5:150404680:-#6:117324415:-","common_mapping_reads":0,"spanning_pairs":3,"spanning_unique_reads":7,"longest_anchor":30,"fusion_type":"in-frame"}],"pizzly":[{"pair_count":0,"split_count":2}]},"dbs":["FusionGDB","Mitelman","COSMIC"]},"EWSR1--ATF1":{"score":0.0,"tools":{"ericscript":[{"position":"22:29287134:+#12:50814280:+","discordant_reads":11,"junction_reads":25,"fusion_type":"inter-chromosomal","gene_expr1":37.0,"gene_expr2":12.27,"gene_expr_fusion":553.3}],"starfusion":[{"position":"22:29287134:+#12:50814280:+","junction_reads":8,"spanning_reads":3,"ffmp":876.7734}],"fusioncatcher":[{"position":"22:29287134:+#12:50814280:+","common_mapping_reads":0,"spanning_pairs":10,"spanning_unique_reads":10,"longest_anchor":30,"fusion_type":"in-frame"}],"pizzly":[{"pair_count":3,"split_count":8}],"squid":[{"position":"22:29286921-29287134:+#12:50814279-50814439:-","score":10}]},"dbs":["FusionGDB","Mitelman","COSMIC"]},"EWSR1--FLI1":{"score":0.0,"tools":{"ericscript":[{"position":"22:29287134:+#11:128807180:+","discordant_reads":9,"junction_reads":12,"fusion_type":"inter-chromosomal","gene_expr1":37.0,"gene_expr2":3.29,"gene_expr_fusion":600.14}],"starfusion":[{"position":"22:29287134:+#11:128807180:+","junction_reads":5,"spanning_reads":2,"ffmp":557.9468}],"fusioncatcher":[{"position":"22:29287134:+#11:128807180:+","common_mapping_reads":0,"spanning_pairs":9,"spanning_unique_reads":11,"longest_anchor":28,"fusion_type":"in-frame"}],"pizzly":[{"pair_count":2,"split_count":6}]},"dbs":["FusionGDB","Mitelman","COSMIC"]},"ETV6--NTRK3":{"score":0.0,"tools":{"ericscript":[{"position":"12:11869970:+#15:87940752:-","discordant_reads":7,"junction_reads":24,"fusion_type":"inter-chromosomal","gene_expr1":9.48,"gene_expr2":0.45,"gene_expr_fusion":54.74}],"starfusion":[{"position":"12:11869969:+#15:87940753:-","junction_reads":9,"spanning_reads":4,"ffmp":1036.1868}],"fusioncatcher":[{"position":"12:11869969:+#15:87940753:-","common_mapping_reads":0,"spanning_pairs":6,"spanning_unique_reads":8,"longest_anchor":29,"fusion_type":"in-frame"}],"pizzly":[{"pair_count":2,"split_count":8}],"squid":[{"position":"12:11869684-11869969:+#15:87940621-87940753:+","score":10}]},"dbs":["FusionGDB","Mitelman","COSMIC"]},"CIC--DUX4":{"score":0.0,"tools":{"ericscript":[{"position":"19:42295048:+#4:190174447:+","discordant_reads":6,"junction_reads":6,"fusion_type":"inter-chromosomal","gene_expr1":0.0,"gene_expr2":1.34,"gene_expr_fusion":5.54}],"fusioncatcher":[{"position":"19:42295047:+#4:190174446:+","common_mapping_reads":0,"spanning_pairs":5,"spanning_unique_reads":5,"longest_anchor":34,"fusion_type":"in-frame"}]},"dbs":["Mitelman","COSMIC"]},"EML4--ALK":{"score":0.0,"tools":{"starfusion":[{"position":"2:42301394:+#2:29223584:-","junction_reads":3,"spanning_reads":4,"ffmp":557.9467}],"fusioncatcher":[{"position":"2:42301391:+#2:29223587:-","common_mapping_reads":0,"spanning_pairs":4,"spanning_unique_reads":4,"longest_anchor":33,"fusion_type":"CDS(truncated)/UTR"}]},"dbs":["FusionGDB","Mitelman","COSMIC"]},"CD74--AL132671.2":{"score":0.0,"tools":{"starfusion":[{"position":"5:150404680:-#6:117324415:-","junction_reads":3,"spanning_reads":0,"ffmp":239.12}]},"dbs":[]},"IGH@--CRLF2":{"score":0.0,"tools":{"fusioncatcher":[{"position":"14:105863258:+#X:105799296:-","common_mapping_reads":0,"spanning_pairs":32,"spanning_unique_reads":9,"longest_anchor":94,"fusion_type":"---/intergenic"},{"position":"14:105863343:+#X:105812546:-","common_mapping_reads":0,"spanning_pairs":32,"spanning_unique_reads":6,"longest_anchor":108,"fusion_type":"---/intergenic"},{"position":"14:105863263:+#X:1228687:-","common_mapping_reads":0,"spanning_pairs":32,"spanning_unique_reads":4,"longest_anchor":33,"fusion_type":"---/intergenic"}]},"dbs":[]},"DUX4--IGH@":{"score":0.0,"tools":{"fusioncatcher":[{"position":"4:190174997:+#14:190287409:+","common_mapping_reads":0,"spanning_pairs":5,"spanning_unique_reads":5,"longest_anchor":84,"fusion_type":"CDS(truncated)/---"},{"position":"4:190175284:+#14:106638525:-","common_mapping_reads":0,"spanning_pairs":4,"spanning_unique_reads":8,"longest_anchor":38,"fusion_type":"UTR/---"}]},"dbs":[]},"MALT1--IGH@":{"score":0.0,"tools":{"fusioncatcher":[{"position":"18:58669909:+#14:105888559:+","common_mapping_reads":0,"spanning_pairs":3,"spanning_unique_reads":3,"longest_anchor":34,"fusion_type":"intergenic/---"}]},"dbs":[]},"NPM1--ALK":{"score":0.0,"tools":{"fusioncatcher":[{"position":"5:171391799:+#2:29223528:-","common_mapping_reads":0,"spanning_pairs":3,"spanning_unique_reads":3,"longest_anchor":30,"fusion_type":"in-frame"}]},"dbs":["FusionGDB","Mitelman","COSMIC"]}}
1,462 changes: 1,462 additions & 0 deletions docs/example/index.html

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions fusion_report/db/Cosmic.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
CREATE TABLE "CosmicFusionExport" (
"sample_id" integer NOT NULL,
"sample_name" varchar(50) NOT NULL DEFAULT '',
"primary_site" varchar(50) NOT NULL DEFAULT '',
"site_subtype_1" varchar(50) NOT NULL DEFAULT '',
"site_subtype_2" varchar(50) NOT NULL DEFAULT '',
"site_subtype_3" varchar(50) NOT NULL DEFAULT '',
"primary_histology" varchar(50) NOT NULL DEFAULT '',
"histology_subtype_1" varchar(50) NOT NULL DEFAULT '',
"histology_subtype_2" varchar(50) NOT NULL DEFAULT '',
"histology_subtype_3" varchar(50) NOT NULL DEFAULT '',
"fusion_id" integer NOT NULL,
"translocation_name" varchar(50) NOT NULL DEFAULT '',
"translocation_type" varchar(50) NOT NULL DEFAULT '',
"pubmed" integer NOT NULL,
"id_study" integer NOT NULL
);

.separator "\t"
.import CosmicFusionExport_stripped.tsv CosmicFusionExport
Loading

0 comments on commit ffcf5ea

Please sign in to comment.