smartx-rfid 0.4.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,237 @@
1
+ import asyncio
2
+ import logging
3
+ import time
4
+
5
+ import serial.tools.list_ports
6
+ import serial_asyncio
7
+ from typing import Callable
8
+ from smartx_rfid.utils.event import on_event
9
+
10
+
11
+ class SERIAL(asyncio.Protocol):
12
+ """
13
+ Asynchronous Serial Communication Protocol Handler
14
+
15
+ This class implements an asyncio-based serial communication protocol
16
+ that supports automatic port detection, connection management, and
17
+ data handling with timeout mechanisms.
18
+
19
+ Features:
20
+ - Automatic port detection by VID/PID
21
+ - Automatic reconnection on connection loss
22
+ - Message buffering with timeout handling
23
+ - CRC16 checksum calculation
24
+ - Event-driven architecture
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ name: str = "GENERIC_SERIAL",
30
+ port: str = "AUTO",
31
+ baudrate: int = 115200,
32
+ vid: int = 1,
33
+ pid: int = 1,
34
+ reconnection_time: int = 3,
35
+ ):
36
+ """
37
+ Initialize the SERIAL protocol handler.
38
+
39
+ Args:
40
+ name: Device name identifier
41
+ port: Serial port ('AUTO' for automatic detection)
42
+ baudrate: Communication baudrate
43
+ vid: USB Vendor ID for auto-detection
44
+ pid: USB Product ID for auto-detection
45
+ reconnection_time: Delay between reconnection attempts
46
+ """
47
+ self.name = name
48
+ self.device_type = "generic"
49
+
50
+ self.port = port
51
+ self.baudrate = baudrate
52
+ self.vid = vid
53
+ self.pid = pid
54
+ self.reconnection_time = reconnection_time
55
+
56
+ self.transport = None
57
+ self.on_con_lost = None
58
+ self.rx_buffer = bytearray()
59
+ self.last_byte_time = None
60
+ self.is_auto = self.port == "AUTO"
61
+
62
+ self.is_connected = False
63
+ self.is_reading = False
64
+
65
+ self.on_event: Callable = on_event
66
+
67
+ def connection_made(self, transport):
68
+ """
69
+ Callback invoked when a connection is established.
70
+
71
+ Args:
72
+ transport: The transport object for communication
73
+ """
74
+ self.transport = transport
75
+ self.is_connected = True
76
+ self.on_event(self.name, "connection", True)
77
+
78
+ def data_received(self, data):
79
+ """
80
+ Callback invoked when data is received from the serial port.
81
+
82
+ Handles incoming data with automatic message parsing and timeout management.
83
+ Messages are delimited by '\n' or '\r' characters.
84
+
85
+ Args:
86
+ data: Raw bytes received from the serial port
87
+ """
88
+ now = time.time()
89
+ self.rx_buffer += data
90
+ self.last_byte_time = now
91
+
92
+ # Cancela tarefa anterior de timeout
93
+ if hasattr(self, "_timeout_task") and self._timeout_task and not self._timeout_task.done():
94
+ self._timeout_task.cancel()
95
+
96
+ # Cria nova tarefa de timeout
97
+ async def timeout_clear():
98
+ await asyncio.sleep(0.3) # 300 ms
99
+ if self.last_byte_time and (time.time() - self.last_byte_time) >= 0.3:
100
+ if self.rx_buffer:
101
+ self.rx_buffer.clear()
102
+ logging.warning("⚠️ Buffer cleared due to 300ms timeout without receiving data.")
103
+
104
+ self._timeout_task = asyncio.create_task(timeout_clear())
105
+
106
+ # Processa mensagens completas
107
+ while b"\n" in self.rx_buffer or b"\r" in self.rx_buffer:
108
+ # Encontra posição do primeiro delimitador
109
+ positions = [p for p in [self.rx_buffer.find(b"\n"), self.rx_buffer.find(b"\r")] if p != -1]
110
+ pos = min(positions)
111
+
112
+ # Extrai mensagem em bytes e converte para string
113
+ message_bytes = self.rx_buffer[:pos]
114
+ message = message_bytes.decode(errors="ignore").strip("\r\n")
115
+
116
+ # Remove mensagem do buffer
117
+ self.rx_buffer = self.rx_buffer[pos + 1 :]
118
+
119
+ if message:
120
+ self.on_event(self.name, "receive", message)
121
+
122
+ def connection_lost(self, exc):
123
+ """
124
+ Callback invoked when the connection is lost.
125
+
126
+ Args:
127
+ exc: Exception that caused the disconnection (if any)
128
+ """
129
+ logging.warning("⚠️ Serial connection lost.")
130
+ self.transport = None
131
+ self.is_connected = False
132
+ self.step = 0
133
+
134
+ if self.on_con_lost:
135
+ self.on_con_lost.set()
136
+ self.on_event(self.name, "connection", False)
137
+
138
+ def write(self, to_send, verbose=True):
139
+ """
140
+ Send data through the serial connection.
141
+
142
+ Automatically adds newline to strings and calculates CRC16 for bytes.
143
+
144
+ Args:
145
+ to_send: Data to send (str or bytes)
146
+ verbose: Enable logging of sent data
147
+ """
148
+ if self.transport:
149
+ if isinstance(to_send, str):
150
+ to_send += "\n"
151
+ to_send = to_send.encode()
152
+
153
+ # If it's bytes, calculate CRC and replace last two bytes
154
+ if isinstance(to_send, bytes) and len(to_send) >= 2:
155
+ crc = self.crc16(to_send)
156
+ to_send = to_send[:-2] + bytes([crc & 0xFF, crc >> 8])
157
+
158
+ if verbose:
159
+ if isinstance(to_send, bytes):
160
+ hex_list = [f"0x{b:02X}" for b in to_send]
161
+ logging.info(f"📤 Sending: {hex_list}")
162
+ else:
163
+ logging.info(f"📤 Sending: {to_send}")
164
+
165
+ self.transport.write(to_send)
166
+ else:
167
+ logging.warning("❌ Send attempt failed: connection not established.")
168
+
169
+ async def connect(self):
170
+ """
171
+ Establish and maintain serial connection with automatic reconnection.
172
+
173
+ This method runs continuously, attempting to connect to the specified
174
+ serial port or auto-detecting it by VID/PID. When connection is lost,
175
+ it automatically attempts to reconnect.
176
+ """
177
+ loop = asyncio.get_running_loop()
178
+
179
+ while True:
180
+ self.on_con_lost = asyncio.Event()
181
+
182
+ # If AUTO mode, try to detect port by VID/PID
183
+ if self.is_auto:
184
+ logging.info("🔍 Auto-detecting port")
185
+ ports = serial.tools.list_ports.comports()
186
+ found_port = None
187
+ for p in ports:
188
+ # p.vid and p.pid are integers (e.g. 0x0001 == 1 decimal)
189
+ if p.vid == self.vid and p.pid == self.pid:
190
+ found_port = p.device
191
+ logging.info(f"✅ Detected port: {found_port}")
192
+ break
193
+
194
+ if found_port is None:
195
+ logging.warning(f"⚠️ No port with VID={self.vid} and PID={self.pid} found.")
196
+ logging.info(f"⏳ Retrying in {self.reconnection_time} seconds...")
197
+ await asyncio.sleep(self.reconnection_time)
198
+ continue # try to detect again in next loop
199
+ else:
200
+ self.port = found_port
201
+
202
+ try:
203
+ logging.info(f"🔌 Trying to connect to {self.port} at {self.baudrate} bps...")
204
+ await serial_asyncio.create_serial_connection(loop, lambda: self, self.port, baudrate=self.baudrate)
205
+ logging.info("🟢 Successfully connected.")
206
+ await self.on_con_lost.wait()
207
+ logging.info("🔄 Connection lost. Attempting to reconnect...")
208
+ except Exception as e:
209
+ logging.warning(f"❌ Connection error: {e}")
210
+
211
+ # If in AUTO mode, reset port to "AUTO" to force detection next loop
212
+ if self.is_auto:
213
+ self.port = "AUTO"
214
+
215
+ logging.info("⏳ Waiting 3 seconds before retrying...")
216
+ await asyncio.sleep(3)
217
+
218
+ def crc16(self, data: bytes, poly=0x8408):
219
+ """
220
+ Calculate CRC-16/CCITT-FALSE checksum.
221
+
222
+ Args:
223
+ data: Input bytes (last 2 bytes are excluded from calculation)
224
+ poly: CRC polynomial (default: 0x8408 for CCITT-FALSE)
225
+
226
+ Returns:
227
+ int: 16-bit CRC checksum
228
+ """
229
+ crc = 0xFFFF
230
+ for byte in data[:-2]: # exclude last two bytes (CRC placeholder)
231
+ crc ^= byte
232
+ for _ in range(8):
233
+ if crc & 0x0001:
234
+ crc = (crc >> 1) ^ poly
235
+ else:
236
+ crc >>= 1
237
+ return crc & 0xFFFF
@@ -0,0 +1,72 @@
1
+ import asyncio
2
+ import logging
3
+
4
+ from .helpers import Helpers
5
+ from smartx_rfid.utils.event import on_event
6
+ from typing import Callable
7
+
8
+
9
+ class TCP(Helpers):
10
+ def __init__(
11
+ self,
12
+ name: str = "GENERIC_TCP",
13
+ ip: str = "192.168.1.101",
14
+ port: int = 23,
15
+ ):
16
+ self.name = name
17
+ self.device_type = "generic"
18
+
19
+ self.ip = ip
20
+ self.port = port
21
+
22
+ self.reader = None
23
+ self.writer = None
24
+
25
+ self.is_connected = False
26
+ self.on_event: Callable = on_event
27
+
28
+ async def connect(self):
29
+ while True:
30
+ try:
31
+ logging.info(f"Connecting: {self.name} - {self.ip}:{self.port}")
32
+ self.reader, self.writer = await asyncio.wait_for(
33
+ asyncio.open_connection(self.ip, self.port), timeout=3
34
+ )
35
+ self.is_connected = True
36
+ self.on_event(self.name, "connection", True)
37
+
38
+ # Start the receive and monitor tasks
39
+ tasks = [
40
+ asyncio.create_task(self.receive_data()),
41
+ asyncio.create_task(self.monitor_connection()),
42
+ ]
43
+
44
+ # Wait until one of the tasks completes (e.g. disconnection)
45
+ done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
46
+
47
+ # Cancel any remaining tasks
48
+ for task in pending:
49
+ task.cancel()
50
+
51
+ self.is_connected = False
52
+ self.on_event(self.name, "connection", False)
53
+
54
+ except Exception as e:
55
+ self.is_connected = False
56
+ self.on_event(self.name, "connection", False)
57
+ logging.error(f"[CONNECTION ERROR] {e}")
58
+
59
+ await asyncio.sleep(3)
60
+
61
+ async def write(self, data: str, verbose=True):
62
+ if self.is_connected and self.writer:
63
+ try:
64
+ data = data + "\n"
65
+ self.writer.write(data.encode())
66
+ await self.writer.drain()
67
+ if verbose:
68
+ logging.info(f"[SENT] {data.strip()}")
69
+ except Exception as e:
70
+ logging.warning(f"[SEND ERROR] {e}")
71
+ self.is_connected = False
72
+ self.on_event(self.name, "connection", False)
@@ -0,0 +1,41 @@
1
+ import asyncio
2
+ import logging
3
+
4
+
5
+ class Helpers:
6
+ async def monitor_connection(self):
7
+ while self.is_connected:
8
+ await asyncio.sleep(3)
9
+ if (self.writer and self.writer.is_closing()) or (self.reader and self.reader.at_eof()):
10
+ self.is_connected = False
11
+ logging.info("[DISCONNECTED] Socket closed.")
12
+ break
13
+
14
+ await self.write("ping", verbose=False)
15
+
16
+ async def receive_data(self):
17
+ buffer = ""
18
+ try:
19
+ while True:
20
+ try:
21
+ data = await asyncio.wait_for(self.reader.read(1024), timeout=0.1)
22
+ except asyncio.TimeoutError:
23
+ # Timeout: process what's in the buffer as a command
24
+ if buffer:
25
+ await self.on_received_cmd(buffer.strip())
26
+ buffer = ""
27
+ continue
28
+
29
+ if not data:
30
+ raise ConnectionError("Connection lost")
31
+
32
+ buffer += data.decode(errors="ignore")
33
+
34
+ while "\n" in buffer:
35
+ line, buffer = buffer.split("\n", 1)
36
+ # event received
37
+ self.on_event(self.name, "receive", line.strip())
38
+
39
+ except Exception as e:
40
+ self.is_connected = False
41
+ logging.error(f"[RECEIVE ERROR] {e}")
@@ -0,0 +1,55 @@
1
+ import re
2
+ from typing import Optional
3
+ from pydantic import BaseModel, Field, field_validator
4
+
5
+
6
+ class TagSchema(BaseModel):
7
+ epc: str = Field("000000000000000000000001")
8
+ tid: Optional[str | None] = Field("E28000000000000000000001")
9
+ ant: Optional[int | None] = 0
10
+ rssi: Optional[int | None] = 0
11
+
12
+ @field_validator("epc", "tid")
13
+ def validate_epc_length_and_hex(cls, v, field):
14
+ if v is None:
15
+ return v
16
+ if len(v) != 24:
17
+ raise ValueError(f"{field} must have exactly 24 characters")
18
+ if not re.fullmatch(r"[0-9a-fA-F]{24}", v):
19
+ raise ValueError(f"{field} must contain only hexadecimal characters (0-9, a-f)")
20
+ return v.lower()
21
+
22
+
23
+ class WriteTagValidator(BaseModel):
24
+ target_identifier: Optional[str] = Field(None, description='Identifier type: "epc", "tid", or None')
25
+ target_value: Optional[str] = Field(None, description="Current value of the identifier (24 hexadecimal characters)")
26
+ new_epc: str = Field(..., description="New EPC value to write (24 hexadecimal characters)")
27
+ password: str = Field(..., description="Password to access the tag (8 hexadecimal characters)")
28
+
29
+ @field_validator("target_identifier")
30
+ def validate_identifier(cls, v):
31
+ if v == "None" or v is None:
32
+ return None
33
+ allowed_values = ("epc", "tid", None)
34
+ if v not in allowed_values:
35
+ raise ValueError(f"target_identifier must be one of {allowed_values}")
36
+ return v.lower()
37
+
38
+ @field_validator("target_value", "new_epc")
39
+ def validate_epc_length_and_hex(cls, v, field):
40
+ if v is None or v == "None":
41
+ v = "0" * 24
42
+
43
+ if len(v) != 24:
44
+ raise ValueError(f"{field} must have exactly 24 characters")
45
+ if not re.fullmatch(r"[0-9a-fA-F]{24}", v):
46
+ raise ValueError(f"{field} must contain only hexadecimal characters (0-9, a-f)")
47
+ return v.lower()
48
+
49
+ @field_validator("password")
50
+ def validate_password_length_and_hex(cls, v, field):
51
+ if len(v) != 8:
52
+ raise ValueError(f"{field} must have exactly 8 characters")
53
+ if not re.fullmatch(r"[0-9a-fA-F]{8}", v):
54
+ raise ValueError(f"{field} must contain only hexadecimal characters (0-9, a-f)")
55
+ return v.lower()
File without changes
@@ -0,0 +1,15 @@
1
+ import logging
2
+
3
+
4
+ def on_event(name: str, event_type: str, event_data=None):
5
+ """
6
+ Default event handler for protocol events.
7
+
8
+ This method can be overridden to handle specific events like
9
+ connection status changes or received messages.
10
+
11
+ Args:
12
+ event_type: Type of event ('connection', 'receive', etc.)
13
+ event_data: Associated data with the event
14
+ """
15
+ logging.info(f"{name} -> 🔔 Event: {event_type}, Data: {event_data}")
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: smartx-rfid
3
+ Version: 0.4.0
4
+ Summary: SmartX RFID library
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: python,library,RFID,smartx,packaging
8
+ Author: Gabriel Henrique Pascon
9
+ Author-email: gh.pascon@gmail.com
10
+ Requires-Python: >=3.11,<4.0
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Dist: bleak (>=1.1.1,<2.0.0)
21
+ Requires-Dist: httpx (==0.28.1)
22
+ Requires-Dist: pydantic (>=2.12.5,<3.0.0)
23
+ Requires-Dist: pyepc (==0.5.0)
24
+ Requires-Dist: pyserial (==3.5)
25
+ Requires-Dist: pyserial-asyncio (==0.6)
26
+ Requires-Dist: ruff (>=0.14.11,<0.15.0)
27
+ Project-URL: Documentation, https://github.com/ghpascon/smartx_rfid#readme
28
+ Project-URL: Homepage, https://github.com/ghpascon/smartx_rfid
29
+ Project-URL: Repository, https://github.com/ghpascon/smartx_rfid
30
+ Description-Content-Type: text/markdown
31
+
32
+ # SmartX RFID
33
+
34
+ The official SmartX RFID Python library for seamless integration with RFID systems and devices.
35
+
36
+ ## Overview
37
+
38
+ SmartX RFID is a comprehensive Python package designed to provide easy-to-use interfaces for RFID operations and device management. This library serves as the foundation for building robust RFID applications.
39
+
40
+ ## Features (Current & Planned)
41
+
42
+ - **Device Communication**: Asynchronous serial communication with RFID devices
43
+ - **Auto-Detection**: Automatic port detection for USB devices by VID/PID
44
+ - **Connection Management**: Automatic reconnection and error handling
45
+ - **External Device Support**: Interface with various RFID readers and writers *(coming soon)*
46
+ - **Tag Operations**: Read, write, and manage RFID tags *(coming soon)*
47
+ - **Protocol Support**: Multiple RFID protocols and standards *(coming soon)*
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install smartx-rfid
53
+ ```
54
+
55
+ ## Quick Start
56
+
57
+ ```python
58
+ from smartx_rfid.devices import SERIAL
59
+ import asyncio
60
+
61
+ async def main():
62
+ device = SERIAL(name="MyRFIDDevice")
63
+ await device.connect()
64
+
65
+ asyncio.run(main())
66
+ ```
67
+
68
+ ## Development Status
69
+
70
+ This library is actively under development. Current focus areas include:
71
+
72
+ - Core device communication protocols
73
+ - External device integration
74
+ - Enhanced error handling
75
+ - Comprehensive documentation
76
+
77
+ ## License
78
+
79
+ MIT License
80
+
81
+ ## Support
82
+
83
+ For issues and support, please visit our [GitHub repository](https://github.com/ghpascon/smartx_rfid).
84
+
@@ -0,0 +1,24 @@
1
+ smartx_rfid/__init__.py,sha256=XCdKdP8o5LT3VjJtWOuFHJzztc0FXt3Ms89qsx0p6sM,282
2
+ smartx_rfid/devices/RFID/R700_IOT/_main.py,sha256=xd9roD7ovnJAXLp6r7PtxYz98CgKO1t6gz3t4V0KIMU,6293
3
+ smartx_rfid/devices/RFID/R700_IOT/on_event.py,sha256=JmJ6RcGgX34b2gkuqyZ4G1LSDliRRALg_DdvenHOZVk,538
4
+ smartx_rfid/devices/RFID/R700_IOT/reader_config_example.py,sha256=oi4vg3_zjYFDUeq1Y_d5InUx_Rq5M2CB5RDVkK3WCgU,2264
5
+ smartx_rfid/devices/RFID/R700_IOT/reader_helpers.py,sha256=UC4ozq9FmuQtCK5Nv8Mjg4tZyuFetjx1eMWiw5yb0XY,5692
6
+ smartx_rfid/devices/RFID/R700_IOT/write_commands.py,sha256=d1GXBaBVaNqvPWitYNZ94uZJOy2wwbj-2uzCxWOflPo,2037
7
+ smartx_rfid/devices/RFID/X714/_main.py,sha256=ERMeQnbLevGXtZ-8Roykb67jbt53yacawibPkCs4eGo,4753
8
+ smartx_rfid/devices/RFID/X714/ble_protocol.py,sha256=M3Cs66WX6oa_EPdMTLEjD-IRxAjpfrKzGj4hn1vxlxQ,6951
9
+ smartx_rfid/devices/RFID/X714/on_receive.py,sha256=yjkxWL1ZGzcZKZXKwQ85cvl33fsjhXATOwg9QQBk-GQ,1641
10
+ smartx_rfid/devices/RFID/X714/rfid.py,sha256=W-8-ADYv9oYE9PHDEynQhHCXyVecDRfplWRIxb3cqts,1974
11
+ smartx_rfid/devices/RFID/X714/serial_protocol.py,sha256=eOGM5sK35SxXfS9KjB8JQzYhR1qs4H3vw3-n3oPOjQQ,3681
12
+ smartx_rfid/devices/RFID/X714/tcp_protocol.py,sha256=GthynyN6YMl6R16nrAnlrDK61QnuZAOhYhPqycBCwJA,4804
13
+ smartx_rfid/devices/RFID/X714/write_commands.py,sha256=gkfsQ--bijZWw0e1FP8Wb0qtFtexM4hP6HJz6y5hvOM,987
14
+ smartx_rfid/devices/__init__.py,sha256=EVhtb-IBLpits8dr-I1ZYYuWHw9ddqiu-98myg-iyFg,251
15
+ smartx_rfid/devices/generic/SERIAL/_main.py,sha256=BB4b6t6NI7tQDZH1ZAl-ZqSG8WH_HmGJIrIs5hIxpS8,8356
16
+ smartx_rfid/devices/generic/TCP/_main.py,sha256=BzpIiCxamtauQCa7GCldNlG_fhSLJoSxDs2lhGEc31M,2318
17
+ smartx_rfid/devices/generic/TCP/helpers.py,sha256=5ySy0Wv6D3q9AEV8kVLw1aMbe3kMDm4lFxP3KLIMMWM,1386
18
+ smartx_rfid/schemas/tag.py,sha256=gq38yKPqPBPaF-wx_0_lnhttz0neM_bob94puv-5Z7Y,2285
19
+ smartx_rfid/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ smartx_rfid/utils/event.py,sha256=7QOdiSfiMscoAaTq4U3E3asYGSVnolr9OD3FSYGh6bg,469
21
+ smartx_rfid-0.4.0.dist-info/METADATA,sha256=OPKyl9QnD1r2um-C9pwMXuKsMxEji8qv7xItz5T8MsQ,2676
22
+ smartx_rfid-0.4.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
23
+ smartx_rfid-0.4.0.dist-info/licenses/LICENSE,sha256=npGJO3HuHM3JdFNEXv1jRmP_wRYq9-ujkqJPmLxrtEw,1080
24
+ smartx_rfid-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gabriel Henrique Pascon
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.