marchward 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.
marchward/__init__.py ADDED
@@ -0,0 +1,51 @@
1
+ """
2
+ Marchward Python SDK — runtime authority for AI agents.
3
+
4
+ The wedge persona builds on LangGraph/LangChain (both Python), so this is
5
+ the primary, first-class SDK. Mirrors the `/v1/execute` contract and the
6
+ TypeScript SDK's `execute()` surface.
7
+
8
+ Quickstart
9
+ ----------
10
+ from marchward import MarchwardClient
11
+
12
+ tenet = MarchwardClient(api_key="tnt_...") # or TENET_API_KEY env
13
+
14
+ decision = tenet.execute(
15
+ service="github",
16
+ tool_name="github.repos.delete",
17
+ arguments={"owner": "acme", "repo": "old"},
18
+ context={"env": "production"},
19
+ )
20
+
21
+ if decision.allowed:
22
+ ... # safe to proceed
23
+ elif decision.escalated:
24
+ ... # paused for human approval (decision.review_id)
25
+ elif decision.blocked:
26
+ ... # refused by policy
27
+ """
28
+
29
+ from .client import MarchwardClient
30
+ from .models import Decision, Outcome
31
+ from .errors import MarchwardError, MarchwardAuthError, MarchwardAPIError
32
+
33
+ __all__ = [
34
+ "MarchwardClient",
35
+ "Decision",
36
+ "Outcome",
37
+ "MarchwardError",
38
+ "MarchwardAuthError",
39
+ "MarchwardAPIError",
40
+ ]
41
+
42
+ # ── Back-compat aliases (pre-Marchward names; deprecated, kept one major cycle) ──
43
+ # Existing `from marchward import TenetClient` (or legacy `tenet`) consumers keep working.
44
+ TenetClient = MarchwardClient
45
+ TenetError = MarchwardError
46
+ TenetAuthError = MarchwardAuthError
47
+ TenetAPIError = MarchwardAPIError
48
+
49
+ __all__ += ["TenetClient", "TenetError", "TenetAuthError", "TenetAPIError"]
50
+
51
+ __version__ = "0.1.0"
marchward/client.py ADDED
@@ -0,0 +1,264 @@
1
+ """
2
+ MarchwardClient — the primary Python entry point.
3
+
4
+ Zero runtime dependencies (stdlib urllib) so it drops into any agent
5
+ environment without dependency conflicts. Speaks the Model-B contract:
6
+
7
+ client.execute(service="github", tool_name="github.repos.delete",
8
+ arguments={"owner": "acme", "repo": "old"})
9
+
10
+ You send a logical tool call (service + tool_name + arguments); Marchward
11
+ resolves the real downstream HTTP request, governs it, injects the
12
+ credential server-side, executes it, and returns the result. The agent
13
+ never holds downstream credentials.
14
+
15
+ Response model (matches /v1/execute):
16
+ 403 BLOCK -> Decision(blocked) [immediate]
17
+ 202 ESCALATE + reviewId -> Decision(escalated) [immediate]
18
+ 202 ALLOW + jobId -> poll GET /v1/jobs/:id until terminal,
19
+ then Decision(allowed) with .execution set
20
+ 401 -> MarchwardAuthError
21
+ 5xx / unknown_tool etc. -> MarchwardAPIError / surfaced on the Decision
22
+
23
+ ALLOW is asynchronous server-side (Marchward fires the downstream in the
24
+ background and hands back a jobId). By default execute() polls that job to
25
+ completion so callers get the result synchronously; pass wait=False to get
26
+ the pending Decision back immediately and poll yourself.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ import os
33
+ import time
34
+ import uuid
35
+ import urllib.request
36
+ import urllib.error
37
+ from typing import Any
38
+
39
+ from .models import Decision, Outcome
40
+ from .errors import MarchwardAuthError, MarchwardAPIError
41
+
42
+ # Identify as the Marchward SDK, not the stdlib default. urllib's default
43
+ # `Python-urllib/3.x` User-Agent is fingerprinted and BANNED by Cloudflare's
44
+ # browser-integrity check (returns "error code: 1010" — the request never
45
+ # reaches the API). A normal product UA passes. This matters for real
46
+ # customers too: agent SDKs on stdlib HTTP must not look like a bot.
47
+ _USER_AGENT = "tenet-python-sdk/0.1"
48
+ _DEFAULT_API_URL = "https://api.trytenet.com"
49
+ _DEFAULT_TIMEOUT = 30.0
50
+ _DEFAULT_POLL_TIMEOUT = 120.0
51
+ _DEFAULT_POLL_INTERVAL = 0.75
52
+
53
+
54
+ class MarchwardClient:
55
+ """Client for the Marchward runtime-authority API.
56
+
57
+ Args:
58
+ api_key: Your tenant API key (``tnt_...``). Falls back to the
59
+ ``TENET_API_KEY`` env var.
60
+ api_url: API base URL. Falls back to ``TENET_API_URL`` env var,
61
+ then the production default.
62
+ default_agent_id: Agent identity attached to every call. Defaults
63
+ to ``"default"`` — the agent every tenant auto-provisions and
64
+ that the dashboard binds connected services to. Override only
65
+ if you created a differently-named agent and bound your
66
+ credentials to it (otherwise credential resolution won't find
67
+ a binding and calls return 403 credential_not_found).
68
+ timeout: Per-request timeout in seconds.
69
+ poll_timeout: Max seconds to wait for an async ALLOW job to finish.
70
+ poll_interval: Seconds between job polls.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ api_key: str | None = None,
76
+ *,
77
+ api_url: str | None = None,
78
+ default_agent_id: str = "default",
79
+ timeout: float = _DEFAULT_TIMEOUT,
80
+ poll_timeout: float = _DEFAULT_POLL_TIMEOUT,
81
+ poll_interval: float = _DEFAULT_POLL_INTERVAL,
82
+ ) -> None:
83
+ self.api_key = api_key or (os.environ.get("MARCHWARD_API_KEY") or os.environ.get("TENET_API_KEY"))
84
+ if not self.api_key:
85
+ raise MarchwardAuthError(
86
+ "No API key. Pass api_key=... or set the MARCHWARD_API_KEY (or legacy TENET_API_KEY) env var."
87
+ )
88
+ self.api_url = (api_url or (os.environ.get("MARCHWARD_API_URL") or os.environ.get("TENET_API_URL")) or _DEFAULT_API_URL).rstrip("/")
89
+ self.default_agent_id = default_agent_id
90
+ self.timeout = timeout
91
+ self.poll_timeout = poll_timeout
92
+ self.poll_interval = poll_interval
93
+
94
+ # ──────────────────────────────────────────────────────────────────
95
+ def execute(
96
+ self,
97
+ *,
98
+ service: str,
99
+ tool_name: str,
100
+ arguments: dict[str, Any] | None = None,
101
+ context: dict[str, Any] | None = None,
102
+ agent_id: str | None = None,
103
+ request_id: str | None = None,
104
+ wait: bool = True,
105
+ ) -> Decision:
106
+ """Authorize + execute a tool call through Marchward (Model B).
107
+
108
+ Sends `service` + `tool_name` + `arguments`; Marchward resolves the
109
+ downstream call from its tool catalog. Returns a :class:`Decision`.
110
+ Never raises on a normal governance outcome (ALLOW/ESCALATE/BLOCK)
111
+ — only on auth/transport errors.
112
+
113
+ When the outcome is ALLOW, the downstream runs asynchronously
114
+ server-side. With ``wait=True`` (default) this polls the job to
115
+ completion and attaches the result to ``decision.execution``. With
116
+ ``wait=False`` you get the pending Decision immediately (poll the
117
+ job yourself via ``get_job(decision.job_id)``).
118
+ """
119
+ rid = request_id or str(uuid.uuid4())
120
+ body = json.dumps(
121
+ {
122
+ "requestId": rid,
123
+ "service": service,
124
+ "toolCall": {"toolName": tool_name, "arguments": arguments or {}},
125
+ "agent": {"agentId": agent_id or self.default_agent_id},
126
+ "context": context or {},
127
+ }
128
+ ).encode()
129
+
130
+ payload, status = self._post("/v1/execute", body, idempotency_key=rid)
131
+ decision = self._to_decision(payload, status)
132
+
133
+ # ALLOW arrives as 202 + jobId (async downstream). Poll unless the
134
+ # caller opted out. ESCALATE/BLOCK are terminal and have no job.
135
+ job_id = payload.get("jobId") if isinstance(payload, dict) else None
136
+ decision.job_id = job_id if isinstance(job_id, str) else None
137
+ if wait and decision.job_id and status == 202 and decision.allowed:
138
+ return self._await_job(decision)
139
+
140
+ return decision
141
+
142
+ # ──────────────────────────────────────────────────────────────────
143
+ def get_job(self, job_id: str) -> dict[str, Any]:
144
+ """Poll a single async job once. Returns the raw job payload
145
+ (`status` is one of pending / running / completed / failed)."""
146
+ payload, _ = self._get(f"/v1/jobs/{job_id}")
147
+ return payload if isinstance(payload, dict) else {}
148
+
149
+ # ── Internals ──────────────────────────────────────────────────────
150
+ def _await_job(self, decision: Decision) -> Decision:
151
+ """Poll GET /v1/jobs/:id until the downstream call terminates,
152
+ then fold the result into the Decision."""
153
+ assert decision.job_id is not None
154
+ deadline = time.monotonic() + self.poll_timeout
155
+ while True:
156
+ payload, status = self._get(f"/v1/jobs/{decision.job_id}")
157
+ job_status = payload.get("status") if isinstance(payload, dict) else None
158
+
159
+ if job_status in ("completed", "failed"):
160
+ decision.http_status = status
161
+ decision.raw = payload
162
+ if job_status == "completed":
163
+ decision.execution = payload.get("execution")
164
+ else:
165
+ decision.execution_error = payload.get("executionError") or "downstream_failed"
166
+ return decision
167
+
168
+ if time.monotonic() >= deadline:
169
+ decision.execution_error = "poll_timeout"
170
+ return decision
171
+
172
+ time.sleep(self.poll_interval)
173
+
174
+ def _post(self, path: str, body: bytes, *, idempotency_key: str | None = None):
175
+ headers = {
176
+ "Content-Type": "application/json",
177
+ "Authorization": f"Bearer {self.api_key}",
178
+ "User-Agent": _USER_AGENT,
179
+ }
180
+ if idempotency_key:
181
+ headers["Idempotency-Key"] = idempotency_key
182
+ req = urllib.request.Request(f"{self.api_url}{path}", data=body, headers=headers, method="POST")
183
+ return self._send(req)
184
+
185
+ def _get(self, path: str):
186
+ req = urllib.request.Request(
187
+ f"{self.api_url}{path}",
188
+ headers={"Authorization": f"Bearer {self.api_key}", "User-Agent": _USER_AGENT},
189
+ method="GET",
190
+ )
191
+ return self._send(req)
192
+
193
+ def _send(self, req: urllib.request.Request):
194
+ _debug = (os.environ.get("MARCHWARD_DEBUG") or os.environ.get("TENET_DEBUG"))
195
+ if _debug:
196
+ body_preview = (req.data or b"").decode("utf-8", "replace")[:300]
197
+ print(f"[tenet] → {req.method} {req.full_url}")
198
+ print(f"[tenet] headers: { {k: ('***' if k.lower()=='authorization' else v) for k,v in req.headers.items()} }")
199
+ print(f"[tenet] body: {body_preview}")
200
+ try:
201
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
202
+ payload = json.loads(resp.read() or b"{}")
203
+ if _debug:
204
+ print(f"[tenet] ← {resp.status} {json.dumps(payload)[:400]}")
205
+ return payload, resp.status
206
+ except urllib.error.HTTPError as e:
207
+ raw = e.read() or b"{}"
208
+ try:
209
+ payload = json.loads(raw)
210
+ except json.JSONDecodeError:
211
+ payload = {}
212
+ status = e.code
213
+ if _debug:
214
+ print(f"[tenet] ← {status} (raw: {raw.decode('utf-8','replace')[:400]})")
215
+ if status == 401:
216
+ raise MarchwardAuthError("Marchward rejected the API key (401).") from e
217
+ if status >= 500:
218
+ raise MarchwardAPIError(
219
+ f"Marchward API error ({status}).", status=status, body=raw.decode("utf-8", "replace")
220
+ ) from e
221
+ # 4xx governance outcomes (403 BLOCK) + resolver 4xx (400
222
+ # unknown_tool etc.) arrive here as HTTPError; fall through so
223
+ # the caller sees them on the Decision rather than as an
224
+ # exception.
225
+ return payload, status
226
+ except urllib.error.URLError as e:
227
+ raise MarchwardAPIError(f"Could not reach Marchward at {self.api_url}: {e}") from e
228
+
229
+ # ──────────────────────────────────────────────────────────────────
230
+ @staticmethod
231
+ def _to_decision(payload: dict[str, Any], status: int) -> Decision:
232
+ # The API returns the outcome either at the top level (`decision`
233
+ # string + `decisionId`) or nested under `decision`; handle both.
234
+ decision_obj = payload.get("decision")
235
+ if isinstance(decision_obj, dict):
236
+ outcome_str = decision_obj.get("outcome") or decision_obj.get("decision")
237
+ decision_id = decision_obj.get("decisionId") or decision_obj.get("id")
238
+ review_id = decision_obj.get("reviewId")
239
+ reasons = decision_obj.get("reasonCodes") or decision_obj.get("reason_codes") or []
240
+ else:
241
+ outcome_str = decision_obj if isinstance(decision_obj, str) else payload.get("outcome")
242
+ decision_id = payload.get("decisionId")
243
+ review_id = payload.get("reviewId")
244
+ reasons = payload.get("reasonCodes") or payload.get("reason_codes") or []
245
+
246
+ try:
247
+ outcome = Outcome(outcome_str)
248
+ except (ValueError, TypeError):
249
+ # No recognizable outcome — infer from HTTP status as a fallback.
250
+ outcome = {200: Outcome.ALLOW, 202: Outcome.ESCALATE, 403: Outcome.BLOCK}.get(
251
+ status, Outcome.BLOCK
252
+ )
253
+
254
+ return Decision(
255
+ outcome=outcome,
256
+ decision_id=decision_id,
257
+ review_id=review_id,
258
+ reason_codes=list(reasons),
259
+ http_status=status,
260
+ raw=payload,
261
+ execution_error=(
262
+ payload.get("executionError") if isinstance(payload, dict) else None
263
+ ),
264
+ )
marchward/errors.py ADDED
@@ -0,0 +1,20 @@
1
+ """Marchward SDK exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class MarchwardError(Exception):
7
+ """Base class for all Marchward SDK errors."""
8
+
9
+
10
+ class MarchwardAuthError(MarchwardError):
11
+ """Raised on a 401 — the API key is missing, invalid, or revoked."""
12
+
13
+
14
+ class MarchwardAPIError(MarchwardError):
15
+ """Raised on an unexpected API response (5xx, malformed body, etc.)."""
16
+
17
+ def __init__(self, message: str, *, status: int | None = None, body: str | None = None) -> None:
18
+ super().__init__(message)
19
+ self.status = status
20
+ self.body = body
marchward/models.py ADDED
@@ -0,0 +1,69 @@
1
+ """Typed result objects for Marchward decisions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+
10
+ class Outcome(str, Enum):
11
+ """The governance outcome for a tool call."""
12
+
13
+ ALLOW = "ALLOW"
14
+ ALLOW_WITH_CONDITIONS = "ALLOW_WITH_CONDITIONS"
15
+ ESCALATE = "ESCALATE"
16
+ BLOCK = "BLOCK"
17
+
18
+
19
+ @dataclass
20
+ class Decision:
21
+ """The result of a `tenet.execute()` call.
22
+
23
+ The boolean helpers (`allowed` / `escalated` / `blocked`) are the
24
+ ergonomic surface most agent code branches on; the raw fields are
25
+ there when you need them.
26
+ """
27
+
28
+ outcome: Outcome
29
+ decision_id: str | None = None
30
+ review_id: str | None = None
31
+ reason_codes: list[str] = field(default_factory=list)
32
+ http_status: int = 0
33
+ raw: dict[str, Any] = field(default_factory=dict)
34
+ # ── Model-B async-execute fields ────────────────────────────────────
35
+ # For ALLOW, the downstream runs asynchronously: `job_id` identifies the
36
+ # async job; after the SDK polls it to completion, `execution` holds the
37
+ # downstream result ({status, headers, body, durationMs}). On a failed
38
+ # downstream (or unmet binding / poll timeout), `execution_error` is set.
39
+ job_id: str | None = None
40
+ execution: dict[str, Any] | None = None
41
+ execution_error: str | None = None
42
+
43
+ # ── Ergonomic branch helpers ───────────────────────────────────────
44
+ @property
45
+ def allowed(self) -> bool:
46
+ return self.outcome in (Outcome.ALLOW, Outcome.ALLOW_WITH_CONDITIONS)
47
+
48
+ @property
49
+ def escalated(self) -> bool:
50
+ return self.outcome == Outcome.ESCALATE
51
+
52
+ @property
53
+ def blocked(self) -> bool:
54
+ return self.outcome == Outcome.BLOCK
55
+
56
+ @property
57
+ def executed(self) -> bool:
58
+ """True only if the downstream actually ran (ALLOW + a completed
59
+ execution with no error). An ALLOW with no connected credential, a
60
+ failed downstream, or a still-pending job is NOT executed."""
61
+ return self.allowed and self.execution is not None and self.execution_error is None
62
+
63
+ def __str__(self) -> str:
64
+ bits = [self.outcome.value]
65
+ if self.review_id:
66
+ bits.append(f"review={self.review_id}")
67
+ if self.reason_codes:
68
+ bits.append(f"reasons={','.join(self.reason_codes)}")
69
+ return f"Decision({' '.join(bits)})"
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: marchward
3
+ Version: 0.1.0
4
+ Summary: Tenet — runtime authority for AI agents. Gate every tool call through cost caps, approval gates, and a tamper-evident audit log.
5
+ Project-URL: Homepage, https://trytenet.com
6
+ Project-URL: Documentation, https://trytenet.com/docs
7
+ Project-URL: Source, https://github.com/trytenet/tenet-python
8
+ Project-URL: Changelog, https://github.com/trytenet/tenet-python/releases
9
+ Author-email: Tenet <team@trytenet.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,ai,audit,cost-cap,governance,guardrails,human-in-the-loop,langchain,langgraph
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Marchward — Python SDK
27
+
28
+ Runtime authority for AI agents. Gate every tool call through a cost cap,
29
+ approval gates on irreversible actions, and a tamper-evident audit log.
30
+
31
+ **Python is the primary Marchward SDK** — the wedge persona builds on
32
+ LangGraph / LangChain (both Python). Zero runtime dependencies (stdlib only).
33
+
34
+ ## Install
35
+ ```bash
36
+ pip install tenet-python
37
+ ```
38
+
39
+ ## Quickstart
40
+ ```python
41
+ from tenet import TenetClient
42
+
43
+ tenet = TenetClient(api_key="tnt_...") # or set TENET_API_KEY
44
+
45
+ decision = tenet.execute(
46
+ service="github",
47
+ tool_name="github.repos.delete",
48
+ arguments={"owner": "acme", "repo": "old-experiment"},
49
+ context={"env": "production"},
50
+ )
51
+
52
+ if decision.allowed:
53
+ do_the_delete()
54
+ elif decision.escalated:
55
+ print(f"Paused for approval — review {decision.review_id}")
56
+ elif decision.blocked:
57
+ print(f"Blocked: {decision.reason_codes}")
58
+ ```
59
+
60
+ ## With LangGraph (the persona's stack)
61
+ ```python
62
+ from langchain_core.tools import tool
63
+ from tenet import TenetClient
64
+
65
+ tenet = TenetClient()
66
+
67
+ @tool
68
+ def delete_repo(owner: str, repo: str) -> str:
69
+ """Delete a GitHub repository."""
70
+ d = tenet.execute(service="github", tool_name="github.repos.delete",
71
+ arguments={"owner": owner, "repo": repo})
72
+ if not d.allowed:
73
+ return f"Refused by Marchward ({d.outcome.value})."
74
+ # ... real delete here ...
75
+ return "deleted"
76
+ ```
77
+
78
+ ## How it works (Model B)
79
+ You send a logical tool call — `service` + `tool_name` + `arguments`. Marchward
80
+ resolves the real downstream HTTP request from its tool catalog, governs it,
81
+ injects your stored credential server-side, and executes it. Your agent holds
82
+ only `TENET_API_KEY`; it never touches downstream credentials. Connect those
83
+ once in the dashboard (Settings → Connected services).
84
+
85
+ ## API
86
+ - `TenetClient(api_key=None, *, api_url=None, default_agent_id="python-sdk", timeout=30.0, poll_timeout=120.0, poll_interval=0.75)`
87
+ - `.execute(*, service, tool_name, arguments=None, context=None, agent_id=None, request_id=None, wait=True) -> Decision`
88
+ - `.get_job(job_id) -> dict` — poll one async job manually (for `wait=False`).
89
+ - `Decision`: `.allowed` / `.escalated` / `.blocked` / `.executed`, plus `.outcome`,
90
+ `.decision_id`, `.review_id`, `.reason_codes`, `.http_status`, `.raw`,
91
+ `.job_id`, `.execution`, `.execution_error`.
92
+
93
+ ## Contract
94
+ | HTTP | Outcome | Meaning |
95
+ |---|---|---|
96
+ | 200/202 + jobId | ALLOW | authorized; downstream runs async — the SDK polls the job and fills `.execution` (set `wait=False` to poll yourself) |
97
+ | 202 + reviewId | ESCALATE | held for human approval; auto-executes on approve |
98
+ | 403 | BLOCK | refused by policy |
99
+ | 401 | — | `TenetAuthError` (bad/missing/revoked key) |
100
+
101
+ `.executed` is `True` only when an ALLOW actually ran its downstream — an
102
+ ALLOW with no connected credential, a failed downstream, or a still-pending
103
+ job is allowed-but-not-executed.
104
+
105
+ ## Risk classification
106
+ Risk is classified by the **resolved HTTP method**, not the tool name — any
107
+ `DELETE` (or a flagged destructive `POST` like `stripe.charges.create`) is
108
+ treated as irreversible and gated, regardless of what the tool is named. So a
109
+ custom-named destructive tool can't slip past the approval gate.
110
+
111
+ ## Tests
112
+ ```bash
113
+ cd packages/sdk-python && python -m unittest discover -s tests
114
+ ```
@@ -0,0 +1,8 @@
1
+ marchward/__init__.py,sha256=lfvtK8lBOx13KRNjcM2ZEhHpsWwsPgDEWTMmxRPryaE,1495
2
+ marchward/client.py,sha256=CDn9gWhYwc3i7j4Kh8Kw8GvS0OI6rwYH3AQ60HoE31I,12330
3
+ marchward/errors.py,sha256=lJS2Y9PqskP4uhL82hDj712Y2CSaIXe94Ce1aIAxRuY,582
4
+ marchward/models.py,sha256=CGye295YbHDm_4e61BtAee03GLkfvxEqVzz7WXRykMI,2536
5
+ marchward-0.1.0.dist-info/METADATA,sha256=4nbFe3bWgToCUlPyjDSZChFAJi4VecQUj4GtXXSwn7U,4463
6
+ marchward-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ marchward-0.1.0.dist-info/licenses/LICENSE,sha256=UbfFi7BOqNW_2AixHhoot6oQ0ohuoMiA2aw0S92M8HI,1062
8
+ marchward-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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tenet
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.