pipsocket 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.
pipsocket/types.py ADDED
@@ -0,0 +1,48 @@
1
+ """The API's answer and request types, generated from its OpenAPI document.
2
+
3
+ Answers are plain dicts; these TypedDicts describe them for type checkers and editors. Nothing
4
+ is checked at run time, so a field or value a newer API adds is passed through as it is. Enums
5
+ in answers are open (a known value or any other string), enums in requests closed: the closed
6
+ copy of an enum used on both sides has the suffix "Param".
7
+ """
8
+
9
+ from ._generated.types import (
10
+ AccountView as AccountView,
11
+ Action as Action,
12
+ Blocking as Blocking,
13
+ CheckResult as CheckResult,
14
+ CheckView as CheckView,
15
+ CloseBy as CloseBy,
16
+ ClosePosition as ClosePosition,
17
+ DealView as DealView,
18
+ EngageKillSwitch as EngageKillSwitch,
19
+ ErrorBody as ErrorBody,
20
+ Filling as Filling,
21
+ FillingParam as FillingParam,
22
+ HandSettlement as HandSettlement,
23
+ HistoryOrderView as HistoryOrderView,
24
+ KillSwitchNotConnected as KillSwitchNotConnected,
25
+ KillSwitchOutcome as KillSwitchOutcome,
26
+ KillSwitchView as KillSwitchView,
27
+ ModifyOrder as ModifyOrder,
28
+ OrderType as OrderType,
29
+ OrderTypeParam as OrderTypeParam,
30
+ PendingOrderView as PendingOrderView,
31
+ PlaceOrder as PlaceOrder,
32
+ PositionLevels as PositionLevels,
33
+ PositionView as PositionView,
34
+ Remaining as Remaining,
35
+ RequestResult as RequestResult,
36
+ RequestState as RequestState,
37
+ RequestView as RequestView,
38
+ SentRequest as SentRequest,
39
+ Settlement as Settlement,
40
+ SweptOrder as SweptOrder,
41
+ SweptPosition as SweptPosition,
42
+ SymbolSummaryView as SymbolSummaryView,
43
+ SymbolView as SymbolView,
44
+ TickView as TickView,
45
+ TradeResult as TradeResult,
46
+ TypeTime as TypeTime,
47
+ TypeTimeParam as TypeTimeParam,
48
+ )
@@ -0,0 +1,358 @@
1
+ Metadata-Version: 2.5
2
+ Name: pipsocket
3
+ Version: 0.1.0
4
+ Summary: Python client for the Pipsocket API: trade MetaTrader 5 accounts hosted by Pipsocket from Linux, macOS or Windows.
5
+ Project-URL: Homepage, https://pipsocket.com
6
+ Project-URL: Documentation, https://docs.pipsocket.com
7
+ Project-URL: Source, https://github.com/pipsocket/pipsocket-python
8
+ Project-URL: Changelog, https://github.com/pipsocket/pipsocket-python/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/pipsocket/pipsocket-python/issues
10
+ Author-email: Pipsocket <support@pipsocket.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: algotrading,api-client,forex,pipsocket,trading,trading-api,websocket
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Office/Business :: Financial :: Investment
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Requires-Dist: httpx<1,>=0.27
29
+ Requires-Dist: websockets<18,>=16.1
30
+ Description-Content-Type: text/markdown
31
+
32
+ # pipsocket
33
+
34
+ Python client for the Pipsocket API: trade MetaTrader 5 accounts hosted by Pipsocket from Linux,
35
+ macOS or Windows.
36
+
37
+ ## Why
38
+
39
+ The terminal runs at Pipsocket, so your code runs anywhere Python does: one `pip install`, no
40
+ terminal on your machine, and typed answers.
41
+
42
+ | | The MetaTrader5 package | pipsocket |
43
+ | --- | --- | --- |
44
+ | Runs on | Windows only (its wheels on PyPI are all `win_amd64`, checked 9 Nov 2026) | Linux, macOS, Windows |
45
+ | Needs | An MT5 terminal running on the same machine | A Pipsocket account; the terminal is hosted |
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install pipsocket
51
+ ```
52
+
53
+ Python 3.10 or later. Its dependencies are [httpx](https://www.python-httpx.org) and
54
+ [websockets](https://websockets.readthedocs.io).
55
+
56
+ ## Quickstart
57
+
58
+ Make your workspace's API key on the dashboard's **API key** page, where your accounts' ids are
59
+ listed too, and put the key in `PIPSOCKET_KEY`. Use a demo account while you try things.
60
+
61
+ Each symbol allows its own filling modes, and MT5 rejects an order with any other (retcode
62
+ 10030), so read them, and the smallest volume, from the symbol rather than assuming one:
63
+
64
+ ```python
65
+ from pipsocket import Pipsocket
66
+
67
+ ps = Pipsocket() # reads PIPSOCKET_KEY
68
+ acct = ps.account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
69
+
70
+ print(acct.get()["balance"])
71
+ eurusd = acct.symbols.get("EURUSD")
72
+ filling = eurusd["filling_modes"][0] # one the symbol allows
73
+ volume = eurusd["volume_min"]
74
+
75
+ order = acct.orders.place(symbol="EURUSD", type="buy", volume=volume, type_filling=filling)
76
+ if order.outstanding: # pending or unknown: it may still execute
77
+ order = acct.requests.wait(order.id)
78
+
79
+ if order.accepted and order.result["deal"]: # MT5 took it and made a deal: filled
80
+ print("filled:", order.result["deal"], "at", order.result["price"])
81
+ # On a hedging account the position's ticket is the order's.
82
+ position = order.result["position"] or order.result["order"]
83
+ closed = acct.positions.close(position, volume=volume, type_filling=filling)
84
+ print("closed:", closed.state)
85
+ elif order.state == "rejected":
86
+ print("rejected:", order.retcode_name)
87
+ else:
88
+ print(order.state, order.message)
89
+ ```
90
+
91
+ Answers are plain dicts, typed with `TypedDict`s from the API's OpenAPI document
92
+ (`pipsocket.types`), so they go straight into anything that takes dicts:
93
+
94
+ ```python
95
+ from pipsocket import Pipsocket
96
+
97
+ acct = Pipsocket().account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
98
+
99
+ # History ranges are the broker's server time in seconds, both ends included, not UTC: take
100
+ # them from the account's server_time (None until its terminal has reported).
101
+ now = acct.get()["server_time"]
102
+ if now is not None:
103
+ deals = acct.history.deals(now - 86400, now) # the last 24 hours
104
+ profit = sum(deal["profit"] for deal in deals)
105
+ print(len(deals), "deals,", profit)
106
+ ```
107
+
108
+ ## Order outcomes
109
+
110
+ Each trade call (`orders.place`, `orders.modify`, `orders.cancel`, `positions.close`,
111
+ `positions.close_by`, `positions.set_levels`) is one MT5 trade request, sent once and recorded as
112
+ a request (`req_...`). The call returns that request as a `TradeRequest`, **whatever its state**:
113
+ a rejection is a result, not an exception. `acct.requests.get(id)` looks one up, and
114
+ `acct.requests.wait(id, timeout=30)` looks it up until it is final or the time is up, then
115
+ returns it as it stands; it never raises for "still pending" and never sends anything.
116
+
117
+ A `TradeRequest` has `id`, `state`, `sent`, `result`, `message`, `error`, `settlement`, the
118
+ whole body as `raw`, the `idempotency_key` it was sent with and the `http_status`, plus
119
+ `accepted`, `final`, `outstanding`, `retcode` and `retcode_name`.
120
+
121
+ ```python
122
+ from pipsocket import Pipsocket
123
+
124
+ acct = Pipsocket().account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
125
+
126
+ request = acct.requests.get("req_5f0c2a9e41d7b3a8c6e2f190")
127
+ match request.state:
128
+ case "completed" | "placed" | "partial":
129
+ if request.result["deal"]:
130
+ print("filled: deal", request.result["deal"])
131
+ else: # a pending order placed, or a change made: no deal
132
+ print("done, no deal: order", request.result["order"])
133
+ case "rejected":
134
+ print("MT5 rejected it:", request.retcode, request.retcode_name)
135
+ case "pending" | "unknown":
136
+ print("no outcome yet: it may still execute")
137
+ case _:
138
+ print(request.state, request.message)
139
+ ```
140
+
141
+ | `state` | Meaning |
142
+ | --- | --- |
143
+ | `completed` | MT5 completed the request (retcode 10009). Not necessarily a fill: see below. Final. |
144
+ | `placed` | MT5 placed it (retcode 10008), such as a pending order. Final. |
145
+ | `partial` | MT5 completed part of it (retcode 10010). Final. |
146
+ | `rejected` | MT5 rejected it: `retcode` says why. Final. |
147
+ | `pending` | Sent, no answer from the trade server yet. Never sent again. |
148
+ | `unknown` | No answer within 130 seconds: it may have executed. Settled by proof only. |
149
+ | `not_sent` | The terminal didn't send it: `result["error"]` says why. Final. |
150
+ | `not_executed` | Was `unknown`, settled as never executed (`settlement` says by whom). Final. |
151
+ | `executed` | Was `unknown`, settled as executed by the account's owner. Final. |
152
+
153
+ `completed` means MT5 completed the request, whatever it was: a market order filled, but also a
154
+ pending order placed, an order modified or cancelled, or stops set. So `accepted` (completed,
155
+ placed or partial) says MT5 took the request, not that anything filled. A fill shows as a deal:
156
+ `result["deal"]` is non-zero (and `sent["action"]` is `"deal"`). A pending order that was placed
157
+ has `result["order"]` set and `result["deal"]` 0.
158
+
159
+ `raise_for_state()` raises `TradeNotAccepted` for `rejected`, `not_sent` and `not_executed`, for
160
+ those who prefer exceptions. `orders.check(...)` is MT5's dry run of an order and sends nothing to
161
+ the broker. A state this SDK doesn't know yet comes through as its string,
162
+ neither accepted nor final. See [Errors and retcodes](https://docs.pipsocket.com/errors).
163
+
164
+ ## Streaming
165
+
166
+ `acct.stream(...)` keeps the account's WebSocket open: requests settling, deals, prices and the
167
+ account as they happen, reconnected for you.
168
+
169
+ ```python
170
+ from pipsocket import Pipsocket, TradeRequest
171
+
172
+ acct = Pipsocket().account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
173
+
174
+ with acct.stream(ticks=["EURUSD"]) as events:
175
+ for event in events:
176
+ match event["type"]:
177
+ case "resync": # after every (re)connection or gap: a new baseline
178
+ print("resync:", event["reason"], len(event["positions"] or []), "positions")
179
+ case "tick":
180
+ print(event["symbol"], event["bid"], event["ask"])
181
+ case "request":
182
+ print("settled:", TradeRequest.from_event(event).state)
183
+ break
184
+ ```
185
+
186
+ Events are the API's messages as they are (see [the WebSocket stream](https://docs.pipsocket.com/websocket)),
187
+ plus one the SDK adds, `resync`. The helper sends the tick set again after every reconnect,
188
+ answers the keep-alive, and resyncs when the socket was closed, the stream said `lagged`, the
189
+ account's host came back, or your code fell behind (`reason`: `connected`, `reconnected`,
190
+ `lagged`, `host_back`, `overflow`).
191
+
192
+ What a `resync` gives:
193
+
194
+ - **A new baseline**: `account`, `positions` and `orders` read over REST after the stream was
195
+ live. Replace your own state with it. `positions` and `orders` are `None` when the terminal
196
+ isn't running, which is not the same as empty.
197
+ - **History since the last checkpoint**, best effort: `history` has the `deals` and `orders` from
198
+ the broker's server time `from` to `to`, or is `None` with `history_reason` saying why
199
+ (`no_checkpoint`, `account_changed`, `range_too_large` or `unavailable`).
200
+ - Every deal, request, transaction and status event from then on, delivered after it.
201
+
202
+ What it doesn't give: one moment in time (the reads happen one after another), an order between
203
+ events and the snapshot (events are notices, not deltas: never add them to the snapshot), or the
204
+ events from a gap. **Events that happened while the stream was away are not replayed**, `request`
205
+ events included: the resync's snapshot and `history` cover what changed, and for a request you
206
+ are waiting on, `acct.requests.wait(id)` looks it up over REST. Ticks and transaction events from
207
+ a gap are gone. Deduplicate deals and orders by ticket and requests by id, and read again when
208
+ exact state matters. `snapshot=False` sends `resync` without reading anything.
209
+
210
+ A resync's `history` starts 5 minutes before its checkpoint (the newest broker time the stream
211
+ delivered), so it can include deals from before you opened the stream; deals the stream already
212
+ delivered as events are left out of it.
213
+
214
+ The sync iterator (`for event in events`) blocks until an event arrives. To stop it, break out of
215
+ the loop, or call `events.close()` from another thread: iteration then ends at once, and events
216
+ still queued are not delivered. `AsyncPipsocket` has the same helper, with `async with` and
217
+ `async for`, where cancelling the task stops it too.
218
+
219
+ While a stream is open on an account handle, `acct.requests.wait(id)` ends as soon as the stream
220
+ sees the request settle.
221
+
222
+ ## Errors, retries and idempotency keys
223
+
224
+ An exception means there is no recorded request in hand. Every exception derives from
225
+ `PipsocketError`. A refusal from the API is an `APIError` with `status`, `code` (the API's
226
+ `error`), `message`, `body` and `raw`, and a subclass for each kind: `AuthenticationError`,
227
+ `NotFound`, `InvalidRequest`, `SymbolBusy`, `KillSwitchEngaged`, `BillingHold`,
228
+ `AccountUnavailable`, `EngineBusy`, `Unavailable`, `TerminalTimeout`, `TerminalError`, `NoPrice`
229
+ and the rest. A network failure is an `APIConnectionError` (or `APITimeoutError`). None of
230
+ these, raised from a trade call, means anything was sent, with one exception:
231
+
232
+ **`OutcomeUnknown`** means the trade may have reached the account's host and no answer came
233
+ back. It may have executed. It carries the `idempotency_key` the call sent and the last error as
234
+ `cause`. Call again with the same key: you get the recorded request if there is one, and the
235
+ trade is sent if it never arrived. Or read positions and history first.
236
+
237
+ Every trade call sends an `Idempotency-Key`: a new one per call, the same one on every retry
238
+ inside the call. Calls that fail in a way worth repeating (the host busy or away, a dropped
239
+ connection, an answer that isn't the API's JSON, `symbol_busy` with `Retry-After`) are tried
240
+ again, twice by default, waiting 0.5 s and then 1 s (or what `Retry-After` says), and no new
241
+ attempt starts 60 seconds after the first: a market order sent minutes late is not what you
242
+ asked for. Refusals that won't change, such as a bad key or `kill_switch_engaged`, are raised at
243
+ once.
244
+
245
+ To survive a crash or a cancellation, make the key yourself and save it before sending. After a
246
+ restart, the same call with the same key returns the recorded request instead of trading again.
247
+ Make a new key for every new trade, and never build one from the order's fields (such as
248
+ "buy-eurusd"): the key is the whole match, so a second order with the same key returns the first
249
+ one and sends nothing.
250
+
251
+ A key protects a trade only while Pipsocket keeps its request: until 7 days after its outcome is
252
+ known. After that the same key sends a new trade. So resolve a journal's open entries promptly,
253
+ and never replay an old one blindly: check positions and history first.
254
+
255
+ ```python
256
+ import json
257
+ import pathlib
258
+
259
+ import pipsocket
260
+ from pipsocket import OutcomeUnknown, Pipsocket
261
+
262
+ acct = Pipsocket().account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
263
+ journal = pathlib.Path("orders.jsonl")
264
+
265
+
266
+ def note(entry: dict[str, object]) -> None:
267
+ with journal.open("a") as file:
268
+ file.write(json.dumps(entry) + "\n")
269
+
270
+
271
+ eurusd = acct.symbols.get("EURUSD")
272
+ filling, volume = eurusd["filling_modes"][0], eurusd["volume_min"]
273
+ key = pipsocket.new_key()
274
+ note({"key": key, "symbol": "EURUSD", "volume": volume}) # saved before it is sent
275
+
276
+ try:
277
+ order = acct.orders.place(
278
+ symbol="EURUSD", type="buy", volume=volume, type_filling=filling, idempotency_key=key
279
+ )
280
+ except OutcomeUnknown as error:
281
+ # The entry stays open: after a restart, resolve it with the same key.
282
+ print("may have executed; call again with", error.idempotency_key)
283
+ else:
284
+ if order.final:
285
+ note({"key": key, "request": order.id, "state": order.state}) # resolved
286
+ else:
287
+ print("no outcome yet: wait for", order.id, "before closing the entry")
288
+ print(order.id, order.state)
289
+ ```
290
+
291
+ Timeouts and retries can be set on the client, or per call:
292
+
293
+ ```python
294
+ from pipsocket import Pipsocket
295
+
296
+ ps = Pipsocket(timeout=20, max_retries=4, retry_deadline=30) # timeout is per attempt
297
+ acct = ps.account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
298
+ print(acct.kill_switch.get(max_retries=0)["engaged"])
299
+ ```
300
+
301
+ The kill switch closes every position and cancels every pending order, and refuses new exposure
302
+ until it is lifted. Engaging it again retries whatever is left:
303
+
304
+ ```python
305
+ from pipsocket import AccountUnavailable, Pipsocket
306
+
307
+ acct = Pipsocket().account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
308
+ try:
309
+ outcome = acct.kill_switch.engage(deviation=20)
310
+ print("flat:", outcome["flat"])
311
+ except AccountUnavailable as error:
312
+ print("engaged, but the terminal didn't answer:", error.kill_switch)
313
+ ```
314
+
315
+ ## Sync and async
316
+
317
+ `Pipsocket` is synchronous; `AsyncPipsocket` has the same calls, awaited.
318
+
319
+ ```python
320
+ import asyncio
321
+
322
+ from pipsocket import AsyncPipsocket
323
+
324
+
325
+ async def main() -> None:
326
+ async with AsyncPipsocket() as ps:
327
+ acct = ps.account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
328
+ account, positions = await asyncio.gather(acct.get(), acct.positions.list())
329
+ print(account["equity"], len(positions))
330
+
331
+
332
+ asyncio.run(main())
333
+ ```
334
+
335
+ Keep the key on your server: the API doesn't answer browsers, and the key is never logged or put
336
+ in an error message by this SDK.
337
+
338
+ ## Links
339
+
340
+ - Docs: [Quickstart](https://docs.pipsocket.com/quickstart),
341
+ [API reference](https://docs.pipsocket.com/api/place-an-order),
342
+ [WebSocket stream](https://docs.pipsocket.com/websocket),
343
+ [Errors and retcodes](https://docs.pipsocket.com/errors)
344
+ - [MCP server](https://docs.pipsocket.com/mcp) for AI apps
345
+ - [Changelog](CHANGELOG.md)
346
+
347
+ ## Versioning and support
348
+
349
+ Versions follow semver and are 0.x for now: until 1.0, a minor release (0.2, 0.3) may change
350
+ the API of this SDK, and the changelog says how. Report bugs in this SDK on
351
+ [GitHub issues](https://github.com/pipsocket/pipsocket-python/issues). Questions about your account
352
+ or billing go to [support@pipsocket.com](mailto:support@pipsocket.com).
353
+
354
+ ## Licence
355
+
356
+ MIT. See [LICENSE](LICENSE).
357
+
358
+ MetaTrader is a trademark of MetaQuotes Ltd. Pipsocket is not affiliated with MetaQuotes.
@@ -0,0 +1,20 @@
1
+ pipsocket/__init__.py,sha256=pZ2pLGe0ExQMsTfbmsBLycuBljBv47uMsCemOC8sWY4,1414
2
+ pipsocket/_core.py,sha256=JDWjIO49Y-e_MwBzUnpbp99n8rRpBg_rvJHoOvpKa0k,24741
3
+ pipsocket/_http.py,sha256=3ZNzC62okAr6QPe0_4gdEe1qMgQVeSDcrnzKsysOe34,8478
4
+ pipsocket/_stream_core.py,sha256=v-VXoI54sFN6o9gbD7bRpDg_UR3AMuJrq-RMMBYxKBs,31680
5
+ pipsocket/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
6
+ pipsocket/aio.py,sha256=uR1TBxiv5G9onC5SxtHoULSOHJhLFKi1o6gRbGpsrhU,26815
7
+ pipsocket/client.py,sha256=2LA99SyxO-6KP_p4PIGmxqeNgBoh_pVDXsPVDN6fyDQ,25358
8
+ pipsocket/errors.py,sha256=f16_-DpeYJH13IREENc25Fb8ZYYPwgBLOV2ngmhNs8M,8734
9
+ pipsocket/models.py,sha256=zQj25D569LGjxfWNdRjEBHebDb0b_WmCy4G2dztEztA,5758
10
+ pipsocket/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ pipsocket/stream.py,sha256=YeZi2qI2gp8s4_aWPAkiAnsP2-7XpgP1RQrbihIQWb8,25907
12
+ pipsocket/types.py,sha256=U6ecHjhpQvfLKoAg09bX9tZiM8x8FM9G-JkDAH1sXlo,1723
13
+ pipsocket/_generated/__init__.py,sha256=F3JxYZhP_J8MH26ydHmC1vXLWyxZ7CjsSuVNJREtauY,67
14
+ pipsocket/_generated/operations.py,sha256=5v0GBfE16IKgR6IneNhQ_90gKjw2bFyCig35FpvrcTs,1533
15
+ pipsocket/_generated/tables.py,sha256=AJ2SJujRA46rtM4L0XOUy-ZmpJfBb4BoTt88CsJgqrk,5009
16
+ pipsocket/_generated/types.py,sha256=cP__ETYwmSd6KYpW8MLEQ7lMQpFexwMfiatRab5MZEU,12824
17
+ pipsocket-0.1.0.dist-info/METADATA,sha256=5y31vdpXWxUjXhtJUsk4SiK8iElY0AHYfkrI5b7Sf18,16016
18
+ pipsocket-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
19
+ pipsocket-0.1.0.dist-info/licenses/LICENSE,sha256=gd_IFd70HOl6rg-nnzpEjHD-8vcuM-HQwTNHrD3R8ik,1088
20
+ pipsocket-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wilier Labs Pty Ltd (Pipsocket)
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.