From 086cc4aebdd5ba5d7184d85cd2df6167e957ae36 Mon Sep 17 00:00:00 2001
From: Benedikt Seeger <benedikt.seeger@ptb.de>
Date: Thu, 25 Apr 2024 15:59:23 +0200
Subject: [PATCH] added REST API

---
 .gitignore       |   2 +
 dockerfile       |   8 +++-
 readme.md        | 110 +++++++++++++++++++++++++++++++++++++++++++++++
 requirements.txt |   5 +++
 restAPIServer.py |  56 ++++++++++++++++++++++++
 supervisor.conf  |  16 +++++++
 test_API.py      |  36 ++++++++++++++++
 7 files changed, 231 insertions(+), 2 deletions(-)
 create mode 100644 restAPIServer.py
 create mode 100644 supervisor.conf
 create mode 100644 test_API.py

diff --git a/.gitignore b/.gitignore
index b607d6c..f8657bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ venv/
 .idea/
 
 .vscode/
+
+__pycache__/
diff --git a/dockerfile b/dockerfile
index e157f0e..ef9db19 100644
--- a/dockerfile
+++ b/dockerfile
@@ -8,8 +8,12 @@ ENV GIT_SSL_NO_VERIFY=1
 RUN git clone https://gitlab1.ptb.de/digitaldynamicmeasurement/dsi-parser-frontend.git
 
 RUN pip install --no-cache-dir -r dsi-parser-frontend/requirements.txt
+# Install Supervisor to manage multiple processes
+RUN pip install supervisor
 
-EXPOSE 5020
+EXPOSE 5020 5021
 
-CMD ["bokeh", "serve", "dsi-parser-frontend/", "--port", "5020", "--allow-websocket-origin", "*","--use-xheaders","--prefix","dsi-parser-frontend"]
+# Command to start Supervisor, which handles starting both the Bokeh server and FastAPI
+CMD ["supervisord", "-c", "dsi-parser-frontend/supervisord.conf"]
+#CMD ["bokeh", "serve", "dsi-parser-frontend/", "--port", "5020", "--allow-websocket-origin", "*","--use-xheaders","--prefix","dsi-parser-frontend"]
 
diff --git a/readme.md b/readme.md
index b486875..842261b 100644
--- a/readme.md
+++ b/readme.md
@@ -13,6 +13,7 @@ pip install -r requirements.txt
 ```bash
 bokeh serve ./ --port 5020
 ```
+### Run the rest API
 
 ## Use Docker Container
 
@@ -21,3 +22,112 @@ docker pull benesee/dsiunitsfrontend:latest
 docker run -p 5020:5020 benesee/dsiunitsfrontend:latest
 ```
 
+
+Creating comprehensive API documentation for your FastAPI application helps users understand how to interact with your endpoints effectively. Below is a markdown format that you can use for your README file or any other documentation platform. This example includes descriptions of the endpoints, expected inputs, and outputs based on the FastAPI code you provided.
+
+---
+
+# API Documentation for Unit Conversion Service
+
+This documentation covers the available REST API endpoints for converting unit strings to UTF-8 and LaTeX formats, as well as comparing different units for scalability and conversion. Each endpoint is described with its functionality, required input, and example responses.
+
+## Endpoints
+
+### Convert to UTF-8
+
+- **URL**: `/convert/utf8/`
+- **Method**: `POST`
+- **Description**: Converts a unit string to its UTF-8 representation.
+- **Request Body**:
+  ```json
+  {
+    "unit_string": "String"
+  }
+  ```
+- **Responses**:
+  - **200 OK**:
+    ```json
+    {
+      "utf8_string": "Converted UTF-8 string"
+    }
+    ```
+  - **500 Internal Server Error**:
+    ```json
+    {
+      "detail": "Error message"
+    }
+    ```
+
+### Convert to LaTeX
+
+- **URL**: `/convert/latex/`
+- **Method**: `POST`
+- **Description**: Converts a unit string to its LaTeX representation.
+- **Request Body**:
+  ```json
+  {
+    "unit_string": "String"
+  }
+  ```
+- **Responses**:
+  - **200 OK**:
+    ```json
+    {
+      "latex_string": "Converted LaTeX string"
+    }
+    ```
+  - **500 Internal Server Error**:
+    ```json
+    {
+      "detail": "Error message"
+    }
+    ```
+
+### Compare Units
+
+- **URL**: `/compare/units/`
+- **Method**: `POST`
+- **Description**: Compares two unit strings for scalability and equivalence. Optionally performs a complete conversion comparison if specified.
+- **Request Body**:
+  ```json
+  {
+    "unit_string1": "First unit string",
+    "unit_string2": "Second unit string",
+    "complete": false
+  }
+  ```
+- **Responses**:
+  - **200 OK**:
+    ```json
+    {
+      "scale_factor": "Scale factor if units are scalable",
+      "base_unit": "Base unit derived from comparison"
+    }
+    ```
+  - **400 Bad Request**:
+    ```json
+    {
+    "detail": "Warnings or error details from unit validation"
+    }
+    ```
+  - **500 Internal Server Error**:
+    ```json
+    {
+      "detail": "Error message"
+    }
+    ```
+
+## Error Handling
+
+Errors are returned as standard HTTP response codes along with a JSON body specifying the detail of the error.
+
+- **400 Bad Request**: Sent when there's a validation error with the input data.
+- **500 Internal Server Error**: Sent when the server encounters an unexpected condition.
+
+## Example Usage
+
+Here's an example using `curl` to make a request to the `/convert/utf8/` endpoint:
+
+```bash
+curl -X POST "http://localhost:8000/convert/utf8/" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"unit_string\":\"\\meter\"}"
+```
diff --git a/requirements.txt b/requirements.txt
index 3d806be..9c2f2f6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,3 +2,8 @@ bokeh~=3.3.2
 #-e git+https://dockerPull:-jVp9LBaxeKp9HKAe9dw@gitlab1.ptb.de/digitaldynamicmeasurement/bokehCssPtb.git#egg=bokehCssPTB
 -e git+https://gitlab1.ptb.de/digitaldynamicmeasurement/bokehCssPtb.git#egg=bokehCssPTB
 dsiUnits~=2.2.1
+fastapi
+uvicorn
+httpx
+pydantic
+pytest
\ No newline at end of file
diff --git a/restAPIServer.py b/restAPIServer.py
new file mode 100644
index 0000000..91170e0
--- /dev/null
+++ b/restAPIServer.py
@@ -0,0 +1,56 @@
+import numpy as np
+from fastapi import FastAPI, HTTPException
+from pydantic import BaseModel
+from dsiUnits import dsiUnit
+app = FastAPI()
+
+class UnitRequest(BaseModel):
+    unit_string: str
+
+class UnitComparisonRequest(BaseModel):
+    unit_string1: str
+    unit_string2: str
+    complete: bool = False
+
+@app.post("/convert/utf8/")
+async def convert_to_utf8(request: UnitRequest):
+    try:
+        unit=dsiUnit(request.unit_string)
+        if unit.valid:
+            return {unit.toUTF8()}
+        else:
+            raise HTTPException(status_code=500, detail=str(unit.warnings))
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/convert/latex/")
+async def convert_to_latex(request: UnitRequest):
+    try:
+        unit = dsiUnit(request.unit_string)
+        # Assuming you have a function to convert unit strings to LaTeX
+        if unit.valid:
+            return {unit.toLatex()}
+        else:
+            raise HTTPException(status_code=500, detail=str(unit.warnings))
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.post("/compare/units/")
+async def compare_units(request: UnitComparisonRequest):
+    try:
+        unit1 = dsiUnit(request.unit_string1)
+        unit2 = dsiUnit(request.unit_string2)
+        complete = request.complete
+        if unit1.valid and unit2.valid:
+            scale_factor, base_unit = unit1.isScalablyEqualTo(unit2, complete=complete)
+            if not np.isnan(scale_factor):
+                return {"scale_factor": scale_factor, "base_unit": str(base_unit)}
+            else:
+                return {"error": "Units are not scalably equal try complete conversion instead. "}
+        else:
+            warnings = unit1.warnings if not unit1.valid else unit2.warnings
+            raise HTTPException(status_code=400, detail=str(warnings))
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=str(e))
+
diff --git a/supervisor.conf b/supervisor.conf
new file mode 100644
index 0000000..2f4e940
--- /dev/null
+++ b/supervisor.conf
@@ -0,0 +1,16 @@
+[supervisord]
+nodaemon=true
+
+[program:bokeh]
+command=bokeh serve dsi-parser-frontend/ --port 5020 --allow-websocket-origin=* --use-xheaders --prefix dsi-parser-frontend
+autostart=true
+autorestart=true
+stderr_logfile=/var/log/bokeh_stderr.log
+stdout_logfile=/var/log/bokeh_stdout.log
+
+[program:fastapi]
+command=uvicorn restAPIServer:app --host 0.0.0.0 --port 5021
+autostart=true
+autorestart=true
+stderr_logfile=/var/log/fastapi_stderr.log
+stdout_logfile=/var/log/fastapi_stdout.log
diff --git a/test_API.py b/test_API.py
new file mode 100644
index 0000000..9c57970
--- /dev/null
+++ b/test_API.py
@@ -0,0 +1,36 @@
+from fastapi.testclient import TestClient
+from restAPIServer import app
+
+client = TestClient(app)
+
+def test_convert_to_utf8():
+    response = client.post("/convert/utf8/", json={"unit_string": r"\metre"})
+    assert response.status_code == 200
+    assert response.json() == ["m"]
+
+def test_convert_to_latex():
+    response = client.post("/convert/latex/", json={"unit_string": r"\metre\tothe{2}"})
+    assert response.status_code == 200
+    assert response.json() == ['$$\\mathrm{m}^{2}$$'] # Adjust based on actual response structure
+
+def test_compare_units_equal():
+    response = client.post("/compare/units/", json={"unit_string1": r"\watt", "unit_string2": r"\joule\per\second"})
+    assert response.status_code == 200
+    assert response.json() == {'scale_factor': 1.0, 'base_unit': '\\kilogram\\metre\\tothe{2}\\second\\tothe{-3}'}  # Example expected response
+
+def test_compare_units_not_equal():
+    response = client.post("/compare/units/", json={"unit_string1": r"\metre", "unit_string2": r"\second"})
+    assert response.status_code == 200
+    assert "error" in response.json()  # Checking for error message
+
+def test_compare_units_not_equal_but_equal_WithCompleate():
+    response = client.post("/compare/units/", json={"unit_string1": r"\one", "unit_string2": r"\percent", })
+    assert response.status_code == 200
+    assert "error" in response.json()  # Checking for error message
+    response = client.post("/compare/units/", json={"unit_string1": r"\one", "unit_string2": r"\percent", "complete":True})
+    assert response.status_code == 200
+    assert response.json()  ==  {'scale_factor': 0.01, 'base_unit': '\\one'}
+
+def test_invalid_unit():
+    response = client.post("/convert/utf8/", json={"unit_string": "not_a_unit"})
+    assert response.status_code == 500  # Assuming your API returns 500 for invalid units
-- 
GitLab