tomlrange 0.2.0__tar.gz → 0.2.2__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.
@@ -0,0 +1,22 @@
1
+ # Dependabot: GitHub Actions and uv (pyproject.toml + uv.lock)
2
+ version: 2
3
+ updates:
4
+ - package-ecosystem: github-actions
5
+ directory: /
6
+ schedule:
7
+ interval: weekly
8
+ day: monday
9
+ groups:
10
+ github-actions:
11
+ patterns:
12
+ - "*"
13
+
14
+ - package-ecosystem: uv
15
+ directory: /
16
+ schedule:
17
+ interval: weekly
18
+ day: monday
19
+ groups:
20
+ python:
21
+ patterns:
22
+ - "*"
@@ -0,0 +1,174 @@
1
+ name: Bump version
2
+ run-name: Bump ${{ inputs.bump }} (${{ inputs.publish && 'publish' || 'draft' }})
3
+
4
+ on:
5
+ workflow_dispatch:
6
+ inputs:
7
+ bump:
8
+ description: uv version --bump (one per dispatch)
9
+ required: true
10
+ type: choice
11
+ options:
12
+ - patch
13
+ - minor
14
+ - major
15
+ - rc
16
+ - beta
17
+ - alpha
18
+ - stable
19
+ - post
20
+ - dev
21
+ default: patch
22
+ publish:
23
+ description: Published Release after merge (false = draft; tag is still created)
24
+ required: true
25
+ type: boolean
26
+ default: true
27
+
28
+ permissions:
29
+ contents: write
30
+ pull-requests: write
31
+ issues: write
32
+
33
+ concurrency:
34
+ group: bump-version
35
+ cancel-in-progress: false
36
+
37
+ jobs:
38
+ bump:
39
+ name: Bump and open PR
40
+ runs-on: ubuntu-latest
41
+ env:
42
+ BUMP: ${{ inputs.bump }}
43
+ PUBLISH: ${{ inputs.publish }}
44
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45
+ GH_REPO: ${{ github.repository }}
46
+ steps:
47
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
48
+ with:
49
+ ref: main
50
+
51
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
52
+
53
+ - id: versions
54
+ name: Compute next version
55
+ run: |
56
+ set -euo pipefail
57
+
58
+ OLD_VER="$(uv version --short --no-sync)"
59
+ OLD_VER="${OLD_VER%%$'\n'}"
60
+ NEW_VER="$(uv version --dry-run --bump "$BUMP" --short --no-sync)"
61
+ NEW_VER="${NEW_VER%%$'\n'}"
62
+ if [ -z "$NEW_VER" ]; then
63
+ echo "uv version --dry-run did not print a version" >&2
64
+ exit 1
65
+ fi
66
+ echo "Next version: ${OLD_VER} → ${NEW_VER} (bump=${BUMP})"
67
+ {
68
+ echo "old=${OLD_VER}"
69
+ echo "new=${NEW_VER}"
70
+ } >> "$GITHUB_OUTPUT"
71
+
72
+ - name: Bump ${{ steps.versions.outputs.old }} → ${{ steps.versions.outputs.new }}
73
+ env:
74
+ OLD_VER: ${{ steps.versions.outputs.old }}
75
+ NEW_VER: ${{ steps.versions.outputs.new }}
76
+ run: |
77
+ set -euo pipefail
78
+
79
+ if [ -n "$(git ls-remote --tags origin "refs/tags/v${NEW_VER}")" ]; then
80
+ echo "Tag v${NEW_VER} already exists; never retag." >&2
81
+ exit 1
82
+ fi
83
+
84
+ open_prs="$(gh pr list --head "bump/v${NEW_VER}" --state open --json number --jq 'length')"
85
+ if [ "$open_prs" != "0" ]; then
86
+ echo "An open PR already exists from bump/v${NEW_VER}." >&2
87
+ gh pr list --head "bump/v${NEW_VER}" --state open
88
+ exit 1
89
+ fi
90
+
91
+ uv version --bump "$BUMP" --no-sync
92
+ actual="$(uv version --short --no-sync)"
93
+ if [ "$actual" != "$NEW_VER" ]; then
94
+ echo "uv version wrote ${actual}, expected ${NEW_VER}" >&2
95
+ exit 1
96
+ fi
97
+
98
+ git checkout -b "bump/v${NEW_VER}"
99
+
100
+ git add pyproject.toml
101
+ if ! git diff --quiet -- uv.lock; then
102
+ git add uv.lock
103
+ fi
104
+ if git diff --cached --quiet; then
105
+ echo "uv version did not change pyproject.toml or uv.lock" >&2
106
+ exit 1
107
+ fi
108
+
109
+ git config user.name "github-actions[bot]"
110
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
111
+ git commit -m "chore: bump version to ${NEW_VER}." -m "Co-authored-by: Joseph Chiocchi <joe@yolk.cc>"
112
+
113
+ git push -u origin "HEAD:bump/v${NEW_VER}"
114
+
115
+ if [ -n "$(git ls-remote --tags origin "refs/tags/v${OLD_VER}")" ]; then
116
+ compare_base="v${OLD_VER}"
117
+ else
118
+ compare_base="main"
119
+ fi
120
+ OLD_TREE_URL="https://github.com/${GH_REPO}/tree/${compare_base}"
121
+ NEW_TREE_URL="https://github.com/${GH_REPO}/tree/bump/v${NEW_VER}"
122
+ COMPARE_URL="https://github.com/${GH_REPO}/compare/${compare_base}...bump/v${NEW_VER}"
123
+ echo "${OLD_TREE_URL}"
124
+ echo "${NEW_TREE_URL}"
125
+ echo "${COMPARE_URL}"
126
+ {
127
+ echo "Bump [${OLD_VER}](${OLD_TREE_URL}) → [${NEW_VER}](${NEW_TREE_URL})"
128
+ echo
129
+ echo "Compare: ${COMPARE_URL}"
130
+ } >> "$GITHUB_STEP_SUMMARY"
131
+
132
+ if [ "$PUBLISH" = "true" ]; then
133
+ mode_comment="<!-- tomlrange-release: publish -->"
134
+ label="release:publish"
135
+ mode_word="published"
136
+ else
137
+ mode_comment="<!-- tomlrange-release: draft -->"
138
+ label="release:draft"
139
+ mode_word="draft"
140
+ fi
141
+
142
+ ensure_label() {
143
+ local name="$1" color="$2" desc="$3"
144
+ if gh label list --limit 100 --json name --jq '.[].name' | grep -Fxq "$name"; then
145
+ return 0
146
+ fi
147
+ gh label create "$name" --color "$color" --description "$desc"
148
+ }
149
+ ensure_label "release:publish" "0e8a16" "Cut a published GitHub Release when this bump PR merges"
150
+ ensure_label "release:draft" "fbca04" "Cut a draft GitHub Release when this bump PR merges"
151
+
152
+ {
153
+ echo "Bumps \`[project].version\` with \`uv version --bump ${BUMP}\` to **${NEW_VER}**."
154
+ echo
155
+ echo "${mode_comment}"
156
+ echo
157
+ echo "Merging this PR triggers **Cut release**, which creates GitHub Release \`v${NEW_VER}\` immediately (${mode_word})."
158
+ echo
159
+ echo "A published Release then runs **Publish** (TestPyPI; PyPI only when the version is not a pre-release). A draft does not, until someone publishes it in the UI."
160
+ echo
161
+ echo "Do not push tags by hand. Never retag."
162
+ } > "$RUNNER_TEMP/pr-body.md"
163
+
164
+ PR_URL="$(gh pr create \
165
+ --base main \
166
+ --head "bump/v${NEW_VER}" \
167
+ --title "chore: bump version to ${NEW_VER}" \
168
+ --label "$label" \
169
+ --body-file "$RUNNER_TEMP/pr-body.md")"
170
+ echo "${PR_URL}"
171
+ {
172
+ echo
173
+ echo "PR: [#${PR_URL##*/}](${PR_URL})"
174
+ } >> "$GITHUB_STEP_SUMMARY"
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ workflow_dispatch:
7
+
8
+ concurrency:
9
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
20
+ with:
21
+ persist-credentials: false
22
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
23
+ - run: uv sync --group dev
24
+ - run: uv run ruff check
25
+ - run: uv run ruff format --check
26
+ - run: uv run pytest
@@ -0,0 +1,115 @@
1
+ name: Cut release
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+
7
+ permissions:
8
+ contents: write
9
+ actions: write
10
+
11
+ jobs:
12
+ cut:
13
+ name: Create GitHub Release
14
+ if: >
15
+ github.event.pull_request.merged == true &&
16
+ github.event.pull_request.base.ref == github.event.repository.default_branch &&
17
+ startsWith(github.event.pull_request.head.ref, 'bump/v')
18
+ runs-on: ubuntu-latest
19
+ env:
20
+ HEAD_REF: ${{ github.event.pull_request.head.ref }}
21
+ MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
22
+ PR_NUMBER: ${{ github.event.pull_request.number }}
23
+ PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
24
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25
+ GH_REPO: ${{ github.repository }}
26
+ steps:
27
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
28
+ with:
29
+ ref: ${{ github.event.pull_request.merge_commit_sha }}
30
+
31
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
32
+
33
+ - name: Create GitHub Release
34
+ run: |
35
+ set -euo pipefail
36
+
37
+ PR_BODY="$(gh pr view "$PR_NUMBER" --json body --jq .body)"
38
+ VER="$(python3 -c 'import tomllib, pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
39
+ expected="${HEAD_REF#bump/v}"
40
+ if [ "$VER" != "$expected" ]; then
41
+ echo "pyproject version ${VER} does not match branch ${HEAD_REF} (expected ${expected})." >&2
42
+ exit 1
43
+ fi
44
+
45
+ IS_PRERELEASE="$(VER="$VER" uv run --with packaging --no-project python -c 'import os; from packaging.version import Version; print(Version(os.environ["VER"]).is_prerelease)')"
46
+ pre_args=()
47
+ if [ "$IS_PRERELEASE" = "True" ]; then
48
+ pre_args+=(--prerelease)
49
+ fi
50
+
51
+ has_pub_label=0
52
+ has_draft_label=0
53
+ case ",${PR_LABELS}," in
54
+ *",release:publish,"*) has_pub_label=1 ;;
55
+ esac
56
+ case ",${PR_LABELS}," in
57
+ *",release:draft,"*) has_draft_label=1 ;;
58
+ esac
59
+
60
+ mode=""
61
+ if [ "$has_pub_label" -eq 1 ] && [ "$has_draft_label" -eq 1 ]; then
62
+ echo "PR has both release:publish and release:draft; pick one." >&2
63
+ exit 1
64
+ elif [ "$has_pub_label" -eq 1 ]; then
65
+ mode=publish
66
+ elif [ "$has_draft_label" -eq 1 ]; then
67
+ mode=draft
68
+ elif printf '%s\n' "$PR_BODY" | grep -Fq '<!-- tomlrange-release: publish -->'; then
69
+ if printf '%s\n' "$PR_BODY" | grep -Fq '<!-- tomlrange-release: draft -->'; then
70
+ echo "PR body has both tomlrange-release comments; pick one." >&2
71
+ exit 1
72
+ fi
73
+ mode=publish
74
+ elif printf '%s\n' "$PR_BODY" | grep -Fq '<!-- tomlrange-release: draft -->'; then
75
+ mode=draft
76
+ else
77
+ echo "Cannot determine release mode: need label release:publish or release:draft, or HTML comment <!-- tomlrange-release: publish --> / <!-- tomlrange-release: draft -->." >&2
78
+ exit 1
79
+ fi
80
+
81
+ if [ -n "$(git ls-remote --tags origin "refs/tags/v${VER}")" ]; then
82
+ echo "Tag v${VER} already exists; never retag." >&2
83
+ exit 1
84
+ fi
85
+
86
+ echo "Cutting v${VER} as ${mode} at ${MERGE_SHA} (is_prerelease=${IS_PRERELEASE})"
87
+ if [ "$mode" = draft ]; then
88
+ gh release create "v${VER}" \
89
+ --draft \
90
+ "${pre_args[@]}" \
91
+ --generate-notes \
92
+ --title "v${VER}" \
93
+ --target "$MERGE_SHA"
94
+ echo "Draft: not dispatching Publish (UI publish still fires on: release)."
95
+ else
96
+ gh release create "v${VER}" \
97
+ "${pre_args[@]}" \
98
+ --generate-notes \
99
+ --title "v${VER}" \
100
+ --target "$MERGE_SHA"
101
+
102
+ # GITHUB_TOKEN creates do not fire on: release; workflow_dispatch does cascade.
103
+ if [ "$IS_PRERELEASE" = "True" ]; then
104
+ echo "Dispatching Publish (testpypi only) for pre-release v${VER}"
105
+ gh workflow run publish.yml --ref "v${VER}" -f target=testpypi -f release_tag="v${VER}"
106
+ echo "Dispatched workflow runs (listing may be racy):"
107
+ gh run list --workflow=publish.yml --event=workflow_dispatch --limit 2 || true
108
+ else
109
+ echo "Dispatching Publish (testpypi + pypi) for v${VER}"
110
+ gh workflow run publish.yml --ref "v${VER}" -f target=testpypi -f release_tag="v${VER}"
111
+ gh workflow run publish.yml --ref "v${VER}" -f target=pypi -f release_tag="v${VER}"
112
+ echo "Dispatched workflow runs (listing may be racy):"
113
+ gh run list --workflow=publish.yml --event=workflow_dispatch --limit 4 || true
114
+ fi
115
+ fi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tomlrange
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Parse and validate { from, to } tables from decoded TOML.
5
5
  Project-URL: Homepage, https://github.com/yyolk/tomlrange
6
6
  Project-URL: Repository, https://github.com/yyolk/tomlrange
@@ -78,7 +78,7 @@ from tomlrange import Domain
78
78
  Month = Domain(int, lo=1, hi=12, name="month")
79
79
 
80
80
  data = tomllib.loads(config)
81
- year = Month.bound(data["months"]) # 1..12
81
+ year = Month.bound(data["months"]) # 1..12
82
82
  windows = Month.bounds(data["windows"]) # 1..4 and 6..10
83
83
 
84
84
  assert list(year) == list(range(1, 13))
@@ -104,13 +104,15 @@ Or name the domain by subclassing `Spec` — the class body is the schema:
104
104
  ```python
105
105
  from tomlrange import Spec
106
106
 
107
+
107
108
  class Month(Spec):
108
109
  typ = int
109
110
  lo = 1
110
111
  hi = 12
111
112
 
113
+
112
114
  Month.parse({"from": 1, "to": 4})
113
- Month.parse_many([{ "from": 1, "to": 4 }, { "from": 6, "to": 10 }])
115
+ Month.parse_many([{"from": 1, "to": 4}, {"from": 6, "to": 10}])
114
116
  ```
115
117
 
116
118
  ## Validation
@@ -127,6 +129,10 @@ On a list, `overlap=` is `"reject"` (default), `"allow"`, or `"merge"`.
127
129
  Reject treats a shared endpoint as overlap (`1–4` and `4–6` fail).
128
130
  Adjacent integers (`1–4` then `5–8`) are fine; `merge()` will coalesce them.
129
131
 
132
+ `list()`, `len()`, `width`, and `as_range()` are int-only — they raise
133
+ `TypeError` (`"{name} width is only defined for int"`). Membership,
134
+ `as_tuple()`, and `as_table()` work for any ordered `typ`.
135
+
130
136
  Errors carry a path:
131
137
 
132
138
  ```
@@ -140,6 +146,7 @@ months_ranges[1].to: 13 is above month 12
140
146
  - Not a two-element array (`[1, 12]`). After TOML decode that is a list, not a table.
141
147
  - Not wrap-around (`from = 11, to = 2`). Write two spans.
142
148
  - Not `step`. A range table is a closed interval.
149
+ - Not iteration over a non-int domain. Walk dates or strings yourself.
143
150
 
144
151
  ## Install
145
152
 
@@ -38,7 +38,7 @@ from tomlrange import Domain
38
38
  Month = Domain(int, lo=1, hi=12, name="month")
39
39
 
40
40
  data = tomllib.loads(config)
41
- year = Month.bound(data["months"]) # 1..12
41
+ year = Month.bound(data["months"]) # 1..12
42
42
  windows = Month.bounds(data["windows"]) # 1..4 and 6..10
43
43
 
44
44
  assert list(year) == list(range(1, 13))
@@ -64,13 +64,15 @@ Or name the domain by subclassing `Spec` — the class body is the schema:
64
64
  ```python
65
65
  from tomlrange import Spec
66
66
 
67
+
67
68
  class Month(Spec):
68
69
  typ = int
69
70
  lo = 1
70
71
  hi = 12
71
72
 
73
+
72
74
  Month.parse({"from": 1, "to": 4})
73
- Month.parse_many([{ "from": 1, "to": 4 }, { "from": 6, "to": 10 }])
75
+ Month.parse_many([{"from": 1, "to": 4}, {"from": 6, "to": 10}])
74
76
  ```
75
77
 
76
78
  ## Validation
@@ -87,6 +89,10 @@ On a list, `overlap=` is `"reject"` (default), `"allow"`, or `"merge"`.
87
89
  Reject treats a shared endpoint as overlap (`1–4` and `4–6` fail).
88
90
  Adjacent integers (`1–4` then `5–8`) are fine; `merge()` will coalesce them.
89
91
 
92
+ `list()`, `len()`, `width`, and `as_range()` are int-only — they raise
93
+ `TypeError` (`"{name} width is only defined for int"`). Membership,
94
+ `as_tuple()`, and `as_table()` work for any ordered `typ`.
95
+
90
96
  Errors carry a path:
91
97
 
92
98
  ```
@@ -100,6 +106,7 @@ months_ranges[1].to: 13 is above month 12
100
106
  - Not a two-element array (`[1, 12]`). After TOML decode that is a list, not a table.
101
107
  - Not wrap-around (`from = 11, to = 2`). Write two spans.
102
108
  - Not `step`. A range table is a closed interval.
109
+ - Not iteration over a non-int domain. Walk dates or strings yourself.
103
110
 
104
111
  ## Install
105
112
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tomlrange"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Parse and validate { from, to } tables from decoded TOML."
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -49,6 +49,7 @@ explicit = true
49
49
  dev = [
50
50
  "pytest>=8.0",
51
51
  "ruff>=0.16.8",
52
+ "pre-commit>=4.0",
52
53
  ]
53
54
 
54
55
  [tool.pytest.ini_options]
@@ -74,3 +75,6 @@ select = [
74
75
  "TC",
75
76
  ]
76
77
  ignore = ["E501"]
78
+
79
+ [tool.ruff.lint.flake8-type-checking]
80
+ runtime-evaluated-decorators = ["dataclasses.dataclass"]
@@ -1,9 +1,16 @@
1
1
  """Parse and validate `{ from = …, to = … }` tables from decoded TOML."""
2
2
 
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
3
5
  from tomlrange.bound import Bound, Bounds
4
6
  from tomlrange.domain import Domain, Spec
5
7
  from tomlrange.error import TomlRangeError
6
8
 
9
+ try:
10
+ __version__ = version("tomlrange")
11
+ except PackageNotFoundError:
12
+ __version__ = "unknown"
13
+
7
14
  __all__ = [
8
15
  "Bound",
9
16
  "Bounds",
@@ -11,4 +18,3 @@ __all__ = [
11
18
  "Spec",
12
19
  "TomlRangeError",
13
20
  ]
14
- __version__ = "0.2.0"
@@ -1,16 +1,24 @@
1
1
  from collections.abc import Iterator, Mapping
2
2
  from dataclasses import dataclass
3
- from typing import Any
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from tomlrange.domain import Domain
6
6
  from tomlrange.error import TomlRangeError
7
- from tomlrange.paths import Overlap, index_path, join_path
7
+ from tomlrange.paths import index_path, join_path
8
8
 
9
+ if TYPE_CHECKING:
10
+ from tomlrange.paths import Overlap
9
11
 
10
- def _as_table(raw: Any, *, path: str) -> Mapping[str, Any]:
12
+
13
+ def _as_table(raw: Any, *, path: str, keys: tuple[str, str]) -> Mapping[str, Any]:
11
14
  if isinstance(raw, Mapping):
12
15
  return raw
13
- raise TomlRangeError(path, "expected a table { from = …, to = … }", raw)
16
+ start_key, stop_key = keys
17
+ raise TomlRangeError(
18
+ path,
19
+ f"expected a table {{ {start_key} = …, {stop_key} = … }}",
20
+ raw,
21
+ )
14
22
 
15
23
 
16
24
  @dataclass(frozen=True, slots=True)
@@ -23,8 +31,8 @@ class Bound[T]:
23
31
 
24
32
  @classmethod
25
33
  def parse(cls, raw: Any, domain: Domain[T], *, path: str = ".") -> Bound[T]:
26
- table = _as_table(raw, path=path)
27
34
  start_key, stop_key = domain.keys
35
+ table = _as_table(raw, path=path, keys=domain.keys)
28
36
  extra = set(table) - {start_key, stop_key}
29
37
  if extra:
30
38
  raise TomlRangeError(
@@ -48,16 +56,18 @@ class Bound[T]:
48
56
  )
49
57
  return cls(start, stop, domain)
50
58
 
51
- @property
52
- def width(self) -> int:
59
+ def _require_int(self) -> None:
53
60
  if type(self.start) is not int or type(self.stop) is not int:
54
61
  raise TypeError(f"{self.domain.name} width is only defined for int")
55
- return self.stop - self.start + 1
62
+
63
+ @property
64
+ def width(self) -> int:
65
+ self._require_int()
66
+ return self.stop - self.start + 1 # type: ignore[operator]
56
67
 
57
68
  def as_range(self) -> range:
58
- if type(self.start) is not int or type(self.stop) is not int:
59
- raise TypeError(f"{self.domain.name} as_range() is only defined for int")
60
- return range(self.start, self.stop + 1)
69
+ self._require_int()
70
+ return range(self.start, self.stop + 1) # type: ignore[arg-type]
61
71
 
62
72
  def as_tuple(self) -> tuple[T, T]:
63
73
  return (self.start, self.stop)
@@ -72,9 +82,11 @@ class Bound[T]:
72
82
  return self.start <= item <= self.stop # type: ignore[operator]
73
83
 
74
84
  def __iter__(self) -> Iterator[T]:
85
+ self._require_int()
75
86
  yield from self.as_range() # type: ignore[misc]
76
87
 
77
88
  def __len__(self) -> int:
89
+ self._require_int()
78
90
  return self.width
79
91
 
80
92
  def overlaps(self, other: Bound[T]) -> bool:
@@ -145,6 +157,10 @@ class Bounds[T]:
145
157
  def merge(self) -> Bounds[T]:
146
158
  return Bounds(_coalesce(self.spans, self.domain), self.domain)
147
159
 
160
+ def _require_int(self) -> None:
161
+ if self.domain.typ is not int:
162
+ raise TypeError(f"{self.domain.name} width is only defined for int")
163
+
148
164
  def covers(self, item: object) -> bool:
149
165
  return any(item in span for span in self.spans)
150
166
 
@@ -152,6 +168,7 @@ class Bounds[T]:
152
168
  return self.covers(item)
153
169
 
154
170
  def __iter__(self) -> Iterator[T]:
171
+ self._require_int()
155
172
  seen: set[T] = set()
156
173
  for span in self.spans:
157
174
  for item in span:
@@ -161,6 +178,7 @@ class Bounds[T]:
161
178
  yield item
162
179
 
163
180
  def __len__(self) -> int:
181
+ self._require_int()
164
182
  return sum(1 for _ in self)
165
183
 
166
184
  def __repr__(self) -> str:
@@ -168,7 +186,9 @@ class Bounds[T]:
168
186
  return f"Bounds({inner}, {self.domain.name})"
169
187
 
170
188
 
171
- def _coalesce[T](spans: tuple[Bound[T], ...], domain: Domain[T]) -> tuple[Bound[T], ...]:
189
+ def _coalesce[T](
190
+ spans: tuple[Bound[T], ...], domain: Domain[T]
191
+ ) -> tuple[Bound[T], ...]:
172
192
  if not spans:
173
193
  return ()
174
194
  ordered = sorted(spans, key=lambda s: (s.start, s.stop))
@@ -2,10 +2,10 @@ from dataclasses import dataclass
2
2
  from typing import TYPE_CHECKING, Any
3
3
 
4
4
  from tomlrange.error import TomlRangeError
5
- from tomlrange.paths import Overlap
6
5
 
7
6
  if TYPE_CHECKING:
8
7
  from tomlrange.bound import Bound, Bounds
8
+ from tomlrange.paths import Overlap
9
9
 
10
10
  _FROM = "from"
11
11
  _TO = "to"
@@ -64,8 +64,8 @@ class Domain[T]:
64
64
  )
65
65
  return raw
66
66
 
67
- def bound(self, raw: Any, *, path: str = ".") -> "Bound[T]":
68
- from tomlrange.bound import Bound
67
+ def bound(self, raw: Any, *, path: str = ".") -> Bound[T]:
68
+ from tomlrange.bound import Bound # cycle: Domain ↔ Bound
69
69
 
70
70
  return Bound.parse(raw, self, path=path)
71
71
 
@@ -75,13 +75,13 @@ class Domain[T]:
75
75
  *,
76
76
  path: str = ".",
77
77
  overlap: Overlap = "reject",
78
- ) -> "Bounds[T]":
79
- from tomlrange.bound import Bounds
78
+ ) -> Bounds[T]:
79
+ from tomlrange.bound import Bounds # cycle: Domain ↔ Bound
80
80
 
81
81
  return Bounds.parse(raw, self, path=path, overlap=overlap)
82
82
 
83
- def full(self) -> "Bound[T]":
84
- from tomlrange.bound import Bound
83
+ def full(self) -> Bound[T]:
84
+ from tomlrange.bound import Bound # cycle: Domain ↔ Bound
85
85
 
86
86
  if self.lo is None or self.hi is None:
87
87
  raise TomlRangeError(".", f"{self.name} domain has no closed lo/hi")
@@ -129,8 +129,8 @@ class Spec:
129
129
  )
130
130
 
131
131
  @classmethod
132
- def parse(cls, raw: Any, *, path: str = ".") -> "Bound[Any]":
133
- from tomlrange.bound import Bound
132
+ def parse(cls, raw: Any, *, path: str = ".") -> Bound[Any]:
133
+ from tomlrange.bound import Bound # cycle: Domain ↔ Bound
134
134
 
135
135
  return Bound.parse(raw, cls.domain, path=path)
136
136
 
@@ -141,8 +141,8 @@ class Spec:
141
141
  *,
142
142
  path: str = ".",
143
143
  overlap: Overlap | None = None,
144
- ) -> "Bounds[Any]":
145
- from tomlrange.bound import Bounds
144
+ ) -> Bounds[Any]:
145
+ from tomlrange.bound import Bounds # cycle: Domain ↔ Bound
146
146
 
147
147
  return Bounds.parse(
148
148
  raw,
@@ -152,5 +152,5 @@ class Spec:
152
152
  )
153
153
 
154
154
  @classmethod
155
- def full(cls) -> "Bound[Any]":
155
+ def full(cls) -> Bound[Any]:
156
156
  return cls.domain.full()
File without changes
@@ -122,6 +122,31 @@ def test_custom_keys() -> None:
122
122
  hours.bound({"from": 7, "to": 16})
123
123
  with pytest.raises(TomlRangeError, match="must have keys"):
124
124
  hours.bound({"start": 7})
125
+ with pytest.raises(
126
+ TomlRangeError, match=r"expected a table \{ start = …, end = … \}"
127
+ ):
128
+ hours.bound([7, 16])
129
+
130
+
131
+ def test_int_only_iteration() -> None:
132
+ labels = Domain(str, name="label")
133
+ bound = labels.bound({"from": "a", "to": "c"})
134
+ assert bound.as_tuple() == ("a", "c")
135
+ assert "b" in bound
136
+ with pytest.raises(TypeError, match="label width is only defined for int"):
137
+ list(bound)
138
+ with pytest.raises(TypeError, match="label width is only defined for int"):
139
+ len(bound)
140
+ with pytest.raises(TypeError, match="label width is only defined for int"):
141
+ _ = bound.width
142
+ with pytest.raises(TypeError, match="label width is only defined for int"):
143
+ bound.as_range()
144
+ spans = labels.bounds([{"from": "a", "to": "b"}, {"from": "d", "to": "e"}])
145
+ assert "a" in spans
146
+ with pytest.raises(TypeError, match="label width is only defined for int"):
147
+ list(spans)
148
+ with pytest.raises(TypeError, match="label width is only defined for int"):
149
+ len(spans)
125
150
 
126
151
 
127
152
  def test_error_paths() -> None:
@@ -131,3 +156,80 @@ def test_error_paths() -> None:
131
156
  path="months_ranges",
132
157
  )
133
158
  assert exc.value.path == "months_ranges[1].to"
159
+
160
+
161
+ def test_open_int_domain_parse() -> None:
162
+ open_int = Domain(int, name="n")
163
+ bound = open_int.bound({"from": -3, "to": 99})
164
+ assert bound.as_tuple() == (-3, 99)
165
+ assert -3 in bound
166
+ assert 99 in bound
167
+
168
+
169
+ def test_full_requires_closed_lo_hi() -> None:
170
+ open_int = Domain(int, name="n")
171
+ with pytest.raises(TomlRangeError, match="n domain has no closed lo/hi"):
172
+ open_int.full()
173
+ half = Domain(int, lo=1, name="n")
174
+ with pytest.raises(TomlRangeError, match="n domain has no closed lo/hi"):
175
+ half.full()
176
+
177
+ class Open(Spec):
178
+ typ = int
179
+ name = "open"
180
+
181
+ with pytest.raises(TomlRangeError, match="open domain has no closed lo/hi"):
182
+ Open.full()
183
+
184
+
185
+ def test_bounds_empty_list() -> None:
186
+ empty = MONTH.bounds([])
187
+ assert empty.spans == ()
188
+ assert list(empty) == []
189
+
190
+
191
+ def test_unknown_overlap_policy() -> None:
192
+ with pytest.raises(ValueError, match="unknown overlap policy 'bogus'"):
193
+ MONTH.bounds([{"from": 1, "to": 2}], overlap="bogus")
194
+
195
+
196
+ def test_float_domain_parse() -> None:
197
+ ratio = Domain(float, lo=0.0, hi=1.0, name="ratio")
198
+ bound = ratio.bound({"from": 0.25, "to": 0.75})
199
+ assert bound.as_tuple() == (0.25, 0.75)
200
+ assert 0.5 in bound
201
+ assert bound.as_table() == {"from": 0.25, "to": 0.75}
202
+
203
+
204
+ def test_spec_name_suffix_stripping() -> None:
205
+ class MonthBounds(Spec):
206
+ typ = int
207
+ lo = 1
208
+ hi = 12
209
+
210
+ class MonthRange(Spec):
211
+ typ = int
212
+ lo = 1
213
+ hi = 12
214
+
215
+ class MonthSpec(Spec):
216
+ typ = int
217
+ lo = 1
218
+ hi = 12
219
+
220
+ assert MonthBounds.domain.name == "month"
221
+ assert MonthRange.domain.name == "month"
222
+ assert MonthSpec.domain.name == "month"
223
+
224
+
225
+ def test_spec_overlap_class_default() -> None:
226
+ class AllowingMonth(Spec):
227
+ typ = int
228
+ lo = 1
229
+ hi = 12
230
+ overlap = "allow"
231
+
232
+ raw = [{"from": 1, "to": 4}, {"from": 4, "to": 6}]
233
+ allowed = AllowingMonth.parse_many(raw)
234
+ assert list(allowed) == [1, 2, 3, 4, 5, 6]
235
+ assert len(allowed.spans) == 2
@@ -0,0 +1,233 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.14"
4
+
5
+ [[package]]
6
+ name = "cfgv"
7
+ version = "3.5.0"
8
+ source = { registry = "https://pypi.org/simple/" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "colorama"
16
+ version = "0.4.6"
17
+ source = { registry = "https://pypi.org/simple/" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "distlib"
25
+ version = "0.4.3"
26
+ source = { registry = "https://pypi.org/simple/" }
27
+ sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" },
30
+ ]
31
+
32
+ [[package]]
33
+ name = "filelock"
34
+ version = "3.32.7"
35
+ source = { registry = "https://pypi.org/simple/" }
36
+ sdist = { url = "https://files.pythonhosted.org/packages/0f/59/e19834834cb01a32febfbb0f8a23a9088088f5d45991824ff2bc3b5e8acb/filelock-3.32.7.tar.gz", hash = "sha256:37b8a3d9811b0f9aef7e5ec5c71bb320de52df51e6ca9bcd6f5ad81187660da7", size = 225154, upload-time = "2026-09-16T00:24:20.907Z" }
37
+ wheels = [
38
+ { url = "https://files.pythonhosted.org/packages/15/df/31098c5aeb4d966b553641472bd55fcf5fdfac953549894b8a765ba44e91/filelock-3.32.7-py3-none-any.whl", hash = "sha256:65ff0d0190ea42038b32bda4b77834fb05be2cad4c5b9b01aa4dfb3614536e52", size = 100157, upload-time = "2026-09-16T00:24:19.543Z" },
39
+ ]
40
+
41
+ [[package]]
42
+ name = "identify"
43
+ version = "2.6.19"
44
+ source = { registry = "https://pypi.org/simple/" }
45
+ sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
46
+ wheels = [
47
+ { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
48
+ ]
49
+
50
+ [[package]]
51
+ name = "iniconfig"
52
+ version = "2.3.0"
53
+ source = { registry = "https://pypi.org/simple/" }
54
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
55
+ wheels = [
56
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
57
+ ]
58
+
59
+ [[package]]
60
+ name = "nodeenv"
61
+ version = "1.10.0"
62
+ source = { registry = "https://pypi.org/simple/" }
63
+ sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
64
+ wheels = [
65
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
66
+ ]
67
+
68
+ [[package]]
69
+ name = "packaging"
70
+ version = "26.3"
71
+ source = { registry = "https://pypi.org/simple/" }
72
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
73
+ wheels = [
74
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
75
+ ]
76
+
77
+ [[package]]
78
+ name = "platformdirs"
79
+ version = "4.11.9"
80
+ source = { registry = "https://pypi.org/simple/" }
81
+ sdist = { url = "https://files.pythonhosted.org/packages/58/b9/8adc4e1b422b27fd88540ec7bf1f406f77ef393ec070e26fc430e914cde8/platformdirs-4.11.9.tar.gz", hash = "sha256:e2c66a8d384596cd98e3c4aea2d761df7bac95d9d8a2cc3946daa8cdafdaebc1", size = 38345, upload-time = "2026-09-16T13:31:45.259Z" }
82
+ wheels = [
83
+ { url = "https://files.pythonhosted.org/packages/f3/94/803ba86705257d7eedddac4b02eb88a7483b1600e9200c1fefc6f1a9a3ff/platformdirs-4.11.9-py3-none-any.whl", hash = "sha256:0a3958f58a9e30321eaef0a424dd0b77cce242886b36b8aa992f9731ef2d59c1", size = 24472, upload-time = "2026-09-16T13:31:44.049Z" },
84
+ ]
85
+
86
+ [[package]]
87
+ name = "pluggy"
88
+ version = "1.6.0"
89
+ source = { registry = "https://pypi.org/simple/" }
90
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
91
+ wheels = [
92
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
93
+ ]
94
+
95
+ [[package]]
96
+ name = "pre-commit"
97
+ version = "4.6.2"
98
+ source = { registry = "https://pypi.org/simple/" }
99
+ dependencies = [
100
+ { name = "cfgv" },
101
+ { name = "identify" },
102
+ { name = "nodeenv" },
103
+ { name = "pyyaml" },
104
+ { name = "virtualenv" },
105
+ ]
106
+ sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" }
107
+ wheels = [
108
+ { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" },
109
+ ]
110
+
111
+ [[package]]
112
+ name = "pygments"
113
+ version = "2.21.0"
114
+ source = { registry = "https://pypi.org/simple/" }
115
+ sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
116
+ wheels = [
117
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
118
+ ]
119
+
120
+ [[package]]
121
+ name = "pytest"
122
+ version = "9.1.1"
123
+ source = { registry = "https://pypi.org/simple/" }
124
+ dependencies = [
125
+ { name = "colorama", marker = "sys_platform == 'win32'" },
126
+ { name = "iniconfig" },
127
+ { name = "packaging" },
128
+ { name = "pluggy" },
129
+ { name = "pygments" },
130
+ ]
131
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
132
+ wheels = [
133
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
134
+ ]
135
+
136
+ [[package]]
137
+ name = "python-discovery"
138
+ version = "1.6.0"
139
+ source = { registry = "https://pypi.org/simple/" }
140
+ dependencies = [
141
+ { name = "filelock" },
142
+ ]
143
+ sdist = { url = "https://files.pythonhosted.org/packages/91/96/0f93e27c9f60a650838f2118159aa115fd5732c0716247917b7ba7ede665/python_discovery-1.6.0.tar.gz", hash = "sha256:6393b4eae1be8b2182670635e7baff89ac21cb9f8e86fd1ff40c7b1144febb4c", size = 82849, upload-time = "2026-08-28T17:30:02.366Z" }
144
+ wheels = [
145
+ { url = "https://files.pythonhosted.org/packages/43/5e/21abf578182fb15006a57faf3711a1e659e29d600d19b6e557eae908c81d/python_discovery-1.6.0-py3-none-any.whl", hash = "sha256:d4e244cf17b8b29819ed78003d55fbacf86eda23425b075454fff9271b79377a", size = 38451, upload-time = "2026-08-28T17:30:01.236Z" },
146
+ ]
147
+
148
+ [[package]]
149
+ name = "pyyaml"
150
+ version = "6.0.3"
151
+ source = { registry = "https://pypi.org/simple/" }
152
+ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
153
+ wheels = [
154
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
155
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
156
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
157
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
158
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
159
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
160
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
161
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
162
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
163
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
164
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
165
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
166
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
167
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
168
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
169
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
170
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
171
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
172
+ ]
173
+
174
+ [[package]]
175
+ name = "ruff"
176
+ version = "0.16.8"
177
+ source = { registry = "https://pypi.org/simple/" }
178
+ sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" }
179
+ wheels = [
180
+ { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" },
181
+ { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" },
182
+ { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" },
183
+ { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" },
184
+ { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" },
185
+ { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" },
186
+ { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" },
187
+ { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" },
188
+ { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" },
189
+ { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" },
190
+ { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" },
191
+ { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" },
192
+ { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" },
193
+ { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" },
194
+ { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" },
195
+ { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" },
196
+ { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" },
197
+ ]
198
+
199
+ [[package]]
200
+ name = "tomlrange"
201
+ version = "0.2.2"
202
+ source = { editable = "." }
203
+
204
+ [package.dev-dependencies]
205
+ dev = [
206
+ { name = "pre-commit" },
207
+ { name = "pytest" },
208
+ { name = "ruff" },
209
+ ]
210
+
211
+ [package.metadata]
212
+
213
+ [package.metadata.requires-dev]
214
+ dev = [
215
+ { name = "pre-commit", specifier = ">=4.0" },
216
+ { name = "pytest", specifier = ">=8.0" },
217
+ { name = "ruff", specifier = ">=0.16.8" },
218
+ ]
219
+
220
+ [[package]]
221
+ name = "virtualenv"
222
+ version = "21.7.10"
223
+ source = { registry = "https://pypi.org/simple/" }
224
+ dependencies = [
225
+ { name = "distlib" },
226
+ { name = "filelock" },
227
+ { name = "platformdirs" },
228
+ { name = "python-discovery" },
229
+ ]
230
+ sdist = { url = "https://files.pythonhosted.org/packages/45/9d/5acd348310e0803c658c8cf7c4d928e2d22fc4f79c29098b651cd3edfdba/virtualenv-21.7.10.tar.gz", hash = "sha256:a7bf10f37ecc36f1942d6e469d6b59f5fe308f60ac711f66f51ef3bd8cb2c9aa", size = 5350247, upload-time = "2026-09-15T23:36:05.739Z" }
231
+ wheels = [
232
+ { url = "https://files.pythonhosted.org/packages/85/7c/b75957b57ef372d84628b1099c4a530b3c361878cc6c54a8dfc5071d371a/virtualenv-21.7.10-py3-none-any.whl", hash = "sha256:d7ac9669ba19e675ffadbb97fe8e887ef92fb1743fe9a0032be62b947657327a", size = 5324900, upload-time = "2026-09-15T23:36:03.751Z" },
233
+ ]
@@ -1 +0,0 @@
1
-
File without changes
File without changes