rich-stepper 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.
Files changed (36) hide show
  1. rich_stepper-0.1.0/.github/workflows/ci.yml +45 -0
  2. rich_stepper-0.1.0/.github/workflows/publish.yml +62 -0
  3. rich_stepper-0.1.0/.gitignore +107 -0
  4. rich_stepper-0.1.0/LICENSE +21 -0
  5. rich_stepper-0.1.0/PKG-INFO +366 -0
  6. rich_stepper-0.1.0/README.md +331 -0
  7. rich_stepper-0.1.0/examples/01_basic.py +15 -0
  8. rich_stepper-0.1.0/examples/02_custom_colors.py +22 -0
  9. rich_stepper-0.1.0/examples/03_emoji_symbols.py +21 -0
  10. rich_stepper-0.1.0/examples/04_thick_connectors.py +19 -0
  11. rich_stepper-0.1.0/examples/05_step_gap.py +19 -0
  12. rich_stepper-0.1.0/examples/06_all_completed.py +16 -0
  13. rich_stepper-0.1.0/examples/07_in_progress.py +29 -0
  14. rich_stepper-0.1.0/examples/08_minimal_theme.py +22 -0
  15. rich_stepper-0.1.0/examples/09_dense_layout.py +21 -0
  16. rich_stepper-0.1.0/examples/10_many_steps.py +32 -0
  17. rich_stepper-0.1.0/examples/11_single_step.py +14 -0
  18. rich_stepper-0.1.0/examples/12_live_update.py +29 -0
  19. rich_stepper-0.1.0/examples/13_elapsed_time.py +41 -0
  20. rich_stepper-0.1.0/examples/14_progress_bar.py +36 -0
  21. rich_stepper-0.1.0/examples/15_step_logging.py +43 -0
  22. rich_stepper-0.1.0/examples/16_full_featured.py +59 -0
  23. rich_stepper-0.1.0/examples/17_web_scraper.py +114 -0
  24. rich_stepper-0.1.0/main.py +41 -0
  25. rich_stepper-0.1.0/pyproject.toml +58 -0
  26. rich_stepper-0.1.0/stepper/__init__.py +19 -0
  27. rich_stepper-0.1.0/stepper/columns.py +151 -0
  28. rich_stepper-0.1.0/stepper/stepper.py +151 -0
  29. rich_stepper-0.1.0/stepper/theme.py +37 -0
  30. rich_stepper-0.1.0/stepper/types.py +22 -0
  31. rich_stepper-0.1.0/tests/__init__.py +0 -0
  32. rich_stepper-0.1.0/tests/test_columns.py +147 -0
  33. rich_stepper-0.1.0/tests/test_logging.py +170 -0
  34. rich_stepper-0.1.0/tests/test_stepper.py +357 -0
  35. rich_stepper-0.1.0/tests/test_theme.py +152 -0
  36. rich_stepper-0.1.0/uv.lock +231 -0
@@ -0,0 +1,45 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ name: Test (Python ${{ matrix.python-version }})
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ - name: Install uv
23
+ run: pip install uv
24
+ - name: Install dependencies
25
+ run: uv sync --group dev
26
+ - name: Run tests
27
+ run: uv run pytest tests/ -v
28
+
29
+ build:
30
+ name: Build package
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - uses: actions/setup-python@v5
35
+ with:
36
+ python-version: "3.13"
37
+ - name: Install uv
38
+ run: pip install uv
39
+ - name: Build sdist and wheel
40
+ run: uv build
41
+ - name: Upload artifacts
42
+ uses: actions/upload-artifact@v4
43
+ with:
44
+ name: dist
45
+ path: dist/
@@ -0,0 +1,62 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: write
10
+ id-token: write
11
+
12
+ jobs:
13
+ build:
14
+ name: Build package
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.13"
21
+ - name: Install uv
22
+ run: pip install uv
23
+ - name: Build sdist and wheel
24
+ run: uv build
25
+ - name: Upload artifacts
26
+ uses: actions/upload-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+
31
+ publish:
32
+ name: Publish to PyPI
33
+ needs: [build]
34
+ runs-on: ubuntu-latest
35
+ environment: pypi
36
+ permissions:
37
+ id-token: write
38
+ steps:
39
+ - uses: actions/download-artifact@v4
40
+ with:
41
+ name: dist
42
+ path: dist/
43
+ - name: Publish
44
+ uses: pypa/gh-action-pypi-publish@release/v1
45
+
46
+ github-release:
47
+ name: Create GitHub Release
48
+ needs: [build]
49
+ runs-on: ubuntu-latest
50
+ permissions:
51
+ contents: write
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+ with:
55
+ fetch-depth: 0
56
+ - uses: actions/download-artifact@v4
57
+ with:
58
+ name: dist
59
+ path: dist/
60
+ - uses: softprops/action-gh-release@v2
61
+ with:
62
+ files: dist/*
@@ -0,0 +1,107 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ *.egg
26
+ MANIFEST
27
+
28
+ # PyInstaller
29
+ *.manifest
30
+ *.spec
31
+
32
+ # Installer logs
33
+ pip-log.txt
34
+ pip-delete-this-directory.txt
35
+
36
+ # Unit test / coverage reports
37
+ htmlcov/
38
+ .tox/
39
+ .nox/
40
+ .coverage
41
+ .coverage.*
42
+ .cache
43
+ nosetests.xml
44
+ coverage.xml
45
+ *.cover
46
+ *.py,cover
47
+ .hypothesis/
48
+ .pytest_cache/
49
+ junit-report.xml
50
+
51
+ # Translations
52
+ *.mo
53
+ *.pot
54
+
55
+ # Environments
56
+ .env
57
+ .env.*
58
+ .venv/
59
+ env/
60
+ venv/
61
+ ENV/
62
+ env.bak/
63
+ venv.bak/
64
+
65
+ # Spyder project settings
66
+ .spyderproject
67
+ .spyproject
68
+
69
+ # Rope project settings
70
+ .ropeproject
71
+
72
+ # mypy
73
+ .mypy_cache/
74
+ .dmypy.json
75
+ dmypy.json
76
+
77
+ # Pyre type checker
78
+ .pyre/
79
+
80
+ # pytype static type analyzer
81
+ .pytype/
82
+
83
+ # Cython debug symbols
84
+ cython_debug/
85
+
86
+ # Ruff
87
+ .ruff_cache/
88
+
89
+ # Pyright
90
+ pyrightconfig.json
91
+
92
+ # uv
93
+ .python-version
94
+
95
+ # IDE
96
+ .idea/
97
+ .vscode/
98
+ *.swp
99
+ *.swo
100
+ *~
101
+
102
+ # OS
103
+ .DS_Store
104
+ Thumbs.db
105
+
106
+ # Sisyphus
107
+ .sisyphus/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abbas
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,366 @@
1
+ Metadata-Version: 2.4
2
+ Name: rich-stepper
3
+ Version: 0.1.0
4
+ Summary: Beautiful multi-step terminal progress widget for Rich. Connected indicators, per-step logging, elapsed timers, and optional progress bars.
5
+ Project-URL: Homepage, https://github.com/abbazs/rich-stepper
6
+ Project-URL: Source, https://github.com/abbazs/rich-stepper
7
+ Project-URL: Issues, https://github.com/abbazs/rich-stepper/issues
8
+ Project-URL: Changelog, https://github.com/abbazs/rich-stepper/releases
9
+ Author-email: Abbas <abbazs@users.noreply.github.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: cli,progress,rich,stepper,terminal,tui,workflow
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Software Development :: Libraries
26
+ Classifier: Topic :: Software Development :: User Interfaces
27
+ Classifier: Topic :: Terminals
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.10
30
+ Requires-Dist: rich>=13.0.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: pyright>=1.1.408; extra == 'dev'
33
+ Requires-Dist: pytest>=9.0.2; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # rich-stepper
37
+
38
+ [![PyPI version](https://img.shields.io/pypi/v/rich-stepper)](https://pypi.org/project/rich-stepper)
39
+ [![Python versions](https://img.shields.io/pypi/pyversions/rich-stepper)](https://pypi.org/project/rich-stepper)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/abbazs/rich-stepper/blob/main/LICENSE)
41
+
42
+ Beautiful multi-step terminal progress widget for [Rich](https://rich.readthedocs.io/). Connected indicators, per-step logging, elapsed timers, and optional progress bars — all rendered live in your terminal.
43
+
44
+ ## Installation
45
+
46
+ ```
47
+ pip install rich-stepper
48
+ ```
49
+
50
+ Requires Python 3.10+ and [Rich](https://pypi.org/project/rich/) 13+.
51
+
52
+ ## Quick Start
53
+
54
+ ```python
55
+ from rich.console import Console
56
+ from stepper import StepDefinition, StepStatus, Stepper
57
+
58
+ steps = [
59
+ StepDefinition("Clone repository", StepStatus.COMPLETED),
60
+ StepDefinition("Install dependencies", StepStatus.ACTIVE),
61
+ StepDefinition("Run tests", StepStatus.PENDING),
62
+ StepDefinition("Deploy", StepStatus.PENDING),
63
+ ]
64
+
65
+ stepper = Stepper(steps=steps)
66
+ Console().print(stepper)
67
+ ```
68
+
69
+ Renders a connected vertical stepper with status indicators:
70
+
71
+ ```
72
+ ● Clone repository
73
+
74
+ ◉ Install dependencies
75
+
76
+ ○ Run tests
77
+
78
+ ○ Deploy
79
+ ```
80
+
81
+ ## Live Updates
82
+
83
+ Use the context manager for animated step progression:
84
+
85
+ ```python
86
+ import time
87
+ from stepper import StepDefinition, StepStatus, Stepper
88
+
89
+ steps = [
90
+ StepDefinition("Connecting", StepStatus.ACTIVE),
91
+ StepDefinition("Downloading", StepStatus.PENDING),
92
+ StepDefinition("Installing", StepStatus.PENDING),
93
+ StepDefinition("Done", StepStatus.PENDING),
94
+ ]
95
+
96
+ stepper = Stepper(steps=steps)
97
+ with stepper:
98
+ for i in range(len(steps)):
99
+ time.sleep(0.5)
100
+ if i > 0:
101
+ stepper.set_step_status(i, StepStatus.ACTIVE)
102
+ time.sleep(0.5)
103
+ stepper.set_step_status(i, StepStatus.COMPLETED)
104
+ ```
105
+
106
+ ## Per-Step Logging
107
+
108
+ Append log messages to any step with `stepper.log()`:
109
+
110
+ ```python
111
+ from rich.console import Console
112
+ from stepper import StepDefinition, StepStatus, Stepper, StepperTheme
113
+
114
+ theme = StepperTheme(max_log_rows=3, log_style="dim italic", log_prefix="›")
115
+
116
+ stepper = Stepper(
117
+ steps=[
118
+ StepDefinition("Fetch schema", StepStatus.COMPLETED, step_description="200 OK"),
119
+ StepDefinition("Migrate database", StepStatus.ACTIVE, step_description="Applying patches…"),
120
+ StepDefinition("Seed data", StepStatus.PENDING),
121
+ ],
122
+ theme=theme,
123
+ console=Console(),
124
+ auto_refresh=False,
125
+ )
126
+
127
+ stepper.log(0, "Connected to postgres://db:5432")
128
+ stepper.log(0, "Fetched 42 table definitions")
129
+ stepper.log(1, "Applying 001_create_users.sql")
130
+ stepper.log(1, "Applying 002_add_indexes.sql")
131
+ stepper.log(1, "Applying 003_seed_config.sql")
132
+ stepper.log(1, "Applying 004_migrate_orders.sql") # oldest dropped (max_log_rows=3)
133
+
134
+ Console().print(stepper)
135
+ ```
136
+
137
+ Logs render inline below (or above) each step label. Set `max_log_rows` to cap visible lines, or leave it `None` for terminal-height-aware truncation.
138
+
139
+ ### Log position
140
+
141
+ Control where logs appear relative to the step label:
142
+
143
+ ```python
144
+ from stepper import LogPosition, StepperTheme
145
+
146
+ theme = StepperTheme(log_position=LogPosition.ABOVE) # logs above the label
147
+ ```
148
+
149
+ ## Progress Bars
150
+
151
+ Enable per-step progress bars with `show_bar=True`:
152
+
153
+ ```python
154
+ theme = StepperTheme(show_bar=True, bar_width=20)
155
+ stepper = Stepper(steps=steps, theme=theme, console=Console(), auto_refresh=False)
156
+ stepper.set_step_progress(1, 0.6) # 60% complete
157
+ Console().print(stepper)
158
+ ```
159
+
160
+ The `percent` argument is clamped to `[0.0, 1.0]`.
161
+
162
+ ## Elapsed Time
163
+
164
+ Show elapsed time per step:
165
+
166
+ ```python
167
+ theme = StepperTheme(show_elapsed_time=True)
168
+ ```
169
+
170
+ Completed and active steps show elapsed time. Pending steps display `-:--:--`. Time freezes when a step is marked completed.
171
+
172
+ ## Dynamic Steps
173
+
174
+ Add steps at runtime inside a context manager:
175
+
176
+ ```python
177
+ with Stepper(theme=theme, console=console) as stepper:
178
+ for url, label in sites:
179
+ idx = stepper.add_step(label, status=StepStatus.ACTIVE, step_description=url)
180
+ stepper.log(idx, f"Fetching {url}")
181
+ # ... do work ...
182
+ stepper.set_step_status(idx, StepStatus.COMPLETED)
183
+ ```
184
+
185
+ ## Theming
186
+
187
+ `StepperTheme` is a frozen dataclass — set any combination of fields at construction:
188
+
189
+ ### Symbols and styles
190
+
191
+ ```python
192
+ theme = StepperTheme(
193
+ completed_symbol="✓",
194
+ active_symbol="◎",
195
+ pending_symbol="○",
196
+ completed_style="green bold",
197
+ active_style="magenta bold",
198
+ pending_style="bright_black",
199
+ )
200
+ ```
201
+
202
+ ### Connectors
203
+
204
+ ```python
205
+ theme = StepperTheme(
206
+ connector_symbol="│",
207
+ connector_style="bright_black",
208
+ line_thickness=2, # repeat connector symbol N times
209
+ step_gap=1, # blank lines between steps
210
+ )
211
+ ```
212
+
213
+ ### Label formatting
214
+
215
+ ```python
216
+ theme = StepperTheme(
217
+ label_style="",
218
+ step_description_style="dim",
219
+ label_padding=2, # spaces before label text
220
+ )
221
+ ```
222
+
223
+ ### Progress bar styling
224
+
225
+ ```python
226
+ theme = StepperTheme(
227
+ show_bar=True,
228
+ bar_width=20,
229
+ bar_complete_style="bar.complete",
230
+ bar_finished_style="bar.finished",
231
+ bar_pulse_style="bar.pulse",
232
+ )
233
+ ```
234
+
235
+ ### Log styling
236
+
237
+ ```python
238
+ from stepper import LogPosition
239
+
240
+ theme = StepperTheme(
241
+ log_position=LogPosition.BELOW, # or LogPosition.ABOVE
242
+ max_log_rows=5, # None for terminal-aware
243
+ log_style="dim italic",
244
+ log_prefix="›",
245
+ )
246
+ ```
247
+
248
+ ### Time column
249
+
250
+ ```python
251
+ theme = StepperTheme(
252
+ show_elapsed_time=True,
253
+ time_style="progress.elapsed",
254
+ )
255
+ ```
256
+
257
+ ## API Reference
258
+
259
+ ### `Stepper`
260
+
261
+ Extends `rich.progress.Progress`. All Progress constructor args are forwarded.
262
+
263
+ | Method | Description |
264
+ |---|---|
265
+ | `Stepper(steps, theme, console, ...)` | Create stepper with optional initial steps and theme |
266
+ | `add_step(label, status, step_description)` | Add a step, returns `TaskID` |
267
+ | `add_steps(steps)` | Add multiple steps from `StepDefinition` list |
268
+ | `set_step_status(index, status)` | Update step status |
269
+ | `set_step_progress(index, percent)` | Set progress bar (0.0–1.0) |
270
+ | `log(index, message)` | Append a log message to a step |
271
+
272
+ ### `StepStatus`
273
+
274
+ | Value | Description |
275
+ |---|---|
276
+ | `StepStatus.PENDING` | Not yet started |
277
+ | `StepStatus.ACTIVE` | Currently running |
278
+ | `StepStatus.COMPLETED` | Finished |
279
+
280
+ ### `LogPosition`
281
+
282
+ | Value | Description |
283
+ |---|---|
284
+ | `LogPosition.BELOW` | Logs render below the step label (default) |
285
+ | `LogPosition.ABOVE` | Logs render above the step label |
286
+
287
+ ### `StepDefinition`
288
+
289
+ ```python
290
+ StepDefinition(label, status=StepStatus.PENDING, step_description=None)
291
+ ```
292
+
293
+ ### `StepperTheme`
294
+
295
+ All fields have sensible defaults. Override any combination:
296
+
297
+ | Field | Type | Default |
298
+ |---|---|---|
299
+ | `completed_symbol` | `str` | `"●"` |
300
+ | `active_symbol` | `str` | `"◉"` |
301
+ | `pending_symbol` | `str` | `"○"` |
302
+ | `completed_style` | `str` | `"green"` |
303
+ | `active_style` | `str` | `"cyan bold"` |
304
+ | `pending_style` | `str` | `"bright_black"` |
305
+ | `connector_symbol` | `str` | `"│"` |
306
+ | `connector_style` | `str` | `"bright_black"` |
307
+ | `line_thickness` | `int` | `1` |
308
+ | `step_gap` | `int` | `0` |
309
+ | `label_style` | `str` | `""` |
310
+ | `step_description_style` | `str` | `"dim"` |
311
+ | `label_padding` | `int` | `1` |
312
+ | `show_elapsed_time` | `bool` | `False` |
313
+ | `time_style` | `str` | `"progress.elapsed"` |
314
+ | `show_bar` | `bool` | `False` |
315
+ | `bar_width` | `int \| None` | `20` |
316
+ | `bar_complete_style` | `str` | `"bar.complete"` |
317
+ | `bar_finished_style` | `str` | `"bar.finished"` |
318
+ | `bar_pulse_style` | `str` | `"bar.pulse"` |
319
+ | `log_position` | `LogPosition` | `LogPosition.BELOW` |
320
+ | `max_log_rows` | `int \| None` | `None` |
321
+ | `log_style` | `str` | `"dim italic"` |
322
+ | `log_prefix` | `str` | `"›"` |
323
+
324
+ ## Examples
325
+
326
+ The `examples/` directory contains 17 runnable scripts covering every feature:
327
+
328
+ | Example | Description |
329
+ |---|---|
330
+ | `01_basic` | Default theme with mixed statuses |
331
+ | `02_custom_colors` | Custom symbol and style colors |
332
+ | `03_emoji_symbols` | Emoji-based step indicators |
333
+ | `04_thick_connectors` | Multi-line connector glyphs |
334
+ | `05_step_gap` | Blank lines between steps |
335
+ | `06_all_completed` | All steps in completed state |
336
+ | `07_in_progress` | Partially completed workflow |
337
+ | `08_minimal_theme` | Stripped-down minimal appearance |
338
+ | `09_dense_layout` | Compact layout with descriptions |
339
+ | `10_many_steps` | Handling 10+ steps |
340
+ | `11_single_step` | Single-step edge case |
341
+ | `12_live_update` | Animated context manager usage |
342
+ | `13_elapsed_time` | Elapsed time column |
343
+ | `14_progress_bar` | Per-step progress bars |
344
+ | `15_step_logging` | Inline log messages |
345
+ | `16_full_featured` | Everything combined |
346
+ | `17_web_scraper` | Real-world web scraper demo |
347
+
348
+ Run any example directly:
349
+
350
+ ```bash
351
+ python examples/16_full_featured.py
352
+ ```
353
+
354
+ ## How It Works
355
+
356
+ `Stepper` extends Rich's `Progress` class with custom columns:
357
+
358
+ - **StepIndicatorColumn** — renders status symbols and vertical connectors
359
+ - **StepLabelColumn** — renders labels, descriptions, and inline logs
360
+ - **StepperTimeColumn** — renders elapsed/frozen time per step
361
+
362
+ Status mapping and log rendering are handled by shared `StatusMapper` and `LogRenderer` helpers for consistency across columns.
363
+
364
+ ## License
365
+
366
+ [MIT](https://github.com/abbazs/rich-stepper/blob/main/LICENSE)