deploy-guard-engine 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,262 @@
1
+ """Text, Markdown and JSON renderers for a scan result."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from collections.abc import Iterable
8
+
9
+ from deploy_guard.engine import ScanResult
10
+
11
+ _SEV_LABEL = {"block": "BLOCK ", "review": "REVIEW", "warn": "WARN "}
12
+ _SEV_MARK = {"block": "●", "review": "◐", "warn": "○"}
13
+
14
+
15
+ def _rel(result: ScanResult, path: str) -> str:
16
+ try:
17
+ return os.path.relpath(path, result.project.root)
18
+ except ValueError:
19
+ return path
20
+
21
+
22
+ # --------------------------------------------------------------------------
23
+ # Plain text (terminal)
24
+ # --------------------------------------------------------------------------
25
+
26
+ def render_text(
27
+ result: ScanResult, *, show_behavior: bool = False, show_notes: bool = False
28
+ ) -> str:
29
+ p = result.project
30
+ lines: list[str] = []
31
+ lines.append(f"Deployment Guard - scan of {p.root}")
32
+ langs = ", ".join(sorted(p.languages)) or "none detected"
33
+ builds = ", ".join(p.build_systems) or "unknown"
34
+ lines.append(f" languages: {langs} build: {builds}")
35
+ lines.append(
36
+ f" modules: {len(p.modules)} functions analysed: {len(result.functions)}"
37
+ )
38
+ for note in p.notes:
39
+ lines.append(f" note: {note}")
40
+ lines.append("")
41
+
42
+ counts = result.counts_by_severity()
43
+ summary = (
44
+ f"Findings: {counts['block']} block {counts['review']} review "
45
+ f"{counts['warn']} warn"
46
+ )
47
+ if counts["note"]:
48
+ summary += f" ({counts['note']} note)"
49
+ lines.append(summary)
50
+ if result.collapsed_count:
51
+ lines.append(
52
+ f" ({result.collapsed_count} duplicate finding(s) from copy-pasted "
53
+ "functions folded in - see [same finding in N copies ...]; --no-collapse to expand)"
54
+ )
55
+ if result.disabled_rules:
56
+ lines.append(f" [disabled rules: {', '.join(sorted(result.disabled_rules))}]")
57
+ lines.append("")
58
+
59
+ for f in result.findings:
60
+ loc = f"{_rel(result, f.file)}:{f.lineno}"
61
+ lines.append(f" {_SEV_MARK[f.severity]} {_SEV_LABEL[f.severity]} {f.rule}")
62
+ lines.append(f" {loc} in {f.qualname}")
63
+ lines.append(f" {f.message}")
64
+ if f.detail:
65
+ lines.append(f" {f.detail}")
66
+ lines.append("")
67
+
68
+ notes = result.notes
69
+ if notes:
70
+ by_rule: dict[str, int] = {}
71
+ for n in notes:
72
+ by_rule[n.rule] = by_rule.get(n.rule, 0) + 1
73
+ breakdown = ", ".join(f"{v}x {k}" for k, v in sorted(by_rule.items()))
74
+ if show_notes:
75
+ lines.append(f"Notes ({len(notes)}: {breakdown}):")
76
+ for n in notes:
77
+ lines.append(
78
+ f" · {_rel(result, n.file)}:{n.lineno} {n.rule} - {n.message}"
79
+ )
80
+ lines.append("")
81
+ else:
82
+ lines.append(f"Notes: {len(notes)} ({breakdown}) - run with --notes to list")
83
+ lines.append("")
84
+
85
+ analysis_errors = [fr for fr in result.functions if fr.error]
86
+ if analysis_errors:
87
+ lines.append(f"Analysis errors ({len(analysis_errors)}):")
88
+ for fr in analysis_errors:
89
+ lines.append(f" {fr.fn.qualname}: {fr.error}")
90
+ lines.append("")
91
+
92
+ if result.parse_errors:
93
+ lines.append(f"Unparseable modules ({len(result.parse_errors)}):")
94
+ for name, err in result.parse_errors:
95
+ lines.append(f" {name}: {err}")
96
+ lines.append("")
97
+
98
+ if show_behavior:
99
+ lines.append("Behavior spec:")
100
+ lines.extend(_behavior_block(result, indent=" "))
101
+ lines.append("")
102
+
103
+ if result.generated:
104
+ lines.append("Generated tests (use --write-tests to save):")
105
+ for g in result.generated:
106
+ lines.append(f" .deploy-guard/generated/{g.relpath} [{g.kind}]")
107
+ lines.append("")
108
+
109
+ return "\n".join(lines).rstrip() + "\n"
110
+
111
+
112
+ def _behavior_block(result: ScanResult, *, indent: str) -> list[str]:
113
+ out: list[str] = []
114
+ for fr in result.functions:
115
+ if fr.error or not fr.spec.entries:
116
+ continue
117
+ out.append(f"{indent}{fr.fn.qualname}")
118
+ for e in fr.spec.entries:
119
+ when = " and ".join(e.when) if e.when else "always"
120
+ if e.outcome == "returns":
121
+ what = f"returns {e.value}"
122
+ elif e.outcome == "raises":
123
+ what = f"raises {e.value}"
124
+ elif e.outcome == "implicit-none":
125
+ what = "returns None (falls off the end)"
126
+ elif e.outcome == "truncated":
127
+ what = "... (path bound hit)"
128
+ else:
129
+ what = e.outcome
130
+ out.append(f"{indent} when {when}: {what}")
131
+ return out
132
+
133
+
134
+ # --------------------------------------------------------------------------
135
+ # Markdown
136
+ # --------------------------------------------------------------------------
137
+
138
+ def render_markdown(result: ScanResult) -> str:
139
+ p = result.project
140
+ counts = result.counts_by_severity()
141
+ md: list[str] = []
142
+ md.append("# Deployment Guard - scan report")
143
+ md.append("")
144
+ md.append(f"- **Root:** `{p.root}`")
145
+ md.append(f"- **Languages:** {', '.join(sorted(p.languages)) or 'none detected'}")
146
+ md.append(f"- **Build systems:** {', '.join(p.build_systems) or 'unknown'}")
147
+ md.append(f"- **Modules:** {len(p.modules)} | **Functions analysed:** {len(result.functions)}")
148
+ finding_line = (
149
+ f"- **Findings:** {counts['block']} block, {counts['review']} review, "
150
+ f"{counts['warn']} warn"
151
+ )
152
+ if counts["note"]:
153
+ finding_line += f" ({counts['note']} note)"
154
+ md.append(finding_line)
155
+ if result.disabled_rules:
156
+ md.append(f"- **Disabled rules:** {', '.join(sorted(result.disabled_rules))}")
157
+ md.append("")
158
+
159
+ if result.findings:
160
+ md.append("## Findings")
161
+ md.append("")
162
+ md.append("| Severity | Rule | Location | Function | Detail |")
163
+ md.append("|---|---|---|---|---|")
164
+ for f in result.findings:
165
+ loc = f"{_rel(result, f.file)}:{f.lineno}"
166
+ detail = f.message + (f" _{f.detail}_" if f.detail else "")
167
+ md.append(
168
+ f"| {f.severity} | `{f.rule}` | `{loc}` | `{f.qualname}` | {detail} |"
169
+ )
170
+ md.append("")
171
+
172
+ if result.notes:
173
+ md.append("## Notes (informational, not gate findings)")
174
+ md.append("")
175
+ for n in result.notes:
176
+ md.append(
177
+ f"- `{n.rule}` `{_rel(result, n.file)}:{n.lineno}` `{n.qualname}` - {n.message}"
178
+ )
179
+ md.append("")
180
+
181
+ if result.parse_errors:
182
+ md.append("## Unparseable modules")
183
+ md.append("")
184
+ for name, err in result.parse_errors:
185
+ md.append(f"- `{name}` - {err}")
186
+ md.append("")
187
+
188
+ md.append("## Behavior spec")
189
+ md.append("")
190
+ for line in _behavior_block(result, indent=""):
191
+ if not line.startswith(" "):
192
+ md.append(f"### `{line}`")
193
+ else:
194
+ md.append(f"- {line.strip()}")
195
+ md.append("")
196
+ return "\n".join(md).rstrip() + "\n"
197
+
198
+
199
+ # --------------------------------------------------------------------------
200
+ # JSON
201
+ # --------------------------------------------------------------------------
202
+
203
+ def render_json(result: ScanResult) -> str:
204
+ p = result.project
205
+ payload = {
206
+ "root": str(p.root),
207
+ "languages": sorted(p.languages),
208
+ "build_systems": p.build_systems,
209
+ "modules": len(p.modules),
210
+ "functions_analysed": len(result.functions),
211
+ "project_notes": p.notes,
212
+ "disabled_rules": sorted(result.disabled_rules),
213
+ "counts": result.counts_by_severity(),
214
+ "findings": [
215
+ {
216
+ "rule": f.rule,
217
+ "severity": f.severity,
218
+ "message": f.message,
219
+ "detail": f.detail,
220
+ "qualname": f.qualname,
221
+ "file": _rel(result, f.file),
222
+ "lineno": f.lineno,
223
+ }
224
+ for f in result.findings
225
+ ],
226
+ "notes": [
227
+ {
228
+ "rule": n.rule,
229
+ "message": n.message,
230
+ "qualname": n.qualname,
231
+ "file": _rel(result, n.file),
232
+ "lineno": n.lineno,
233
+ }
234
+ for n in result.notes
235
+ ],
236
+ "parse_errors": [
237
+ {"module": name, "error": err} for name, err in result.parse_errors
238
+ ],
239
+ "behavior": [
240
+ {
241
+ "qualname": fr.fn.qualname,
242
+ "truncated": fr.spec.truncated,
243
+ "paths": [
244
+ {
245
+ "when": e.when,
246
+ "outcome": e.outcome,
247
+ "value": e.value,
248
+ "lineno": e.lineno,
249
+ }
250
+ for e in fr.spec.entries
251
+ ],
252
+ }
253
+ for fr in result.functions
254
+ if not fr.error
255
+ ],
256
+ "generated": [g.relpath for g in result.generated],
257
+ }
258
+ return json.dumps(payload, indent=2)
259
+
260
+
261
+ def _iter_lines(chunks: Iterable[str]) -> str: # pragma: no cover - helper
262
+ return "\n".join(chunks)
deploy_guard/store.py ADDED
@@ -0,0 +1,50 @@
1
+ """Management of the ``.deploy-guard/`` working directory.
2
+
3
+ Holds generated tests and (from M3) contract baselines and golden files.
4
+ Everything here is meant to be committed and reviewed in PRs like code.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ from deploy_guard.generators.base import GeneratedFile
12
+
13
+ STORE_DIRNAME = ".deploy-guard"
14
+ GENERATED_SUBDIR = "generated"
15
+
16
+ _GITIGNORE = """\
17
+ # Deployment Guard Engine - transient analysis artifacts.
18
+ # Generated tests and baselines ARE committed; caches are not.
19
+ cache/
20
+ *.log
21
+ """
22
+
23
+
24
+ def store_dir(root: str | Path) -> Path:
25
+ return Path(root).resolve() / STORE_DIRNAME
26
+
27
+
28
+ def generated_dir(root: str | Path) -> Path:
29
+ return store_dir(root) / GENERATED_SUBDIR
30
+
31
+
32
+ def ensure_store(root: str | Path) -> Path:
33
+ d = store_dir(root)
34
+ (d / GENERATED_SUBDIR).mkdir(parents=True, exist_ok=True)
35
+ gi = d / ".gitignore"
36
+ if not gi.exists():
37
+ gi.write_text(_GITIGNORE, encoding="utf-8")
38
+ return d
39
+
40
+
41
+ def write_generated(root: str | Path, files: list[GeneratedFile]) -> list[Path]:
42
+ ensure_store(root)
43
+ out_dir = generated_dir(root)
44
+ written: list[Path] = []
45
+ for f in files:
46
+ target = out_dir / f.relpath
47
+ target.parent.mkdir(parents=True, exist_ok=True)
48
+ target.write_text(f.content, encoding="utf-8")
49
+ written.append(target)
50
+ return written
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.5
2
+ Name: deploy-guard-engine
3
+ Version: 0.1.0
4
+ Summary: Static root-cause analysis for a Python stack trace - offline, no account. Plus a deployment-gate scanner.
5
+ Project-URL: Homepage, https://github.com/sai55387/deploy-guard
6
+ Project-URL: Issues, https://github.com/sai55387/deploy-guard/issues
7
+ Project-URL: Changelog, https://github.com/sai55387/deploy-guard/blob/main/CHANGELOG.md
8
+ Author: sai55387
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ci,control-flow,debugging,deployment,incident,nullability,root-cause,stacktrace,static-analysis,traceback
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Debuggers
23
+ Classifier: Topic :: Software Development :: Quality Assurance
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Requires-Python: >=3.10
26
+ Provides-Extra: dev
27
+ Requires-Dist: build; extra == 'dev'
28
+ Requires-Dist: pytest-cov; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0; extra == 'dev'
30
+ Requires-Dist: ruff; extra == 'dev'
31
+ Requires-Dist: twine; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # deploy-guard
35
+
36
+ **Static root-cause analysis for a Python stack trace.** Paste a traceback,
37
+ point it at the repo, and get: which variable is `None` and *why*, the branch
38
+ path that reached the crash, where the bad value entered across the call
39
+ chain, and a concrete fix. Fully local — no server, no account, no
40
+ network. Zero dependencies. Python 3.10+.
41
+
42
+ It also ships a deployment-gate scanner (`scan` / `check`), but for
43
+ general-purpose linting you should run **[ruff](https://docs.astral.sh/ruff/)
44
+ + [mypy](https://mypy-lang.org/)** — they are faster and deeper. What
45
+ deploy-guard does that they don't is turn *a traceback you already have* into
46
+ an explanation grounded in your code.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install deploy-guard-engine # or: pipx install deploy-guard-engine
52
+ ```
53
+
54
+ Or run it with no install at all — a single ~120 KB file:
55
+
56
+ ```bash
57
+ ./scripts/build-standalone.sh # -> dist/deploy-guard.pyz
58
+ python deploy-guard.pyz explain --project ./service < traceback.txt
59
+ ```
60
+
61
+ ## Explain a traceback
62
+
63
+ ```bash
64
+ deploy-guard explain --project ./service --file traceback.txt
65
+ # or pipe it
66
+ deploy-guard explain --project ./service < traceback.txt
67
+ ```
68
+
69
+ ```
70
+ Explaining: AttributeError: 'NoneType' object has no attribute 'empty'
71
+
72
+ Crash site: dg_financial_profile/financial_main.py:231 create_json
73
+ 231 | if profile_df.empty or profile_df is None:
74
+
75
+ Why:
76
+ - `profile_df` can be None here - the default passed to .get() is None.
77
+ - It reaches this line when: overall_financial_dict
78
+ - This line does check `profile_df is None`, but only after dereferencing it -
79
+ and/or evaluate left to right, so the attribute access raises first.
80
+
81
+ Call chain (from the traceback):
82
+ multiprocessing_script:164 lambda_handler
83
+ └─ financial_main:231 create_json <- raised
84
+
85
+ Matches scan finding: none-dereference (block) at financial_main.py:231
86
+
87
+ Fix: reorder the check so `profile_df is None` comes first.
88
+ ```
89
+
90
+ Dedicated explanations for `AttributeError` / `TypeError` (None), `KeyError`,
91
+ `IndexError`, `ZeroDivisionError`, `UnboundLocalError`, `NameError`, and
92
+ `int()` / `float()` `ValueError`. Anything else falls back to the call chain
93
+ + the branch conditions that reach the line. The call chain is analysed
94
+ *interprocedurally* &mdash; each project frame is checked, and a `None` that
95
+ originates in an argument is traced back to the caller that passed it.
96
+
97
+ ## Scan a project
98
+
99
+ ```bash
100
+ deploy-guard scan ./service # report, never fails
101
+ deploy-guard scan ./service --behavior # + the when-X-returns-Y table
102
+ deploy-guard check ./service --fail-on review # gate: exit 1 on findings
103
+ deploy-guard scan ./service --report report.json
104
+ ```
105
+
106
+ | Rule | Severity | Catches |
107
+ |---|---|---|
108
+ | `none-dereference` | block / review | attr/subscript on a possibly-`None` value (interprocedural: a call to a project function that can return `None` counts) |
109
+ | `inconsistent-return` | review / note | returns a value on some paths, `None` on others &mdash; **review only when an in-project caller uses the result without a guard**, otherwise a note |
110
+ | `swallowed-exception` | review | `except …: pass` |
111
+ | `unreachable-code` | warn | code after an always-diverting branch |
112
+ | `bare-except` | warn | `except:` with no type |
113
+ | `mutable-default-arg` | warn | `def f(x=[])` |
114
+ | `invalid-escape` | note | `"\d"` in a non-raw string |
115
+ | `path-explosion` | note | too-branchy function (detection still complete) |
116
+
117
+ Notes are collapsed to a one-line count; `--notes` lists them. Identical
118
+ findings from copy-pasted functions fold into one; `--no-collapse` expands.
119
+
120
+ ## Configuration
121
+
122
+ A `[tool.deploy-guard]` table in the **scanned project's** `pyproject.toml`
123
+ (Python 3.11+ for TOML reading):
124
+
125
+ ```toml
126
+ [tool.deploy-guard]
127
+ disable = ["bare-except"]
128
+ exclude = ["vendor", "migrations"]
129
+ fail-on = "review"
130
+ include-tests = false
131
+ ```
132
+
133
+ CLI flags override the file.
134
+
135
+ ## How it works
136
+
137
+ ```
138
+ discover -> lower to IR -> CFG per function -> call graph
139
+ -> behavior spec (path conditions -> returns/raises)
140
+ -> nullability data-flow (merges None facts at every branch join; interprocedural)
141
+ -> findings | explain (traceback -> the above, focused on one failure)
142
+ ```
143
+
144
+ The nullability pass is a forward fixpoint over the CFG &mdash; it merges
145
+ facts at branch joins, so it covers 100% of a function regardless of how
146
+ branchy it is, in roughly linear time.
147
+
148
+ `scan` and `explain` only **read** your code &mdash; never import or run it.
149
+
150
+ ## Develop
151
+
152
+ ```bash
153
+ pip install -e ".[dev]"
154
+ pytest
155
+ ruff check src
156
+ ```
157
+
158
+ ## What changed from the prototype
159
+
160
+ See [CHANGELOG.md](CHANGELOG.md). Headline: an interprocedural call graph, so
161
+ `explain` traces a `None` across frames and `inconsistent-return` no longer
162
+ fires on copy-pasted helpers whose callers all guard the result; plus real
163
+ packaging and a config file.
@@ -0,0 +1,35 @@
1
+ deploy_guard/__init__.py,sha256=ZnpgqPXRdlEhr6-MW9oauCMFhnAi6LvV4L62zuPmmk4,299
2
+ deploy_guard/__main__.py,sha256=EpBj9gNzkz57SL1By2r7bQyQrK8hJzTiXgIVdjS6umc,91
3
+ deploy_guard/cli.py,sha256=Kdfc38f4m-OHyCbrIqbEjAUaswRHSxMrHcua2Oc4EnY,7288
4
+ deploy_guard/config.py,sha256=i3cq_P8cWKyfHt_sLYlr4_ONAHMEjQwuirPsfCKwNH0,2221
5
+ deploy_guard/engine.py,sha256=PCZFyGkhavDj9zjCiJ9i3BvIRD3oL1QyU6kX7z1vAnY,9567
6
+ deploy_guard/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ deploy_guard/store.py,sha256=Uay0bRYRAPKhOVEejap4o7PVx00JWXK5J7M7ZtOhKtk,1367
8
+ deploy_guard/analysis/__init__.py,sha256=MBZ6qK7vPyivfM734ZuwhOenuUTmYl_KrMx99pt7rX0,1119
9
+ deploy_guard/analysis/callgraph.py,sha256=xg7GUZQniEIJHZyRvMYKDG86ee94Tuj183Y3eth1I8A,5078
10
+ deploy_guard/analysis/context.py,sha256=tmeV-yWWwlmQCdakpQ1VqenxN_0E-pklcLGLS5H-VjM,1182
11
+ deploy_guard/analysis/findings.py,sha256=fd7euStrpLJO49YKMsclvtp0Ale61ws3B3dlh6RafZM,11650
12
+ deploy_guard/analysis/nullability.py,sha256=vvYVp4fC3qAAY3sphmiGBm7pEryX1SslbRzs9S_a6UU,18082
13
+ deploy_guard/analysis/paths.py,sha256=9zl3Pcr3dPiscBB3LqdzRqZ0ZZvZmYvKDKoXBlv3qzU,6939
14
+ deploy_guard/explain/__init__.py,sha256=JRhvmlhCH8hzS3CNyMh1Oelcvt7YhG_VqVMO65SGtPQ,521
15
+ deploy_guard/explain/explainer.py,sha256=txzs0o5F7hlqqKcjwNnkFjEJjRpY9xF73VxI9TlwYN4,17682
16
+ deploy_guard/explain/render.py,sha256=lE5l3Y01IK3NTf8DFsEQDo1tXFuF9RDWfyyagJLZkNI,1766
17
+ deploy_guard/explain/traceback_parse.py,sha256=2_JyusIvHyBgM8JHeryokKwjGvMyLOj4xuTKA26Ak6Q,2496
18
+ deploy_guard/frontend/__init__.py,sha256=WfuzQpPNurN45Du2DXyPNHuM4dM8RSRc0jENUuZfQME,347
19
+ deploy_guard/frontend/python_cfg.py,sha256=tYKOGJ5oaPs7Frn_OQHMWfBOXwJ-pEuUPJaX3hnzYDU,12677
20
+ deploy_guard/frontend/python_frontend.py,sha256=4GLgzmI4Kc5fsHFSmSuqUHgaMYCIAi4xJ9_jPuNE_HI,5761
21
+ deploy_guard/generators/__init__.py,sha256=Da-ussjB1y1nN5U-HL-UwJ1V-SYwJcsfgM8JmmdAQtI,457
22
+ deploy_guard/generators/base.py,sha256=EKuB6MCEuzG8ri7EP_A2dxhSUN82LnYjkDxpqVwLP64,247
23
+ deploy_guard/generators/import_smoke.py,sha256=fJ2qjl170tlPLTc7RAUfuFb_OWT7hr2hKvSeGeUK5Ns,3071
24
+ deploy_guard/ingest/__init__.py,sha256=1HbCTyg5BaxrS4xLsCYgnSjC1OsZNYFIpgYYQBaA9k0,155
25
+ deploy_guard/ingest/discover.py,sha256=c_BwT67wu99Wbqh79qsrcT9VAwmxP2aWGNWAWbmdRHQ,5249
26
+ deploy_guard/ir/__init__.py,sha256=WrxIHAPxRwVvgWn5dCC3BIso7A1HEzT-L6hpMduFeYk,470
27
+ deploy_guard/ir/cfg.py,sha256=UuMgKhY5aDRL89j0wsLpJ7jG--KeFKgsp5jpBMxoW3Y,3778
28
+ deploy_guard/ir/model.py,sha256=Xa1r3QvMkMaoA0gJD28qvOUzo5lxf7YjDYXP9giWPXg,3980
29
+ deploy_guard/report/__init__.py,sha256=8Qe7ha2-GYiBc3JFuB16kHrgy09pExi6FyytKLu5X0o,228
30
+ deploy_guard/report/render.py,sha256=TVR537HN9VcWo800bbs9WJzVs7xvC6piUZbqZjZm85w,9121
31
+ deploy_guard_engine-0.1.0.dist-info/METADATA,sha256=zrsNu6ZN3dGScYjkY36lPT5-WKFn-T3o30OhcoQdtPE,6323
32
+ deploy_guard_engine-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
33
+ deploy_guard_engine-0.1.0.dist-info/entry_points.txt,sha256=bh2T76PdJzYqirWSMYzzSa4xbrV5AyySPxz3zsSUMf8,82
34
+ deploy_guard_engine-0.1.0.dist-info/licenses/LICENSE,sha256=3lCxvJJXjv3cG2n8Zw0LyBS1L6cfYcCgnh7MeZQBYsw,1065
35
+ deploy_guard_engine-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ deploy-guard = deploy_guard.cli:main
3
+ dg = deploy_guard.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sai55387
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.