pyweft 0.2.0__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.
Files changed (58) hide show
  1. pyweft-0.2.0/.gitignore +10 -0
  2. pyweft-0.2.0/CHANGELOG.md +40 -0
  3. pyweft-0.2.0/LICENSE +21 -0
  4. pyweft-0.2.0/PKG-INFO +324 -0
  5. pyweft-0.2.0/README.md +295 -0
  6. pyweft-0.2.0/docs/getting-started.md +2172 -0
  7. pyweft-0.2.0/pyproject.toml +72 -0
  8. pyweft-0.2.0/src/pyweft/__init__.py +60 -0
  9. pyweft-0.2.0/src/pyweft/__main__.py +4 -0
  10. pyweft-0.2.0/src/pyweft/background.py +203 -0
  11. pyweft-0.2.0/src/pyweft/binding.py +118 -0
  12. pyweft-0.2.0/src/pyweft/cli.py +1006 -0
  13. pyweft-0.2.0/src/pyweft/component.py +487 -0
  14. pyweft-0.2.0/src/pyweft/desktop.py +606 -0
  15. pyweft-0.2.0/src/pyweft/dialogs.py +123 -0
  16. pyweft-0.2.0/src/pyweft/differ.py +195 -0
  17. pyweft-0.2.0/src/pyweft/dommodel.py +63 -0
  18. pyweft-0.2.0/src/pyweft/effects.py +191 -0
  19. pyweft-0.2.0/src/pyweft/events.py +102 -0
  20. pyweft-0.2.0/src/pyweft/expressions.py +184 -0
  21. pyweft-0.2.0/src/pyweft/hotreload.py +122 -0
  22. pyweft-0.2.0/src/pyweft/packaging.py +186 -0
  23. pyweft-0.2.0/src/pyweft/parser.py +426 -0
  24. pyweft-0.2.0/src/pyweft/prerender.py +121 -0
  25. pyweft-0.2.0/src/pyweft/renderer.py +802 -0
  26. pyweft-0.2.0/src/pyweft/routing.py +364 -0
  27. pyweft-0.2.0/src/pyweft/static/loading.html +102 -0
  28. pyweft-0.2.0/src/pyweft/static/runtime.js +341 -0
  29. pyweft-0.2.0/src/pyweft/static/shell.html +693 -0
  30. pyweft-0.2.0/src/pyweft/static/worker.js +89 -0
  31. pyweft-0.2.0/src/pyweft/store.py +118 -0
  32. pyweft-0.2.0/src/pyweft/styles.py +171 -0
  33. pyweft-0.2.0/src/pyweft/vdom.py +57 -0
  34. pyweft-0.2.0/tests/test_app_settings.py +183 -0
  35. pyweft-0.2.0/tests/test_async.py +587 -0
  36. pyweft-0.2.0/tests/test_background.py +245 -0
  37. pyweft-0.2.0/tests/test_cli.py +345 -0
  38. pyweft-0.2.0/tests/test_components_kit.py +414 -0
  39. pyweft-0.2.0/tests/test_compose.py +320 -0
  40. pyweft-0.2.0/tests/test_desktop.py +203 -0
  41. pyweft-0.2.0/tests/test_dialogs.py +136 -0
  42. pyweft-0.2.0/tests/test_differ.py +205 -0
  43. pyweft-0.2.0/tests/test_dynamic.py +127 -0
  44. pyweft-0.2.0/tests/test_effects.py +433 -0
  45. pyweft-0.2.0/tests/test_events.py +294 -0
  46. pyweft-0.2.0/tests/test_focus.py +98 -0
  47. pyweft-0.2.0/tests/test_folders.py +232 -0
  48. pyweft-0.2.0/tests/test_hotreload.py +509 -0
  49. pyweft-0.2.0/tests/test_inline_handlers.py +142 -0
  50. pyweft-0.2.0/tests/test_invalidate.py +117 -0
  51. pyweft-0.2.0/tests/test_libraries.py +121 -0
  52. pyweft-0.2.0/tests/test_packaging.py +136 -0
  53. pyweft-0.2.0/tests/test_parser.py +156 -0
  54. pyweft-0.2.0/tests/test_prerender.py +147 -0
  55. pyweft-0.2.0/tests/test_renderer.py +205 -0
  56. pyweft-0.2.0/tests/test_routing.py +249 -0
  57. pyweft-0.2.0/tests/test_store.py +183 -0
  58. pyweft-0.2.0/tests/test_styles.py +194 -0
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ dist-desktop/
8
+ .venv/
9
+ CLAUDE.md
10
+ PROMPTS.md
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ What changed in each release of `pyweft`. Newest first. The toolkit,
4
+ `pyweft-ui`, has [its own](packages/pyweft-ui/CHANGELOG.md).
5
+
6
+ Add a line under **Unreleased** with each change worth knowing about;
7
+ releasing moves them under the new version (see [RELEASING.md](RELEASING.md)).
8
+
9
+ ## Unreleased
10
+
11
+ ## 0.2.0
12
+
13
+ The first release. (It starts at 0.2.0 because an earlier, unrelated
14
+ project named `pyweft` used 0.1.0 to 0.1.3 and deleted them, and PyPI never
15
+ lets a deleted file name be used again.)
16
+
17
+ - **Components** in `.weft` files: an XML template, a Python code block and
18
+ a scoped `<style>` block. `{{ }}`, `:attr`, `@event` handlers with
19
+ modifiers and a detailed event object, `@bind` for every form control,
20
+ `<If>`/`<ElseIf>`/`<Else>`, keyed `<For>`, `@property` values.
21
+ - **Composition**: props, `emit`, slots (named, with fallbacks and slot
22
+ props), `@bind` on components, attributes passed to the root element,
23
+ components in folders, component libraries from packages, `<Dynamic>`.
24
+ - **Pages**: `router("pages")`, `<Link>`, `navigate()`, `back()`, typed
25
+ values from the address and query, a not-found page.
26
+ - **State**: `Store` for state shared across components, `@effect`,
27
+ lifecycle hooks, `state_has_changed()`.
28
+ - **Async**: async handlers and `on_init`, `start_task()`, and
29
+ `run_in_background()` (a Web Worker in the browser, a thread elsewhere).
30
+ - **Focus and the page**: `ref=` with `focus()`, `scroll_to()` and friends;
31
+ `set_title()`; `pyweft.dialogs` for opening and saving files and asking
32
+ for confirmation.
33
+ - **Browser target**: Pyodide, with the first screen rendered ahead of time
34
+ and a loading screen that shows real progress.
35
+ - **Desktop target**: `pyweft run --desktop` (pywebview, the `desktop` extra)
36
+ and `pyweft build --desktop` for a standalone program (PyInstaller, the
37
+ `package` extra); title, icon and window settings in `[tool.pyweft]`.
38
+ - **CLI**: `pyweft new`, `run` (hot reload that keeps state, error overlay)
39
+ and `build` (a static site); PyPI dependencies installed in the browser;
40
+ `head.html`, `assets/` and CSS frameworks.
pyweft-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Prassanth Nicholas
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.
pyweft-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,324 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyweft
3
+ Version: 0.2.0
4
+ Summary: A UI framework for Python: XML templates, Python code-behind, one render core for web, desktop and mobile.
5
+ Project-URL: Homepage, https://github.com/prassanthn/PyWeft
6
+ Project-URL: Documentation, https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md
7
+ Project-URL: Issues, https://github.com/prassanthn/PyWeft/issues
8
+ Author: Prassanth Nicholas
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: components,framework,pyodide,ui,webassembly,webview
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: User Interfaces
20
+ Requires-Python: >=3.10
21
+ Provides-Extra: desktop
22
+ Requires-Dist: pywebview>=5; extra == 'desktop'
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7; extra == 'dev'
25
+ Provides-Extra: package
26
+ Requires-Dist: pyinstaller>=6; extra == 'package'
27
+ Requires-Dist: pywebview>=5; extra == 'package'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # PyWeft
31
+
32
+ **Build web and desktop apps in Python.** Write components as `.weft` files,
33
+ an XML template plus a Python code block, and PyWeft runs them in the browser
34
+ with real CPython compiled to WebAssembly, or in a native window with Python
35
+ running on your machine. No JavaScript to write, no build step.
36
+
37
+ *PyWeft: the weft is the thread woven through the warp to make fabric.*
38
+
39
+ ```xml
40
+ <template>
41
+ <div>
42
+ <h1>Hello, {{ name or "stranger" }}!</h1>
43
+ <input @bind="name" placeholder="your name" />
44
+ <button @click="count += 1">Clicked {{ count }} times</button>
45
+ </div>
46
+ </template>
47
+
48
+ <code>
49
+ name: str = ""
50
+ count: int = 0
51
+ </code>
52
+ ```
53
+
54
+ Type in the box and the heading follows; click and the count goes up. State
55
+ is plain Python fields, and the page updates itself after every event.
56
+
57
+ > **Status:** a working proof of concept. Web apps and desktop apps (run
58
+ > locally or built into a program to hand out) work today. APIs may still
59
+ > change. Not on PyPI yet, so you install from GitHub (below).
60
+
61
+ ## Quick start
62
+
63
+ You need Python 3.10 or newer. With [uv](https://docs.astral.sh/uv/):
64
+
65
+ ```bash
66
+ uv init myproject && cd myproject
67
+ uv add git+https://github.com/prassanthn/PyWeft.git
68
+ uv run pyweft new myapp
69
+ uv run pyweft run myapp
70
+ ```
71
+
72
+ Open <http://127.0.0.1:8642/>. Your page shows straight away, dimmed, while
73
+ the browser downloads Python in the background (a few seconds the first
74
+ time, cached after that); then it comes alive.
75
+
76
+ With pip instead:
77
+
78
+ ```bash
79
+ pip install git+https://github.com/prassanthn/PyWeft.git
80
+ pyweft new myapp
81
+ pyweft run myapp
82
+ ```
83
+
84
+ Now open `myapp/app.weft` in your editor, change something and save: the page
85
+ updates in place and keeps what you'd typed. Make a mistake and an overlay
86
+ shows the file and line; fix it and save again.
87
+
88
+ `myapp/` is your whole app:
89
+
90
+ ```
91
+ myapp/
92
+ app.weft the root component
93
+ styles.css page-wide CSS (optional)
94
+ pyproject.toml your app's dependencies, installed in the browser
95
+ head.html extra <head> tags: fonts, CDN stylesheets (optional)
96
+ loading.html what visitors see while Python starts (optional)
97
+ ```
98
+
99
+ When you're ready to publish, `uv run pyweft build myapp` writes a static site
100
+ to `myapp/dist/` that any web host can serve, such as GitHub Pages or Netlify.
101
+
102
+ The same app also runs as a desktop app, in a native window with Python on
103
+ your machine:
104
+
105
+ ```bash
106
+ uv add "pyweft[desktop]"
107
+ uv run pyweft run myapp --desktop
108
+ ```
109
+
110
+ To hand it to people without Python, build it into one program (an `.exe`
111
+ on Windows, a `.app` on macOS) in `myapp/dist-desktop/`:
112
+
113
+ ```bash
114
+ uv add --dev "pyweft[package]"
115
+ uv run pyweft build myapp --desktop
116
+ ```
117
+
118
+ Name the app and give it an icon in `myapp/pyproject.toml`, for the browser
119
+ tab and the window alike, and size the window there too:
120
+
121
+ ```toml
122
+ [tool.pyweft]
123
+ title = "Acme Orders"
124
+ icon = "assets/icon.ico"
125
+
126
+ [tool.pyweft.desktop]
127
+ width = 1200
128
+ height = 800
129
+ ```
130
+
131
+ ## Ready-made controls (no HTML or CSS needed)
132
+
133
+ [`pyweft-ui`](packages/pyweft-ui/README.md) gives you styled, accessible
134
+ controls as components: buttons, text fields, selects, checkboxes, switches,
135
+ cards, tabs, sortable tables, dialogs, alerts, toasts, layout and light/dark
136
+ themes.
137
+
138
+ ```bash
139
+ uv add "pyweft-ui @ git+https://github.com/prassanthn/PyWeft.git#subdirectory=packages/pyweft-ui"
140
+ ```
141
+
142
+ Add `"pyweft-ui"` to the `dependencies` list in `myapp/pyproject.toml`, then
143
+ use the controls:
144
+
145
+ ```xml
146
+ <template>
147
+ <ui.Theme mode="auto">
148
+ <ui.AppShell title="Orders">
149
+ <ui.Card title="New order">
150
+ <ui.TextField label="Customer" @bind="customer" required="true" />
151
+ <ui.Select label="Size" options="Small, Medium, Large" @bind="size" />
152
+ <ui.Switch label="Express delivery" @bind="express" />
153
+ <Fill name="footer">
154
+ <ui.Button variant="primary" icon="send" @click="place_order">Place order</ui.Button>
155
+ </Fill>
156
+ </ui.Card>
157
+ </ui.AppShell>
158
+ </ui.Theme>
159
+ </template>
160
+
161
+ <code>
162
+ from pyweft_ui import ui
163
+
164
+ customer: str = ""
165
+ size: str = "Medium"
166
+ express: bool = False
167
+
168
+ def place_order(self):
169
+ ...
170
+ </code>
171
+ ```
172
+
173
+ PyWeft notices the toolkit is installed and sends it to the browser for you.
174
+ To see every control live, clone this repository and run
175
+ `uv run pyweft run packages/pyweft-ui/gallery`.
176
+
177
+ ## What you get
178
+
179
+ - **Components in one file:** template, Python code and a `<style>` block
180
+ that only applies to that component.
181
+ - **Template syntax:**
182
+ - **Values:** `{{ expression }}` shows a value, `:attr="expression"` sets an
183
+ attribute from Python, `@bind="field"` keeps a form control and a field
184
+ in sync.
185
+ - **Events:** `@click="method"` handles an event, with modifiers like
186
+ `@submit.prevent` and `@keydown.enter`.
187
+ - **Focus:** `ref="name"` marks an element; `self.focus("name")` and
188
+ `self.scroll_to("name")` move the keyboard focus or the page to it.
189
+ - **Control flow:** `<If>`, `<ElseIf>`, `<Else>` and `<For>`.
190
+ - **Composition:** components inside components, with props, events, slots
191
+ for passing content, and `@bind` on your own components. Organise them in
192
+ folders, or install them as packages.
193
+ - **Shared state:** a `Store` holds what many components use, like a cart
194
+ or the signed-in user; change it from anywhere and every page showing it
195
+ updates.
196
+ - **Pages:** a `pages/` folder becomes your app's screens, with links, Back
197
+ and Forward, typed values from the address (`/orders/{id}`), and a
198
+ not-found page.
199
+ - **Lifecycle and effects:** `on_init`, `on_after_render` and `on_dispose`
200
+ run as a component appears, updates and goes away; `@effect` methods
201
+ re-run when the state they read changes.
202
+ - **Async:** `async def` handlers, plus `run_in_background()` to run heavy
203
+ Python in a Web Worker so the page never freezes.
204
+ - **Styling your way:** scoped styles, plain CSS files, or frameworks such as
205
+ Tailwind and Bootstrap through `head.html`.
206
+ - **Packages:** any pure-Python package from PyPI, declared in
207
+ `pyproject.toml`, works in the browser.
208
+ - **Files and dialogs:** `pyweft.dialogs` opens and saves files and asks for
209
+ confirmation with the same code on both targets: the browser's picker and
210
+ downloads on the web, the system's own dialogs on the desktop.
211
+ - **A fast dev loop:** hot reload that keeps your app's state, and readable
212
+ errors that point to your `.weft` file and line.
213
+ - **Fast first load:** your app's first screen is rendered ahead of time and
214
+ shows at once, while Python starts in the background.
215
+
216
+ ## Learn more
217
+
218
+ | You want to | Read |
219
+ |---|---|
220
+ | Learn the basics: state, templates, events | [Getting started](docs/getting-started.md), sections 1–3 |
221
+ | Run code when a component appears, updates or goes away | [Lifecycle](docs/getting-started.md#lifecycle-when-your-code-runs) |
222
+ | Split an app into components, use slots | [Composing components](docs/getting-started.md#4-composing-components) |
223
+ | Build an app with several pages | [Pages and navigation](docs/getting-started.md#pages-and-navigation) |
224
+ | Handle forms and keyboard input | [Binding form controls](docs/getting-started.md#binding-form-controls), [event modifiers](docs/getting-started.md#event-modifiers) |
225
+ | Load data, run async code, do heavy work | [Async handlers](docs/getting-started.md#async-handlers-and-background-work), [`run_in_background`](docs/getting-started.md#heavy-work-run_in_background) |
226
+ | Style with Tailwind or Bootstrap | [Using CSS frameworks](docs/getting-started.md#using-css-frameworks) |
227
+ | Build screens with the ready-made controls | [pyweft-ui guide](packages/pyweft-ui/docs/guide.md), then the [reference](packages/pyweft-ui/README.md) |
228
+ | Deploy your app | [Building for deployment](docs/getting-started.md#building-for-deployment) |
229
+ | Run it as a desktop app | [A desktop app](docs/getting-started.md#a-desktop-app-pyweft-run---desktop) |
230
+ | Hand out a desktop app (`.exe`, `.app`) | [Handing out a desktop app](docs/getting-started.md#handing-out-a-desktop-app-pyweft-build---desktop) |
231
+ | Open and save files, ask for confirmation | [Files and dialogs](docs/getting-started.md#files-and-dialogs) |
232
+ | Name the app, give it an icon | [The app's title and icon](docs/getting-started.md#the-apps-title-and-icon) |
233
+ | Test components without a browser | [Using the engine from plain Python](docs/getting-started.md#6-using-the-engine-from-plain-python) |
234
+
235
+ `examples/todo/` is a small complete app, and `examples/shop/` one with
236
+ pages; run them from a clone with `uv run pyweft run examples/todo`.
237
+
238
+ ## Good to know
239
+
240
+ - **Templates must be well-formed XML:** close every tag (`<br/>`, `<input />`)
241
+ and quote every attribute.
242
+ - **No threads in the browser.** Use `async`/`await` for waiting and
243
+ `run_in_background` for heavy computation.
244
+ - **Code runs in the visitor's browser.** Your `.weft` files are visible to
245
+ them, as with any website's JavaScript, so keep secrets on a server.
246
+ - **Not every package works in a browser:** pure-Python packages do, and so
247
+ do the ~250 that Pyodide ships prebuilt (NumPy, pandas, …). Anything that
248
+ needs raw sockets or subprocesses doesn't.
249
+
250
+ ## How it works
251
+
252
+ PyWeft renders HTML rather than native widgets. A component renders to a
253
+ lightweight tree of elements; after every event PyWeft renders again, compares
254
+ the new tree with the old one and sends only the differences to the page as
255
+ small JSON instructions. The core never touches the page itself. That is what
256
+ makes other targets cheap: a desktop app is a webview showing the same page,
257
+ with Python running natively and the same instructions crossing a bridge.
258
+
259
+ | Target | Where Python runs | What draws the page |
260
+ |---|---|---|
261
+ | Web | Pyodide, in the browser | the browser |
262
+ | Desktop (`pyweft run --desktop`) | CPython on the machine | a webview (WebView2, WKWebView, WebKitGTK) |
263
+ | Mobile (later) | CPython via Briefcase | a webview |
264
+
265
+ There is no reactive state system to learn: a handler runs, the component
266
+ renders again, and the comparison finds what changed. `@effect` is the one
267
+ place where PyWeft tracks which fields you read.
268
+
269
+ ## Working on PyWeft
270
+
271
+ ```bash
272
+ git clone https://github.com/prassanthn/PyWeft.git && cd PyWeft
273
+ uv sync --extra dev # also installs packages/pyweft-ui, editable
274
+ uv run pytest # 920 tests, including pyweft-ui's
275
+ uv run python examples/demo.py # render -> event -> diff -> apply, no browser
276
+ uv run pyweft run examples/todo # the todo app
277
+ uv run pyweft run packages/pyweft-ui/gallery # every control
278
+ ```
279
+
280
+ With pip: `pip install -e ".[dev]"` and `pip install -e packages/pyweft-ui`.
281
+
282
+ Releases go to PyPI by pushing a version tag; [RELEASING.md](RELEASING.md)
283
+ has the steps, and [CHANGELOG.md](CHANGELOG.md) what each release changed.
284
+
285
+ ```
286
+ src/pyweft/ the framework (zero runtime dependencies)
287
+ static/ the browser side: runtime.js, the page shell, the worker
288
+ packages/pyweft-ui/ the control toolkit, released as its own package
289
+ docs/ the guide
290
+ examples/ a headless demo, the todo app and the shop (pages)
291
+ tests/ the framework's tests
292
+ ```
293
+
294
+ Under the hood:
295
+ - **Patches:** each patch instruction says where to insert, remove, move or
296
+ change something. They're ordered so the page can apply them front to back
297
+ with no bookkeeping.
298
+ - **Testing the differ:** tests apply the patches to the old tree and check
299
+ the result equals the new one, rather than checking the exact instructions.
300
+ 400 of the tests do this on randomly generated trees.
301
+ - **Handler ids** are built from list keys where available, so a row keeps
302
+ its handlers when a list is reordered.
303
+
304
+ ## Roadmap
305
+
306
+ **Done:** the render core and differ; the browser target; the desktop target
307
+ (pywebview), and standalone desktop programs (PyInstaller); components with props, events, slots and component libraries;
308
+ pages and navigation; shared state with stores; events with modifiers and
309
+ `@bind` for every form control; moving focus from Python; effects; async
310
+ handlers and Web Worker background work; file and confirmation dialogs on
311
+ both targets; the app's title and icon, and window settings; scoped styles
312
+ and CSS framework support; the `pyweft` CLI with hot reload, static builds
313
+ and pre-rendered first screens; `pyweft-ui` with 22 keyboard-accessible
314
+ controls and a gallery.
315
+
316
+ **Next:** publishing `pyweft` and `pyweft-ui` to PyPI; more controls (menus,
317
+ a date picker, form validation); installers (`.msi`, `.dmg`) and code
318
+ signing for desktop apps.
319
+
320
+ **Later:** mobile packaging with Briefcase, developer tools.
321
+
322
+ ## License
323
+
324
+ MIT. See [LICENSE](LICENSE).