repedal 0.1.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.
- repedal-0.1.0/CHANGELOG.md +16 -0
- repedal-0.1.0/LICENSE +21 -0
- repedal-0.1.0/MANIFEST.in +1 -0
- repedal-0.1.0/PKG-INFO +347 -0
- repedal-0.1.0/README.md +320 -0
- repedal-0.1.0/pyproject.toml +39 -0
- repedal-0.1.0/repedal.egg-info/PKG-INFO +347 -0
- repedal-0.1.0/repedal.egg-info/SOURCES.txt +13 -0
- repedal-0.1.0/repedal.egg-info/dependency_links.txt +1 -0
- repedal-0.1.0/repedal.egg-info/entry_points.txt +2 -0
- repedal-0.1.0/repedal.egg-info/requires.txt +1 -0
- repedal-0.1.0/repedal.egg-info/top_level.txt +1 -0
- repedal-0.1.0/repedal.py +1788 -0
- repedal-0.1.0/setup.cfg +4 -0
- repedal-0.1.0/tests/test_repedal.py +421 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to repedal are documented here. The project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] - 2026-09-02
|
|
7
|
+
|
|
8
|
+
Initial public release.
|
|
9
|
+
|
|
10
|
+
- Reconstruct damper-pedal and optional sostenuto data from over-held piano MIDI notes.
|
|
11
|
+
- Offer legato, voice-tracking, hand-span, and fixed-duration articulation models.
|
|
12
|
+
- Preserve unmanaged MIDI events and verify sounding ends before writing atomically.
|
|
13
|
+
- Select channels and tracks while keeping channel-wide pedal safety intact.
|
|
14
|
+
- Preview conversions, compare articulation models, and emit versioned JSON summaries.
|
|
15
|
+
|
|
16
|
+
[0.1.0]: https://github.com/ssmall256/repedal/releases/tag/v0.1.0
|
repedal-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ssmall256
|
|
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 @@
|
|
|
1
|
+
include CHANGELOG.md
|
repedal-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: repedal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Turn manually over-held MIDI piano notes into real CC64 sustain-pedal data
|
|
5
|
+
Author: ssmall256
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ssmall256/repedal
|
|
8
|
+
Project-URL: Repository, https://github.com/ssmall256/repedal
|
|
9
|
+
Project-URL: Issues, https://github.com/ssmall256/repedal/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/ssmall256/repedal/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: midi,piano,sustain,pedal,cc64,articulation
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: mido>=1.2
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# repedal
|
|
29
|
+
|
|
30
|
+
Most DAWs can flatten sustain-pedal (CC64) data into long MIDI note lengths. Going the
|
|
31
|
+
other way—recovering plausible key releases and real pedal data from those long notes—usually
|
|
32
|
+
requires a custom script. `repedal` does that conversion for piano MIDI.
|
|
33
|
+
|
|
34
|
+
It infers when the player's fingers could have left each key, shortens the MIDI notes to those
|
|
35
|
+
times, and writes damper-pedal events that preserve the input's sounding ends. The assembled
|
|
36
|
+
result is parsed again and simulated before anything is written.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m pip install repedal
|
|
40
|
+
repedal gymnopedie.mid
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
gymnopedie.mid -> gymnopedie.pedaled.mid
|
|
45
|
+
notes 282 (125 shortened, 157 left as written)
|
|
46
|
+
finger-sustain moved to the pedal: median 857ms per note, 159.3s total
|
|
47
|
+
pedal presses 71 damper (CC64)
|
|
48
|
+
pedal down ch0 43%, ch1 89% of the 121s piece
|
|
49
|
+
handover 21ms minimum before a key release
|
|
50
|
+
verification worst sounding-end shift 0ms, 0 note(s) over the 120ms tolerance; exact
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Python 3.9+ is required; [`mido`](https://mido.readthedocs.io) is installed automatically.
|
|
54
|
+
From a source checkout, use `python -m pip install .` to install the command and importable
|
|
55
|
+
module. Installing `mido` alone is enough to run `python repedal.py` directly.
|
|
56
|
+
|
|
57
|
+
Repedal is a public beta. The Python API and versioned JSON schema are intended for integration;
|
|
58
|
+
the prose reports printed for people may become clearer over time and should not be parsed by
|
|
59
|
+
software.
|
|
60
|
+
|
|
61
|
+
## Preview before writing
|
|
62
|
+
|
|
63
|
+
Pedal reconstruction is underdetermined: a long note might have been held by a finger or by a
|
|
64
|
+
pedal. Preview the chosen interpretation before committing to it:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
repedal piece.mid --dry-run --report
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`--dry-run` prints the same verification summary as a real conversion, and `--report` adds
|
|
71
|
+
individual presses and rejection reasons. It never writes a file. To compare the three
|
|
72
|
+
inference models that make musical judgements:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
repedal piece.mid --compare-articulations
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
piece.mid [articulation comparison; no file written]
|
|
80
|
+
mode shortened damper sostenuto rejected worst shift status
|
|
81
|
+
legato 125 71 0 0 0ms exact
|
|
82
|
+
voices 33 33 0 0 0ms exact
|
|
83
|
+
hands 26 26 0 0 0ms exact
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Machine-readable output
|
|
87
|
+
|
|
88
|
+
Use `--json` when another program needs the result:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
repedal piece.mid --dry-run --json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The command prints exactly one JSON object after its arguments have parsed successfully. Schema
|
|
95
|
+
version 1 has stable field names; fields may be added compatibly, while a removal or rename will
|
|
96
|
+
use a new `schema_version`. Warnings are included in the object instead of being printed
|
|
97
|
+
separately. `--json` and the prose-oriented `--report` cannot be combined.
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"input": "piece.mid",
|
|
102
|
+
"mode": "dry-run",
|
|
103
|
+
"output": null,
|
|
104
|
+
"result": {
|
|
105
|
+
"notes": {"shortened": 125, "total": 282, "unchanged": 157},
|
|
106
|
+
"pedal": {"damper_presses": 71, "rejected_presses": 0, "sostenuto_presses": 0},
|
|
107
|
+
"verification": {
|
|
108
|
+
"base_tolerance_ms": 120.0,
|
|
109
|
+
"exact": true,
|
|
110
|
+
"fixed_event_errors": 0,
|
|
111
|
+
"out_of_tolerance_notes": 0,
|
|
112
|
+
"unmatched_notes": 0,
|
|
113
|
+
"within_tolerance": true,
|
|
114
|
+
"worst_shift_ms": 0.0
|
|
115
|
+
},
|
|
116
|
+
"warnings": []
|
|
117
|
+
},
|
|
118
|
+
"schema_version": 1,
|
|
119
|
+
"status": "ok",
|
|
120
|
+
"written": false
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
For example, a subprocess consumer can replace regular-expression parsing with:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
import json
|
|
128
|
+
import subprocess
|
|
129
|
+
|
|
130
|
+
completed = subprocess.run(
|
|
131
|
+
["repedal", "piece.mid", "--dry-run", "--json"],
|
|
132
|
+
capture_output=True,
|
|
133
|
+
check=False,
|
|
134
|
+
text=True,
|
|
135
|
+
)
|
|
136
|
+
payload = json.loads(completed.stdout)
|
|
137
|
+
shortened = payload["result"]["notes"]["shortened"]
|
|
138
|
+
presses = payload["result"]["pedal"]["damper_presses"]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Exit status 0 means the requested operation completed, 1 means an input, configuration, or write
|
|
142
|
+
error, and 2 means verification was unsafe and no output was written. Articulation comparisons
|
|
143
|
+
still exit 0 when every requested analysis completes; inspect their JSON `status` fields for
|
|
144
|
+
safety. A deliberate `--allow-lossy` write also exits 0 and reports `"status": "unsafe"`.
|
|
145
|
+
|
|
146
|
+
## Choosing an articulation model
|
|
147
|
+
|
|
148
|
+
`--articulation` controls how finger releases are inferred:
|
|
149
|
+
|
|
150
|
+
| Mode | Behaviour |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `legato` *(default)* | Releases an attack group when the next selected attack group arrives. This recovers the most pedal from ordinary homophonic piano writing. |
|
|
153
|
+
| `voices` | Tracks non-crossing melodic streams and releases a note when its own stream moves on. A melody held over a moving bass therefore stays finger-held longer. |
|
|
154
|
+
| `hands` | Releases notes only when the held keys exceed `--max-fingers` or `--hand-span`. This is the most conservative musical model. |
|
|
155
|
+
| `fixed` | Uses only `--max-hold-beats` and/or `--max-hold-ms`. With neither cap, it is a no-op articulation model. |
|
|
156
|
+
|
|
157
|
+
`voices` uses a self-contained, pitch-ordered dynamic program; it does not require `music21`.
|
|
158
|
+
All attack grouping and hand/voice inference is limited to the selected notes.
|
|
159
|
+
|
|
160
|
+
## Selecting channels and tracks
|
|
161
|
+
|
|
162
|
+
Real MIDI files often contain drums, orchestration, or reference tracks that should not affect
|
|
163
|
+
piano articulation. Selection indexes are zero-based, matching `mido` and repedal's reports:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
repedal arrangement.mid --channels 0,1
|
|
167
|
+
repedal arrangement.mid --tracks 2,3
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- `--channels` chooses the MIDI channels whose notes may be shortened and whose CC64 stream is
|
|
171
|
+
regenerated. Other channels and their pedal events remain unchanged.
|
|
172
|
+
- `--tracks` chooses which tracks supply notes eligible for shortening. Because MIDI pedal is
|
|
173
|
+
channel-wide, unselected notes on the same channel still participate in safety simulation.
|
|
174
|
+
- General MIDI percussion channel 9 is ignored by default and reported as a warning. Use
|
|
175
|
+
`--include-percussion`, or select channel 9 explicitly, to process it.
|
|
176
|
+
- A selected channel declaring a non-piano General MIDI program is reported as a warning. The
|
|
177
|
+
conversion continues because program maps are conventions, not proof of instrumentation.
|
|
178
|
+
|
|
179
|
+
MIDI type 0 and type 1 files are supported. Type 2 files contain independent sequences with no
|
|
180
|
+
single shared timeline, so repedal rejects them; split them into separate type 0 or type 1 files
|
|
181
|
+
first.
|
|
182
|
+
|
|
183
|
+
## Pedal scope and existing pedal
|
|
184
|
+
|
|
185
|
+
CC64 is a per-channel controller. `--pedal-scope channel` (the default) plans each selected
|
|
186
|
+
channel independently, allowing a bass channel to be pedalled without smearing a melody
|
|
187
|
+
channel. `--pedal-scope global` derives one shared pedal plan and copies it to the selected
|
|
188
|
+
channels, which is appropriate when those channels will be flattened onto one instrument.
|
|
189
|
+
|
|
190
|
+
A global plan cannot preserve different existing pedal streams on different channels. Repedal
|
|
191
|
+
rejects that combination unless `--existing-pedal ignore` is used or channel scope is selected.
|
|
192
|
+
|
|
193
|
+
`--existing-pedal` controls existing CC64 on processed channels:
|
|
194
|
+
|
|
195
|
+
| Value | Behaviour |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `keep` *(default)* | Treats existing pedal as authoritative, keeps its sounding result, and adds any required coverage. |
|
|
198
|
+
| `replace` | Reconstructs pedal from what the input currently sounds like. |
|
|
199
|
+
| `ignore` | Discards existing CC64 on processed channels and interprets written note lengths literally. |
|
|
200
|
+
|
|
201
|
+
Existing CC66 is always part of the input sound model. A newly proposed sostenuto span is
|
|
202
|
+
rejected if it overlaps existing CC66, because a piano has only one middle pedal.
|
|
203
|
+
|
|
204
|
+
## Verification and write safety
|
|
205
|
+
|
|
206
|
+
Repedal verifies two things after assembling the output:
|
|
207
|
+
|
|
208
|
+
1. Every note is paired with its input note and its simulated sounding end is compared in real
|
|
209
|
+
time, through tempo changes.
|
|
210
|
+
2. Every event outside the documented transformation surface retains its track, absolute tick,
|
|
211
|
+
payload, and same-tick order. Note-off timing/velocity and CC64/CC66 on processed channels are
|
|
212
|
+
the only managed event classes.
|
|
213
|
+
|
|
214
|
+
The default `--blur-tol-ms 120` permits a musically bounded amount of over-ring when exact pedal
|
|
215
|
+
expression is impossible. This means “within tolerance” is not necessarily tick-exact.
|
|
216
|
+
`--strict` sets that tolerance to zero and requires exact sounding ends.
|
|
217
|
+
|
|
218
|
+
If verification exceeds the configured tolerance, the CLI prints the analysis, exits with
|
|
219
|
+
status 2, and does **not** write an output file. Library saves behave the same way. Output is
|
|
220
|
+
written through a temporary file and atomically moved into place only after validation.
|
|
221
|
+
|
|
222
|
+
`--allow-lossy` (or `Result.save(..., allow_lossy=True)`) is the explicit escape hatch for an
|
|
223
|
+
analysed result that the caller has decided to accept.
|
|
224
|
+
|
|
225
|
+
## Using it as a library
|
|
226
|
+
|
|
227
|
+
The CLI is a wrapper over the public API:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from repedal import Options, convert, convert_file
|
|
231
|
+
|
|
232
|
+
result = convert("in.mid", Options(articulation="voices", channels=(0, 1)))
|
|
233
|
+
print(result.summary())
|
|
234
|
+
if result.exact:
|
|
235
|
+
result.save("out.mid")
|
|
236
|
+
|
|
237
|
+
# Converts, verifies, and atomically writes. Raises ValueError if verification fails.
|
|
238
|
+
result = convert_file("in.mid", "out.mid")
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`convert` also accepts an already loaded `mido.MidiFile`. It reads but does not modify that
|
|
242
|
+
object:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
import mido
|
|
246
|
+
from repedal import convert
|
|
247
|
+
|
|
248
|
+
source = mido.MidiFile("in.mid")
|
|
249
|
+
result = convert(source)
|
|
250
|
+
extra_track = mido.MidiTrack()
|
|
251
|
+
result.midi.tracks.append(extra_track)
|
|
252
|
+
result.save("out.mid")
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A `Result` exposes both the file and the evidence behind it:
|
|
256
|
+
|
|
257
|
+
| Attribute | Contents |
|
|
258
|
+
| --- | --- |
|
|
259
|
+
| `midi` | Finished `mido.MidiFile` in memory. |
|
|
260
|
+
| `within_tolerance` / `lossless` | Whether verification satisfies the configured tolerance. `lossless` remains as a compatibility alias. |
|
|
261
|
+
| `exact` | Whether every sounding end is tick-exact and all fixed events match. |
|
|
262
|
+
| `presses` / `rejected` | Written and abandoned `Press` objects; rejected presses carry a `.reason`. |
|
|
263
|
+
| `presses[i].kind` | `"damper"` (CC64) or `"sostenuto"` (CC66). |
|
|
264
|
+
| `notes` / `shortened` | Parsed `Note` objects with `start`, `end`, `sound_end`, `key_end`, and `new_end`. |
|
|
265
|
+
| `deviations` / `out_of_tolerance` | `Deviation` objects containing `.ticks`, `.seconds`, and `.note`. |
|
|
266
|
+
| `event_errors` | Any change found outside managed note-off and pedal events. |
|
|
267
|
+
| `warnings` | Input preflight or planning warnings. |
|
|
268
|
+
| `min_handover_seconds` | Smallest press-to-key-release margin. |
|
|
269
|
+
| `summary()` / `report()` | The concise and detailed CLI-style reports. |
|
|
270
|
+
|
|
271
|
+
For analysis without output assembly, use `read_score` and `plan`:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
from repedal import Options, plan, read_score
|
|
275
|
+
|
|
276
|
+
warnings = []
|
|
277
|
+
score = read_score("in.mid")
|
|
278
|
+
target, scopes = plan(score, Options(channels=(0,)), warnings)
|
|
279
|
+
for note in score.notes[:5]:
|
|
280
|
+
print(note.pitch, note.start, note.sound_end, "->", note.key_end)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Sostenuto for pedal points
|
|
284
|
+
|
|
285
|
+
Damper pedal sustains every released key on its channel. It cannot express a long bass pedal
|
|
286
|
+
point while upper chords on that channel remain detached. `--sostenuto` allows repedal to move
|
|
287
|
+
eligible cases to CC66, which captures only keys held when the middle pedal is pressed:
|
|
288
|
+
|
|
289
|
+
```text
|
|
290
|
+
input 0:on36 240:on60,64,67 440:off60,64,67 1920:off36
|
|
291
|
+
output 0:on36 0:CC66=127 240:off36 240:on60,64,67 1920:CC66=0
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The capture set, existing CC66, and other generated spans are included in the final simulation.
|
|
295
|
+
Use this only with instruments that implement CC66.
|
|
296
|
+
|
|
297
|
+
## Pedal timing and physical controls
|
|
298
|
+
|
|
299
|
+
`--pedal-lag-ms 50` delays a press after its associated attack, reproducing syncopated
|
|
300
|
+
pedalling: strings are struck with the dampers down, and the foot follows. The requested lag
|
|
301
|
+
shrinks when necessary to catch the first key release safely.
|
|
302
|
+
|
|
303
|
+
`--min-handover-ms` (default 20) is the minimum intended margin between a press and the key
|
|
304
|
+
release it catches. The margin protects frame-sampled renderers from quantising both events
|
|
305
|
+
into one update. The achieved minimum is reported; dense textures can leave less room than
|
|
306
|
+
requested.
|
|
307
|
+
|
|
308
|
+
`--change-gap-ms` adds time between a lift and the next press. `--damping-ramp-ms` replaces an
|
|
309
|
+
instantaneous lift with four sub-threshold values (63 → 42 → 21 → 0) for modelled pianos that
|
|
310
|
+
follow continuous CC64. Switch-style instruments still see the lift at the first value below
|
|
311
|
+
64. `--release-velocity` changes the note-off velocity only for shortened keys.
|
|
312
|
+
|
|
313
|
+
Every millisecond option is converted through the complete tempo map rather than sampled at one
|
|
314
|
+
tempo. A 50 ms setting therefore remains 50 ms across accelerando or ritardando even though its
|
|
315
|
+
tick length changes.
|
|
316
|
+
|
|
317
|
+
`--blur-tol-alpha` can make the over-ring tolerance stricter in the bass and looser in the
|
|
318
|
+
treble. With a 120 ms base and alpha 1.5, the tolerance is approximately 15 ms at C2, 42 ms at
|
|
319
|
+
C3, and 339 ms at C5. `--min-note-ms`, `--min-shorten-ms`, and `--min-pedal-ms` suppress
|
|
320
|
+
implausibly short notes, edits, and presses.
|
|
321
|
+
|
|
322
|
+
Run `repedal --help` for the complete option list.
|
|
323
|
+
|
|
324
|
+
## Development
|
|
325
|
+
|
|
326
|
+
The regression suite covers event-order preservation, generated/existing sostenuto conflicts,
|
|
327
|
+
safe atomic writes, mixed-channel selection, percussion handling, type-2 rejection, preview
|
|
328
|
+
commands, and randomized no-op files:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
python -m unittest discover -s tests -v
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The same suite runs in CI on every Python version from 3.9 through 3.13. CI also builds and checks
|
|
335
|
+
the wheel and source distribution, then installs each into a clean environment.
|
|
336
|
+
|
|
337
|
+
## Versioning and license
|
|
338
|
+
|
|
339
|
+
Repedal follows semantic versioning. During the 0.x public beta, minor releases may revise the
|
|
340
|
+
Python API; changes are documented in the
|
|
341
|
+
[changelog](https://github.com/ssmall256/repedal/blob/main/CHANGELOG.md). The JSON interface is
|
|
342
|
+
independently versioned by its `schema_version` as described above.
|
|
343
|
+
|
|
344
|
+
Repedal is released under the
|
|
345
|
+
[MIT License](https://github.com/ssmall256/repedal/blob/main/LICENSE). Source, issues, and
|
|
346
|
+
releases are hosted at
|
|
347
|
+
[github.com/ssmall256/repedal](https://github.com/ssmall256/repedal).
|