From 5ac5597ca235ef746e5bfdffd47fa4a931306003 Mon Sep 17 00:00:00 2001 From: Stig Ofstad Date: Tue, 9 Apr 2024 12:15:58 +0200 Subject: [PATCH] feat: add product endpoint --- api/src/app.py | 15 +++- api/src/calculators/fraction_interpolator.py | 83 ++++++++----------- api/src/config.py | 2 + api/src/controllers/products.py | 31 ++++++- api/src/util/authentication.py | 2 + api/src/util/azure_table.py | 2 +- docker-compose.yaml | 1 + .../Bridging/Graphs/BridgeGraph.tsx | 2 - .../Components/Bridging/InputContainer.tsx | 2 +- .../Combinations/CombinationCard.tsx | 2 +- web/src/Utils.ts | 2 +- 11 files changed, 83 insertions(+), 61 deletions(-) diff --git a/api/src/app.py b/api/src/app.py index 6f79114..a3b251e 100644 --- a/api/src/app.py +++ b/api/src/app.py @@ -7,7 +7,7 @@ from controllers.combination import bridge_from_combination from controllers.optimal_bridge import bridgeRequestHandler from controllers.optimizer import optimizer_request_handler -from controllers.products import products_get +from controllers.products import products_get, products_post from controllers.report import create_report from util.authentication import authorize from util.sync_share_point_az import sync_all @@ -22,10 +22,17 @@ def init_api(): app = init_api() -@app.route("/api/products", methods=["GET"]) +@app.route("/api/products", methods=["GET", "POST"]) @authorize -def products(): - return products_get() +def products() -> Response: + if request.method == "GET": + return products_get() + if request.method == "POST": + name: str = request.json.get("productName") + supplier: str = request.json.get("productSupplier") + product_data: [[float, float]] = request.json.get("productData") + return products_post(name, supplier, product_data) + raise ValueError("Invalid method for endpoint") @app.route("/api/report", methods=["POST"]) diff --git a/api/src/calculators/fraction_interpolator.py b/api/src/calculators/fraction_interpolator.py index 7642fff..eeeaa08 100644 --- a/api/src/calculators/fraction_interpolator.py +++ b/api/src/calculators/fraction_interpolator.py @@ -1,63 +1,48 @@ -import csv -from copy import copy +from bisect import bisect_left from numpy import log from calculators.bridge import SIZE_STEPS -def lookup_smaller(table: dict, value: float): - n = [i for i in table.keys() if i <= value] - return max(n) +def find_closest_bigger_index(array: list[float], target: float) -> int: + index = bisect_left(array, target) + if index == 0: + raise ValueError("Failed to find closest biggest index") + return index + 1 -def lookup_bigger(table: dict, value: float): - n = [i for i in table.keys() if i >= value] - return min(n) +def log_interpolate_or_extrapolate(xMin: float, yMin: float, xMax: float, yMax: float, z: float) -> float: + increase = (log(z) - log(xMin)) / (log(xMax) - log(xMin)) + return increase * (yMax - yMin) + yMin -def fraction_interpolator(x: list[float], y: list[float], z: list[float]) -> list[float]: - table_dict = dict(zip(x, y, strict=False)) - max_x = max(x) +def fraction_interpolator_and_extrapolator( + xArray: list[float], yArray: list[float], zArray: list[float] = SIZE_STEPS +) -> list[float]: + sizes_dict = {size: 0 for size in zArray} # Populate size_dict with 0 values + starting_index = find_closest_bigger_index(zArray, min(xArray)) - 1 + s = sum(yArray) + print(f"Sum Y: {s}") - z_table = {} - for _z in z: - if _z > max_x: - break - smaller_x = lookup_smaller(table_dict, _z) - bigger_x = lookup_bigger(table_dict, _z) - z_table[_z] = {"x1": smaller_x, "x2": bigger_x, "y1": table_dict[smaller_x], "y2": table_dict[bigger_x]} + for zIndex, z in enumerate(zArray[starting_index:]): + if z < xArray[0]: # Don't extrapolate down from first measuring point + continue + # If z is above the range of xArray, use the last two points for extrapolation + elif z > xArray[-1]: + yz = log_interpolate_or_extrapolate(xArray[-2], yArray[-2], xArray[-1], yArray[-1], z) + else: + # Find the interval that z falls into for interpolation + for i in range(1, len(xArray)): + if xArray[i - 1] <= z <= xArray[i]: + yz = log_interpolate_or_extrapolate(xArray[i - 1], yArray[i - 1], xArray[i], yArray[i], z) + break - for zz, values in z_table.items(): - x1 = values["x1"] - x2 = values["x2"] - y1 = values["y1"] - y2 = values["y2"] + if yz > 100: # 100% volume has been reached. Stop extrapolation. Set all remaining to 100% + for key in zArray[zIndex + starting_index :]: + sizes_dict[key] = 100 + return list(sizes_dict.values()) - values["j"] = (y2 - y1) / (log(x2) - log(x1)) * (log(zz) - log(x1)) + y1 - return [round(v["j"], 3) for v in z_table.values()] + sizes_dict[z] = round(yz, ndigits=3) - -def from_csv_to_csv(): - with open("test_data/interpolate_input.csv") as csvfile: - reader = csv.DictReader(csvfile) - fields_copy = copy(reader.fieldnames) - fields_copy.pop(0) - products = {name: [] for name in fields_copy} - a_x = [] - for line in reader: - a_x.append(float(line["Size"])) - for n in products: - products[n].append(float(line[n])) - - for name in products: - b_y = fraction_interpolator(x=a_x, y=products[name], z=SIZE_STEPS) - with open(f"test_data/{name}.csv", "w+") as newcsvfile: - writer = csv.DictWriter(newcsvfile, fieldnames=["Size", "Cumulative"]) - writer.writeheader() - for step, interpol_value in zip(SIZE_STEPS, b_y, strict=False): - writer.writerow({"Size": step, "Cumulative": interpol_value}) - - -if __name__ == "__main__": - from_csv_to_csv() + return list(sizes_dict.values()) diff --git a/api/src/config.py b/api/src/config.py index bc73125..6b7f605 100644 --- a/api/src/config.py +++ b/api/src/config.py @@ -4,6 +4,7 @@ class Config: + AUTH_DISABLED = os.getenv("AUTH_DISABLED", "false") TABLE_ACCOUNT_NAME = os.getenv("TABLE_ACCOUNT_NAME", "lcmdevstorage") TABLE_KEY = os.getenv("TABLE_KEY") BLOB_CONTAINER_NAME = "lcm-file-blobs" @@ -19,4 +20,5 @@ class Config: DEFAULT_MAX_ITERATIONS = 100 HOME_DIR = str(Path(__file__).parent.absolute()) PRODUCT_TABLE_NAME = "products" + CUSTOM_PRODUCT_TABLE = "interpolatedproducts" named_supplier = ("Baker Hughes", "Halliburton", "Schlumberger") diff --git a/api/src/controllers/products.py b/api/src/controllers/products.py index 743abed..b5ff6e4 100644 --- a/api/src/controllers/products.py +++ b/api/src/controllers/products.py @@ -1,8 +1,9 @@ from cachetools import TTLCache, cached from flask import Response +from calculators.fraction_interpolator import fraction_interpolator_and_extrapolator from config import Config -from util.azure_table import get_service +from util.azure_table import get_table_service, sanitize_row_key def sort_products(products: dict[str, dict]): @@ -14,7 +15,11 @@ def sort_products(products: dict[str, dict]): @cached(cache=TTLCache(maxsize=128, ttl=300)) def products_get(): - products = get_service().query_entities(Config.PRODUCT_TABLE_NAME) + table_service = get_table_service() + + share_point_products = table_service.query_entities(Config.PRODUCT_TABLE_NAME) + interpolated_products = table_service.query_entities(Config.CUSTOM_PRODUCT_TABLE) + products = [*share_point_products, *interpolated_products] products_response = {} for p in products: @@ -36,3 +41,25 @@ def products_get(): sorted_products = sort_products(products_response) return sorted_products + + +def products_post(product_name: str, supplier_name: str, product_data: [[float, float]]) -> Response: + product_id = sanitize_row_key(product_name) + sizes = [p[0] for p in product_data] + cumulative = [p[1] for p in product_data] + table_entry = { + "PartitionKey": Config.CUSTOM_PRODUCT_TABLE, + "RowKey": product_id, + "id": product_id, + "title": product_name, + "supplier": supplier_name, + "cumulative": str(fraction_interpolator_and_extrapolator(sizes, cumulative)), + "sack_size": 25, + "environment": "Green", + "cost": 100, + "co2": 1000, + } + + get_table_service().insert_entity(Config.CUSTOM_PRODUCT_TABLE, table_entry) + products_get.cache_clear() + return table_entry diff --git a/api/src/util/authentication.py b/api/src/util/authentication.py index 3c6d191..ddbdb1b 100644 --- a/api/src/util/authentication.py +++ b/api/src/util/authentication.py @@ -45,6 +45,8 @@ def decode_jwt(token): def authorize(f): @wraps(f) def wrap(*args, **kwargs): + if Config.AUTH_DISABLED == "true": + return f(*args, **kwargs) if "Authorization" not in request.headers: abort(401, "Missing 'Authorization' header") try: diff --git a/api/src/util/azure_table.py b/api/src/util/azure_table.py index a5e5709..fd6583f 100644 --- a/api/src/util/azure_table.py +++ b/api/src/util/azure_table.py @@ -47,7 +47,7 @@ def sanitize_row_key(value: str) -> str: return value.replace("/", "-").replace("\\", "-").replace("#", "").replace("?", "-").replace(" ", "").lower() -def get_service(): +def get_table_service() -> TableService: return TableService(account_name=Config.TABLE_ACCOUNT_NAME, account_key=Config.TABLE_KEY) diff --git a/docker-compose.yaml b/docker-compose.yaml index 2ce7ca4..c334c4d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,6 +15,7 @@ services: restart: unless-stopped environment: ENVIRONMENT: development + AUTH_DISABLED: "true" FLASK_DEBUG: "true" TABLE_KEY: ${TABLE_KEY} ports: diff --git a/web/src/Components/Bridging/Graphs/BridgeGraph.tsx b/web/src/Components/Bridging/Graphs/BridgeGraph.tsx index f7efea9..d2547ee 100644 --- a/web/src/Components/Bridging/Graphs/BridgeGraph.tsx +++ b/web/src/Components/Bridging/Graphs/BridgeGraph.tsx @@ -15,8 +15,6 @@ type BridgeGraphProps = { const CustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { - console.log(payload) - console.log(label) return (
{`Particle size : ${label}µm`}
diff --git a/web/src/Components/Bridging/InputContainer.tsx b/web/src/Components/Bridging/InputContainer.tsx index dee2bb2..41a90c9 100644 --- a/web/src/Components/Bridging/InputContainer.tsx +++ b/web/src/Components/Bridging/InputContainer.tsx @@ -139,7 +139,7 @@ const InputContainer = ({ Optimal Bridge:
{[10, 50, 90].map(d => ( -

+

D{d}: {findDValue(optimalBridgeGraphData, d, 'Bridge')} {'\u00B5m'}

diff --git a/web/src/Components/Combinations/CombinationCard.tsx b/web/src/Components/Combinations/CombinationCard.tsx index 0ef043a..d684716 100644 --- a/web/src/Components/Combinations/CombinationCard.tsx +++ b/web/src/Components/Combinations/CombinationCard.tsx @@ -101,8 +101,8 @@ export const CombinationCard = ({ setD90(findDValue(graphData, 90, combination.name)) }) .catch(error => { - ErrorToast(`${error.response.data}`, error.response.status) console.error('fetch error' + error) + ErrorToast(`${error.response.data}`, error.response.status) }) }, [combination, allProducts]) diff --git a/web/src/Utils.ts b/web/src/Utils.ts index ea8f4c9..4464724 100644 --- a/web/src/Utils.ts +++ b/web/src/Utils.ts @@ -45,7 +45,7 @@ export function findDValue(graphData: GraphData[], goalYValue: number, bridgeNam } return false }) - if (!indexOfClosestHigherYValue) throw new Error('Failed to find D-value of bridge') + if (!indexOfClosestHigherYValue) return 0 // interpolate the values to get an approx value for the exact D requested return linearInterpolation(