expression-py 0.1.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,171 @@
1
+ # Copyright (C) 2026 Jakub T. Jankiewicz <https://jcu.bi/>
2
+ #
3
+ # This file is part of expression.py.
4
+ #
5
+ # expression.py is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # expression.py is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with expression.py. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import re
19
+ import token
20
+ import tokenize
21
+
22
+
23
+ class ExpressionTokenizer:
24
+ """Custom tokenizer for the expression language, compatible with pegen's Parser."""
25
+
26
+ TOKEN_PATTERNS = [
27
+ ('WHITESPACE', r'[ \t]+'),
28
+ ('HEX', r'0x[0-9A-Fa-f]+'),
29
+ ('BIN', r'0b[01]+'),
30
+ ('FLOAT', r'(?:[0-9]+\.[0-9]*|\.[0-9]+)(?:e[+-]?[0-9]+)?|[0-9]+e[+-]?[0-9]+'),
31
+ ('INT', r'[0-9]+'),
32
+ ('DSTRING', r'"[^"\\]*(?:\\[\S\s][^"\\]*)*"'),
33
+ ('SSTRING', r"'[^'\\]*(?:\\[\S\s][^'\\]*)*'"),
34
+ ('REGEX', r'/(?:[^/\\]|\\.)+/[imsxUXJ]*'),
35
+ ('STRICT_EQ', r'==='),
36
+ ('STRICT_NE', r'!=='),
37
+ ('EQ', r'=='),
38
+ ('MATCH', r'=~'),
39
+ ('NE', r'!='),
40
+ ('NOT', r'!'),
41
+ ('ASSIGN', r'='),
42
+ ('AND', r'&&'),
43
+ ('OR', r'\|\|'),
44
+ ('POWER', r'\*\*'),
45
+ ('SPACESHIP', r'<=>'),
46
+ ('LSHIFT', r'<<'),
47
+ ('RSHIFT', r'>>'),
48
+ ('GE', r'>='),
49
+ ('LE', r'<='),
50
+ ('GT', r'>'),
51
+ ('LT', r'<'),
52
+ ('PLUS', r'\+'),
53
+ ('MINUS', r'-'),
54
+ ('TIMES', r'\*'),
55
+ ('DIV', r'/'),
56
+ ('MOD', r'%'),
57
+ ('AMP', r'&'),
58
+ ('PIPE', r'\|'),
59
+ ('QUESTION', r'\?'),
60
+ ('CARET', r'\^'),
61
+ ('LPAREN', r'\('),
62
+ ('RPAREN', r'\)'),
63
+ ('LBRACKET', r'\['),
64
+ ('RBRACKET', r'\]'),
65
+ ('LBRACE', r'\{'),
66
+ ('RBRACE', r'\}'),
67
+ ('COMMA', r','),
68
+ ('COLON', r':'),
69
+ ('SEMICOLON', r';'),
70
+ ('DOLLAR_NAME', r'\$[0-9A-Za-z_]+'),
71
+ ('NAME', r'[A-Za-z_][A-Za-z_0-9]*'),
72
+ ]
73
+
74
+ _compiled = re.compile('|'.join(f'(?P<{name}>{pattern})' for name, pattern in TOKEN_PATTERNS))
75
+
76
+ OP_TOKENS = {
77
+ 'STRICT_EQ', 'STRICT_NE', 'EQ', 'MATCH', 'NE', 'NOT', 'ASSIGN',
78
+ 'AND', 'OR', 'POWER', 'SPACESHIP', 'LSHIFT', 'RSHIFT', 'GE', 'LE', 'GT', 'LT',
79
+ 'PLUS', 'MINUS', 'TIMES', 'DIV', 'MOD', 'AMP', 'PIPE', 'QUESTION', 'CARET',
80
+ 'LPAREN', 'RPAREN', 'LBRACKET', 'RBRACKET', 'LBRACE', 'RBRACE',
81
+ 'COMMA', 'COLON', 'SEMICOLON',
82
+ }
83
+
84
+ def __init__(self, text):
85
+ self._text = text
86
+ self._tokens = []
87
+ self._index = 0
88
+ self._tokenize(text)
89
+
90
+ REGEX_TOKEN_TYPE = 100
91
+
92
+ def _can_be_regex(self):
93
+ if not self._tokens:
94
+ return True
95
+ last = self._tokens[-1]
96
+ if last.type == token.NUMBER:
97
+ return False
98
+ if last.type == token.NAME:
99
+ return False
100
+ if last.type == token.STRING:
101
+ return False
102
+ if last.string in (')', ']'):
103
+ return False
104
+ return True
105
+
106
+ def _tokenize(self, text):
107
+ pos = 0
108
+ line = text
109
+ while pos < len(text):
110
+ m = self._compiled.match(text, pos)
111
+ if m is None:
112
+ raise SyntaxError(f"Unexpected character: {text[pos]!r} at position {pos}")
113
+ kind = m.lastgroup
114
+ value = m.group()
115
+ start = (1, pos)
116
+ end = (1, pos + len(value))
117
+ pos = m.end()
118
+ if kind == 'WHITESPACE':
119
+ continue
120
+ if kind == 'REGEX':
121
+ if self._can_be_regex():
122
+ tok = tokenize.TokenInfo(self.REGEX_TOKEN_TYPE, value, start, end, line)
123
+ else:
124
+ div_end = (1, start[1] + 1)
125
+ tok = tokenize.TokenInfo(token.OP, '/', start, div_end, line)
126
+ self._tokens.append(tok)
127
+ pos = start[1] + 1
128
+ continue
129
+ elif kind in ('HEX', 'BIN', 'FLOAT', 'INT'):
130
+ tok = tokenize.TokenInfo(token.NUMBER, value, start, end, line)
131
+ elif kind in ('DSTRING', 'SSTRING'):
132
+ tok = tokenize.TokenInfo(token.STRING, value, start, end, line)
133
+ elif kind in ('NAME', 'DOLLAR_NAME'):
134
+ tok = tokenize.TokenInfo(token.NAME, value, start, end, line)
135
+ elif kind in self.OP_TOKENS:
136
+ tok = tokenize.TokenInfo(token.OP, value, start, end, line)
137
+ else:
138
+ tok = tokenize.TokenInfo(token.OP, value, start, end, line)
139
+ self._tokens.append(tok)
140
+ self._tokens.append(
141
+ tokenize.TokenInfo(token.ENDMARKER, '', (1, pos), (1, pos), line)
142
+ )
143
+
144
+ def peek(self):
145
+ if self._index < len(self._tokens):
146
+ return self._tokens[self._index]
147
+ return self._tokens[-1]
148
+
149
+ def getnext(self):
150
+ tok = self.peek()
151
+ if self._index < len(self._tokens):
152
+ self._index += 1
153
+ return tok
154
+
155
+ def mark(self):
156
+ return self._index
157
+
158
+ def reset(self, index):
159
+ self._index = index
160
+
161
+ def diagnose(self):
162
+ if not self._tokens:
163
+ return tokenize.TokenInfo(token.ENDMARKER, '', (1, 0), (1, 0), '')
164
+ return self._tokens[min(self._index, len(self._tokens) - 1)]
165
+
166
+ def get_last_non_whitespace_token(self):
167
+ for i in range(self._index - 1, -1, -1):
168
+ tok = self._tokens[i]
169
+ if tok.type not in (token.ENDMARKER, token.NEWLINE, token.INDENT, token.DEDENT):
170
+ return tok
171
+ return self._tokens[0] if self._tokens else None
@@ -0,0 +1,350 @@
1
+ Metadata-Version: 2.4
2
+ Name: expression-py
3
+ Version: 0.1.0
4
+ Summary: Mathematical expression evaluation library with PEG parser
5
+ Author-email: "Jakub T. Jankiewicz" <jcubic.jj@gmail.com>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/jcubic/expression.py
8
+ Project-URL: Repository, https://github.com/jcubic/expression.py
9
+ Project-URL: Issues, https://github.com/jcubic/expression.py/issues
10
+ Keywords: expression,evaluator,parser,peg,math
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
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 :: Scientific/Engineering :: Mathematics
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: pegen>=0.3.0
25
+ Dynamic: license-file
26
+
27
+ # expression.py
28
+
29
+ [![pip](https://img.shields.io/badge/pip-0.1.0-blue.svg)](https://pypi.org/project/expression-py/)
30
+ [![CI](https://github.com/jcubic/expression.py/actions/workflows/ci.yml/badge.svg)](https://github.com/jcubic/expression.py/actions/workflows/ci.yml)
31
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/expression-py?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/expression-py)
32
+ [![expression.py GitHub repo](https://img.shields.io/badge/github-expression.py-orange?logo=github)](https://github.com/jcubic/expression.py)
33
+ [![Coverage Status](https://coveralls.io/repos/github/jcubic/expression.py/badge.svg?branch=master)](https://coveralls.io/github/jcubic/expression.py?branch=master)
34
+ [![LICENSE GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue.svg)](https://github.com/jcubic/expression.py/blob/master/LICENSE)
35
+
36
+ Mathematical expression evaluation library for Python, built with a PEG parser generated by
37
+ [pegen](https://github.com/we-like-parsers/pegen). An alternative to
38
+ [safeeval](https://pypi.org/project/safeeval/) that supports more Ruby operators like `=~` and array
39
+ operators.
40
+
41
+ This is a Python port of [jcubic/expression.php](https://github.com/jcubic/expression.php).
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install expression-py
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ```python
52
+ from expression import Expression
53
+
54
+ expr = Expression()
55
+
56
+ # Basic arithmetic
57
+ expr.evaluate("2 + 3 * 4") # 14
58
+ expr.evaluate("(2 + 3) * 4") # 20
59
+ expr.evaluate("2 ** 10") # 1024
60
+ expr.evaluate("10 % 3") # 1
61
+
62
+ # Floating point
63
+ expr.evaluate("0.1 + 0.2") # 0.30000000000000004
64
+ expr.evaluate("1.5e2") # 150.0
65
+
66
+ # Hex and binary literals
67
+ expr.evaluate("0xFF") # 255
68
+ expr.evaluate("0b1010") # 10
69
+
70
+ # Bitshift operators
71
+ expr.evaluate("100 >> 2") # 25
72
+ expr.evaluate("100 << 2") # 400
73
+ ```
74
+
75
+ ### Variables
76
+
77
+ ```python
78
+ expr = Expression()
79
+
80
+ # Assignment
81
+ expr.evaluate("x = 10")
82
+ expr.evaluate("x + 2") # 12
83
+
84
+ # Pre-set variables
85
+ expr.variables = {"width": 100, "height": 50}
86
+ expr.evaluate("width * height") # 5000
87
+ ```
88
+
89
+ ### Built-in Constants and Functions
90
+
91
+ ```python
92
+ expr = Expression()
93
+
94
+ # Constants
95
+ expr.evaluate("pi") # 3.141592653589793
96
+ expr.evaluate("e") # 2.718281828459045
97
+
98
+ # Math functions
99
+ expr.evaluate("sqrt(16)") # 4.0
100
+ expr.evaluate("sin(pi / 2)") # 1.0
101
+ expr.evaluate("abs(-42)") # 42
102
+ expr.evaluate("log(e)") # 1.0
103
+ ```
104
+
105
+ Available built-in functions: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`, `sqrt`, `abs`, `log`, `ln`.
106
+
107
+ Aliases: `arcsin`/`arccos`/`arctan`/`arcsinh`/`arccosh`/`arctanh` map to their `a`-prefixed equivalents. `ln` is an alias for `log`.
108
+
109
+ ### Custom Functions
110
+
111
+ ```python
112
+ expr = Expression()
113
+
114
+ # Define functions in the expression language
115
+ expr.evaluate("f(x, y) = x + y")
116
+ expr.evaluate("f(10, 20)") # 30
117
+
118
+ expr.evaluate("square(x) = x * x")
119
+ expr.evaluate("square(5)") # 25
120
+
121
+ # Register Python functions
122
+ expr.functions["even"] = lambda x: x % 2 == 0
123
+ expr.evaluate("even(4)") # True
124
+
125
+ # Access defined functions from Python
126
+ expr.evaluate("add(a, b) = a + b")
127
+ expr.functions["add"](3, 4) # 7
128
+ ```
129
+
130
+ ### Implicit Multiplication
131
+
132
+ ```python
133
+ expr = Expression()
134
+
135
+ expr.evaluate("f(x) = 2x")
136
+ expr.evaluate("f(5)") # 10
137
+ expr.evaluate("x = 3")
138
+ expr.evaluate("2x") # 6
139
+ expr.evaluate("2(3 + 4)") # 14
140
+ ```
141
+
142
+ ### Boolean and Comparison Operators
143
+
144
+ ```python
145
+ expr = Expression()
146
+
147
+ # Comparisons
148
+ expr.evaluate("10 == 10") # True
149
+ expr.evaluate("10 != 20") # True
150
+ expr.evaluate("10 > 5") # True
151
+ expr.evaluate("10 <= 10") # True
152
+
153
+ # Strict equality (type-aware)
154
+ expr.evaluate("2 === 2") # True
155
+ expr.evaluate("'2' !== 2") # True
156
+
157
+ # Boolean operators
158
+ expr.evaluate("10 > 5 && 20 > 10") # True
159
+ expr.evaluate("10 > 50 || 20 > 10") # True
160
+ expr.evaluate("!0") # True
161
+
162
+ # Keywords
163
+ expr.evaluate("true == true") # True
164
+ expr.evaluate("null == null") # True
165
+ ```
166
+
167
+ ### Strings
168
+
169
+ ```python
170
+ expr = Expression()
171
+
172
+ # String literals (single or double quotes)
173
+ expr.evaluate('"hello" + " " + "world"') # "hello world"
174
+ expr.evaluate("'foo' == 'foo'") # True
175
+
176
+ # Ruby-style string operators
177
+ expr.evaluate('"ab" * 3') # "ababab" (repeat)
178
+ expr.evaluate('"a" << "b"') # "ab" (append; mutates a variable)
179
+ expr.evaluate('"a" <=> "b"') # -1 (lexicographic compare)
180
+ ```
181
+
182
+ ### Regular Expressions
183
+
184
+ ```python
185
+ expr = Expression()
186
+
187
+ # Match operator
188
+ expr.evaluate('"foobar" =~ /([fo]+)/i') # True
189
+ expr.evaluate("$1") # "Foo" (capture group)
190
+
191
+ expr.evaluate('"hello" =~ /([0-9]+)/') # False
192
+ ```
193
+
194
+ ### JSON Literals
195
+
196
+ ```python
197
+ expr = Expression()
198
+
199
+ # Objects and arrays
200
+ expr.evaluate('{"name": "Alice"}') # {"name": "Alice"}
201
+ expr.evaluate('[10, 20, 30]') # [10, 20, 30]
202
+
203
+ # Property access
204
+ expr.evaluate('{"foo": "bar"}["foo"]') # "bar"
205
+ expr.evaluate('[10, 20][0]') # 10
206
+
207
+ # Comparison
208
+ expr.evaluate('{"a": 1} == {"a": 1}') # True
209
+ expr.evaluate('[1, 2] != [3, 4]') # True
210
+ ```
211
+
212
+ ### Array Operators
213
+
214
+ Ruby-inspired operators for concise list manipulation. When one operand is an
215
+ array and the other is a scalar, the scalar is coerced to a single-element array.
216
+
217
+ ```python
218
+ expr = Expression()
219
+
220
+ # Intersection (&) — common elements, deduped, left order preserved
221
+ expr.evaluate("[1, 1, 2, 3] & [3, 4]") # [3]
222
+
223
+ # Union (|) — combined elements, deduped
224
+ expr.evaluate("[1, 2] | [2, 3]") # [1, 2, 3]
225
+
226
+ # Difference (-) — left elements not in right
227
+ expr.evaluate("[1, 2, 2, 3] - [2]") # [1, 3]
228
+
229
+ # Concatenation (+) — keeps duplicates
230
+ expr.evaluate("[1, 2] + [2, 3]") # [1, 2, 2, 3]
231
+
232
+ # Append (<<) — mutates the left operand when it is a variable
233
+ expr.evaluate("[1, 2] << 3") # [1, 2, 3]
234
+
235
+ # Multiplication (*) — repeat with an integer
236
+ expr.evaluate("[1, 2] * 3") # [1, 2, 1, 2, 1, 2]
237
+ # Join (*) — with a string separator
238
+ expr.evaluate('["a", "b"] * "-"') # "a-b"
239
+
240
+ # Deep equality (==) and spaceship (<=>)
241
+ expr.evaluate("[1, 2] == [1, 2]") # True
242
+ expr.evaluate("[1, 2] <=> [1, 3]") # -1
243
+
244
+ # Membership (in) — array element, or substring of a string
245
+ expr.evaluate("2 in [1, 2, 3]") # True
246
+ expr.evaluate('"py" in "python"') # True
247
+
248
+ # Scalar coercion
249
+ expr.evaluate("[1, 2, 3] & 2") # [2]
250
+ expr.evaluate("1 + [2, 3]") # [1, 2, 3]
251
+ ```
252
+
253
+ When neither operand is an array, these operators fall back to scalar
254
+ semantics: `&` and `|` are bitwise AND/OR, `<<`/`>>` are bitshifts, `<=>`
255
+ compares numbers or strings, and `+`/`-`/`*` are arithmetic.
256
+
257
+ ```python
258
+ expr.evaluate("6 & 3") # 2 (bitwise AND)
259
+ expr.evaluate("6 | 1") # 7 (bitwise OR)
260
+ expr.evaluate("5 <=> 3") # 1
261
+ ```
262
+
263
+ Empty arrays are falsy (unlike JavaScript), so they work directly in boolean
264
+ contexts:
265
+
266
+ ```python
267
+ expr.evaluate("!![]") # False
268
+ expr.evaluate('[] || "default"') # "default"
269
+ expr.evaluate("[] ? 'yes' : 'no'") # "no"
270
+
271
+ # Validator pattern: true only when at least one skill matches
272
+ expr.variables = {"skills": ["Python", "AI"]}
273
+ bool(expr.evaluate('skills & ["AI", "ML"]')) # True
274
+ ```
275
+
276
+ ### Conditional (Ternary) Operator
277
+
278
+ ```python
279
+ expr = Expression()
280
+
281
+ expr.evaluate("1 > 0 ? 'yes' : 'no'") # "yes"
282
+ expr.evaluate("[] ? 'yes' : 'no'") # "no"
283
+ ```
284
+
285
+ ### Error Handling
286
+
287
+ ```python
288
+ expr = Expression()
289
+
290
+ # Errors raise exceptions by default
291
+ try:
292
+ expr.evaluate("10 / 0")
293
+ except ZeroDivisionError:
294
+ print("Division by zero!")
295
+
296
+ # Suppress errors (returns None on error)
297
+ expr.suppress_errors = True
298
+ expr.evaluate("unknown_var") # None
299
+ print(expr.last_error) # error message
300
+ ```
301
+
302
+ ### Semicolons
303
+
304
+ Trailing semicolons are optional and ignored:
305
+
306
+ ```python
307
+ expr.evaluate("10 + 10;") # 20
308
+ ```
309
+
310
+ ## Operator Precedence (lowest to highest)
311
+
312
+ | Precedence | Operators |
313
+ |-----------|-----------|
314
+ | 0 | `? :` (ternary) |
315
+ | 1 | `\|\|` |
316
+ | 2 | `&&` |
317
+ | 3 | `==`, `!=`, `===`, `!==`, `=~`, `<=>`, `>`, `<`, `>=`, `<=`, `in` |
318
+ | 4 | `<<`, `>>` |
319
+ | 5 | `+`, `-`, `\|` |
320
+ | 6 | `*`, `/`, `%`, `&`, `[]`, implicit multiplication |
321
+ | 7 | `!`, unary `-`, unary `+` |
322
+ | 8 | `**`, `^` |
323
+
324
+ Operators `&` (intersection), `\|` (union), `-` (difference), `+`
325
+ (concatenation), `<<` (append), `*` (repeat/join), `==` (deep equality), `<=>`
326
+ (spaceship), and `in` (membership) are type-dispatched: they apply array
327
+ semantics when either operand is an array, otherwise scalar semantics.
328
+
329
+ ## Development
330
+
331
+ ```bash
332
+ # Install in development mode
333
+ pip install -e ".[dev]"
334
+
335
+ # Run tests
336
+ pytest
337
+
338
+ # Regenerate parser from grammar
339
+ python -m pegen expression/grammar.peg -o expression/parser.py
340
+ ```
341
+
342
+ ## License
343
+
344
+ Copyright (c) 2026 [Jakub T. Jankiewicz](https://jakub.jankiewicz.org/)
345
+
346
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU
347
+ General Public License as published by the Free Software Foundation, either version 3 of the
348
+ License, or (at your option) any later version.
349
+
350
+ See [LICENSE](https://github.com/jcubic/expression.py/blob/master/LICENSE) for the full text.
@@ -0,0 +1,10 @@
1
+ expression/__init__.py,sha256=WsgYZ6L0lcV_4xlZFPeFcJR4QkvzfiQCY9_GPT_IBEQ,12147
2
+ expression/grammar.peg,sha256=aQ6Xttkp5ZYvOdoPLDIo5HsQ5DjUiLpvaXiQGwasQQM,6254
3
+ expression/helpers.py,sha256=KJgoiJganC3u_0l-F4YgFJ3yuFAd9OzwjLbvlg6ObhY,4681
4
+ expression/parser.py,sha256=rXeAC8498WMBEaXE7su8V8o4rIN7BF1RmpktwPLpVJg,26024
5
+ expression/tokenizer.py,sha256=pb3HAzk7Qvd4ue7hA4QQu5SeCON3Ma7tVJpZeNK5XzU,5893
6
+ expression_py-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
+ expression_py-0.1.0.dist-info/METADATA,sha256=DOQXXwRCJgAKtUlo3bwf9LJavBN3OAU05xgj7xhEtgI,10527
8
+ expression_py-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ expression_py-0.1.0.dist-info/top_level.txt,sha256=C_mXdwowx0RYeeMT6UIE9x297SZHlvVgtCIjaf24L8k,11
10
+ expression_py-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+