bedspec 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
CONTRIBUTING.md ADDED
@@ -0,0 +1,94 @@
1
+ # Development and Testing
2
+
3
+ ## Primary Development Commands
4
+
5
+ To check and resolve linting issues in the codebase, run:
6
+
7
+ ```console
8
+ poetry run ruff check --fix
9
+ ```
10
+
11
+ To check and resolve formatting issues in the codebase, run:
12
+
13
+ ```console
14
+ poetry run ruff format
15
+ ```
16
+
17
+ To check the unit tests in the codebase, run:
18
+
19
+ ```console
20
+ poetry run pytest
21
+ ```
22
+
23
+ To check the typing in the codebase, run:
24
+
25
+ ```console
26
+ poetry run mypy
27
+ ```
28
+
29
+ To generate a code coverage report after testing locally, run:
30
+
31
+ ```console
32
+ poetry run coverage html
33
+ ```
34
+
35
+ To check the lock file is up-to-date:
36
+
37
+ ```console
38
+ poetry check --lock
39
+ ```
40
+
41
+ ## Shortcut Task Commands
42
+
43
+ To be able to run shortcut task commands, first install the Poetry plugin [`poethepoet`](https://poethepoet.natn.io/index.html):
44
+
45
+ ```console
46
+ poetry self add 'poethepoet[poetry_plugin]'
47
+ ```
48
+
49
+ > [!NOTE]
50
+ > Upon the release of Poetry [v2.0.0](https://github.com/orgs/python-poetry/discussions/9793#discussioncomment-11043205), Poetry will automatically support bootstrap installation of [project-specific plugins](https://github.com/python-poetry/poetry/pull/9547) and installation of the task runner will become automatic for this project.
51
+ > The `pyproject.toml` syntax will be:
52
+ >
53
+ > ```toml
54
+ > [tool.poetry]
55
+ > requires-poetry = ">=2.0"
56
+ >
57
+ > [tool.poetry.requires-plugins]
58
+ > poethepoet = ">=0.29"
59
+ > ```
60
+
61
+ ### For Running Individual Checks
62
+
63
+ ```console
64
+ poetry task check-lock
65
+ poetry task check-format
66
+ poetry task check-lint
67
+ poetry task check-tests
68
+ poetry task check-typing
69
+ ```
70
+
71
+ ### For Running All Checks
72
+
73
+ ```console
74
+ poetry task check-all
75
+ ```
76
+
77
+ ### For Running Individual Fixes
78
+
79
+ ```console
80
+ poetry task fix-format
81
+ poetry task fix-lint
82
+ ```
83
+
84
+ ### For Running All Fixes
85
+
86
+ ```console
87
+ poetry task fix-all
88
+ ```
89
+
90
+ ### For Running All Fixes and Checks
91
+
92
+ ```console
93
+ poetry task fix-and-check-all
94
+ ```
LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2024 Clint Valentine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
bedspec/__init__.py ADDED
@@ -0,0 +1,43 @@
1
+ from ._bedspec import Bed2
2
+ from ._bedspec import Bed3
3
+ from ._bedspec import Bed4
4
+ from ._bedspec import Bed5
5
+ from ._bedspec import Bed6
6
+ from ._bedspec import Bed12
7
+ from ._bedspec import BedColor
8
+ from ._bedspec import BedGraph
9
+ from ._bedspec import BedLike
10
+ from ._bedspec import BedPE
11
+ from ._bedspec import BedStrand
12
+ from ._bedspec import BedType
13
+ from ._bedspec import Named
14
+ from ._bedspec import PairBed
15
+ from ._bedspec import PointBed
16
+ from ._bedspec import ReferenceSpan
17
+ from ._bedspec import SimpleBed
18
+ from ._bedspec import Stranded
19
+ from ._reader import BedReader
20
+ from ._writer import BedWriter
21
+
22
+ __all__ = [
23
+ "Bed2",
24
+ "Bed3",
25
+ "Bed4",
26
+ "Bed5",
27
+ "Bed6",
28
+ "Bed12",
29
+ "BedColor",
30
+ "BedGraph",
31
+ "BedLike",
32
+ "BedPE",
33
+ "BedStrand",
34
+ "BedType",
35
+ "Named",
36
+ "PairBed",
37
+ "PointBed",
38
+ "ReferenceSpan",
39
+ "SimpleBed",
40
+ "Stranded",
41
+ "BedReader",
42
+ "BedWriter",
43
+ ]
bedspec/_bedspec.py ADDED
@@ -0,0 +1,370 @@
1
+ import dataclasses
2
+ from abc import ABC
3
+ from abc import abstractmethod
4
+ from collections.abc import Iterator
5
+ from dataclasses import Field
6
+ from dataclasses import dataclass
7
+ from dataclasses import field
8
+ from enum import Enum
9
+ from enum import unique
10
+ from typing import Any
11
+ from typing import ClassVar
12
+ from typing import Protocol
13
+ from typing import TypeVar
14
+ from typing import final
15
+ from typing import runtime_checkable
16
+
17
+ from typing_extensions import Self
18
+ from typing_extensions import override
19
+
20
+ COMMENT_PREFIXES: set[str] = {"#", "browser", "track"}
21
+ """The set of BED comment prefixes that this library supports."""
22
+
23
+ MISSING_FIELD: str = "."
24
+ """The string used to indicate a missing field in a BED record."""
25
+
26
+
27
+ @runtime_checkable
28
+ class DataclassInstance(Protocol):
29
+ """A protocol for objects that are dataclass instances."""
30
+
31
+ __dataclass_fields__: ClassVar[dict[str, Field[Any]]]
32
+
33
+
34
+ @unique
35
+ class BedStrand(str, Enum):
36
+ """BED strands for forward and reverse orientations."""
37
+
38
+ Positive = "+"
39
+ """The positive BED strand."""
40
+
41
+ Negative = "-"
42
+ """The negative BED strand."""
43
+
44
+ def opposite(self) -> "BedStrand":
45
+ """Return the opposite BED strand."""
46
+ if self is BedStrand.Positive:
47
+ return BedStrand.Negative
48
+ else:
49
+ return BedStrand.Positive
50
+
51
+ @override
52
+ def __str__(self) -> str:
53
+ """Return this strand as a string."""
54
+ return self.value
55
+
56
+
57
+ @runtime_checkable
58
+ class ReferenceSpan(Protocol):
59
+ """A structural protocol for 0-based half-open objects located on a reference sequence."""
60
+
61
+ refname: str
62
+ start: int
63
+ end: int
64
+
65
+
66
+ @runtime_checkable
67
+ class Named(Protocol):
68
+ """A structural protocol for a named BED type."""
69
+
70
+ name: str | None
71
+
72
+
73
+ @runtime_checkable
74
+ class Stranded(Protocol):
75
+ """A structural protocol for stranded BED types."""
76
+
77
+ strand: BedStrand | None
78
+
79
+
80
+ class BedLike(ABC, DataclassInstance):
81
+ """An abstract base class for all types of BED records."""
82
+
83
+ @abstractmethod
84
+ def territory(self) -> Iterator[ReferenceSpan]:
85
+ """Return intervals that describe the territory of this BED record."""
86
+
87
+
88
+ BedType = TypeVar("BedType", bound=BedLike)
89
+ """A type variable for any kind of BED record type."""
90
+
91
+
92
+ @dataclass
93
+ class PointBed(BedLike, ABC):
94
+ """An abstract class for a BED record that describes a 0-based 1-length point."""
95
+
96
+ refname: str
97
+ start: int
98
+
99
+ def __init_subclass__(cls) -> None:
100
+ if not dataclasses.is_dataclass(cls):
101
+ raise TypeError("You must annotate custom BED class definitions with @dataclass!")
102
+ return super().__init_subclass__()
103
+
104
+ @final
105
+ def __len__(self) -> int:
106
+ """The length of this record."""
107
+ return 1
108
+
109
+ @override
110
+ def territory(self) -> Iterator[ReferenceSpan]:
111
+ """Return the territory of a single point BED record which is 1-length."""
112
+ yield Bed3(refname=self.refname, start=self.start, end=self.start + 1)
113
+
114
+
115
+ @dataclass
116
+ class SimpleBed(BedLike, ReferenceSpan, ABC):
117
+ """An abstract class for a BED record that describes a contiguous linear interval."""
118
+
119
+ refname: str
120
+ start: int
121
+ end: int
122
+
123
+ def __init_subclass__(cls) -> None:
124
+ if not dataclasses.is_dataclass(cls):
125
+ raise TypeError("You must annotate custom BED class definitions with @dataclass!")
126
+ return super().__init_subclass__()
127
+
128
+ def __post_init__(self) -> None:
129
+ """Validate this linear BED record."""
130
+ if self.start >= self.end or self.start < 0:
131
+ raise ValueError("start must be greater than 0 and less than end!")
132
+
133
+ @final
134
+ def __len__(self) -> int:
135
+ """The length of this record."""
136
+ return self.end - self.start
137
+
138
+ @override
139
+ def territory(self) -> Iterator[ReferenceSpan]:
140
+ """Return the territory of a linear BED record which is just itself."""
141
+ yield self
142
+
143
+
144
+ @dataclass
145
+ class PairBed(BedLike, ABC):
146
+ """An abstract base class for a BED record that describes a pair of linear linear intervals."""
147
+
148
+ refname1: str
149
+ start1: int
150
+ end1: int
151
+ refname2: str
152
+ start2: int
153
+ end2: int
154
+
155
+ def __init_subclass__(cls) -> None:
156
+ if not dataclasses.is_dataclass(cls):
157
+ raise TypeError("You must annotate custom BED class definitions with @dataclass!")
158
+ return super().__init_subclass__()
159
+
160
+ def __post_init__(self) -> None:
161
+ """Validate this pair of BED records."""
162
+ if self.start1 >= self.end1 or self.start1 < 0:
163
+ raise ValueError("start1 must be greater than 0 and less than end1!")
164
+ if self.start2 >= self.end2 or self.start2 < 0:
165
+ raise ValueError("start2 must be greater than 0 and less than end2!")
166
+
167
+ @property
168
+ def bed1(self) -> SimpleBed:
169
+ """The first of the two intervals."""
170
+ return Bed3(refname=self.refname1, start=self.start1, end=self.end1)
171
+
172
+ @property
173
+ def bed2(self) -> SimpleBed:
174
+ """The second of the two intervals."""
175
+ return Bed3(refname=self.refname2, start=self.start2, end=self.end2)
176
+
177
+ @override
178
+ def territory(self) -> Iterator[ReferenceSpan]:
179
+ """Return the territory of this BED record which are two intervals."""
180
+ yield self.bed1
181
+ yield self.bed2
182
+
183
+
184
+ @dataclass(slots=True, unsafe_hash=True)
185
+ class BedColor:
186
+ """The color of a BED record in red, green, and blue color values."""
187
+
188
+ r: int
189
+ g: int
190
+ b: int
191
+
192
+ def __post_init__(self) -> None:
193
+ """Validate that all color values are well-formatted."""
194
+ if any(value > 255 or value < 0 for value in (self.r, self.g, self.b)):
195
+ raise ValueError(f"RGB color values must be in the range [0, 255] but found: {self}")
196
+
197
+ @classmethod
198
+ def from_string(cls, string: str) -> Self:
199
+ """Build a BED color instance from a string."""
200
+ try:
201
+ r, g, b = map(int, string.split(","))
202
+ except ValueError as error:
203
+ raise ValueError(f"Invalid string '{string}'. Expected 'int,int,int'!") from error
204
+ return cls(r, g, b)
205
+
206
+ @override
207
+ def __str__(self) -> str:
208
+ """Return a comma-delimited string representation of this BED color."""
209
+ return f"{self.r},{self.g},{self.b}"
210
+
211
+
212
+ @dataclass(slots=True, unsafe_hash=True)
213
+ class Bed2(PointBed):
214
+ """A BED2 record that describes a single 0-based 1-length point."""
215
+
216
+ refname: str
217
+ start: int
218
+
219
+
220
+ @dataclass(slots=True, unsafe_hash=True)
221
+ class Bed3(SimpleBed):
222
+ """A BED3 record that describes a contiguous linear interval."""
223
+
224
+ refname: str
225
+ start: int = field(kw_only=True)
226
+ end: int = field(kw_only=True)
227
+
228
+
229
+ @dataclass(slots=True, unsafe_hash=True)
230
+ class Bed4(SimpleBed):
231
+ """A BED4 record that describes a contiguous linear interval."""
232
+
233
+ refname: str
234
+ start: int = field(kw_only=True)
235
+ end: int = field(kw_only=True)
236
+ name: str | None = field(kw_only=True)
237
+
238
+
239
+ @dataclass(slots=True, unsafe_hash=True)
240
+ class Bed5(SimpleBed, Named):
241
+ """A BED5 record that describes a contiguous linear interval."""
242
+
243
+ refname: str
244
+ start: int = field(kw_only=True)
245
+ end: int = field(kw_only=True)
246
+ name: str | None = field(kw_only=True)
247
+ score: int | None = field(kw_only=True)
248
+
249
+
250
+ @dataclass(slots=True, unsafe_hash=True)
251
+ class Bed6(SimpleBed, Named, Stranded):
252
+ """A BED6 record that describes a contiguous linear interval."""
253
+
254
+ refname: str
255
+ start: int = field(kw_only=True)
256
+ end: int = field(kw_only=True)
257
+ name: str | None = field(kw_only=True)
258
+ score: int | None = field(kw_only=True)
259
+ strand: BedStrand | None = field(kw_only=True)
260
+
261
+
262
+ @dataclass(slots=True, unsafe_hash=True)
263
+ class Bed12(SimpleBed, Named, Stranded):
264
+ """A BED12 record that describes a contiguous linear interval."""
265
+
266
+ refname: str
267
+ start: int = field(kw_only=True)
268
+ end: int = field(kw_only=True)
269
+ name: str | None = field(kw_only=True)
270
+ score: int | None = field(kw_only=True)
271
+ strand: BedStrand | None = field(kw_only=True)
272
+ thick_start: int | None = field(kw_only=True)
273
+ thick_end: int | None = field(kw_only=True)
274
+ item_rgb: BedColor | None = field(kw_only=True)
275
+ block_count: int | None = field(kw_only=True)
276
+ block_sizes: list[int] | None = field(kw_only=True)
277
+ block_starts: list[int] | None = field(kw_only=True)
278
+
279
+ def __post_init__(self) -> None:
280
+ """Validate this BED12 record."""
281
+ super(Bed12, self).__post_init__()
282
+ if (self.thick_start is None) != (self.thick_end is None):
283
+ raise ValueError("thick_start and thick_end must both be None or both be set!")
284
+ if self.block_count is None:
285
+ if self.block_sizes is not None or self.block_starts is not None:
286
+ raise ValueError("block_count, block_sizes, block_starts must all be set or unset!")
287
+ else:
288
+ if self.block_sizes is None or self.block_starts is None:
289
+ raise ValueError("block_count, block_sizes, block_starts must all be set or unset!")
290
+ if self.block_count <= 0:
291
+ raise ValueError("When set, block_count must be greater than or equal to 1!")
292
+ if self.block_count != len(self.block_sizes) or self.block_count != len(
293
+ self.block_starts
294
+ ):
295
+ raise ValueError("Length of block_sizes and block_starts must equal block_count!")
296
+ if self.block_starts[0] != 0:
297
+ raise ValueError("block_starts must start with 0!")
298
+ if any(size <= 0 for size in self.block_sizes):
299
+ raise ValueError("All sizes in block_size must be greater than or equal to one!")
300
+ if (self.start + self.block_starts[-1] + self.block_sizes[-1]) != self.end:
301
+ raise ValueError("The last defined block's end must be equal to the BED end!")
302
+
303
+
304
+ @dataclass(slots=True, unsafe_hash=True)
305
+ class BedGraph(SimpleBed):
306
+ """A bedGraph feature for continuous-valued data."""
307
+
308
+ refname: str
309
+ start: int = field(kw_only=True)
310
+ end: int = field(kw_only=True)
311
+ value: float = field(kw_only=True)
312
+
313
+
314
+ @dataclass(slots=True, unsafe_hash=True)
315
+ class BedPE(PairBed, Named):
316
+ """A BED record that describes a pair of BED records as per the bedtools spec."""
317
+
318
+ refname1: str = field(kw_only=True)
319
+ start1: int = field(kw_only=True)
320
+ end1: int = field(kw_only=True)
321
+ refname2: str = field(kw_only=True)
322
+ start2: int = field(kw_only=True)
323
+ end2: int = field(kw_only=True)
324
+ name: str | None = field(kw_only=True)
325
+ score: int | None = field(kw_only=True)
326
+ strand1: BedStrand | None = field(kw_only=True)
327
+ strand2: BedStrand | None = field(kw_only=True)
328
+
329
+ @property
330
+ @override
331
+ def bed1(self) -> Bed6:
332
+ """The first of the two intervals as a BED6 record."""
333
+ return Bed6(
334
+ refname=self.refname1,
335
+ start=self.start1,
336
+ end=self.end1,
337
+ name=self.name,
338
+ score=self.score,
339
+ strand=self.strand1,
340
+ )
341
+
342
+ @property
343
+ @override
344
+ def bed2(self) -> Bed6:
345
+ """The second of the two intervals as a BED6 record."""
346
+ return Bed6(
347
+ refname=self.refname2,
348
+ start=self.start2,
349
+ end=self.end2,
350
+ name=self.name,
351
+ score=self.score,
352
+ strand=self.strand2,
353
+ )
354
+
355
+ @classmethod
356
+ def from_bed6(
357
+ cls, bed1: Bed6, bed2: Bed6, name: str | None = None, score: int | None = None
358
+ ) -> Self:
359
+ return cls(
360
+ refname1=bed1.refname,
361
+ start1=bed1.start,
362
+ end1=bed1.end,
363
+ refname2=bed2.refname,
364
+ start2=bed2.start,
365
+ end2=bed2.end,
366
+ name=name,
367
+ score=score,
368
+ strand1=bed1.strand,
369
+ strand2=bed2.strand,
370
+ )
bedspec/_reader.py ADDED
@@ -0,0 +1,94 @@
1
+ from io import TextIOWrapper
2
+ from pathlib import Path
3
+ from types import NoneType
4
+ from types import UnionType
5
+ from typing import Any
6
+ from typing import get_args
7
+ from typing import get_origin
8
+
9
+ from typeline import TsvRecordReader
10
+ from typing_extensions import Self
11
+ from typing_extensions import override
12
+
13
+ from bedspec._bedspec import COMMENT_PREFIXES
14
+ from bedspec._bedspec import MISSING_FIELD
15
+ from bedspec._bedspec import BedColor
16
+ from bedspec._bedspec import BedStrand
17
+ from bedspec._bedspec import BedType
18
+
19
+
20
+ class BedReader(TsvRecordReader[BedType]):
21
+ """A reader of BED records."""
22
+
23
+ @override
24
+ def __init__(
25
+ self,
26
+ handle: TextIOWrapper,
27
+ record_type: type[BedType],
28
+ /,
29
+ header: bool = False,
30
+ comment_prefixes: set[str] = COMMENT_PREFIXES,
31
+ ):
32
+ """Instantiate a new BED reader.
33
+
34
+ Args:
35
+ handle: a file-like object to read delimited data from.
36
+ record_type: the type of BED record we will be writing.
37
+ header: whether we expect the first line to be a header or not.
38
+ comment_prefixes: skip lines that have any of these string prefixes.
39
+ """
40
+ super().__init__(handle, record_type, header=header, comment_prefixes=comment_prefixes)
41
+
42
+ @override
43
+ def _decode(self, field_type: type[Any] | str | Any, item: str) -> str:
44
+ """A callback for overriding the string formatting of builtin and custom types."""
45
+ if field_type is BedStrand:
46
+ return f'"{item}"'
47
+ elif field_type is BedColor:
48
+ color = BedColor.from_string(item)
49
+ return f'{{"r":{color.r},"g":{color.g},"b":{color.b}}}'
50
+
51
+ is_union: bool = isinstance(field_type, UnionType)
52
+ type_args: tuple[type, ...] = get_args(field_type)
53
+ is_optional: bool = is_union and NoneType in type_args
54
+
55
+ if is_optional:
56
+ if item == MISSING_FIELD:
57
+ return "null"
58
+ elif type_args.count(BedStrand) == 1:
59
+ return f'"{item}"'
60
+ elif type_args.count(BedColor) == 1:
61
+ if item == "0":
62
+ return "null"
63
+ else:
64
+ color = BedColor.from_string(item)
65
+ return f'{{"r":{color.r},"g":{color.g},"b":{color.b}}}'
66
+
67
+ type_origin: type | None = get_origin(field_type)
68
+
69
+ if type_origin in (frozenset, list, set, tuple):
70
+ return f"[{item.rstrip(',')}]"
71
+
72
+ return super()._decode(field_type, item=item)
73
+
74
+ @classmethod
75
+ @override
76
+ def from_path(
77
+ cls,
78
+ path: Path | str,
79
+ record_type: type[BedType],
80
+ /,
81
+ header: bool = False,
82
+ comment_prefixes: set[str] = COMMENT_PREFIXES,
83
+ ) -> Self:
84
+ """Construct a BED reader from a file path.
85
+
86
+ Args:
87
+ path: the path to the file to read delimited data from.
88
+ record_type: the type of the object we will be writing.
89
+ header: whether we expect the first line to be a header or not.
90
+ comment_prefixes: skip lines that have any of these string prefixes.
91
+ """
92
+ handle = Path(path).open("r")
93
+ reader = cls(handle, record_type, header=header, comment_prefixes=comment_prefixes)
94
+ return reader
bedspec/_writer.py ADDED
@@ -0,0 +1,29 @@
1
+ from typing import Any
2
+
3
+ from typeline import TsvRecordWriter
4
+ from typing_extensions import override
5
+
6
+ from bedspec._bedspec import COMMENT_PREFIXES
7
+ from bedspec._bedspec import BedColor
8
+ from bedspec._bedspec import BedType
9
+
10
+
11
+ class BedWriter(TsvRecordWriter[BedType]):
12
+ """A writer for writing dataclasses into BED text data."""
13
+
14
+ @override
15
+ def _encode(self, item: Any) -> Any:
16
+ """A callback for overriding the encoding of builtin types and custom types."""
17
+ if item is None:
18
+ return "."
19
+ elif isinstance(item, (frozenset, list, set, tuple)):
20
+ return ",".join(map(str, item)) # pyright: ignore[reportUnknownArgumentType]
21
+ elif isinstance(item, BedColor):
22
+ return str(item)
23
+ return super()._encode(item=item)
24
+
25
+ def write_comment(self, comment: str) -> None:
26
+ """Write a comment to the BED output."""
27
+ for line in comment.splitlines():
28
+ prefix = "" if any(line.startswith(prefix) for prefix in COMMENT_PREFIXES) else "# "
29
+ _ = self._handle.write(f"{prefix}{line}\n")
@@ -0,0 +1,5 @@
1
+ from ._overlap import OverlapDetector
2
+
3
+ __all__ = [
4
+ "OverlapDetector",
5
+ ]
@@ -0,0 +1,86 @@
1
+ from collections import defaultdict
2
+ from collections.abc import Iterable
3
+ from collections.abc import Iterator
4
+ from itertools import chain
5
+ from typing import Generic
6
+ from typing import TypeAlias
7
+ from typing import TypeVar
8
+
9
+ from superintervals import ( # type: ignore[import-untyped] # pyright: ignore[reportMissingTypeStubs]
10
+ IntervalSet, # pyright: ignore[reportUnknownVariableType]
11
+ )
12
+ from typing_extensions import override
13
+
14
+ from bedspec._bedspec import ReferenceSpan
15
+
16
+ ReferenceSpanType = TypeVar("ReferenceSpanType", bound=ReferenceSpan)
17
+ """Type variable for features stored within the overlap detector."""
18
+
19
+ Refname: TypeAlias = str
20
+ """A type alias for a reference sequence name string."""
21
+
22
+ IntervalTree: TypeAlias = IntervalSet # pyright: ignore[reportUnknownVariableType]
23
+ """A type alias for the untyped interval set."""
24
+
25
+
26
+ class OverlapDetector(Iterable[ReferenceSpanType], Generic[ReferenceSpanType]):
27
+ """Detects and returns overlaps between a collection of reference features and query feature.
28
+
29
+ The overlap detector may be built with any feature-like Python object that has the following
30
+ properties:
31
+
32
+ * `refname`: The reference sequence name
33
+ * `start`: A 0-based start position
34
+ * `end`: A 0-based half-open end position
35
+
36
+ This detector is most efficiently used when all features to be queried are added ahead of time.
37
+ """
38
+
39
+ def __init__(self, features: Iterable[ReferenceSpanType] | None = None) -> None:
40
+ self._refname_to_features: dict[Refname, list[ReferenceSpanType]] = defaultdict(list)
41
+ self._refname_to_tree: dict[Refname, IntervalTree] = defaultdict(IntervalTree) # pyright: ignore[reportUnknownArgumentType]
42
+ self._refname_to_is_indexed: dict[Refname, bool] = defaultdict(lambda: False)
43
+ if features is not None:
44
+ self.add(*features)
45
+
46
+ @override
47
+ def __iter__(self) -> Iterator[ReferenceSpanType]:
48
+ """Iterate over the features in the overlap detector."""
49
+ return chain(*self._refname_to_features.values())
50
+
51
+ def add(self, *features: ReferenceSpanType) -> None:
52
+ """Add a feature to this overlap detector."""
53
+ for feature in features:
54
+ refname: Refname = feature.refname
55
+ feature_index: int = len(self._refname_to_features[refname])
56
+
57
+ self._refname_to_features[refname].append(feature)
58
+ self._refname_to_tree[refname].add(feature.start, feature.end - 1, feature_index) # pyright: ignore[reportUnknownMemberType]
59
+ self._refname_to_is_indexed[refname] = False # mark that this tree needs re-indexing
60
+
61
+ def overlapping(self, feature: ReferenceSpan) -> Iterator[ReferenceSpanType]:
62
+ """Yields all the overlapping features for a given query feature."""
63
+ refname: Refname = feature.refname
64
+
65
+ if refname in self._refname_to_tree.keys() and not self._refname_to_is_indexed[refname]: # pyright: ignore[reportUnknownMemberType]
66
+ self._refname_to_tree[refname].index() # pyright: ignore[reportUnknownMemberType]
67
+
68
+ index: int
69
+ for index in self._refname_to_tree[refname].find_overlaps(feature.start, feature.end - 1): # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
70
+ yield self._refname_to_features[refname][index]
71
+
72
+ def overlaps(self, feature: ReferenceSpan) -> bool:
73
+ """Determine if a query feature overlaps any other features."""
74
+ return next(self.overlapping(feature), None) is not None
75
+
76
+ def enclosing(self, feature: ReferenceSpan) -> Iterator[ReferenceSpanType]:
77
+ """Yields all the overlapping features that completely enclose the given query feature."""
78
+ for overlap in self.overlapping(feature):
79
+ if feature.start >= overlap.start and feature.end <= overlap.end:
80
+ yield overlap
81
+
82
+ def enclosed_by(self, feature: ReferenceSpan) -> Iterator[ReferenceSpanType]:
83
+ """Yields all the overlapping features that are enclosed by the given query feature."""
84
+ for overlap in self.overlapping(feature):
85
+ if feature.start <= overlap.start and feature.end >= overlap.end:
86
+ yield overlap
bedspec/py.typed ADDED
File without changes
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2024 Clint Valentine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.1
2
+ Name: bedspec
3
+ Version: 0.6.0
4
+ Summary: An HTS-specs compliant BED toolkit.
5
+ Home-page: https://github.com/clintval/bedspec
6
+ License: MIT
7
+ Keywords: bioinformatics,BED,NGS,HTS,interval
8
+ Author: Clint Valentine
9
+ Author-email: valentine.clint@gmail.com
10
+ Requires-Python: >=3.10.0,<4.0.0
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: File Formats
24
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
25
+ Classifier: Topic :: Software Development :: Documentation
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Dist: superintervals (>=0.2.2,<0.3.0)
29
+ Requires-Dist: typeline (>=0.6,<0.7)
30
+ Requires-Dist: typing-extensions (>=4.12,<5.0)
31
+ Project-URL: Repository, https://github.com/clintval/bedspec
32
+ Description-Content-Type: text/markdown
33
+
34
+ # bedspec
35
+
36
+ [![PyPi Release](https://badge.fury.io/py/bedspec.svg)](https://badge.fury.io/py/bedspec)
37
+ [![CI](https://github.com/clintval/bedspec/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/clintval/bedspec/actions/workflows/tests.yml?query=branch%3Amain)
38
+ [![Python Versions](https://img.shields.io/badge/python-3.10_|_3.11_|_3.12-blue)](https://github.com/clintval/typeline)
39
+ [![basedpyright](https://img.shields.io/badge/basedpyright-checked-42b983)](https://docs.basedpyright.com/latest/)
40
+ [![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
41
+ [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
42
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://docs.astral.sh/ruff/)
43
+
44
+ An HTS-specs compliant BED toolkit.
45
+
46
+ ## Installation
47
+
48
+ The package can be installed with `pip`:
49
+
50
+ ```console
51
+ pip install bedspec
52
+ ```
53
+
54
+ ## Quickstart
55
+
56
+ ### Building a BED Feature
57
+
58
+ ```pycon
59
+ >>> from bedspec import Bed3
60
+ >>>
61
+ >>> bed = Bed3("chr1", start=2, end=8)
62
+
63
+ ```
64
+
65
+ ### Writing
66
+
67
+ ```pycon
68
+ >>> from bedspec import BedWriter
69
+ >>> from tempfile import NamedTemporaryFile
70
+ >>>
71
+ >>> temp_file = NamedTemporaryFile(mode="w+t", suffix=".txt")
72
+ >>>
73
+ >>> with BedWriter.from_path(temp_file.name, Bed3) as writer:
74
+ ... writer.write(bed)
75
+
76
+ ```
77
+
78
+ ### Reading
79
+
80
+ ```pycon
81
+ >>> from bedspec import BedReader
82
+ >>>
83
+ >>> with BedReader.from_path(temp_file.name, Bed3) as reader:
84
+ ... for bed in reader:
85
+ ... print(bed)
86
+ Bed3(refname='chr1', start=2, end=8)
87
+
88
+ ```
89
+
90
+ ### BED Types
91
+
92
+ This package provides builtin classes for the following BED formats:
93
+
94
+ ```pycon
95
+ >>> from bedspec import Bed2
96
+ >>> from bedspec import Bed3
97
+ >>> from bedspec import Bed4
98
+ >>> from bedspec import Bed5
99
+ >>> from bedspec import Bed6
100
+ >>> from bedspec import Bed12
101
+ >>> from bedspec import BedGraph
102
+ >>> from bedspec import BedPE
103
+
104
+ ```
105
+
106
+ ### Overlap Detection
107
+
108
+ Use a fast overlap detector for any collection of interval types, including third-party:
109
+
110
+ ```pycon
111
+ >>> from bedspec import Bed3, Bed4
112
+ >>> from bedspec.overlap import OverlapDetector
113
+ >>>
114
+ >>> bed1 = Bed3("chr1", start=1, end=4)
115
+ >>> bed2 = Bed3("chr1", start=5, end=9)
116
+ >>>
117
+ >>> detector = OverlapDetector[Bed3]([bed1, bed2])
118
+ >>>
119
+ >>> my_feature = Bed4("chr1", start=2, end=3, name="hi-mom")
120
+ >>> detector.overlaps(my_feature)
121
+ True
122
+
123
+ ```
124
+
125
+ The overlap detector supports the following operations:
126
+
127
+ - `overlapping`: return all overlapping features
128
+ - `overlaps`: test if any overlapping features exist
129
+ - `enclosed_by`: return those enclosed by the input feature
130
+ - `enclosing`: return those enclosing the input feature
131
+
132
+ ### Custom BED Types
133
+
134
+ To create a custom BED record, inherit from the relevant BED-type (`PointBed`, `SimpleBed`, `PairBed`).
135
+
136
+ For example, to create a custom BED3+1 class:
137
+
138
+ ```pycon
139
+ >>> from dataclasses import dataclass
140
+ >>>
141
+ >>> from bedspec import SimpleBed
142
+ >>>
143
+ >>> @dataclass
144
+ ... class Bed3Plus1(SimpleBed):
145
+ ... refname: str
146
+ ... start: int
147
+ ... end: int
148
+ ... my_custom_field: float | None
149
+
150
+ ```
151
+
152
+ ## Development and Testing
153
+
154
+ See the [contributing guide](./CONTRIBUTING.md) for more information.
155
+
@@ -0,0 +1,13 @@
1
+ CONTRIBUTING.md,sha256=selnIsM_6KTwdFyc0B3u4n2bM_sBAeSbQV-Fd7mzHuE,1803
2
+ LICENSE,sha256=nN9Rgc41QgFz1uJfX7AsXRQJxlcJotVEPTNPwUAqfys,1070
3
+ bedspec/__init__.py,sha256=MWneXWftcndpg-cSUkbLwuPMty0klBf_SpaaRm0jgKM,907
4
+ bedspec/_bedspec.py,sha256=5JypI3D9q1Jp326CHhU8v3hy9XONJv5Q79qc-2RRhzE,11721
5
+ bedspec/_reader.py,sha256=y4MqHYHvNwE-vk5Kpx2KP6PXtjc00S_D0fgE9hejzYU,3265
6
+ bedspec/_writer.py,sha256=KsF4A2q_fgCNFLnskEwyFW4NMwkF0JcXtrbvJ5UXbBI,1082
7
+ bedspec/overlap/__init__.py,sha256=aArIVkYIMBBC7YZkPP7KPcy6FjPZdKVl0Yi2as4pWNQ,76
8
+ bedspec/overlap/_overlap.py,sha256=s3yROqIs3AcJeCMdLWXBy4eoBz1aytGIwuQG33e-fcY,4153
9
+ bedspec/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ bedspec-0.6.0.dist-info/LICENSE,sha256=nN9Rgc41QgFz1uJfX7AsXRQJxlcJotVEPTNPwUAqfys,1070
11
+ bedspec-0.6.0.dist-info/METADATA,sha256=5yEWscD0bkFQHxJ_5mWKfZQ9R29aVvKgtbXFn9ANdkg,4547
12
+ bedspec-0.6.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
13
+ bedspec-0.6.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any