lrsched 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lrsched/__init__.py +34 -0
- lrsched/_types.py +4 -0
- lrsched/_validate.py +19 -0
- lrsched/compose.py +48 -0
- lrsched/py.typed +0 -0
- lrsched/schedules.py +170 -0
- lrsched-0.1.0.dist-info/METADATA +146 -0
- lrsched-0.1.0.dist-info/RECORD +10 -0
- lrsched-0.1.0.dist-info/WHEEL +4 -0
- lrsched-0.1.0.dist-info/licenses/LICENSE +21 -0
lrsched/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Framework-agnostic learning-rate schedules as pure functions, with zero dependencies."""
|
|
2
|
+
|
|
3
|
+
from ._types import Schedule
|
|
4
|
+
from .compose import sample, sequential, with_warmup
|
|
5
|
+
from .schedules import (
|
|
6
|
+
constant,
|
|
7
|
+
cosine,
|
|
8
|
+
cosine_restarts,
|
|
9
|
+
exponential,
|
|
10
|
+
inverse_sqrt,
|
|
11
|
+
linear,
|
|
12
|
+
multi_step,
|
|
13
|
+
one_cycle,
|
|
14
|
+
polynomial,
|
|
15
|
+
step_decay,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Schedule",
|
|
20
|
+
"constant",
|
|
21
|
+
"cosine",
|
|
22
|
+
"cosine_restarts",
|
|
23
|
+
"exponential",
|
|
24
|
+
"inverse_sqrt",
|
|
25
|
+
"linear",
|
|
26
|
+
"multi_step",
|
|
27
|
+
"one_cycle",
|
|
28
|
+
"polynomial",
|
|
29
|
+
"sample",
|
|
30
|
+
"sequential",
|
|
31
|
+
"step_decay",
|
|
32
|
+
"with_warmup",
|
|
33
|
+
]
|
|
34
|
+
__version__ = "0.1.0"
|
lrsched/_types.py
ADDED
lrsched/_validate.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from math import isfinite
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def check_finite(name: str, value: float) -> float:
|
|
5
|
+
if not isfinite(value):
|
|
6
|
+
raise ValueError(f"{name} must be a finite number, received {value!r}")
|
|
7
|
+
return value
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_positive_int(name: str, value: int) -> int:
|
|
11
|
+
if not isinstance(value, int) or value <= 0:
|
|
12
|
+
raise ValueError(f"{name} must be a positive integer, received {value!r}")
|
|
13
|
+
return value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_step(step: int) -> int:
|
|
17
|
+
if not isinstance(step, int) or step < 0:
|
|
18
|
+
raise ValueError(f"step must be a non-negative integer, received {step!r}")
|
|
19
|
+
return step
|
lrsched/compose.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from ._types import Schedule
|
|
4
|
+
from ._validate import check_finite, check_positive_int, check_step
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def with_warmup(schedule: Schedule, *, warmup_steps: int, start_lr: float) -> Schedule:
|
|
8
|
+
"""Prepend a linear warmup from start_lr to the wrapped schedule's value at step 0.
|
|
9
|
+
After warmup, the wrapped schedule runs with its step counted from the warmup end."""
|
|
10
|
+
check_positive_int("warmup_steps", warmup_steps)
|
|
11
|
+
check_finite("start_lr", start_lr)
|
|
12
|
+
target = schedule(0)
|
|
13
|
+
|
|
14
|
+
def wrapped(step: int) -> float:
|
|
15
|
+
check_step(step)
|
|
16
|
+
if step < warmup_steps:
|
|
17
|
+
return start_lr + (target - start_lr) * (step / warmup_steps)
|
|
18
|
+
return schedule(step - warmup_steps)
|
|
19
|
+
|
|
20
|
+
return wrapped
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sequential(phases: Sequence[tuple[int, Schedule]]) -> Schedule:
|
|
24
|
+
"""Run schedules back to back. Each phase is (duration, schedule), and within a phase
|
|
25
|
+
the schedule sees a step counted from that phase's start. Past the final phase, the
|
|
26
|
+
last phase's final value is held."""
|
|
27
|
+
if len(phases) == 0:
|
|
28
|
+
raise ValueError("sequential requires at least one phase")
|
|
29
|
+
for duration, _ in phases:
|
|
30
|
+
check_positive_int("phase duration", duration)
|
|
31
|
+
|
|
32
|
+
def wrapped(step: int) -> float:
|
|
33
|
+
check_step(step)
|
|
34
|
+
offset = 0
|
|
35
|
+
for duration, schedule in phases:
|
|
36
|
+
if step < offset + duration:
|
|
37
|
+
return schedule(step - offset)
|
|
38
|
+
offset += duration
|
|
39
|
+
last_duration, last_schedule = phases[-1]
|
|
40
|
+
return last_schedule(last_duration - 1)
|
|
41
|
+
|
|
42
|
+
return wrapped
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def sample(schedule: Schedule, *, num_steps: int) -> list[float]:
|
|
46
|
+
"""Evaluate a schedule at steps 0..num_steps-1, useful for plotting or testing."""
|
|
47
|
+
check_positive_int("num_steps", num_steps)
|
|
48
|
+
return [schedule(i) for i in range(num_steps)]
|
lrsched/py.typed
ADDED
|
File without changes
|
lrsched/schedules.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from math import cos, pi
|
|
3
|
+
|
|
4
|
+
from ._types import Schedule
|
|
5
|
+
from ._validate import check_finite, check_positive_int, check_step
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def constant(*, lr: float) -> Schedule:
|
|
9
|
+
"""A flat learning rate."""
|
|
10
|
+
check_finite("lr", lr)
|
|
11
|
+
|
|
12
|
+
def schedule(step: int) -> float:
|
|
13
|
+
check_step(step)
|
|
14
|
+
return lr
|
|
15
|
+
|
|
16
|
+
return schedule
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def step_decay(*, base_lr: float, gamma: float, step_size: int) -> Schedule:
|
|
20
|
+
"""Multiply by gamma every step_size steps."""
|
|
21
|
+
check_finite("base_lr", base_lr)
|
|
22
|
+
check_finite("gamma", gamma)
|
|
23
|
+
check_positive_int("step_size", step_size)
|
|
24
|
+
|
|
25
|
+
def schedule(step: int) -> float:
|
|
26
|
+
check_step(step)
|
|
27
|
+
return base_lr * gamma ** (step // step_size)
|
|
28
|
+
|
|
29
|
+
return schedule
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def multi_step(*, base_lr: float, milestones: Sequence[int], gamma: float) -> Schedule:
|
|
33
|
+
"""Multiply by gamma at each milestone step."""
|
|
34
|
+
check_finite("base_lr", base_lr)
|
|
35
|
+
check_finite("gamma", gamma)
|
|
36
|
+
ordered = sorted(milestones)
|
|
37
|
+
for m in ordered:
|
|
38
|
+
check_positive_int("milestone", m)
|
|
39
|
+
|
|
40
|
+
def schedule(step: int) -> float:
|
|
41
|
+
check_step(step)
|
|
42
|
+
drops = sum(1 for m in ordered if step >= m)
|
|
43
|
+
return base_lr * gamma**drops
|
|
44
|
+
|
|
45
|
+
return schedule
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def exponential(*, base_lr: float, gamma: float) -> Schedule:
|
|
49
|
+
"""Decay by a constant factor gamma each step: base_lr * gamma ** step."""
|
|
50
|
+
check_finite("base_lr", base_lr)
|
|
51
|
+
check_finite("gamma", gamma)
|
|
52
|
+
|
|
53
|
+
def schedule(step: int) -> float:
|
|
54
|
+
check_step(step)
|
|
55
|
+
return base_lr * gamma**step
|
|
56
|
+
|
|
57
|
+
return schedule
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def polynomial(*, base_lr: float, end_lr: float, total_steps: int, power: float) -> Schedule:
|
|
61
|
+
"""Polynomial decay from base_lr to end_lr over total_steps, then hold end_lr."""
|
|
62
|
+
check_finite("base_lr", base_lr)
|
|
63
|
+
check_finite("end_lr", end_lr)
|
|
64
|
+
check_positive_int("total_steps", total_steps)
|
|
65
|
+
check_finite("power", power)
|
|
66
|
+
|
|
67
|
+
def schedule(step: int) -> float:
|
|
68
|
+
check_step(step)
|
|
69
|
+
t = min(step, total_steps)
|
|
70
|
+
remaining = 1.0 - t / total_steps
|
|
71
|
+
return float(end_lr + (base_lr - end_lr) * remaining**power)
|
|
72
|
+
|
|
73
|
+
return schedule
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def linear(*, base_lr: float, end_lr: float, total_steps: int) -> Schedule:
|
|
77
|
+
"""Linear interpolation from base_lr to end_lr over total_steps, then hold end_lr."""
|
|
78
|
+
check_finite("base_lr", base_lr)
|
|
79
|
+
check_finite("end_lr", end_lr)
|
|
80
|
+
check_positive_int("total_steps", total_steps)
|
|
81
|
+
|
|
82
|
+
def schedule(step: int) -> float:
|
|
83
|
+
check_step(step)
|
|
84
|
+
t = min(step, total_steps)
|
|
85
|
+
return base_lr + (end_lr - base_lr) * (t / total_steps)
|
|
86
|
+
|
|
87
|
+
return schedule
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cosine(*, base_lr: float, min_lr: float, total_steps: int) -> Schedule:
|
|
91
|
+
"""Cosine annealing from base_lr at step 0 to min_lr at total_steps, then hold."""
|
|
92
|
+
check_finite("base_lr", base_lr)
|
|
93
|
+
check_finite("min_lr", min_lr)
|
|
94
|
+
check_positive_int("total_steps", total_steps)
|
|
95
|
+
|
|
96
|
+
def schedule(step: int) -> float:
|
|
97
|
+
check_step(step)
|
|
98
|
+
t = min(step, total_steps)
|
|
99
|
+
return min_lr + 0.5 * (base_lr - min_lr) * (1.0 + cos(pi * t / total_steps))
|
|
100
|
+
|
|
101
|
+
return schedule
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cosine_restarts(*, base_lr: float, min_lr: float, period: int, t_mult: int) -> Schedule:
|
|
105
|
+
"""Cosine annealing with warm restarts (SGDR). Each cycle resets to base_lr; cycle
|
|
106
|
+
length is multiplied by t_mult after each restart (t_mult = 1 keeps it periodic)."""
|
|
107
|
+
check_finite("base_lr", base_lr)
|
|
108
|
+
check_finite("min_lr", min_lr)
|
|
109
|
+
check_positive_int("period", period)
|
|
110
|
+
check_positive_int("t_mult", t_mult)
|
|
111
|
+
|
|
112
|
+
def schedule(step: int) -> float:
|
|
113
|
+
check_step(step)
|
|
114
|
+
start = 0
|
|
115
|
+
current = period
|
|
116
|
+
while step >= start + current:
|
|
117
|
+
start += current
|
|
118
|
+
current *= t_mult
|
|
119
|
+
t = step - start
|
|
120
|
+
return min_lr + 0.5 * (base_lr - min_lr) * (1.0 + cos(pi * t / current))
|
|
121
|
+
|
|
122
|
+
return schedule
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def inverse_sqrt(*, base_lr: float, warmup_steps: int) -> Schedule:
|
|
126
|
+
"""Transformer schedule: linear warmup to base_lr at warmup_steps, then decay by the
|
|
127
|
+
inverse square root of the step."""
|
|
128
|
+
check_finite("base_lr", base_lr)
|
|
129
|
+
check_positive_int("warmup_steps", warmup_steps)
|
|
130
|
+
|
|
131
|
+
def schedule(step: int) -> float:
|
|
132
|
+
check_step(step)
|
|
133
|
+
if step < warmup_steps:
|
|
134
|
+
return base_lr * step / warmup_steps
|
|
135
|
+
return float(base_lr * (warmup_steps / step) ** 0.5)
|
|
136
|
+
|
|
137
|
+
return schedule
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def one_cycle(
|
|
141
|
+
*, max_lr: float, total_steps: int, pct_start: float, initial_div: float, final_div: float
|
|
142
|
+
) -> Schedule:
|
|
143
|
+
"""One-cycle policy: cosine ramp up to max_lr over the first pct_start of training,
|
|
144
|
+
then cosine anneal down to max_lr / final_div. Starts at max_lr / initial_div."""
|
|
145
|
+
check_finite("max_lr", max_lr)
|
|
146
|
+
check_positive_int("total_steps", total_steps)
|
|
147
|
+
check_finite("pct_start", pct_start)
|
|
148
|
+
check_finite("initial_div", initial_div)
|
|
149
|
+
check_finite("final_div", final_div)
|
|
150
|
+
if not 0.0 < pct_start < 1.0:
|
|
151
|
+
raise ValueError(f"pct_start must be between 0 and 1, received {pct_start!r}")
|
|
152
|
+
if initial_div <= 0.0 or final_div <= 0.0:
|
|
153
|
+
raise ValueError("initial_div and final_div must be positive")
|
|
154
|
+
if total_steps < 2:
|
|
155
|
+
raise ValueError("total_steps must be at least 2 for one_cycle")
|
|
156
|
+
|
|
157
|
+
warmup = min(max(1, round(pct_start * total_steps)), total_steps - 1)
|
|
158
|
+
start_lr = max_lr / initial_div
|
|
159
|
+
end_lr = max_lr / final_div
|
|
160
|
+
|
|
161
|
+
def schedule(step: int) -> float:
|
|
162
|
+
check_step(step)
|
|
163
|
+
if step <= warmup:
|
|
164
|
+
frac = step / warmup
|
|
165
|
+
return start_lr + 0.5 * (max_lr - start_lr) * (1.0 - cos(pi * frac))
|
|
166
|
+
t = min(step, total_steps)
|
|
167
|
+
frac = (t - warmup) / (total_steps - warmup)
|
|
168
|
+
return end_lr + 0.5 * (max_lr - end_lr) * (1.0 + cos(pi * frac))
|
|
169
|
+
|
|
170
|
+
return schedule
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lrsched
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Framework-agnostic learning-rate schedules as pure functions, in Python with zero dependencies.
|
|
5
|
+
Project-URL: Homepage, https://github.com/amaar-mc/lrsched
|
|
6
|
+
Project-URL: Repository, https://github.com/amaar-mc/lrsched
|
|
7
|
+
Project-URL: Issues, https://github.com/amaar-mc/lrsched/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/amaar-mc/lrsched/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Amaar Chughtai
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 Amaar Chughtai
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: cosine-annealing,deep-learning,learning-rate,lr-schedule,machine-learning,one-cycle,scheduler,sgdr,training,warmup
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: Intended Audience :: Science/Research
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: >=3.10
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: hypothesis>=6; extra == 'dev'
|
|
47
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
48
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
49
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
# lrsched
|
|
53
|
+
|
|
54
|
+
<p align="center">
|
|
55
|
+
<img src="assets/logo.png" alt="lrsched logo" width="160">
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
[](https://pypi.org/project/lrsched/)
|
|
59
|
+
[](https://github.com/amaar-mc/lrsched/actions/workflows/ci.yml)
|
|
60
|
+
[](./LICENSE)
|
|
61
|
+
|
|
62
|
+
Framework-agnostic learning-rate schedules as pure functions, in Python with zero dependencies. Each schedule maps a step to a learning rate, so it works in any training loop or framework, or none.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
pip install lrsched
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 30-second example
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from lrsched import cosine, with_warmup, sample
|
|
74
|
+
|
|
75
|
+
schedule = with_warmup(
|
|
76
|
+
cosine(base_lr=1e-3, min_lr=1e-5, total_steps=1000),
|
|
77
|
+
warmup_steps=100,
|
|
78
|
+
start_lr=0.0,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
lr = schedule(250) # learning rate at step 250
|
|
82
|
+
curve = sample(schedule, num_steps=1000) # the whole curve, for plotting or logging
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A schedule is just `Callable[[int], float]`. Plug `schedule(step)` into your optimizer
|
|
86
|
+
however your framework expects, or use it to drive a plain training loop.
|
|
87
|
+
|
|
88
|
+
## Why this exists
|
|
89
|
+
|
|
90
|
+
Every learning-rate scheduler is tied to a framework: `torch.optim.lr_scheduler`,
|
|
91
|
+
`timm`, `transformers`, or `optax` for JAX. If you write a custom loop, use a
|
|
92
|
+
non-PyTorch stack, or just want to plot a schedule, you end up pasting a `LambdaLR`
|
|
93
|
+
snippet. `lrsched` is a small, dependency-free library where each schedule is a pure
|
|
94
|
+
function, easy to test, plot, log, and reuse anywhere.
|
|
95
|
+
|
|
96
|
+
## Comparison
|
|
97
|
+
|
|
98
|
+
| | lrsched | torch / timm | optax |
|
|
99
|
+
|---|:---:|:---:|:---:|
|
|
100
|
+
| Framework | none | PyTorch | JAX |
|
|
101
|
+
| Pure step to lr function | yes | no (optimizer-bound) | partial |
|
|
102
|
+
| Zero dependencies | yes | no | no |
|
|
103
|
+
| Composable warmup and phases | yes | partial | yes |
|
|
104
|
+
|
|
105
|
+
## Schedules
|
|
106
|
+
|
|
107
|
+
- `constant`, `step_decay`, `multi_step`, `exponential`
|
|
108
|
+
- `linear`, `polynomial`
|
|
109
|
+
- `cosine`, `cosine_restarts` (SGDR)
|
|
110
|
+
- `inverse_sqrt` (Transformer)
|
|
111
|
+
- `one_cycle`
|
|
112
|
+
|
|
113
|
+
## Composition
|
|
114
|
+
|
|
115
|
+
- `with_warmup(schedule, ...)` prepends a linear warmup.
|
|
116
|
+
- `sequential(phases)` runs schedules back to back.
|
|
117
|
+
- `sample(schedule, num_steps=...)` evaluates a schedule over a range.
|
|
118
|
+
|
|
119
|
+
Parameters are required keyword arguments, so every schedule reads explicitly at the call
|
|
120
|
+
site. Schedules hold their final value past the end rather than erroring, and a negative
|
|
121
|
+
step raises.
|
|
122
|
+
|
|
123
|
+
## Examples
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
python examples/schedules.py
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Testing
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
pip install -e ".[dev]"
|
|
133
|
+
pytest
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Tests cover the exact value of each schedule at known steps, schedule-specific shapes
|
|
137
|
+
(restarts, the one-cycle peak, the warmup handoff), and invariants checked with
|
|
138
|
+
Hypothesis (cosine stays within bounds, warmup is monotone).
|
|
139
|
+
|
|
140
|
+
## Contributing
|
|
141
|
+
|
|
142
|
+
Issues and pull requests are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
lrsched/__init__.py,sha256=IIce4y2wDGfX20N5mDrg4n_auf5XTz8Y0gE13yvORDs,640
|
|
2
|
+
lrsched/_types.py,sha256=juJaHUF6QXBdcDR0x5PS9gkdqB9NhiVkmL999gNHCKA,138
|
|
3
|
+
lrsched/_validate.py,sha256=5GC6kEL4Qx7BIM_tzbD-PDMJfOuuOQFvIBmblAw6sQA,592
|
|
4
|
+
lrsched/compose.py,sha256=yhFPuT3FJQ5ierXeNAT_OGU-IfWAT-KjxU9K6rvXIhE,1835
|
|
5
|
+
lrsched/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
lrsched/schedules.py,sha256=gx5K_mc1Wu6HzAhjeJk_FAZiKJx1n21eWkuyZ1gRXKc,5761
|
|
7
|
+
lrsched-0.1.0.dist-info/METADATA,sha256=oXungvasmPsK9ef61NjjOhEqaC6cFlaLouR2hlXMdxQ,5511
|
|
8
|
+
lrsched-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
9
|
+
lrsched-0.1.0.dist-info/licenses/LICENSE,sha256=NORTVJb4x206RXQkpZt_vpj8I7aZL6Vn26BvTOq6J40,1071
|
|
10
|
+
lrsched-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amaar Chughtai
|
|
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.
|