envgap 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class EnvVar:
9
+ key: str
10
+ value: str
11
+ path: Path
12
+ line: int
13
+ raw: str
14
+
15
+
16
+ @dataclass
17
+ class EnvFile:
18
+ path: Path
19
+ vars: list[EnvVar] = field(default_factory=list)
20
+ duplicates: dict[str, list[EnvVar]] = field(default_factory=dict)
21
+ parse_warnings: list[str] = field(default_factory=list)
22
+
23
+ @property
24
+ def values(self) -> dict[str, EnvVar]:
25
+ return {var.key: var for var in self.vars}
26
+
27
+ def contains(self, key: str) -> bool:
28
+ return key in self.values
29
+
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class CodeUsage:
9
+ key: str
10
+ path: Path
11
+ line: int
12
+ required: bool
13
+ source: str
14
+
15
+
16
+ @dataclass
17
+ class ExpectedVar:
18
+ key: str
19
+ documented_in: Path | None = None
20
+ code_usages: list[CodeUsage] = field(default_factory=list)
21
+
22
+ @property
23
+ def required(self) -> bool:
24
+ return any(usage.required for usage in self.code_usages)
25
+
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from pathlib import Path
6
+
7
+
8
+ class Severity(str, Enum):
9
+ ERROR = "error"
10
+ WARNING = "warning"
11
+ INFO = "info"
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Finding:
16
+ code: str
17
+ severity: Severity
18
+ title: str
19
+ message: str
20
+ key: str | None = None
21
+ path: Path | None = None
22
+ line: int | None = None
23
+ suggestion: str | None = None
24
+ details: list[str] = field(default_factory=list)
25
+
@@ -0,0 +1,2 @@
1
+ """Render envgap check results."""
2
+
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from envgap.checker import CheckResult
8
+ from envgap.model import Finding, Severity
9
+
10
+
11
+ def render_json(result: CheckResult) -> str:
12
+ payload = {
13
+ "root": str(result.root),
14
+ "files": {
15
+ "env": _file_info(result.env_path),
16
+ "example": _file_info(result.example_path),
17
+ },
18
+ "shell": {
19
+ "checked": result.include_shell,
20
+ "expected_keys_found": sorted(key for key in result.expected_keys if key in result.shell_env),
21
+ "expected_keys_missing": sorted(key for key in result.expected_keys if key not in result.shell_env),
22
+ },
23
+ "summary": {
24
+ "findings": len(result.findings),
25
+ "errors": sum(1 for finding in result.findings if finding.severity == Severity.ERROR),
26
+ "warnings": sum(1 for finding in result.findings if finding.severity == Severity.WARNING),
27
+ "expected_keys": len(result.expected_keys),
28
+ "code_usages": len(result.code_usages),
29
+ },
30
+ "findings": [_finding_to_dict(finding, result.root) for finding in result.findings],
31
+ "code_usages": [
32
+ {
33
+ "key": usage.key,
34
+ "path": _relative(usage.path, result.root),
35
+ "line": usage.line,
36
+ "required": usage.required,
37
+ "source": usage.source,
38
+ }
39
+ for usage in result.code_usages
40
+ ],
41
+ }
42
+ return json.dumps(payload, indent=2, sort_keys=True)
43
+
44
+
45
+ def _finding_to_dict(finding: Finding, root: Path) -> dict[str, Any]:
46
+ return {
47
+ "code": finding.code,
48
+ "severity": finding.severity.value,
49
+ "title": finding.title,
50
+ "message": finding.message,
51
+ "key": finding.key,
52
+ "path": _relative(finding.path, root) if finding.path else None,
53
+ "line": finding.line,
54
+ "suggestion": finding.suggestion,
55
+ "details": finding.details,
56
+ }
57
+
58
+
59
+ def _file_info(path: Path) -> dict[str, Any]:
60
+ return {"path": str(path), "exists": path.exists()}
61
+
62
+
63
+ def _relative(path: Path, root: Path) -> str:
64
+ try:
65
+ return str(path.relative_to(root))
66
+ except ValueError:
67
+ return str(path)
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from collections import Counter
5
+ from pathlib import Path
6
+
7
+ from envgap.checker import CheckResult
8
+ from envgap.model import Finding, Severity
9
+
10
+ ICONS = {
11
+ Severity.ERROR: "!",
12
+ Severity.WARNING: "~",
13
+ Severity.INFO: "i",
14
+ }
15
+
16
+ SEVERITY_ORDER = {
17
+ Severity.ERROR: 0,
18
+ Severity.WARNING: 1,
19
+ Severity.INFO: 2,
20
+ }
21
+
22
+ DIAGNOSIS_LABELS = {
23
+ "code_missing_from_example": "Code/documentation drift",
24
+ "duplicate_key": "Duplicate key",
25
+ "empty_value": "Empty value",
26
+ "missing_env_file": "Missing source",
27
+ "missing_example_file": "Missing source",
28
+ "missing_key": "Missing value",
29
+ "parse_warning": "Parse warning",
30
+ "placeholder_value": "Placeholder value",
31
+ "possible_typo": "Possible typo",
32
+ "undocumented_key": "Undocumented key",
33
+ }
34
+
35
+
36
+ def render_terminal(result: CheckResult, strict: bool = False) -> str:
37
+ lines: list[str] = []
38
+ counts = Counter(finding.severity for finding in result.findings)
39
+
40
+ lines.append("envgap check")
41
+ lines.append("=" * 15)
42
+ lines.append("")
43
+ lines.append(f"Project: {result.root}")
44
+ lines.append("Checked:")
45
+ lines.append(f" shell environment: {_shell_summary(result)}")
46
+ lines.append(f" .env: {_dotenv_summary(result.env_path, len(result.env_file.vars))}")
47
+ lines.append(f" .env.example: {_dotenv_summary(result.example_path, len(result.example_file.vars))}")
48
+ lines.append(f" Python code: {len(result.code_usages)} env usage(s)")
49
+ lines.append("")
50
+
51
+ if not result.findings:
52
+ lines.append("OK No environment config issues found.")
53
+ lines.append(_exit_code_line(strict))
54
+ return "\n".join(lines)
55
+
56
+ lines.append(
57
+ "Summary: "
58
+ f"{counts[Severity.ERROR]} error(s), "
59
+ f"{counts[Severity.WARNING]} warning(s), "
60
+ f"{counts[Severity.INFO]} info"
61
+ )
62
+ lines.append("")
63
+ lines.append("Diagnosis:")
64
+
65
+ for group, findings in _group_findings(result.findings).items():
66
+ lines.append(f" {group}")
67
+ for finding in sorted(findings, key=_finding_sort_key):
68
+ lines.extend(_format_finding(finding, result))
69
+ lines.append("")
70
+
71
+ lines.append(_exit_code_line(strict))
72
+ return "\n".join(lines).rstrip()
73
+
74
+
75
+ def _format_finding(finding: Finding, result: CheckResult) -> list[str]:
76
+ icon = ICONS[finding.severity]
77
+ location = _location(finding, result.root)
78
+ label = DIAGNOSIS_LABELS.get(finding.code, finding.code.replace("_", " ").title())
79
+ lines = [f" {icon} {label}: {finding.title}{location}", f" {finding.message}"]
80
+ checked = [detail for detail in finding.details if _is_checked_detail(detail)]
81
+ usages = [detail for detail in finding.details if not _is_checked_detail(detail)]
82
+ if checked:
83
+ lines.append(" Checked:")
84
+ for detail in checked:
85
+ lines.append(f" {detail}")
86
+ elif finding.key:
87
+ lines.extend(_checked_lines(finding.key, result))
88
+ for detail in usages:
89
+ lines.append(f" Used at: {_relative_detail(detail, result.root)}")
90
+ if finding.suggestion:
91
+ lines.append(f" Suggested fix: {finding.suggestion}")
92
+ return lines
93
+
94
+
95
+ def _group_findings(findings: list[Finding]) -> dict[str, list[Finding]]:
96
+ groups: dict[str, list[Finding]] = defaultdict(list)
97
+ for finding in findings:
98
+ groups[finding.key or "Project setup"].append(finding)
99
+ return {
100
+ key: groups[key]
101
+ for key in sorted(groups, key=lambda value: (value != "Project setup", value))
102
+ }
103
+
104
+
105
+ def _finding_sort_key(finding: Finding) -> tuple[int, str, int]:
106
+ return (SEVERITY_ORDER[finding.severity], finding.code, finding.line or 0)
107
+
108
+
109
+ def _checked_lines(key: str, result: CheckResult) -> list[str]:
110
+ return [
111
+ " Checked:",
112
+ f" shell environment: {_shell_key_status(key, result)}",
113
+ f" .env: {_env_file_status(key, result.env_path, result.env_file.values)}",
114
+ f" .env.example: {_env_file_status(key, result.example_path, result.example_file.values)}",
115
+ f" Python code: {_code_status(key, result)}",
116
+ ]
117
+
118
+
119
+ def _is_checked_detail(detail: str) -> bool:
120
+ return detail.startswith(("shell environment:", ".env:", ".env.example:", "Python code:"))
121
+
122
+
123
+ def _dotenv_summary(path: Path, count: int) -> str:
124
+ if not path.exists():
125
+ return "not found"
126
+ return f"found ({count} key(s))"
127
+
128
+
129
+ def _shell_summary(result: CheckResult) -> str:
130
+ if not result.include_shell:
131
+ return "ignored (--no-shell)"
132
+ if not result.expected_keys:
133
+ return f"available ({len(result.shell_env)} variable(s))"
134
+ found = sum(1 for key in result.expected_keys if key in result.shell_env)
135
+ return f"available ({found}/{len(result.expected_keys)} expected key(s) found)"
136
+
137
+
138
+ def _env_file_status(key: str, path: Path, values: dict) -> str:
139
+ if not path.exists():
140
+ return "file not found"
141
+ return "found" if key in values else "not found"
142
+
143
+
144
+ def _shell_key_status(key: str, result: CheckResult) -> str:
145
+ if not result.include_shell:
146
+ return "ignored"
147
+ return "found" if key in result.shell_env else "not found"
148
+
149
+
150
+ def _code_status(key: str, result: CheckResult) -> str:
151
+ usages = [usage for usage in result.code_usages if usage.key == key]
152
+ if not usages:
153
+ return "not found"
154
+ if any(usage.required for usage in usages):
155
+ return "required"
156
+ return "optional"
157
+
158
+
159
+ def _exit_code_line(strict: bool) -> str:
160
+ if strict:
161
+ return "Exit code: 1 when errors or warnings are present (--strict/--ci), otherwise 0."
162
+ return "Exit code: 1 when errors are present, otherwise 0. Warnings do not fail unless --strict or --ci is used."
163
+
164
+
165
+ def _location(finding: Finding, root: Path) -> str:
166
+ if not finding.path:
167
+ return ""
168
+ path = _relative(finding.path, root)
169
+ if finding.line:
170
+ return f" ({path}:{finding.line})"
171
+ return f" ({path})"
172
+
173
+
174
+ def _relative(path: Path, root: Path) -> str:
175
+ try:
176
+ return str(path.relative_to(root))
177
+ except ValueError:
178
+ return str(path)
179
+
180
+
181
+ def _relative_detail(detail: str, root: Path) -> str:
182
+ root_text = str(root)
183
+ return detail.replace(root_text + "/", "")
@@ -0,0 +1,319 @@
1
+ Metadata-Version: 2.4
2
+ Name: envgap
3
+ Version: 0.1.0
4
+ Summary: Find the gaps in your Python environment config.
5
+ Project-URL: Homepage, https://github.com/Pinak-Datta/envgap
6
+ Project-URL: Repository, https://github.com/Pinak-Datta/envgap
7
+ Project-URL: Issues, https://github.com/Pinak-Datta/envgap/issues
8
+ Project-URL: Changelog, https://github.com/Pinak-Datta/envgap/blob/main/CHANGELOG.md
9
+ Author: envgap contributors
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: cli,configuration,diagnostics,dotenv,environment
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Quality Assurance
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.9
26
+ Provides-Extra: dev
27
+ Requires-Dist: build>=1.2; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: twine>=5.0; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # envgap
33
+
34
+ Find the gaps in your Python environment config.
35
+
36
+ `envgap` is a diagnostic CLI for Python projects that use `.env` files, `.env.example`, shell variables, and `os.environ` / `os.getenv` in code. It does not load your config. It shows the gaps between what your app expects, what your project documents, and what your environment actually provides.
37
+
38
+ ![envgap terminal diagnosis screenshot](docs/assets/envgap-terminal.svg)
39
+
40
+ ## 30-Second Demo
41
+
42
+ Given a project with:
43
+
44
+ ```dotenv
45
+ # .env
46
+ DB_URL=postgres://localhost/app
47
+ OPENAI_API_KEY=changeme
48
+ ```
49
+
50
+ ```python
51
+ # app.py
52
+ import os
53
+
54
+ DATABASE_URL = os.environ["DATABASE_URL"]
55
+ ```
56
+
57
+ Run:
58
+
59
+ ```console
60
+ $ DATABASE_URL=postgres://shell/app envgap check examples/basic
61
+
62
+ envgap check
63
+ ===============
64
+
65
+ Checked:
66
+ shell environment: available (1/4 expected key(s) found)
67
+ .env: found (3 key(s))
68
+ .env.example: found (3 key(s))
69
+ Python code: 3 env usage(s)
70
+
71
+ Diagnosis:
72
+ DATABASE_URL
73
+ ! Missing value: DATABASE_URL is missing from .env (app.py:3)
74
+ It is present in your shell environment, so local commands may work while CI or Docker still fails.
75
+ Suggested fix: Add DATABASE_URL=... to .env or document how CI/Docker should provide it.
76
+
77
+ DB_URL
78
+ ~ Possible typo: DB_URL may be a typo for DATABASE_URL (.env:1)
79
+ Suggested fix: Rename DB_URL to DATABASE_URL if they represent the same setting.
80
+ ```
81
+
82
+ ## Why
83
+
84
+ Environment config bugs are boring until they eat an afternoon.
85
+
86
+ Common examples:
87
+
88
+ - The app expects `DATABASE_URL`, but `.env` contains `DB_URL`.
89
+ - `.env.example` says a key exists, but local `.env` never got it.
90
+ - A secret is still set to `changeme`.
91
+ - A variable works locally only because it is exported in your shell.
92
+ - CI, Docker, or another developer's machine fails because the real required variables are not documented.
93
+
94
+ `envgap` is for that moment when you want the project to explain itself.
95
+
96
+ ## Install
97
+
98
+ ```console
99
+ pip install envgap
100
+ ```
101
+
102
+ From a local checkout:
103
+
104
+ ```console
105
+ pip install -e ".[dev]"
106
+ ```
107
+
108
+ ## Quick Start
109
+
110
+ Run a check in the current project:
111
+
112
+ ```console
113
+ envgap check
114
+ ```
115
+
116
+ Try the included broken example:
117
+
118
+ ```console
119
+ envgap check examples/basic
120
+ ```
121
+
122
+ Show machine-readable output:
123
+
124
+ ```console
125
+ envgap check --json
126
+ ```
127
+
128
+ Ignore shell variables for deterministic CI checks:
129
+
130
+ ```console
131
+ envgap check --no-shell
132
+ ```
133
+
134
+ Fail on warnings as well as errors:
135
+
136
+ ```console
137
+ envgap check --strict
138
+ ```
139
+
140
+ `--ci` is supported as a CI-friendly alias for `--strict`:
141
+
142
+ ```console
143
+ envgap check --ci
144
+ ```
145
+
146
+ Use custom dotenv filenames:
147
+
148
+ ```console
149
+ envgap check --env-file .env.local --example-file .env.example
150
+ ```
151
+
152
+ ## What It Checks Today
153
+
154
+ `envgap check` currently inspects:
155
+
156
+ - current shell environment
157
+ - `.env`
158
+ - `.env.example`
159
+ - Python files using common environment variable APIs
160
+
161
+ It detects:
162
+
163
+ - missing keys
164
+ - undocumented extra keys
165
+ - duplicate keys
166
+ - empty values
167
+ - placeholder values like `your-key-here`, `changeme`, `todo`, and `replace-me`
168
+ - likely typo pairs like `DB_URL` vs `DATABASE_URL`
169
+ - required env vars used in Python code but missing from `.env.example`
170
+ - missing `.env`
171
+ - missing `.env.example`
172
+
173
+ It scans Python code for:
174
+
175
+ ```python
176
+ os.environ["DATABASE_URL"]
177
+ os.getenv("DATABASE_URL")
178
+ os.getenv("DATABASE_URL", "sqlite:///local.db")
179
+ os.environ.get("DATABASE_URL", "sqlite:///local.db")
180
+ ```
181
+
182
+ Required vs optional behavior:
183
+
184
+ - `os.environ["KEY"]` is required
185
+ - `os.getenv("KEY")` is required
186
+ - `os.getenv("KEY", default)` is optional
187
+ - `os.environ.get("KEY", default)` is optional
188
+
189
+ ## Exit Codes
190
+
191
+ | Command | Exit code behavior |
192
+ | --- | --- |
193
+ | `envgap check` | exits `1` when errors are present |
194
+ | `envgap check --strict` | exits `1` when errors or warnings are present |
195
+ | `envgap check --ci` | same as `--strict` |
196
+ | `envgap check --json` | same pass/fail behavior, JSON output |
197
+ | `envgap check --no-shell` | ignores current shell variables when diagnosing missing keys |
198
+
199
+ Warnings do not fail a normal check unless `--strict` or `--ci` is used.
200
+
201
+ ## CI
202
+
203
+ ```yaml
204
+ name: envgap
205
+
206
+ on: [push, pull_request]
207
+
208
+ jobs:
209
+ envgap:
210
+ runs-on: ubuntu-latest
211
+ steps:
212
+ - uses: actions/checkout@v4
213
+ - uses: actions/setup-python@v5
214
+ with:
215
+ python-version: "3.12"
216
+ - run: pip install envgap
217
+ - run: envgap check --ci
218
+ ```
219
+
220
+ ## Example Diagnosis
221
+
222
+ Given:
223
+
224
+ ```dotenv
225
+ # .env
226
+ DB_URL=postgres://localhost/app
227
+ OPENAI_API_KEY=changeme
228
+ ```
229
+
230
+ ```dotenv
231
+ # .env.example
232
+ DATABASE_URL=
233
+ OPENAI_API_KEY=your-key-here
234
+ ```
235
+
236
+ ```python
237
+ # app.py
238
+ import os
239
+
240
+ DATABASE_URL = os.environ["DATABASE_URL"]
241
+ DEBUG = os.getenv("DEBUG", "false")
242
+ ```
243
+
244
+ `envgap` can report:
245
+
246
+ - `DATABASE_URL` is required in code but missing from `.env`
247
+ - `DB_URL` may be a typo for `DATABASE_URL`
248
+ - `OPENAI_API_KEY` still looks like a placeholder
249
+ - `DEBUG` is optional because it has a default
250
+
251
+ ## Why Not Just python-dotenv?
252
+
253
+ `python-dotenv` loads environment variables.
254
+
255
+ `envgap` explains whether the environment variables your app expects match the variables your project defines and documents.
256
+
257
+ The useful question is not only:
258
+
259
+ > Did `.env` load?
260
+
261
+ It is:
262
+
263
+ > What does my app expect, where should it come from, and why is it missing or wrong here?
264
+
265
+ ## Current Scope
266
+
267
+ This is intentionally a small diagnostic tool, not a config framework.
268
+
269
+ In scope now:
270
+
271
+ - `.env`
272
+ - `.env.example`
273
+ - shell environment
274
+ - Python `os.environ` / `os.getenv` scanning
275
+ - terminal and JSON reports
276
+ - CI-friendly exit codes
277
+
278
+ Not in scope yet:
279
+
280
+ - loading or mutating your environment
281
+ - validating every framework-specific settings pattern
282
+ - Docker Compose parsing
283
+ - GitHub Actions secrets parsing
284
+ - Pydantic `BaseSettings` extraction
285
+
286
+ ## Roadmap
287
+
288
+ - Pydantic `BaseSettings` support
289
+ - Django settings helper detection
290
+ - Docker Compose env detection
291
+ - GitHub Actions env/secrets detection
292
+ - precedence explanations for shell vs `.env` vs framework defaults
293
+ - GitHub Actions annotations
294
+ - richer JSON schema for editor and CI integrations
295
+
296
+ ## Development
297
+
298
+ ```console
299
+ python -m venv .venv
300
+ . .venv/bin/activate
301
+ pip install -e ".[dev]"
302
+ pytest
303
+ ```
304
+
305
+ Run the example locally:
306
+
307
+ ```console
308
+ envgap check examples/basic
309
+ ```
310
+
311
+ Run the shell-aware example:
312
+
313
+ ```console
314
+ DATABASE_URL=postgres://shell/app envgap check examples/basic
315
+ ```
316
+
317
+ ## License
318
+
319
+ MIT
@@ -0,0 +1,18 @@
1
+ envgap/__init__.py,sha256=pGzI4Jg8YQnLWnSukTFYR0bAehgUoJ3ZqIjAEIcqMMI,77
2
+ envgap/checker.py,sha256=gaKZuG4r8jMObbVfsu2XW7SuZQ0zDgivluP-vc_e2z8,14489
3
+ envgap/cli.py,sha256=KmG7F3CWt5MFIbChOIWXEITJfJRVDp9rPakD5wGedm0,2592
4
+ envgap/extractors/__init__.py,sha256=nACaqCNkydmDvzoE6-JVgRtnWrY3Zkch-8Mk2vET6ZI,77
5
+ envgap/extractors/dotenv.py,sha256=5CEJA2X3NGkZCQ4mpU0I3_U20XLrOwZ9wsxF94IVJ9s,2132
6
+ envgap/extractors/python_ast.py,sha256=utf8UOOpe8bQCd2LRFKrgpRDOCB8RBVXKXQHzjqgZi8,3381
7
+ envgap/model/__init__.py,sha256=XR_3UlT31a1Q4Y2IdlIxew5xc_-2U4yOxMB9XhLpVBs,276
8
+ envgap/model/env_source.py,sha256=B8JvXQteTjO0ij319BdBUIejxVC77qiunZAtWXufHw4,625
9
+ envgap/model/expected_var.py,sha256=PnBdvCg06U6PhJtfc4ILbGVL92hggqPfwvM0jaY7L-I,481
10
+ envgap/model/finding.py,sha256=vW3i5L7dnJs8DkDvUQCo06uN9H1zewnRojKHUOj6oL8,497
11
+ envgap/reporters/__init__.py,sha256=my7j9pg8jRzUEUX8o7LOoqmGCPoANyMlhRufIoGG8HU,36
12
+ envgap/reporters/json.py,sha256=4sl5YwrkheBCI9mjF5NooAmuXdBbaanJeEF8FM7kND4,2265
13
+ envgap/reporters/terminal.py,sha256=2AhId3bAyS7R4WHwswo-U3M5DVjRjQY_sRtGcGGo0Kg,6220
14
+ envgap-0.1.0.dist-info/METADATA,sha256=QUqKJz4Umd1R7gIX2RLYfMHeL8AJPyBR5ZrB58RMry8,7472
15
+ envgap-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
16
+ envgap-0.1.0.dist-info/entry_points.txt,sha256=n3ti1tfkj0SoYffpqS6Yl1QjhUh8iEtnWalN7JAN0U4,43
17
+ envgap-0.1.0.dist-info/licenses/LICENSE,sha256=czunO6Ct_QtfNBeUU5BNSUvz8lNkni5wzoWcJavSusM,1077
18
+ envgap-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ envgap = envgap.cli:main