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 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,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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ zerodep.py
4
+ zerodep.egg-info/PKG-INFO
5
+ zerodep.egg-info/SOURCES.txt
6
+ zerodep.egg-info/dependency_links.txt
7
+ zerodep.egg-info/entry_points.txt
8
+ zerodep.egg-info/requires.txt
9
+ zerodep.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ zerodep = zerodep:main
@@ -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
@@ -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()