runmark 0.2.1__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.
- runmark/__init__.py +7 -0
- runmark/__main__.py +8 -0
- runmark/cli/__init__.py +5 -0
- runmark/cli/app.py +74 -0
- runmark/cli/commands/__init__.py +27 -0
- runmark/cli/commands/check.py +54 -0
- runmark/cli/commands/contract.py +236 -0
- runmark/cli/commands/diff.py +90 -0
- runmark/cli/commands/doctor.py +73 -0
- runmark/cli/commands/history.py +25 -0
- runmark/cli/commands/init.py +65 -0
- runmark/cli/commands/scan.py +25 -0
- runmark/cli/commands/share.py +104 -0
- runmark/cli/commands/snapshot.py +39 -0
- runmark/cli/commands/verify.py +75 -0
- runmark/cli/commands/version.py +36 -0
- runmark/contracts/__init__.py +60 -0
- runmark/contracts/canonicalizer.py +119 -0
- runmark/contracts/diff.py +390 -0
- runmark/contracts/discovery.py +33 -0
- runmark/contracts/evaluator.py +724 -0
- runmark/contracts/evidence.py +448 -0
- runmark/contracts/generator.py +107 -0
- runmark/contracts/parser.py +60 -0
- runmark/contracts/security.py +11 -0
- runmark/contracts/validator.py +157 -0
- runmark/contracts/version_constraints.py +234 -0
- runmark/core/__init__.py +32 -0
- runmark/core/contract_check.py +66 -0
- runmark/core/contract_diff.py +65 -0
- runmark/core/contract_init.py +80 -0
- runmark/core/diff.py +522 -0
- runmark/core/doctor.py +240 -0
- runmark/core/reporter.py +163 -0
- runmark/core/scanner.py +173 -0
- runmark/core/snapshot.py +34 -0
- runmark/core/verifier.py +83 -0
- runmark/detectors/__init__.py +86 -0
- runmark/detectors/base.py +66 -0
- runmark/detectors/containers/__init__.py +5 -0
- runmark/detectors/containers/compose.py +82 -0
- runmark/detectors/dependencies/__init__.py +9 -0
- runmark/detectors/dependencies/node.py +185 -0
- runmark/detectors/dependencies/python.py +162 -0
- runmark/detectors/environment/__init__.py +5 -0
- runmark/detectors/environment/env.py +89 -0
- runmark/detectors/git/__init__.py +5 -0
- runmark/detectors/git/git.py +81 -0
- runmark/detectors/network/__init__.py +5 -0
- runmark/detectors/network/ports.py +90 -0
- runmark/detectors/project/__init__.py +11 -0
- runmark/detectors/project/docker.py +48 -0
- runmark/detectors/project/node.py +95 -0
- runmark/detectors/project/python.py +97 -0
- runmark/detectors/registry.py +39 -0
- runmark/detectors/runtimes/__init__.py +13 -0
- runmark/detectors/runtimes/docker.py +59 -0
- runmark/detectors/runtimes/git.py +59 -0
- runmark/detectors/runtimes/node.py +59 -0
- runmark/detectors/runtimes/python.py +62 -0
- runmark/detectors/services/__init__.py +9 -0
- runmark/detectors/services/postgres.py +104 -0
- runmark/detectors/services/redis.py +104 -0
- runmark/detectors/system/__init__.py +5 -0
- runmark/detectors/system/system.py +40 -0
- runmark/models/__init__.py +81 -0
- runmark/models/common.py +36 -0
- runmark/models/container.py +14 -0
- runmark/models/contract.py +122 -0
- runmark/models/contract_result.py +84 -0
- runmark/models/dependency.py +19 -0
- runmark/models/diagnostic.py +133 -0
- runmark/models/environment.py +32 -0
- runmark/models/git.py +16 -0
- runmark/models/network.py +17 -0
- runmark/models/project.py +20 -0
- runmark/models/runmark.py +58 -0
- runmark/models/runtime.py +19 -0
- runmark/models/service.py +21 -0
- runmark/models/system.py +13 -0
- runmark/output/__init__.py +34 -0
- runmark/output/contract.py +337 -0
- runmark/output/json.py +16 -0
- runmark/output/markdown.py +172 -0
- runmark/output/tables.py +300 -0
- runmark/output/terminal.py +70 -0
- runmark/security/__init__.py +29 -0
- runmark/security/contract_sanitizer.py +38 -0
- runmark/security/export_sanitizer.py +142 -0
- runmark/security/redactor.py +75 -0
- runmark/security/sanitizer.py +105 -0
- runmark/security/secret_patterns.py +116 -0
- runmark/storage/__init__.py +6 -0
- runmark/storage/filesystem.py +133 -0
- runmark/storage/paths.py +60 -0
- runmark/utils/__init__.py +20 -0
- runmark/utils/commands.py +117 -0
- runmark/utils/hashing.py +240 -0
- runmark/utils/platform.py +36 -0
- runmark-0.2.1.dist-info/METADATA +149 -0
- runmark-0.2.1.dist-info/RECORD +105 -0
- runmark-0.2.1.dist-info/WHEEL +5 -0
- runmark-0.2.1.dist-info/entry_points.txt +2 -0
- runmark-0.2.1.dist-info/licenses/LICENSE +21 -0
- runmark-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Semantic contract diffing engine comparing environment requirements."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from runmark.models.common import RunmarkBaseModel
|
|
7
|
+
from runmark.models.contract import RunmarkContract
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ContractChangeKind(str, Enum):
|
|
11
|
+
"""Classification of requirement differences."""
|
|
12
|
+
|
|
13
|
+
ADDED = "ADDED"
|
|
14
|
+
REMOVED = "REMOVED"
|
|
15
|
+
CHANGED = "CHANGED"
|
|
16
|
+
UNCHANGED = "UNCHANGED"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContractChange(RunmarkBaseModel):
|
|
20
|
+
"""A discrete difference between two contract definitions."""
|
|
21
|
+
|
|
22
|
+
category: str
|
|
23
|
+
name: str
|
|
24
|
+
change: ContractChangeKind
|
|
25
|
+
before: Any | None = None
|
|
26
|
+
after: Any | None = None
|
|
27
|
+
details: dict[str, Any] = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ContractDiffSummary(RunmarkBaseModel):
|
|
31
|
+
"""Aggregated counters for contract changes."""
|
|
32
|
+
|
|
33
|
+
added: int = 0
|
|
34
|
+
removed: int = 0
|
|
35
|
+
changed: int = 0
|
|
36
|
+
unchanged: int = 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ContractDiffResult(RunmarkBaseModel):
|
|
40
|
+
"""Root model for semantic contract comparison results."""
|
|
41
|
+
|
|
42
|
+
status: str # "CHANGED" or "UNCHANGED"
|
|
43
|
+
summary: ContractDiffSummary
|
|
44
|
+
changes: list[ContractChange]
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def has_changes(self) -> bool:
|
|
48
|
+
"""Return True if any requirements were added, removed, or changed."""
|
|
49
|
+
return self.summary.added > 0 or self.summary.removed > 0 or self.summary.changed > 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ContractDiffEngine:
|
|
53
|
+
"""Performs semantic comparison between two RunmarkContract definitions."""
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def diff(cls, before: RunmarkContract, after: RunmarkContract) -> ContractDiffResult:
|
|
57
|
+
"""Compare two contracts and produce a normalized list of semantic changes."""
|
|
58
|
+
changes: list[ContractChange] = []
|
|
59
|
+
|
|
60
|
+
# 1. Project identity
|
|
61
|
+
b_name = before.project.name
|
|
62
|
+
a_name = after.project.name
|
|
63
|
+
if b_name != a_name:
|
|
64
|
+
changes.append(
|
|
65
|
+
ContractChange(
|
|
66
|
+
category="project",
|
|
67
|
+
name="name",
|
|
68
|
+
change=ContractChangeKind.CHANGED,
|
|
69
|
+
before=b_name,
|
|
70
|
+
after=a_name,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# 2. Platform OS
|
|
75
|
+
b_os = set(before.platform.os)
|
|
76
|
+
a_os = set(after.platform.os)
|
|
77
|
+
for os_name in sorted(a_os - b_os):
|
|
78
|
+
changes.append(
|
|
79
|
+
ContractChange(
|
|
80
|
+
category="platform",
|
|
81
|
+
name=f"os:{os_name}",
|
|
82
|
+
change=ContractChangeKind.ADDED,
|
|
83
|
+
before=None,
|
|
84
|
+
after=os_name,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
for os_name in sorted(b_os - a_os):
|
|
88
|
+
changes.append(
|
|
89
|
+
ContractChange(
|
|
90
|
+
category="platform",
|
|
91
|
+
name=f"os:{os_name}",
|
|
92
|
+
change=ContractChangeKind.REMOVED,
|
|
93
|
+
before=os_name,
|
|
94
|
+
after=None,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Platform Architecture
|
|
99
|
+
b_arch = set(before.platform.architecture)
|
|
100
|
+
a_arch = set(after.platform.architecture)
|
|
101
|
+
for arch_name in sorted(a_arch - b_arch):
|
|
102
|
+
changes.append(
|
|
103
|
+
ContractChange(
|
|
104
|
+
category="platform",
|
|
105
|
+
name=f"arch:{arch_name}",
|
|
106
|
+
change=ContractChangeKind.ADDED,
|
|
107
|
+
before=None,
|
|
108
|
+
after=arch_name,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
for arch_name in sorted(b_arch - a_arch):
|
|
112
|
+
changes.append(
|
|
113
|
+
ContractChange(
|
|
114
|
+
category="platform",
|
|
115
|
+
name=f"arch:{arch_name}",
|
|
116
|
+
change=ContractChangeKind.REMOVED,
|
|
117
|
+
before=arch_name,
|
|
118
|
+
after=None,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# 3. Runtimes
|
|
123
|
+
all_runtimes = sorted(set(before.runtime.keys()) | set(after.runtime.keys()))
|
|
124
|
+
for r_name in all_runtimes:
|
|
125
|
+
b_val = before.runtime.get(r_name)
|
|
126
|
+
a_val = after.runtime.get(r_name)
|
|
127
|
+
if b_val is None and a_val is not None:
|
|
128
|
+
changes.append(
|
|
129
|
+
ContractChange(
|
|
130
|
+
category="runtime",
|
|
131
|
+
name=r_name,
|
|
132
|
+
change=ContractChangeKind.ADDED,
|
|
133
|
+
before=None,
|
|
134
|
+
after=a_val,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
elif b_val is not None and a_val is None:
|
|
138
|
+
changes.append(
|
|
139
|
+
ContractChange(
|
|
140
|
+
category="runtime",
|
|
141
|
+
name=r_name,
|
|
142
|
+
change=ContractChangeKind.REMOVED,
|
|
143
|
+
before=b_val,
|
|
144
|
+
after=None,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
elif b_val != a_val:
|
|
148
|
+
changes.append(
|
|
149
|
+
ContractChange(
|
|
150
|
+
category="runtime",
|
|
151
|
+
name=r_name,
|
|
152
|
+
change=ContractChangeKind.CHANGED,
|
|
153
|
+
before=b_val,
|
|
154
|
+
after=a_val,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# 4. Dependencies
|
|
159
|
+
cls._diff_deps(
|
|
160
|
+
"dependency:python", before.dependencies.python, after.dependencies.python, changes
|
|
161
|
+
)
|
|
162
|
+
cls._diff_deps(
|
|
163
|
+
"dependency:node", before.dependencies.node, after.dependencies.node, changes
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# 5. Services
|
|
167
|
+
all_services = sorted(set(before.services.keys()) | set(after.services.keys()))
|
|
168
|
+
for s_name in all_services:
|
|
169
|
+
b_svc = before.services.get(s_name)
|
|
170
|
+
a_svc = after.services.get(s_name)
|
|
171
|
+
if b_svc is None and a_svc is not None:
|
|
172
|
+
changes.append(
|
|
173
|
+
ContractChange(
|
|
174
|
+
category="service",
|
|
175
|
+
name=s_name,
|
|
176
|
+
change=ContractChangeKind.ADDED,
|
|
177
|
+
before=None,
|
|
178
|
+
after=a_svc.version,
|
|
179
|
+
details={"required": a_svc.required},
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
elif b_svc is not None and a_svc is None:
|
|
183
|
+
changes.append(
|
|
184
|
+
ContractChange(
|
|
185
|
+
category="service",
|
|
186
|
+
name=s_name,
|
|
187
|
+
change=ContractChangeKind.REMOVED,
|
|
188
|
+
before=b_svc.version,
|
|
189
|
+
after=None,
|
|
190
|
+
details={"required": b_svc.required},
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
elif b_svc is not None and a_svc is not None:
|
|
194
|
+
if b_svc.version != a_svc.version or b_svc.required != a_svc.required:
|
|
195
|
+
changes.append(
|
|
196
|
+
ContractChange(
|
|
197
|
+
category="service",
|
|
198
|
+
name=s_name,
|
|
199
|
+
change=ContractChangeKind.CHANGED,
|
|
200
|
+
before=b_svc.version,
|
|
201
|
+
after=a_svc.version,
|
|
202
|
+
details={
|
|
203
|
+
"before_required": b_svc.required,
|
|
204
|
+
"after_required": a_svc.required,
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# 6. Environment variables
|
|
210
|
+
b_req = set(before.environment.required)
|
|
211
|
+
a_req = set(after.environment.required)
|
|
212
|
+
for var in sorted(a_req - b_req):
|
|
213
|
+
changes.append(
|
|
214
|
+
ContractChange(
|
|
215
|
+
category="environment",
|
|
216
|
+
name=var,
|
|
217
|
+
change=ContractChangeKind.ADDED,
|
|
218
|
+
before=None,
|
|
219
|
+
after="required",
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
for var in sorted(b_req - a_req):
|
|
223
|
+
changes.append(
|
|
224
|
+
ContractChange(
|
|
225
|
+
category="environment",
|
|
226
|
+
name=var,
|
|
227
|
+
change=ContractChangeKind.REMOVED,
|
|
228
|
+
before="required",
|
|
229
|
+
after=None,
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
b_opt = set(before.environment.optional)
|
|
234
|
+
a_opt = set(after.environment.optional)
|
|
235
|
+
for var in sorted(a_opt - b_opt):
|
|
236
|
+
changes.append(
|
|
237
|
+
ContractChange(
|
|
238
|
+
category="environment",
|
|
239
|
+
name=var,
|
|
240
|
+
change=ContractChangeKind.ADDED,
|
|
241
|
+
before=None,
|
|
242
|
+
after="optional",
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
for var in sorted(b_opt - a_opt):
|
|
246
|
+
changes.append(
|
|
247
|
+
ContractChange(
|
|
248
|
+
category="environment",
|
|
249
|
+
name=var,
|
|
250
|
+
change=ContractChangeKind.REMOVED,
|
|
251
|
+
before="optional",
|
|
252
|
+
after=None,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# 7. Network Ports
|
|
257
|
+
all_ports = sorted(
|
|
258
|
+
set(before.network.ports.keys()) | set(after.network.ports.keys()),
|
|
259
|
+
key=lambda x: int(x) if x.isdigit() else x,
|
|
260
|
+
)
|
|
261
|
+
for p_str in all_ports:
|
|
262
|
+
b_p = before.network.ports.get(p_str)
|
|
263
|
+
a_p = after.network.ports.get(p_str)
|
|
264
|
+
if b_p is None and a_p is not None:
|
|
265
|
+
changes.append(
|
|
266
|
+
ContractChange(
|
|
267
|
+
category="network",
|
|
268
|
+
name=f"port:{p_str}/{a_p.protocol}",
|
|
269
|
+
change=ContractChangeKind.ADDED,
|
|
270
|
+
before=None,
|
|
271
|
+
after=f"{p_str}/{a_p.protocol}",
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
elif b_p is not None and a_p is None:
|
|
275
|
+
changes.append(
|
|
276
|
+
ContractChange(
|
|
277
|
+
category="network",
|
|
278
|
+
name=f"port:{p_str}/{b_p.protocol}",
|
|
279
|
+
change=ContractChangeKind.REMOVED,
|
|
280
|
+
before=f"{p_str}/{b_p.protocol}",
|
|
281
|
+
after=None,
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
elif b_p is not None and a_p is not None:
|
|
285
|
+
if b_p.protocol != a_p.protocol or b_p.required != a_p.required:
|
|
286
|
+
changes.append(
|
|
287
|
+
ContractChange(
|
|
288
|
+
category="network",
|
|
289
|
+
name=f"port:{p_str}",
|
|
290
|
+
change=ContractChangeKind.CHANGED,
|
|
291
|
+
before=f"{b_p.protocol} (required={b_p.required})",
|
|
292
|
+
after=f"{a_p.protocol} (required={a_p.required})",
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# 8. Containers
|
|
297
|
+
b_docker = before.containers.docker.required if before.containers.docker else False
|
|
298
|
+
a_docker = after.containers.docker.required if after.containers.docker else False
|
|
299
|
+
if b_docker != a_docker:
|
|
300
|
+
changes.append(
|
|
301
|
+
ContractChange(
|
|
302
|
+
category="container",
|
|
303
|
+
name="docker",
|
|
304
|
+
change=ContractChangeKind.CHANGED,
|
|
305
|
+
before=f"required={b_docker}",
|
|
306
|
+
after=f"required={a_docker}",
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
b_compose = before.containers.compose.required if before.containers.compose else False
|
|
311
|
+
a_compose = after.containers.compose.required if after.containers.compose else False
|
|
312
|
+
if b_compose != a_compose:
|
|
313
|
+
changes.append(
|
|
314
|
+
ContractChange(
|
|
315
|
+
category="container",
|
|
316
|
+
name="compose",
|
|
317
|
+
change=ContractChangeKind.CHANGED,
|
|
318
|
+
before=f"required={b_compose}",
|
|
319
|
+
after=f"required={a_compose}",
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Compute summary
|
|
324
|
+
added_count = sum(1 for c in changes if c.change == ContractChangeKind.ADDED)
|
|
325
|
+
removed_count = sum(1 for c in changes if c.change == ContractChangeKind.REMOVED)
|
|
326
|
+
changed_count = sum(1 for c in changes if c.change == ContractChangeKind.CHANGED)
|
|
327
|
+
unchanged_count = sum(1 for c in changes if c.change == ContractChangeKind.UNCHANGED)
|
|
328
|
+
|
|
329
|
+
summary = ContractDiffSummary(
|
|
330
|
+
added=added_count,
|
|
331
|
+
removed=removed_count,
|
|
332
|
+
changed=changed_count,
|
|
333
|
+
unchanged=unchanged_count,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
status = (
|
|
337
|
+
"CHANGED"
|
|
338
|
+
if (added_count > 0 or removed_count > 0 or changed_count > 0)
|
|
339
|
+
else "UNCHANGED"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return ContractDiffResult(
|
|
343
|
+
status=status,
|
|
344
|
+
summary=summary,
|
|
345
|
+
changes=changes,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
@classmethod
|
|
349
|
+
def _diff_deps(
|
|
350
|
+
cls,
|
|
351
|
+
cat: str,
|
|
352
|
+
before_deps: dict[str, str],
|
|
353
|
+
after_deps: dict[str, str],
|
|
354
|
+
changes: list[ContractChange],
|
|
355
|
+
) -> None:
|
|
356
|
+
"""Helper to diff package dependencies."""
|
|
357
|
+
all_pkgs = sorted(set(before_deps.keys()) | set(after_deps.keys()))
|
|
358
|
+
for pkg in all_pkgs:
|
|
359
|
+
b_v = before_deps.get(pkg)
|
|
360
|
+
a_v = after_deps.get(pkg)
|
|
361
|
+
if b_v is None and a_v is not None:
|
|
362
|
+
changes.append(
|
|
363
|
+
ContractChange(
|
|
364
|
+
category=cat,
|
|
365
|
+
name=pkg,
|
|
366
|
+
change=ContractChangeKind.ADDED,
|
|
367
|
+
before=None,
|
|
368
|
+
after=a_v,
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
elif b_v is not None and a_v is None:
|
|
372
|
+
changes.append(
|
|
373
|
+
ContractChange(
|
|
374
|
+
category=cat,
|
|
375
|
+
name=pkg,
|
|
376
|
+
change=ContractChangeKind.REMOVED,
|
|
377
|
+
before=b_v,
|
|
378
|
+
after=None,
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
elif b_v != a_v:
|
|
382
|
+
changes.append(
|
|
383
|
+
ContractChange(
|
|
384
|
+
category=cat,
|
|
385
|
+
name=pkg,
|
|
386
|
+
change=ContractChangeKind.CHANGED,
|
|
387
|
+
before=b_v,
|
|
388
|
+
after=a_v,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Contract file discovery utilities for Runmark."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
DEFAULT_CONTRACT_FILENAME = "runmark.json"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def find_contract_file(target_path: Path | str | None = None) -> Path:
|
|
9
|
+
"""Locate the runmark.json contract file in the target directory or path.
|
|
10
|
+
|
|
11
|
+
Raises FileNotFoundError if runmark.json does not exist.
|
|
12
|
+
"""
|
|
13
|
+
if target_path is not None:
|
|
14
|
+
p = Path(target_path).resolve()
|
|
15
|
+
if p.is_file() and p.exists():
|
|
16
|
+
return p
|
|
17
|
+
if p.is_dir():
|
|
18
|
+
candidate = p / DEFAULT_CONTRACT_FILENAME
|
|
19
|
+
if candidate.exists() and candidate.is_file():
|
|
20
|
+
return candidate
|
|
21
|
+
raise FileNotFoundError(
|
|
22
|
+
f"Contract file '{DEFAULT_CONTRACT_FILENAME}' not found in '{p}'."
|
|
23
|
+
)
|
|
24
|
+
raise FileNotFoundError(f"Target path '{p}' does not exist.")
|
|
25
|
+
|
|
26
|
+
# Search in current working directory
|
|
27
|
+
cwd_candidate = Path.cwd() / DEFAULT_CONTRACT_FILENAME
|
|
28
|
+
if cwd_candidate.exists() and cwd_candidate.is_file():
|
|
29
|
+
return cwd_candidate
|
|
30
|
+
|
|
31
|
+
raise FileNotFoundError(
|
|
32
|
+
f"Contract file '{DEFAULT_CONTRACT_FILENAME}' not found in current directory ({Path.cwd()})."
|
|
33
|
+
)
|