SSPM 1.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.
sspm/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .plugin import Plugin
2
+ from .plugin_base import PluginBase
3
+ from .pluginmanager import PluginManager
4
+
5
+ __all__ = ["PluginManager", "Plugin", "PluginBase"]
sspm/plugin.py ADDED
@@ -0,0 +1,110 @@
1
+ from configparser import ConfigParser
2
+
3
+ from packaging.version import Version
4
+
5
+ # Defaults used for any [Documentation] field a plugin's info file doesn't specify.
6
+ _DOCUMENTATION_DEFAULTS = {
7
+ "Author": "Unknown",
8
+ "Version": "0.0",
9
+ "Website": "None",
10
+ "Copyright": "Unknown",
11
+ "Description": "",
12
+ }
13
+
14
+
15
+ class Plugin:
16
+ """
17
+ Represents a single plugin: the metadata parsed from its info file (name, version, author,
18
+ copyright, website, description) plus, once PluginManager has instantiated it, the
19
+ plugin_object itself.
20
+
21
+ Metadata is backed by a configparser.ConfigParser (see `details`). Values are read with a
22
+ fallback, so a plugin's info file only needs to specify what it wants to override -- the
23
+ fallback values themselves are never written back into `details`, so `details` reflects
24
+ only what was actually parsed from the info file (plus whatever `name`/`path`/etc. setters
25
+ have explicitly been called with).
26
+ """
27
+
28
+ def __init__(self, plugin_name, plugin_path):
29
+ # Used as the fallback for name/path if `details` doesn't specify Core/Name or
30
+ # Core/Module -- see __read().
31
+ self.__default_name = plugin_name
32
+ self.__default_path = plugin_path
33
+ self.__details = ConfigParser()
34
+
35
+ # Set by PluginManager once the plugin's module class has been instantiated; None until then.
36
+ self.plugin_object = None
37
+
38
+ def __read(self, section: str, option: str, default: str) -> str:
39
+ return self.__details.get(section, option, fallback=default)
40
+
41
+ def __write(self, section: str, option: str, value: str) -> None:
42
+ if not self.__details.has_section(section):
43
+ self.__details.add_section(section)
44
+ self.__details.set(section, option, value)
45
+
46
+ @property
47
+ def details(self) -> ConfigParser:
48
+ return self.__details
49
+
50
+ @details.setter
51
+ def details(self, config_details: ConfigParser) -> None:
52
+ self.__details = config_details
53
+
54
+ @property
55
+ def name(self):
56
+ return self.__read("Core", "Name", self.__default_name)
57
+
58
+ @name.setter
59
+ def name(self, name):
60
+ self.__write("Core", "Name", name)
61
+
62
+ @property
63
+ def path(self):
64
+ return self.__read("Core", "Module", self.__default_path)
65
+
66
+ @path.setter
67
+ def path(self, path):
68
+ self.__write("Core", "Module", path)
69
+
70
+ @property
71
+ def version(self):
72
+ return Version(self.__read("Documentation", "Version", _DOCUMENTATION_DEFAULTS["Version"]))
73
+
74
+ @version.setter
75
+ def version(self, ver):
76
+ if isinstance(ver, Version):
77
+ ver = str(ver)
78
+ self.__write("Documentation", "Version", ver)
79
+
80
+ @property
81
+ def author(self):
82
+ return self.__read("Documentation", "Author", _DOCUMENTATION_DEFAULTS["Author"])
83
+
84
+ @author.setter
85
+ def author(self, author):
86
+ self.__write("Documentation", "Author", author)
87
+
88
+ @property
89
+ def copyright(self):
90
+ return self.__read("Documentation", "Copyright", _DOCUMENTATION_DEFAULTS["Copyright"])
91
+
92
+ @copyright.setter
93
+ def copyright(self, copyright):
94
+ self.__write("Documentation", "Copyright", copyright)
95
+
96
+ @property
97
+ def website(self):
98
+ return self.__read("Documentation", "Website", _DOCUMENTATION_DEFAULTS["Website"])
99
+
100
+ @website.setter
101
+ def website(self, website):
102
+ self.__write("Documentation", "Website", website)
103
+
104
+ @property
105
+ def description(self):
106
+ return self.__read("Documentation", "Description", _DOCUMENTATION_DEFAULTS["Description"])
107
+
108
+ @description.setter
109
+ def description(self, description):
110
+ self.__write("Documentation", "Description", description)
sspm/plugin_base.py ADDED
@@ -0,0 +1,6 @@
1
+ class PluginBase:
2
+ """
3
+ Marker base class for plugins. Subclass this (directly or via your own intermediate base class,
4
+ e.g. to define a plugin category) so PluginManager can find and categorize your plugin.
5
+ """
6
+ pass
sspm/pluginmanager.py ADDED
@@ -0,0 +1,193 @@
1
+ import importlib.util
2
+ import inspect
3
+ import logging
4
+ import pathlib
5
+ import sys
6
+ import threading
7
+ from configparser import ConfigParser
8
+
9
+ from .plugin import Plugin
10
+
11
+
12
+ class PluginManager:
13
+ """
14
+ A basic python plugin manager. Plugins live one per subdirectory of a user-specified plugin
15
+ folder, each with a Python module and an info file (see plugin.py) describing it.
16
+
17
+ Safe to use from multiple threads: all reads and mutations of the manager's internal plugin
18
+ state (import_plugins, rescan, remove_plugin, get_active_plugin, active_plugins,
19
+ categorized_plugins) are serialized with an internal lock, and active_plugins/
20
+ categorized_plugins return snapshots rather than live references, so a caller iterating one
21
+ won't be affected by a concurrent import_plugins()/rescan()/remove_plugin() call.
22
+ """
23
+
24
+ def __init__(self, plugin_folder: str, plugin_info_ext="info", log=logging):
25
+ """
26
+ This is the initialization method. User must set the plugin folder location. They can also set their own
27
+ logging should they have their own.
28
+ :param plugin_folder: Base dir for plugins.
29
+ :param plugin_info_ext: Allows user to define a custom extension for their plugin info files.
30
+ :param log: Python logging.
31
+ """
32
+ self.__logging = log
33
+ self.__plugin_folder = pathlib.Path(plugin_folder)
34
+ self.__plugin_config_ext = plugin_info_ext
35
+ self.__imported_plugins = dict()
36
+ self.__categorized_plugins = dict()
37
+ self.__lock = threading.Lock()
38
+
39
+ # Follows the recipe in the Python docs for importing a source file directly:
40
+ # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
41
+ # This is a better and more flexible solution than the python path modification. This also allows for
42
+ # subdirectories within the provided plugin directory.
43
+ def __load_plugin_src(self, name: str, plugin_path: str):
44
+ """
45
+ Import a Python source file and return the loaded module.
46
+
47
+ :param name: The name for the loaded module. It may contain `.` and even characters
48
+ that would normally not be allowed (e.g., `-`).
49
+ :param plugin_path: The full path to the source file. It may contain characters like
50
+ `.` or `-`.
51
+ :return: The imported module.
52
+ :raises ImportError: If the file cannot be imported (e.g., if it's not a `.py` file or
53
+ if it does not exist).
54
+ :raises Exception: Any exception that is raised while executing the module (e.g., a
55
+ `SyntaxError`). These are errors made by the author of the module!
56
+ """
57
+ spec = importlib.util.spec_from_file_location(name, plugin_path)
58
+ if spec is None:
59
+ raise ImportError(f"Could not load spec for module '{name}' at: {plugin_path}")
60
+ module = importlib.util.module_from_spec(spec)
61
+ sys.modules[name] = module
62
+ try:
63
+ spec.loader.exec_module(module)
64
+ except FileNotFoundError as e:
65
+ raise ImportError(f"{e.strerror}: {plugin_path}") from e
66
+ return module
67
+
68
+ def __import_plugin_info(self, plugin_info_path: pathlib.Path) -> None:
69
+ """
70
+ Loads a single plugin from its info file and, if successful, registers it. Shared by
71
+ import_plugins() and rescan(); callers are expected to hold self.__lock.
72
+ """
73
+ config_parser = ConfigParser()
74
+ config_parser.read(plugin_info_path)
75
+ try:
76
+ if config_parser.get("Core", "Module"):
77
+ module_name = config_parser.get("Core", "Module")
78
+ module_parent_dir = plugin_info_path.parent
79
+ module_path = module_parent_dir.joinpath(f"{module_name}.py").resolve().as_posix()
80
+ importlib.invalidate_caches()
81
+ module = self.__load_plugin_src(module_name, module_path)
82
+ if module:
83
+ if config_parser.get("Core", "Name"):
84
+ plugin = Plugin(config_parser.get("Core", "Name"), module_path)
85
+ plugin.details = config_parser
86
+
87
+ cls_names = [m[0] for m in inspect.getmembers(module, inspect.isclass) if
88
+ m[1].__module__ == module.__name__]
89
+
90
+ cls_name = [cls for cls in cls_names
91
+ if "PluginBase" in [x.__name__ for x in type.mro(getattr(module, cls))]]
92
+
93
+ if cls_name.__len__() != 1:
94
+ self.__logging.error(
95
+ f"Illegal action: expected exactly one plugin class in module. "
96
+ f"The plugin file: {module_name} contains {len(cls_name)} plugin classes.")
97
+ return
98
+
99
+ _cls = getattr(module, cls_name[0])
100
+ plugin.plugin_object = _cls()
101
+
102
+ self.__imported_plugins[plugin.name] = plugin
103
+ self.__categorize_plugin(plugin)
104
+
105
+ self.__logging.info(f"{plugin.name} imported successfully.")
106
+ else:
107
+ self.__logging.warning(f"Missing Module for Plugin: {plugin_info_path.absolute().as_posix()}")
108
+ else:
109
+ raise ValueError("Plugin Config file is missing necessary parameters.")
110
+ except ModuleNotFoundError:
111
+ self.__logging.warning(f"Missing Module for Plugin: {plugin_info_path.absolute().as_posix()}")
112
+
113
+ def import_plugins(self) -> None:
114
+ """
115
+ Imports all plugins in the user-defined plugin directory. Each subdirectory containing a matching
116
+ plugin info file is expected to hold exactly one module defining exactly one PluginBase subclass;
117
+ plugins that don't meet that contract, or whose module can't be found, are skipped and logged
118
+ rather than raised.
119
+
120
+ Every matching plugin is (re)imported and its plugin_object (re)instantiated, even ones
121
+ already active from a previous call -- to pick up only newly-added plugins without
122
+ disturbing already-loaded ones, use rescan() instead.
123
+ """
124
+ with self.__lock:
125
+ for plugin_info_path in pathlib.Path(self.__plugin_folder).glob(f"**/*.{self.__plugin_config_ext}"):
126
+ self.__import_plugin_info(plugin_info_path)
127
+
128
+ def rescan(self) -> None:
129
+ """
130
+ Scans the plugin directory for plugins that aren't already active and imports those,
131
+ leaving already-active plugins (matched by the Name in their info file) untouched -- their
132
+ existing plugin_object is not re-instantiated or replaced. Use this to pick up plugins
133
+ dropped into the plugin directory after the initial import_plugins() call, while the
134
+ process is still running.
135
+ """
136
+ with self.__lock:
137
+ for plugin_info_path in pathlib.Path(self.__plugin_folder).glob(f"**/*.{self.__plugin_config_ext}"):
138
+ config_parser = ConfigParser()
139
+ config_parser.read(plugin_info_path)
140
+ name = config_parser.get("Core", "Name", fallback=None)
141
+ if name is not None and name in self.__imported_plugins:
142
+ continue
143
+ self.__import_plugin_info(plugin_info_path)
144
+
145
+ def __categorize_plugin(self, plugin) -> None:
146
+ """
147
+ Indexes a plugin under the name of each base class its plugin_object directly subclasses, so it can
148
+ later be looked up by category via categorized_plugins. Callers are expected to hold self.__lock.
149
+ :param plugin: The Plugin to index.
150
+ """
151
+ plugin_types = [x.__name__ for x in type(plugin.plugin_object).__bases__]
152
+
153
+ for plugin_type in plugin_types:
154
+ if plugin_type not in self.__categorized_plugins:
155
+ self.__categorized_plugins[plugin_type] = {plugin.name: plugin}
156
+ else:
157
+ plugins_store = self.__categorized_plugins.get(plugin_type)
158
+ plugins_store[plugin.name] = plugin
159
+
160
+ def get_active_plugin(self, plugin_name: str) -> Plugin:
161
+ """
162
+ Retrieves a plugin from the active plugins.
163
+ :param plugin_name: User defined name of plugin from plugin info file
164
+ :return: The matching Plugin, or None if no plugin with that name is active.
165
+ """
166
+ with self.__lock:
167
+ return self.__imported_plugins.get(plugin_name)
168
+
169
+ def remove_plugin(self, plugin_name) -> None:
170
+ """
171
+ Removes a loaded plugin.
172
+ :param plugin_name: the name of the plugin to be removed
173
+ :return: None
174
+ :raises KeyError: if no plugin with that name is currently active.
175
+ """
176
+ with self.__lock:
177
+ del self.__imported_plugins[plugin_name]
178
+ self.__logging.info(f"{plugin_name} removed successfully.")
179
+
180
+ @property
181
+ def active_plugins(self):
182
+ """A snapshot dict of all successfully imported plugins, keyed by plugin name."""
183
+ with self.__lock:
184
+ return dict(self.__imported_plugins)
185
+
186
+ @property
187
+ def categorized_plugins(self):
188
+ """
189
+ A snapshot dict mapping each PluginBase subclass name to a dict of the plugins (keyed by
190
+ plugin name) whose plugin_object directly subclasses it.
191
+ """
192
+ with self.__lock:
193
+ return {plugin_type: dict(plugins) for plugin_type, plugins in self.__categorized_plugins.items()}
sspm/version.py ADDED
@@ -0,0 +1,27 @@
1
+ import sys
2
+ from collections import namedtuple
3
+
4
+ _SSPM_VERSION_CLS = namedtuple("_SSPM_VERSION_CLS", "major minor bugfix pre post dev")
5
+
6
+ version_tuple = _SSPM_VERSION_CLS(1, 0, 0, None, None, None)
7
+
8
+ version = "{0.major:d}.{0.minor:d}.{0.bugfix:d}".format(version_tuple)
9
+ if version_tuple.pre is not None:
10
+ version += version_tuple.pre
11
+ if version_tuple.post is not None:
12
+ version += ".post{0.post:d}".format(version_tuple)
13
+ if version_tuple.dev is not None:
14
+ version += ".dev{0.dev:d}".format(version_tuple)
15
+
16
+ info = """\
17
+ Summary of the SSPM configuration
18
+ ---------------------------------
19
+
20
+ SSPM %(sspm)s
21
+ Python %(python)s
22
+ Platform %(platform)s
23
+ """ % {
24
+ 'sspm': version,
25
+ 'python': sys.version,
26
+ 'platform': sys.platform,
27
+ }
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: SSPM
3
+ Version: 1.0.0
4
+ Summary: SSPM: a simple, hands-off Python plugin manager
5
+ Author-email: Steven Nix <stevencnix@gmail.com>
6
+ License: PolyForm-Noncommercial-1.0.0
7
+ Project-URL: Homepage, https://github.com/stevencnix/sspm
8
+ Project-URL: Repository, https://github.com/stevencnix/sspm
9
+ Project-URL: Changelog, https://github.com/stevencnix/sspm/blob/develop/CHANGELOG
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: packaging
21
+ Provides-Extra: dev
22
+ Requires-Dist: ruff; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # Super Simple Plugin Manager - SSPM
26
+
27
+ ## About SSPM
28
+
29
+ Super Simple Plugin Manager - SSPM is a lightweight, hands-off Python plugin manager. Drop a
30
+ plugin's code and a small `.info` file describing it into a plugins directory, and SSPM discovers,
31
+ imports, categorizes, and instantiates it for you — no extra registration step needed.
32
+
33
+ ## Requirements
34
+
35
+ - Python 3.10+
36
+ - [packaging](https://pypi.org/project/packaging/) (installed automatically as a dependency)
37
+
38
+ ## Installation
39
+
40
+ The easiest way to install is to use pip:
41
+
42
+ pip install SSPM
43
+
44
+ or if you have cloned the repo:
45
+
46
+ cd <path to repo>
47
+ pip install .
48
+
49
+ ## Writing a Plugin
50
+
51
+ A plugin is a folder containing two files: a Python module with your plugin's code, and an `.info` file
52
+ (a standard `configparser`/INI file) describing it. Each module must define **exactly one** class that
53
+ subclasses `PluginBase` (or a subclass of it) — SSPM uses that to find and categorize your plugin.
54
+
55
+ ```
56
+ plugins/
57
+ └── add_plugin/
58
+ ├── add_plugin.py
59
+ └── add.info
60
+ ```
61
+
62
+ `add_plugin.py`:
63
+
64
+ ```python
65
+ from sspm import PluginBase
66
+
67
+
68
+ class AddPlugin(PluginBase):
69
+
70
+ def operation(self, x, y):
71
+ return x + y
72
+ ```
73
+
74
+ `add.info`:
75
+
76
+ ```ini
77
+ [Core]
78
+ Name = Add Plugin
79
+ Module = add_plugin
80
+
81
+ [Documentation]
82
+ Author = Your Name
83
+ Version = 1.0.0
84
+ Website = None
85
+ Description = Plugin performing basic add operation
86
+ ```
87
+
88
+ `Core.Name` and `Core.Module` are required (`Module` is the module filename, without the `.py`
89
+ extension). Everything under `[Documentation]` is optional — SSPM fills in sensible defaults
90
+ (`Unknown`, `0.0`, etc.) for anything you leave out.
91
+
92
+ You can subclass `PluginBase` further to create your own plugin categories (e.g. a
93
+ `CalculatorPluginBase`) and group related plugins together — see
94
+ [`examples/`](examples) for a full working example with multiple plugin types.
95
+
96
+ ## Basic Usage
97
+
98
+ 1. Initialize the plugin manager, pointing it at the folder containing your plugins:
99
+
100
+ ```python
101
+ from sspm import PluginManager
102
+
103
+ plugin_manager = PluginManager(plugin_folder="./plugins")
104
+ ```
105
+
106
+ 2. Import the plugins in the plugins directory:
107
+
108
+ ```python
109
+ plugin_manager.import_plugins()
110
+ ```
111
+
112
+ 3. Get a specific imported plugin by the name from its `.info` file:
113
+
114
+ ```python
115
+ plugin = plugin_manager.get_active_plugin("Add Plugin")
116
+ result = plugin.plugin_object.operation(1, 2)
117
+ ```
118
+
119
+ or get all active plugins:
120
+
121
+ ```python
122
+ plugins = plugin_manager.active_plugins
123
+ ```
124
+
125
+ or get plugins grouped by their `PluginBase` category:
126
+
127
+ ```python
128
+ calculator_plugins = plugin_manager.categorized_plugins.get("CalculatorPluginBase")
129
+ ```
130
+
131
+ A `Plugin` object exposes the metadata from its `.info` file (`name`, `version`, `author`,
132
+ `website`, `copyright`, `description`) as well as `plugin_object`, the instantiated plugin class
133
+ itself.
134
+
135
+ ## Runtime Plugins
136
+
137
+ `PluginManager` is meant to be used for plugins that come and go while your process is running,
138
+ not just a one-time scan at startup:
139
+
140
+ - **Adding a plugin after startup**: drop a new plugin's folder into the plugin directory, then
141
+ call `plugin_manager.rescan()`. Unlike `import_plugins()`, `rescan()` only loads plugins that
142
+ aren't already active — it won't re-instantiate or replace a plugin that's already loaded.
143
+ - **Removing a plugin**: `plugin_manager.remove_plugin("Plugin Name")` (raises `KeyError` if no
144
+ plugin with that name is active).
145
+ - **Thread safety**: `PluginManager` is safe to use from multiple threads. All reads and writes go
146
+ through an internal lock, and `active_plugins`/`categorized_plugins` return a snapshot rather
147
+ than a live reference, so code iterating one of them won't be affected by a concurrent
148
+ `import_plugins()`, `rescan()`, or `remove_plugin()` call on another thread.
149
+
150
+ ## More Examples
151
+
152
+ See the [`examples/`](examples) directory for a runnable calculator example with `Add` and
153
+ `Subtract` plugins, and [`CHANGELOG`](CHANGELOG) for release history.
154
+
155
+ ## License
156
+
157
+ SSPM is licensed under the [PolyForm Noncommercial License 1.0.0](LICENSE) — free to use, modify,
158
+ and distribute for any noncommercial purpose. Commercial use requires a separate license from the
159
+ author.
@@ -0,0 +1,10 @@
1
+ sspm/__init__.py,sha256=UFO8Z4la54nrwuULkcNcbylwfOIfwIAxvhNPatlpiKI,157
2
+ sspm/plugin.py,sha256=_lewbtSt3zFMmswXmADMApdm1Ep3SucJ-zKlawLLqY0,3654
3
+ sspm/plugin_base.py,sha256=5fcXZkhIFm9TLnLsdiD2C_lykeB0B6GvBqrDsQ_tIfQ,235
4
+ sspm/pluginmanager.py,sha256=eQQjVW6Fw4puC0Gur24ggrpkrlWQB-vxoouWIQACHOo,9440
5
+ sspm/version.py,sha256=U0uwb3JvG1jJIXdCHkdc1Ue6wYSbF_obwNVRfdUP-HU,743
6
+ sspm-1.0.0.dist-info/licenses/LICENSE,sha256=YAVWzDDyrpBjlrKa69ZGW6HUyVChzZawY2T7b-6RdVU,4639
7
+ sspm-1.0.0.dist-info/METADATA,sha256=QQV0HkfVBDd63xYNSJ_tO5i8pQmIPPyIB-vDAeLAC0A,5109
8
+ sspm-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ sspm-1.0.0.dist-info/top_level.txt,sha256=x80ciy1FOK_VHkApkgYSVIkkw9jF288FnVL6fDrL4q8,5
10
+ sspm-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,75 @@
1
+ # PolyForm Noncommercial License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
+
5
+ Required Notice: Copyright Steven Nix (https://github.com/stevencnix/sspm)
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncommercial Purposes
34
+
35
+ Any noncommercial purpose is a permitted purpose.
36
+
37
+ ## Personal Uses
38
+
39
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
40
+
41
+ ## Noncommercial Organizations
42
+
43
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
44
+
45
+ ## Fair Use
46
+
47
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
48
+
49
+ ## No Other Rights
50
+
51
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
52
+
53
+ ## Patent Defense
54
+
55
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
56
+
57
+ ## Violations
58
+
59
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
60
+
61
+ ## No Liability
62
+
63
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
64
+
65
+ ## Definitions
66
+
67
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
68
+
69
+ **You** refers to the individual or entity agreeing to these terms.
70
+
71
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
72
+
73
+ **Your licenses** are all the licenses granted to you for the software under these terms.
74
+
75
+ **Use** means anything you do with the software requiring one of your licenses.
@@ -0,0 +1 @@
1
+ sspm