apyefa 0.0.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.
Potentially problematic release.
This version of apyefa might be problematic. Click here for more details.
- apyefa/__init__.py +21 -0
- apyefa/client.py +215 -0
- apyefa/commands/__init__.py +15 -0
- apyefa/commands/command.py +115 -0
- apyefa/commands/command_departures.py +49 -0
- apyefa/commands/command_serving_lines.py +60 -0
- apyefa/commands/command_stop_finder.py +48 -0
- apyefa/commands/command_system_info.py +28 -0
- apyefa/commands/command_trip.py +31 -0
- apyefa/commands/parsers/__init__.py +0 -0
- apyefa/commands/parsers/parser.py +7 -0
- apyefa/commands/parsers/rapid_json_parser.py +11 -0
- apyefa/commands/parsers/xml_parser.py +6 -0
- apyefa/data_classes.py +381 -0
- apyefa/exceptions.py +18 -0
- apyefa/helpers.py +74 -0
- apyefa-0.0.1.dist-info/LICENSE +21 -0
- apyefa-0.0.1.dist-info/METADATA +413 -0
- apyefa-0.0.1.dist-info/RECORD +21 -0
- apyefa-0.0.1.dist-info/WHEEL +5 -0
- apyefa-0.0.1.dist-info/top_level.txt +1 -0
apyefa/data_classes.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from enum import IntEnum, StrEnum
|
|
5
|
+
from typing import Final, Self
|
|
6
|
+
|
|
7
|
+
import voluptuous as vol
|
|
8
|
+
|
|
9
|
+
from apyefa.helpers import parse_date, parse_datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Enums
|
|
13
|
+
class LocationType(StrEnum):
|
|
14
|
+
STOP = "stop"
|
|
15
|
+
POI = "poi"
|
|
16
|
+
ADDRESS = "address"
|
|
17
|
+
STREET = "street"
|
|
18
|
+
LOCALITY = "locality"
|
|
19
|
+
SUBURB = "suburb"
|
|
20
|
+
PLATFORM = "platform"
|
|
21
|
+
UNKNOWN = "unknown"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TransportType(IntEnum):
|
|
25
|
+
RAIL = 0 # RB
|
|
26
|
+
SUBURBAN = 1 # S-Bahn
|
|
27
|
+
SUBWAY = 2 # U-Bahn
|
|
28
|
+
CITY_RAIL = 3 # Stadtbahn
|
|
29
|
+
TRAM = 4 # Straßenbahn
|
|
30
|
+
BUS = 5 # Bus
|
|
31
|
+
RBUS = 6 # Regional Bus
|
|
32
|
+
EXPRESS_BUS = 7 # Schnellbus
|
|
33
|
+
CABLE_TRAM = 8 # Seilbahn
|
|
34
|
+
FERRY = 9 # Schief
|
|
35
|
+
AST = 10 # Anruf-Sammel-Taxi
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LocationFilter(IntEnum):
|
|
39
|
+
NO_FILTER = 0
|
|
40
|
+
LOCATIONS = 1
|
|
41
|
+
STOPS = 2
|
|
42
|
+
STREETS = 4
|
|
43
|
+
ADDRESSES = 8
|
|
44
|
+
INTERSACTIONS = 16
|
|
45
|
+
POIS = 32
|
|
46
|
+
POST_CODES = 64
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CoordFormat(StrEnum):
|
|
50
|
+
WGS84 = "WGS84[dd.ddddd]"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Validation schemas
|
|
54
|
+
def IsLocationType(type: str):
|
|
55
|
+
if type not in [x.value for x in LocationFilter]:
|
|
56
|
+
raise ValueError
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
SCHEMA_PROPERTIES = vol.Schema(
|
|
60
|
+
{
|
|
61
|
+
vol.Required("stopId"): str,
|
|
62
|
+
vol.Optional("downloads"): list,
|
|
63
|
+
vol.Optional("area"): str,
|
|
64
|
+
vol.Optional("platform"): str,
|
|
65
|
+
vol.Optional("platformName"): str,
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
SCHEMA_LINE_PROPERTIES: Final = vol.Schema(
|
|
70
|
+
{
|
|
71
|
+
vol.Required("globalId"): str,
|
|
72
|
+
vol.Required("isROP"): bool,
|
|
73
|
+
vol.Required("isSTT"): bool,
|
|
74
|
+
vol.Required("isTTB"): bool,
|
|
75
|
+
vol.Required("lineDisplay"): str,
|
|
76
|
+
vol.Required("timetablePeriod"): str,
|
|
77
|
+
vol.Required("tripCode"): int,
|
|
78
|
+
vol.Required("validity"): vol.Schema(
|
|
79
|
+
{
|
|
80
|
+
vol.Required("from"): vol.Date("%Y-%m-%d"),
|
|
81
|
+
vol.Required("to"): vol.Date("%Y-%m-%d"),
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
SCHEMA_PRODUCT = vol.Schema(
|
|
88
|
+
{
|
|
89
|
+
vol.Required("id"): int,
|
|
90
|
+
vol.Required("class"): int,
|
|
91
|
+
vol.Required("name"): str,
|
|
92
|
+
vol.Optional("iconId"): int,
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
SCHEMA_STOP = vol.Schema(
|
|
97
|
+
{
|
|
98
|
+
vol.Required("name"): str,
|
|
99
|
+
vol.Required("type"): IsLocationType,
|
|
100
|
+
vol.Optional("id"): str,
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
SCHEMA_PARENT = vol.Schema(
|
|
105
|
+
{
|
|
106
|
+
vol.Required("name"): str,
|
|
107
|
+
vol.Required("type"): str,
|
|
108
|
+
vol.Optional("id"): str,
|
|
109
|
+
vol.Optional("isGlobalId"): vol.Boolean,
|
|
110
|
+
vol.Optional("disassembledName"): str,
|
|
111
|
+
vol.Optional("parent"): vol.Self,
|
|
112
|
+
vol.Optional("properties"): SCHEMA_PROPERTIES,
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
SCHEMA_OPERATOR = vol.Schema(
|
|
117
|
+
{
|
|
118
|
+
vol.Required("id"): str,
|
|
119
|
+
vol.Required("name"): str,
|
|
120
|
+
vol.Optional("code"): str,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
SCHEMA_LOCATION: Final = vol.Schema(
|
|
125
|
+
{
|
|
126
|
+
vol.Required("name"): str,
|
|
127
|
+
vol.Required("type"): vol.In([x.value for x in LocationType]),
|
|
128
|
+
vol.Optional("id"): str,
|
|
129
|
+
vol.Optional("disassembledName"): str,
|
|
130
|
+
vol.Optional("coord"): list,
|
|
131
|
+
vol.Optional("isGlobalId"): vol.Boolean,
|
|
132
|
+
vol.Optional("isBest"): vol.Boolean,
|
|
133
|
+
vol.Optional("productClasses"): list[vol.Range(min=0, max=10)],
|
|
134
|
+
vol.Optional("parent"): SCHEMA_PARENT,
|
|
135
|
+
vol.Optional("assignedStops"): [vol.Self],
|
|
136
|
+
vol.Optional("properties"): SCHEMA_PROPERTIES,
|
|
137
|
+
vol.Optional("matchQuality"): int,
|
|
138
|
+
},
|
|
139
|
+
extra=vol.ALLOW_EXTRA,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
SCHEMA_TRANSPORTATION: Final = vol.Schema(
|
|
143
|
+
{
|
|
144
|
+
vol.Required("id"): str,
|
|
145
|
+
vol.Required("name"): str,
|
|
146
|
+
vol.Required("disassembledName"): str,
|
|
147
|
+
vol.Required("number"): str,
|
|
148
|
+
vol.Required("description"): str,
|
|
149
|
+
vol.Required("product"): SCHEMA_PRODUCT,
|
|
150
|
+
vol.Optional("operator"): SCHEMA_OPERATOR,
|
|
151
|
+
vol.Optional("destination"): SCHEMA_LOCATION,
|
|
152
|
+
vol.Optional("origin"): SCHEMA_LOCATION,
|
|
153
|
+
vol.Optional("properties"): dict,
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
SCHEMA_SYSTEM_INFO: Final = vol.Schema(
|
|
158
|
+
{
|
|
159
|
+
vol.Required("version"): str,
|
|
160
|
+
vol.Required("ptKernel"): vol.Schema(
|
|
161
|
+
{
|
|
162
|
+
vol.Required("appVersion"): str,
|
|
163
|
+
vol.Required("dataFormat"): str,
|
|
164
|
+
vol.Required("dataBuild"): str,
|
|
165
|
+
}
|
|
166
|
+
),
|
|
167
|
+
vol.Required("validity"): vol.Schema(
|
|
168
|
+
{
|
|
169
|
+
vol.Required("from"): vol.Date("%Y-%m-%d"),
|
|
170
|
+
vol.Required("to"): vol.Date("%Y-%m-%d"),
|
|
171
|
+
}
|
|
172
|
+
),
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
SCHEMA_DEPARTURE: Final = vol.Schema(
|
|
177
|
+
{
|
|
178
|
+
vol.Required("location"): SCHEMA_LOCATION,
|
|
179
|
+
vol.Required("departureTimePlanned"): vol.Datetime("%Y-%m-%dT%H:%M:%S%z"),
|
|
180
|
+
vol.Optional("departureTimeEstimated"): vol.Datetime("%Y-%m-%dT%H:%M:%S%z"),
|
|
181
|
+
vol.Required("transportation"): SCHEMA_TRANSPORTATION,
|
|
182
|
+
vol.Optional("infos"): list,
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass(frozen=True)
|
|
188
|
+
class _Base:
|
|
189
|
+
raw_data: dict = field(repr=False)
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
@abstractmethod
|
|
193
|
+
def from_dict(cls, data: dict):
|
|
194
|
+
raise NotImplementedError
|
|
195
|
+
|
|
196
|
+
def to_dict(self) -> dict:
|
|
197
|
+
return self.raw_data
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# Data classes
|
|
201
|
+
@dataclass(frozen=True)
|
|
202
|
+
class SystemInfo(_Base):
|
|
203
|
+
version: str
|
|
204
|
+
app_version: str
|
|
205
|
+
data_format: str
|
|
206
|
+
data_build: str
|
|
207
|
+
valid_from: date
|
|
208
|
+
valid_to: date
|
|
209
|
+
|
|
210
|
+
_schema = SCHEMA_SYSTEM_INFO
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def from_dict(cls, data: dict) -> Self | None:
|
|
214
|
+
if not data:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
if not isinstance(data, dict):
|
|
218
|
+
raise ValueError(f"Expected a dictionary, provided {type(data)}")
|
|
219
|
+
|
|
220
|
+
cls._schema(data)
|
|
221
|
+
|
|
222
|
+
return SystemInfo(
|
|
223
|
+
data,
|
|
224
|
+
data.get("version"),
|
|
225
|
+
data.get("ptKernel").get("appVersion"),
|
|
226
|
+
data.get("ptKernel").get("dataFormat"),
|
|
227
|
+
data.get("ptKernel").get("dataBuild"),
|
|
228
|
+
parse_date(data.get("validity").get("from")),
|
|
229
|
+
parse_date(data.get("validity").get("to")),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass(frozen=True)
|
|
234
|
+
class Location(_Base):
|
|
235
|
+
name: str
|
|
236
|
+
loc_type: LocationType
|
|
237
|
+
id: str = ""
|
|
238
|
+
coord: list[int] = field(default_factory=[])
|
|
239
|
+
transports: list[TransportType] = field(default_factory=[])
|
|
240
|
+
parent: Self | None = None
|
|
241
|
+
stops: list[Self] = field(default_factory=[])
|
|
242
|
+
properties: dict = field(default_factory={})
|
|
243
|
+
disassembled_name: str = field(repr=False, default="")
|
|
244
|
+
match_quality: int = field(repr=False, default=0)
|
|
245
|
+
|
|
246
|
+
_schema = SCHEMA_LOCATION
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def from_dict(cls, data: dict) -> Self | None:
|
|
250
|
+
if not data:
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
if not isinstance(data, dict):
|
|
254
|
+
raise ValueError(f"Expected a dictionary, provided {type(data)}")
|
|
255
|
+
|
|
256
|
+
# validate data dictionary
|
|
257
|
+
cls._schema(data)
|
|
258
|
+
|
|
259
|
+
name = data.get("name")
|
|
260
|
+
id = data.get("id", "")
|
|
261
|
+
loc_type = LocationType(data.get("type", "unknown"))
|
|
262
|
+
disassembled_name = data.get("disassembledName", None)
|
|
263
|
+
coord = data.get("coord", [])
|
|
264
|
+
match_quality = data.get("matchQuality", 0)
|
|
265
|
+
transports = [TransportType(x) for x in data.get("productClasses", [])]
|
|
266
|
+
properties = data.get("properties", {})
|
|
267
|
+
parent = Location.from_dict(data.get("parent"))
|
|
268
|
+
stops = [Location.from_dict(x) for x in data.get("assignedStops", [])]
|
|
269
|
+
|
|
270
|
+
return Location(
|
|
271
|
+
data,
|
|
272
|
+
name,
|
|
273
|
+
loc_type,
|
|
274
|
+
id,
|
|
275
|
+
coord,
|
|
276
|
+
transports,
|
|
277
|
+
parent,
|
|
278
|
+
stops,
|
|
279
|
+
properties,
|
|
280
|
+
disassembled_name,
|
|
281
|
+
match_quality,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass(frozen=True)
|
|
286
|
+
class Departure(_Base):
|
|
287
|
+
location: Location = field(repr=False)
|
|
288
|
+
line_name: str
|
|
289
|
+
route: str
|
|
290
|
+
origin: Location
|
|
291
|
+
destination: Location
|
|
292
|
+
transport: TransportType
|
|
293
|
+
planned_time: datetime
|
|
294
|
+
estimated_time: datetime | None = None
|
|
295
|
+
infos: list[dict] = field(default_factory=[])
|
|
296
|
+
|
|
297
|
+
_schema = SCHEMA_DEPARTURE
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def from_dict(cls, data: dict) -> Self | None:
|
|
301
|
+
if not data:
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
if not isinstance(data, dict):
|
|
305
|
+
raise ValueError(f"Expected a dictionary, provided {type(data)}")
|
|
306
|
+
|
|
307
|
+
# validate data dictionary
|
|
308
|
+
cls._schema(data)
|
|
309
|
+
|
|
310
|
+
location = Location.from_dict(data.get("location"))
|
|
311
|
+
planned_time = parse_datetime(data.get("departureTimePlanned", None))
|
|
312
|
+
estimated_time = parse_datetime(data.get("departureTimeEstimated", None))
|
|
313
|
+
infos = data.get("infos")
|
|
314
|
+
|
|
315
|
+
line = Line.from_dict(data.get("transportation"))
|
|
316
|
+
line_name = line.name
|
|
317
|
+
transport = line.product
|
|
318
|
+
origin = line.origin
|
|
319
|
+
destination = line.destination
|
|
320
|
+
route = line.description
|
|
321
|
+
|
|
322
|
+
return Departure(
|
|
323
|
+
data,
|
|
324
|
+
location,
|
|
325
|
+
line_name,
|
|
326
|
+
route,
|
|
327
|
+
origin,
|
|
328
|
+
destination,
|
|
329
|
+
transport,
|
|
330
|
+
planned_time,
|
|
331
|
+
estimated_time,
|
|
332
|
+
infos,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@dataclass(frozen=True)
|
|
337
|
+
class Line(_Base):
|
|
338
|
+
id: str
|
|
339
|
+
name: str
|
|
340
|
+
description: str
|
|
341
|
+
product: TransportType
|
|
342
|
+
operator: str
|
|
343
|
+
destination: Location
|
|
344
|
+
origin: Location
|
|
345
|
+
properties: dict = field(default_factory={})
|
|
346
|
+
|
|
347
|
+
_schema = SCHEMA_TRANSPORTATION
|
|
348
|
+
|
|
349
|
+
@classmethod
|
|
350
|
+
def from_dict(cls, data: dict) -> Self | None:
|
|
351
|
+
if not data:
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
if not isinstance(data, dict):
|
|
355
|
+
raise ValueError(f"Expected a dictionary, provided {type(data)}")
|
|
356
|
+
|
|
357
|
+
# validate data dictionary
|
|
358
|
+
cls._schema(data)
|
|
359
|
+
|
|
360
|
+
id = data.get("id")
|
|
361
|
+
name = data.get("number")
|
|
362
|
+
# disassembled_name = data.get("disassembledName")
|
|
363
|
+
# number = data.get("number")
|
|
364
|
+
description = data.get("description")
|
|
365
|
+
product = TransportType(data.get("product").get("class"))
|
|
366
|
+
operator = data.get("operator").get("name")
|
|
367
|
+
destination = Location.from_dict(data.get("destination"))
|
|
368
|
+
origin = Location.from_dict(data.get("origin"))
|
|
369
|
+
properties = data.get("properties", {})
|
|
370
|
+
|
|
371
|
+
return Line(
|
|
372
|
+
data,
|
|
373
|
+
id,
|
|
374
|
+
name,
|
|
375
|
+
description,
|
|
376
|
+
product,
|
|
377
|
+
operator,
|
|
378
|
+
destination,
|
|
379
|
+
origin,
|
|
380
|
+
properties,
|
|
381
|
+
)
|
apyefa/exceptions.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class EfaConnectionError(IOError):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EfaParameterError(ValueError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EfaParseError(AttributeError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EfaResponseInvalid(ValueError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EfaFormatNotSupported(Exception):
|
|
18
|
+
pass
|
apyefa/helpers.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import re
|
|
3
|
+
from zoneinfo import ZoneInfo
|
|
4
|
+
|
|
5
|
+
TZ_INFO = ZoneInfo("Europe/Berlin")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_datetime(date: str) -> datetime.datetime:
|
|
9
|
+
if not date:
|
|
10
|
+
return None
|
|
11
|
+
|
|
12
|
+
dt = datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S%z")
|
|
13
|
+
|
|
14
|
+
return dt.astimezone(TZ_INFO)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_date(date: str) -> datetime.date:
|
|
18
|
+
if not date:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
return datetime.datetime.strptime(date, "%Y-%m-%d").date()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def to_date(date: datetime.date) -> datetime.date:
|
|
25
|
+
return datetime.datetime.strftime(date, "%Y-%m-%d")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_datetime(date: str):
|
|
29
|
+
if not isinstance(date, str) or not date:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
pattern = re.compile(r"\d{8} \d{2}:\d{2}")
|
|
33
|
+
|
|
34
|
+
if not bool(pattern.match(date)):
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
date_str = date.split(" ")[0]
|
|
38
|
+
time_str = date.split(" ")[1]
|
|
39
|
+
|
|
40
|
+
return is_date(date_str) and is_time(time_str)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_date(date: str):
|
|
44
|
+
if not isinstance(date, str) or not date:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
pattern = re.compile(r"(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})")
|
|
48
|
+
|
|
49
|
+
if not bool(pattern.match(date)):
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
matches = pattern.search(date)
|
|
53
|
+
|
|
54
|
+
month = int(matches.group("month"))
|
|
55
|
+
day = int(matches.group("day"))
|
|
56
|
+
|
|
57
|
+
return (month >= 1 and month <= 12) and (day >= 1 and day <= 31)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_time(time: str):
|
|
61
|
+
if not isinstance(time, str) or not time:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
pattern = re.compile(r"(?P<hours>\d{2}):(?P<minutes>\d{2})")
|
|
65
|
+
|
|
66
|
+
if not bool(pattern.match(time)):
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
matches = pattern.search(time)
|
|
70
|
+
|
|
71
|
+
hours = int(matches.group("hours"))
|
|
72
|
+
minutes = int(matches.group("minutes"))
|
|
73
|
+
|
|
74
|
+
return (hours >= 0 and hours < 24) and (minutes >= 0 and minutes < 60)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Alex Jung
|
|
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.
|