agent-framework-botsrated 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.
- agent_framework_botsrated/__init__.py +28 -0
- agent_framework_botsrated/_middleware.py +168 -0
- agent_framework_botsrated/py.typed +0 -0
- agent_framework_botsrated-0.1.0.dist-info/METADATA +127 -0
- agent_framework_botsrated-0.1.0.dist-info/RECORD +7 -0
- agent_framework_botsrated-0.1.0.dist-info/WHEEL +4 -0
- agent_framework_botsrated-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""BotsRated pre-payment checks for Microsoft Agent Framework (Python)."""
|
|
2
|
+
|
|
3
|
+
from agent_framework_botsrated._middleware import (
|
|
4
|
+
DECISION_METADATA_KEY,
|
|
5
|
+
BotsRatedPaymentBlocked,
|
|
6
|
+
BotsRatedPaymentMiddleware,
|
|
7
|
+
PaymentQuote,
|
|
8
|
+
QuoteExtractor,
|
|
9
|
+
botsrated_check_payment_tool,
|
|
10
|
+
decision_to_dict,
|
|
11
|
+
default_quote_extractor,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
BOTSRATED_MCP_URL = "https://botsrated.com/mcp"
|
|
15
|
+
"""BotsRated's remote MCP server (streamable HTTP, no auth). Free tools: preflight, coverage, sample, history."""
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"BOTSRATED_MCP_URL",
|
|
19
|
+
"DECISION_METADATA_KEY",
|
|
20
|
+
"BotsRatedPaymentBlocked",
|
|
21
|
+
"BotsRatedPaymentMiddleware",
|
|
22
|
+
"PaymentQuote",
|
|
23
|
+
"QuoteExtractor",
|
|
24
|
+
"botsrated_check_payment_tool",
|
|
25
|
+
"decision_to_dict",
|
|
26
|
+
"default_quote_extractor",
|
|
27
|
+
]
|
|
28
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""BotsRated pre-payment check for Microsoft Agent Framework function tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Awaitable, Callable, Collection, Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Annotated, Any, Literal
|
|
10
|
+
|
|
11
|
+
from agent_framework import FunctionInvocationContext, FunctionMiddleware, FunctionTool, MiddlewareFailure, tool
|
|
12
|
+
from botsrated import BotsRatedClient, Decision, PrePaymentGuard, Reason
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
DECISION_METADATA_KEY = "botsrated_decision"
|
|
16
|
+
"""Key of the ``botsrated.Decision`` stored in ``FunctionInvocationContext.metadata`` for checked calls."""
|
|
17
|
+
|
|
18
|
+
DEFAULT_NETWORK = "eip155:8453"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class PaymentQuote:
|
|
23
|
+
"""The x402 payment option a payment tool is about to pay."""
|
|
24
|
+
|
|
25
|
+
url: str
|
|
26
|
+
pay_to: str
|
|
27
|
+
amount: str
|
|
28
|
+
network: str = DEFAULT_NETWORK
|
|
29
|
+
scheme: str = "exact"
|
|
30
|
+
|
|
31
|
+
def requirements(self) -> dict[str, str]:
|
|
32
|
+
return {"scheme": self.scheme, "network": self.network, "payTo": self.pay_to, "amount": self.amount}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
QuoteExtractor = Callable[[Mapping[str, Any]], PaymentQuote]
|
|
36
|
+
"""Reads the payment quote from a payment tool's arguments. Raise any exception if it can't."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _first(args: Mapping[str, Any], *names: str) -> Any:
|
|
40
|
+
for name in names:
|
|
41
|
+
value = args.get(name)
|
|
42
|
+
if value not in (None, ""):
|
|
43
|
+
return value
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def default_quote_extractor(args: Mapping[str, Any]) -> PaymentQuote:
|
|
48
|
+
"""Read ``url``, ``pay_to`` (or ``payTo``), ``amount`` and optional ``network``/``scheme`` arguments."""
|
|
49
|
+
url = _first(args, "url", "resource_url", "resource")
|
|
50
|
+
pay_to = _first(args, "pay_to", "payTo")
|
|
51
|
+
amount = _first(args, "amount", "amount_atomic", "max_amount_required", "maxAmountRequired")
|
|
52
|
+
if not isinstance(url, str) or not isinstance(pay_to, str) or amount is None:
|
|
53
|
+
missing = [n for n, v in (("url", url), ("pay_to", pay_to), ("amount", amount)) if v is None]
|
|
54
|
+
raise ValueError(f"payment arguments are missing or not text: {', '.join(missing) or 'url/pay_to'}")
|
|
55
|
+
if isinstance(amount, bool) or not str(amount).isdigit():
|
|
56
|
+
raise ValueError("amount must be an integer number of atomic units")
|
|
57
|
+
network = _first(args, "network") or DEFAULT_NETWORK
|
|
58
|
+
scheme = _first(args, "scheme") or "exact"
|
|
59
|
+
return PaymentQuote(url=url, pay_to=pay_to, amount=str(amount), network=str(network), scheme=str(scheme))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BotsRatedPaymentBlocked(MiddlewareFailure):
|
|
63
|
+
"""Raised for a blocked payment when the middleware uses ``on_block="raise"``. It ends the run."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, decision: Decision) -> None:
|
|
66
|
+
super().__init__(decision.summary())
|
|
67
|
+
self.decision = decision
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _as_mapping(arguments: BaseModel | Mapping[str, Any]) -> Mapping[str, Any]:
|
|
71
|
+
if isinstance(arguments, BaseModel):
|
|
72
|
+
return arguments.model_dump(mode="json")
|
|
73
|
+
return arguments
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BotsRatedPaymentMiddleware(FunctionMiddleware):
|
|
77
|
+
"""Function middleware that checks an x402 payment with BotsRated before a payment tool runs.
|
|
78
|
+
|
|
79
|
+
Only the functions named in ``payment_tools`` are checked; every other call passes through
|
|
80
|
+
untouched. For a payment tool, the middleware reads the quote (URL, payTo, amount, network)
|
|
81
|
+
from the call's arguments and asks BotsRated's free preflight (no key, no wallet) whether
|
|
82
|
+
that payTo was observed for the seller. A blocked call never reaches ``call_next``: the
|
|
83
|
+
reason becomes the call's result, so the model reads it, or with ``on_block="raise"`` the
|
|
84
|
+
run ends with ``BotsRatedPaymentBlocked``.
|
|
85
|
+
|
|
86
|
+
Default policy (from ``botsrated.GuardPolicy``): a payTo that BotsRated has not seen for the
|
|
87
|
+
host is blocked; an uncovered host or stale data is allowed; a price above the observed one
|
|
88
|
+
is a warning. If BotsRated can't be reached, the payment is allowed (``fail_open=True`` on
|
|
89
|
+
the guard). If the quote can't be read from the arguments, the call is blocked.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
payment_tools: Names of the payment functions to check, or a mapping from name to a
|
|
93
|
+
``QuoteExtractor`` (``None`` uses ``default_quote_extractor``).
|
|
94
|
+
guard: A ``botsrated.PrePaymentGuard``. Defaults to a free-only guard (nothing is paid).
|
|
95
|
+
on_block: ``"result"`` (default) returns the reason as the call's result; ``"raise"`` ends the run.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
payment_tools: Collection[str] | Mapping[str, QuoteExtractor | None],
|
|
101
|
+
*,
|
|
102
|
+
guard: PrePaymentGuard | None = None,
|
|
103
|
+
on_block: Literal["result", "raise"] = "result",
|
|
104
|
+
) -> None:
|
|
105
|
+
if isinstance(payment_tools, str):
|
|
106
|
+
payment_tools = [payment_tools]
|
|
107
|
+
if isinstance(payment_tools, Mapping):
|
|
108
|
+
self.extractors: dict[str, QuoteExtractor] = {
|
|
109
|
+
name: extractor or default_quote_extractor for name, extractor in payment_tools.items()
|
|
110
|
+
}
|
|
111
|
+
else:
|
|
112
|
+
self.extractors = {name: default_quote_extractor for name in payment_tools}
|
|
113
|
+
if not self.extractors:
|
|
114
|
+
raise ValueError("payment_tools must name at least one payment function")
|
|
115
|
+
self.guard = guard or PrePaymentGuard(BotsRatedClient())
|
|
116
|
+
self.on_block = on_block
|
|
117
|
+
|
|
118
|
+
async def process(
|
|
119
|
+
self,
|
|
120
|
+
context: FunctionInvocationContext,
|
|
121
|
+
call_next: Callable[[], Awaitable[None]],
|
|
122
|
+
) -> None:
|
|
123
|
+
extractor = self.extractors.get(context.function.name)
|
|
124
|
+
if extractor is None:
|
|
125
|
+
await call_next()
|
|
126
|
+
return
|
|
127
|
+
try:
|
|
128
|
+
quote = extractor(_as_mapping(context.arguments))
|
|
129
|
+
except Exception as error: # noqa: BLE001 - any unreadable quote fails closed
|
|
130
|
+
decision = Decision("block", None, [Reason("quote_unreadable", str(error), "block")])
|
|
131
|
+
else:
|
|
132
|
+
decision = await asyncio.to_thread(self.guard.decide, quote.url, quote.requirements())
|
|
133
|
+
context.metadata[DECISION_METADATA_KEY] = decision
|
|
134
|
+
if self.guard.on_decision:
|
|
135
|
+
self.guard.on_decision(decision)
|
|
136
|
+
if decision.action == "allow":
|
|
137
|
+
await call_next()
|
|
138
|
+
return
|
|
139
|
+
if self.on_block == "raise":
|
|
140
|
+
raise BotsRatedPaymentBlocked(decision)
|
|
141
|
+
context.result = f"Payment tool '{context.function.name}' was not executed. {decision.summary()}"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def decision_to_dict(decision: Decision) -> dict[str, Any]:
|
|
145
|
+
return {
|
|
146
|
+
"action": decision.action,
|
|
147
|
+
"host": decision.host,
|
|
148
|
+
"reasons": [{"code": r.code, "detail": r.detail, "effect": r.effect} for r in decision.reasons],
|
|
149
|
+
"receipt_url": decision.facts.receipt_url if decision.facts else None,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def botsrated_check_payment_tool(guard: PrePaymentGuard | None = None) -> FunctionTool:
|
|
154
|
+
"""A function tool the model can call to check an x402 payment option before paying it (free preflight)."""
|
|
155
|
+
checker = guard or PrePaymentGuard(BotsRatedClient())
|
|
156
|
+
|
|
157
|
+
async def botsrated_check_payment(
|
|
158
|
+
url: Annotated[str, Field(description="The seller URL you are about to pay.")],
|
|
159
|
+
pay_to: Annotated[str, Field(description="The payTo address from the seller's 402 payment option.")],
|
|
160
|
+
amount: Annotated[str, Field(description="The amount in atomic units from that option.")],
|
|
161
|
+
network: Annotated[str, Field(description="The CAIP-2 network of that option.")] = DEFAULT_NETWORK,
|
|
162
|
+
) -> str:
|
|
163
|
+
"""Check an x402 payment option against BotsRated's measurements BEFORE paying it."""
|
|
164
|
+
quote = PaymentQuote(url=url, pay_to=pay_to, amount=amount, network=network)
|
|
165
|
+
decision = await asyncio.to_thread(checker.decide, quote.url, quote.requirements())
|
|
166
|
+
return json.dumps(decision_to_dict(decision))
|
|
167
|
+
|
|
168
|
+
return tool(botsrated_check_payment)
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agent-framework-botsrated
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: BotsRated pre-payment check for Microsoft Agent Framework: function middleware that checks an x402 payTo with the free BotsRated preflight before a payment tool runs.
|
|
5
|
+
Project-URL: Homepage, https://botsrated.com
|
|
6
|
+
Project-URL: Repository, https://github.com/botsrated/botsrated
|
|
7
|
+
Project-URL: Documentation, https://github.com/botsrated/botsrated/blob/main/integrations/microsoft-agent-framework/README.md
|
|
8
|
+
Project-URL: Issues, https://github.com/botsrated/botsrated/issues
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent-framework,base,botsrated,microsoft-agent-framework,middleware,payments,usdc,x402
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: agent-framework-core<2,>=1.22
|
|
14
|
+
Requires-Dist: botsrated<0.2,>=0.1.2
|
|
15
|
+
Provides-Extra: mcp
|
|
16
|
+
Requires-Dist: mcp<3,>=1.24; extra == 'mcp'
|
|
17
|
+
Requires-Dist: tenacity<10,>=9.1.4; extra == 'mcp'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# agent-framework-botsrated: BotsRated pre-payment check for Microsoft Agent Framework (Python)
|
|
21
|
+
|
|
22
|
+
Function middleware for [Microsoft Agent Framework](https://github.com/microsoft/agent-framework) that checks an
|
|
23
|
+
x402 payment with [BotsRated](https://botsrated.com) **before a payment tool runs**. BotsRated probes x402 sellers on
|
|
24
|
+
Base and records which `payTo` wallet each one actually quoted. The middleware asks BotsRated's free preflight
|
|
25
|
+
(no API key, no wallet) whether the `payTo` your agent is about to pay was observed for that seller, and stops the call
|
|
26
|
+
if it wasn't.
|
|
27
|
+
|
|
28
|
+
Maintained by BotsRated, not by Microsoft. Issues: https://github.com/botsrated/botsrated/issues/new
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install "agent-framework-botsrated @ git+https://github.com/botsrated/botsrated#subdirectory=integrations/microsoft-agent-framework"
|
|
34
|
+
# with the hosted MCP server example:
|
|
35
|
+
pip install "agent-framework-botsrated[mcp] @ git+https://github.com/botsrated/botsrated#subdirectory=integrations/microsoft-agent-framework"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Python 3.10+, `agent-framework-core>=1.22,<2` and [`botsrated`](https://pypi.org/project/botsrated/) 0.1.2+.
|
|
39
|
+
Tested with agent-framework-core 1.22.0 (with mcp 1.30) and 1.23.0 (with mcp 2.3). agent-framework-core 1.22 can't
|
|
40
|
+
initialize MCP sessions with mcp 2.x, so pin `mcp<2` there if you use the hosted MCP server.
|
|
41
|
+
|
|
42
|
+
## Middleware: check payments before they run
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from agent_framework import Agent
|
|
46
|
+
from agent_framework_botsrated import BotsRatedPaymentMiddleware
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def pay_x402(url: str, pay_to: str, amount: str, network: str = "eip155:8453") -> str:
|
|
50
|
+
"""Pay an x402 seller."""
|
|
51
|
+
... # your payment code
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
agent = Agent(
|
|
55
|
+
client=chat_client,
|
|
56
|
+
tools=[pay_x402],
|
|
57
|
+
middleware=[BotsRatedPaymentMiddleware(["pay_x402"])],
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- Only the functions you name are checked. Every other function call passes through untouched.
|
|
62
|
+
- The quote is read from the call's arguments: `url`, `pay_to` (or `payTo`), `amount` (atomic units) and optional
|
|
63
|
+
`network` (CAIP-2, default `eip155:8453`). For other argument names pass an extractor:
|
|
64
|
+
`BotsRatedPaymentMiddleware({"buy": lambda a: PaymentQuote(url=a["endpoint"], pay_to=a["recipient"], amount=str(a["atomic"]))})`.
|
|
65
|
+
- A blocked call never reaches `call_next`. The reason (with a link to the BotsRated probe receipt) becomes the call's
|
|
66
|
+
result, so the model reads it. With `on_block="raise"` the run ends with `BotsRatedPaymentBlocked` (a
|
|
67
|
+
`MiddlewareFailure`).
|
|
68
|
+
- The `botsrated.Decision` is stored in `context.metadata["botsrated_decision"]`; `guard.on_decision` is also called.
|
|
69
|
+
|
|
70
|
+
Default policy (from `botsrated.GuardPolicy`, configurable through `guard=PrePaymentGuard(policy=...)`):
|
|
71
|
+
|
|
72
|
+
| Situation | Result |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `payTo` never observed by BotsRated for that host (same network) | **blocked** (`payto_mismatch`) |
|
|
75
|
+
| Quote can't be read from the arguments | **blocked** (`quote_unreadable`) |
|
|
76
|
+
| Price above the last observed price on the same route | allowed with a warning |
|
|
77
|
+
| Host not covered, or data older than 24 h | allowed (info) |
|
|
78
|
+
| BotsRated unreachable | allowed (`fail_open=True`); set `PrePaymentGuard(fail_open=False)` to block |
|
|
79
|
+
|
|
80
|
+
The middleware checks what the payment tool was asked to pay. If your tool fetches the 402 itself and signs it, also
|
|
81
|
+
register `botsrated.PrePaymentGuard().x402_hook` on your `x402Client`'s `on_before_payment_creation` so the check
|
|
82
|
+
runs on the exact quote being signed.
|
|
83
|
+
|
|
84
|
+
## Tool: let the model check a payment option
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from agent_framework_botsrated import botsrated_check_payment_tool
|
|
88
|
+
|
|
89
|
+
agent = Agent(client=chat_client, tools=[botsrated_check_payment_tool(), pay_x402])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`botsrated_check_payment(url, pay_to, amount, network)` returns JSON with `action` (`allow`/`block`), `reasons` and
|
|
93
|
+
`receipt_url`. It uses the free preflight only.
|
|
94
|
+
|
|
95
|
+
## Hosted MCP server (no package needed)
|
|
96
|
+
|
|
97
|
+
BotsRated also runs a remote MCP server (streamable HTTP, no auth). Its free tools are `preflight`, `coverage`,
|
|
98
|
+
`sample` and `history`:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from agent_framework import MCPStreamableHTTPTool
|
|
102
|
+
|
|
103
|
+
botsrated_mcp = MCPStreamableHTTPTool(
|
|
104
|
+
name="botsrated",
|
|
105
|
+
url="https://botsrated.com/mcp",
|
|
106
|
+
allowed_tools=["preflight", "coverage", "sample", "history"],
|
|
107
|
+
)
|
|
108
|
+
async with botsrated_mcp:
|
|
109
|
+
result = await Agent(client=chat_client, tools=[botsrated_mcp]).run("Is blockrun.ai a known x402 seller?")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The paid lookups (`check` $0.001, `report` $0.002, `best` $0.01 USDC on Base; the first 100 checks and 10 reports per
|
|
113
|
+
client per day are free) are not used by this package unless you pass a paid-enabled `BotsRatedClient` to the guard.
|
|
114
|
+
|
|
115
|
+
## Run the example and tests
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install -e ".[mcp]" pytest pytest-asyncio
|
|
119
|
+
python examples/quickstart.py # scripted model, real free preflight, stub payment
|
|
120
|
+
pytest -m "not live" # offline
|
|
121
|
+
pytest -m live # real free BotsRated preflight + hosted MCP server; nothing is paid
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`examples/quickstart.py` uses a scripted chat client instead of a model, so it runs without an API key. One payment
|
|
125
|
+
call uses an unknown `payTo` and is blocked; the other uses the `payTo` BotsRated observed for blockrun.ai and runs.
|
|
126
|
+
|
|
127
|
+
BotsRated results are measurements, not endorsements. MIT licensed.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
agent_framework_botsrated/__init__.py,sha256=JS6gucnzE3OTWNgM5pwkGB3RqsS-7_HMLTkCR8szvic,793
|
|
2
|
+
agent_framework_botsrated/_middleware.py,sha256=2DttyywIlrOBb7IWFY50h-b7j1M7rIHBtgqtFNkBNR0,7740
|
|
3
|
+
agent_framework_botsrated/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
agent_framework_botsrated-0.1.0.dist-info/METADATA,sha256=wSCGL6rUANn8Y93roKoMajbx3bhG28P2v0yhk8QEhw4,6147
|
|
5
|
+
agent_framework_botsrated-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
6
|
+
agent_framework_botsrated-0.1.0.dist-info/licenses/LICENSE,sha256=3mkSwjwOJ_YSnozCObgbawNbWffdiM2Z04kunzx7yBg,1066
|
|
7
|
+
agent_framework_botsrated-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BotsRated
|
|
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.
|