mstache 0.2.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021-2024 Felipe A Hernandez <ergoithz@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of the 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,175 @@
1
+ Metadata-Version: 2.1
2
+ Name: mstache
3
+ Version: 0.2.0
4
+ Summary: mstache, Mustache for Python
5
+ Home-page: https://gitlab.com/ergoithz/mstache
6
+ Author: Felipe A Hernandez
7
+ Author-email: ergoithz@gmail.com
8
+ License: MIT
9
+ Keywords: template,mustache
10
+ Platform: any
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.10.0
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Provides-Extra: benchmark
21
+ Requires-Dist: ustache ==0.1.5 ; extra == 'benchmark'
22
+ Requires-Dist: chevron ; extra == 'benchmark'
23
+ Requires-Dist: tabulate ; extra == 'benchmark'
24
+ Provides-Extra: codestyle
25
+ Requires-Dist: pycodestyle ; extra == 'codestyle'
26
+ Requires-Dist: ruff ; extra == 'codestyle'
27
+ Requires-Dist: sphinx ; extra == 'codestyle'
28
+ Requires-Dist: rstcheck ; extra == 'codestyle'
29
+ Requires-Dist: mypy ; extra == 'codestyle'
30
+ Provides-Extra: coverage
31
+ Requires-Dist: coverage[toml] ; extra == 'coverage'
32
+ Provides-Extra: dev
33
+ Requires-Dist: pycodestyle ; extra == 'dev'
34
+ Requires-Dist: ruff ; extra == 'dev'
35
+ Requires-Dist: sphinx ; extra == 'dev'
36
+ Requires-Dist: rstcheck ; extra == 'dev'
37
+ Requires-Dist: mypy ; extra == 'dev'
38
+ Requires-Dist: coverage[toml] ; extra == 'dev'
39
+ Requires-Dist: recommonmark ; extra == 'dev'
40
+ Requires-Dist: sphinx-autobuild ; extra == 'dev'
41
+ Requires-Dist: sphinx-autodoc-typehints ; extra == 'dev'
42
+ Requires-Dist: build ; extra == 'dev'
43
+ Requires-Dist: twine ; extra == 'dev'
44
+ Requires-Dist: ustache ==0.1.5 ; extra == 'dev'
45
+ Requires-Dist: chevron ; extra == 'dev'
46
+ Requires-Dist: tabulate ; extra == 'dev'
47
+ Provides-Extra: docs
48
+ Requires-Dist: recommonmark ; extra == 'docs'
49
+ Requires-Dist: sphinx ; extra == 'docs'
50
+ Requires-Dist: sphinx-autobuild ; extra == 'docs'
51
+ Requires-Dist: sphinx-autodoc-typehints ; extra == 'docs'
52
+ Provides-Extra: release
53
+ Requires-Dist: build ; extra == 'release'
54
+ Requires-Dist: twine ; extra == 'release'
55
+
56
+ # mstache
57
+
58
+ Mustache for Python.
59
+
60
+ Documentation: [mstache.readthedocs.io](https://mstache.readthedocs.io)
61
+
62
+ ## License
63
+
64
+ [MIT License](https://gitlab.com/ergoithz/mstache/-/blob/master/LICENSE)
65
+
66
+ ## Installation
67
+
68
+ ```python
69
+ pip install mstache
70
+ ```
71
+
72
+ ## Usage
73
+
74
+ Python:
75
+
76
+ ```python
77
+ import mstache
78
+
79
+ print(mstache.render('Hello {{v}}', {'v': 'World!'}))
80
+ # Hello World!
81
+ ```
82
+
83
+ Command line:
84
+
85
+ ```sh
86
+ $ mstache -j data.json -o output.html template.mustache
87
+ ```
88
+
89
+ ## Highlights
90
+
91
+ - The fastest pure-python Mustache implementation to this date.
92
+ - Command line interface.
93
+ - Spec compliant, but also highly compatible with `Mustache.js`.
94
+ - Small codebase, efficiently rendering to `str` or `bytes`,
95
+ either buffered or streaming.
96
+ - Fully customizable behavior: cache, property getter, partial resolver,
97
+ stringification, escaping and lambdas.
98
+ - No dynamic code generation, both jit and transpiler friendly.
99
+
100
+ ## Considerations
101
+
102
+ For inter-compatibility with JavaScript (especially `Mustache.js`, enabling
103
+ client-side rendering with the same templates), **mstache** must behave
104
+ somewhat atypically for a `Python` template engine:
105
+
106
+ - Mustache blocks stick to JavaScript truthyness
107
+ (`Mustache.js` truthyness emulation):
108
+ - Empty mappings (such as `dict`) are unconditionally truthy.
109
+ - `NaN`/`-NaN` are falsy.
110
+ - Mustache blocks loop through any iterable other than mappings and strings
111
+ (`Mustache.js` `Array` handling).
112
+ - Non-mapping sized objects will expose a virtual `length` property
113
+ (JavaScript `Array.length` emulation, customizable via `getter` parameter).
114
+ - Mapping keys containing dot (`.`) or whitespace (` `) are unreachable,
115
+ (`Mustache.js` property limitation, customizable via `getter` parameter).
116
+ - Sequence elements are accessible by positive index in the same way mapping
117
+ integer-keyed items are also accessible when no string key conflicts, as
118
+ properties (JavaScript `Object` emulation, customizable via `getter` parameter).
119
+
120
+ ## Syntax
121
+
122
+ Check out the [mustache(5) manual](https://mustache.github.io/mustache.5.html).
123
+
124
+ For quick reference, here is a quick overview of the Mustache syntax.
125
+
126
+ Template (`template.mustache`):
127
+ ```handlebars
128
+ {{!comment}}
129
+ <ul>
130
+ {{#object}}<li>{{property}}</li>{{/object}}
131
+ {{^object}}<li>As <b>object</b> is truthy, this won't be shown</li>{{/object}}
132
+ {{^null}}<li><b>null</b> is falsy</li>{{/null}}
133
+ {{#array}}<li>{{property}}</li>
134
+ {{/array}}
135
+ {{^array}}<li>Array isn't empty, this won't be shown.</li>{{/array}}
136
+ {{#empty_array}}<li>Empty Array, this won't be shown</li>{{/empty_array}}
137
+ {{^empty_array}}<li>empty_array is empty</li>{{/empty_array}}
138
+ {{&unescaped_html}}
139
+ </ul>
140
+ ```
141
+
142
+ Data (`data.json`):
143
+ ```json
144
+ {
145
+ "object": {
146
+ "property": "Object property value"
147
+ },
148
+ "null": null,
149
+ "array": [
150
+ {"property": "Array item1 property"},
151
+ {"property": "Array item2 property"},
152
+ {"property": "Array item3 property"}
153
+ ],
154
+ "empty_array": [],
155
+ "unescaped_html": "<li>this is unescaped html</li>"
156
+ }
157
+ ```
158
+
159
+ Command:
160
+ ```sh
161
+ $ mstache -j data.json -o output.html template.mustache
162
+ ```
163
+
164
+ Output:
165
+ ```html
166
+ <ul>
167
+ <li>Object property value</li>
168
+ <li><b>null</b> is falsy</li>
169
+ <li>Array item1 property</li>
170
+ <li>Array item2 property</li>
171
+ <li>Array item3 property</li>
172
+ <li>empty_array is empty</li>
173
+ <li>this is unescaped html</li>
174
+ </ul>
175
+ ```
@@ -0,0 +1,8 @@
1
+ mstache.py,sha256=Fyb7XLb-uoPskeArd_Dl1hF8ACaSXEl0TjZyQlK2piY,35598
2
+ mstache-0.2.0.dist-info/LICENSE,sha256=Dm6iE3DBTvHjFk7lmBlxltSnL_cniKTuDGimSeYnILw,1100
3
+ mstache-0.2.0.dist-info/METADATA,sha256=Sokx2kaCTNqoS97LHCmWsfzSykrrlRJvXE7CR4_VmLY,5341
4
+ mstache-0.2.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
5
+ mstache-0.2.0.dist-info/entry_points.txt,sha256=Og2tE2b_-ptIXcCpFPseKLtRaa1EE2ixpAGiZlNsg_4,62
6
+ mstache-0.2.0.dist-info/top_level.txt,sha256=Xoa-62eFM8ceW2fl0WB_tFJJXPuGxuyiVkGVNhkHBeM,8
7
+ mstache-0.2.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
+ mstache-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ mstache = mstache:cli
3
+ ustache = mstache:cli
@@ -0,0 +1 @@
1
+ mstache
@@ -0,0 +1 @@
1
+
mstache.py ADDED
@@ -0,0 +1,1147 @@
1
+ """
2
+ mstache module.
3
+
4
+ This module alone implements the entire mstache library, including its
5
+ minimal command line interface.
6
+
7
+ The main functions are considered to be :func:`cli`, :func:`render` and
8
+ :func:`stream`, and they expose different approaches to template rendering:
9
+ command line interface, buffered and streaming rendering API, respectively.
10
+
11
+ Other functionality will be considered **advanced**, exposing some
12
+ implementation-specific complexity and potentially non-standard behavior
13
+ which could reduce compatibility with other mustache implementations and
14
+ future major mstache revisions.
15
+
16
+ """
17
+ """
18
+ mstache, Mustache for Python
19
+ ============================
20
+
21
+ See `README.md`_, `project documentation`_ and `project repository`_.
22
+
23
+ .. _README.md: https://mstache.readthedocs.io/en/latest/README.html
24
+ .. _project documentation: https://mstache.readthedocs.io
25
+ .. _project repository: https://gitlab.com/ergoithz/mstache
26
+
27
+
28
+ License
29
+ -------
30
+
31
+ Copyright (c) 2021-2024, Felipe A Hernandez.
32
+
33
+ MIT License (see `LICENSE`_).
34
+
35
+ .. _LICENSE: https://gitlab.com/ergoithz/mstache/-/blob/master/LICENSE
36
+
37
+ """
38
+
39
+ import codecs
40
+ import collections
41
+ import collections.abc
42
+ import math
43
+ import types
44
+ import typing
45
+
46
+ __author__ = 'Felipe A Hernandez'
47
+ __email__ = 'ergoithz@gmail.com'
48
+ __license__ = 'MIT'
49
+ __version__ = '0.2.0'
50
+ __all__ = (
51
+ # api
52
+ 'tokenize',
53
+ 'stream',
54
+ 'render',
55
+ 'cli',
56
+ # exceptions
57
+ 'TokenException',
58
+ 'ClosingTokenException',
59
+ 'UnclosedTokenException',
60
+ 'DelimiterTokenException',
61
+ 'TemplateRecursionError',
62
+ # defaults
63
+ 'default_recursion_limit',
64
+ 'default_resolver',
65
+ 'default_getter',
66
+ 'default_stringify',
67
+ 'default_escape',
68
+ 'default_lambda_render',
69
+ 'default_tags',
70
+ 'default_cache',
71
+ 'default_cache_make_key',
72
+ 'default_virtuals',
73
+ # types
74
+ 'TString',
75
+ 'PartialResolver',
76
+ 'PropertyGetter',
77
+ 'StringifyFunction',
78
+ 'EscapeFunction',
79
+ 'LambdaRenderFunctionConstructor',
80
+ 'VirtualPropertyFunction',
81
+ 'VirtualPropertyMapping',
82
+ 'TagsTuple',
83
+ 'TagsByteTuple',
84
+ 'CompiledToken',
85
+ 'CompiledTemplate',
86
+ 'CompiledTemplateCache',
87
+ 'CacheMakeKeyFunction',
88
+ )
89
+
90
+ K = typing.TypeVar('K')
91
+ """Generic."""
92
+
93
+ T = typing.TypeVar('T')
94
+ """Generic."""
95
+
96
+ D = typing.TypeVar('D')
97
+ """Generic."""
98
+
99
+ TString = typing.TypeVar('TString', str, bytes)
100
+ """String/bytes generic."""
101
+
102
+ PartialResolver = collections.abc.Callable[[typing.AnyStr], typing.AnyStr]
103
+ """Template partial tag resolver function interface."""
104
+
105
+ PropertyGetter = collections.abc.Callable[
106
+ [typing.Any, collections.abc.Sequence, typing.AnyStr, typing.Any],
107
+ typing.Any,
108
+ ]
109
+ """Template property getter function interface."""
110
+
111
+ StringifyFunction = collections.abc.Callable[[bytes, bool], bytes]
112
+ """Template variable general stringification function interface."""
113
+
114
+ EscapeFunction = collections.abc.Callable[[bytes], bytes]
115
+ """Template variable value escape function interface."""
116
+
117
+ LambdaRenderFunctionConstructor = collections.abc.Callable[
118
+ ...,
119
+ collections.abc.Callable[..., typing.AnyStr],
120
+ ]
121
+ """Lambda render function constructor interface."""
122
+
123
+ LambdaRenderFunctionFactory = LambdaRenderFunctionConstructor
124
+ """
125
+ .. deprecated:: 0.1.3
126
+ Use :attr:`LambdaRenderFunctionConstructor` instead.
127
+
128
+ """
129
+
130
+ VirtualPropertyFunction = collections.abc.Callable[[typing.Any], typing.Any]
131
+ """Virtual property implementation callable interface."""
132
+
133
+ VirtualPropertyMapping = collections.abc.Mapping[str, VirtualPropertyFunction]
134
+ """Virtual property mapping interface."""
135
+
136
+ TagsTuple = tuple[typing.AnyStr, typing.AnyStr]
137
+ """Mustache tag tuple interface."""
138
+
139
+ TagsByteTuple = tuple[bytes, bytes]
140
+ """Mustache tag byte tuple interface."""
141
+
142
+ CompiledToken = tuple[bool, bool, bool, bytes, bytes, int]
143
+ """
144
+ Compiled template token.
145
+
146
+ Tokens are tuples containing a renderer decision path, key, content and flags.
147
+
148
+ ``type: bool``
149
+ Decision for rendering path node `a`.
150
+
151
+ ``type: bool``
152
+ Decision for rendering path node `b`.
153
+
154
+ ``type: bool``
155
+ Decision for rendering path node `c`
156
+
157
+ ``key: bytes``
158
+ Template substring with token scope key.
159
+
160
+ ``content: bytes`
161
+ Template substring with token content data.
162
+
163
+ ``flags: int``
164
+ Token flags.
165
+
166
+ - Unused: ``-1`` (default)
167
+ - Variable flags:
168
+ - ``0`` - escaped
169
+ - ``1`` - unescaped
170
+ - Block start flags:
171
+ - ``0`` - falsy
172
+ - ``1`` - truthy
173
+ - Block end value: block content index.
174
+
175
+ """
176
+
177
+ CompiledTemplate = tuple[CompiledToken, ...]
178
+ """
179
+ Compiled template interface.
180
+
181
+ .. seealso::
182
+
183
+ :py:attr:`mstache.CompiledToken`
184
+ Item type.
185
+
186
+ :py:attr:`mstache.CompiledTemplateCache`
187
+ Interface exposing this type.
188
+
189
+ """
190
+
191
+
192
+ class CompiledTemplateCache(typing.Protocol):
193
+ """
194
+ Cache object protocol.
195
+
196
+ .. seealso::
197
+
198
+ :py:attr:`mstache.CompiledTemplate`
199
+ Item type.
200
+
201
+ :py:attr:`mstache.CacheMakeKeyFunction`
202
+ Cache key function interface.
203
+
204
+ """
205
+
206
+ def get(self, key: K) -> CompiledTemplate | None:
207
+ """Get compiled template from key, if any."""
208
+
209
+ def __setitem__(self, key: K, value: CompiledTemplate) -> None:
210
+ """Assign compiled template to key."""
211
+
212
+
213
+ CacheMakeKeyFunction = typing.Callable[
214
+ [tuple[bytes, bytes, bytes, bool]],
215
+ collections.abc.Hashable,
216
+ ]
217
+ """
218
+ Cache mapping key function interface.
219
+
220
+ .. seealso::
221
+
222
+ :py:attr:`mstache.CompiledTemplateCache`
223
+ Interface exposing this type.
224
+
225
+ """
226
+
227
+
228
+ class LRUCache(collections.OrderedDict, typing.Generic[T]):
229
+ """Capped mapping discarding least recently used elements."""
230
+
231
+ def __init__(self, maxsize: int, *args, **kwargs) -> None:
232
+ """
233
+ Initialize.
234
+
235
+ :param maxsize: maximum number of elements will be kept
236
+
237
+ Any parameter excess will be passed straight to dict constructor.
238
+
239
+ """
240
+ self.maxsize = maxsize
241
+ super().__init__(*args, **kwargs)
242
+
243
+ def get(self, key: collections.abc.Hashable, default: D = None) -> T | D:
244
+ """
245
+ Get value for given key or default if not present.
246
+
247
+ :param key: hashable
248
+ :param default: value will be returned if key is not present
249
+ :returns: value if key is present, default if not
250
+
251
+ """
252
+ try:
253
+ return self[key]
254
+ except KeyError:
255
+ return default # type: ignore
256
+
257
+ def __getitem__(self, key: collections.abc.Hashable) -> T:
258
+ """
259
+ Get value for given key.
260
+
261
+ :param key: hashable
262
+ :returns: value if key is present
263
+ :raises KeyError: if key is not present
264
+
265
+ """
266
+ self.move_to_end(key)
267
+ return super().__getitem__(key)
268
+
269
+ def __setitem__(self, key: collections.abc.Hashable, value: T) -> None:
270
+ """
271
+ Set value for given key.
272
+
273
+ :param key: hashable will be used to retrieve values later on
274
+ :param value: value for given key
275
+
276
+ """
277
+ super().__setitem__(key, value)
278
+ try:
279
+ self.move_to_end(key)
280
+ while len(self) > self.maxsize:
281
+ self.popitem(last=False)
282
+ except KeyError: # race condition
283
+ pass
284
+
285
+
286
+ default_recursion_limit = 1024
287
+ """
288
+ Default number of maximum nested renders.
289
+
290
+ A nested render happens whenener the lambda render parameter is used or when
291
+ a partial template is injected.
292
+
293
+ Meant to avoid recursion bombs on templates.
294
+
295
+ """
296
+
297
+ default_tags = b'{{', b'}}'
298
+ """Tuple of default mustache tags as in `tuple[bytes, bytes]`."""
299
+
300
+ default_cache: CompiledTemplateCache = LRUCache(1024)
301
+ """
302
+ Default template cache mapping, keeping the 1024 most recently used
303
+ compiled templates (LRU expiration).
304
+ """
305
+
306
+ default_cache_make_key: CacheMakeKeyFunction = tuple
307
+ """
308
+ Defaut template cache key function, :py:class:`tuple`, so keys would be
309
+ as in `tuple[bytes, bytes, bytes, bool]` containing the relevant
310
+ :py:func:`mstache.tokenizer` parameters.
311
+ """
312
+
313
+
314
+ def virtual_length(ref: collections.abc.Sized) -> int:
315
+ """
316
+ Resolve virtual length property.
317
+
318
+ :param ref: any non-mapping object implementing `__len__`
319
+ :returns: number of items
320
+ :raises TypeError: if ref is mapping or has no `__len__` method
321
+
322
+ """
323
+ if isinstance(ref, collections.abc.Mapping):
324
+ raise TypeError
325
+ return len(ref)
326
+
327
+
328
+ default_virtuals: VirtualPropertyMapping = types.MappingProxyType({
329
+ 'length': virtual_length,
330
+ })
331
+ """
332
+ Immutable mapping with default virtual properties.
333
+
334
+ The following virtual properties are implemented:
335
+
336
+ - **length**, for non-mapping sized objects, returning ``len(ref)``.
337
+
338
+ """
339
+
340
+ TOKEN_TYPES = tuple({
341
+ **dict.fromkeys(range(0x100), (True, False, True, False)),
342
+ 0x21: (False, False, False, False), # '!'
343
+ 0x23: (False, True, True, False), # '#'
344
+ 0x26: (True, True, True, True), # '&'
345
+ 0x2F: (False, True, False, False), # '/'
346
+ 0x3D: (False, False, False, True), # '='
347
+ 0x3E: (False, False, True, False), # '>'
348
+ 0x5E: (False, True, True, True), # '^'
349
+ 0x7B: (True, True, False, True), # '{'
350
+ }.values())
351
+ """ASCII-indexed tokenizer decision matrix."""
352
+
353
+ COMMON_FALSY: tuple = None, False, 0, '', b'', (), [], frozenset()
354
+ NON_LOOPING_TYPES: tuple = str, bytes, collections.abc.Mapping
355
+ EMPTY = b''
356
+ SELF_SCOPE = frozenset((b'.', '.'))
357
+ MISSING = object()
358
+
359
+
360
+ class TokenException(SyntaxError):
361
+ """Invalid token found during tokenization."""
362
+
363
+ _fmt = 'Invalid tag {tag} at line {row} column {column}'
364
+
365
+ @classmethod
366
+ def from_template(
367
+ cls,
368
+ template: bytes,
369
+ start: int,
370
+ end: int,
371
+ ) -> 'TokenException':
372
+ """
373
+ Create exception instance from parsing data.
374
+
375
+ :param template: template bytes
376
+ :param start: character position where the offending tag starts at
377
+ :param end: character position where the offending tag ends at
378
+ :returns: exception instance
379
+
380
+ """
381
+ tag = template[start:end].decode(errors='replace')
382
+ row = 1 + template[:start].count(b'\n')
383
+ column = 1 + start - max(0, template.rfind(b'\n', 0, start))
384
+ return cls(cls._fmt.format(tag=tag, row=row, column=column))
385
+
386
+
387
+ class ClosingTokenException(TokenException):
388
+ """Non-matching closing token found during tokenization."""
389
+
390
+ _fmt = 'Non-matching tag {tag} at line {row} column {column}'
391
+
392
+
393
+ class UnclosedTokenException(ClosingTokenException):
394
+ """Unclosed token found during tokenization."""
395
+
396
+ _fmt = 'Unclosed tag {tag} at line {row} column {column}'
397
+
398
+
399
+ class DelimiterTokenException(TokenException):
400
+ """
401
+ Invalid delimiters token found during tokenization.
402
+
403
+ .. versionadded:: 0.1.1
404
+
405
+ """
406
+
407
+ _fmt = 'Invalid delimiters {tag} at line {row} column {column}'
408
+
409
+
410
+ class TemplateRecursionError(RecursionError):
411
+ """Template rendering exceeded maximum render recursion."""
412
+
413
+
414
+ def default_stringify(data: typing.Any, text: bool) -> bytes:
415
+ """
416
+ Convert arbitrary data to bytes.
417
+
418
+ :param data: value will be serialized
419
+ :param text: whether running in text mode or not (bytes mode)
420
+ :returns: template bytes
421
+
422
+ """
423
+ return (
424
+ data if isinstance(data, bytes) and not text else
425
+ f'{data}'.encode()
426
+ )
427
+
428
+
429
+ def default_escape(data: bytes) -> bytes:
430
+ """
431
+ Convert bytes conflicting with HTML to their escape sequences.
432
+
433
+ :param data: bytes containing text
434
+ :returns: escaped text bytes
435
+
436
+ """
437
+ return (
438
+ data
439
+ .replace(b'&', b'&amp;')
440
+ .replace(b'<', b'&lt;')
441
+ .replace(b'>', b'&gt;')
442
+ .replace(b'"', b'&quot;')
443
+ .replace(b"'", b'&#x60;')
444
+ .replace(b'`', b'&#x3D;')
445
+ )
446
+
447
+
448
+ def default_resolver(name: typing.AnyStr) -> bytes:
449
+ """
450
+ Mustache partial resolver function (stub).
451
+
452
+ :param name: partial template name
453
+ :returns: empty bytes
454
+
455
+ """
456
+ return EMPTY
457
+
458
+
459
+ def default_getter(
460
+ scope: typing.Any,
461
+ scopes: collections.abc.Iterable,
462
+ key: typing.AnyStr,
463
+ default: typing.Any = None,
464
+ *,
465
+ virtuals: VirtualPropertyMapping = default_virtuals,
466
+ ) -> typing.Any:
467
+ """
468
+ Extract property value from scope hierarchy.
469
+
470
+ :param scope: uppermost scope (corresponding to key ``'.'``)
471
+ :param scopes: parent scope sequence
472
+ :param key: property key
473
+ :param default: value will be used as default when missing
474
+ :param virtuals: mapping of virtual property callables
475
+ :returns: value from scope or default
476
+
477
+ .. versionadded:: 0.1.3
478
+ *virtuals* parameter.
479
+
480
+ Both :class:`AttributeError` and :class:`TypeError` exceptions
481
+ raised by virtual property implementations will be handled as if
482
+ that property doesn't exist, which can be useful to filter out
483
+ incompatible types.
484
+
485
+ """
486
+ if key in SELF_SCOPE:
487
+ return scope
488
+
489
+ binary_mode = not isinstance(key, str)
490
+ components = key.split(b'.' if binary_mode else '.') # type: ignore
491
+ for ref in (*scopes, scope)[::-1]:
492
+ for name in components:
493
+ if binary_mode:
494
+ try:
495
+ ref = ref[name]
496
+ continue
497
+ except (KeyError, TypeError, AttributeError):
498
+ pass
499
+
500
+ try:
501
+ name = name.decode() # type: ignore
502
+ except UnicodeDecodeError:
503
+ break
504
+
505
+ try:
506
+ ref = ref[name]
507
+ continue
508
+ except (KeyError, TypeError, AttributeError):
509
+ pass
510
+
511
+ if name.isdigit():
512
+ try:
513
+ ref = ref[int(name)]
514
+ continue
515
+ except (TypeError, KeyError, IndexError):
516
+ pass
517
+ else:
518
+ try:
519
+ ref = getattr(ref, name) # type: ignore
520
+ continue
521
+ except AttributeError:
522
+ pass
523
+
524
+ try:
525
+ ref = virtuals[name](ref) # type: ignore
526
+ continue
527
+ except (KeyError, TypeError, AttributeError):
528
+ pass
529
+
530
+ break
531
+
532
+ else:
533
+ return ref
534
+
535
+ return default
536
+
537
+
538
+ def default_lambda_render(
539
+ scope: typing.Any,
540
+ **kwargs,
541
+ ) -> collections.abc.Callable[[TString], TString]:
542
+ r"""
543
+ Generate a template-only render function with fixed parameters.
544
+
545
+ :param scope: current scope
546
+ :param \**kwargs: parameters forwarded to :func:`render`
547
+ :returns: template render function
548
+
549
+ """
550
+
551
+ def lambda_render(template: TString) -> TString:
552
+ """
553
+ Render given template to string/bytes.
554
+
555
+ :param template: template text
556
+ :returns: rendered string or bytes (depending on template type)
557
+
558
+ """
559
+ return render(template, scope, **kwargs)
560
+
561
+ return lambda_render
562
+
563
+
564
+ def tokenize(
565
+ template: bytes,
566
+ *,
567
+ tags: TagsByteTuple = default_tags,
568
+ comments: bool = False,
569
+ cache: CompiledTemplateCache = default_cache,
570
+ cache_make_key: CacheMakeKeyFunction = default_cache_make_key,
571
+ ) -> CompiledTemplate:
572
+ """
573
+ Compile mustache template as a tuple of token tuples.
574
+
575
+ :param template: template as utf-8 encoded bytes
576
+ :param tags: mustache tag tuple (open, close)
577
+ :param comments: whether yield comment tokens or not (ignore comments)
578
+ :param cache: mutable mapping for compiled template cache
579
+ :param cache_make_key: key function for compiled template cache
580
+ :returns: tuple of token tuples
581
+
582
+ :raises UnclosedTokenException: if token is left unclosed
583
+ :raises ClosingTokenException: if block closing token does not match
584
+ :raises DelimiterTokenException: if delimiter token syntax is invalid
585
+
586
+ """
587
+ tokenization_key = cache_make_key((template, *tags, comments))
588
+
589
+ if cached := cache.get(tokenization_key):
590
+ return cached
591
+
592
+ template_find = template.find
593
+
594
+ stack: list[tuple[bytes | None, int, int, int]] = []
595
+ stack_append = stack.append
596
+ stack_pop = stack.pop
597
+ scope_label = None
598
+ scope_head = 0
599
+ scope_start = 0
600
+ scope_index = 0
601
+
602
+ empty = EMPTY
603
+ start_tag, end_tag = tags
604
+ end_literal = b'}' + end_tag
605
+ end_switch = b'=' + end_tag
606
+ start_len = len(start_tag)
607
+ end_len = len(end_tag)
608
+
609
+ token_types = TOKEN_TYPES
610
+ recording: list[CompiledToken] = []
611
+ record = recording.append
612
+
613
+ text_start, text_end = 0, template_find(start_tag)
614
+ while text_end != -1:
615
+ if text_start < text_end: # text
616
+ record((
617
+ False, True, False,
618
+ empty,
619
+ template[text_start:text_end],
620
+ -1,
621
+ ))
622
+
623
+ tag_start = text_end + start_len
624
+ try:
625
+ a, b, c, d = token_types[template[tag_start]]
626
+ except IndexError:
627
+ raise UnclosedTokenException.from_template(
628
+ template=template,
629
+ start=text_end,
630
+ end=tag_start,
631
+ ) from None
632
+
633
+ if a: # variables
634
+ tag_start += b
635
+
636
+ if c: # variable
637
+ tag_end = template_find(end_tag, tag_start)
638
+ text_start = tag_end + end_len
639
+
640
+ else: # triple-keyed variable
641
+ tag_end = template_find(end_literal, tag_start)
642
+ text_start = tag_end + end_len + 1
643
+
644
+ record((
645
+ False, True, True,
646
+ template[tag_start:tag_end].strip(),
647
+ empty,
648
+ d,
649
+ ))
650
+
651
+ elif b:
652
+ tag_start += 1
653
+ tag_end = template_find(end_tag, tag_start)
654
+ text_start = tag_end + end_len
655
+
656
+ if c: # block
657
+ stack_append((scope_label, text_end, scope_start, scope_index))
658
+ scope_label = template[tag_start:tag_end].strip()
659
+ scope_head = text_end
660
+ scope_start = text_start
661
+ scope_index = len(recording) + 1
662
+ record((
663
+ True, False, True,
664
+ scope_label,
665
+ empty,
666
+ d,
667
+ ))
668
+
669
+ elif scope_label != template[tag_start:tag_end].strip():
670
+ raise ClosingTokenException.from_template(
671
+ template=template,
672
+ start=text_end,
673
+ end=text_start,
674
+ )
675
+
676
+ else: # close / loop
677
+ record((
678
+ True, True, True,
679
+ scope_label,
680
+ template[scope_start:text_end].strip(),
681
+ scope_index,
682
+ ))
683
+ scope_label, scope_head, scope_start, scope_index = stack_pop()
684
+
685
+ elif c: # partial
686
+ tag_start += 1
687
+ tag_end = template_find(end_tag, tag_start)
688
+ text_start = tag_end + end_len
689
+ record((
690
+ True, False, False,
691
+ template[tag_start:tag_end].strip(),
692
+ empty,
693
+ -1,
694
+ ))
695
+
696
+ elif d: # tags
697
+ tag_start += 1
698
+ tag_end = template_find(end_switch, tag_start)
699
+ text_start = tag_end + end_len + 1
700
+
701
+ try:
702
+ start_tag, end_tag = template[tag_start:tag_end].split(b' ')
703
+ except ValueError:
704
+ raise DelimiterTokenException.from_template(
705
+ template=template,
706
+ start=text_end,
707
+ end=text_start,
708
+ ) from None
709
+
710
+ if not (start_tag and end_tag):
711
+ raise DelimiterTokenException.from_template(
712
+ template=template,
713
+ start=text_end,
714
+ end=text_start,
715
+ )
716
+
717
+ end_literal = b'}' + end_tag
718
+ end_switch = b'=' + end_tag
719
+ start_len = len(start_tag)
720
+ end_len = len(end_tag)
721
+ start_end = tag_start + start_len
722
+ end_start = tag_end - end_len
723
+
724
+ record((
725
+ False, False, True,
726
+ template[tag_start:start_end].strip(),
727
+ template[end_start:tag_end].strip(),
728
+ -1,
729
+ ))
730
+
731
+ else: # comment
732
+ tag_start += 1
733
+ tag_end = template_find(end_tag, tag_start)
734
+ text_start = tag_end + end_len
735
+
736
+ if comments:
737
+ record((
738
+ False, False, False,
739
+ empty,
740
+ template[tag_start:tag_end].strip(),
741
+ -1,
742
+ ))
743
+
744
+ if tag_end < 0:
745
+ raise UnclosedTokenException.from_template(
746
+ template=template,
747
+ start=tag_start,
748
+ end=tag_end,
749
+ )
750
+
751
+ text_end = template_find(start_tag, text_start)
752
+
753
+ if stack:
754
+ raise UnclosedTokenException.from_template(
755
+ template=template,
756
+ start=scope_head,
757
+ end=scope_start,
758
+ )
759
+
760
+ if (tail := template[text_start:]) or not recording:
761
+ record((False, True, False, empty, tail, -1)) # text
762
+
763
+ tokens = cache[tokenization_key] = tuple(recording)
764
+ return tokens
765
+
766
+
767
+ def process(
768
+ template: TString,
769
+ scope: typing.Any,
770
+ *,
771
+ scopes: collections.abc.Iterable = (),
772
+ resolver: PartialResolver = default_resolver,
773
+ getter: PropertyGetter = default_getter,
774
+ stringify: StringifyFunction = default_stringify,
775
+ escape: EscapeFunction = default_escape,
776
+ lambda_render: LambdaRenderFunctionConstructor = default_lambda_render,
777
+ tags: TagsTuple = default_tags,
778
+ cache: CompiledTemplateCache = default_cache,
779
+ cache_make_key: CacheMakeKeyFunction = default_cache_make_key,
780
+ recursion_limit: int = default_recursion_limit,
781
+ ) -> collections.abc.Generator[bytes, None, None]:
782
+ """
783
+ Generate rendered mustache template byte chunks.
784
+
785
+ :param template: mustache template string
786
+ :param scope: root object used as root mustache scope
787
+ :param scopes: iterable of parent scopes
788
+ :param resolver: callable will be used to resolve partials (bytes)
789
+ :param getter: callable will be used to pick variables from scope
790
+ :param stringify: callable will be used to render python types (bytes)
791
+ :param escape: callable will be used to escape template (bytes)
792
+ :param lambda_render: lambda render function constructor
793
+ :param tags: mustache tag tuple (open, close)
794
+ :param cache: mutable mapping for compiled template cache
795
+ :param cache_make_key: key function for compiled template cache
796
+ :param recursion_limit: maximum number of nested render operations
797
+ :returns: byte chunk generator
798
+
799
+ :raises UnclosedTokenException: if token is left unclosed
800
+ :raises ClosingTokenException: if block closing token does not match
801
+ :raises DelimiterTokenException: if delimiter token syntax is invalid
802
+ :raises TemplateRecursionError: if rendering recursion limit is exceeded
803
+
804
+ """
805
+ if recursion_limit < 0:
806
+ message = 'Template rendering exceeded maximum render recursion'
807
+ raise TemplateRecursionError(message)
808
+
809
+ text_mode = isinstance(template, str)
810
+ next_recursion_level = recursion_limit - 1
811
+
812
+ # encoding
813
+ data: bytes = (
814
+ template.encode() if text_mode else # type: ignore
815
+ template
816
+ )
817
+ current_tags: TagsByteTuple = tuple( # type: ignore
818
+ tag.encode() if isinstance(tag, str) else tag
819
+ for tag in tags[:2]
820
+ )
821
+
822
+ # current context
823
+ siblings: collections.abc.Iterator | None = None
824
+ callback = False
825
+ silent = False
826
+
827
+ # context stack
828
+ stack: list[tuple[collections.abc.Iterator | None, bool, bool]] = []
829
+ stack_append = stack.append
830
+ stack_pop = stack.pop
831
+
832
+ # scope stack
833
+ scopes = list(scopes)
834
+ scopes_append = scopes.append
835
+ scopes_pop = scopes.pop
836
+
837
+ # locals
838
+ decode: typing.Callable[[bytes], typing.AnyStr] = (
839
+ bytes.decode if text_mode else # type: ignore
840
+ bytes
841
+ )
842
+ isnan = math.isnan
843
+ missing = MISSING
844
+ falsies = COMMON_FALSY
845
+ non_looping = NON_LOOPING_TYPES
846
+ mappings = collections.abc.Mapping
847
+
848
+ start = 0
849
+ tokens = tokenized = tokenize(
850
+ data,
851
+ tags=current_tags,
852
+ cache=cache,
853
+ cache_make_key=cache_make_key,
854
+ )
855
+ while True:
856
+ for a, b, c, token_name, token_content, token_option in tokens:
857
+ if silent:
858
+ if a:
859
+ if b: # close / loop
860
+ closing_scope = scope
861
+ closing_callback = callback
862
+
863
+ scope = scopes_pop()
864
+ siblings, callback, silent = stack_pop()
865
+
866
+ if closing_callback and not silent:
867
+ yield stringify(
868
+ closing_scope(
869
+ decode(token_content),
870
+ lambda_render(
871
+ scope=scope,
872
+ scopes=scopes,
873
+ resolver=resolver,
874
+ getter=getter,
875
+ stringify=stringify,
876
+ escape=escape,
877
+ lambda_render=lambda_render,
878
+ tags=current_tags,
879
+ cache=cache,
880
+ cache_make_key=cache_make_key,
881
+ recursion_limit=next_recursion_level,
882
+ ),
883
+ ),
884
+ text_mode,
885
+ )
886
+
887
+ elif c: # block
888
+ scopes_append(scope)
889
+ stack_append((siblings, callback, silent))
890
+
891
+ elif a:
892
+ if b: # close / loop
893
+ if siblings:
894
+ try:
895
+ scope = next(siblings)
896
+ callback = callable(scope)
897
+ silent = callback
898
+
899
+ if start != token_option:
900
+ start = token_option
901
+ tokens = tokenized[start:]
902
+
903
+ break # restart
904
+ except StopIteration:
905
+ scope = scopes_pop()
906
+ siblings, callback, silent = stack_pop()
907
+ else:
908
+ scope = scopes_pop()
909
+ siblings, callback, silent = stack_pop()
910
+
911
+ elif c: # block
912
+ curr = scope
913
+ scope = getter(curr, scopes, decode(token_name), None)
914
+
915
+ scopes_append(curr)
916
+ stack_append((siblings, callback, silent))
917
+
918
+ falsy = (
919
+ hasattr(scope, '__float__') and isnan(scope)
920
+ if scope else
921
+ scope in falsies or not isinstance(scope, mappings)
922
+ )
923
+ if token_option: # falsy block
924
+ siblings = None
925
+ callback = False
926
+ silent = not falsy
927
+ elif falsy: # truthy block with falsy value
928
+ siblings = None
929
+ callback = False
930
+ silent = True
931
+ elif (hasattr(scope, '__iter__')
932
+ and not isinstance(scope, non_looping)): # loop
933
+ try:
934
+ siblings = iter(scope)
935
+ scope = next(siblings) # type: ignore
936
+ callback = callable(scope)
937
+ silent = callback
938
+ except StopIteration:
939
+ siblings = None
940
+ scope = None
941
+ callback = False
942
+ silent = True
943
+ else: # truthy block with truthy value
944
+ siblings = None
945
+ callback = callable(scope)
946
+ silent = callback
947
+
948
+ else: # partial
949
+ value = resolver(decode(token_name))
950
+ if value:
951
+ yield from process(
952
+ template=value,
953
+ scope=scope,
954
+ scopes=scopes,
955
+ resolver=resolver,
956
+ getter=getter,
957
+ stringify=stringify,
958
+ escape=escape,
959
+ lambda_render=lambda_render,
960
+ tags=current_tags,
961
+ cache=cache,
962
+ cache_make_key=cache_make_key,
963
+ recursion_limit=next_recursion_level,
964
+ )
965
+
966
+ elif b:
967
+ if c: # variable
968
+ value = getter(scope, scopes, decode(token_name), missing)
969
+ if value is not missing:
970
+ yield (
971
+ stringify(value, text_mode) if token_option else
972
+ escape(stringify(value, text_mode))
973
+ )
974
+
975
+ else: # text
976
+ yield token_content
977
+
978
+ elif c: # tags
979
+ current_tags = token_name, token_content
980
+
981
+ # else comment
982
+
983
+ else:
984
+ break
985
+
986
+
987
+ def stream(
988
+ template: TString,
989
+ scope: typing.Any,
990
+ *,
991
+ scopes: collections.abc.Iterable = (),
992
+ resolver: PartialResolver = default_resolver,
993
+ getter: PropertyGetter = default_getter,
994
+ stringify: StringifyFunction = default_stringify,
995
+ escape: EscapeFunction = default_escape,
996
+ lambda_render: LambdaRenderFunctionConstructor = default_lambda_render,
997
+ tags: TagsTuple = default_tags,
998
+ cache: CompiledTemplateCache = default_cache,
999
+ cache_make_key: CacheMakeKeyFunction = default_cache_make_key,
1000
+ recursion_limit: int = default_recursion_limit,
1001
+ ) -> collections.abc.Generator[TString, None, None]:
1002
+ """
1003
+ Generate rendered mustache template chunks.
1004
+
1005
+ :param template: mustache template (str or bytes)
1006
+ :param scope: current rendering scope (data object)
1007
+ :param scopes: list of precedent scopes
1008
+ :param resolver: callable will be used to resolve partials (bytes)
1009
+ :param getter: callable will be used to pick variables from scope
1010
+ :param stringify: callable will be used to render python types (bytes)
1011
+ :param escape: callable will be used to escape template (bytes)
1012
+ :param lambda_render: lambda render function constructor
1013
+ :param tags: tuple (start, end) specifying the initial mustache delimiters
1014
+ :param cache: mutable mapping for compiled template cache
1015
+ :param cache_make_key: key function for compiled template cache
1016
+ :param recursion_limit: maximum number of nested render operations
1017
+ :returns: generator of bytes/str chunks (same type as template)
1018
+
1019
+ :raises UnclosedTokenException: if token is left unclosed
1020
+ :raises ClosingTokenException: if block closing token does not match
1021
+ :raises DelimiterTokenException: if delimiter token syntax is invalid
1022
+ :raises RenderingRecursionError: if rendering recursion limit is exceeded
1023
+
1024
+ """
1025
+ chunks = process(
1026
+ template=template,
1027
+ scope=scope,
1028
+ scopes=scopes,
1029
+ resolver=resolver,
1030
+ getter=getter,
1031
+ stringify=stringify,
1032
+ escape=escape,
1033
+ lambda_render=lambda_render,
1034
+ tags=tags,
1035
+ cache=cache,
1036
+ cache_make_key=cache_make_key,
1037
+ recursion_limit=recursion_limit,
1038
+ )
1039
+ return (
1040
+ codecs.iterdecode(chunks, 'utf8') if isinstance(template, str) else
1041
+ chunks
1042
+ )
1043
+
1044
+
1045
+ def render(
1046
+ template: TString,
1047
+ scope: typing.Any,
1048
+ *,
1049
+ scopes: collections.abc.Iterable = (),
1050
+ resolver: PartialResolver = default_resolver,
1051
+ getter: PropertyGetter = default_getter,
1052
+ stringify: StringifyFunction = default_stringify,
1053
+ escape: EscapeFunction = default_escape,
1054
+ lambda_render: LambdaRenderFunctionConstructor = default_lambda_render,
1055
+ tags: TagsTuple = default_tags,
1056
+ cache: CompiledTemplateCache = default_cache,
1057
+ cache_make_key: CacheMakeKeyFunction = default_cache_make_key,
1058
+ recursion_limit: int = default_recursion_limit,
1059
+ ) -> TString:
1060
+ """
1061
+ Render mustache template.
1062
+
1063
+ :param template: mustache template
1064
+ :param scope: current rendering scope (data object)
1065
+ :param scopes: list of precedent scopes
1066
+ :param resolver: callable will be used to resolve partials (bytes)
1067
+ :param getter: callable will be used to pick variables from scope
1068
+ :param stringify: callable will be used to render python types (bytes)
1069
+ :param escape: callable will be used to escape template (bytes)
1070
+ :param lambda_render: lambda render function constructor
1071
+ :param tags: tuple (start, end) specifying the initial mustache delimiters
1072
+ :param cache: mutable mapping for compiled template cache
1073
+ :param cache_make_key: key function for compiled template cache
1074
+ :param recursion_limit: maximum number of nested render operations
1075
+ :returns: rendered bytes/str (type depends on template)
1076
+
1077
+ :raises UnclosedTokenException: if token is left unclosed
1078
+ :raises ClosingTokenException: if block closing token does not match
1079
+ :raises DelimiterTokenException: if delimiter token syntax is invalid
1080
+ :raises TemplateRecursionError: if rendering recursion limit is exceeded
1081
+
1082
+ """
1083
+ data = b''.join(process(
1084
+ template=template,
1085
+ scope=scope,
1086
+ scopes=scopes,
1087
+ resolver=resolver,
1088
+ getter=getter,
1089
+ stringify=stringify,
1090
+ escape=escape,
1091
+ lambda_render=lambda_render,
1092
+ tags=tags,
1093
+ cache=cache,
1094
+ cache_make_key=cache_make_key,
1095
+ recursion_limit=recursion_limit,
1096
+ ))
1097
+ return data.decode() if isinstance(template, str) else data
1098
+
1099
+
1100
+ def cli(argv: collections.abc.Sequence[str] | None = None) -> None:
1101
+ """
1102
+ Render template from command line.
1103
+
1104
+ Use `python -m mstache --help` to check available options.
1105
+
1106
+ :param argv: command line arguments, :attr:`sys.argv` when None
1107
+
1108
+ """
1109
+ import argparse
1110
+ import json
1111
+ import sys
1112
+
1113
+ arguments = argparse.ArgumentParser(
1114
+ description='Render mustache template.',
1115
+ )
1116
+ arguments.add_argument(
1117
+ 'template',
1118
+ metavar='PATH',
1119
+ type=argparse.FileType('r'),
1120
+ help='template file',
1121
+ )
1122
+ arguments.add_argument(
1123
+ '-j', '--json',
1124
+ metavar='PATH',
1125
+ type=argparse.FileType('r'),
1126
+ default=sys.stdin,
1127
+ help='JSON file, default: stdin',
1128
+ )
1129
+ arguments.add_argument(
1130
+ '-o', '--output',
1131
+ metavar='PATH',
1132
+ type=argparse.FileType('w'),
1133
+ default=sys.stdout,
1134
+ help='output file, default: stdout',
1135
+ )
1136
+ args = arguments.parse_args(argv)
1137
+ try:
1138
+ args.output.write(render(args.template.read(), json.load(args.json)))
1139
+ finally:
1140
+ args.template.close()
1141
+ for fd, std in ((args.json, sys.stdin), (args.output, sys.stdout)):
1142
+ if fd is not std:
1143
+ fd.close()
1144
+
1145
+
1146
+ if __name__ == '__main__':
1147
+ cli()