diff --git a/calculator/Calculator.py b/calculator/Calculator.py
new file mode 100644
index 0000000000000000000000000000000000000000..f69d7238f0f2b3a48b51b380f256ffede8fb70dc
--- /dev/null
+++ b/calculator/Calculator.py
@@ -0,0 +1,57 @@
+"""
+This Calculator module is a simple calculator that can parse and evaluate expressions of the form:
+expression ::= term | expression operator expression
+operator ::= + | - | * | /
+with the usual precedence rules.
+"""
+
+from Operators import Operator, STANDARD_OPERATORS
+from Expression import Token, Term, Expression, TermExpression, OperatorExpression
+
+class Calculator:
+    def __init__(self, operators = STANDARD_OPERATORS):
+        self.operators = operators
+
+    def _tokenize(self, line: str) -> list[Token]:
+        """
+        Tokenize an expression into a list of tokens.
+        """
+        tokens = []
+        for token in line.split():
+            if token in self.operators:
+                tokens.append(self.operators[token])
+            else:
+                try:
+                    term = float(token)
+                    tokens.append(term)
+                except ValueError:
+                    raise ValueError(f"Unknown token: {token}")
+        return tokens
+
+    def _parse(self, tokens: list[Token]) -> Expression:
+        if not tokens:
+            raise ValueError("Empty expression")
+        if len(tokens) == 1:
+            if isinstance(tokens[0], Term):
+                return TermExpression(tokens[0])
+            raise ValueError(f"Expected a term, got {tokens[0]}")
+        if len(tokens) == 2:
+            raise ValueError("Invalid expression")
+
+        # Find the rightest operator with the lowest precedence
+        operator = None
+        for i, token in enumerate(tokens):
+            if isinstance(token, Operator):
+                if operator is None or token.precedence <= operator.precedence:
+                    operator = token
+                    operator_index = i
+
+        # Split the expression into two parts
+        left = tokens[:operator_index]
+        right = tokens[operator_index + 1:]
+
+        # Parse the left and right parts recursively
+        return OperatorExpression(operator, self._parse(left), self._parse(right))
+
+    def __call__(self, expression: str) -> Term:
+        return self._parse(self._tokenize(expression))()
diff --git a/calculator/Expression.py b/calculator/Expression.py
new file mode 100644
index 0000000000000000000000000000000000000000..d19ea88ffd75c185ae706239408735ac488f9f79
--- /dev/null
+++ b/calculator/Expression.py
@@ -0,0 +1,32 @@
+from typing import Union
+from Operators import Operator
+
+Term: type = float
+Token: type = Union[Operator, Term]
+
+
+class OperatorExpression:
+    def __init__(self, operator: Operator, left, right):
+        self.operator = operator
+        self.left = left
+        self.right = right
+
+    def __repr__(self):
+        return f"({self.left} {self.operator} {self.right})"
+
+    def __call__(self) -> Term:
+        return self.operator(self.left(), self.right())
+
+
+class TermExpression:
+    def __init__(self, value: Term):
+        self.value = value
+
+    def __repr__(self):
+        return str(self.value)
+
+    def __call__(self) -> Term:
+        return self.value
+
+
+Expression: type = Union[OperatorExpression, TermExpression]
diff --git a/calculator/Operators.py b/calculator/Operators.py
new file mode 100644
index 0000000000000000000000000000000000000000..fdd2090d31600b428cabcbd97622e1dc6e1d7cb1
--- /dev/null
+++ b/calculator/Operators.py
@@ -0,0 +1,46 @@
+class Operator:
+    def __init__(self, symbol, precedence):
+        self.symbol = symbol
+        self.precedence = precedence
+
+    def __repr__(self):
+        return self.symbol
+
+    def __call__(self):
+        raise NotImplemented
+
+class Adder(Operator):
+    def __init__(self):
+        super().__init__('+', 1)
+
+    def __call__(self, a, b):
+        return a + b
+
+class Subtracter(Operator):
+    def __init__(self):
+        super().__init__('-', 1)
+
+    def __call__(self, a, b):
+        return a - b
+
+class Multiplyer(Operator):
+    def __init__(self):
+        super().__init__('*', 2)
+
+    def __call__(self, a, b):
+        return a * b
+
+class Divider(Operator):
+    def __init__(self):
+        super().__init__('/', 2)
+
+    def __call__(self, a, b):
+        return a / b
+
+
+STANDARD_OPERATORS = {
+    '+': Adder(),
+    '-': Subtracter(),
+    '*': Multiplyer(),
+    '/': Divider(),
+}
diff --git a/calculator/README.md b/calculator/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c8389d840435ab15abba9e16b53e28d250a28e26
--- /dev/null
+++ b/calculator/README.md
@@ -0,0 +1,3 @@
+# ViaRézo Calculator
+
+This is a simple two-operand calculator. It is a good example of how to use the [GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) to lint, test and deploy a project.
diff --git a/calculator/__pycache__/Calculator.cpython-310.pyc b/calculator/__pycache__/Calculator.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..24c0a7f8401cd6d66646eb6938fc36c5b8dfbf5d
Binary files /dev/null and b/calculator/__pycache__/Calculator.cpython-310.pyc differ
diff --git a/calculator/__pycache__/Expression.cpython-310.pyc b/calculator/__pycache__/Expression.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..39cc4147cc27f4e3f9656a4e48fe56674b8f5f41
Binary files /dev/null and b/calculator/__pycache__/Expression.cpython-310.pyc differ
diff --git a/calculator/__pycache__/Operator.cpython-310.pyc b/calculator/__pycache__/Operator.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..53418f864c433e3db52a95846f613ed4d9581a61
Binary files /dev/null and b/calculator/__pycache__/Operator.cpython-310.pyc differ
diff --git a/calculator/__pycache__/Operators.cpython-310.pyc b/calculator/__pycache__/Operators.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..6e8f7a6a441d4e1396401e05cfb89d4727635010
Binary files /dev/null and b/calculator/__pycache__/Operators.cpython-310.pyc differ
diff --git a/calculator/__pycache__/calculator.cpython-310.pyc b/calculator/__pycache__/calculator.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e137347312a05e1267631372d1a44dda7fc76488
Binary files /dev/null and b/calculator/__pycache__/calculator.cpython-310.pyc differ
diff --git a/calculator/__pycache__/server.cpython-310.pyc b/calculator/__pycache__/server.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..02f6dd8e95307bfb97827355b4dc24d6df06874c
Binary files /dev/null and b/calculator/__pycache__/server.cpython-310.pyc differ
diff --git a/calculator/requirements.txt b/calculator/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9c7788af16fa05ef099b6c72c09d5702edf5712c
--- /dev/null
+++ b/calculator/requirements.txt
@@ -0,0 +1,2 @@
+fastapi[all]
+uvicorn[standard]
diff --git a/calculator/server.py b/calculator/server.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f5fed6b46f404572e5e3dfdad0b628f8add9db8
--- /dev/null
+++ b/calculator/server.py
@@ -0,0 +1,19 @@
+from fastapi import FastAPI
+from fastapi.requests import Request
+from fastapi.templating import Jinja2Templates
+from Calculator import Calculator
+
+app = FastAPI()
+templates = Jinja2Templates(directory="templates")
+calc = Calculator()
+
+@app.get("/icon")
+
+@app.get("/")
+async def root(request: Request):
+    expression = request.query_params.get("expression", "")
+    context = { "request": request }
+    if expression:
+        result = calc(expression)
+        context = { "request": request, "expression": expression, "result": result}
+    return templates.TemplateResponse("index.html", context)
diff --git a/calculator/templates/index.html b/calculator/templates/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..1806d83aff2e9f7c81cb7d20e746a821f84bafaa
--- /dev/null
+++ b/calculator/templates/index.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>Calculator</title>
+</head>
+<body>
+  <header>
+    <h1>ViaRézo Calculator</h1>
+  </header>
+  <div>
+    <form action="/" method="get">
+      {% if expression %}
+      <input type="text" name="expression" placeholder="12 / 2 + 6 * 8" value="{{ expression }}">
+      {% else %}
+      <input type="text" name="expression" placeholder="12 / 2 + 6 * 8">
+      {% endif %}
+      <button type="submit">Calculate</button>
+    </form>
+    {% if result %}
+    <p>Result: {{ result }}</p>
+    {% endif %}
+  </div>
+</body>
+</html