graphrag-common 3.0.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,4 @@
1
+ # Copyright (c) 2024 Microsoft Corporation.
2
+ # Licensed under the MIT License
3
+
4
+ """GraphRAG Common package."""
@@ -0,0 +1,8 @@
1
+ # Copyright (c) 2024 Microsoft Corporation.
2
+ # Licensed under the MIT License
3
+
4
+ """The GraphRAG config module."""
5
+
6
+ from graphrag_common.config.load_config import ConfigParsingError, load_config
7
+
8
+ __all__ = ["ConfigParsingError", "load_config"]
@@ -0,0 +1,205 @@
1
+ # Copyright (c) 2024 Microsoft Corporation.
2
+ # Licensed under the MIT License
3
+
4
+ """Load configuration."""
5
+
6
+ import json
7
+ import os
8
+ from collections.abc import Callable
9
+ from pathlib import Path
10
+ from string import Template
11
+ from typing import Any, TypeVar
12
+
13
+ import yaml
14
+ from dotenv import load_dotenv
15
+
16
+ T = TypeVar("T", covariant=True)
17
+
18
+ _default_config_files = ["settings.yaml", "settings.yml", "settings.json"]
19
+
20
+
21
+ class ConfigParsingError(ValueError):
22
+ """Configuration Parsing Error."""
23
+
24
+ def __init__(self, msg: str) -> None:
25
+ """Initialize the ConfigParsingError."""
26
+ super().__init__(msg)
27
+
28
+
29
+ def _get_config_file_path(config_dir_or_file: Path) -> Path:
30
+ """Resolve the config path from the given directory or file."""
31
+ config_dir_or_file = Path(config_dir_or_file)
32
+
33
+ if config_dir_or_file.is_file():
34
+ return config_dir_or_file
35
+
36
+ if not config_dir_or_file.is_dir():
37
+ msg = f"Invalid config path: {config_dir_or_file} is not a directory"
38
+ raise FileNotFoundError(msg)
39
+
40
+ for file in _default_config_files:
41
+ if (config_dir_or_file / file).is_file():
42
+ return config_dir_or_file / file
43
+
44
+ msg = f"No 'settings.[yaml|yml|json]' config file found in directory: {config_dir_or_file}"
45
+ raise FileNotFoundError(msg)
46
+
47
+
48
+ def _load_dotenv(env_file_path: Path, required: bool) -> None:
49
+ """Load the .env file if it exists."""
50
+ if not env_file_path.is_file():
51
+ if not required:
52
+ return
53
+ msg = f"dot_env_path not found: {env_file_path}"
54
+ raise FileNotFoundError(msg)
55
+ load_dotenv(env_file_path)
56
+
57
+
58
+ def _parse_json(data: str) -> dict[str, Any]:
59
+ """Parse JSON data."""
60
+ return json.loads(data)
61
+
62
+
63
+ def _parse_yaml(data: str) -> dict[str, Any]:
64
+ """Parse YAML data."""
65
+ return yaml.safe_load(data)
66
+
67
+
68
+ def _get_parser_for_file(file_path: str | Path) -> Callable[[str], dict[str, Any]]:
69
+ """Get the parser for the given file path."""
70
+ file_path = Path(file_path).resolve()
71
+ match file_path.suffix.lower():
72
+ case ".json":
73
+ return _parse_json
74
+ case ".yaml" | ".yml":
75
+ return _parse_yaml
76
+ case _:
77
+ msg = (
78
+ f"Failed to parse, {file_path}. Unsupported file extension, "
79
+ + f"{file_path.suffix}. Pass in a custom config_parser argument or "
80
+ + "use one of the supported file extensions, .json, .yaml, .yml, .toml."
81
+ )
82
+ raise ConfigParsingError(msg)
83
+
84
+
85
+ def _parse_env_variables(text: str) -> str:
86
+ """Parse environment variables in the configuration text."""
87
+ try:
88
+ return Template(text).substitute(os.environ)
89
+ except KeyError as error:
90
+ msg = f"Environment variable not found: {error}"
91
+ raise ConfigParsingError(msg) from error
92
+
93
+
94
+ def _recursive_merge_dicts(dest: dict[str, Any], src: dict[str, Any]) -> None:
95
+ """Recursively merge two dictionaries in place."""
96
+ for key, value in src.items():
97
+ if isinstance(value, dict):
98
+ if isinstance(dest.get(key), dict):
99
+ _recursive_merge_dicts(dest[key], value)
100
+ else:
101
+ dest[key] = value
102
+ else:
103
+ dest[key] = value
104
+
105
+
106
+ def load_config(
107
+ config_initializer: Callable[..., T],
108
+ config_path: str | Path | None = None,
109
+ overrides: dict[str, Any] | None = None,
110
+ set_cwd: bool = True,
111
+ parse_env_vars: bool = True,
112
+ load_dot_env_file: bool = True,
113
+ dot_env_path: str | Path | None = None,
114
+ config_parser: Callable[[str], dict[str, Any]] | None = None,
115
+ file_encoding: str = "utf-8",
116
+ ) -> T:
117
+ """Load configuration from a file.
118
+
119
+ Parameters
120
+ ----------
121
+ config_initializer : Callable[..., T]
122
+ Configuration constructor/initializer.
123
+ Should accept **kwargs to initialize the configuration,
124
+ e.g., Config(**kwargs).
125
+ config_path : str | Path | None, optional (default=None)
126
+ Path to the configuration directory containing settings.[yaml|yml|json].
127
+ Or path to a configuration file itself.
128
+ If None, search the current working directory for
129
+ settings.[yaml|yml|json].
130
+ overrides : dict[str, Any] | None, optional (default=None)
131
+ Configuration overrides.
132
+ Useful for overriding configuration settings programmatically,
133
+ perhaps from CLI flags.
134
+ set_cwd : bool, optional (default=True)
135
+ Whether to set the current working directory to the directory
136
+ containing the configuration file. Helpful for resolving relative paths
137
+ in the configuration file.
138
+ parse_env_vars : bool, optional (default=True)
139
+ Whether to parse environment variables in the configuration text.
140
+ load_dot_env_file : bool, optional (default=True)
141
+ Whether to load the .env file prior to parsing environment variables.
142
+ dot_env_path : str | Path | None, optional (default=None)
143
+ Optional .env file to load prior to parsing env variables.
144
+ If None and load_dot_env_file is True, looks for a .env file in the
145
+ same directory as the config file.
146
+ config_parser : Callable[[str], dict[str, Any]] | None, optional (default=None)
147
+ function to parse the configuration text, (str) -> dict[str, Any].
148
+ If None, the parser is inferred from the file extension.
149
+ Supported extensions: .json, .yaml, .yml.
150
+ file_encoding : str, optional (default="utf-8")
151
+ File encoding to use when reading the configuration file.
152
+
153
+ Returns
154
+ -------
155
+ T
156
+ The initialized configuration object.
157
+
158
+ Raises
159
+ ------
160
+ FileNotFoundError
161
+ - If the config file is not found.
162
+ - If the .env file is not found when parse_env_vars is True and dot_env_path is provided.
163
+
164
+ ConfigParsingError
165
+ - If an environment variable is not found when parsing env variables.
166
+ - If there was a problem merging the overrides with the configuration.
167
+ - If parser=None and load_config was unable to determine how to parse
168
+ the file based on the file extension.
169
+ - If the parser fails to parse the configuration text.
170
+ """
171
+ config_path = Path(config_path).resolve() if config_path else Path.cwd()
172
+ config_path = _get_config_file_path(config_path)
173
+
174
+ file_contents = config_path.read_text(encoding=file_encoding)
175
+
176
+ if parse_env_vars:
177
+ if load_dot_env_file:
178
+ required = dot_env_path is not None
179
+ dot_env_path = (
180
+ Path(dot_env_path) if dot_env_path else config_path.parent / ".env"
181
+ )
182
+ _load_dotenv(dot_env_path, required=required)
183
+ file_contents = _parse_env_variables(file_contents)
184
+
185
+ if config_parser is None:
186
+ config_parser = _get_parser_for_file(config_path)
187
+
188
+ config_data: dict[str, Any] = {}
189
+ try:
190
+ config_data = config_parser(file_contents)
191
+ except Exception as error:
192
+ msg = f"Failed to parse config_path: {config_path}. Error: {error}"
193
+ raise ConfigParsingError(msg) from error
194
+
195
+ if overrides is not None:
196
+ try:
197
+ _recursive_merge_dicts(config_data, overrides)
198
+ except Exception as error:
199
+ msg = f"Failed to merge overrides with config_path: {config_path}. Error: {error}"
200
+ raise ConfigParsingError(msg) from error
201
+
202
+ if set_cwd:
203
+ os.chdir(config_path.parent)
204
+
205
+ return config_initializer(**config_data)
@@ -0,0 +1,8 @@
1
+ # Copyright (c) 2024 Microsoft Corporation.
2
+ # Licensed under the MIT License
3
+
4
+ """The GraphRAG factory module."""
5
+
6
+ from graphrag_common.factory.factory import Factory, ServiceScope
7
+
8
+ __all__ = ["Factory", "ServiceScope"]
@@ -0,0 +1,113 @@
1
+ # Copyright (c) 2025 Microsoft Corporation.
2
+ # Licensed under the MIT License
3
+
4
+ """Factory ABC."""
5
+
6
+ from abc import ABC
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass
9
+ from typing import Any, ClassVar, Generic, Literal, TypeVar
10
+
11
+ from graphrag_common.hasher import hash_data
12
+
13
+ T = TypeVar("T", covariant=True)
14
+
15
+ ServiceScope = Literal["singleton", "transient"]
16
+
17
+
18
+ @dataclass
19
+ class _ServiceDescriptor(Generic[T]):
20
+ """Descriptor for a service."""
21
+
22
+ scope: ServiceScope
23
+ initializer: Callable[..., T]
24
+
25
+
26
+ class Factory(ABC, Generic[T]):
27
+ """Abstract base class for factories."""
28
+
29
+ _instance: ClassVar["Factory | None"] = None
30
+
31
+ def __new__(cls, *args: Any, **kwargs: Any) -> "Factory[T]":
32
+ """Create a new instance of Factory if it does not exist."""
33
+ if cls._instance is None:
34
+ cls._instance = super().__new__(cls, *args, **kwargs)
35
+ return cls._instance
36
+
37
+ def __init__(self):
38
+ if not hasattr(self, "_initialized"):
39
+ self._service_initializers: dict[str, _ServiceDescriptor[T]] = {}
40
+ self._initialized_services: dict[str, T] = {}
41
+ self._initialized = True
42
+
43
+ def __contains__(self, strategy: str) -> bool:
44
+ """Check if a strategy is registered."""
45
+ return strategy in self._service_initializers
46
+
47
+ def keys(self) -> list[str]:
48
+ """Get a list of registered strategy names."""
49
+ return list(self._service_initializers.keys())
50
+
51
+ def register(
52
+ self,
53
+ strategy: str,
54
+ initializer: Callable[..., T],
55
+ scope: ServiceScope = "transient",
56
+ ) -> None:
57
+ """
58
+ Register a new service.
59
+
60
+ Args
61
+ ----
62
+ strategy: str
63
+ The name of the strategy.
64
+ initializer: Callable[..., T]
65
+ A callable that creates an instance of T.
66
+ scope: ServiceScope (default: "transient")
67
+ The scope of the service ("singleton" or "transient").
68
+ Singleton services are cached based on their init args
69
+ so that the same instance is returned for the same init args.
70
+ """
71
+ self._service_initializers[strategy] = _ServiceDescriptor(scope, initializer)
72
+
73
+ def create(self, strategy: str, init_args: dict[str, Any] | None = None) -> T:
74
+ """
75
+ Create a service instance based on the strategy.
76
+
77
+ Args
78
+ ----
79
+ strategy: str
80
+ The name of the strategy.
81
+ init_args: dict[str, Any] | None
82
+ A dictionary of keyword arguments to pass to the service initializer.
83
+
84
+ Returns
85
+ -------
86
+ An instance of T.
87
+
88
+ Raises
89
+ ------
90
+ ValueError: If the strategy is not registered.
91
+ """
92
+ if strategy not in self._service_initializers:
93
+ msg = f"Strategy '{strategy}' is not registered. Registered strategies are: {', '.join(list(self._service_initializers.keys()))}"
94
+ raise ValueError(msg)
95
+
96
+ # Delete entries with value None
97
+ # That way services can have default values
98
+ init_args = {k: v for k, v in (init_args or {}).items() if v is not None}
99
+
100
+ service_descriptor = self._service_initializers[strategy]
101
+ if service_descriptor.scope == "singleton":
102
+ cache_key = hash_data({
103
+ "strategy": strategy,
104
+ "init_args": init_args,
105
+ })
106
+
107
+ if cache_key not in self._initialized_services:
108
+ self._initialized_services[cache_key] = service_descriptor.initializer(
109
+ **init_args
110
+ )
111
+ return self._initialized_services[cache_key]
112
+
113
+ return service_descriptor.initializer(**(init_args or {}))
@@ -0,0 +1,18 @@
1
+ # Copyright (c) 2024 Microsoft Corporation.
2
+ # Licensed under the MIT License
3
+
4
+ """The GraphRAG hasher module."""
5
+
6
+ from graphrag_common.hasher.hasher import (
7
+ Hasher,
8
+ hash_data,
9
+ make_yaml_serializable,
10
+ sha256_hasher,
11
+ )
12
+
13
+ __all__ = [
14
+ "Hasher",
15
+ "hash_data",
16
+ "make_yaml_serializable",
17
+ "sha256_hasher",
18
+ ]
@@ -0,0 +1,59 @@
1
+ # Copyright (c) 2024 Microsoft Corporation.
2
+ # Licensed under the MIT License
3
+
4
+ """The GraphRAG hasher module."""
5
+
6
+ import hashlib
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+ Hasher = Callable[[str], str]
13
+ """Type alias for a hasher function (data: str) -> str."""
14
+
15
+
16
+ def sha256_hasher(data: str) -> str:
17
+ """Generate a SHA-256 hash for the input data."""
18
+ return hashlib.sha256(data.encode("utf-8")).hexdigest()
19
+
20
+
21
+ def make_yaml_serializable(data: Any) -> Any:
22
+ """Convert data to a YAML-serializable format."""
23
+ if isinstance(data, (list, tuple)):
24
+ return tuple(make_yaml_serializable(item) for item in data)
25
+
26
+ if isinstance(data, set):
27
+ return tuple(sorted(make_yaml_serializable(item) for item in data))
28
+
29
+ if isinstance(data, dict):
30
+ return tuple(
31
+ sorted((key, make_yaml_serializable(value)) for key, value in data.items())
32
+ )
33
+
34
+ return str(data)
35
+
36
+
37
+ def hash_data(data: Any, *, hasher: Hasher | None = None) -> str:
38
+ """Hash the input data dictionary using the specified hasher function.
39
+
40
+ Args
41
+ ----
42
+ data: dict[str, Any]
43
+ The input data to be hashed.
44
+ The input data is serialized using yaml
45
+ to support complex data structures such as classes and functions.
46
+ hasher: Hasher | None (default: sha256_hasher)
47
+ The hasher function to use. (data: str) -> str
48
+
49
+ Returns
50
+ -------
51
+ str
52
+ The resulting hash of the input data.
53
+
54
+ """
55
+ hasher = hasher or sha256_hasher
56
+ try:
57
+ return hasher(yaml.dump(data, sort_keys=True))
58
+ except TypeError:
59
+ return hasher(yaml.dump(make_yaml_serializable(data), sort_keys=True))
File without changes
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: graphrag-common
3
+ Version: 3.0.0
4
+ Summary: Common utilities and types for GraphRAG
5
+ Project-URL: Source, https://github.com/microsoft/graphrag
6
+ Author: Mónica Carvajal
7
+ Author-email: Alonso Guevara Fernández <alonsog@microsoft.com>, Andrés Morales Esquivel <andresmor@microsoft.com>, Chris Trevino <chtrevin@microsoft.com>, David Tittsworth <datittsw@microsoft.com>, Dayenne de Souza <ddesouza@microsoft.com>, Derek Worthen <deworthe@microsoft.com>, Gaudy Blanco Meneses <gaudyb@microsoft.com>, Ha Trinh <trinhha@microsoft.com>, Jonathan Larson <jolarso@microsoft.com>, Josh Bradley <joshbradley@microsoft.com>, Kate Lytvynets <kalytv@microsoft.com>, Kenny Zhang <zhangken@microsoft.com>, Nathan Evans <naevans@microsoft.com>, Rodrigo Racanicci <rracanicci@microsoft.com>, Sarah Smith <smithsarah@microsoft.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Python: <3.14,>=3.11
15
+ Requires-Dist: python-dotenv~=1.0
16
+ Requires-Dist: pyyaml~=6.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # GraphRAG Common
20
+
21
+ This package provides utility modules for GraphRAG, including a flexible factory system for dependency injection and service registration, and a comprehensive configuration loading system with Pydantic model support, environment variable substitution, and automatic file discovery.
22
+
23
+ ## Factory module
24
+
25
+ The Factory class provides a flexible dependency injection pattern that can register and create instances of classes implementing a common interface using string-based strategies. It supports both transient scope (creates new instances on each request) and singleton scope (returns the same instance after first creation).
26
+
27
+ ```python
28
+ from abc import ABC, abstractmethod
29
+
30
+ from graphrag_common.factory import Factory
31
+
32
+ class SampleABC(ABC):
33
+
34
+ @abstractmethod
35
+ def get_value(self) -> str:
36
+ msg = "Subclasses must implement the get_value method."
37
+ raise NotImplementedError(msg)
38
+
39
+
40
+ class ConcreteClass(SampleABC):
41
+ def __init__(self, value: str):
42
+ self._value = value
43
+
44
+ def get_value(self) -> str:
45
+ return self._value
46
+
47
+ class SampleFactory(Factory[SampleABC]):
48
+ """A Factory for SampleABC classes."""
49
+
50
+ factory = SampleFactory()
51
+
52
+ # Registering transient services
53
+ # A new one is created for every request
54
+ factory.register("some_strategy", ConcreteTestClass)
55
+
56
+ trans1 = factory.create("some_strategy", {"value": "test1"})
57
+ trans2 = factory.create("some_strategy", {"value": "test2"})
58
+
59
+ assert trans1 is not trans2
60
+ assert trans1.get_value() == "test1"
61
+ assert trans2.get_value() == "test2"
62
+
63
+ # Registering singleton services
64
+ # After first creation, the same one is returned every time
65
+ factory.register("some_other_strategy", ConcreteTestClass, scope="singleton")
66
+
67
+ single1 = factory.create("some_other_strategy", {"value": "singleton"})
68
+ single2 = factory.create("some_other_strategy", {"value": "ignored"})
69
+
70
+ assert single1 is single2
71
+ assert single1.get_value() == "singleton"
72
+ assert single2.get_value() == "singleton"
73
+ ```
74
+
75
+ ## Config module
76
+
77
+ The load_config function provides a comprehensive configuration loading system that automatically discovers and parses YAML/JSON config files into Pydantic models with support for environment variable substitution and .env file loading. It offers flexible features like config overrides, custom parsers for different file formats, and automatically sets the working directory to the config file location for relative path resolution.
78
+
79
+ ```python
80
+ from pydantic import BaseModel, Field
81
+ from graphrag_common.config import load_config
82
+
83
+ from pathlib import Path
84
+
85
+ class Logging(BaseModel):
86
+ """Test nested model."""
87
+
88
+ directory: str = Field(default="output/logs")
89
+ filename: str = Field(default="logs.txt")
90
+
91
+ class Config(BaseModel):
92
+ """Test configuration model."""
93
+
94
+ name: str = Field(description="Name field.")
95
+ logging: Logging = Field(description="Nested model field.")
96
+
97
+ # Basic - by default:
98
+ # - searches for Path.cwd() / settings.[yaml|yml|json]
99
+ # - sets the CWD to the directory containing the config file.
100
+ # so if no custom config path is provided than CWD remains unchanged.
101
+ # - loads config_directory/.env file
102
+ # - parses ${env} in the config file
103
+ config = load_config(Config)
104
+
105
+ # Custom file location
106
+ config = load_config(Config, "path_to_config_filename_or_directory_containing_settings.[yaml|yml|json]")
107
+
108
+ # Using a custom file extension with
109
+ # custom config parser (str) -> dict[str, Any]
110
+ config = load_config(
111
+ config_initializer=Config,
112
+ config_path="config.toml",
113
+ config_parser=lambda contents: toml.loads(contents) # Needs toml pypi package
114
+ )
115
+
116
+ # With overrides - provided values override whats in the config file
117
+ # Only overrides what is specified - recursively merges settings.
118
+ config = load_config(
119
+ config_initializer=Config,
120
+ overrides={
121
+ "name": "some name",
122
+ "logging": {
123
+ "filename": "my_logs.txt"
124
+ }
125
+ },
126
+ )
127
+
128
+ # By default, sets CWD to directory containing config file
129
+ # So custom config paths will change the CWD.
130
+ config = load_config(
131
+ config_initializer=Config,
132
+ config_path="some/path/to/config.yaml",
133
+ set_cwd=True # default
134
+ )
135
+
136
+ # now cwd == some/path/to
137
+ assert Path.cwd() == "some/path/to"
138
+
139
+ # And now throughout the codebase resolving relative paths in config
140
+ # will resolve relative to the config directory
141
+ Path(config.logging.directory) == "some/path/to/output/logs"
142
+
143
+ ```
@@ -0,0 +1,12 @@
1
+ graphrag_common/__init__.py,sha256=s68tLiBfUEDmUvx-HugWKjamxVULHs70LCS4DoMlU7I,109
2
+ graphrag_common/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ graphrag_common/config/__init__.py,sha256=SjctkqbxSprZSkpDa9s-1l39dyNOPqfPyGXNj33RSEo,241
4
+ graphrag_common/config/load_config.py,sha256=9pGAnRP8ZfzD5yDOBMqumBPnQ3acnDgaK3HtgnJTJmY,7361
5
+ graphrag_common/factory/__init__.py,sha256=UVp3KH-wSfJSXJpIyTxPSml6tPZWsyfNrUIJFlMJqPo,219
6
+ graphrag_common/factory/factory.py,sha256=rHXGkYZztKD_rid-kxYk8-2qio0O3htWRLlM1myNWMg,3749
7
+ graphrag_common/hasher/__init__.py,sha256=6CxkLbWyTFfQvwTkkzqtxUGUuUPEEquJ6_YXhWKj3xE,330
8
+ graphrag_common/hasher/hasher.py,sha256=JaPTwJnWru4Nhx9UXNdFwV8y7RSb-XLR6OOYIRJiovc,1709
9
+ graphrag_common-3.0.0.dist-info/METADATA,sha256=2MLK4ExHqIVBAp6bmf7sjZ2xv6MyVAwatT6GTXoZ_QI,5563
10
+ graphrag_common-3.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ graphrag_common-3.0.0.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
12
+ graphrag_common-3.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation.
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