intranges 0.2.0__tar.gz
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.
- intranges-0.2.0/.gitattributes +1 -0
- intranges-0.2.0/.github/FUNDING.yml +1 -0
- intranges-0.2.0/.gitignore +12 -0
- intranges-0.2.0/CHANGELOG.md +49 -0
- intranges-0.2.0/CONTRIBUTING.md +19 -0
- intranges-0.2.0/IMPROVEMENTS-round.md +44 -0
- intranges-0.2.0/LICENSE +21 -0
- intranges-0.2.0/PKG-INFO +173 -0
- intranges-0.2.0/README.md +144 -0
- intranges-0.2.0/docs/GUIDE.md +148 -0
- intranges-0.2.0/pyproject.toml +56 -0
- intranges-0.2.0/requirements-dev.txt +3 -0
- intranges-0.2.0/scripts/check.ps1 +7 -0
- intranges-0.2.0/scripts/release.ps1 +28 -0
- intranges-0.2.0/src/intranges/__init__.py +50 -0
- intranges-0.2.0/src/intranges/_cli.py +174 -0
- intranges-0.2.0/src/intranges/_compact.py +72 -0
- intranges-0.2.0/src/intranges/_dialect.py +108 -0
- intranges-0.2.0/src/intranges/_errors.py +67 -0
- intranges-0.2.0/src/intranges/_intranges.py +199 -0
- intranges-0.2.0/src/intranges/_parse.py +189 -0
- intranges-0.2.0/src/intranges/_spans.py +81 -0
- intranges-0.2.0/src/intranges/_tokens.py +180 -0
- intranges-0.2.0/src/intranges/py.typed +0 -0
- intranges-0.2.0/tests/test_cli.py +103 -0
- intranges-0.2.0/tests/test_compact.py +44 -0
- intranges-0.2.0/tests/test_contract.py +178 -0
- intranges-0.2.0/tests/test_dialects.py +148 -0
- intranges-0.2.0/tests/test_e2e.py +77 -0
- intranges-0.2.0/tests/test_errors_and_policies.py +165 -0
- intranges-0.2.0/tests/test_intranges.py +134 -0
- intranges-0.2.0/tests/test_package.py +30 -0
- intranges-0.2.0/tests/test_parse.py +70 -0
- intranges-0.2.0/tests/test_property.py +136 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
buy_me_a_coffee: jlianesglrs
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `IntRanges`, an immutable `collections.abc.Set[int]` stored as spans. Membership,
|
|
14
|
+
`count`, iteration, union, intersection, difference, symmetric difference, subset
|
|
15
|
+
tests and `complement(lower, upper)` never expand values.
|
|
16
|
+
- `parse_ranges`, `count` and `normalize` read strings into spans, with
|
|
17
|
+
`on_overlap="merge" | "reject"` and `on_unsorted="allow" | "reject"` policies.
|
|
18
|
+
- `Dialect` and the presets `DEFAULT`, `PAGES`, `CPULIST`, `SLURM` and `HTTP`, plus the
|
|
19
|
+
`DIALECTS` registry: kernel `:used/group` and taskset `:N` strides, `N` and `all`,
|
|
20
|
+
Slurm steps and bracketed hostlist numbers, RFC 9110 open ends, suffix ranges and
|
|
21
|
+
clamping.
|
|
22
|
+
- Open-ended ranges such as `5-` when `upper=` is given; `lower=` and `upper=` bounds.
|
|
23
|
+
- Typed errors `RangeSyntaxError`, `DescendingRangeError`, `RangeOverlapError`,
|
|
24
|
+
`RangeOrderError`, `RangeBoundsError` and `RangeSizeError`. `RangeError` now carries
|
|
25
|
+
`text`, `token` and `offset` and offers `pointer()`.
|
|
26
|
+
- `parse` gains `dialect`, `lower`, `upper` and `max_size`; `compact` gains `dialect` and
|
|
27
|
+
`width`, and writes an `IntRanges` or a step-1 `range` without expanding it.
|
|
28
|
+
- `DEFAULT_MAX_SIZE` and `DEFAULT_MAX_SPANS`.
|
|
29
|
+
- The `intranges` command with `expand`, `compress`, `normalize`, `count`, `contains`
|
|
30
|
+
and `complement`.
|
|
31
|
+
- `docs/GUIDE.md`.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `parse` refuses to build more than 10,000,000 values unless `max_size` is raised or
|
|
36
|
+
set to `None`, so a string like `0-10000000000` fails fast instead of exhausting
|
|
37
|
+
memory.
|
|
38
|
+
- `compact` now rejects values that are not integers (such as floats) with `TypeError`
|
|
39
|
+
instead of writing them as text.
|
|
40
|
+
- Separators containing digits raise `ValueError`, since they make tokens ambiguous.
|
|
41
|
+
|
|
42
|
+
## [0.1.0]
|
|
43
|
+
|
|
44
|
+
- First release.
|
|
45
|
+
- `parse` expands a compact range string into a list of ints, with support for single
|
|
46
|
+
values, ranges, negatives, whitespace and configurable separators.
|
|
47
|
+
- `compact` collapses an iterable of ints into a range string, sorting and de-duplicating
|
|
48
|
+
the input first.
|
|
49
|
+
- `RangeError` is raised for malformed or descending tokens.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for taking the time. This project keeps a small, predictable shape.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install -r requirements-dev.txt
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Before you open a pull request
|
|
12
|
+
|
|
13
|
+
- Keep public API in `src/intranges/__init__.py`; internals live in `_`-prefixed modules.
|
|
14
|
+
- Add a test for the behaviour you change. Tests inject time, disk and network; they
|
|
15
|
+
never touch the real thing.
|
|
16
|
+
- Run `scripts/check.ps1` (format, lint, types, tests) and make sure it passes.
|
|
17
|
+
- One logical change per commit, imperative subject line.
|
|
18
|
+
|
|
19
|
+
Open an issue first if the change is large or shifts the public API.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Improvement round: ranked plan
|
|
2
|
+
|
|
3
|
+
Ranked by user value. Every item is additive: `parse`, `compact` and `RangeError` keep
|
|
4
|
+
their signatures and their documented behaviour.
|
|
5
|
+
|
|
6
|
+
1. **Lazy `IntRanges` set type.** An immutable `collections.abc.Set[int]` stored as sorted,
|
|
7
|
+
disjoint, inclusive spans. Membership is a binary search, `count` is arithmetic, and
|
|
8
|
+
iteration is streamed, so `"0-10000000000"` costs two integers. Union, intersection,
|
|
9
|
+
difference, symmetric difference, subset tests and `complement(lower, upper)` work on
|
|
10
|
+
spans and never expand values.
|
|
11
|
+
2. **Size guards.** `parse` refuses to build a list larger than `max_size` values
|
|
12
|
+
(default 10,000,000) and checks the total before allocating anything. Strided input
|
|
13
|
+
that would create more than `max_spans` spans raises too. Both raise
|
|
14
|
+
`RangeSizeError`.
|
|
15
|
+
3. **Typed errors that point at the bad token.** `RangeError` gains `text`, `token` and
|
|
16
|
+
`offset` attributes, and specific subclasses: `RangeSyntaxError`,
|
|
17
|
+
`DescendingRangeError`, `RangeOverlapError`, `RangeOrderError`, `RangeBoundsError`,
|
|
18
|
+
`RangeSizeError`.
|
|
19
|
+
All stay `ValueError` subclasses.
|
|
20
|
+
4. **Named dialects grounded in documented syntaxes.** A frozen `Dialect` dataclass plus
|
|
21
|
+
presets: `DEFAULT`, `PAGES` (CUPS `page-ranges`), `CPULIST` (Linux kernel cpu lists
|
|
22
|
+
with `:used/group`, util-linux `taskset` stride `:N`, `N` for the last CPU and `all`),
|
|
23
|
+
`SLURM` (sbatch `--array` step `:N`, optional hostlist brackets) and `HTTP`
|
|
24
|
+
(RFC 9110 byte ranges, open ends, suffix ranges, clamping). Custom dialects are a
|
|
25
|
+
`Dialect(...)` call away.
|
|
26
|
+
5. **Unambiguous negative-number rule.** A `-` directly before a digit at the start of a
|
|
27
|
+
bound is a sign; the separator is the first `-` after the first bound. Dialects that
|
|
28
|
+
disallow negatives reject `-3` with a clear message instead of guessing.
|
|
29
|
+
6. **Open-ended ranges with explicit bounds.** `"5-"` means "5 to the upper bound" and is
|
|
30
|
+
only accepted when the caller supplies `upper=`. Without it the error says so.
|
|
31
|
+
7. **Overlap and order policy.** `parse_ranges(..., on_overlap="merge" | "reject",
|
|
32
|
+
on_unsorted="allow" | "reject")`. Rejection names the offending token.
|
|
33
|
+
8. **Canonical compression and round trip.** `normalize(text)` returns the canonical
|
|
34
|
+
string; `compact` accepts an `IntRanges` without expanding it, a `dialect` and a
|
|
35
|
+
zero-pad `width`. `parse_ranges(compact(x)) == IntRanges(x)` for every finite set.
|
|
36
|
+
9. **`count(text)` without materialising** and bound checks via `lower=` / `upper=`.
|
|
37
|
+
10. **CLI** `intranges` with `expand`, `compress`, `normalize`, `count`, `contains` and
|
|
38
|
+
`complement`, dialect and bound options, runnable `--help` examples, errors on stderr
|
|
39
|
+
with exit code 1.
|
|
40
|
+
11. **Docs.** Docstrings on every public name, a `docs/GUIDE.md` covering grammar,
|
|
41
|
+
dialects, policies and recipes, and README sources for every dialect.
|
|
42
|
+
12. **Contracts and end-to-end tests.** A contract test over every public name, a
|
|
43
|
+
seeded property test against a brute-force `set` model, and a real wheel install
|
|
44
|
+
driving the console script.
|
intranges-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Javier Lianes
|
|
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.
|
intranges-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: intranges
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Typed conversion between range strings like 1-3,5 and ints, with a lazy set type, dialects and a CLI
|
|
5
|
+
Project-URL: Homepage, https://lianes8studio.com
|
|
6
|
+
Project-URL: Repository, https://github.com/JaviLianes8/intranges
|
|
7
|
+
Project-URL: Issues, https://github.com/JaviLianes8/intranges/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/JaviLianes8/intranges/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: LinkedIn, https://www.linkedin.com/in/jlianes/
|
|
10
|
+
Project-URL: Funding, https://buymeacoffee.com/jlianesglrs
|
|
11
|
+
Author-email: Javier Lianes <javier@lianes8studio.com>
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: cli,compact,cpulist,integers,intervals,page-ranges,parse,range-string,ranges
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Environment :: Console
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build; extra == 'dev'
|
|
24
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
27
|
+
Requires-Dist: twine; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# intranges
|
|
31
|
+
|
|
32
|
+
Typed, zero-dependency conversion between range strings like `1-3,5,7-9` and integers,
|
|
33
|
+
with a lazy set type that never expands huge ranges, named dialects for real-world
|
|
34
|
+
syntaxes, typed errors that point at the bad token, and a command-line tool.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
pip install intranges
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from intranges import compact, parse
|
|
46
|
+
|
|
47
|
+
parse("1-5,8,10-12")
|
|
48
|
+
# [1, 2, 3, 4, 5, 8, 10, 11, 12]
|
|
49
|
+
|
|
50
|
+
compact([6, 5, 1, 1, 3, 2])
|
|
51
|
+
# "1-3,5-6"
|
|
52
|
+
|
|
53
|
+
parse("-3--1, 0, 2")
|
|
54
|
+
# [-3, -2, -1, 0, 2]
|
|
55
|
+
|
|
56
|
+
parse("1..3;6", range_sep="..", list_sep=";")
|
|
57
|
+
# [1, 2, 3, 6]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`parse` keeps input order and repeats. It refuses to build more than 10,000,000 values
|
|
61
|
+
by default (`max_size=`), and it checks that before allocating anything.
|
|
62
|
+
|
|
63
|
+
### A set that never expands
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from intranges import IntRanges, parse_ranges, count, normalize
|
|
67
|
+
|
|
68
|
+
r = parse_ranges("0-10000000000,5") # two spans, not ten billion ints
|
|
69
|
+
r.count # 10000000001
|
|
70
|
+
7_000_000_000 in r # True, binary search
|
|
71
|
+
r.spans # ((0, 10000000000),)
|
|
72
|
+
|
|
73
|
+
free = IntRanges.parse("0-3,8-11").complement(0, 15)
|
|
74
|
+
str(free) # "4-7,12-15"
|
|
75
|
+
|
|
76
|
+
a, b = IntRanges.parse("1-10"), IntRanges.parse("5-15")
|
|
77
|
+
str(a & b), str(a - b), str(a ^ b) # ("5-10", "1-4", "1-4,11-15")
|
|
78
|
+
|
|
79
|
+
count("1-1000000,500000-2000000") # 2000000
|
|
80
|
+
normalize("8, 1-3, 2, 4") # "1-4,8"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`IntRanges` is an immutable `collections.abc.Set[int]`: `in`, iteration (ascending and
|
|
84
|
+
`reversed`), `len`, `|`, `&`, `-`, `^`, subset tests, `first`, `last`, `complement`,
|
|
85
|
+
hashing and pickling all work on spans.
|
|
86
|
+
|
|
87
|
+
### Dialects
|
|
88
|
+
|
|
89
|
+
| Dialect | Example | Source |
|
|
90
|
+
|-----------|--------------------------------|--------|
|
|
91
|
+
| `DEFAULT` | `-3--1,5,9-` (with `upper=`) | this library |
|
|
92
|
+
| `PAGES` | `1-4,7,9-12` | CUPS `-o page-ranges=1-4,7,9-12`, [Command-Line Printing and Options](https://www.cups.org/doc/options.html) |
|
|
93
|
+
| `CPULIST` | `0-3,8-11`, `100-2000:2/25`, `16-N`, `all` | Linux kernel, [kernel-parameters: CPU lists](https://docs.kernel.org/admin-guide/kernel-parameters.html) |
|
|
94
|
+
| `CPULIST` | `0-10:3` (stride) | util-linux [taskset(1)](https://man7.org/linux/man-pages/man1/taskset.1.html): "0-10:3 is interpreted as 0,3,6,9" |
|
|
95
|
+
| `SLURM` | `0-15:4`, `[1-3,5]`, `001-010` | [sbatch `--array`](https://slurm.schedmd.com/sbatch.html), hostlist numbers as in `tux[1-3]` from [scontrol](https://slurm.schedmd.com/scontrol.html) |
|
|
96
|
+
| `HTTP` | `bytes=0-499,9500-`, `-500` | [RFC 9110 section 14.1.2, Byte Ranges](https://www.rfc-editor.org/rfc/rfc9110#section-14.1.2) |
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from intranges import CPULIST, HTTP, SLURM, parse, parse_ranges, compact
|
|
100
|
+
|
|
101
|
+
parse("100-200:2/25", dialect=CPULIST) # [100, 101, 125, 126, 150, 151, 175, 176, 200]
|
|
102
|
+
parse("16-N", dialect=CPULIST, upper=19) # [16, 17, 18, 19]
|
|
103
|
+
parse("0-15:4", dialect=SLURM) # [0, 4, 8, 12]
|
|
104
|
+
parse_ranges("bytes=-500", dialect=HTTP, upper=9999).spans # ((9500, 9999),)
|
|
105
|
+
compact(range(1, 11), dialect=SLURM, width=3) # "001-010"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Open ends such as `"5-"` are accepted only when you pass `upper=`. Unsupported parts of
|
|
109
|
+
a source syntax are rejected rather than guessed, for example Slurm's `%` throttle
|
|
110
|
+
suffix. The HTTP dialect clamps ends past `upper` as RFC 9110 describes; `upper` is the
|
|
111
|
+
representation length minus one.
|
|
112
|
+
|
|
113
|
+
### Errors
|
|
114
|
+
|
|
115
|
+
Every error is a `RangeError`, a `ValueError` subclass, carrying `text`, `token` and
|
|
116
|
+
`offset`:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from intranges import RangeError, parse
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
parse("1-3, 5, x-9")
|
|
123
|
+
except RangeError as err:
|
|
124
|
+
print(err)
|
|
125
|
+
print(err.pointer())
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
prints
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
invalid range token 'x-9' for the default dialect at offset 8
|
|
132
|
+
1-3, 5, x-9
|
|
133
|
+
^^^
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Subclasses: `RangeSyntaxError`, `DescendingRangeError`, `RangeOverlapError`,
|
|
137
|
+
`RangeOrderError`, `RangeBoundsError`, `RangeSizeError`. Overlapping and unsorted tokens
|
|
138
|
+
are merged by default; pass `on_overlap="reject"` or `on_unsorted="reject"` to
|
|
139
|
+
`parse_ranges` to refuse them.
|
|
140
|
+
|
|
141
|
+
## Command line
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
intranges expand "1-3,7" # 1 2 3 7, one per line
|
|
145
|
+
intranges expand "0-10:3" --dialect cpulist -d , # 0,3,6,9
|
|
146
|
+
seq 1 5 | intranges compress # 1-5
|
|
147
|
+
intranges normalize "8, 1-3, 2, 4" # 1-4,8
|
|
148
|
+
intranges count "0-10000000000" # 10000000001
|
|
149
|
+
intranges contains "0-3,8-11" 9 12 # 9 yes, 12 no; exit code 2
|
|
150
|
+
intranges complement "0-3,8-11" --lower 0 --upper 15 # 4-7,12-15
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Errors go to stderr with exit code 1. Put `--` before a range string that starts with
|
|
154
|
+
`-`. See `intranges --help` and [the guide](docs/GUIDE.md) for everything else.
|
|
155
|
+
|
|
156
|
+
## Why
|
|
157
|
+
|
|
158
|
+
Expressing a set of integers as a compact string like `1-5,8,10-12` is common in page
|
|
159
|
+
selectors, CPU affinity, job arrays and config files, but converting it back and forth
|
|
160
|
+
usually ends up reimplemented in each project. `intspan` covers the idea with a
|
|
161
|
+
set-like class, yet it is untyped (it ships no `py.typed`) and expands its ranges in
|
|
162
|
+
memory. `intranges` is strictly typed, keeps ranges as spans, and follows documented
|
|
163
|
+
syntaxes instead of inventing new ones.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT. See [LICENSE](LICENSE).
|
|
168
|
+
|
|
169
|
+
## Links
|
|
170
|
+
|
|
171
|
+
- Website: https://lianes8studio.com
|
|
172
|
+
- GitHub: https://github.com/JaviLianes8
|
|
173
|
+
- LinkedIn: https://www.linkedin.com/in/jlianes/
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# intranges
|
|
2
|
+
|
|
3
|
+
Typed, zero-dependency conversion between range strings like `1-3,5,7-9` and integers,
|
|
4
|
+
with a lazy set type that never expands huge ranges, named dialects for real-world
|
|
5
|
+
syntaxes, typed errors that point at the bad token, and a command-line tool.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pip install intranges
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from intranges import compact, parse
|
|
17
|
+
|
|
18
|
+
parse("1-5,8,10-12")
|
|
19
|
+
# [1, 2, 3, 4, 5, 8, 10, 11, 12]
|
|
20
|
+
|
|
21
|
+
compact([6, 5, 1, 1, 3, 2])
|
|
22
|
+
# "1-3,5-6"
|
|
23
|
+
|
|
24
|
+
parse("-3--1, 0, 2")
|
|
25
|
+
# [-3, -2, -1, 0, 2]
|
|
26
|
+
|
|
27
|
+
parse("1..3;6", range_sep="..", list_sep=";")
|
|
28
|
+
# [1, 2, 3, 6]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`parse` keeps input order and repeats. It refuses to build more than 10,000,000 values
|
|
32
|
+
by default (`max_size=`), and it checks that before allocating anything.
|
|
33
|
+
|
|
34
|
+
### A set that never expands
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from intranges import IntRanges, parse_ranges, count, normalize
|
|
38
|
+
|
|
39
|
+
r = parse_ranges("0-10000000000,5") # two spans, not ten billion ints
|
|
40
|
+
r.count # 10000000001
|
|
41
|
+
7_000_000_000 in r # True, binary search
|
|
42
|
+
r.spans # ((0, 10000000000),)
|
|
43
|
+
|
|
44
|
+
free = IntRanges.parse("0-3,8-11").complement(0, 15)
|
|
45
|
+
str(free) # "4-7,12-15"
|
|
46
|
+
|
|
47
|
+
a, b = IntRanges.parse("1-10"), IntRanges.parse("5-15")
|
|
48
|
+
str(a & b), str(a - b), str(a ^ b) # ("5-10", "1-4", "1-4,11-15")
|
|
49
|
+
|
|
50
|
+
count("1-1000000,500000-2000000") # 2000000
|
|
51
|
+
normalize("8, 1-3, 2, 4") # "1-4,8"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`IntRanges` is an immutable `collections.abc.Set[int]`: `in`, iteration (ascending and
|
|
55
|
+
`reversed`), `len`, `|`, `&`, `-`, `^`, subset tests, `first`, `last`, `complement`,
|
|
56
|
+
hashing and pickling all work on spans.
|
|
57
|
+
|
|
58
|
+
### Dialects
|
|
59
|
+
|
|
60
|
+
| Dialect | Example | Source |
|
|
61
|
+
|-----------|--------------------------------|--------|
|
|
62
|
+
| `DEFAULT` | `-3--1,5,9-` (with `upper=`) | this library |
|
|
63
|
+
| `PAGES` | `1-4,7,9-12` | CUPS `-o page-ranges=1-4,7,9-12`, [Command-Line Printing and Options](https://www.cups.org/doc/options.html) |
|
|
64
|
+
| `CPULIST` | `0-3,8-11`, `100-2000:2/25`, `16-N`, `all` | Linux kernel, [kernel-parameters: CPU lists](https://docs.kernel.org/admin-guide/kernel-parameters.html) |
|
|
65
|
+
| `CPULIST` | `0-10:3` (stride) | util-linux [taskset(1)](https://man7.org/linux/man-pages/man1/taskset.1.html): "0-10:3 is interpreted as 0,3,6,9" |
|
|
66
|
+
| `SLURM` | `0-15:4`, `[1-3,5]`, `001-010` | [sbatch `--array`](https://slurm.schedmd.com/sbatch.html), hostlist numbers as in `tux[1-3]` from [scontrol](https://slurm.schedmd.com/scontrol.html) |
|
|
67
|
+
| `HTTP` | `bytes=0-499,9500-`, `-500` | [RFC 9110 section 14.1.2, Byte Ranges](https://www.rfc-editor.org/rfc/rfc9110#section-14.1.2) |
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from intranges import CPULIST, HTTP, SLURM, parse, parse_ranges, compact
|
|
71
|
+
|
|
72
|
+
parse("100-200:2/25", dialect=CPULIST) # [100, 101, 125, 126, 150, 151, 175, 176, 200]
|
|
73
|
+
parse("16-N", dialect=CPULIST, upper=19) # [16, 17, 18, 19]
|
|
74
|
+
parse("0-15:4", dialect=SLURM) # [0, 4, 8, 12]
|
|
75
|
+
parse_ranges("bytes=-500", dialect=HTTP, upper=9999).spans # ((9500, 9999),)
|
|
76
|
+
compact(range(1, 11), dialect=SLURM, width=3) # "001-010"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Open ends such as `"5-"` are accepted only when you pass `upper=`. Unsupported parts of
|
|
80
|
+
a source syntax are rejected rather than guessed, for example Slurm's `%` throttle
|
|
81
|
+
suffix. The HTTP dialect clamps ends past `upper` as RFC 9110 describes; `upper` is the
|
|
82
|
+
representation length minus one.
|
|
83
|
+
|
|
84
|
+
### Errors
|
|
85
|
+
|
|
86
|
+
Every error is a `RangeError`, a `ValueError` subclass, carrying `text`, `token` and
|
|
87
|
+
`offset`:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from intranges import RangeError, parse
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
parse("1-3, 5, x-9")
|
|
94
|
+
except RangeError as err:
|
|
95
|
+
print(err)
|
|
96
|
+
print(err.pointer())
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
prints
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
invalid range token 'x-9' for the default dialect at offset 8
|
|
103
|
+
1-3, 5, x-9
|
|
104
|
+
^^^
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Subclasses: `RangeSyntaxError`, `DescendingRangeError`, `RangeOverlapError`,
|
|
108
|
+
`RangeOrderError`, `RangeBoundsError`, `RangeSizeError`. Overlapping and unsorted tokens
|
|
109
|
+
are merged by default; pass `on_overlap="reject"` or `on_unsorted="reject"` to
|
|
110
|
+
`parse_ranges` to refuse them.
|
|
111
|
+
|
|
112
|
+
## Command line
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
intranges expand "1-3,7" # 1 2 3 7, one per line
|
|
116
|
+
intranges expand "0-10:3" --dialect cpulist -d , # 0,3,6,9
|
|
117
|
+
seq 1 5 | intranges compress # 1-5
|
|
118
|
+
intranges normalize "8, 1-3, 2, 4" # 1-4,8
|
|
119
|
+
intranges count "0-10000000000" # 10000000001
|
|
120
|
+
intranges contains "0-3,8-11" 9 12 # 9 yes, 12 no; exit code 2
|
|
121
|
+
intranges complement "0-3,8-11" --lower 0 --upper 15 # 4-7,12-15
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Errors go to stderr with exit code 1. Put `--` before a range string that starts with
|
|
125
|
+
`-`. See `intranges --help` and [the guide](docs/GUIDE.md) for everything else.
|
|
126
|
+
|
|
127
|
+
## Why
|
|
128
|
+
|
|
129
|
+
Expressing a set of integers as a compact string like `1-5,8,10-12` is common in page
|
|
130
|
+
selectors, CPU affinity, job arrays and config files, but converting it back and forth
|
|
131
|
+
usually ends up reimplemented in each project. `intspan` covers the idea with a
|
|
132
|
+
set-like class, yet it is untyped (it ships no `py.typed`) and expands its ranges in
|
|
133
|
+
memory. `intranges` is strictly typed, keeps ranges as spans, and follows documented
|
|
134
|
+
syntaxes instead of inventing new ones.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT. See [LICENSE](LICENSE).
|
|
139
|
+
|
|
140
|
+
## Links
|
|
141
|
+
|
|
142
|
+
- Website: https://lianes8studio.com
|
|
143
|
+
- GitHub: https://github.com/JaviLianes8
|
|
144
|
+
- LinkedIn: https://www.linkedin.com/in/jlianes/
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# intranges guide
|
|
2
|
+
|
|
3
|
+
This guide covers the grammar, the dialects, the policies and a few recipes. The README
|
|
4
|
+
has the short version.
|
|
5
|
+
|
|
6
|
+
## Grammar
|
|
7
|
+
|
|
8
|
+
A range string is a list of tokens joined by `list_sep` (default `,`). Whitespace
|
|
9
|
+
around tokens and around separators is ignored. An empty or blank string is the empty
|
|
10
|
+
set. An empty token, as in `1,,2` or a trailing `1,2,`, is a `RangeSyntaxError`.
|
|
11
|
+
|
|
12
|
+
| Token | Meaning | Needs |
|
|
13
|
+
|---------------|--------------------------------------------|-----------------------------|
|
|
14
|
+
| `7` | the single value 7 | |
|
|
15
|
+
| `1-5` | 1 to 5 inclusive | |
|
|
16
|
+
| `5-` | 5 to `upper` | `upper=`, `open_end` |
|
|
17
|
+
| `-5` | the last 5 values up to `upper` (HTTP) | `upper=`, `suffix` |
|
|
18
|
+
| `0-15:4` | every 4th value: 0, 4, 8, 12 | `stride` |
|
|
19
|
+
| `0-99:2/25` | 2 values from the start of every group of 25 | `stride`, `groups` |
|
|
20
|
+
| `N` | `upper` (kernel cpu lists) | `upper=`, `last_alias` |
|
|
21
|
+
| `all` | `lower` (or the dialect minimum) to `upper` | `upper=`, `all_alias` |
|
|
22
|
+
|
|
23
|
+
A range must run low to high: `5-1` is a `DescendingRangeError`.
|
|
24
|
+
|
|
25
|
+
### Negative numbers and the dash
|
|
26
|
+
|
|
27
|
+
When a dialect allows negatives (only `DEFAULT` among the presets), a `-` that starts a
|
|
28
|
+
bound and is followed by a digit is a sign. The range separator is the first `-` after
|
|
29
|
+
the first bound. So `-3--1` is -3 to -1, `-2-2` is -2 to 2, `-5` is the single value -5
|
|
30
|
+
and `5-` is an open end. `1--3` reads as 1 to -3 and is rejected as descending. `--3`
|
|
31
|
+
and `+3` are syntax errors. Dialects without negatives reject `-3` instead of guessing,
|
|
32
|
+
except `HTTP`, where a leading `-` is a suffix range as RFC 9110 defines it.
|
|
33
|
+
|
|
34
|
+
### Bounds
|
|
35
|
+
|
|
36
|
+
`lower=` and `upper=` restrict every value and supply the meaning of open ends, suffix
|
|
37
|
+
ranges, `N` and `all`. A value outside them is a `RangeBoundsError`. A dialect may also
|
|
38
|
+
set a `minimum` (1 for pages, 0 for cpu lists, Slurm indexes and bytes). When both are
|
|
39
|
+
given the stricter floor wins. `lower > upper` is a plain `ValueError`.
|
|
40
|
+
|
|
41
|
+
## Dialects
|
|
42
|
+
|
|
43
|
+
Each preset follows a documented syntax. Anything outside it is rejected.
|
|
44
|
+
|
|
45
|
+
- `PAGES`: CUPS `-o page-ranges=1-4,7,9-12`. Pages start at 1. CUPS prints pages in
|
|
46
|
+
ascending order whatever the input order, which matches `parse_ranges`. The open
|
|
47
|
+
forms `-4` and `9-` are not part of this preset; build a `Dialect` with
|
|
48
|
+
`open_end=True` and pass `upper=` when you need `9-`.
|
|
49
|
+
- `CPULIST`: the Linux kernel cpu list format, `0-3,8-11`, with the kernel's
|
|
50
|
+
`a-b:used/group` form (`100-2000:2/25` is 100, 101, 125, 126, ...), `N` for the last
|
|
51
|
+
CPU and `all`. It also accepts the util-linux `taskset` stride `a-b:N`, which equals
|
|
52
|
+
`a-b:1/N`.
|
|
53
|
+
- `SLURM`: `sbatch --array` indexes such as `0,6,16-32` and the step form `0-15:4`.
|
|
54
|
+
The numeric part of a hostlist may be wrapped in brackets (`[1-3,5]`) and zero-padded
|
|
55
|
+
(`001-010`). The `%` throttle suffix is not part of the index set and is rejected.
|
|
56
|
+
- `HTTP`: RFC 9110 byte ranges. The `bytes=` unit is optional on input and written on
|
|
57
|
+
output. `500-` runs to `upper`, `-500` is the last 500 bytes, and ends past `upper` are
|
|
58
|
+
clamped. A start past `upper` is unsatisfiable and raises `RangeBoundsError`. A user
|
|
59
|
+
`lower=` is a hard bound, not a clamp target. Pass `upper=length - 1`.
|
|
60
|
+
|
|
61
|
+
Build your own with `Dialect(...)`; incoherent settings, such as two equal separators
|
|
62
|
+
or suffix ranges together with negatives, raise `ValueError` at construction.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from intranges import Dialect, parse
|
|
66
|
+
|
|
67
|
+
dots = Dialect(name="dots", range_sep="..", list_sep=";", stride="by")
|
|
68
|
+
parse("1..9by4;20", dialect=dots) # [1, 5, 9, 20]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Lists versus sets
|
|
72
|
+
|
|
73
|
+
| Function | Returns | Order and repeats | Memory |
|
|
74
|
+
|-----------------|-------------|-------------------------|----------------------------|
|
|
75
|
+
| `parse` | `list[int]` | input order, repeats kept | one int per value, capped by `max_size` |
|
|
76
|
+
| `parse_ranges` | `IntRanges` | sorted, merged | one pair per span |
|
|
77
|
+
| `count` | `int` | distinct values | one pair per span |
|
|
78
|
+
| `normalize` | `str` | canonical | one pair per span |
|
|
79
|
+
|
|
80
|
+
Strides create one span per selected group, so `parse_ranges` caps them with
|
|
81
|
+
`max_spans` (default 1,000,000).
|
|
82
|
+
|
|
83
|
+
## Policies
|
|
84
|
+
|
|
85
|
+
`parse_ranges` merges overlapping, repeated and unsorted tokens by default.
|
|
86
|
+
|
|
87
|
+
- `on_overlap="reject"` raises `RangeOverlapError` naming the later of two overlapping
|
|
88
|
+
tokens. Adjacent tokens such as `1-2,3-4` do not overlap.
|
|
89
|
+
- `on_unsorted="reject"` raises `RangeOrderError` when a token starts below the start of
|
|
90
|
+
the token before it.
|
|
91
|
+
|
|
92
|
+
## Canonical form
|
|
93
|
+
|
|
94
|
+
`compact` and `IntRanges.format` write the canonical string: ascending, merged, with a
|
|
95
|
+
run of two or more consecutive values written as `low-high` and single values alone.
|
|
96
|
+
For any finite set of ints, `parse_ranges(compact(values)) == IntRanges(values)` and
|
|
97
|
+
`normalize(compact(values)) == compact(values)`.
|
|
98
|
+
|
|
99
|
+
## Errors
|
|
100
|
+
|
|
101
|
+
All errors subclass `RangeError`, which subclasses `ValueError`. Parse errors carry
|
|
102
|
+
`text`, `token` and `offset`, and `err.pointer()` draws a caret line under the token.
|
|
103
|
+
|
|
104
|
+
## Recipes
|
|
105
|
+
|
|
106
|
+
Free CPUs on a 16-CPU machine:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from intranges import CPULIST, parse_ranges
|
|
110
|
+
|
|
111
|
+
busy = parse_ranges("0-3,8-11", dialect=CPULIST)
|
|
112
|
+
busy.complement(0, 15).format(CPULIST) # "4-7,12-15"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Validate a user page selection for a 12-page document:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from intranges import PAGES, RangeError, parse_ranges
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
pages = parse_ranges(user_input, dialect=PAGES, upper=12)
|
|
122
|
+
except RangeError as err:
|
|
123
|
+
show_error(str(err), err.pointer())
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Merge byte ranges of a request against a 10,000-byte file:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from intranges import HTTP, parse_ranges
|
|
130
|
+
|
|
131
|
+
parse_ranges("bytes=0-99,50-199,-100", dialect=HTTP, upper=9999).spans
|
|
132
|
+
# ((0, 199), (9900, 9999))
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Command line
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
intranges expand TEXT [-d DELIM] [--unique] [--max-size N] # N=0 lifts the cap
|
|
139
|
+
intranges compress [VALUES ...] [--width W] # values from stdin when omitted;
|
|
140
|
+
# --lower/--upper validate them
|
|
141
|
+
intranges normalize TEXT [--to DIALECT] [--width W]
|
|
142
|
+
intranges count TEXT
|
|
143
|
+
intranges contains TEXT VALUE [VALUE ...] # exit 2 if any value is absent
|
|
144
|
+
intranges complement TEXT --lower L --upper U
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Every command takes `--dialect`, `--lower`, `--upper`, `--range-sep` and `--list-sep`.
|
|
148
|
+
`TEXT` may be `-` to read from stdin. Errors go to stderr with exit code 1.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "intranges"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Typed conversion between range strings like 1-3,5 and ints, with a lazy set type, dialects and a CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Javier Lianes", email = "javier@lianes8studio.com" }]
|
|
14
|
+
keywords = ["ranges", "integers", "parse", "compact", "range-string", "cpulist", "page-ranges", "intervals", "cli"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Topic :: Utilities",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
intranges = "intranges._cli:main"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = ["ruff", "mypy", "pytest", "build", "twine"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://lianes8studio.com"
|
|
33
|
+
Repository = "https://github.com/JaviLianes8/intranges"
|
|
34
|
+
Issues = "https://github.com/JaviLianes8/intranges/issues"
|
|
35
|
+
Changelog = "https://github.com/JaviLianes8/intranges/blob/main/CHANGELOG.md"
|
|
36
|
+
LinkedIn = "https://www.linkedin.com/in/jlianes/"
|
|
37
|
+
Funding = "https://buymeacoffee.com/jlianesglrs"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/intranges"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 100
|
|
44
|
+
target-version = "py310"
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
48
|
+
|
|
49
|
+
[tool.mypy]
|
|
50
|
+
python_version = "3.10"
|
|
51
|
+
strict = true
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
pythonpath = ["src"]
|
|
56
|
+
addopts = "-q"
|