metaspn-engine 0.0.1__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Leo Guinan
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.
@@ -0,0 +1,359 @@
1
+ Metadata-Version: 2.4
2
+ Name: metaspn-engine
3
+ Version: 0.0.1
4
+ Summary: Minimal signal processing engine for observable games
5
+ Author-email: Leo Guinan <leo@metaspn.network>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/metaspn/metaspn-engine
8
+ Project-URL: Documentation, https://docs.metaspn.network/engine
9
+ Project-URL: Repository, https://github.com/metaspn/metaspn-engine
10
+ Project-URL: Bug Tracker, https://github.com/metaspn/metaspn-engine/issues
11
+ Keywords: signal-processing,game-engine,transformation,pipeline,metaspn
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
26
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
27
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
28
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # MetaSPN Engine
32
+
33
+ **Minimal signal processing engine for observable games.**
34
+
35
+ Zero game semantics. Pure signal flow. Maximum composability.
36
+
37
+ ## Philosophy
38
+
39
+ The MetaSPN Engine is a **dumb pipe**. It knows nothing about podcasts, tweets, G1-G6 games, or any domain-specific concepts. It only knows:
40
+
41
+ - **Signals** flow in (typed, timestamped, immutable)
42
+ - **Pipelines** process them (pure functions, composable)
43
+ - **State** accumulates (typed, versioned)
44
+ - **Emissions** flow out (typed, traceable)
45
+
46
+ Everything else is built on top through game wrappers.
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install metaspn-engine
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ ```python
57
+ from dataclasses import dataclass
58
+ from datetime import datetime
59
+ from metaspn_engine import Signal, Emission, State, Pipeline, Engine
60
+ from metaspn_engine.transforms import emit_if, accumulate, update_state
61
+
62
+ # 1. Define your signal payload type
63
+ @dataclass(frozen=True)
64
+ class ScoreEvent:
65
+ user_id: str
66
+ score: float
67
+
68
+ # 2. Define your state type
69
+ @dataclass
70
+ class GameState:
71
+ total_signals: int = 0
72
+ running_total: float = 0.0
73
+ high_score: float = 0.0
74
+
75
+ # 3. Build your pipeline
76
+ pipeline = Pipeline([
77
+ # Count signals
78
+ accumulate("total_signals", lambda acc, _: (acc or 0) + 1),
79
+
80
+ # Track running total
81
+ accumulate("running_total", lambda acc, payload: (acc or 0) + payload.score),
82
+
83
+ # Update high score
84
+ update_state(lambda payload, state:
85
+ GameState(
86
+ total_signals=state.total_signals,
87
+ running_total=state.running_total,
88
+ high_score=max(state.high_score, payload.score)
89
+ ) if payload.score > state.high_score else state
90
+ ),
91
+
92
+ # Emit on high score
93
+ emit_if(
94
+ condition=lambda payload, state: payload.score > state.high_score,
95
+ emission_type="new_high_score",
96
+ payload_extractor=lambda payload, state: {
97
+ "user_id": payload.user_id,
98
+ "score": payload.score,
99
+ "previous_high": state.high_score,
100
+ }
101
+ ),
102
+ ])
103
+
104
+ # 4. Create engine
105
+ engine = Engine(
106
+ pipeline=pipeline,
107
+ initial_state=GameState(),
108
+ )
109
+
110
+ # 5. Process signals
111
+ signal = Signal(
112
+ payload=ScoreEvent(user_id="user_123", score=95.5),
113
+ timestamp=datetime.now(),
114
+ source="game_server",
115
+ )
116
+
117
+ emissions = engine.process(signal)
118
+
119
+ # 6. Check results
120
+ print(f"State: {engine.get_state()}")
121
+ print(f"Emissions: {emissions}")
122
+ ```
123
+
124
+ ## Documentation
125
+
126
+ Full documentation is in the **[docs](docs/)** folder:
127
+
128
+ | Document | Description |
129
+ |----------|-------------|
130
+ | [**Docs index**](docs/index.md) | Entry point — overview and links to everything |
131
+ | [Core concepts](docs/concepts.md) | Why the engine exists; signals, state, emissions |
132
+ | [Quick start tutorial](docs/quickstart.md) | Build your first game in ~15 minutes |
133
+ | [Mental model](docs/mental-model.md) | One-page architecture overview |
134
+ | [Designing games](docs/designing-games.md) | How to design new games; four questions, patterns |
135
+ | [API cheatsheet](docs/cheatsheet.md) | Quick reference for types and methods |
136
+ | [Architecture](docs/architecture.mermaid) · [Data flow](docs/flow.mermaid) | Mermaid diagrams |
137
+
138
+ **Examples:** [Podcast Game](examples/podcast_game.py) · [Creator Scoring Game](examples/creator_scoring_game.py)
139
+
140
+ ## Core Concepts
141
+
142
+ ### Signals
143
+
144
+ Immutable input events with typed payloads:
145
+
146
+ ```python
147
+ @dataclass(frozen=True)
148
+ class PodcastListen:
149
+ episode_id: str
150
+ duration_seconds: int
151
+ completed: bool
152
+
153
+ signal = Signal(
154
+ payload=PodcastListen("ep_123", 3600, True),
155
+ timestamp=datetime.now(),
156
+ source="overcast",
157
+ )
158
+ ```
159
+
160
+ ### Pipelines
161
+
162
+ Sequences of pure steps that process signals:
163
+
164
+ ```python
165
+ pipeline = Pipeline([
166
+ step_one,
167
+ step_two,
168
+ step_three,
169
+ ], name="my_pipeline")
170
+
171
+ # Pipelines are composable
172
+ combined = pipeline_a + pipeline_b
173
+
174
+ # Pipelines support branching
175
+ branched = pipeline.branch(
176
+ predicate=lambda s: s.payload.type == "podcast",
177
+ if_true=podcast_pipeline,
178
+ if_false=other_pipeline,
179
+ )
180
+ ```
181
+
182
+ ### State
183
+
184
+ Mutable accumulated context:
185
+
186
+ ```python
187
+ @dataclass
188
+ class MyState:
189
+ count: int = 0
190
+ items: list = field(default_factory=list)
191
+
192
+ state = State(value=MyState())
193
+ state.enable_history() # Track state transitions
194
+
195
+ # State updates happen through pipeline steps
196
+ # using update functions
197
+ ```
198
+
199
+ ### Emissions
200
+
201
+ Immutable output events:
202
+
203
+ ```python
204
+ emission = Emission(
205
+ payload={"score": 0.85},
206
+ caused_by=signal.signal_id, # Traceability
207
+ emission_type="score_computed",
208
+ )
209
+ ```
210
+
211
+ ## Transforms
212
+
213
+ Built-in step functions for common operations:
214
+
215
+ ```python
216
+ from metaspn_engine.transforms import (
217
+ # Mapping
218
+ map_to_emission,
219
+
220
+ # State management
221
+ accumulate,
222
+ set_state,
223
+ update_state,
224
+
225
+ # Windowing
226
+ window,
227
+ time_window,
228
+
229
+ # Emissions
230
+ emit,
231
+ emit_if,
232
+ emit_on_change,
233
+
234
+ # Control flow
235
+ branch,
236
+ merge,
237
+ sequence,
238
+
239
+ # Utilities
240
+ log,
241
+ tap,
242
+ )
243
+ ```
244
+
245
+ ## Building Game Wrappers
246
+
247
+ The engine is meant to be wrapped by game-specific packages:
248
+
249
+ ```python
250
+ # metaspn_podcast/game.py
251
+ from metaspn_engine import Signal, Pipeline, Engine
252
+ from metaspn_engine.protocols import GameProtocol
253
+
254
+ class PodcastGame:
255
+ """Podcast listening game built on MetaSPN Engine."""
256
+
257
+ name = "podcast"
258
+ version = "1.0.0"
259
+
260
+ def create_signal(self, data: dict) -> Signal[PodcastListen]:
261
+ return Signal(
262
+ payload=PodcastListen(
263
+ episode_id=data["episode_id"],
264
+ duration_seconds=data["duration"],
265
+ completed=data.get("completed", False),
266
+ ),
267
+ timestamp=datetime.fromisoformat(data["timestamp"]),
268
+ source=data.get("source", "unknown"),
269
+ )
270
+
271
+ def initial_state(self) -> PodcastState:
272
+ return PodcastState()
273
+
274
+ def pipeline(self) -> Pipeline:
275
+ return Pipeline([
276
+ track_listening,
277
+ compute_influence_vector,
278
+ update_trajectory,
279
+ emit_if_significant,
280
+ ])
281
+
282
+ # Usage
283
+ game = PodcastGame()
284
+ engine = Engine(
285
+ pipeline=game.pipeline(),
286
+ initial_state=game.initial_state(),
287
+ )
288
+
289
+ for event in listening_events:
290
+ signal = game.create_signal(event)
291
+ emissions = engine.process(signal)
292
+ ```
293
+
294
+ ## Architecture
295
+
296
+ ```
297
+ ┌─────────────────────────────────────────────────────────────┐
298
+ │ Game Wrappers │
299
+ │ (PodcastGame, TwitterGame, CreatorScoring, etc.) │
300
+ │ │
301
+ │ - Define signal types │
302
+ │ - Define state shape │
303
+ │ - Build domain-specific pipelines │
304
+ │ - Handle game-specific logic │
305
+ └─────────────────────────────────────────────────────────────┘
306
+
307
+ implements GameProtocol
308
+
309
+
310
+ ┌─────────────────────────────────────────────────────────────┐
311
+ │ metaspn-engine (core) │
312
+ │ │
313
+ │ Signal[T] ──▶ Pipeline[Steps] ──▶ Emission[U] │
314
+ │ │ │
315
+ │ reads/writes │
316
+ │ │ │
317
+ │ State[S] │
318
+ │ │
319
+ │ - Type-safe signal flow │
320
+ │ - Pure function pipelines │
321
+ │ - Versioned state management │
322
+ │ - Traceable emissions │
323
+ └─────────────────────────────────────────────────────────────┘
324
+ ```
325
+
326
+ ## Design Principles
327
+
328
+ 1. **Zero Dependencies** - The core engine has no external dependencies
329
+ 2. **Pure Functions** - All transforms are pure (state updates are explicit)
330
+ 3. **Type Safety** - Full generic type support for signals, state, emissions
331
+ 4. **Composability** - Pipelines compose, games compose, everything composes
332
+ 5. **Traceability** - Every emission traces back to its causing signal
333
+ 6. **Testability** - Given input + state, output is deterministic
334
+
335
+ ## Why This Exists
336
+
337
+ MetaSPN measures transformation, not engagement. But transformation can happen in many contexts:
338
+
339
+ - Podcast listening → G3 (Models) learning
340
+ - Tweet threads → G2 (Idea Mining) extraction
341
+ - Creator output → G1 (Identity) development
342
+ - Network connections → G6 (Network) building
343
+
344
+ Each context is a different **game**, but they all share the same underlying mechanics:
345
+
346
+ - Signals come in (things happen)
347
+ - State accumulates (context builds)
348
+ - Transformations occur (changes happen)
349
+ - Emissions go out (observable results)
350
+
351
+ This engine is the shared foundation. Game wrappers add the semantics.
352
+
353
+ ## Contributing
354
+
355
+ Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, running tests and checks, and how to submit changes. We also have a [Code of Conduct](CODE_OF_CONDUCT.md) and [Security](SECURITY.md) policy.
356
+
357
+ ## License
358
+
359
+ MIT
@@ -0,0 +1,329 @@
1
+ # MetaSPN Engine
2
+
3
+ **Minimal signal processing engine for observable games.**
4
+
5
+ Zero game semantics. Pure signal flow. Maximum composability.
6
+
7
+ ## Philosophy
8
+
9
+ The MetaSPN Engine is a **dumb pipe**. It knows nothing about podcasts, tweets, G1-G6 games, or any domain-specific concepts. It only knows:
10
+
11
+ - **Signals** flow in (typed, timestamped, immutable)
12
+ - **Pipelines** process them (pure functions, composable)
13
+ - **State** accumulates (typed, versioned)
14
+ - **Emissions** flow out (typed, traceable)
15
+
16
+ Everything else is built on top through game wrappers.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install metaspn-engine
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```python
27
+ from dataclasses import dataclass
28
+ from datetime import datetime
29
+ from metaspn_engine import Signal, Emission, State, Pipeline, Engine
30
+ from metaspn_engine.transforms import emit_if, accumulate, update_state
31
+
32
+ # 1. Define your signal payload type
33
+ @dataclass(frozen=True)
34
+ class ScoreEvent:
35
+ user_id: str
36
+ score: float
37
+
38
+ # 2. Define your state type
39
+ @dataclass
40
+ class GameState:
41
+ total_signals: int = 0
42
+ running_total: float = 0.0
43
+ high_score: float = 0.0
44
+
45
+ # 3. Build your pipeline
46
+ pipeline = Pipeline([
47
+ # Count signals
48
+ accumulate("total_signals", lambda acc, _: (acc or 0) + 1),
49
+
50
+ # Track running total
51
+ accumulate("running_total", lambda acc, payload: (acc or 0) + payload.score),
52
+
53
+ # Update high score
54
+ update_state(lambda payload, state:
55
+ GameState(
56
+ total_signals=state.total_signals,
57
+ running_total=state.running_total,
58
+ high_score=max(state.high_score, payload.score)
59
+ ) if payload.score > state.high_score else state
60
+ ),
61
+
62
+ # Emit on high score
63
+ emit_if(
64
+ condition=lambda payload, state: payload.score > state.high_score,
65
+ emission_type="new_high_score",
66
+ payload_extractor=lambda payload, state: {
67
+ "user_id": payload.user_id,
68
+ "score": payload.score,
69
+ "previous_high": state.high_score,
70
+ }
71
+ ),
72
+ ])
73
+
74
+ # 4. Create engine
75
+ engine = Engine(
76
+ pipeline=pipeline,
77
+ initial_state=GameState(),
78
+ )
79
+
80
+ # 5. Process signals
81
+ signal = Signal(
82
+ payload=ScoreEvent(user_id="user_123", score=95.5),
83
+ timestamp=datetime.now(),
84
+ source="game_server",
85
+ )
86
+
87
+ emissions = engine.process(signal)
88
+
89
+ # 6. Check results
90
+ print(f"State: {engine.get_state()}")
91
+ print(f"Emissions: {emissions}")
92
+ ```
93
+
94
+ ## Documentation
95
+
96
+ Full documentation is in the **[docs](docs/)** folder:
97
+
98
+ | Document | Description |
99
+ |----------|-------------|
100
+ | [**Docs index**](docs/index.md) | Entry point — overview and links to everything |
101
+ | [Core concepts](docs/concepts.md) | Why the engine exists; signals, state, emissions |
102
+ | [Quick start tutorial](docs/quickstart.md) | Build your first game in ~15 minutes |
103
+ | [Mental model](docs/mental-model.md) | One-page architecture overview |
104
+ | [Designing games](docs/designing-games.md) | How to design new games; four questions, patterns |
105
+ | [API cheatsheet](docs/cheatsheet.md) | Quick reference for types and methods |
106
+ | [Architecture](docs/architecture.mermaid) · [Data flow](docs/flow.mermaid) | Mermaid diagrams |
107
+
108
+ **Examples:** [Podcast Game](examples/podcast_game.py) · [Creator Scoring Game](examples/creator_scoring_game.py)
109
+
110
+ ## Core Concepts
111
+
112
+ ### Signals
113
+
114
+ Immutable input events with typed payloads:
115
+
116
+ ```python
117
+ @dataclass(frozen=True)
118
+ class PodcastListen:
119
+ episode_id: str
120
+ duration_seconds: int
121
+ completed: bool
122
+
123
+ signal = Signal(
124
+ payload=PodcastListen("ep_123", 3600, True),
125
+ timestamp=datetime.now(),
126
+ source="overcast",
127
+ )
128
+ ```
129
+
130
+ ### Pipelines
131
+
132
+ Sequences of pure steps that process signals:
133
+
134
+ ```python
135
+ pipeline = Pipeline([
136
+ step_one,
137
+ step_two,
138
+ step_three,
139
+ ], name="my_pipeline")
140
+
141
+ # Pipelines are composable
142
+ combined = pipeline_a + pipeline_b
143
+
144
+ # Pipelines support branching
145
+ branched = pipeline.branch(
146
+ predicate=lambda s: s.payload.type == "podcast",
147
+ if_true=podcast_pipeline,
148
+ if_false=other_pipeline,
149
+ )
150
+ ```
151
+
152
+ ### State
153
+
154
+ Mutable accumulated context:
155
+
156
+ ```python
157
+ @dataclass
158
+ class MyState:
159
+ count: int = 0
160
+ items: list = field(default_factory=list)
161
+
162
+ state = State(value=MyState())
163
+ state.enable_history() # Track state transitions
164
+
165
+ # State updates happen through pipeline steps
166
+ # using update functions
167
+ ```
168
+
169
+ ### Emissions
170
+
171
+ Immutable output events:
172
+
173
+ ```python
174
+ emission = Emission(
175
+ payload={"score": 0.85},
176
+ caused_by=signal.signal_id, # Traceability
177
+ emission_type="score_computed",
178
+ )
179
+ ```
180
+
181
+ ## Transforms
182
+
183
+ Built-in step functions for common operations:
184
+
185
+ ```python
186
+ from metaspn_engine.transforms import (
187
+ # Mapping
188
+ map_to_emission,
189
+
190
+ # State management
191
+ accumulate,
192
+ set_state,
193
+ update_state,
194
+
195
+ # Windowing
196
+ window,
197
+ time_window,
198
+
199
+ # Emissions
200
+ emit,
201
+ emit_if,
202
+ emit_on_change,
203
+
204
+ # Control flow
205
+ branch,
206
+ merge,
207
+ sequence,
208
+
209
+ # Utilities
210
+ log,
211
+ tap,
212
+ )
213
+ ```
214
+
215
+ ## Building Game Wrappers
216
+
217
+ The engine is meant to be wrapped by game-specific packages:
218
+
219
+ ```python
220
+ # metaspn_podcast/game.py
221
+ from metaspn_engine import Signal, Pipeline, Engine
222
+ from metaspn_engine.protocols import GameProtocol
223
+
224
+ class PodcastGame:
225
+ """Podcast listening game built on MetaSPN Engine."""
226
+
227
+ name = "podcast"
228
+ version = "1.0.0"
229
+
230
+ def create_signal(self, data: dict) -> Signal[PodcastListen]:
231
+ return Signal(
232
+ payload=PodcastListen(
233
+ episode_id=data["episode_id"],
234
+ duration_seconds=data["duration"],
235
+ completed=data.get("completed", False),
236
+ ),
237
+ timestamp=datetime.fromisoformat(data["timestamp"]),
238
+ source=data.get("source", "unknown"),
239
+ )
240
+
241
+ def initial_state(self) -> PodcastState:
242
+ return PodcastState()
243
+
244
+ def pipeline(self) -> Pipeline:
245
+ return Pipeline([
246
+ track_listening,
247
+ compute_influence_vector,
248
+ update_trajectory,
249
+ emit_if_significant,
250
+ ])
251
+
252
+ # Usage
253
+ game = PodcastGame()
254
+ engine = Engine(
255
+ pipeline=game.pipeline(),
256
+ initial_state=game.initial_state(),
257
+ )
258
+
259
+ for event in listening_events:
260
+ signal = game.create_signal(event)
261
+ emissions = engine.process(signal)
262
+ ```
263
+
264
+ ## Architecture
265
+
266
+ ```
267
+ ┌─────────────────────────────────────────────────────────────┐
268
+ │ Game Wrappers │
269
+ │ (PodcastGame, TwitterGame, CreatorScoring, etc.) │
270
+ │ │
271
+ │ - Define signal types │
272
+ │ - Define state shape │
273
+ │ - Build domain-specific pipelines │
274
+ │ - Handle game-specific logic │
275
+ └─────────────────────────────────────────────────────────────┘
276
+
277
+ implements GameProtocol
278
+
279
+
280
+ ┌─────────────────────────────────────────────────────────────┐
281
+ │ metaspn-engine (core) │
282
+ │ │
283
+ │ Signal[T] ──▶ Pipeline[Steps] ──▶ Emission[U] │
284
+ │ │ │
285
+ │ reads/writes │
286
+ │ │ │
287
+ │ State[S] │
288
+ │ │
289
+ │ - Type-safe signal flow │
290
+ │ - Pure function pipelines │
291
+ │ - Versioned state management │
292
+ │ - Traceable emissions │
293
+ └─────────────────────────────────────────────────────────────┘
294
+ ```
295
+
296
+ ## Design Principles
297
+
298
+ 1. **Zero Dependencies** - The core engine has no external dependencies
299
+ 2. **Pure Functions** - All transforms are pure (state updates are explicit)
300
+ 3. **Type Safety** - Full generic type support for signals, state, emissions
301
+ 4. **Composability** - Pipelines compose, games compose, everything composes
302
+ 5. **Traceability** - Every emission traces back to its causing signal
303
+ 6. **Testability** - Given input + state, output is deterministic
304
+
305
+ ## Why This Exists
306
+
307
+ MetaSPN measures transformation, not engagement. But transformation can happen in many contexts:
308
+
309
+ - Podcast listening → G3 (Models) learning
310
+ - Tweet threads → G2 (Idea Mining) extraction
311
+ - Creator output → G1 (Identity) development
312
+ - Network connections → G6 (Network) building
313
+
314
+ Each context is a different **game**, but they all share the same underlying mechanics:
315
+
316
+ - Signals come in (things happen)
317
+ - State accumulates (context builds)
318
+ - Transformations occur (changes happen)
319
+ - Emissions go out (observable results)
320
+
321
+ This engine is the shared foundation. Game wrappers add the semantics.
322
+
323
+ ## Contributing
324
+
325
+ Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, running tests and checks, and how to submit changes. We also have a [Code of Conduct](CODE_OF_CONDUCT.md) and [Security](SECURITY.md) policy.
326
+
327
+ ## License
328
+
329
+ MIT