zerodep 0.2.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.
- zerodep-0.2.0/PKG-INFO +146 -0
- zerodep-0.2.0/README.md +83 -0
- zerodep-0.2.0/pyproject.toml +149 -0
- zerodep-0.2.0/setup.cfg +4 -0
- zerodep-0.2.0/zerodep.egg-info/PKG-INFO +146 -0
- zerodep-0.2.0/zerodep.egg-info/SOURCES.txt +9 -0
- zerodep-0.2.0/zerodep.egg-info/dependency_links.txt +1 -0
- zerodep-0.2.0/zerodep.egg-info/entry_points.txt +2 -0
- zerodep-0.2.0/zerodep.egg-info/requires.txt +75 -0
- zerodep-0.2.0/zerodep.egg-info/top_level.txt +1 -0
- zerodep-0.2.0/zerodep.py +554 -0
zerodep-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zerodep
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Zero-dependency, single-file Python implementations of popular libraries
|
|
5
|
+
Author: Peng Ding
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/Oaklight/zerodep
|
|
8
|
+
Project-URL: Issues, https://github.com/Oaklight/zerodep/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
14
|
+
Requires-Dist: pytest-benchmark>=4.0.0; extra == "test"
|
|
15
|
+
Provides-Extra: bench-aes
|
|
16
|
+
Requires-Dist: pycryptodome>=3.20.0; extra == "bench-aes"
|
|
17
|
+
Provides-Extra: bench-qr
|
|
18
|
+
Requires-Dist: qrcode>=7.0; extra == "bench-qr"
|
|
19
|
+
Requires-Dist: Pillow>=10.0.0; extra == "bench-qr"
|
|
20
|
+
Provides-Extra: bench-http
|
|
21
|
+
Requires-Dist: httpx>=0.27.0; extra == "bench-http"
|
|
22
|
+
Provides-Extra: bench-dotenv
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == "bench-dotenv"
|
|
24
|
+
Provides-Extra: bench-yaml
|
|
25
|
+
Requires-Dist: PyYAML>=6.0; extra == "bench-yaml"
|
|
26
|
+
Provides-Extra: bench-jsonc
|
|
27
|
+
Requires-Dist: commentjson>=0.9.0; extra == "bench-jsonc"
|
|
28
|
+
Provides-Extra: bench-structlog
|
|
29
|
+
Requires-Dist: structlog>=24.0.0; extra == "bench-structlog"
|
|
30
|
+
Provides-Extra: bench-retry
|
|
31
|
+
Requires-Dist: tenacity>=8.0.0; extra == "bench-retry"
|
|
32
|
+
Provides-Extra: bench-toon
|
|
33
|
+
Requires-Dist: toon-format>=0.1.0; extra == "bench-toon"
|
|
34
|
+
Provides-Extra: bench-tabulate
|
|
35
|
+
Requires-Dist: tabulate>=0.9.0; extra == "bench-tabulate"
|
|
36
|
+
Provides-Extra: bench-soup
|
|
37
|
+
Requires-Dist: beautifulsoup4>=4.12.0; extra == "bench-soup"
|
|
38
|
+
Provides-Extra: bench-validate
|
|
39
|
+
Requires-Dist: pydantic>=2.0.0; extra == "bench-validate"
|
|
40
|
+
Provides-Extra: bench-sse
|
|
41
|
+
Requires-Dist: httpx-sse>=0.4.0; extra == "bench-sse"
|
|
42
|
+
Requires-Dist: httpx>=0.27.0; extra == "bench-sse"
|
|
43
|
+
Provides-Extra: bench-markdown
|
|
44
|
+
Requires-Dist: mistune>=3.0.0; extra == "bench-markdown"
|
|
45
|
+
Provides-Extra: bench-diff
|
|
46
|
+
Requires-Dist: unidiff>=0.7.0; extra == "bench-diff"
|
|
47
|
+
Provides-Extra: bench-scheduler
|
|
48
|
+
Requires-Dist: APScheduler>=3.10.0; extra == "bench-scheduler"
|
|
49
|
+
Requires-Dist: schedule>=1.2.0; extra == "bench-scheduler"
|
|
50
|
+
Requires-Dist: croniter>=1.3.0; extra == "bench-scheduler"
|
|
51
|
+
Provides-Extra: bench-search
|
|
52
|
+
Requires-Dist: rank-bm25>=0.2.2; extra == "bench-search"
|
|
53
|
+
Provides-Extra: bench-frontmatter
|
|
54
|
+
Requires-Dist: python-frontmatter>=1.1.0; extra == "bench-frontmatter"
|
|
55
|
+
Provides-Extra: bench-config
|
|
56
|
+
Requires-Dist: python-decouple>=3.8; extra == "bench-config"
|
|
57
|
+
Provides-Extra: bench-cache
|
|
58
|
+
Requires-Dist: cachetools>=5.0.0; extra == "bench-cache"
|
|
59
|
+
Provides-Extra: bench-xml
|
|
60
|
+
Requires-Dist: xmltodict>=0.13.0; extra == "bench-xml"
|
|
61
|
+
Provides-Extra: dev
|
|
62
|
+
Requires-Dist: zerodep[bench-aes,bench-cache,bench-config,bench-diff,bench-dotenv,bench-frontmatter,bench-http,bench-jsonc,bench-markdown,bench-qr,bench-retry,bench-scheduler,bench-search,bench-soup,bench-sse,bench-structlog,bench-tabulate,bench-toon,bench-validate,bench-xml,bench-yaml,test]; extra == "dev"
|
|
63
|
+
|
|
64
|
+
# zerodep
|
|
65
|
+
|
|
66
|
+
Zero-dependency, single-file Python implementations of popular libraries — stdlib only, Python 3.10+.
|
|
67
|
+
|
|
68
|
+
零依赖、单文件的 Python 常用库实现 —— 仅使用标准库,支持 Python 3.10+。
|
|
69
|
+
|
|
70
|
+
## Modules
|
|
71
|
+
|
|
72
|
+
**Web & Networking**
|
|
73
|
+
|
|
74
|
+
| Module | Description | Benchmark Against |
|
|
75
|
+
|--------|-------------|-------------------|
|
|
76
|
+
| `httpclient/` | Sync + async REST client with connection pooling, proxy, and auth | `httpx` |
|
|
77
|
+
| `sse/` | Server-Sent Events client with auto-reconnect | `httpx-sse` |
|
|
78
|
+
|
|
79
|
+
**Data Formats**
|
|
80
|
+
|
|
81
|
+
| Module | Description | Benchmark Against |
|
|
82
|
+
|--------|-------------|-------------------|
|
|
83
|
+
| `yaml/` | YAML parser and serializer (common subset) | `PyYAML` |
|
|
84
|
+
| `jsonc/` | JSONC parser (JSON with comments and trailing commas) | `commentjson` |
|
|
85
|
+
| `toon/` | TOON (Token-Oriented Object Notation) encoder/decoder | `toon_format` |
|
|
86
|
+
| `frontmatter/` | Frontmatter parser and serializer (YAML/TOML/JSON file-header metadata) | `python-frontmatter` |
|
|
87
|
+
|
|
88
|
+
**Data Validation**
|
|
89
|
+
|
|
90
|
+
| Module | Description | Benchmark Against |
|
|
91
|
+
|--------|-------------|-------------------|
|
|
92
|
+
| `validate/` | Runtime TypedDict/dataclass validator with JSON Schema generation | `pydantic` |
|
|
93
|
+
|
|
94
|
+
**Text & Markup**
|
|
95
|
+
|
|
96
|
+
| Module | Description | Benchmark Against |
|
|
97
|
+
|--------|-------------|-------------------|
|
|
98
|
+
| `markdown/` | Markdown to HTML renderer (CommonMark subset + GFM tables) | `mistune` |
|
|
99
|
+
| `soup/` | HTML parser with BeautifulSoup-like API (find, select, CSS selectors) | `beautifulsoup4` |
|
|
100
|
+
| `diff/` | Unified diff parser, patch apply/reverse, three-way merge | `unidiff` |
|
|
101
|
+
|
|
102
|
+
**Search & Retrieval**
|
|
103
|
+
|
|
104
|
+
| Module | Description | Benchmark Against |
|
|
105
|
+
|--------|-------------|-------------------|
|
|
106
|
+
| `search/` | BM25/BM25+/BM25L/BM25F + TF-IDF full-text search engine | `rank-bm25` |
|
|
107
|
+
|
|
108
|
+
**Configuration**
|
|
109
|
+
|
|
110
|
+
| Module | Description | Benchmark Against |
|
|
111
|
+
|--------|-------------|-------------------|
|
|
112
|
+
| `dotenv/` | .env file parser (load_dotenv, dotenv_values) | `python-dotenv` |
|
|
113
|
+
|
|
114
|
+
**CLI & Terminal**
|
|
115
|
+
|
|
116
|
+
| Module | Description | Benchmark Against |
|
|
117
|
+
|--------|-------------|-------------------|
|
|
118
|
+
| `ansi/` | ANSI terminal styling: colors, attributes, detection, strip/visible_len | — |
|
|
119
|
+
| `tabulate/` | Table formatting with multiple output styles | `tabulate` |
|
|
120
|
+
| `prompt/` | Interactive CLI prompts (confirm, select, text) | `questionary` |
|
|
121
|
+
|
|
122
|
+
**Security & Encoding**
|
|
123
|
+
|
|
124
|
+
| Module | Description | Benchmark Against |
|
|
125
|
+
|--------|-------------|-------------------|
|
|
126
|
+
| `aes/` | AES encryption: ECB, CBC, CTR, GCM modes (pure Python + OpenSSL via ctypes) | `pycryptodome` |
|
|
127
|
+
| `qr/` | QR Code generation with terminal rendering | `qrcode` |
|
|
128
|
+
|
|
129
|
+
**Infrastructure & Tools**
|
|
130
|
+
|
|
131
|
+
| Module | Description | Benchmark Against |
|
|
132
|
+
|--------|-------------|-------------------|
|
|
133
|
+
| `retry/` | Retry decorator with configurable backoff strategies | `tenacity` |
|
|
134
|
+
| `scheduler/` | In-process task scheduler with cron, interval, one-shot triggers | `APScheduler` |
|
|
135
|
+
| `structlog/` | Structured logging with pretty console output | `structlog` |
|
|
136
|
+
| `vcs/` | Git/Hg/Jujutsu CLI wrapper (diff, status, log, blame) | — |
|
|
137
|
+
|
|
138
|
+
## Usage
|
|
139
|
+
|
|
140
|
+
Each module is a **self-contained single file** that you can copy directly into your project. No installation required.
|
|
141
|
+
|
|
142
|
+
Some modules have optional **sibling dependencies** on other zerodep modules (e.g. `structlog` can use `ansi` for color support). These are loaded via guarded imports — if the sibling module is absent, the module falls back to inline constants and remains fully functional.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
zerodep-0.2.0/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# zerodep
|
|
2
|
+
|
|
3
|
+
Zero-dependency, single-file Python implementations of popular libraries — stdlib only, Python 3.10+.
|
|
4
|
+
|
|
5
|
+
零依赖、单文件的 Python 常用库实现 —— 仅使用标准库,支持 Python 3.10+。
|
|
6
|
+
|
|
7
|
+
## Modules
|
|
8
|
+
|
|
9
|
+
**Web & Networking**
|
|
10
|
+
|
|
11
|
+
| Module | Description | Benchmark Against |
|
|
12
|
+
|--------|-------------|-------------------|
|
|
13
|
+
| `httpclient/` | Sync + async REST client with connection pooling, proxy, and auth | `httpx` |
|
|
14
|
+
| `sse/` | Server-Sent Events client with auto-reconnect | `httpx-sse` |
|
|
15
|
+
|
|
16
|
+
**Data Formats**
|
|
17
|
+
|
|
18
|
+
| Module | Description | Benchmark Against |
|
|
19
|
+
|--------|-------------|-------------------|
|
|
20
|
+
| `yaml/` | YAML parser and serializer (common subset) | `PyYAML` |
|
|
21
|
+
| `jsonc/` | JSONC parser (JSON with comments and trailing commas) | `commentjson` |
|
|
22
|
+
| `toon/` | TOON (Token-Oriented Object Notation) encoder/decoder | `toon_format` |
|
|
23
|
+
| `frontmatter/` | Frontmatter parser and serializer (YAML/TOML/JSON file-header metadata) | `python-frontmatter` |
|
|
24
|
+
|
|
25
|
+
**Data Validation**
|
|
26
|
+
|
|
27
|
+
| Module | Description | Benchmark Against |
|
|
28
|
+
|--------|-------------|-------------------|
|
|
29
|
+
| `validate/` | Runtime TypedDict/dataclass validator with JSON Schema generation | `pydantic` |
|
|
30
|
+
|
|
31
|
+
**Text & Markup**
|
|
32
|
+
|
|
33
|
+
| Module | Description | Benchmark Against |
|
|
34
|
+
|--------|-------------|-------------------|
|
|
35
|
+
| `markdown/` | Markdown to HTML renderer (CommonMark subset + GFM tables) | `mistune` |
|
|
36
|
+
| `soup/` | HTML parser with BeautifulSoup-like API (find, select, CSS selectors) | `beautifulsoup4` |
|
|
37
|
+
| `diff/` | Unified diff parser, patch apply/reverse, three-way merge | `unidiff` |
|
|
38
|
+
|
|
39
|
+
**Search & Retrieval**
|
|
40
|
+
|
|
41
|
+
| Module | Description | Benchmark Against |
|
|
42
|
+
|--------|-------------|-------------------|
|
|
43
|
+
| `search/` | BM25/BM25+/BM25L/BM25F + TF-IDF full-text search engine | `rank-bm25` |
|
|
44
|
+
|
|
45
|
+
**Configuration**
|
|
46
|
+
|
|
47
|
+
| Module | Description | Benchmark Against |
|
|
48
|
+
|--------|-------------|-------------------|
|
|
49
|
+
| `dotenv/` | .env file parser (load_dotenv, dotenv_values) | `python-dotenv` |
|
|
50
|
+
|
|
51
|
+
**CLI & Terminal**
|
|
52
|
+
|
|
53
|
+
| Module | Description | Benchmark Against |
|
|
54
|
+
|--------|-------------|-------------------|
|
|
55
|
+
| `ansi/` | ANSI terminal styling: colors, attributes, detection, strip/visible_len | — |
|
|
56
|
+
| `tabulate/` | Table formatting with multiple output styles | `tabulate` |
|
|
57
|
+
| `prompt/` | Interactive CLI prompts (confirm, select, text) | `questionary` |
|
|
58
|
+
|
|
59
|
+
**Security & Encoding**
|
|
60
|
+
|
|
61
|
+
| Module | Description | Benchmark Against |
|
|
62
|
+
|--------|-------------|-------------------|
|
|
63
|
+
| `aes/` | AES encryption: ECB, CBC, CTR, GCM modes (pure Python + OpenSSL via ctypes) | `pycryptodome` |
|
|
64
|
+
| `qr/` | QR Code generation with terminal rendering | `qrcode` |
|
|
65
|
+
|
|
66
|
+
**Infrastructure & Tools**
|
|
67
|
+
|
|
68
|
+
| Module | Description | Benchmark Against |
|
|
69
|
+
|--------|-------------|-------------------|
|
|
70
|
+
| `retry/` | Retry decorator with configurable backoff strategies | `tenacity` |
|
|
71
|
+
| `scheduler/` | In-process task scheduler with cron, interval, one-shot triggers | `APScheduler` |
|
|
72
|
+
| `structlog/` | Structured logging with pretty console output | `structlog` |
|
|
73
|
+
| `vcs/` | Git/Hg/Jujutsu CLI wrapper (diff, status, log, blame) | — |
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
Each module is a **self-contained single file** that you can copy directly into your project. No installation required.
|
|
78
|
+
|
|
79
|
+
Some modules have optional **sibling dependencies** on other zerodep modules (e.g. `structlog` can use `ansi` for color support). These are loaded via guarded imports — if the sibling module is absent, the module falls back to inline constants and remains fully functional.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools]
|
|
6
|
+
py-modules = ["zerodep"]
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "zerodep"
|
|
10
|
+
version = "0.2.0"
|
|
11
|
+
description = "Zero-dependency, single-file Python implementations of popular libraries"
|
|
12
|
+
authors = [{ name = "Peng Ding" }]
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
license = "MIT"
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
zerodep = "zerodep:main"
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Repository = "https://github.com/Oaklight/zerodep"
|
|
22
|
+
Issues = "https://github.com/Oaklight/zerodep/issues"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
test = [
|
|
26
|
+
"pytest>=7.0.0",
|
|
27
|
+
"pytest-asyncio>=0.21.0",
|
|
28
|
+
"pytest-benchmark>=4.0.0",
|
|
29
|
+
]
|
|
30
|
+
# Reference libraries for correctness and benchmark comparisons
|
|
31
|
+
bench-aes = [
|
|
32
|
+
"pycryptodome>=3.20.0",
|
|
33
|
+
]
|
|
34
|
+
bench-qr = [
|
|
35
|
+
"qrcode>=7.0",
|
|
36
|
+
"Pillow>=10.0.0",
|
|
37
|
+
]
|
|
38
|
+
bench-http = [
|
|
39
|
+
"httpx>=0.27.0",
|
|
40
|
+
]
|
|
41
|
+
bench-dotenv = [
|
|
42
|
+
"python-dotenv>=1.0.0",
|
|
43
|
+
]
|
|
44
|
+
bench-yaml = [
|
|
45
|
+
"PyYAML>=6.0",
|
|
46
|
+
]
|
|
47
|
+
bench-jsonc = [
|
|
48
|
+
"commentjson>=0.9.0",
|
|
49
|
+
]
|
|
50
|
+
bench-structlog = [
|
|
51
|
+
"structlog>=24.0.0",
|
|
52
|
+
]
|
|
53
|
+
bench-retry = [
|
|
54
|
+
"tenacity>=8.0.0",
|
|
55
|
+
]
|
|
56
|
+
bench-toon = [
|
|
57
|
+
"toon-format>=0.1.0",
|
|
58
|
+
]
|
|
59
|
+
bench-tabulate = [
|
|
60
|
+
"tabulate>=0.9.0",
|
|
61
|
+
]
|
|
62
|
+
bench-soup = [
|
|
63
|
+
"beautifulsoup4>=4.12.0",
|
|
64
|
+
]
|
|
65
|
+
bench-validate = [
|
|
66
|
+
"pydantic>=2.0.0",
|
|
67
|
+
]
|
|
68
|
+
bench-sse = [
|
|
69
|
+
"httpx-sse>=0.4.0",
|
|
70
|
+
"httpx>=0.27.0",
|
|
71
|
+
]
|
|
72
|
+
bench-markdown = [
|
|
73
|
+
"mistune>=3.0.0",
|
|
74
|
+
]
|
|
75
|
+
bench-diff = [
|
|
76
|
+
"unidiff>=0.7.0",
|
|
77
|
+
]
|
|
78
|
+
bench-scheduler = [
|
|
79
|
+
"APScheduler>=3.10.0",
|
|
80
|
+
"schedule>=1.2.0",
|
|
81
|
+
"croniter>=1.3.0",
|
|
82
|
+
]
|
|
83
|
+
bench-search = [
|
|
84
|
+
"rank-bm25>=0.2.2",
|
|
85
|
+
]
|
|
86
|
+
bench-frontmatter = [
|
|
87
|
+
"python-frontmatter>=1.1.0",
|
|
88
|
+
]
|
|
89
|
+
bench-config = [
|
|
90
|
+
"python-decouple>=3.8",
|
|
91
|
+
]
|
|
92
|
+
bench-cache = [
|
|
93
|
+
"cachetools>=5.0.0",
|
|
94
|
+
]
|
|
95
|
+
bench-xml = [
|
|
96
|
+
"xmltodict>=0.13.0",
|
|
97
|
+
]
|
|
98
|
+
dev = [
|
|
99
|
+
"zerodep[test,bench-aes,bench-qr,bench-http,bench-dotenv,bench-yaml,bench-jsonc,bench-structlog,bench-retry,bench-toon,bench-tabulate,bench-soup,bench-validate,bench-sse,bench-markdown,bench-diff,bench-scheduler,bench-search,bench-frontmatter,bench-config,bench-cache,bench-xml]",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
[tool.pytest.ini_options]
|
|
103
|
+
testpaths = ["aes", "qr", "httpclient", "dotenv", "yaml", "jsonc", "structlog", "retry", "toon", "tabulate", "soup", "prompt", "validate", "sse", "markdown", "diff", "vcs", "ansi", "scheduler", "search", "frontmatter", "config", "cache", "runner", "xml"]
|
|
104
|
+
|
|
105
|
+
[tool.ruff]
|
|
106
|
+
target-version = "py310"
|
|
107
|
+
|
|
108
|
+
[tool.ruff.lint]
|
|
109
|
+
select = ["E", "F", "I", "W"]
|
|
110
|
+
|
|
111
|
+
[tool.ruff.lint.per-file-ignores]
|
|
112
|
+
"aes/aes.py" = ["E501"]
|
|
113
|
+
"aes/aes_openssl.py" = ["E501"]
|
|
114
|
+
"qr/qr.py" = ["E501"]
|
|
115
|
+
"yaml/yaml.py" = ["E501"]
|
|
116
|
+
"validate/validate.py" = ["E501"]
|
|
117
|
+
"diff/diff.py" = ["E501"]
|
|
118
|
+
"vcs/vcs.py" = ["E501"]
|
|
119
|
+
"xml/xml.py" = ["E501"]
|
|
120
|
+
"**/test_*.py" = ["E402"]
|
|
121
|
+
|
|
122
|
+
[tool.ty.environment]
|
|
123
|
+
extra-paths = [
|
|
124
|
+
"./aes",
|
|
125
|
+
"./ansi",
|
|
126
|
+
"./diff",
|
|
127
|
+
"./dotenv",
|
|
128
|
+
"./httpclient",
|
|
129
|
+
"./jsonc",
|
|
130
|
+
"./markdown",
|
|
131
|
+
"./prompt",
|
|
132
|
+
"./qr",
|
|
133
|
+
"./retry",
|
|
134
|
+
"./soup",
|
|
135
|
+
"./sse",
|
|
136
|
+
"./structlog",
|
|
137
|
+
"./tabulate",
|
|
138
|
+
"./toon",
|
|
139
|
+
"./validate",
|
|
140
|
+
"./vcs",
|
|
141
|
+
"./yaml",
|
|
142
|
+
"./scheduler",
|
|
143
|
+
"./search",
|
|
144
|
+
"./frontmatter",
|
|
145
|
+
"./config",
|
|
146
|
+
"./cache",
|
|
147
|
+
"./runner",
|
|
148
|
+
"./xml",
|
|
149
|
+
]
|
zerodep-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zerodep
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Zero-dependency, single-file Python implementations of popular libraries
|
|
5
|
+
Author: Peng Ding
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/Oaklight/zerodep
|
|
8
|
+
Project-URL: Issues, https://github.com/Oaklight/zerodep/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
14
|
+
Requires-Dist: pytest-benchmark>=4.0.0; extra == "test"
|
|
15
|
+
Provides-Extra: bench-aes
|
|
16
|
+
Requires-Dist: pycryptodome>=3.20.0; extra == "bench-aes"
|
|
17
|
+
Provides-Extra: bench-qr
|
|
18
|
+
Requires-Dist: qrcode>=7.0; extra == "bench-qr"
|
|
19
|
+
Requires-Dist: Pillow>=10.0.0; extra == "bench-qr"
|
|
20
|
+
Provides-Extra: bench-http
|
|
21
|
+
Requires-Dist: httpx>=0.27.0; extra == "bench-http"
|
|
22
|
+
Provides-Extra: bench-dotenv
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == "bench-dotenv"
|
|
24
|
+
Provides-Extra: bench-yaml
|
|
25
|
+
Requires-Dist: PyYAML>=6.0; extra == "bench-yaml"
|
|
26
|
+
Provides-Extra: bench-jsonc
|
|
27
|
+
Requires-Dist: commentjson>=0.9.0; extra == "bench-jsonc"
|
|
28
|
+
Provides-Extra: bench-structlog
|
|
29
|
+
Requires-Dist: structlog>=24.0.0; extra == "bench-structlog"
|
|
30
|
+
Provides-Extra: bench-retry
|
|
31
|
+
Requires-Dist: tenacity>=8.0.0; extra == "bench-retry"
|
|
32
|
+
Provides-Extra: bench-toon
|
|
33
|
+
Requires-Dist: toon-format>=0.1.0; extra == "bench-toon"
|
|
34
|
+
Provides-Extra: bench-tabulate
|
|
35
|
+
Requires-Dist: tabulate>=0.9.0; extra == "bench-tabulate"
|
|
36
|
+
Provides-Extra: bench-soup
|
|
37
|
+
Requires-Dist: beautifulsoup4>=4.12.0; extra == "bench-soup"
|
|
38
|
+
Provides-Extra: bench-validate
|
|
39
|
+
Requires-Dist: pydantic>=2.0.0; extra == "bench-validate"
|
|
40
|
+
Provides-Extra: bench-sse
|
|
41
|
+
Requires-Dist: httpx-sse>=0.4.0; extra == "bench-sse"
|
|
42
|
+
Requires-Dist: httpx>=0.27.0; extra == "bench-sse"
|
|
43
|
+
Provides-Extra: bench-markdown
|
|
44
|
+
Requires-Dist: mistune>=3.0.0; extra == "bench-markdown"
|
|
45
|
+
Provides-Extra: bench-diff
|
|
46
|
+
Requires-Dist: unidiff>=0.7.0; extra == "bench-diff"
|
|
47
|
+
Provides-Extra: bench-scheduler
|
|
48
|
+
Requires-Dist: APScheduler>=3.10.0; extra == "bench-scheduler"
|
|
49
|
+
Requires-Dist: schedule>=1.2.0; extra == "bench-scheduler"
|
|
50
|
+
Requires-Dist: croniter>=1.3.0; extra == "bench-scheduler"
|
|
51
|
+
Provides-Extra: bench-search
|
|
52
|
+
Requires-Dist: rank-bm25>=0.2.2; extra == "bench-search"
|
|
53
|
+
Provides-Extra: bench-frontmatter
|
|
54
|
+
Requires-Dist: python-frontmatter>=1.1.0; extra == "bench-frontmatter"
|
|
55
|
+
Provides-Extra: bench-config
|
|
56
|
+
Requires-Dist: python-decouple>=3.8; extra == "bench-config"
|
|
57
|
+
Provides-Extra: bench-cache
|
|
58
|
+
Requires-Dist: cachetools>=5.0.0; extra == "bench-cache"
|
|
59
|
+
Provides-Extra: bench-xml
|
|
60
|
+
Requires-Dist: xmltodict>=0.13.0; extra == "bench-xml"
|
|
61
|
+
Provides-Extra: dev
|
|
62
|
+
Requires-Dist: zerodep[bench-aes,bench-cache,bench-config,bench-diff,bench-dotenv,bench-frontmatter,bench-http,bench-jsonc,bench-markdown,bench-qr,bench-retry,bench-scheduler,bench-search,bench-soup,bench-sse,bench-structlog,bench-tabulate,bench-toon,bench-validate,bench-xml,bench-yaml,test]; extra == "dev"
|
|
63
|
+
|
|
64
|
+
# zerodep
|
|
65
|
+
|
|
66
|
+
Zero-dependency, single-file Python implementations of popular libraries — stdlib only, Python 3.10+.
|
|
67
|
+
|
|
68
|
+
零依赖、单文件的 Python 常用库实现 —— 仅使用标准库,支持 Python 3.10+。
|
|
69
|
+
|
|
70
|
+
## Modules
|
|
71
|
+
|
|
72
|
+
**Web & Networking**
|
|
73
|
+
|
|
74
|
+
| Module | Description | Benchmark Against |
|
|
75
|
+
|--------|-------------|-------------------|
|
|
76
|
+
| `httpclient/` | Sync + async REST client with connection pooling, proxy, and auth | `httpx` |
|
|
77
|
+
| `sse/` | Server-Sent Events client with auto-reconnect | `httpx-sse` |
|
|
78
|
+
|
|
79
|
+
**Data Formats**
|
|
80
|
+
|
|
81
|
+
| Module | Description | Benchmark Against |
|
|
82
|
+
|--------|-------------|-------------------|
|
|
83
|
+
| `yaml/` | YAML parser and serializer (common subset) | `PyYAML` |
|
|
84
|
+
| `jsonc/` | JSONC parser (JSON with comments and trailing commas) | `commentjson` |
|
|
85
|
+
| `toon/` | TOON (Token-Oriented Object Notation) encoder/decoder | `toon_format` |
|
|
86
|
+
| `frontmatter/` | Frontmatter parser and serializer (YAML/TOML/JSON file-header metadata) | `python-frontmatter` |
|
|
87
|
+
|
|
88
|
+
**Data Validation**
|
|
89
|
+
|
|
90
|
+
| Module | Description | Benchmark Against |
|
|
91
|
+
|--------|-------------|-------------------|
|
|
92
|
+
| `validate/` | Runtime TypedDict/dataclass validator with JSON Schema generation | `pydantic` |
|
|
93
|
+
|
|
94
|
+
**Text & Markup**
|
|
95
|
+
|
|
96
|
+
| Module | Description | Benchmark Against |
|
|
97
|
+
|--------|-------------|-------------------|
|
|
98
|
+
| `markdown/` | Markdown to HTML renderer (CommonMark subset + GFM tables) | `mistune` |
|
|
99
|
+
| `soup/` | HTML parser with BeautifulSoup-like API (find, select, CSS selectors) | `beautifulsoup4` |
|
|
100
|
+
| `diff/` | Unified diff parser, patch apply/reverse, three-way merge | `unidiff` |
|
|
101
|
+
|
|
102
|
+
**Search & Retrieval**
|
|
103
|
+
|
|
104
|
+
| Module | Description | Benchmark Against |
|
|
105
|
+
|--------|-------------|-------------------|
|
|
106
|
+
| `search/` | BM25/BM25+/BM25L/BM25F + TF-IDF full-text search engine | `rank-bm25` |
|
|
107
|
+
|
|
108
|
+
**Configuration**
|
|
109
|
+
|
|
110
|
+
| Module | Description | Benchmark Against |
|
|
111
|
+
|--------|-------------|-------------------|
|
|
112
|
+
| `dotenv/` | .env file parser (load_dotenv, dotenv_values) | `python-dotenv` |
|
|
113
|
+
|
|
114
|
+
**CLI & Terminal**
|
|
115
|
+
|
|
116
|
+
| Module | Description | Benchmark Against |
|
|
117
|
+
|--------|-------------|-------------------|
|
|
118
|
+
| `ansi/` | ANSI terminal styling: colors, attributes, detection, strip/visible_len | — |
|
|
119
|
+
| `tabulate/` | Table formatting with multiple output styles | `tabulate` |
|
|
120
|
+
| `prompt/` | Interactive CLI prompts (confirm, select, text) | `questionary` |
|
|
121
|
+
|
|
122
|
+
**Security & Encoding**
|
|
123
|
+
|
|
124
|
+
| Module | Description | Benchmark Against |
|
|
125
|
+
|--------|-------------|-------------------|
|
|
126
|
+
| `aes/` | AES encryption: ECB, CBC, CTR, GCM modes (pure Python + OpenSSL via ctypes) | `pycryptodome` |
|
|
127
|
+
| `qr/` | QR Code generation with terminal rendering | `qrcode` |
|
|
128
|
+
|
|
129
|
+
**Infrastructure & Tools**
|
|
130
|
+
|
|
131
|
+
| Module | Description | Benchmark Against |
|
|
132
|
+
|--------|-------------|-------------------|
|
|
133
|
+
| `retry/` | Retry decorator with configurable backoff strategies | `tenacity` |
|
|
134
|
+
| `scheduler/` | In-process task scheduler with cron, interval, one-shot triggers | `APScheduler` |
|
|
135
|
+
| `structlog/` | Structured logging with pretty console output | `structlog` |
|
|
136
|
+
| `vcs/` | Git/Hg/Jujutsu CLI wrapper (diff, status, log, blame) | — |
|
|
137
|
+
|
|
138
|
+
## Usage
|
|
139
|
+
|
|
140
|
+
Each module is a **self-contained single file** that you can copy directly into your project. No installation required.
|
|
141
|
+
|
|
142
|
+
Some modules have optional **sibling dependencies** on other zerodep modules (e.g. `structlog` can use `ansi` for color support). These are loaded via guarded imports — if the sibling module is absent, the module falls back to inline constants and remains fully functional.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
|
|
2
|
+
[bench-aes]
|
|
3
|
+
pycryptodome>=3.20.0
|
|
4
|
+
|
|
5
|
+
[bench-cache]
|
|
6
|
+
cachetools>=5.0.0
|
|
7
|
+
|
|
8
|
+
[bench-config]
|
|
9
|
+
python-decouple>=3.8
|
|
10
|
+
|
|
11
|
+
[bench-diff]
|
|
12
|
+
unidiff>=0.7.0
|
|
13
|
+
|
|
14
|
+
[bench-dotenv]
|
|
15
|
+
python-dotenv>=1.0.0
|
|
16
|
+
|
|
17
|
+
[bench-frontmatter]
|
|
18
|
+
python-frontmatter>=1.1.0
|
|
19
|
+
|
|
20
|
+
[bench-http]
|
|
21
|
+
httpx>=0.27.0
|
|
22
|
+
|
|
23
|
+
[bench-jsonc]
|
|
24
|
+
commentjson>=0.9.0
|
|
25
|
+
|
|
26
|
+
[bench-markdown]
|
|
27
|
+
mistune>=3.0.0
|
|
28
|
+
|
|
29
|
+
[bench-qr]
|
|
30
|
+
qrcode>=7.0
|
|
31
|
+
Pillow>=10.0.0
|
|
32
|
+
|
|
33
|
+
[bench-retry]
|
|
34
|
+
tenacity>=8.0.0
|
|
35
|
+
|
|
36
|
+
[bench-scheduler]
|
|
37
|
+
APScheduler>=3.10.0
|
|
38
|
+
schedule>=1.2.0
|
|
39
|
+
croniter>=1.3.0
|
|
40
|
+
|
|
41
|
+
[bench-search]
|
|
42
|
+
rank-bm25>=0.2.2
|
|
43
|
+
|
|
44
|
+
[bench-soup]
|
|
45
|
+
beautifulsoup4>=4.12.0
|
|
46
|
+
|
|
47
|
+
[bench-sse]
|
|
48
|
+
httpx-sse>=0.4.0
|
|
49
|
+
httpx>=0.27.0
|
|
50
|
+
|
|
51
|
+
[bench-structlog]
|
|
52
|
+
structlog>=24.0.0
|
|
53
|
+
|
|
54
|
+
[bench-tabulate]
|
|
55
|
+
tabulate>=0.9.0
|
|
56
|
+
|
|
57
|
+
[bench-toon]
|
|
58
|
+
toon-format>=0.1.0
|
|
59
|
+
|
|
60
|
+
[bench-validate]
|
|
61
|
+
pydantic>=2.0.0
|
|
62
|
+
|
|
63
|
+
[bench-xml]
|
|
64
|
+
xmltodict>=0.13.0
|
|
65
|
+
|
|
66
|
+
[bench-yaml]
|
|
67
|
+
PyYAML>=6.0
|
|
68
|
+
|
|
69
|
+
[dev]
|
|
70
|
+
zerodep[bench-aes,bench-cache,bench-config,bench-diff,bench-dotenv,bench-frontmatter,bench-http,bench-jsonc,bench-markdown,bench-qr,bench-retry,bench-scheduler,bench-search,bench-soup,bench-sse,bench-structlog,bench-tabulate,bench-toon,bench-validate,bench-xml,bench-yaml,test]
|
|
71
|
+
|
|
72
|
+
[test]
|
|
73
|
+
pytest>=7.0.0
|
|
74
|
+
pytest-asyncio>=0.21.0
|
|
75
|
+
pytest-benchmark>=4.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zerodep
|
zerodep-0.2.0/zerodep.py
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"""zerodep CLI — copy zero-dependency Python modules into your project.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
python zerodep.py list # list available modules
|
|
6
|
+
python zerodep.py info <module> # module details + deps
|
|
7
|
+
python zerodep.py add <module> [...] # copy modules to cwd
|
|
8
|
+
python zerodep.py manifest # regenerate manifest.json
|
|
9
|
+
|
|
10
|
+
Requires Python 3.10+, zero external dependencies.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import ast
|
|
19
|
+
import json
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
import textwrap
|
|
23
|
+
import urllib.error
|
|
24
|
+
import urllib.request
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
# ── Constants ──
|
|
29
|
+
|
|
30
|
+
REPO_OWNER = "Oaklight"
|
|
31
|
+
REPO_NAME = "zerodep"
|
|
32
|
+
BRANCH = "master"
|
|
33
|
+
|
|
34
|
+
_SOURCES = [
|
|
35
|
+
f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/{{path}}",
|
|
36
|
+
f"https://cdn.jsdelivr.net/gh/{REPO_OWNER}/{REPO_NAME}@{BRANCH}/{{path}}",
|
|
37
|
+
f"https://fastly.jsdelivr.net/gh/{REPO_OWNER}/{REPO_NAME}@{BRANCH}/{{path}}",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
CACHE_DIR = Path.home() / ".zerodep" / "cache"
|
|
41
|
+
MANIFEST_PATH = "manifest.json"
|
|
42
|
+
|
|
43
|
+
# Modules where the directory name != file name (multi-file or renamed)
|
|
44
|
+
# Discovered automatically during manifest generation.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── Network ──
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _fetch_url(url: str, timeout: int = 15) -> bytes:
|
|
51
|
+
"""Fetch raw bytes from a URL using urllib."""
|
|
52
|
+
req = urllib.request.Request(url, headers={"User-Agent": "zerodep-cli"})
|
|
53
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
54
|
+
return resp.read()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _fetch_with_fallback(path: str, *, offline: bool = False) -> bytes:
|
|
58
|
+
"""Try sources in order, cache result, fall back to cache."""
|
|
59
|
+
cache_file = CACHE_DIR / path
|
|
60
|
+
if offline:
|
|
61
|
+
if cache_file.exists():
|
|
62
|
+
return cache_file.read_bytes()
|
|
63
|
+
_die(f"offline mode: {path!r} not found in cache ({cache_file})")
|
|
64
|
+
|
|
65
|
+
for src in _SOURCES:
|
|
66
|
+
url = src.format(path=path)
|
|
67
|
+
try:
|
|
68
|
+
data = _fetch_url(url)
|
|
69
|
+
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
cache_file.write_bytes(data)
|
|
71
|
+
return data
|
|
72
|
+
except (urllib.error.URLError, OSError, TimeoutError):
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# All sources failed — try cache
|
|
76
|
+
if cache_file.exists():
|
|
77
|
+
_warn(f"network unavailable, using cached {path}")
|
|
78
|
+
return cache_file.read_bytes()
|
|
79
|
+
|
|
80
|
+
_die(
|
|
81
|
+
f"could not fetch {path!r} from any source and no cache exists.\n"
|
|
82
|
+
" Check your network or use --offline with a populated cache."
|
|
83
|
+
)
|
|
84
|
+
return b"" # unreachable, for type checker
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _load_manifest(*, offline: bool = False, local: bool = False) -> dict:
|
|
88
|
+
"""Load manifest.json from network/cache or local file."""
|
|
89
|
+
if local:
|
|
90
|
+
# Use local manifest.json in repo root
|
|
91
|
+
local_path = Path(__file__).resolve().parent / MANIFEST_PATH
|
|
92
|
+
if not local_path.exists():
|
|
93
|
+
_die(
|
|
94
|
+
f"local {MANIFEST_PATH} not found. "
|
|
95
|
+
"Run 'zerodep manifest' to generate it."
|
|
96
|
+
)
|
|
97
|
+
return json.loads(local_path.read_text())
|
|
98
|
+
|
|
99
|
+
data = _fetch_with_fallback(MANIFEST_PATH, offline=offline)
|
|
100
|
+
return json.loads(data)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ── Dependency Resolution ──
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _resolve_deps(
|
|
107
|
+
names: list[str],
|
|
108
|
+
manifest: dict,
|
|
109
|
+
*,
|
|
110
|
+
no_deps: bool = False,
|
|
111
|
+
) -> list[str]:
|
|
112
|
+
"""Topological resolve: return ordered list of modules to copy."""
|
|
113
|
+
modules = manifest["modules"]
|
|
114
|
+
resolved: list[str] = []
|
|
115
|
+
seen: set[str] = set()
|
|
116
|
+
|
|
117
|
+
def _visit(name: str) -> None:
|
|
118
|
+
if name in seen:
|
|
119
|
+
return
|
|
120
|
+
if name not in modules:
|
|
121
|
+
_die(
|
|
122
|
+
f"unknown module: {name!r}. "
|
|
123
|
+
"Run 'zerodep list' to see available modules."
|
|
124
|
+
)
|
|
125
|
+
seen.add(name)
|
|
126
|
+
if not no_deps:
|
|
127
|
+
for dep in modules[name].get("deps", []):
|
|
128
|
+
_visit(dep)
|
|
129
|
+
resolved.append(name)
|
|
130
|
+
|
|
131
|
+
for n in names:
|
|
132
|
+
_visit(n)
|
|
133
|
+
return resolved
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ── Manifest Generation ──
|
|
137
|
+
|
|
138
|
+
# Directories/files to skip when scanning for modules
|
|
139
|
+
_SKIP_DIRS = {
|
|
140
|
+
"docs_en",
|
|
141
|
+
"docs_zh",
|
|
142
|
+
"plans",
|
|
143
|
+
".git",
|
|
144
|
+
".github",
|
|
145
|
+
"__pycache__",
|
|
146
|
+
".pytest_cache",
|
|
147
|
+
".ruff_cache",
|
|
148
|
+
"site",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _scan_modules(repo_root: Path) -> dict:
|
|
153
|
+
"""Scan repo recursively for module directories and extract metadata.
|
|
154
|
+
|
|
155
|
+
A directory is considered a module if it contains non-test ``.py`` files.
|
|
156
|
+
The module name is the leaf directory name (e.g. ``network/httpclient/``
|
|
157
|
+
registers as ``httpclient``). Intermediate grouping directories that
|
|
158
|
+
contain no ``.py`` files are traversed but not registered.
|
|
159
|
+
"""
|
|
160
|
+
modules: dict[str, dict] = {}
|
|
161
|
+
|
|
162
|
+
def _walk(directory: Path) -> None:
|
|
163
|
+
for entry in sorted(directory.iterdir()):
|
|
164
|
+
if (
|
|
165
|
+
not entry.is_dir()
|
|
166
|
+
or entry.name.startswith(".")
|
|
167
|
+
or entry.name in _SKIP_DIRS
|
|
168
|
+
):
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Find non-test .py files in this directory
|
|
172
|
+
py_files = sorted(
|
|
173
|
+
f
|
|
174
|
+
for f in entry.glob("*.py")
|
|
175
|
+
if not f.name.startswith("test_") and f.name != "conftest.py"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if py_files:
|
|
179
|
+
mod_name = entry.name
|
|
180
|
+
if mod_name in modules:
|
|
181
|
+
prev_dir = str(Path(modules[mod_name]["files"][0]).parent)
|
|
182
|
+
cur_dir = str(entry.relative_to(repo_root))
|
|
183
|
+
_warn(
|
|
184
|
+
f"duplicate module name {mod_name!r}: "
|
|
185
|
+
f"found in {prev_dir} and {cur_dir}, keeping first"
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
# Primary module file: prefer dir_name.py, else first file
|
|
189
|
+
primary = None
|
|
190
|
+
for f in py_files:
|
|
191
|
+
if f.stem == entry.name:
|
|
192
|
+
primary = f
|
|
193
|
+
break
|
|
194
|
+
if primary is None:
|
|
195
|
+
primary = py_files[0]
|
|
196
|
+
|
|
197
|
+
source = primary.read_text(encoding="utf-8")
|
|
198
|
+
meta = _extract_frontmatter(source)
|
|
199
|
+
version = meta.get("version", "0.0.0")
|
|
200
|
+
deps = meta.get("deps", [])
|
|
201
|
+
description = _extract_docstring_first_line(source) or ""
|
|
202
|
+
files = [str(f.relative_to(repo_root)) for f in py_files]
|
|
203
|
+
|
|
204
|
+
modules[mod_name] = {
|
|
205
|
+
"description": description,
|
|
206
|
+
"files": files,
|
|
207
|
+
"version": version,
|
|
208
|
+
"deps": deps,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Recurse into subdirectories
|
|
212
|
+
_walk(entry)
|
|
213
|
+
|
|
214
|
+
_walk(repo_root)
|
|
215
|
+
return modules
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _extract_frontmatter(source: str) -> dict[str, str | list]:
|
|
219
|
+
"""Extract metadata from ``# /// zerodep`` frontmatter block.
|
|
220
|
+
|
|
221
|
+
Parses a PEP 723-style comment block::
|
|
222
|
+
|
|
223
|
+
# /// zerodep
|
|
224
|
+
# version = "0.2.0"
|
|
225
|
+
# deps = ["httpclient"]
|
|
226
|
+
# ///
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Dict with parsed key-value pairs (values via ``ast.literal_eval``).
|
|
230
|
+
"""
|
|
231
|
+
result: dict[str, str | list] = {}
|
|
232
|
+
in_block = False
|
|
233
|
+
for line in source.splitlines():
|
|
234
|
+
stripped = line.strip()
|
|
235
|
+
if stripped == "# /// zerodep":
|
|
236
|
+
in_block = True
|
|
237
|
+
continue
|
|
238
|
+
if stripped == "# ///" and in_block:
|
|
239
|
+
break
|
|
240
|
+
if in_block and stripped.startswith("# "):
|
|
241
|
+
content = stripped[2:]
|
|
242
|
+
if "=" in content:
|
|
243
|
+
key, _, val = content.partition("=")
|
|
244
|
+
key = key.strip()
|
|
245
|
+
val = val.strip()
|
|
246
|
+
try:
|
|
247
|
+
result[key] = ast.literal_eval(val)
|
|
248
|
+
except (ValueError, SyntaxError):
|
|
249
|
+
result[key] = val
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _extract_docstring_first_line(source: str) -> str | None:
|
|
254
|
+
"""Extract the first line of the module docstring."""
|
|
255
|
+
try:
|
|
256
|
+
tree = ast.parse(source)
|
|
257
|
+
except SyntaxError:
|
|
258
|
+
return None
|
|
259
|
+
if (
|
|
260
|
+
tree.body
|
|
261
|
+
and isinstance(tree.body[0], ast.Expr)
|
|
262
|
+
and isinstance(tree.body[0].value, ast.Constant)
|
|
263
|
+
and isinstance(tree.body[0].value.value, str)
|
|
264
|
+
):
|
|
265
|
+
first_line = tree.body[0].value.value.strip().split("\n")[0]
|
|
266
|
+
# Remove trailing period/dash fragments
|
|
267
|
+
return first_line.rstrip(".")
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _generate_manifest(repo_root: Path) -> dict:
|
|
272
|
+
"""Generate the full manifest dict."""
|
|
273
|
+
modules = _scan_modules(repo_root)
|
|
274
|
+
return {
|
|
275
|
+
"version": "1",
|
|
276
|
+
"generated": datetime.now(timezone.utc).isoformat(),
|
|
277
|
+
"modules": modules,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ── Output Helpers ──
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _die(msg: str) -> None:
|
|
285
|
+
print(f"error: {msg}", file=sys.stderr)
|
|
286
|
+
sys.exit(1)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _warn(msg: str) -> None:
|
|
290
|
+
print(f"warning: {msg}", file=sys.stderr)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _ok(msg: str) -> None:
|
|
294
|
+
print(msg)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ── Commands ──
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def cmd_list(args: argparse.Namespace) -> None:
|
|
301
|
+
"""List available modules."""
|
|
302
|
+
manifest = _load_manifest(offline=args.offline, local=args.local)
|
|
303
|
+
modules = manifest["modules"]
|
|
304
|
+
|
|
305
|
+
if not modules:
|
|
306
|
+
_ok("No modules found.")
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
# Column widths
|
|
310
|
+
name_w = max(len(n) for n in modules)
|
|
311
|
+
ver_w = max(len(m.get("version", "")) for m in modules.values())
|
|
312
|
+
|
|
313
|
+
header = f" {'Module':<{name_w}} {'Version':<{ver_w}} Description"
|
|
314
|
+
_ok(header)
|
|
315
|
+
_ok(f" {'-' * name_w} {'-' * ver_w} {'-' * 40}")
|
|
316
|
+
|
|
317
|
+
for name in sorted(modules):
|
|
318
|
+
mod = modules[name]
|
|
319
|
+
ver = mod.get("version", "?")
|
|
320
|
+
desc = mod.get("description", "")
|
|
321
|
+
# Truncate description
|
|
322
|
+
max_desc = shutil.get_terminal_size((80, 24)).columns - name_w - ver_w - 8
|
|
323
|
+
if max_desc > 10 and len(desc) > max_desc:
|
|
324
|
+
desc = desc[: max_desc - 3] + "..."
|
|
325
|
+
_ok(f" {name:<{name_w}} {ver:<{ver_w}} {desc}")
|
|
326
|
+
|
|
327
|
+
_ok(f"\n {len(modules)} modules available")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def cmd_info(args: argparse.Namespace) -> None:
|
|
331
|
+
"""Show module details."""
|
|
332
|
+
manifest = _load_manifest(offline=args.offline, local=args.local)
|
|
333
|
+
modules = manifest["modules"]
|
|
334
|
+
name = args.module
|
|
335
|
+
|
|
336
|
+
if name not in modules:
|
|
337
|
+
_die(f"unknown module: {name!r}. Run 'zerodep list' to see available modules.")
|
|
338
|
+
|
|
339
|
+
mod = modules[name]
|
|
340
|
+
_ok(f"Module: {name}")
|
|
341
|
+
_ok(f"Version: {mod.get('version', '?')}")
|
|
342
|
+
_ok(f"Description: {mod.get('description', '(none)')}")
|
|
343
|
+
_ok(f"Files: {', '.join(mod.get('files', []))}")
|
|
344
|
+
|
|
345
|
+
deps = mod.get("deps", [])
|
|
346
|
+
if deps:
|
|
347
|
+
_ok(f"Dependencies: {', '.join(deps)}")
|
|
348
|
+
# Show transitive deps
|
|
349
|
+
all_deps = _resolve_deps([name], manifest)
|
|
350
|
+
all_deps.remove(name)
|
|
351
|
+
if all_deps:
|
|
352
|
+
_ok(f" (transitive: {', '.join(all_deps)})")
|
|
353
|
+
else:
|
|
354
|
+
_ok("Dependencies: none")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def cmd_add(args: argparse.Namespace) -> None:
|
|
358
|
+
"""Copy module files to target directory."""
|
|
359
|
+
manifest = _load_manifest(offline=args.offline, local=args.local)
|
|
360
|
+
|
|
361
|
+
# Resolve dependencies
|
|
362
|
+
to_copy = _resolve_deps(args.modules, manifest, no_deps=args.no_deps)
|
|
363
|
+
|
|
364
|
+
# Determine target directory
|
|
365
|
+
target = Path(args.dir).resolve()
|
|
366
|
+
|
|
367
|
+
# Build file list
|
|
368
|
+
file_plan: list[tuple[str, Path]] = [] # (remote_path, local_dest)
|
|
369
|
+
modules_data = manifest["modules"]
|
|
370
|
+
|
|
371
|
+
for mod_name in to_copy:
|
|
372
|
+
mod = modules_data[mod_name]
|
|
373
|
+
for remote_path in mod.get("files", []):
|
|
374
|
+
filename = Path(remote_path).name
|
|
375
|
+
if args.nested:
|
|
376
|
+
dest = target / mod_name / filename
|
|
377
|
+
else:
|
|
378
|
+
dest = target / filename
|
|
379
|
+
file_plan.append((remote_path, dest))
|
|
380
|
+
|
|
381
|
+
if not file_plan:
|
|
382
|
+
_ok("Nothing to copy.")
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
# Show plan
|
|
386
|
+
if not args.yes:
|
|
387
|
+
_ok("Will copy:")
|
|
388
|
+
for remote, dest in file_plan:
|
|
389
|
+
mod_name = remote.split("/")[0]
|
|
390
|
+
label = mod_name
|
|
391
|
+
if mod_name not in args.modules:
|
|
392
|
+
label += " (dependency)"
|
|
393
|
+
if dest.is_relative_to(Path.cwd()):
|
|
394
|
+
rel_dest = dest.relative_to(Path.cwd())
|
|
395
|
+
else:
|
|
396
|
+
rel_dest = dest
|
|
397
|
+
_ok(f" {Path(remote).name:<25s} -> {rel_dest} [{label}]")
|
|
398
|
+
_ok(f"Target: {target}")
|
|
399
|
+
|
|
400
|
+
# Check for existing files
|
|
401
|
+
existing = [dest for _, dest in file_plan if dest.exists()]
|
|
402
|
+
if existing:
|
|
403
|
+
_warn(f"{len(existing)} file(s) will be overwritten")
|
|
404
|
+
|
|
405
|
+
if not args.force:
|
|
406
|
+
answer = input("Continue? [Y/n] ").strip().lower()
|
|
407
|
+
if answer and answer != "y":
|
|
408
|
+
_ok("Aborted.")
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
# Fetch and copy
|
|
412
|
+
repo_root = Path(__file__).resolve().parent
|
|
413
|
+
copied = 0
|
|
414
|
+
for remote_path, dest in file_plan:
|
|
415
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
416
|
+
# Try local first (if running from repo)
|
|
417
|
+
local_file = repo_root / remote_path
|
|
418
|
+
if local_file.exists():
|
|
419
|
+
data = local_file.read_bytes()
|
|
420
|
+
else:
|
|
421
|
+
data = _fetch_with_fallback(remote_path, offline=args.offline)
|
|
422
|
+
dest.write_bytes(data)
|
|
423
|
+
copied += 1
|
|
424
|
+
|
|
425
|
+
_ok(f"Copied {copied} file(s) to {target}")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def cmd_update(args: argparse.Namespace) -> None:
|
|
429
|
+
"""Update existing module files (alias for add --force --yes)."""
|
|
430
|
+
args.force = True
|
|
431
|
+
args.yes = True
|
|
432
|
+
cmd_add(args)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def cmd_manifest(args: argparse.Namespace) -> None:
|
|
436
|
+
"""Regenerate manifest.json from local module source files."""
|
|
437
|
+
repo_root = Path(__file__).resolve().parent
|
|
438
|
+
manifest = _generate_manifest(repo_root)
|
|
439
|
+
|
|
440
|
+
out_path = repo_root / MANIFEST_PATH
|
|
441
|
+
out_path.write_text(
|
|
442
|
+
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
|
|
443
|
+
encoding="utf-8",
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
n = len(manifest["modules"])
|
|
447
|
+
_ok(f"Generated {MANIFEST_PATH} with {n} modules")
|
|
448
|
+
|
|
449
|
+
# Show deps summary
|
|
450
|
+
deps_mods = {
|
|
451
|
+
name: mod["deps"]
|
|
452
|
+
for name, mod in manifest["modules"].items()
|
|
453
|
+
if mod.get("deps")
|
|
454
|
+
}
|
|
455
|
+
if deps_mods:
|
|
456
|
+
_ok("Modules with dependencies:")
|
|
457
|
+
for name, deps in sorted(deps_mods.items()):
|
|
458
|
+
_ok(f" {name} -> {', '.join(deps)}")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def cmd_version(args: argparse.Namespace) -> None:
|
|
462
|
+
"""Print version."""
|
|
463
|
+
_ok(f"zerodep {__version__}")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ── CLI Entry Point ──
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def main(argv: list[str] | None = None) -> None:
|
|
470
|
+
"""CLI dispatcher."""
|
|
471
|
+
parser = argparse.ArgumentParser(
|
|
472
|
+
prog="zerodep",
|
|
473
|
+
description="Copy zero-dependency Python modules into your project.",
|
|
474
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
475
|
+
epilog=textwrap.dedent("""\
|
|
476
|
+
examples:
|
|
477
|
+
zerodep list List all available modules
|
|
478
|
+
zerodep info sse Show module details and deps
|
|
479
|
+
zerodep add scheduler Copy scheduler.py to current dir
|
|
480
|
+
zerodep add sse retry -d lib/ Copy sse + httpclient + retry to lib/
|
|
481
|
+
zerodep add sse --nested Copy into sse/ and httpclient/ subdirs
|
|
482
|
+
zerodep add sse --no-deps Copy only sse.py, skip httpclient
|
|
483
|
+
zerodep manifest Regenerate manifest.json
|
|
484
|
+
"""),
|
|
485
|
+
)
|
|
486
|
+
parser.add_argument(
|
|
487
|
+
"--offline",
|
|
488
|
+
action="store_true",
|
|
489
|
+
help="use only cached files, no network",
|
|
490
|
+
)
|
|
491
|
+
parser.add_argument(
|
|
492
|
+
"--local",
|
|
493
|
+
action="store_true",
|
|
494
|
+
help="use local manifest.json instead of fetching from remote",
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
sub = parser.add_subparsers(dest="command")
|
|
498
|
+
|
|
499
|
+
# list
|
|
500
|
+
sub.add_parser("list", help="list available modules")
|
|
501
|
+
|
|
502
|
+
# info
|
|
503
|
+
p_info = sub.add_parser("info", help="show module details")
|
|
504
|
+
p_info.add_argument("module", help="module name")
|
|
505
|
+
|
|
506
|
+
# add
|
|
507
|
+
p_add = sub.add_parser("add", help="copy modules to your project")
|
|
508
|
+
p_add.add_argument("modules", nargs="+", help="module names to copy")
|
|
509
|
+
p_add.add_argument("-d", "--dir", default=".", help="target directory (default: .)")
|
|
510
|
+
p_add.add_argument(
|
|
511
|
+
"--nested", action="store_true", help="use subdirectories per module"
|
|
512
|
+
)
|
|
513
|
+
p_add.add_argument("--no-deps", action="store_true", help="skip dependencies")
|
|
514
|
+
p_add.add_argument("-y", "--yes", action="store_true", help="skip confirmation")
|
|
515
|
+
p_add.add_argument(
|
|
516
|
+
"-f", "--force", action="store_true", help="overwrite without prompt"
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# update
|
|
520
|
+
p_update = sub.add_parser("update", help="update existing modules")
|
|
521
|
+
p_update.add_argument("modules", nargs="+", help="module names to update")
|
|
522
|
+
p_update.add_argument(
|
|
523
|
+
"-d", "--dir", default=".", help="target directory (default: .)"
|
|
524
|
+
)
|
|
525
|
+
p_update.add_argument(
|
|
526
|
+
"--nested", action="store_true", help="use subdirectories per module"
|
|
527
|
+
)
|
|
528
|
+
p_update.add_argument("--no-deps", action="store_true", help="skip dependencies")
|
|
529
|
+
|
|
530
|
+
# manifest
|
|
531
|
+
sub.add_parser("manifest", help="regenerate manifest.json from source")
|
|
532
|
+
|
|
533
|
+
# version
|
|
534
|
+
sub.add_parser("version", help="show version")
|
|
535
|
+
|
|
536
|
+
args = parser.parse_args(argv)
|
|
537
|
+
|
|
538
|
+
if args.command is None:
|
|
539
|
+
parser.print_help()
|
|
540
|
+
sys.exit(0)
|
|
541
|
+
|
|
542
|
+
commands = {
|
|
543
|
+
"list": cmd_list,
|
|
544
|
+
"info": cmd_info,
|
|
545
|
+
"add": cmd_add,
|
|
546
|
+
"update": cmd_update,
|
|
547
|
+
"manifest": cmd_manifest,
|
|
548
|
+
"version": cmd_version,
|
|
549
|
+
}
|
|
550
|
+
commands[args.command](args)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
if __name__ == "__main__":
|
|
554
|
+
main()
|