commit 632efd52efb761c0d3a6a1bc30f36e394af3adb2
Author: dm1sh <me@dmitriy.icu>
Date:   Mon Oct 9 07:03:54 2023 +0300

    Added math expressions parser

diff --git a/parser/.gitignore b/parser/.gitignore
new file mode 100644
index 0000000..7e99e36
--- /dev/null
+++ b/parser/.gitignore
@@ -0,0 +1 @@
+*.pyc
\ No newline at end of file
diff --git a/parser/README.md b/parser/README.md
new file mode 100644
index 0000000..3e743d3
--- /dev/null
+++ b/parser/README.md
@@ -0,0 +1,63 @@
+# Math expression parser
+
+Math expression evaluation library. It supports most of useful math operations and functions. Expressions can contain variables which can be substituted with `int`s, `float`s or `numpy.ndarray`s.
+
+## Example usage
+
+```python
+from parser import Parser
+
+parser = Parser("(-b + sqrt(b^2-4a c))/(2a)")
+
+parser.variables_names # {'c', 'a', 'b'}
+
+parser.evaluate({"a": 1, "b": -3, "c": 2}) # 1.0
+
+parser.evaluate({"a": [1, 1, 1], "b": [-5, -6, -9], "c": [6, 9, 20]}) # [2. 3. 4.]
+```
+## Expression syntax
+
+Expression can contain numbers or variable names with functions applyed to them, separated with operators or united with braces.
+
+Theese are supported:
+
+Functions: 
+
+|name| math |
+|--|--|
+| `abs` | $\|x\|$ |
+| `acos` | $\cos^{-1}(x)$ |
+| `acosh` | $\cosh^{-1}(x)$ |
+| `acot` | $\cot^{-1}(x)$ |
+| `asin` | $\sin^{-1}(x)$ |
+| `asinh` | $\sinh^{-1}(x)$ |
+| `atan` | $\tan^{-1}(x)$ |
+| `avg` | $\overline x$ |
+| `cos` | $\cos(x)$ |
+| `cosh` | $\cosh(x)$ |
+| `cot` | $\cot(x)$ |
+| `exp` | $\exp(x)$ |
+| `lg` | $\lg(x)$ |
+| `ln` | $\ln(x)$ |
+| `log10` | $\log_{10}(x)$ |
+| `log2` | $\log_2(x)$ |
+| `prod` | $\displaystyle \prod_{i=0}^n x_i$ |
+| `sgn` | $sgn(x)$ |
+| `sin` | $\sin(x)$ |
+| `sinh` | $\sinh(x)$ |
+| `sqrt` | $\sqrt{x}$ |
+| `sum` | $\displaystyle\sum_{i=0}^n x_i$ |
+| `tan` | $\tan(x)$ |
+| `tanh` | $\tanh(x)$ |
+
+Operators: `+`, `-`, `*`, `/`, `^`, `%`
+
+Braces: `()`, `[]`, `{}`
+
+Floating points: `.`, `,`
+
+Functions have only one argument, provided in braces. Operators must have two operands except minus (if it is the first character of equation or braced expression).
+
+`sum` and `prod` sums and multiplies all elements in variable if it is `numpy.ndarray` and produces a `float`.
+
+**! There is no error handling yet !**
diff --git a/parser/parser/__init__.py b/parser/parser/__init__.py
new file mode 100644
index 0000000..3385e94
--- /dev/null
+++ b/parser/parser/__init__.py
@@ -0,0 +1,21 @@
+from .parser import (
+    BinaryExpression,
+    Expression,
+    Operation,
+    Parser,
+    Token,
+    Tokenizer,
+    UnaryExpression,
+    ValueExpression,
+)
+
+__all__ = (
+    "BinaryExpression",
+    "Expression",
+    "Operation",
+    "Parser",
+    "Token",
+    "Tokenizer",
+    "UnaryExpression",
+    "ValueExpression",
+)
diff --git a/parser/parser/__main__.py b/parser/parser/__main__.py
new file mode 100644
index 0000000..bbfe661
--- /dev/null
+++ b/parser/parser/__main__.py
@@ -0,0 +1,16 @@
+from parser import Parser
+
+expression = input("Input math expression: ")
+
+parser = Parser(expression)
+
+print("Variables in your expression: " + ", ".join(parser.variables_names))
+
+variables = {}
+
+for key in parser.variables_names:
+    variables[key] = float(input(f"Input '{key}' variable value: "))
+
+res = parser.evaluate(variables)
+
+print(f"Evaluation result is: {res}")
diff --git a/parser/parser/expression.py b/parser/parser/expression.py
new file mode 100644
index 0000000..8563d76
--- /dev/null
+++ b/parser/parser/expression.py
@@ -0,0 +1,52 @@
+import abc
+from collections.abc import Callable, Mapping
+
+from .types import FunctionType, OperatorType, ValueType
+
+
+class Expression(abc.ABC):
+    _evaluator: Callable[[Mapping[str, ValueType]], ValueType]
+
+    def evaluate(self, variables: Mapping[str, ValueType]):
+        return self._evaluator(variables)
+
+
+class ValueExpression(Expression):
+    def __init__(self, a: str | ValueType):
+        self.__debug_a = a
+
+        if isinstance(a, str):
+            self._evaluator = lambda vars: vars[a]
+        else:
+            self._evaluator = lambda _: a
+
+    def __repr__(self):
+        return f"<{self.__debug_a}>"
+
+
+class UnaryExpression(Expression):
+    def __init__(self, function: FunctionType, a: Expression):
+        self.__debug_f = function.__name__
+        self.__debug_a = repr(a)
+
+        self._evaluator = lambda vars: function(a.evaluate(vars))
+
+    def __repr__(self):
+        return f"<{self.__debug_f}({self.__debug_a})>"
+
+
+class BinaryExpression(Expression):
+    def __init__(
+        self,
+        function: OperatorType,
+        a: Expression,
+        b: Expression,
+    ):
+        self.__debug_f = function.__name__
+        self.__debug_a = repr(a)
+        self.__debug_b = repr(b)
+
+        self._evaluator = lambda vars: function(a.evaluate(vars), b.evaluate(vars))
+
+    def __repr__(self):
+        return f"<{self.__debug_a} {self.__debug_f} {self.__debug_b}>"
diff --git a/parser/parser/operation.py b/parser/parser/operation.py
new file mode 100644
index 0000000..7ff47cd
--- /dev/null
+++ b/parser/parser/operation.py
@@ -0,0 +1,84 @@
+from dataclasses import dataclass
+
+import numpy as np
+
+from .types import FunctionType, OperatorType, ValueType
+
+
+def acot(x: ValueType):
+    return np.arctan(1 / x)
+
+
+def cot(x: ValueType):
+    return 1 / np.tan(x)
+
+
+functions: dict[str, FunctionType] = {
+    "abs": np.abs,
+    "acos": np.arccos,
+    "acosh": np.arccosh,
+    "acot": acot,
+    "asin": np.arcsin,
+    "asinh": np.arcsinh,
+    "atan": np.arctan,
+    "avg": np.average,
+    "cos": np.cos,
+    "cosh": np.cosh,
+    "cot": cot,
+    "exp": np.exp,
+    "lg": np.log10,
+    "ln": np.log,
+    "log10": np.log10,
+    "log2": np.log2,
+    "prod": np.prod,
+    "sgn": np.sign,
+    "sin": np.sin,
+    "sinh": np.sinh,
+    "sqrt": np.sqrt,
+    "sum": np.sum,
+    "tan": np.tan,
+    "tah": np.tanh,
+}
+
+operators: dict[str, OperatorType] = {
+    "+": np.add,
+    "-": np.subtract,
+    "*": np.multiply,
+    "/": np.divide,
+    "%": np.mod,
+    "^": np.float_power,
+}
+
+priorities: dict[str, int] = {
+    "(": 0,
+    "+": 1,
+    "-": 1,
+    "*": 2,
+    "/": 2,
+    "%": 2,
+    "^": 3,
+    "f": 4,  # function
+    ")": 5,
+}
+
+
+@dataclass
+class Operation:
+    evaluator: (FunctionType | OperatorType | str)
+    priority: int
+    size: int
+
+
+class FunctionOperation(Operation):
+    def __init__(self, name: str):
+        super().__init__(functions[name], priorities["f"], 1)
+
+
+class BraceOperation(Operation):
+    def __init__(self, name: str):
+        super().__init__(name, priorities[name], 0)
+
+
+class OperatorOperation(Operation):
+    def __init__(self, name: str):
+        super().__init__(operators[name], priorities[name], 2)
diff --git a/parser/parser/parser.py b/parser/parser/parser.py
new file mode 100644
index 0000000..71eaa1c
--- /dev/null
+++ b/parser/parser/parser.py
@@ -0,0 +1,81 @@
+from collections.abc import Mapping
+
+from .expression import BinaryExpression, Expression, UnaryExpression, ValueExpression
+from .operation import (
+    BraceOperation,
+    FunctionOperation,
+    Operation,
+    OperatorOperation,
+    priorities,
+)
+from .tokenizer import Token, Tokenizer
+from .types import ValueType
+
+
+class Parser:
+    def __init__(self, input_expr: str):
+        self.input_expr = input_expr
+        self.variables_names: set[str] = set()
+
+        self.tokenize()
+        self.parse()
+
+    def tokenize(self):
+        self.tokens = Tokenizer(self.input_expr)
+
+    def parse(self):
+        self.val_stack: list[Expression] = []
+        self.op_stack: list[Operation] = []
+
+        for t_val, t_type in self.tokens:
+            if t_type in (Token.Number, Token.Variable):
+                self.val_stack.append(ValueExpression(t_val))
+
+                if t_type == Token.Variable:
+                    self.variables_names.add(t_val)
+
+            elif t_type == Token.Function:
+                self.op_stack.append(FunctionOperation(t_val))
+
+            elif t_type == Token.LBrace:
+                self.op_stack.append(BraceOperation("("))
+
+            elif t_type == Token.RBrace:
+                while len(self.op_stack) > 0 and not (
+                    self.op_stack[-1].size == 0 and self.op_stack[-1].priority == 0
+                ):  # until next in stack is lbrace
+                    self.do_one()
+                self.op_stack.pop()  # pop lbrace
+
+            elif t_type == Token.Operator:
+                t_priority = priorities[t_val]
+
+                while (
+                    len(self.op_stack) > 0 and self.op_stack[-1].priority > t_priority
+                ):
+                    self.do_one()
+
+                self.op_stack.append(OperatorOperation(t_val))
+
+        while len(self.op_stack) > 0:
+            self.do_one()
+
+        self._evaluator = self.val_stack[0].evaluate
+
+        self.__debug_expr = repr(self.val_stack)
+
+    def evaluate(self, variables: Mapping[str, ValueType]):
+        return self._evaluator(variables)
+
+    def do_one(self):
+        op = self.op_stack.pop()
+
+        if op.size == 1:
+            a = self.val_stack.pop()
+            self.val_stack.append(UnaryExpression(op.evaluator, a))
+        elif op.size == 2:
+            b, a = self.val_stack.pop(), self.val_stack.pop()  # inversed pop order
+            self.val_stack.append(BinaryExpression(op.evaluator, a, b))
+
+    def __repr__(self):
+        return self.__debug_expr
diff --git a/parser/parser/tokenizer.py b/parser/parser/tokenizer.py
new file mode 100644
index 0000000..60501fb
--- /dev/null
+++ b/parser/parser/tokenizer.py
@@ -0,0 +1,106 @@
+from collections.abc import Generator
+from dataclasses import dataclass
+from typing import Optional
+
+from .operation import functions, operators
+from .types import Token, TokenType
+
+
+@dataclass
+class Tokenizer:
+    expression: str
+
+    def __iter__(self) -> Generator[TokenType, None, None]:
+        accumulator = ""
+        prev = None
+
+        for ch in self.expression:
+            if (breaker_type := self.is_breaker(ch)) is not None:
+                if len(accumulator) > 0:
+                    # ch is `(` after function name
+                    if breaker_type == Token.LBrace and self.is_function(accumulator):
+                        yield accumulator, Token.Function
+                        prev = Token.Function
+                        accumulator = ""
+                    else:
+                        value, token_type = self.detect_number(accumulator)
+                        yield value, token_type
+                        prev = token_type
+                        accumulator = ""
+
+                        # `(` after variable or number
+                        if breaker_type == Token.LBrace:
+                            yield "*", Token.Operator
+                            prev = Token.Operator
+
+                # Unary minus case
+                if ch == "-" and (prev == Token.LBrace or prev is None):
+                    yield 0, Token.Number
+                    prev = Token.Number
+
+                # `(expr)(expr)` case
+                if breaker_type == Token.LBrace and prev == Token.RBrace:
+                    yield "*", Token.Operator
+                    prev = Token.Operator
+
+                if breaker_type != Token.Space:
+                    yield ch, breaker_type
+                    prev = breaker_type
+            else:
+                # Variable or function name after braced expr or variable and space
+                if prev == Token.RBrace or prev == Token.Variable:
+                    yield "*", Token.Operator
+                    prev = Token.Operator
+
+                # Floating point number
+                if ch in ",.":
+                    accumulator += "."
+                    continue
+
+                # Variable or function name after number
+                if (
+                    not ch.isdecimal()
+                    and (num := Tokenizer.is_number(accumulator)) is not None
+                ):
+                    yield num, Token.Number
+                    yield "*", Token.Operator
+                    prev = Token.Operator
+                    accumulator = ""
+
+                accumulator += ch
+        if len(accumulator) > 0:
+            yield self.detect_number(accumulator)
+
+    @staticmethod
+    def is_breaker(character) -> Optional[Token]:
+        if character in operators:
+            return Token.Operator
+        if character in "([{":
+            return Token.LBrace
+        if character in ")]}":
+            return Token.RBrace
+        if character == " ":
+            return Token.Space
+
+        return None
+
+    @staticmethod
+    def is_number(string) -> Optional[float]:
+        try:
+            return float(string)
+        except ValueError:
+            return None
+
+    @staticmethod
+    def detect_number(string) -> TokenType:
+        if (num := Tokenizer.is_number(string)) is not None:
+            return num, Token.Number
+        else:
+            return string, Token.Variable
+
+    @staticmethod
+    def is_function(lexeme: str) -> bool:
+        if lexeme in functions:
+            return True
+
+        return False
diff --git a/parser/parser/types.py b/parser/parser/types.py
new file mode 100644
index 0000000..f451f39
--- /dev/null
+++ b/parser/parser/types.py
@@ -0,0 +1,23 @@
+from collections.abc import Callable
+from enum import Enum, auto
+
+import numpy as np
+
+ValueType = int | float | np.ndarray
+
+FunctionType = Callable[[ValueType], ValueType]
+
+OperatorType = Callable[[ValueType, ValueType], ValueType]
+
+
+class Token(Enum):
+    Variable = auto()
+    Number = auto()
+    Function = auto()
+    Operator = auto()
+    LBrace = auto()
+    RBrace = auto()
+    Space = auto()
+
+
+TokenType = tuple[str | float, Token]
diff --git a/parser/poetry.lock b/parser/poetry.lock
new file mode 100644
index 0000000..28b912f
--- /dev/null
+++ b/parser/poetry.lock
@@ -0,0 +1,436 @@
+# This file is automatically @generated by Poetry and should not be changed by hand.
+
+[[package]]
+name = "attrs"
+version = "23.1.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
+    {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
+]
+
+[package.extras]
+cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
+dev = ["attrs[docs,tests]", "pre-commit"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
+tests = ["attrs[tests-no-zope]", "zope-interface"]
+tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+
+[[package]]
+name = "black"
+version = "23.9.1"
+description = "The uncompromising code formatter."
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"},
+    {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"},
+    {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"},
+    {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"},
+    {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"},
+    {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"},
+    {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"},
+    {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"},
+    {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"},
+    {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"},
+    {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"},
+    {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"},
+    {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"},
+    {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"},
+    {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"},
+    {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"},
+    {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"},
+    {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"},
+    {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"},
+    {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"},
+    {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"},
+    {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "cattrs"
+version = "23.1.2"
+description = "Composable complex class support for attrs and dataclasses."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "cattrs-23.1.2-py3-none-any.whl", hash = "sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4"},
+    {file = "cattrs-23.1.2.tar.gz", hash = "sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657"},
+]
+
+[package.dependencies]
+attrs = ">=20"
+
+[package.extras]
+bson = ["pymongo (>=4.2.0,<5.0.0)"]
+cbor2 = ["cbor2 (>=5.4.6,<6.0.0)"]
+msgpack = ["msgpack (>=1.0.2,<2.0.0)"]
+orjson = ["orjson (>=3.5.2,<4.0.0)"]
+pyyaml = ["PyYAML (>=6.0,<7.0)"]
+tomlkit = ["tomlkit (>=0.11.4,<0.12.0)"]
+ujson = ["ujson (>=5.4.0,<6.0.0)"]
+
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+    {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+    {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "docstring-to-markdown"
+version = "0.12"
+description = "On the fly conversion of Python docstrings to markdown"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "docstring-to-markdown-0.12.tar.gz", hash = "sha256:40004224b412bd6f64c0f3b85bb357a41341afd66c4b4896709efa56827fb2bb"},
+    {file = "docstring_to_markdown-0.12-py3-none-any.whl", hash = "sha256:7df6311a887dccf9e770f51242ec002b19f0591994c4783be49d24cdc1df3737"},
+]
+
+[[package]]
+name = "isort"
+version = "5.12.0"
+description = "A Python utility / library to sort Python imports."
+category = "dev"
+optional = false
+python-versions = ">=3.8.0"
+files = [
+    {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
+    {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
+]
+
+[package.extras]
+colors = ["colorama (>=0.4.3)"]
+pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
+plugins = ["setuptools"]
+requirements-deprecated-finder = ["pip-api", "pipreqs"]
+
+[[package]]
+name = "jedi"
+version = "0.19.1"
+description = "An autocompletion tool for Python that can be used for text editors."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"},
+    {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"},
+]
+
+[package.dependencies]
+parso = ">=0.8.3,<0.9.0"
+
+[package.extras]
+docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
+qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
+testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
+
+[[package]]
+name = "jedi-language-server"
+version = "0.41.1"
+description = "A language server for Jedi!"
+category = "dev"
+optional = false
+python-versions = ">=3.8,<4.0"
+files = [
+    {file = "jedi_language_server-0.41.1-py3-none-any.whl", hash = "sha256:ca9b3e7f48b70f0988d85ffde4f01dd1ab94c8e0f69e8c6424e6657117b44f91"},
+    {file = "jedi_language_server-0.41.1.tar.gz", hash = "sha256:3f15ca5cc28e728564f7d63583e171b418025582447ce023512e3f2b2d71ebae"},
+]
+
+[package.dependencies]
+cattrs = ">=23.1.2"
+docstring-to-markdown = "<1.0.0"
+jedi = ">=0.19.0,<0.20.0"
+lsprotocol = ">=2022.0.0a9"
+pygls = ">=1.0.1,<2.0.0"
+
+[[package]]
+name = "lsprotocol"
+version = "2023.0.0b1"
+description = "Python implementation of the Language Server Protocol."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "lsprotocol-2023.0.0b1-py3-none-any.whl", hash = "sha256:ade2cd0fa0ede7965698cb59cd05d3adbd19178fd73e83f72ef57a032fbb9d62"},
+    {file = "lsprotocol-2023.0.0b1.tar.gz", hash = "sha256:f7a2d4655cbd5639f373ddd1789807450c543341fa0a32b064ad30dbb9f510d4"},
+]
+
+[package.dependencies]
+attrs = ">=21.3.0"
+cattrs = "*"
+
+[[package]]
+name = "mypy"
+version = "1.5.1"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"},
+    {file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"},
+    {file = "mypy-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12"},
+    {file = "mypy-1.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d"},
+    {file = "mypy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25"},
+    {file = "mypy-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4"},
+    {file = "mypy-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4"},
+    {file = "mypy-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243"},
+    {file = "mypy-1.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275"},
+    {file = "mypy-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315"},
+    {file = "mypy-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb"},
+    {file = "mypy-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373"},
+    {file = "mypy-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161"},
+    {file = "mypy-1.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a"},
+    {file = "mypy-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1"},
+    {file = "mypy-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65"},
+    {file = "mypy-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160"},
+    {file = "mypy-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2"},
+    {file = "mypy-1.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb"},
+    {file = "mypy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"},
+    {file = "mypy-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a"},
+    {file = "mypy-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14"},
+    {file = "mypy-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb"},
+    {file = "mypy-1.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693"},
+    {file = "mypy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770"},
+    {file = "mypy-1.5.1-py3-none-any.whl", hash = "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5"},
+    {file = "mypy-1.5.1.tar.gz", hash = "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92"},
+]
+
+[package.dependencies]
+mypy-extensions = ">=1.0.0"
+typing-extensions = ">=4.1.0"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+    {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "numpy"
+version = "1.26.0"
+description = "Fundamental package for array computing in Python"
+category = "main"
+optional = false
+python-versions = "<3.13,>=3.9"
+files = [
+    {file = "numpy-1.26.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8db2f125746e44dce707dd44d4f4efeea8d7e2b43aace3f8d1f235cfa2733dd"},
+    {file = "numpy-1.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0621f7daf973d34d18b4e4bafb210bbaf1ef5e0100b5fa750bd9cde84c7ac292"},
+    {file = "numpy-1.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51be5f8c349fdd1a5568e72713a21f518e7d6707bcf8503b528b88d33b57dc68"},
+    {file = "numpy-1.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:767254ad364991ccfc4d81b8152912e53e103ec192d1bb4ea6b1f5a7117040be"},
+    {file = "numpy-1.26.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:436c8e9a4bdeeee84e3e59614d38c3dbd3235838a877af8c211cfcac8a80b8d3"},
+    {file = "numpy-1.26.0-cp310-cp310-win32.whl", hash = "sha256:c2e698cb0c6dda9372ea98a0344245ee65bdc1c9dd939cceed6bb91256837896"},
+    {file = "numpy-1.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:09aaee96c2cbdea95de76ecb8a586cb687d281c881f5f17bfc0fb7f5890f6b91"},
+    {file = "numpy-1.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:637c58b468a69869258b8ae26f4a4c6ff8abffd4a8334c830ffb63e0feefe99a"},
+    {file = "numpy-1.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:306545e234503a24fe9ae95ebf84d25cba1fdc27db971aa2d9f1ab6bba19a9dd"},
+    {file = "numpy-1.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6adc33561bd1d46f81131d5352348350fc23df4d742bb246cdfca606ea1208"},
+    {file = "numpy-1.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e062aa24638bb5018b7841977c360d2f5917268d125c833a686b7cbabbec496c"},
+    {file = "numpy-1.26.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:546b7dd7e22f3c6861463bebb000646fa730e55df5ee4a0224408b5694cc6148"},
+    {file = "numpy-1.26.0-cp311-cp311-win32.whl", hash = "sha256:c0b45c8b65b79337dee5134d038346d30e109e9e2e9d43464a2970e5c0e93229"},
+    {file = "numpy-1.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:eae430ecf5794cb7ae7fa3808740b015aa80747e5266153128ef055975a72b99"},
+    {file = "numpy-1.26.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:166b36197e9debc4e384e9c652ba60c0bacc216d0fc89e78f973a9760b503388"},
+    {file = "numpy-1.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f042f66d0b4ae6d48e70e28d487376204d3cbf43b84c03bac57e28dac6151581"},
+    {file = "numpy-1.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5e18e5b14a7560d8acf1c596688f4dfd19b4f2945b245a71e5af4ddb7422feb"},
+    {file = "numpy-1.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6bad22a791226d0a5c7c27a80a20e11cfe09ad5ef9084d4d3fc4a299cca505"},
+    {file = "numpy-1.26.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4acc65dd65da28060e206c8f27a573455ed724e6179941edb19f97e58161bb69"},
+    {file = "numpy-1.26.0-cp312-cp312-win32.whl", hash = "sha256:bb0d9a1aaf5f1cb7967320e80690a1d7ff69f1d47ebc5a9bea013e3a21faec95"},
+    {file = "numpy-1.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee84ca3c58fe48b8ddafdeb1db87388dce2c3c3f701bf447b05e4cfcc3679112"},
+    {file = "numpy-1.26.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4a873a8180479bc829313e8d9798d5234dfacfc2e8a7ac188418189bb8eafbd2"},
+    {file = "numpy-1.26.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:914b28d3215e0c721dc75db3ad6d62f51f630cb0c277e6b3bcb39519bed10bd8"},
+    {file = "numpy-1.26.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c78a22e95182fb2e7874712433eaa610478a3caf86f28c621708d35fa4fd6e7f"},
+    {file = "numpy-1.26.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f737708b366c36b76e953c46ba5827d8c27b7a8c9d0f471810728e5a2fe57c"},
+    {file = "numpy-1.26.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b44e6a09afc12952a7d2a58ca0a2429ee0d49a4f89d83a0a11052da696440e49"},
+    {file = "numpy-1.26.0-cp39-cp39-win32.whl", hash = "sha256:5671338034b820c8d58c81ad1dafc0ed5a00771a82fccc71d6438df00302094b"},
+    {file = "numpy-1.26.0-cp39-cp39-win_amd64.whl", hash = "sha256:020cdbee66ed46b671429c7265cf00d8ac91c046901c55684954c3958525dab2"},
+    {file = "numpy-1.26.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0792824ce2f7ea0c82ed2e4fecc29bb86bee0567a080dacaf2e0a01fe7654369"},
+    {file = "numpy-1.26.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d484292eaeb3e84a51432a94f53578689ffdea3f90e10c8b203a99be5af57d8"},
+    {file = "numpy-1.26.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:186ba67fad3c60dbe8a3abff3b67a91351100f2661c8e2a80364ae6279720299"},
+    {file = "numpy-1.26.0.tar.gz", hash = "sha256:f93fc78fe8bf15afe2b8d6b6499f1c73953169fad1e9a8dd086cdff3190e7fdf"},
+]
+
+[[package]]
+name = "packaging"
+version = "23.2"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+    {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
+]
+
+[[package]]
+name = "parso"
+version = "0.8.3"
+description = "A Python Parser"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
+    {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
+]
+
+[package.extras]
+qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
+testing = ["docopt", "pytest (<6.0.0)"]
+
+[[package]]
+name = "pathspec"
+version = "0.11.2"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
+    {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "3.11.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
+    {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
+
+[[package]]
+name = "pygls"
+version = "1.1.1"
+description = "A pythonic generic language server (pronounced like 'pie glass')"
+category = "dev"
+optional = false
+python-versions = ">=3.7.9,<4"
+files = [
+    {file = "pygls-1.1.1-py3-none-any.whl", hash = "sha256:330704551a335b443bf1cdfb0507f121608591095898d451f0007eeb1510067c"},
+    {file = "pygls-1.1.1.tar.gz", hash = "sha256:b1b4ddd6f800a5573f61f0ec2cd3bc7a859d171f48142b46e1de35a1357c00fe"},
+]
+
+[package.dependencies]
+lsprotocol = "2023.0.0b1"
+typeguard = ">=3.0.0,<4.0.0"
+
+[package.extras]
+ws = ["websockets (>=11.0.3,<12.0.0)"]
+
+[[package]]
+name = "ruff"
+version = "0.0.292"
+description = "An extremely fast Python linter, written in Rust."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "ruff-0.0.292-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:02f29db018c9d474270c704e6c6b13b18ed0ecac82761e4fcf0faa3728430c96"},
+    {file = "ruff-0.0.292-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:69654e564342f507edfa09ee6897883ca76e331d4bbc3676d8a8403838e9fade"},
+    {file = "ruff-0.0.292-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c3c91859a9b845c33778f11902e7b26440d64b9d5110edd4e4fa1726c41e0a4"},
+    {file = "ruff-0.0.292-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4476f1243af2d8c29da5f235c13dca52177117935e1f9393f9d90f9833f69e4"},
+    {file = "ruff-0.0.292-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be8eb50eaf8648070b8e58ece8e69c9322d34afe367eec4210fdee9a555e4ca7"},
+    {file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9889bac18a0c07018aac75ef6c1e6511d8411724d67cb879103b01758e110a81"},
+    {file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bdfabd4334684a4418b99b3118793f2c13bb67bf1540a769d7816410402a205"},
+    {file = "ruff-0.0.292-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7c77c53bfcd75dbcd4d1f42d6cabf2485d2e1ee0678da850f08e1ab13081a8"},
+    {file = "ruff-0.0.292-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e087b24d0d849c5c81516ec740bf4fd48bf363cfb104545464e0fca749b6af9"},
+    {file = "ruff-0.0.292-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f160b5ec26be32362d0774964e218f3fcf0a7da299f7e220ef45ae9e3e67101a"},
+    {file = "ruff-0.0.292-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ac153eee6dd4444501c4bb92bff866491d4bfb01ce26dd2fff7ca472c8df9ad0"},
+    {file = "ruff-0.0.292-py3-none-musllinux_1_2_i686.whl", hash = "sha256:87616771e72820800b8faea82edd858324b29bb99a920d6aa3d3949dd3f88fb0"},
+    {file = "ruff-0.0.292-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b76deb3bdbea2ef97db286cf953488745dd6424c122d275f05836c53f62d4016"},
+    {file = "ruff-0.0.292-py3-none-win32.whl", hash = "sha256:e854b05408f7a8033a027e4b1c7f9889563dd2aca545d13d06711e5c39c3d003"},
+    {file = "ruff-0.0.292-py3-none-win_amd64.whl", hash = "sha256:f27282bedfd04d4c3492e5c3398360c9d86a295be00eccc63914438b4ac8a83c"},
+    {file = "ruff-0.0.292-py3-none-win_arm64.whl", hash = "sha256:7f67a69c8f12fbc8daf6ae6d36705037bde315abf8b82b6e1f4c9e74eb750f68"},
+    {file = "ruff-0.0.292.tar.gz", hash = "sha256:1093449e37dd1e9b813798f6ad70932b57cf614e5c2b5c51005bf67d55db33ac"},
+]
+
+[[package]]
+name = "typeguard"
+version = "3.0.2"
+description = "Run-time type checker for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7.4"
+files = [
+    {file = "typeguard-3.0.2-py3-none-any.whl", hash = "sha256:bbe993854385284ab42fd5bd3bee6f6556577ce8b50696d6cb956d704f286c8e"},
+    {file = "typeguard-3.0.2.tar.gz", hash = "sha256:fee5297fdb28f8e9efcb8142b5ee219e02375509cd77ea9d270b5af826358d5a"},
+]
+
+[package.extras]
+doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["mypy (>=0.991)", "pytest (>=7)"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.8.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
+    {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
+]
+
+[metadata]
+lock-version = "2.0"
+python-versions = ">=3.11,<3.13"
+content-hash = "3180680d07071b1bed28aa242a0feb41baa73446b6edfefab15e2252da5dfdbf"
diff --git a/parser/pyproject.toml b/parser/pyproject.toml
new file mode 100644
index 0000000..36b5ad1
--- /dev/null
+++ b/parser/pyproject.toml
@@ -0,0 +1,25 @@
+[tool.poetry]
+name = "parser"
+version = "0.1.0"
+description = ""
+authors = ["dm1sh <me@dmitriy.icu>"]
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = ">=3.11,<3.13"
+numpy = "^1.26.0"
+
+
+[tool.poetry.group.dev.dependencies]
+black = "^23.9.1"
+mypy = "^1.5.1"
+ruff = "^0.0.292"
+jedi-language-server = "^0.41.1"
+isort = "^5.12.0"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.isort]
+profile = "black"
\ No newline at end of file
diff --git a/parser/tests/__init__.py b/parser/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/parser/tests/test_parser.py b/parser/tests/test_parser.py
new file mode 100644
index 0000000..2bca931
--- /dev/null
+++ b/parser/tests/test_parser.py
@@ -0,0 +1,14 @@
+from parser import Parser
+
+
+def test_Parser():
+    parser = Parser("(-b + sqrt(b^2-4a c))/(2a)")
+
+    assert parser.variables_names == {"c", "a", "b"}
+
+    assert parser.evaluate({"a": 1, "b": -3, "c": 2}) == 1.0
+
+    assert all(
+        parser.evaluate({"a": [1, 1, 1], "b": [-5, -6, -9], "c": [6, 9, 20]})
+        == [2.0, 3.0, 4.0]
+    )