hpc-sandbox 0.3.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.
@@ -0,0 +1,226 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
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
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ *.lcov
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ # Pipfile.lock
97
+
98
+ # uv
99
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # hpc-sandbox is a library and commits none: ignore the one `uv sync --group dev` writes.
103
+ uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ # poetry.lock
111
+ # poetry.toml
112
+
113
+ # pdm
114
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
115
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
116
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
117
+ # pdm.lock
118
+ # pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # pixi
123
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
124
+ # pixi.lock
125
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
126
+ # in the .venv directory. It is recommended not to include this directory in version control.
127
+ .pixi/*
128
+ !.pixi/config.toml
129
+
130
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
131
+ __pypackages__/
132
+
133
+ # Celery stuff
134
+ celerybeat-schedule*
135
+ celerybeat.pid
136
+
137
+ # Redis
138
+ *.rdb
139
+ *.aof
140
+ *.pid
141
+
142
+ # RabbitMQ
143
+ mnesia/
144
+ rabbitmq/
145
+ rabbitmq-data/
146
+
147
+ # ActiveMQ
148
+ activemq-data/
149
+
150
+ # SageMath parsed files
151
+ *.sage.py
152
+
153
+ # Environments
154
+ .env
155
+ .envrc
156
+ .venv
157
+ env/
158
+ venv/
159
+ ENV/
160
+ env.bak/
161
+ venv.bak/
162
+
163
+ # Spyder project settings
164
+ .spyderproject
165
+ .spyproject
166
+
167
+ # Rope project settings
168
+ .ropeproject
169
+
170
+ # mkdocs/Zensical documentation
171
+ /site
172
+
173
+ # mypy
174
+ .mypy_cache/
175
+ .dmypy.json
176
+ dmypy.json
177
+
178
+ # Pyre type checker
179
+ .pyre/
180
+
181
+ # pytype static type analyzer
182
+ .pytype/
183
+
184
+ # Cython debug symbols
185
+ cython_debug/
186
+
187
+ # PyCharm
188
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
189
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
190
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
191
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
192
+ # .idea/
193
+
194
+ # Abstra
195
+ # Abstra is an AI-powered process automation framework.
196
+ # Ignore directories containing user credentials, local state, and settings.
197
+ # Learn more at https://abstra.io/docs
198
+ .abstra/
199
+
200
+ # Visual Studio Code
201
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore that
202
+ # can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
203
+ # and can be added to the global gitignore or merged into this file. However, if you prefer, you
204
+ # could uncomment the following to ignore the entire vscode folder
205
+ # .vscode/
206
+ # Temporary file for partial code execution
207
+ tempCodeRunnerFile.py
208
+
209
+ # Ruff stuff:
210
+ .ruff_cache/
211
+
212
+ # PyPI configuration file
213
+ .pypirc
214
+
215
+ # Marimo
216
+ marimo/_static/
217
+ marimo/_lsp/
218
+ __marimo__/
219
+
220
+ # Streamlit
221
+ .streamlit/secrets.toml
222
+
223
+ # Local settings of coding assistants, and the logs of batch jobs run from the checkout
224
+ .claude/
225
+ slurm*.out
226
+ nohup*.out
@@ -0,0 +1,238 @@
1
+ # Changelog
2
+
3
+ What each release of hpc-sandbox changes. Its version is below 1.0: a minor release (0.4.0) may
4
+ change the Python API or the command line, and a patch release (0.3.1) only fixes bugs. To stay on
5
+ one minor release, require a range such as `hpc-sandbox>=0.3,<0.4`.
6
+
7
+ ## 0.3.0 (2026-10-18)
8
+
9
+ The first release on PyPI. It adds pods, a fuse-overlay root filesystem layout and per-node image
10
+ templates for Singularity, deletes stopped sessions in the background, and replaces the memory
11
+ watchdog's victim rule. [Upgrading](#upgrading) lists what callers of 0.2.0 may need to change.
12
+
13
+ ### Changed behaviour
14
+
15
+ - **A stop returns once its session left the session root**: it is moved into the root's `.trash`
16
+ under the janitor lock, and a detached `rm` deletes it afterwards, one deletion per session root
17
+ at a time, at the lowest CPU priority (while 16 of a process's deletions are pending, its stops
18
+ delete their sessions themselves). `await provider.wait_for_deletions()` waits for them; call it
19
+ before your program exits inside a batch job, since the end of the job or of its Slurm step kills
20
+ them, and before removing a scratch root yourself. On a scratch too full even to move a session,
21
+ the stop deletes it in place, and a stop that failed is retried by the next. Janitors take over
22
+ the trash entries that no deletion holds: at once when the process that moved them died, a minute
23
+ after the move otherwise. [Stopping and deleting](docs/python-api.md#stopping-and-deleting)
24
+ describes it.
25
+ - **Singularity instances start and stop under their own control lock**, not a node-wide one, so
26
+ they start and stop in parallel; see [Instances](docs/singularity.md#instances).
27
+ - **A provider sweeps abandoned sessions at its first start that finds the janitor lock free**, and
28
+ at a later start otherwise: a start never waits for the janitor lock. Janitors and `reap()` still
29
+ do. A first start stops and moves every abandoned session it finds, but deletes at most 64 trash
30
+ entries; `reap()` and `hpc-sandbox janitor` take the rest, holding at most 64 entries at a time.
31
+ See [Janitors](docs/architecture.md#janitors).
32
+ - **The memory watchdog kills the branch of processes that holds the memory**, not the largest
33
+ single process. It starts from the heaviest of the running commands and the processes left running
34
+ in the background, and descends into the child whose death frees the most, while that child alone
35
+ frees the excess over the limit. A fork copy of its parent with no fork copies of its own counts
36
+ only its private memory towards what killing its branch frees, since the pages it shares with its
37
+ parent stay; one with fork copies below it counts its whole branch. So a harness survives a tool
38
+ command that ran away, even when the harness is the largest process; a child it joins (as
39
+ code-evaluation graders run each sample, with a `multiprocessing.Manager`) dies alone; and a pool
40
+ owner that grows dies with its fork workers. As with the kernel, a pool's worker that holds the
41
+ excess alone dies alone: a `multiprocessing.Pool`, which never notices a lost worker, then waits
42
+ for its task until the command times out. The branch dies whole, with what it forks meanwhile.
43
+ `oom_score_adj` counts as for the kernel's OOM killer, relative to the parent's value and for each
44
+ process alone: tool commands started with `choom -n 1000` go first, the marks of a browser's
45
+ renderers do not add up, and a value every process inherited changes nothing.
46
+ [The memory watchdog](docs/architecture.md#the-memory-watchdog) has the complete rules.
47
+ - **A command whose descendants the watchdog killed keeps its own exit status**, with the
48
+ explanation appended to stderr (also when it then times out); only a command the watchdog killed
49
+ itself returns 137. See [Memory limits](docs/python-api.md#memory-limits).
50
+ - **Processes that commands leave running in a Singularity sandbox can be killed by the watchdog.**
51
+ Only the processes that make up the sandbox when it starts are spared; see
52
+ [What it measures](docs/architecture.md#what-it-measures).
53
+
54
+ ### Added
55
+
56
+ - **Pods**: services sharing one network, like a docker-compose project.
57
+ `provider.start_pod(PodSpec(...))` returns a pod; `pod.start_service(ServiceSpec(...),
58
+ wait="started" | "healthy")` starts each service, with compose healthchecks (`HealthCheck`),
59
+ `health()`, `logs()`, upstream relays to host Unix sockets (`pod.routes`),
60
+ `set_network("none" | "egress")` and `stop()`. Both providers have the `pods` capability; see
61
+ [docs/pods.md](docs/pods.md).
62
+ - **`hpc-sandbox fetch-tools`** downloads the pinned slirp4netns that pods use for egress, for
63
+ hosts that have none; providers take a `slirp4netns` path and a `service_stop_grace_s`. See
64
+ [`fetch-tools`](docs/cli.md#fetch-tools).
65
+ - **A fuse-overlay layout for Singularity sandboxes**: `rootfs_layout="fuse-overlay"` (or
66
+ `HPC_SANDBOX_ROOTFS_LAYOUT` for a whole job) mounts each sandbox's root filesystem as a
67
+ `fuse-overlayfs` of the node's template of the image and a per-session upper layer, run by an
68
+ unprivileged instance. Starts and stops no longer depend on the image's size, and a snapshot holds
69
+ only what the sandbox wrote. It needs `fuse-overlayfs`, `fusermount3` (or `fusermount`),
70
+ `/dev/fuse` and unprivileged user namespaces. GPUs, cgroup limits and `--fakeroot` need the copy
71
+ layout: with an explicit `rootfs_layout="fuse-overlay"` they raise `ValueError`, and
72
+ `HPC_SANDBOX_ROOTFS_LAYOUT` leaves such a provider on the copy layout. Pod services always get the
73
+ copy layout. Janitors unmount the overlays of abandoned sessions, and the dead overlays that
74
+ killed `fuse-overlayfs` processes left anywhere on the node; see
75
+ [The fuse-overlay layout](docs/singularity.md#the-fuse-overlay-layout).
76
+ - **Templates** (`rootfs_templates`): each image extracted once per node, the fuse-overlay layout's
77
+ lower layer; with the copy layout, sandboxes copy it. A template that lost its marker is
78
+ extracted again, and extractions cut short are deleted by janitors. `EnrootProvider` accepts
79
+ `rootfs_templates` and `rootfs_layout="copy"` too. See [Templates](docs/singularity.md#templates).
80
+ - **`provider.wait_for_deletions()`** on both providers, which waits for the background deletions
81
+ of the sessions that the process stopped; see
82
+ [Stopping and deleting](docs/python-api.md#stopping-and-deleting).
83
+ - **`MemoryKill.pids`, `MemoryKill.processes` and `MemoryKill.tmpfs_bytes`** (in
84
+ `sandbox.memory_kills`): the processes of the killed branch, how many they were, and how much the
85
+ sandbox's own tmpfs mounts held (memory no kill frees); see
86
+ [Memory limits](docs/python-api.md#memory-limits).
87
+ - **`hpc-sandbox --version`**, `hpc_sandbox.__version__` and a `py.typed` marker; see
88
+ [Public API and versioning](docs/python-api.md#public-api-and-versioning).
89
+ - **Documentation**: the README is a landing page, and [`docs/`](docs/README.md) holds the guides
90
+ and the reference; `CONTRIBUTING.md` and `SECURITY.md` are new. The JSON keys of `pull` and
91
+ `janitor` are documented as stable ([Command line](docs/cli.md)).
92
+
93
+ ### Fixed
94
+
95
+ - **Cancelling a command while it starts no longer hangs.** On Python 3.11, asyncio waits forever
96
+ when the creation of a subprocess is cancelled while what it started keeps its output open (as
97
+ commands behind `nsenter` do); cancelled `exec` calls and teardowns could hang that way. See
98
+ [Running commands](docs/python-api.md#running-commands).
99
+ - **A janitor sweep no longer stalls a process starting or stopping many sandboxes.** Session
100
+ directories were created and removed in worker threads that waited for the janitor's lock, while
101
+ the sweep holding it needed a worker thread too. See [Sessions](docs/architecture.md#sessions).
102
+ - **A start cancelled while its session directory is created leaves nothing**: the directory and
103
+ its lease were kept until the process exited. See [Providers](docs/python-api.md#providers).
104
+ - **The memory watchdog measures processes on kernels before 4.14**, which have no `smaps_rollup`:
105
+ it reads `smaps` there. 0.2.0 measured every process as zero on them, and never killed. See
106
+ [What it measures](docs/architecture.md#what-it-measures).
107
+ - **An `HPC_SANDBOX_RUNTIME` other than `auto`, `singularity` or `enroot` is an error**: `doctor`,
108
+ `run`, `pull` and `janitor` exit with status 2 unless `--runtime` is given. 0.2.0 took such a
109
+ value for Singularity, so that a typo made `hpc-sandbox janitor` skip enroot's sessions. See
110
+ [Environment variables](docs/configuration.md#environment-variables).
111
+ - **`hpc-sandbox doctor` creates the scratch root as a start does**, with mode 0700, and reports a
112
+ scratch root or image cache that it cannot create or write as a `[FAIL]` line. With enroot, 0.2.0
113
+ could leave a scratch root it created readable by other users, and ended with a traceback where
114
+ it could not write. `allow setuid = no` is now shown as `info`: sandboxes run in user-namespace
115
+ mode too. See [`doctor`](docs/cli.md#doctor).
116
+
117
+ ### Upgrading
118
+
119
+ - **Stops return before their sessions are deleted.** Code that removes a scratch root, or ends its
120
+ process or Slurm step, right after stopping sandboxes should first
121
+ `await provider.wait_for_deletions()`; otherwise the removal races the deletions, and what the end
122
+ of a step kills is left for a janitor. See
123
+ [Stopping and deleting](docs/python-api.md#stopping-and-deleting).
124
+ - **Run `hpc-sandbox janitor` from the batch script, not from an epilogue.** Epilogues, which the
125
+ README of 0.2.0 suggested, usually run as root, whose janitor sweeps root's sessions, not yours;
126
+ see [Getting started](docs/getting-started.md#run-the-janitor-at-the-end-of-the-script).
127
+ - **`hpc_sandbox.sessions.remove_session` is gone**: it blocked a worker thread on the janitor lock.
128
+ Stopping a sandbox deletes its session; see
129
+ [Stopping and deleting](docs/python-api.md#stopping-and-deleting).
130
+ - **`MemoryKill.pid` and `MemoryKill.pss_bytes` describe the killed branch**: its root and what the
131
+ whole branch held, where 0.2.0 recorded the single process it killed. See
132
+ [Memory limits](docs/python-api.md#memory-limits).
133
+ - **A relative `scratch_root` is made absolute.** Sessions and snapshots that 0.2.0 recorded under a
134
+ relative scratch root are not recognised as bound to it: stop such sandboxes with 0.2.0, or
135
+ delete their files by hand. See
136
+ [Options of both providers](docs/configuration.md#options-of-both-providers).
137
+ - **The `test` extra is gone**: development tools are in the `dev` dependency group
138
+ (`uv sync --group dev`; see [CONTRIBUTING.md](CONTRIBUTING.md)).
139
+
140
+ ## 0.2.0
141
+
142
+ Changes since 0.1.0.
143
+
144
+ ### Changed behaviour
145
+
146
+ - **A working directory that is missing or cannot be entered fails the
147
+ command, not the sandbox.** `exec` returns exit code 126 (as `docker exec`
148
+ does), empty stdout and, on stderr, only
149
+ `CWD_ERROR_PREFIX + "<dir>: <reason>"` with a reason from
150
+ `CWD_ERROR_REASONS`. 0.1.0 raised nothing on enroot (the shell's own `cd`
151
+ error and status) and silently ran in `/` on Singularity CE. Nothing falls
152
+ back to another directory. A relative `cwd` resolves from the workdir.
153
+ - **Commands that never started raise `SandboxError`**, or
154
+ `SandboxLostError` when the container is gone, instead of returning the
155
+ runtime's exit status as the command's.
156
+ - **Transfers run from `/`**, so uploads and downloads of absolute paths work
157
+ once the workdir is gone; relative paths resolve from the workdir.
158
+ - **Downloads no longer carry**:
159
+ - owners (files on host mounts have owners the user namespace does not map);
160
+ - setuid, setgid and sticky bits; the caller's umask applies;
161
+ - symlinks that could lead outside the destination: absolute ones, `..`
162
+ climbing above it, or `..` after a name. They are left out, logged, and
163
+ returned by `download()` in a `DownloadReport`.
164
+ - **`download()` returns a `DownloadReport`** (it returned `None`).
165
+ - **Host-side transfer errors stay `OSError`s**, and special files on the host
166
+ raise `SpecialFileError` on upload (a character device given as the source,
167
+ such as `/dev/null`, is read to its end).
168
+ - **The environment reaches commands on stdin**, never on a command line or in
169
+ host-side processes, and commands without `stdin` read `/dev/null`. Names
170
+ the image's `/bin/sh` reserves (bash's `UID`, `EUID`, `PPID`, `BASHPID`, …;
171
+ a non-numeric `OPTIND` in dash) cannot be set.
172
+ - **Images a provider can never run raise `ValueError` before anything is
173
+ created**: a SIF image for enroot, a squashfs image for Singularity, a
174
+ scheme the runtime cannot pull.
175
+ - **enroot sandboxes use a system configuration the provider writes**: the
176
+ standard system mounts, a minimal `/dev`, the host's `/etc/hosts`, no
177
+ environment files, and only the `10-shadow`, `10-aptfix` and `98-nvidia`
178
+ hooks. Site hooks, mounts and environment files (home directories, Slurm and
179
+ PMIx variables, terminal settings) no longer apply. A new sandbox gets the
180
+ host's time zone as a regular `/etc/localtime` file.
181
+ - **`enroot import` gets only the variables pulling needs**, plus `import_env`.
182
+ - **Singularity sandboxes no longer see the host's `TERM` or proxy
183
+ variables**, and `gpu="nv"` sets `NVIDIA_VISIBLE_DEVICES=all` unless set.
184
+ - **enroot sessions live in `sessions-enroot`**, and janitors leave other
185
+ providers' sessions alone. Without `--runtime`, the CLI janitor
186
+ (`python -m hpc_sandbox janitor`) sweeps enroot sessions always and
187
+ Singularity sessions when Singularity or Apptainer is installed, and prints
188
+ one merged report (0.1.0 swept only the runtime it detected).
189
+ - **`output_limit_bytes` must be at least 4096**; smaller values raise
190
+ `ValueError`.
191
+
192
+ ### Added
193
+
194
+ - **Digest pins on enroot**: `docker://repo[:tag]@sha256:<hex>` (imported in
195
+ the `repo:sha256:<hex>` form enroot 3.x understands).
196
+ - **Content-addressed image cache with explicit refresh**:
197
+ - Images are stored as `sha256-<hex>.<ext>`, each named by a per-reference
198
+ pointer (`uri-<hash>.<ext>.json`, `oci-<digest>.<ext>.json`).
199
+ - `ImageStore.pull(reference, refresh=True)` and
200
+ `python -m hpc_sandbox pull [--refresh] IMAGE...` pull a tag again; the
201
+ command prints one JSON line per image with its `sha256` and
202
+ `manifest_digest`.
203
+ - Sandbox starts read the current pointer and never wait for a refresh.
204
+ - `ResolvedImage.sha256` is set for every pulled image, and the digests of
205
+ local images are remembered on node-local scratch across processes.
206
+ - **Stable error contract**: `CWD_ERROR_EXIT_CODE`, `CWD_ERROR_PREFIX`,
207
+ `CWD_ERROR_REASONS`, and `DownloadReport` / `DroppedLink`.
208
+ - **Owner records and snapshot metadata name the provider** and record
209
+ `remap_root` and `container_uid`.
210
+ - **enroot**: accepts the Singularity provider's option names (`gpu`,
211
+ `allow_network_none`, `stop_grace_s`, `cgroup_limits`); new `import_env` and
212
+ `bind_resolv_conf` options.
213
+
214
+ ### Fixed and hardened
215
+
216
+ - **Transfers go through directory descriptors** and never follow a symlink,
217
+ block on a FIFO or recurse into deep trees that code in the sandbox planted.
218
+ They can be cancelled mid-file, and their cleanup is bounded.
219
+ - **Restores**: a restored rootfs cannot make enroot mount its `/etc/fstab` or
220
+ load its `/etc/environment` from the host side.
221
+ - **Snapshot preparation** opens up only enroot's mode-0 mount points.
222
+
223
+ ### Upgrading
224
+
225
+ - **Image caches written by 0.1.0 are reused without pulling.** An image
226
+ stored as `uri-<hash>.<ext>` or `oci-<digest>.<ext>` is linked (or copied)
227
+ under its digest name the first time 0.2.0 resolves it.
228
+ - **0.1.0 keeps working on a cache it shares with 0.2.0.** 0.2.0 never
229
+ removes or rewrites 0.1.0's `uri-…`/`oci-…` image files or their `.json`
230
+ records, and keeps its own pointers in differently named files. 0.1.0
231
+ keeps using the images it pulled, and pulls a reference itself when only
232
+ 0.2.0 has pulled it; for a tag that has moved, the two versions can then
233
+ run different images. Pin images by digest while versions are mixed.
234
+ - **Earlier image files are never deleted automatically**: remove
235
+ `sha256-*` files no pointer names once no job uses them. With enroot every
236
+ refresh adds one, since squashfs images are not byte-reproducible.
237
+ - **Sessions**: enroot janitors still sweep the enroot sessions 0.1.0 left in
238
+ `sessions`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Apurva Gandhi
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.