task-logging 0.0.2__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,20 @@
1
+ from .models import ContextLogMessage, ExceptionLogMessage, OneTaskLog, TaskLogIn
2
+ from .task_logger import (
3
+ ClassFunctionLogger,
4
+ FunctionLogger,
5
+ TaskLogger,
6
+ TaskLoggerFactory,
7
+ )
8
+ from .task_logging_database_interface import TaskLoggingDatabaseInterface
9
+
10
+ __all__: list[str] = [
11
+ "ClassFunctionLogger",
12
+ "ContextLogMessage",
13
+ "ExceptionLogMessage",
14
+ "FunctionLogger",
15
+ "OneTaskLog",
16
+ "TaskLogIn",
17
+ "TaskLogger",
18
+ "TaskLoggerFactory",
19
+ "TaskLoggingDatabaseInterface",
20
+ ]
task_logging/models.py ADDED
@@ -0,0 +1,45 @@
1
+ from datetime import datetime
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ # should be same with database_service.models.task_logging
8
+ # otherwise you will have to write a lot of code to convert between them
9
+ class ExceptionLogMessage(BaseModel):
10
+ name: str = Field(..., title="Exception Name", description="Exception Name")
11
+ details: str = Field(
12
+ ..., title="Exception Details", description="Exception Details"
13
+ )
14
+ stack_trace: str = Field(..., title="Stack Trace", description="Stack Trace")
15
+ locals_dict: dict[str, Any] = Field(
16
+ ..., title="Locals Dict", description="Locals Dict"
17
+ )
18
+
19
+
20
+ class ContextLogMessage(BaseModel):
21
+ hostname: str = Field(..., title="Hostname", description="Hostname")
22
+ process_id: int = Field(..., title="Process ID", description="Process ID")
23
+ thread_name: str = Field(..., title="Thread Name", description="Thread Name")
24
+ module_name: str = Field(..., title="Module Name", description="Module Name")
25
+ function_name: str = Field(..., title="Function Name", description="Function Name")
26
+ line_no: int = Field(..., title="Line Number", description="Line Number")
27
+ filename: str = Field(..., title="Filename", description="Filename")
28
+ # function_args: str = Field(..., title="Function Args", description="Function Args")
29
+ thread_id: int = Field(..., title="Thread ID", description="Thread ID")
30
+ stack_depth: int = Field(..., title="Stack Depth", description="Stack Depth")
31
+
32
+
33
+ class TaskLogIn(BaseModel):
34
+ level: str = Field(..., title="Log Level", description="Log Level")
35
+ message: str = Field(..., title="Message", description="Message")
36
+ ctx_msg: ContextLogMessage = Field(
37
+ ..., title="Context Message", description="Context Message"
38
+ )
39
+ exc_msg: ExceptionLogMessage | None = Field(
40
+ title="Exception Message", description="Exception Message"
41
+ )
42
+
43
+
44
+ class OneTaskLog(TaskLogIn):
45
+ logged_at: datetime = Field(default=..., title="Timestamp", description="Timestamp")
task_logging/py.typed ADDED
File without changes
@@ -0,0 +1,438 @@
1
+ import functools
2
+ import inspect
3
+ import logging
4
+ import os
5
+ import socket
6
+ import sys
7
+ import threading
8
+
9
+ # 就像一个门卫一样,守在函数的入口和出口,记录函数的执行时间和返回值
10
+ import time
11
+ import traceback
12
+ from collections.abc import Callable
13
+ from logging import Logger
14
+ from types import TracebackType
15
+ from typing import Any, ParamSpec, TypeVar, cast
16
+
17
+ from .models import ContextLogMessage, ExceptionLogMessage, TaskLogIn
18
+ from .task_logging_database_interface import TaskLoggingDatabaseInterface
19
+
20
+ P = ParamSpec("P") # Represents all parameters
21
+ R = TypeVar("R") # Represents return value
22
+
23
+ # 感觉这个不好使啊
24
+ # 因为每次都要在参数里面加上一个logger
25
+ # 应该可以实现一个 FunctionLogger
26
+ # 然后它的参数是一个logger
27
+ # 然后它有一个log的装饰器
28
+
29
+
30
+ class FunctionLogger:
31
+ def __init__(self, logger: Logger) -> None:
32
+ self._logger: Logger = logger
33
+
34
+ # 必须定义在这个模块里面,才能正常工作!
35
+ # TODO:日志级别
36
+ def log_func(
37
+ self, level: int = logging.INFO
38
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
39
+ def log_func_impl(func: Callable[P, R]) -> Callable[P, R]:
40
+ """
41
+ A decorator that logs the input parameters and return value of a function.
42
+ """
43
+
44
+ @functools.wraps(wrapped=func)
45
+ def wrapper(*args, **kwargs) -> Any: # type: ignore
46
+ # Log function call with parameters
47
+ # The stacklevel parameter is designed to specify how many levels up the stack to look to find the source location of the logging call. By setting stacklevel=2, the logger will report the location of the call to the decorated function, not the location inside the decorator.
48
+ # 日志一定要选择既精简,又遍于阅读 搜索的方式
49
+ # 比如,这里有几种风格
50
+ # ENTER fund_name
51
+ # [ENTER] func_name
52
+ # ENTER [func_name]
53
+ # 额外的那个括号不仅没有什么用,还会让搜索变得复杂,比如我只想搜索ENTER 我只想搜索func_name
54
+ # 但是如果我想同时搜索 ENTER func_name就会变的复杂,因为[]在regex中是特殊字符,
55
+ # 单纯的大写已经足够醒目了,不需要额外的符号
56
+
57
+ # 现在要根据level来选择调用不同的日志函数,哦 fuck
58
+ # 这个要怎么写
59
+ # 这个函数可以写在 先不急
60
+
61
+ self._logger.log(
62
+ level=level,
63
+ msg=f"Enter {func.__name__}, args: {args}, kwargs: {kwargs}.",
64
+ # stacklevel=2,
65
+ )
66
+
67
+ start_time = time.time() # Capture start time
68
+ result = func(*args, **kwargs)
69
+ end_time = time.time()
70
+
71
+ # Log function return value
72
+ execution_ms = (end_time - start_time) * 1000
73
+ self._logger.log(
74
+ level=level,
75
+ msg=f"EXIT {func.__name__}, return: {result}, cost: {execution_ms:.3f} ms.",
76
+ # stacklevel=2,
77
+ )
78
+ return result
79
+
80
+ return wrapper
81
+
82
+ return log_func_impl
83
+
84
+
85
+ class ClassFunctionLogger:
86
+ """
87
+ A decorator factory for logging class methods that have access to a class logger instance.
88
+ This decorator will use the class's logger attribute to log method entry and exit.
89
+ """
90
+
91
+ def __init__(self, logger_attr: str = "_logger") -> None:
92
+ """
93
+ Initialize with the name of the logger attribute in the class.
94
+
95
+ Args:
96
+ logger_attr: The attribute name of the logger in the class (default: "_logger")
97
+ """
98
+ self._logger_attr = logger_attr
99
+
100
+ def log_func(
101
+ self, level: int = logging.INFO
102
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
103
+ """
104
+ Create a decorator that logs method calls using the class's logger.
105
+
106
+ Args:
107
+ level: The logging level to use
108
+
109
+ Returns:
110
+ A decorator function
111
+ """
112
+
113
+ def decorator(func: Callable[P, R]) -> Callable[P, R]:
114
+ """
115
+ Decorator that logs the input parameters and return value of a class method.
116
+ """
117
+
118
+ @functools.wraps(wrapped=func)
119
+ def wrapper(self_obj: Any, *args: P.args, **kwargs: P.kwargs) -> R:
120
+ # Get the logger from the class instance
121
+ # If not found, just call the function normally
122
+ if not hasattr(self_obj, self._logger_attr):
123
+ return func(self_obj, *args, **kwargs)
124
+
125
+ logger = getattr(self_obj, self._logger_attr)
126
+
127
+ # Log method entry
128
+ logger.log(
129
+ level=level,
130
+ msg=f"ENTER {func.__name__}, args: {args}, kwargs: {kwargs}.",
131
+ )
132
+
133
+ # Execute the method and measure execution time
134
+ start_time = time.time()
135
+ result = func(self_obj, *args, **kwargs)
136
+ execution_ms = (time.time() - start_time) * 1000
137
+
138
+ # Log method exit
139
+ logger.log(
140
+ level=level,
141
+ msg=f"EXIT {func.__name__}, return: {result}, cost: {execution_ms:.3f} ms.",
142
+ )
143
+ return result
144
+
145
+ return wrapper # type: ignore
146
+
147
+ return decorator
148
+
149
+
150
+ # TODO: 咱们把service给改成module名字?
151
+ class TaskLogger(logging.Logger):
152
+ # python的logger本来就有名字
153
+ def __init__(
154
+ self,
155
+ task_logging_db: TaskLoggingDatabaseInterface,
156
+ task_id: str,
157
+ service_name: str,
158
+ level: int = logging.NOTSET,
159
+ ) -> None:
160
+ super().__init__(name=service_name, level=level)
161
+ self._service_name: str = service_name
162
+ self._task_id: str = task_id
163
+ self._db = task_logging_db
164
+ # 插入日志到数据库
165
+ # 标准库有关于日志级别的定义,那咱们就用这个了,也不需要自己来定义
166
+
167
+ def debug(self, msg, *args, **kwargs) -> None: # type: ignore
168
+ super().debug(msg, *args, **kwargs)
169
+ self._append_task_log(level=logging.DEBUG, message=msg)
170
+
171
+ def info(self, msg, *args, **kwargs) -> None: # type: ignore
172
+ super().info(msg, *args, **kwargs)
173
+ self._append_task_log(level=logging.INFO, message=msg)
174
+
175
+ def warning(self, msg, *args, **kwargs) -> None: # type: ignore
176
+ super().warning(msg, *args, **kwargs)
177
+ self._append_task_log(level=logging.WARNING, message=msg)
178
+
179
+ # warn is deprecated
180
+
181
+ def error(self, msg, *args, **kwargs) -> None: # type: ignore
182
+ super().error(msg, *args, **kwargs)
183
+ self._append_task_log(level=logging.ERROR, message=msg)
184
+
185
+ def exception(self, msg, *args, exc_info=True, **kwargs) -> None: # type: ignore
186
+ """
187
+ Convenience method for logging an ERROR with exception information.
188
+ """
189
+ self.error(msg, *args, exc_info=exc_info, **kwargs)
190
+
191
+ def critical(self, msg, *args, **kwargs) -> None: # type: ignore
192
+ super().critical(msg, *args, **kwargs)
193
+ self._append_task_log(level=logging.CRITICAL, message=msg)
194
+
195
+ def fatal(self, msg, *args, **kwargs) -> None: # type: ignore
196
+ """
197
+ Don't use this method, use critical() instead.
198
+ """
199
+ self.critical(msg, *args, **kwargs)
200
+
201
+ def log(self, level, msg, *args, **kwargs) -> None: # type: ignore
202
+ super().log(level, msg, *args, **kwargs)
203
+ self._append_task_log(level=level, message=msg)
204
+
205
+ # 其实都这样了,不如直接用一个函数来实现
206
+ # 但是不行,我们的函数必须是本模块的
207
+ # 不然get context就无法正常工作了
208
+
209
+ # 如果我把迭代器定义在这里怎么样?
210
+
211
+ # 咱们要不用一个Model来定义一下Exception
212
+ # 防止以后还想加东西,结果就要改函数签名
213
+
214
+ # 现在咱们可以去掉这两个函数
215
+ #
216
+ # def error_with_exception(
217
+ # self, msg: str, exc_msg: ExceptionLogMessage, *args, **kwargs
218
+ # ) -> None:
219
+ # self._append_exceptional_task_log(
220
+ # level=logging.ERROR,
221
+ # msg=msg,
222
+ # exc_msg=exc_msg,
223
+ # )
224
+ # super().critical(msg, *args, **kwargs)
225
+
226
+ # def critical_with_exception(
227
+ # self, msg: str, esc_msg: ExceptionLogMessage, *args, **kwargs
228
+ # ) -> None:
229
+ # self._append_exceptional_task_log(
230
+ # level=logging.CRITICAL,
231
+ # msg=msg,
232
+ # exc_msg=esc_msg,
233
+ # )
234
+ # super().critical(msg, *args, **kwargs)
235
+
236
+ def _append_task_log(self, level: int, message: str) -> None:
237
+ ctx_msg = self._get_context()
238
+ exc_msg = self._get_exception_log_message()
239
+
240
+ if self.isEnabledFor(level=level):
241
+ self._db.append_task_log(
242
+ service_name=self._service_name,
243
+ task_id=self._task_id,
244
+ task_log=TaskLogIn(
245
+ level=logging.getLevelName(level=level),
246
+ message=message,
247
+ ctx_msg=ctx_msg,
248
+ exc_msg=exc_msg,
249
+ ),
250
+ )
251
+
252
+ # def _append_exceptional_task_log(
253
+ # self, level: int, msg: str, exc_msg: ExceptionLogMessage
254
+ # ) -> None:
255
+ # self._db.append_exception_task_log(
256
+ # service_name=self._service_name,
257
+ # task_id=self._task_id,
258
+ # log_level=logging.getLevelName(level=level),
259
+ # message=msg,
260
+ # exc_msg=exc_msg,
261
+ # )
262
+
263
+ # 这里应该返回异常信息的类就行了
264
+ def _get_exception_log_message(self) -> ExceptionLogMessage | None:
265
+ """
266
+ 在 except 块中调用时,返回包含异常实例、类名和完整错误堆栈的字典。
267
+ 若不在异常上下文中调用,抛出 ValueError。
268
+ """
269
+ # # 获取当前异常信息
270
+ exc_type, exc_value, exc_traceback = sys.exc_info()
271
+
272
+ if not exc_type:
273
+ return None
274
+
275
+ # # 确保在异常上下文中调用
276
+ # if exc_type is None:
277
+ # # 倒也不用
278
+ # # 如果不是在异常上下文中,那么就不管这个了
279
+ # # raise ValueError("log_error() must be called within an except block")
280
+ # return None
281
+
282
+ # # 生成完整的错误堆栈字符串
283
+ stack_trace = "".join(
284
+ traceback.format_exception(exc_type, exc_value, exc_traceback)
285
+ )
286
+
287
+ """
288
+ 获取包含异常信息及触发点上下文的日志消息
289
+ 返回包含以下信息的对象:
290
+ - 异常类名
291
+ - 异常详细信息
292
+ - 完整堆栈跟踪
293
+ - 异常触发点的全局变量(快照)
294
+ - 异常触发点的局部变量(快照)
295
+ """
296
+ # exc_type, exc_value, exc_traceback = sys.exc_info()
297
+
298
+ # 获取最底层的异常堆栈帧
299
+ deepest_traceback = exc_traceback
300
+ while cast(TracebackType, deepest_traceback).tb_next:
301
+ deepest_traceback = cast(TracebackType, deepest_traceback).tb_next
302
+
303
+ # 获取异常触发点的帧信息
304
+ exception_frame = cast(TracebackType, deepest_traceback).tb_frame
305
+
306
+ # 获取触发点的变量信息(转换为字典避免引用问题)
307
+ # frame_globals = (
308
+ # dict(exception_frame.f_globals) if exception_frame.f_globals else {}
309
+ # )
310
+ frame_locals: dict[str, Any] = (
311
+ dict(exception_frame.f_locals) if exception_frame.f_locals else {}
312
+ )
313
+
314
+ # 把这个frame全部变成str
315
+ # 因为有些变量是无法json化的 直接写入 pydantic model 话,在执行json化的时候会报错
316
+ frame_locals_str = {k: repr(v) for k, v in frame_locals.items()}
317
+
318
+ # print("-----------------------------------------")
319
+ # # globals几乎没用,而且很长很长
320
+ # print("frame_globals:", frame_globals)
321
+ # # frame_locals: {'a': 1, 'b': 2} 这个非常有用了
322
+ # print("frame_locals:", frame_locals)
323
+ # print("-----------------------------------------")
324
+
325
+ # return ExceptionLogMessage(
326
+ # name=exc_type.__name__,
327
+ # details=str(exc_value),
328
+ # stack_trace="".join(traceback.format_exception(exc_type, exc_value, exc_traceback)),
329
+ # globals=frame_globals,
330
+ # locals=frame_locals
331
+ # )
332
+
333
+ return ExceptionLogMessage(
334
+ name=exc_type.__name__,
335
+ details=str(exc_value),
336
+ stack_trace=stack_trace,
337
+ # locals_dict=frame_locals,
338
+ locals_dict=frame_locals_str,
339
+ )
340
+
341
+ # 这个函数不容易测试,因为只有从log进行测试才能成功
342
+ # 这个设计如果可以改进一下就好了
343
+ # 我知道了,应该实现一个函数
344
+ # 教过 get context
345
+ def _get_context(self) -> ContextLogMessage:
346
+ # 不断的向上寻找
347
+ # 知道找到
348
+ stacks = inspect.stack(context=0)
349
+ # 从上往下找 调用栈是随着调用顺序从下往上的
350
+ # 所以我们从上往下找
351
+ # 不管怎么说
352
+ # 调用我们的函数就是logger里面的那几个
353
+ # 哪些接口是固定的
354
+
355
+ ctx_msg = ContextLogMessage(
356
+ hostname=socket.gethostname(),
357
+ process_id=os.getpid(),
358
+ thread_name=threading.current_thread().name,
359
+ filename="",
360
+ module_name="",
361
+ function_name="",
362
+ line_no=-1,
363
+ # Thread identifier of this thread or None if it has not been started.
364
+ #
365
+ # This is a nonzero integer. See the get_ident() function. Thread
366
+ # identifiers may be recycled when a thread exits and another thread is
367
+ # created. The identifier is available even after the thread has exited.
368
+ thread_id=threading.current_thread().ident or 0,
369
+ stack_depth=len(stacks),
370
+ # function_args="",
371
+ )
372
+
373
+ logger_module = self.__class__.__module__ # 获取Logger所在模块名
374
+
375
+ # 遍历调用栈,寻找第一个非Logger模块的栈帧
376
+ for frame_info in inspect.stack(context=0):
377
+ frame_module = frame_info.frame.f_globals.get("__name__", "")
378
+ if frame_module == logger_module:
379
+ continue # 跳过Logger自身模块的帧
380
+
381
+ # 找到调用者帧,填充信息
382
+ ctx_msg.filename = frame_info.filename
383
+ ctx_msg.module_name = frame_module
384
+ ctx_msg.function_name = frame_info.function
385
+ ctx_msg.line_no = frame_info.lineno
386
+ break
387
+
388
+ # for i, stack in enumerate(stacks):
389
+ # if stack.function in [
390
+ # "info",
391
+ # "error",
392
+ # "warning",
393
+ # "debug",
394
+ # "critical",
395
+ # ]:
396
+ # # 如果下一层是wrapper 那就就下去两层
397
+
398
+ # # 这个时候下一层就是
399
+ # if i + 1 >= len(stacks):
400
+ # break
401
+
402
+ # frame = stacks[i + 1]
403
+ # if frame.function == "wrapper" and i + 2 < len(stacks):
404
+ # frame = stacks[i + 2]
405
+ # # # print(frame)
406
+ # # print(frame.filename)
407
+ # # print(frame.function)
408
+ # # print(frame.lineno)
409
+ # # print(frame.frame.f_globals["__name__"])
410
+ # # # 我保留可以获得全部局部变量的能力,就先不写了
411
+ # # # 等0.2.0 写成 enter exit log
412
+ # # # 这个其实也不重要了
413
+ # # print(frame.frame.f_locals)
414
+ # ctx_msg.module_name = frame.frame.f_globals["__name__"]
415
+ # ctx_msg.function_name = frame.function
416
+ # ctx_msg.line_number = frame.lineno
417
+ # ctx_msg.filename = frame.filename
418
+
419
+ # break
420
+ # # else:
421
+ # # 找不到函数的调用信息
422
+ # # 那么就只天蝎hostnane pid tid 即可
423
+
424
+ return ctx_msg
425
+
426
+ # 这个函数大概是通过栈来实现的
427
+
428
+
429
+ class TaskLoggerFactory:
430
+ def __init__(self, task_logging_db: TaskLoggingDatabaseInterface) -> None:
431
+ self._task_logging_db = task_logging_db
432
+
433
+ def new(self, service_name: str, task_id: str) -> TaskLogger:
434
+ return TaskLogger(
435
+ task_logging_db=self._task_logging_db,
436
+ service_name=service_name,
437
+ task_id=task_id,
438
+ )
@@ -0,0 +1,19 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from .models import OneTaskLog, TaskLogIn
4
+
5
+
6
+ class TaskLoggingDatabaseInterface(ABC):
7
+ @abstractmethod
8
+ def append_task_log(
9
+ self, service_name: str, task_id: str, task_log: TaskLogIn
10
+ ) -> None:
11
+ pass
12
+
13
+ @abstractmethod
14
+ def get_all_logs(self, service_name: str, task_id: str) -> list[OneTaskLog]:
15
+ pass
16
+
17
+ @abstractmethod
18
+ def delete_all_logs(self, service_name: str, task_id: str) -> None:
19
+ pass
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: task-logging
3
+ Version: 0.0.2
4
+ Project-URL: Homepage, https://github.com/im-zhong/task-logging
5
+ Project-URL: Repository, https://github.com/im-zhong/task-logging
6
+ Project-URL: Issues, https://github.com/im-zhong/task-logging/issues
7
+ Author-email: zhangzhong <im.zhong@outlook.com>
8
+ License-File: LICENSE
9
+ Keywords: logging,task
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: <4.0,>=3.12
15
+ Requires-Dist: pydantic<3.0.0,>=2.10.6
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Task Logging
19
+
20
+ ## Introduction
21
+
22
+ Task logging is a library for logging in the distributed task-based system.
23
+
24
+ ## Installation
25
+
26
+ `pip install task-logging`
@@ -0,0 +1,9 @@
1
+ task_logging/__init__.py,sha256=HB27U14nngJiITyqxPgiCDzCU_zjv2kbgkLedUBTczQ,510
2
+ task_logging/models.py,sha256=cwrF6jKs36FsW87kif0hCJNwNq-_xgCAdWVDcvmiaIE,2007
3
+ task_logging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ task_logging/task_logger.py,sha256=IK2grj69ogCmCHjQQigi2pSKaUeQao3JYCWYn5Aicy0,16629
5
+ task_logging/task_logging_database_interface.py,sha256=NurssvKIMjsTaMTgsdfNjCoZ7G0StWeUO5AWhVvMcko,483
6
+ task_logging-0.0.2.dist-info/METADATA,sha256=_-1A8JDFa67qGUCaBdufMdqhGcAmSXFSoGWRqLWAAE8,808
7
+ task_logging-0.0.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ task_logging-0.0.2.dist-info/licenses/LICENSE,sha256=IeR4_rPBHfdP9ZKD2E6_5lsQwl8ugMUtIA2YVdVmGVY,1065
9
+ task_logging-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 im-zhong
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.