Skip to content

Commit 1fde1cf

Browse files
committed
Merge branch 'develop' into 'main'
release 1.0.0 See merge request casm/team78/hairpin-core!11
2 parents 059afd1 + 203263e commit 1fde1cf

21 files changed

+1968
-59
lines changed

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
data*
2+
.env/
3+
venv/
4+
.venv/
5+
dist/
6+
*.egg-info/
7+
__pycache__/
8+
.helix/
9+
build/
10+
test_data_creation/
11+
*.txt
12+
*.sif
13+
*.json
14+
poetry.lock
15+
.coverage
16+
test/sim-data/
17+
test/old_*

.gitlab-ci.yml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
stages:
2+
- build
3+
- test
4+
- publish
5+
6+
include:
7+
# DOCS: https://gitlab.internal.sanger.ac.uk/team113sanger/common/cicd-template/-/blob/develop/README.md
8+
- project: 'team113sanger/common/cicd-template'
9+
ref: 0.3.1
10+
file: '.gitlab-ci-components.yml'
11+
12+
#############
13+
# TEMPLATES #
14+
#############
15+
16+
.generic-wo-script-or-rules:
17+
extends:
18+
- .component-variables
19+
- .component-before_script
20+
- .component-tags-shared-large-runner
21+
22+
.specific-variables:
23+
variables:
24+
UNIT_TEST_MOUNT_POINT: /opt/tests
25+
RUN_SCRIPT_MOUNT_POINT: /tmp/run.sh
26+
# We need to set this to 1 to enable BuildKit as the Dockerfile uses BuildKit features to speed up the build
27+
DOCKER_BUILDKIT: 1
28+
PRE_FETCH_BASE_IMAGE: python:3.12-slim
29+
# Incase 'docker compose' build is ever used we want to ensure the image
30+
# does not have sudo. By default CICD jobs do not build with 'docker
31+
# compose' but use 'docker' - so this is just a safety measure.
32+
HAS_SUDO: 0
33+
34+
############
35+
# JOBS #
36+
############
37+
38+
build:
39+
stage: build
40+
extends:
41+
- .generic-wo-script-or-rules
42+
- .specific-variables
43+
- .component-script_docker-build
44+
- .component-rules-except-release
45+
46+
unit-test:
47+
stage: test
48+
extends:
49+
- .generic-wo-script-or-rules
50+
- .specific-variables
51+
- .component-rules-except-release
52+
script:
53+
- echo "*** [SCRIPT] START ***"
54+
- echo "I am a script - I run the Python unit tests in a docker container"
55+
- echo "Unit test against CANDIDATE_IMAGE='${CANDIDATE_IMAGE:?not-set-in-before_script}'"
56+
- docker pull "${CANDIDATE_IMAGE}"
57+
# Test image against unit tests - it requires env vars
58+
- docker run --entrypoint "bash" -e TEST_DIR="${UNIT_TEST_MOUNT_POINT}" -v "${PWD}/test:${UNIT_TEST_MOUNT_POINT}:ro" -v "${PWD}/docker-run-unit-tests.sh:${RUN_SCRIPT_MOUNT_POINT}:ro" --rm "${CANDIDATE_IMAGE}" ${RUN_SCRIPT_MOUNT_POINT}
59+
- echo "*** [SCRIPT] END ***"
60+
61+
publish-develop:
62+
stage: publish
63+
extends:
64+
- .generic-wo-script-or-rules
65+
- .specific-variables
66+
- .component-script-publish-develop-docker-image
67+
- .component-rules-develop-only
68+
69+
publish-tagged_and_latest_docker_images:
70+
stage: publish
71+
extends:
72+
- .generic-wo-script-or-rules
73+
- .specific-variables
74+
- .component-script-publish-tagged+latest-docker-image
75+
- .component-rules-tag-only
76+

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

Dockerfile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
FROM python:3.12-slim
2+
3+
# Set the working directory inside the container
4+
WORKDIR /hairpin2
5+
6+
# Copy the current working directory contents into the container
7+
COPY . /hairpin2
8+
9+
RUN adduser --disabled-password --gecos '' ubuntu && chsh -s /bin/bash && mkdir -p /home/ubuntu
10+
11+
USER ubuntu
12+
WORKDIR /home/ubuntu
13+
14+
# Install the hairpin package
15+
RUN pip install --no-warn-script-location /hairpin2
16+
17+
ENV PATH=$PATH:/home/ubuntu/.local/bin
18+
19+
# Define a test script to check the installation of hairpin
20+
RUN LOC=$(which hairpin2) \
21+
&& if [ -z "$LOC" ]; then \
22+
echo "hairpin install failed" && exit 1; \
23+
else echo "hairpin install successful"; fi
24+
25+
# Set up the default command for the container
26+
ENTRYPOINT ["hairpin2"]
27+

LICENSE

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
MIT License
2+
3+
Copyright (C) 2024 Genome Research Ltd.
4+
5+
Author: Alex Byrne <ab63@sanger.ac.uk>
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.

README.md

Lines changed: 177 additions & 59 deletions
Large diffs are not rendered by default.

Singularity.def

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Bootstrap: docker
2+
From: python:3.12-slim
3+
4+
%files
5+
. hairpin2/
6+
7+
%post
8+
pip install --root-user-action ignore hairpin2/
9+
10+
%test
11+
LOC=$(which hairpin2)
12+
if [ -z "$LOC"]; then
13+
echo "hairpin install failed"
14+
else
15+
echo "hairpin install successful"
16+
fi
17+
18+
%runscript
19+
exec hairpin2 "$@"

docker-run-unit-tests.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
3+
if [ -z ${TEST_DIR} ]; then
4+
echo "TEST_DIR not set!"
5+
exit 1
6+
fi
7+
PKG_DIR=$(python -c "import os;import hairpin2;import inspect;print(os.path.dirname(inspect.getfile(hairpin2)))")
8+
9+
echo "$(python --version)"
10+
echo "Package source directory: ${PKG_DIR}"
11+
12+
pip install \
13+
pytest==8.2.2 \
14+
pytest-cov==5.0.0 \
15+
pysam==0.22 && \
16+
pytest -m "validate" --cov="${PKG_DIR}" "${TEST_DIR}"
17+

hairpin2/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
def _set_version() -> str: # noqa: C901
2+
"""Set the package version from the project metadata in pyproject.toml."""
3+
from warnings import warn
4+
5+
fallback_version = "0.0.0"
6+
try:
7+
# importlib.metadata is present in Python 3.8 and later
8+
import importlib.metadata as importlib_metadata
9+
except ImportError:
10+
# use the shim package importlib-metadata pre-3.8
11+
import importlib_metadata as importlib_metadata
12+
13+
try:
14+
# __package__ allows for the case where __name__ is "__main__"
15+
version = importlib_metadata.version(__package__ or __name__)
16+
except importlib_metadata.PackageNotFoundError:
17+
version = fallback_version
18+
19+
if version == fallback_version:
20+
msg = (
21+
f"Package version will be {fallback_version} because Python could not find "
22+
f"package {__package__ or __name__} in project metadata. Either the "
23+
"version was not set in pyproject.toml or the package was not installed. "
24+
"If developing code, please install the package in editable "
25+
"mode with `poetry install` or `pip install -e .`"
26+
)
27+
warn(msg)
28+
return version
29+
30+
31+
__version__ = _set_version()

hairpin2/constants.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# hairpin2
2+
#
3+
# Copyright (C) 2024 Genome Research Ltd.
4+
#
5+
# Author: Alex Byrne <ab63@sanger.ac.uk>
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in all
15+
# copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
# SOFTWARE.
24+
25+
from enum import IntEnum, Flag
26+
from typing import Callable
27+
import dataclasses as d
28+
29+
EXIT_SUCCESS = 0
30+
EXIT_FAILURE = 1
31+
32+
DEFAULTS: dict[str, int | float] = dict((('al_filter_threshold', 0.93),
33+
('min_clip_quality', 35),
34+
('min_mapping_quality', 11),
35+
('min_base_quality', 25),
36+
('max_read_span', 6),
37+
('edge_definition', 0.15),
38+
('edge_fraction', 0.9),
39+
('min_MAD_one_strand', 0),
40+
('min_sd_one_strand', 4),
41+
('min_MAD_both_strand_weak', 2),
42+
('min_sd_both_strand_weak', 2),
43+
('min_MAD_both_strand_strong', 1),
44+
('min_sd_both_strand_strong', 10),
45+
('min_reads', 1)))
46+
47+
FiltCodes = IntEnum('FiltCodes',
48+
['SIXTYAI',
49+
'SIXTYBI',
50+
'ON_THRESHOLD',
51+
'INSUFFICIENT_READS',
52+
'NO_MUTANTS'],
53+
start=0)
54+
Ops = IntEnum('Ops',
55+
['MATCH',
56+
'INS',
57+
'DEL',
58+
'SKIP',
59+
'SOFT',
60+
'HARD',
61+
'PAD',
62+
'EQUAL',
63+
'DIFF',
64+
'BACK'],
65+
start=0)
66+
ValidatorFlags = Flag('ValidatorFlags',
67+
['CLEAR',
68+
'FLAG',
69+
'MAPQUAL',
70+
'READ_FIELDS_MISSING',
71+
'NOT_ALIGNED',
72+
'BAD_OP',
73+
'NOT_ALT',
74+
'BASEQUAL',
75+
'SHORT',
76+
'CLIPQUAL',
77+
'OVERLAP'],
78+
start=0)
79+
80+
81+
class NoAlts(ValueError):
82+
pass
83+
84+
85+
class NoMutants(ValueError):
86+
pass
87+
88+
89+
@d.dataclass
90+
class FilterData:
91+
name: str
92+
flag: bool = False
93+
code: int | None = None
94+
95+
def set(self):
96+
self.flag = True
97+
98+
def __iter__(self):
99+
return (getattr(self, field.name) for field in d.fields(self))
100+
101+
102+
@d.dataclass
103+
class ADFilter(FilterData):
104+
name: str = d.field(default='ADF')
105+
106+
107+
@d.dataclass
108+
class ALFilter(FilterData):
109+
name: str = d.field(default='ALF')
110+
avg_as: float | None = None
111+
112+
113+
@d.dataclass
114+
class Filters:
115+
AL: ALFilter
116+
HP: ADFilter
117+
118+
def __iter__(self):
119+
return (getattr(self, field.name) for field in d.fields(self))
120+
121+
def fill_field(self, field_name, value):
122+
if hasattr(self, field_name):
123+
setattr(self, field_name, value)
124+
else:
125+
raise AttributeError
126+
127+
def get_field(self, field_name):
128+
if hasattr(self, field_name):
129+
return getattr(self, field_name)
130+
else:
131+
raise AttributeError
132+
133+
134+
FiltReturn = Callable[..., Filters]
135+
FlagReturn = Callable[..., int]

0 commit comments

Comments
 (0)