data-sampler 3.2.1__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.
@@ -0,0 +1,427 @@
1
+ Metadata-Version: 2.4
2
+ Name: data-sampler
3
+ Version: 3.2.1
4
+ Summary: Representative (stratified) sampling and column anonymization for tabular data files, with a terminal UI.
5
+ Project-URL: Homepage, https://github.com/aaronified/data-sampler
6
+ Project-URL: Changelog, https://github.com/aaronified/data-sampler/blob/main/CHANGELOG.md
7
+ Author: aaronified
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 programanalytics-isb
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: anonymization,data,pandas,sampling,stratified,tui
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Environment :: Console
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
41
+ Requires-Python: >=3.10
42
+ Requires-Dist: numpy>=1.23
43
+ Requires-Dist: openpyxl>=3.0
44
+ Requires-Dist: pandas>=1.5
45
+ Requires-Dist: pyarrow>=10
46
+ Requires-Dist: textual>=0.86
47
+ Provides-Extra: dev
48
+ Requires-Dist: build; extra == 'dev'
49
+ Requires-Dist: duckdb>=1.0; extra == 'dev'
50
+ Requires-Dist: pytest>=7; extra == 'dev'
51
+ Requires-Dist: twine; extra == 'dev'
52
+ Provides-Extra: large
53
+ Requires-Dist: duckdb>=1.0; extra == 'large'
54
+ Description-Content-Type: text/markdown
55
+
56
+ # data-sampler
57
+
58
+ Creates representative samples from data files, using stratified sampling to
59
+ preserve the statistical variety of your data — with optional per-column
60
+ anonymization and a colorful terminal UI.
61
+
62
+ Everything ships as a single Python package: launch the TUI with one
63
+ function call or command, or use the sampling/anonymization functions
64
+ directly from Python.
65
+
66
+ ## Install
67
+
68
+ ```sh
69
+ pip install data-sampler # once released on PyPI
70
+ # from a clone, today:
71
+ pip install -e ".[dev]"
72
+ ```
73
+
74
+ Requires Python 3.10+.
75
+
76
+ ## Terminal UI
77
+
78
+ ```sh
79
+ data-sampler # no arguments → opens the TUI
80
+ data-sampler-tui # explicit TUI entry point
81
+ python -m data_sampler # same as data-sampler
82
+ ```
83
+
84
+ Or from Python:
85
+
86
+ ```python
87
+ import data_sampler
88
+
89
+ data_sampler.run_tui() # file picker first
90
+ data_sampler.run_tui("data.csv") # pre-load a file
91
+ ```
92
+
93
+ The TUI is a panel-based dashboard (think btop / lazydocker):
94
+
95
+ 1. **File screen** — type a path or pick a file from the directory browser;
96
+ Excel files take an optional sheet name.
97
+ 2. **Columns screen** — every column with its type, missing %, unique count,
98
+ distribution sparkline, and summary (modelled after the Data Wrangler
99
+ VS Code extension). Select a column to see full stats and distribution
100
+ bars, choose an anonymizer for it, and toggle whether it should be
101
+ skipped when preserving statistical variety (stratification). Set the
102
+ sample size, output folder, optional seed, and run.
103
+ 3. **Report screen** — the stratification comparison and anonymization summary
104
+ on the left, and a **column histograms** panel on the right showing every
105
+ column's source-vs-sample distribution (numeric columns share bin edges;
106
+ others use the source's top categories) so you can see at a glance how well
107
+ the sample preserved each column. The output path is shown too.
108
+
109
+ Key bindings: `ctrl+r` run sample, `a` auto-suggest anonymizer types,
110
+ `s` toggle stratification skip,
111
+ `escape` back, `ctrl+q` quit.
112
+
113
+ ## CLI (headless)
114
+
115
+ ```sh
116
+ data-sampler <source> <count> [options]
117
+ ```
118
+
119
+ | Option | Description |
120
+ | --- | --- |
121
+ | `--sheet NAME` | Sheet name for Excel files (default: first sheet) |
122
+ | `--outdir DIR` | Output folder (default: same folder as source file) |
123
+ | `--random` | Pure random sampling instead of stratified |
124
+ | `--seed N` | Seed for reproducible sampling and anonymization |
125
+ | `--skip COL[,COL]` | Exclude column(s) from stratification (repeatable) |
126
+ | `--anon COL=KIND[:k=v,...]` | Anonymize a column (repeatable) |
127
+ | `-i`, `--interactive` | Guided workflow: choose an anonymizer type per column from a menu |
128
+ | `--suggest` | Auto-assign a suggested anonymizer type to each column from its stats |
129
+ | `--engine {auto,pandas,duckdb}` | Sampling engine (default `auto`: DuckDB for Parquet/large inputs, pandas otherwise) |
130
+ | `--threads N` | DuckDB engine: number of threads (default: all cores) |
131
+ | `--memory-limit SIZE` | DuckDB engine: memory limit before spilling to disk (e.g. `8GB`) |
132
+ | `--tui` | Open the TUI (optionally preloading `source`) |
133
+
134
+ Examples:
135
+
136
+ ```sh
137
+ data-sampler data.csv 500
138
+ data-sampler report.xlsx 200 --sheet "Sheet2" --outdir C:\samples
139
+ data-sampler data.csv 100 --skip region,notes --seed 7 \
140
+ --anon "name=names" \
141
+ --anon "cust_id=sequential_id:start=1000,interval=7" \
142
+ --anon "salary=numeric_jitter:pct=0.1" \
143
+ --anon "email=hex:length=12"
144
+
145
+ # large / out-of-core: sample a Parquet file in parallel with DuckDB
146
+ data-sampler huge.parquet 10000 --engine duckdb --threads 8 --memory-limit 8GB --suggest
147
+ ```
148
+
149
+ ## Python API
150
+
151
+ ```python
152
+ import data_sampler as ds
153
+
154
+ df = ds.load_file("data.xlsx", sheet="Sheet2")
155
+
156
+ # Data Wrangler-style column stats
157
+ for s in ds.compute_stats(df):
158
+ print(s.name, s.kind, s.unique, s.summary())
159
+
160
+ # representative sample; 'notes' never used for stratification
161
+ result = ds.sample(df, 500, exclude_columns=["notes"], random_state=7)
162
+ print(ds.format_stratification_report(df, result))
163
+
164
+ # per-column source-vs-sample histograms (or ds.column_histogram_data for the raw numbers)
165
+ print(ds.format_column_histograms(df, result.data))
166
+
167
+ # anonymize chosen columns of the sample (consistent mapping, NaN preserved)
168
+ anon = ds.anonymize(
169
+ result.data,
170
+ {
171
+ "name": "names",
172
+ "cust_id": ("sequential_id", {"start": 1000, "interval": 7}),
173
+ "salary": ("numeric_jitter", {"pct": 0.1}),
174
+ "email": {"kind": "hex", "length": 12},
175
+ },
176
+ seed=7,
177
+ )
178
+
179
+ ds.save_output(anon, "data.xlsx", tag="sample_500_anon")
180
+ ```
181
+
182
+ ## Try it: bundled example
183
+
184
+ The repo ships a 1,000-row dummy dataset, [examples/employees.csv](examples/employees.csv),
185
+ built to be stratifiable: `department`, `region`, and `employment_type` have
186
+ skewed categorical distributions, `performance_rating` is low-cardinality
187
+ numeric, and `employee_id`/`full_name`/`email`/`salary` are there to
188
+ anonymize.
189
+
190
+ ```text
191
+ employee_id,full_name,email,department,region,employment_type,performance_rating,salary
192
+ E1001,Emily Lee,emily.lee001@example.com,Sales,North,Full-time,4,62000
193
+ E1002,Joshua Clark,joshua.clark002@example.com,Finance,South,Full-time,4,50000
194
+ E1003,Donald Martin,donald.martin003@example.com,Operations,East,Contract,3,68500
195
+ ```
196
+
197
+ ### In the TUI
198
+
199
+ ```sh
200
+ data-sampler examples/employees.csv --tui
201
+ ```
202
+
203
+ The columns screen opens with the stats table. Try: press **`a`** to
204
+ auto-suggest an anonymizer type for every column, then adjust — select
205
+ `full_name` and set its anonymizer to **names**; select `employee_id` and
206
+ choose **sequential id** (start 1000); select `salary` and choose **numeric
207
+ jitter**; select `performance_rating` and flip **skip when stratifying** to
208
+ keep it out of the variety-preservation logic. Set rows to `100`, seed to
209
+ `42`, and press `ctrl+r` — the report screen shows how closely the sample
210
+ tracks the original distributions.
211
+
212
+ ### With the Python functions
213
+
214
+ ```python
215
+ import data_sampler as ds
216
+
217
+ df = ds.load_file("examples/employees.csv")
218
+
219
+ result = ds.sample(df, 100, random_state=42) # stratifies automatically
220
+ print(ds.format_stratification_report(df, result))
221
+
222
+ anon = ds.anonymize(
223
+ result.data,
224
+ {
225
+ "full_name": "names",
226
+ "employee_id": ("sequential_id", {"start": 1000}),
227
+ "salary": "numeric_jitter",
228
+ "email": {"kind": "hex", "length": 10},
229
+ },
230
+ seed=42,
231
+ )
232
+ ds.save_output(anon, "examples/employees.csv", tag="sample_100_anon")
233
+ ```
234
+
235
+ ### From the CLI
236
+
237
+ ```sh
238
+ data-sampler examples/employees.csv 100 --seed 42 \
239
+ --anon "full_name=names" \
240
+ --anon "employee_id=sequential_id:start=1000" \
241
+ --anon "salary=numeric_jitter" \
242
+ --anon "email=hex:length=10"
243
+ ```
244
+
245
+ The run stratifies on `employment_type`, `region`, and `department`, and the
246
+ report shows original vs. sample side by side (excerpt):
247
+
248
+ ```text
249
+ Column: 'employment_type' (3 categories)
250
+ Value Original Sample
251
+ ─────────────────────────────────────────────────────────────────────
252
+ Contract ██░░░░░░░░░░░░░ 10.1% █░░░░░░░░░░░░░░ 9.0%
253
+ Full-time ███████████████ 68.9% ███████████████ 68.0%
254
+ Part-time ████░░░░░░░░░░░ 21.0% █████░░░░░░░░░░ 23.0%
255
+ ─────────────────────────────────────────────────────────────────────
256
+ Totals 1000 100
257
+ ```
258
+
259
+ The anonymized sample keeps the structure but none of the identities —
260
+ repeated values still repeat, salaries stay within ±20 % of the originals:
261
+
262
+ ```text
263
+ employee_id,full_name,email,department,region,employment_type,performance_rating,salary
264
+ 1000,Ravi Andersen,6a78c49ea2,Engineering,South,Full-time,1,62264
265
+ 1001,Thomas Gomez,0e32684b27,Engineering,North,Part-time,3,102743
266
+ 1002,Fatima Singh,b95e909348,Operations,North,Full-time,3,46793
267
+ ```
268
+
269
+ ### Notebook and launcher scripts
270
+
271
+ - [examples/using_data_sampler.ipynb](examples/using_data_sampler.ipynb) —
272
+ the full package walkthrough as an executed Jupyter notebook (load →
273
+ stats → sample → anonymize → save, with outputs included).
274
+ - [scripts/run-tui.sh](scripts/run-tui.sh) — opens the TUI on Linux (any
275
+ distro) and macOS; falls back from the `data-sampler` command to
276
+ `python3 -m data_sampler` and prints install instructions if neither is
277
+ available.
278
+ - [scripts/run-tui.bat](scripts/run-tui.bat) — the same for Windows
279
+ (double-clickable).
280
+
281
+ Both scripts pass arguments through, e.g. `./scripts/run-tui.sh data.csv`.
282
+
283
+ ## Anonymizers
284
+
285
+ Every anonymizer maps each unique original value to exactly one replacement,
286
+ so repeated values stay repeated and the column's distribution — the
287
+ statistical variety this tool exists to preserve — survives anonymization.
288
+ Missing values are left as missing. All anonymizers accept a seed (via
289
+ `anonymize(..., seed=N)` or `--seed`) for reproducible output.
290
+
291
+ | Kind | Replaces values with | Options (defaults) |
292
+ | --- | --- | --- |
293
+ | `names` | Realistic names from a bundled library of first, middle, and last names | `style`: `first_last`, `first_middle_last`, `last_first`, `first`, `last` |
294
+ | `sequential_id` | `start`, `start+interval`, ... in order of first appearance | `start` (1), `interval` (1), `prefix` (`""`), `width` (0, zero-pads) |
295
+ | `numeric_jitter` | A random number within ±`pct` of the original | `pct` (0.2 = ±20 %), `round_to` (decimal places) |
296
+ | `datetime_jitter` | A date/time shifted by a random offset within ±`max_delta` | `max_delta` (`"7D"`; any `pandas.Timedelta` string), `unit` (`"s"`; jitter resolution) |
297
+ | `random_string` | Random character sequences, unique per value | `length` (8), `charset` (`alphanumeric`, `letters`, `digits`, `hex`), `prefix` (`""`) |
298
+ | `hex` | Shorthand for `random_string` with `charset="hex"` | `length` (8) |
299
+
300
+ ## Anonymization workflow
301
+
302
+ Rather than spell out every column by hand, you can drive a guided workflow —
303
+ give it your columns and pick a *type* for each. The three ways to do it share
304
+ one engine (`AnonymizationPlan`) and the same auto-suggestion (`suggest_type`),
305
+ which infers a type from each column's stats (datetime → datetime jitter,
306
+ name/email columns → names/hex, id-ish high-uniqueness columns → sequential id,
307
+ numbers → numeric jitter, free text → random string; categorical/boolean columns
308
+ are left alone so the categories you stratify on survive).
309
+
310
+ - **Choose from options (interactive):** `data-sampler data.csv 100 --interactive`
311
+ walks each column and offers a numbered menu, defaulting to the suggested
312
+ type — press Enter to accept or type a number to override.
313
+ - **Pre-specify through a function (Python):**
314
+
315
+ ```python
316
+ import data_sampler as ds
317
+
318
+ df = ds.load_file("data.csv")
319
+ plan = ds.AnonymizationPlan.suggest(df) # auto-infer every column…
320
+ plan.assign("salary", "numeric_jitter", pct=0.1) # …then override as needed
321
+ plan.clear("region")
322
+ anon = plan.apply(df, seed=7) # runs ds.anonymize under the hood
323
+ ```
324
+
325
+ - **Click in the TUI:** open the columns screen, select a column, and pick its
326
+ anonymizer — or press **`a`** to auto-suggest a type for every column at once,
327
+ then tweak. The `anonymizer` column shows each choice at a glance.
328
+
329
+ `--suggest` applies the suggestions non-interactively (columns you also set with
330
+ `--anon` keep your explicit choice).
331
+
332
+ ## How sampling works
333
+
334
+ **Stratified (default):** columns suitable for stratification are detected
335
+ automatically — categorical or low-cardinality columns with 2–100 unique
336
+ values; long text and ID-like numeric columns are avoided, as are any
337
+ columns you mark as skipped. Rows are grouped by the joint combination of
338
+ all selected columns and sampled proportionally per group, so the sample
339
+ mirrors the original joint distribution. Missing values count as their own
340
+ category. A side-by-side distribution report is produced for every run.
341
+
342
+ **Pure random (`--random`):** rows are drawn uniformly at random.
343
+
344
+ If no suitable stratification columns exist, the tool falls back to pure
345
+ random sampling automatically.
346
+
347
+ ## Large data: the out-of-core DuckDB engine
348
+
349
+ The default pandas path loads the whole file into memory. For inputs that are
350
+ too big for that (toward billions of rows, especially Parquet), install the
351
+ optional engine and let **DuckDB** do the work — multi-threaded, and able to
352
+ spill to disk, so only the resulting sample is ever materialized:
353
+
354
+ ```sh
355
+ pip install "data-sampler[large]"
356
+ ```
357
+
358
+ ```python
359
+ from data_sampler.engine import DuckDBEngine, should_use_engine
360
+
361
+ # reads Parquet/CSV natively; only the sample (count rows) comes back as a DataFrame
362
+ with DuckDBEngine(threads=8, memory_limit="8GB") as engine:
363
+ result = engine.sample("huge.parquet", 10_000, seed=42) # stratifies automatically
364
+ result.data.to_parquet("sample.parquet", index=False)
365
+
366
+ should_use_engine("huge.parquet") # True — Parquet always benefits from pushdown
367
+ ```
368
+
369
+ - **Parallel + out-of-core:** all cores by default; a `memory_limit` makes it
370
+ spill instead of running out of memory.
371
+ - **Native readers:** Parquet is read with projection pushdown (only the scanned
372
+ columns); CSV/TSV/JSON and pandas DataFrames work too. Excel still goes through
373
+ the pandas path.
374
+ - **Streaming sampling:** reservoir sampling for the random case (exact count,
375
+ single pass) and two-pass proportional sampling for the stratified case.
376
+ - **Reproducible:** pass `seed=` (seeded stratified runs go single-threaded so
377
+ the result is deterministic; the distribution is preserved either way).
378
+
379
+ `large_materialization_warning(n_rows, n_cols)` returns a heads-up when a dataset
380
+ is big enough that loading it fully into pandas may exhaust memory — Parquet in
381
+ particular expands well beyond its compressed on-disk size.
382
+
383
+ Measured on a 20M-row Parquet file (5 columns, 12-core machine), sampling
384
+ 10,000 rows:
385
+
386
+ | threads | stratified sample | reservoir sample | `stats()` |
387
+ | ---: | ---: | ---: | ---: |
388
+ | 1 | 14.5 s | 0.39 s | 9.5 s |
389
+ | 4 | 5.1 s | 0.16 s | 2.8 s |
390
+ | 8 | 3.9 s | 0.13 s | 2.1 s |
391
+ | 12 | 4.0 s | 0.10 s | 1.7 s |
392
+
393
+ The pandas path on the same file: 5.6 s total while materializing a ~0.9 GB
394
+ frame in RAM — the engine's reservoir sampling is ~50× faster and never
395
+ materializes the source at all.
396
+
397
+ ## Supported formats
398
+
399
+ | Format | Extensions |
400
+ | --- | --- |
401
+ | CSV | `.csv` |
402
+ | TSV | `.tsv` |
403
+ | JSON | `.json` |
404
+ | Excel | `.xlsx`, `.xls` |
405
+ | Parquet | `.parquet` |
406
+
407
+ Output keeps the source format and is named
408
+ `{stem}_sample_{count}{ext}` — with an `_anon` suffix when anonymization ran
409
+ (e.g. `data_sample_500_anon.csv`).
410
+
411
+ ## Development
412
+
413
+ ```sh
414
+ pip install -e ".[dev]"
415
+ pytest # full suite, incl. headless TUI tests
416
+ python -m build # build the wheel + sdist into dist/
417
+ ```
418
+
419
+ Logging is controlled by `DATA_SAMPLER_LOG` (`quiet`/`info`/`verbose`) and
420
+ `DATA_SAMPLER_LOG_FILE`. See `ROADMAP.md` for planned work and
421
+ `TROUBLESHOOTING.md` for known failure modes.
422
+
423
+ PyPI releases are manual and happen only after extensive testing.
424
+
425
+ ---
426
+
427
+ Built with the assistance of [Claude Code](https://claude.ai/code) (Anthropic).
@@ -0,0 +1,19 @@
1
+ data_sampler/__init__.py,sha256=XHkEEHybLbHtTd33mgT_0ZJMSSuG8xfYvtT2_qpWBM0,2445
2
+ data_sampler/__main__.py,sha256=ajDpX5YMj1AQQxkJrosiHaGyVaYXPef2J55ogOA7xJg,103
3
+ data_sampler/_logging.py,sha256=aOdWjKE48RYQRZK7fahSYPdSZS1VxOz2OVQQYwgqroc,1838
4
+ data_sampler/_names.py,sha256=mc8sEQHRfkpBdh7NtyfiHRguox_qi7CEPzcoq-zRRDg,4913
5
+ data_sampler/anonymize.py,sha256=ueaHZaEolNFqd3ORhDVT53xkOkmNAY5DkqCGT7X2om0,18964
6
+ data_sampler/cli.py,sha256=0RO_e1FYQsPNglTX85ufVsGMD7CPoVUF5E7PyoLCYBc,13672
7
+ data_sampler/engine.py,sha256=P04za_5QJz3CmpqDw8ZK84ARHdltxcAw8uYBz-jcSDw,26962
8
+ data_sampler/io.py,sha256=dfTZhEUgXC2PBVXTndT28Qm2Nd0z33mlxb3C05DVlWg,2477
9
+ data_sampler/report.py,sha256=6uZYyD05nXd1-UYWqBMY92EzvVXh7Ei6DuEzxQwz-QU,12857
10
+ data_sampler/sampling.py,sha256=UoR630lkRVVJ_3-1R97yW8sH1cwcx77yj-9xkHpxfpQ,5985
11
+ data_sampler/stats.py,sha256=SJKx3E41ylJnS4iP7gB1qZuf5K3HFsttkUJwXgkUCkk,6186
12
+ data_sampler/workflow.py,sha256=OMyDlbBIiZBoEhuBRpdKiaATBEhOfHJJ1dyTAVGtHbE,12778
13
+ data_sampler/tui/__init__.py,sha256=L4SgMXL1CvJUGR9YA5FElb-_hzHXE3dSGeZE5tXbWno,391
14
+ data_sampler/tui/app.py,sha256=UpfkQ2CbcY7u9Xc1e_AwR97bep5lQCgiB2wt7Xm0jOA,37060
15
+ data_sampler-3.2.1.dist-info/METADATA,sha256=Bgs0bQPxo1CqBoewk0qG9pKSSvMns0_Arn3XMJBdvDk,17971
16
+ data_sampler-3.2.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
17
+ data_sampler-3.2.1.dist-info/entry_points.txt,sha256=PD6Tr46XjB-Dd35Xc-lYdRdaa2nRJOTQ5G4lU9P3hF0,99
18
+ data_sampler-3.2.1.dist-info/licenses/LICENSE,sha256=u-mGJadAxmib3xMszc-6e3GO2Hma0HxWegIj-O5TJ8o,1077
19
+ data_sampler-3.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ data-sampler = data_sampler.cli:main
3
+ data-sampler-tui = data_sampler.tui:run_tui
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 programanalytics-isb
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.