neev 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.
neev-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,595 @@
1
+ Metadata-Version: 2.3
2
+ Name: neev
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency Python CLI for sharing local directories over HTTP with authentication, file browsing, and ZIP downloads
5
+ Keywords: http,file-server,cli,sharing
6
+ Author: Akshay Prabhu
7
+ Author-email: Akshay Prabhu <akshay.prabhu.mulki@gmail.com>
8
+ License: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Environment :: Console
16
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
17
+ Requires-Python: >=3.11
18
+ Project-URL: Homepage, https://github.com/prabhuakshay/neev
19
+ Project-URL: Repository, https://github.com/prabhuakshay/neev
20
+ Project-URL: Issues, https://github.com/prabhuakshay/neev/issues
21
+ Project-URL: Changelog, https://github.com/prabhuakshay/neev/releases
22
+ Description-Content-Type: text/markdown
23
+
24
+ <p align="center">
25
+ <img src="assets/neev-icon.jpg" alt="neev" width="120">
26
+ </p>
27
+
28
+ <h1 align="center">neev</h1>
29
+
30
+ <p align="center">
31
+ <em>Zero-dependency Python CLI for sharing local directories over HTTP — with auth, file browsing, ZIP downloads, and uploads.</em>
32
+ </p>
33
+
34
+ <p align="center">
35
+ <a href="https://pypi.org/project/neev/"><img src="https://img.shields.io/pypi/v/neev.svg?color=blue" alt="PyPI version"></a>
36
+ <a href="https://pypi.org/project/neev/"><img src="https://img.shields.io/pypi/pyversions/neev.svg" alt="Python versions"></a>
37
+ <a href="https://pypi.org/project/neev/"><img src="https://img.shields.io/pypi/dm/neev.svg" alt="PyPI downloads"></a>
38
+ <a href="https://github.com/prabhuakshay/neev/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/neev.svg" alt="License"></a>
39
+ <a href="https://github.com/prabhuakshay/neev/actions/workflows/ci.yml"><img src="https://github.com/prabhuakshay/neev/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
40
+ <a href="https://github.com/prabhuakshay/neev/actions/workflows/publish.yml"><img src="https://github.com/prabhuakshay/neev/actions/workflows/publish.yml/badge.svg" alt="Publish"></a>
41
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero dependencies">
42
+ <img src="https://img.shields.io/badge/coverage-%E2%89%A595%25-brightgreen" alt="Coverage">
43
+ <a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
44
+ <a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv"></a>
45
+ </p>
46
+
47
+ ---
48
+
49
+ ## Table of Contents
50
+
51
+ - [Why neev?](#why-neev)
52
+ - [Install](#install)
53
+ - [Quick Start](#quick-start)
54
+ - [CLI Reference](#cli-reference)
55
+ - [Configuration File (`neev.toml`)](#configuration-file-neevtoml)
56
+ - [Environment Variables](#environment-variables)
57
+ - [Configuration Precedence](#configuration-precedence)
58
+ - [Features](#features)
59
+ - [HTTP API](#http-api)
60
+ - [Security Model](#security-model)
61
+ - [Recipes](#recipes)
62
+ - [Architecture](#architecture)
63
+ - [Development](#development)
64
+ - [Troubleshooting](#troubleshooting)
65
+ - [FAQ](#faq)
66
+ - [Contributing](#contributing)
67
+ - [License](#license)
68
+
69
+ ---
70
+
71
+ ## Why neev?
72
+
73
+ `python -m http.server` is great, but it has no authentication, no way to download a folder, no uploads, and exposes dotfiles by default. neev is the drop-in replacement with the things you actually need:
74
+
75
+ - **HTTP Basic Auth** — constant-time credential comparison
76
+ - **ZIP folder downloads** — streamed on the fly, no temp files
77
+ - **File uploads** — opt-in, with size limits and path-traversal protection
78
+ - **Clean browser UI** — dark/light theme, Markdown preview, syntax-aware file icons
79
+ - **HTTP Range support** — resumable downloads, video/audio seeking
80
+ - **Concurrent requests** — threaded server, not single-request-at-a-time
81
+ - **Secure defaults** — localhost-only, no writes, no hidden files
82
+ - **Zero dependencies** — pure Python stdlib, Python 3.11+
83
+
84
+ Built for developers sharing build artifacts, teams exchanging files on a LAN, and anyone who wants `http.server` but grown up.
85
+
86
+ ---
87
+
88
+ ## Install
89
+
90
+ ### From PyPI (recommended)
91
+
92
+ ```bash
93
+ # with uv (fastest)
94
+ uv tool install neev
95
+
96
+ # or pipx
97
+ pipx install neev
98
+
99
+ # or plain pip
100
+ pip install neev
101
+ ```
102
+
103
+ ### Run without installing
104
+
105
+ ```bash
106
+ uvx neev --dir ./public
107
+ ```
108
+
109
+ ### From source
110
+
111
+ ```bash
112
+ git clone https://github.com/prabhuakshay/neev
113
+ cd neev
114
+ uv sync
115
+ uv run neev --help
116
+ ```
117
+
118
+ **Requires Python 3.11 or newer.** Works on Linux, macOS, and Windows.
119
+
120
+ ---
121
+
122
+ ## Quick Start
123
+
124
+ ```bash
125
+ # Serve the current directory on http://127.0.0.1:8000
126
+ neev
127
+
128
+ # Serve a specific directory
129
+ neev ./public
130
+
131
+ # With auth, on all interfaces, custom port
132
+ neev ./share --host 0.0.0.0 --port 8080 --auth alice:s3cret
133
+
134
+ # Full-featured: auth + uploads + zip downloads
135
+ neev ./share --auth alice:s3cret --enable-upload --enable-zip-download
136
+ ```
137
+
138
+ Open the URL printed on startup. You'll see a file browser. If auth is enabled, your browser will prompt for credentials.
139
+
140
+ ---
141
+
142
+ ## CLI Reference
143
+
144
+ ```
145
+ neev [DIRECTORY] [OPTIONS]
146
+ ```
147
+
148
+ ### Positional arguments
149
+
150
+ | Argument | Default | Description |
151
+ |----------|---------|-------------|
152
+ | `directory` | `.` | Directory to serve. Must exist. Resolved to its real path (symlinks followed). |
153
+
154
+ ### Options
155
+
156
+ | Flag | Default | Description |
157
+ |------|---------|-------------|
158
+ | `--host HOST` | `127.0.0.1` | Address to bind. Use `0.0.0.0` to expose on LAN. |
159
+ | `--port PORT`, `-p PORT` | `8000` | TCP port (1–65535). |
160
+ | `--auth USER:PASS` | _(none)_ | Enable HTTP Basic Auth. Equivalent to `NEEV_AUTH` env var. |
161
+ | `--show-hidden` / `--no-show-hidden` | off | Show dotfiles and `neev.toml` in listings. |
162
+ | `--enable-zip-download` / `--no-enable-zip-download` | off | Allow folders to be downloaded as streamed ZIP. |
163
+ | `--max-zip-size MB` | `100` | Maximum ZIP archive size in MB. Rejected if exceeded. |
164
+ | `--enable-upload` / `--no-enable-upload` | off | Allow multipart file uploads from the browser. |
165
+ | `--read-only` / `--no-read-only` | off | Force-disable uploads (overrides `--enable-upload`). |
166
+ | `--banner TEXT` | _(none)_ | Message displayed at the top of directory listings. |
167
+ | `-h`, `--help` | — | Show help and exit. |
168
+
169
+ All boolean flags use `argparse.BooleanOptionalAction`, so `--no-<flag>` works too — useful for overriding `neev.toml` from the CLI.
170
+
171
+ ### Examples
172
+
173
+ ```bash
174
+ # Read-only share even if TOML enables uploads
175
+ neev ./build --read-only
176
+
177
+ # Serve on LAN with auth, show dotfiles, custom banner
178
+ neev ./code --host 0.0.0.0 --auth me:pw --show-hidden --banner "Internal only"
179
+
180
+ # Raise ZIP size cap to 500 MB
181
+ neev ./data --enable-zip-download --max-zip-size 500
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Configuration File (`neev.toml`)
187
+
188
+ neev looks for a `neev.toml` file **in the served directory** and merges it with CLI args. CLI flags always win.
189
+
190
+ ### Example
191
+
192
+ ```toml
193
+ # ./neev.toml
194
+ host = "0.0.0.0"
195
+ port = 9000
196
+ show-hidden = false
197
+ enable-zip-download = true
198
+ max-zip-size = 250
199
+ enable-upload = false
200
+ read-only = false
201
+ banner = "Build artifacts — ask #devops for access"
202
+ ```
203
+
204
+ ### Recognized keys
205
+
206
+ | TOML key | Type | Notes |
207
+ |----------|------|-------|
208
+ | `host` | string | Same as `--host`. |
209
+ | `port` | integer | Same as `--port`. |
210
+ | `auth` | string | `"user:pass"`. Same as `--auth`. |
211
+ | `show-hidden` | bool | Same as `--show-hidden`. |
212
+ | `enable-zip-download` | bool | Same as `--enable-zip-download`. |
213
+ | `max-zip-size` | integer | MB. Same as `--max-zip-size`. |
214
+ | `enable-upload` | bool | Same as `--enable-upload`. |
215
+ | `read-only` | bool | Same as `--read-only`. |
216
+ | `banner` | string | Same as `--banner`. |
217
+
218
+ **Denied keys:** `directory` is never read from TOML (the served directory is always set by CLI, to avoid surprise path changes).
219
+
220
+ **Unknown keys** are logged at WARN level and ignored. **Malformed TOML** is logged and the file is skipped — neev keeps running with CLI defaults.
221
+
222
+ The `neev.toml` file itself is hidden from listings unless `--show-hidden` is set.
223
+
224
+ ---
225
+
226
+ ## Environment Variables
227
+
228
+ | Variable | Purpose |
229
+ |----------|---------|
230
+ | `NEEV_AUTH` | Credentials as `user:pass`. Alternative to `--auth`. |
231
+
232
+ `--auth` beats `NEEV_AUTH` when both are set.
233
+
234
+ ---
235
+
236
+ ## Configuration Precedence
237
+
238
+ From highest to lowest:
239
+
240
+ 1. **CLI flags** (explicit `--foo bar`)
241
+ 2. **`NEEV_AUTH`** env var (auth only)
242
+ 3. **`neev.toml`** in the served directory
243
+ 4. **Built-in defaults** (see CLI reference table)
244
+
245
+ ---
246
+
247
+ ## Features
248
+
249
+ ### File browser
250
+
251
+ - Directory listing with sortable columns (name, size, modified).
252
+ - Per-file icons by extension/type.
253
+ - Breadcrumb navigation.
254
+ - In-browser preview for text files, Markdown (rendered), images, and PDFs.
255
+ - Correct MIME types and charset detection per file.
256
+
257
+ ### ZIP folder downloads
258
+
259
+ - Triggered by a "Download as ZIP" button on any folder (if `--enable-zip-download`).
260
+ - **Streamed** — no temp files, constant memory usage.
261
+ - Capped at `--max-zip-size` MB; oversized archives return `413 Payload Too Large`.
262
+ - Hidden files excluded unless `--show-hidden`.
263
+
264
+ ### Uploads
265
+
266
+ - Opt-in via `--enable-upload`. Disabled under `--read-only`.
267
+ - Multipart upload form on each directory page.
268
+ - Filename sanitization — path traversal, null bytes, and absolute paths rejected.
269
+ - Writes land in the directory currently being viewed.
270
+ - Authentication (if configured) is required.
271
+
272
+ ### HTTP Range requests
273
+
274
+ Partial content (`206`) supported for all file downloads — resumable downloads, video/audio seeking, `curl -C -`.
275
+
276
+ ### Concurrency
277
+
278
+ Threaded `HTTPServer` — multiple clients can browse, download, and upload simultaneously.
279
+
280
+ ### Authentication
281
+
282
+ - HTTP Basic Auth.
283
+ - Credentials compared with `hmac.compare_digest` (constant-time).
284
+ - Failed auth returns `401` with a `WWW-Authenticate: Basic` challenge.
285
+
286
+ ---
287
+
288
+ ## HTTP API
289
+
290
+ neev is primarily browser-driven, but every interaction is a plain HTTP request — scriptable with `curl`, `wget`, `httpie`, etc.
291
+
292
+ ### Listing / serving files
293
+
294
+ ```http
295
+ GET /path/to/dir/ → HTML directory listing
296
+ GET /path/to/file.txt → file contents (with Range support)
297
+ ```
298
+
299
+ Trailing slash → directory; no slash → file. Requests to a path without a trailing slash that resolves to a directory are redirected with `301` to the canonical slashed URL.
300
+
301
+ ### ZIP download
302
+
303
+ ```http
304
+ GET /path/to/dir/?zip=1
305
+ ```
306
+
307
+ Returns `application/zip` stream. Filename is `<dirname>.zip`. Disabled unless `--enable-zip-download`.
308
+
309
+ ### File preview (rendered)
310
+
311
+ ```http
312
+ GET /path/to/file.md?preview=1
313
+ ```
314
+
315
+ Returns HTML with Markdown rendered server-side. Works for text and Markdown files.
316
+
317
+ ### Upload
318
+
319
+ ```http
320
+ POST /path/to/dir/
321
+ Content-Type: multipart/form-data; boundary=...
322
+
323
+ <file field named "file">
324
+ ```
325
+
326
+ Returns `303 See Other` redirect to the directory on success, `4xx` on validation failure. Disabled unless `--enable-upload` and not `--read-only`.
327
+
328
+ Example with curl:
329
+
330
+ ```bash
331
+ curl -u "$USER_PASS" -F "file=@./report.pdf" http://localhost:8000/uploads/
332
+ ```
333
+
334
+ ### Authentication
335
+
336
+ Every endpoint honors Basic Auth when enabled:
337
+
338
+ ```bash
339
+ curl -u "$USER_PASS" http://localhost:8000/
340
+ # or (equivalent) — build the header yourself
341
+ curl -H "Authorization: Basic $(printf '%s' "$USER_PASS" | base64)" http://localhost:8000/
342
+ ```
343
+
344
+ ### Status codes
345
+
346
+ | Code | When |
347
+ |------|------|
348
+ | `200 OK` | Successful GET of file/listing. |
349
+ | `206 Partial Content` | Successful Range request. |
350
+ | `301 Moved Permanently` | Directory URL missing trailing slash. |
351
+ | `303 See Other` | Successful upload. |
352
+ | `400 Bad Request` | Malformed request / upload. |
353
+ | `401 Unauthorized` | Auth required or invalid credentials. |
354
+ | `403 Forbidden` | Path traversal, access denied, or writes on read-only. |
355
+ | `404 Not Found` | Path doesn't exist or hidden without `--show-hidden`. |
356
+ | `405 Method Not Allowed` | e.g. `POST` when uploads disabled. |
357
+ | `413 Payload Too Large` | ZIP exceeds `--max-zip-size`. |
358
+ | `416 Range Not Satisfiable` | Invalid Range header. |
359
+ | `500 Internal Server Error` | Unexpected server fault. |
360
+
361
+ ---
362
+
363
+ ## Security Model
364
+
365
+ **What neev protects against:**
366
+
367
+ - **Path traversal** — every request resolves `os.path.realpath()` and verifies the result is a descendant of the served directory. `../`, symlink escapes, and absolute paths are rejected with `403`.
368
+ - **Credential leaks in timing** — `hmac.compare_digest` for both username and password.
369
+ - **Upload filename injection** — rejects traversal, null bytes, and absolute paths.
370
+ - **Accidental exposure** — defaults are localhost-only, no writes, no dotfiles, no ZIPs.
371
+ - **Oversized ZIP abuse** — streaming ZIPs are bounded by `--max-zip-size`.
372
+
373
+ **What neev does NOT do:**
374
+
375
+ - No HTTPS/TLS. Run behind a reverse proxy (nginx, Caddy) for internet-facing deployments.
376
+ - No rate limiting. Use a reverse proxy or firewall.
377
+ - No user management — single Basic Auth pair, one shared credential.
378
+ - No virus scanning on uploads.
379
+ - No audit log beyond stdout request logging.
380
+
381
+ **If exposing to a network or internet:**
382
+
383
+ 1. Always set `--auth`.
384
+ 2. Use a strong password (treat as a bearer token).
385
+ 3. Put it behind a TLS-terminating proxy.
386
+ 4. Prefer `--read-only` unless uploads are required.
387
+ 5. Keep `--show-hidden` off.
388
+
389
+ ---
390
+
391
+ ## Recipes
392
+
393
+ ### Share a build artifact with a coworker
394
+
395
+ ```bash
396
+ neev ./dist --host 0.0.0.0 --auth reviewer:pleasecheck
397
+ # share: http://<your-ip>:8000/
398
+ ```
399
+
400
+ ### Drop box — let someone upload files to you
401
+
402
+ ```bash
403
+ mkdir -p ~/inbox
404
+ neev ~/inbox --host 0.0.0.0 --auth sender:pw --enable-upload
405
+ ```
406
+
407
+ ### Static preview site with Markdown rendering
408
+
409
+ ```bash
410
+ neev ./docs --enable-zip-download --banner "Project docs"
411
+ ```
412
+
413
+ ### Behind Caddy with TLS
414
+
415
+ ```caddy
416
+ share.example.com {
417
+ reverse_proxy localhost:8000
418
+ }
419
+ ```
420
+
421
+ ```bash
422
+ neev /srv/share --auth alice:s3cret --enable-zip-download
423
+ ```
424
+
425
+ ### Systemd service
426
+
427
+ ```ini
428
+ # /etc/systemd/system/neev.service
429
+ [Unit]
430
+ Description=neev file server
431
+ After=network.target
432
+
433
+ [Service]
434
+ ExecStart=/usr/local/bin/neev /srv/share --host 127.0.0.1 --port 8000 --enable-zip-download
435
+ Environment=NEEV_AUTH=alice:s3cret
436
+ Restart=on-failure
437
+ User=neev
438
+
439
+ [Install]
440
+ WantedBy=multi-user.target
441
+ ```
442
+
443
+ ### Docker one-liner
444
+
445
+ ```bash
446
+ docker run --rm -p 8000:8000 -v "$PWD:/srv:ro" python:3.13-slim \
447
+ sh -c "pip install neev && neev /srv --host 0.0.0.0 --read-only"
448
+ ```
449
+
450
+ ---
451
+
452
+ ## Architecture
453
+
454
+ ```
455
+ src/neev/
456
+ ├── cli.py # argparse, validation, entry point
457
+ ├── config.py # frozen Config dataclass
458
+ ├── toml_config.py # neev.toml loader + CLI merge
459
+ ├── server.py # HTTPServer wiring (threaded)
460
+ ├── server_core.py # main request dispatcher
461
+ ├── server_auth.py # HTTP Basic Auth
462
+ ├── server_preview.py # file/Markdown preview
463
+ ├── server_upload.py # multipart upload handler
464
+ ├── server_zip.py # streaming ZIP response
465
+ ├── server_assets.py # bundled static assets (CSS, JS)
466
+ ├── server_utils.py # Range, redirects, MIME helpers
467
+ ├── fs.py # path-safe filesystem ops
468
+ ├── auth.py # constant-time credential check
469
+ ├── upload.py # upload validation
470
+ ├── upload_multipart.py # multipart body parsing
471
+ ├── zip.py # streaming zip generator
472
+ ├── url_utils.py # URL encoding/decoding
473
+ ├── log.py # logging helpers + ANSI styling
474
+ ├── html*.py # HTML templating (no jinja — pure stdlib)
475
+ └── static/ # compiled CSS bundled with package
476
+ ```
477
+
478
+ **Design pillars:**
479
+
480
+ 1. **Stdlib only.** Every feature is built on `http.server`, `zipfile`, `argparse`, `html`, `tomllib`, `base64`, `hmac`.
481
+ 2. **Defense-in-depth on paths.** Every filesystem op goes through `fs.py` which resolves and verifies containment.
482
+ 3. **Frozen config.** `Config` is built once at startup; request handlers never mutate configuration state.
483
+ 4. **No dynamic imports, no plugin system.** Small, auditable surface area.
484
+ 5. **Files under 300 lines.** Enforced by convention — keeps modules focused.
485
+
486
+ ---
487
+
488
+ ## Development
489
+
490
+ Uses [uv](https://docs.astral.sh/uv/) for everything.
491
+
492
+ ### Setup
493
+
494
+ ```bash
495
+ git clone https://github.com/prabhuakshay/neev
496
+ cd neev
497
+ uv sync
498
+ ```
499
+
500
+ ### Run
501
+
502
+ ```bash
503
+ uv run neev --help
504
+ uv run neev ./tests/fixtures --enable-zip-download
505
+ ```
506
+
507
+ ### Test
508
+
509
+ ```bash
510
+ uv run pytest # full suite with coverage (≥95% enforced)
511
+ uv run pytest -k upload # filter
512
+ uv run pytest --no-cov -x # fast iteration
513
+ ```
514
+
515
+ ### Lint & type check
516
+
517
+ ```bash
518
+ uv run ruff check .
519
+ uv run ruff format .
520
+ uv run ty check
521
+ ```
522
+
523
+ ### Pre-commit hooks (prek)
524
+
525
+ ```bash
526
+ uv run prek install
527
+ uv run prek run --all-files
528
+ ```
529
+
530
+ The pre-commit config also recompiles the Tailwind CSS bundle when any `html_*.py` changes.
531
+
532
+ ### Build & publish
533
+
534
+ ```bash
535
+ uv build # produces sdist + wheel in dist/
536
+ # Publishing is automated: create a GitHub Release → PyPI workflow uploads.
537
+ ```
538
+
539
+ ---
540
+
541
+ ## Troubleshooting
542
+
543
+ | Symptom | Likely cause | Fix |
544
+ |---------|-------------|-----|
545
+ | `Address already in use` | Port taken | Pick a different `--port` or kill the prior process. |
546
+ | Browser keeps re-prompting for password | Wrong credentials or auth off on restart | Verify `--auth` / `NEEV_AUTH`, clear browser cache. |
547
+ | `403 Forbidden` on a file that exists | Path-traversal guard / symlink escape | The file isn't inside the served directory's real path. |
548
+ | ZIP download returns `413` | Folder exceeds `--max-zip-size` | Raise the cap or download individual files. |
549
+ | Uploads return `405` | `--enable-upload` not set, or `--read-only` on | Enable uploads and disable read-only. |
550
+ | Dotfiles don't appear | Hidden by default | Use `--show-hidden`. |
551
+ | `neev.toml` takes effect unexpectedly | Merged from served directory | Check startup banner; override with CLI flags. |
552
+
553
+ ---
554
+
555
+ ## FAQ
556
+
557
+ **Does neev support HTTPS?**
558
+ No. Use a reverse proxy (Caddy, nginx, Traefik) for TLS termination.
559
+
560
+ **Can I serve multiple directories?**
561
+ Not in one process. Run multiple `neev` instances on different ports.
562
+
563
+ **Is it safe to expose to the internet?**
564
+ Only behind HTTPS + auth + ideally `--read-only`. For anything mission-critical, reach for a proper server.
565
+
566
+ **Why "neev"?**
567
+ "Neev" (नींव) means *foundation* in Hindi — a simple base to build file sharing on.
568
+
569
+ **Does it work on Windows?**
570
+ Yes. Python 3.11+ required.
571
+
572
+ **What about WebDAV / S3 / FTP?**
573
+ Out of scope. neev is HTTP + browser UI only.
574
+
575
+ ---
576
+
577
+ ## Contributing
578
+
579
+ Issues and PRs welcome at [github.com/prabhuakshay/neev](https://github.com/prabhuakshay/neev).
580
+
581
+ Before submitting:
582
+
583
+ ```bash
584
+ uv run ruff check . && uv run ruff format --check .
585
+ uv run ty check
586
+ uv run pytest
587
+ ```
588
+
589
+ See [CLAUDE.md](CLAUDE.md) for the full development philosophy (stability > features, stdlib only, files < 300 lines, etc.).
590
+
591
+ ---
592
+
593
+ ## License
594
+
595
+ [MIT](LICENSE) © Akshay Prabhu