From 0c33dae183ece893fbf128569e7e3740cf5eeda5 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Mon, 18 May 2026 14:56:55 +0300 Subject: [PATCH] baserow add --- core/nodeslicer/nodes.py | 64 ++++++++++++++++++++ core/nodeslicer/tree.py | 1 + infra/networking/baserow_backend.py | 87 ++++++++++++++++++++++++++++ infra/networking/printer_backends.py | 2 +- 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 infra/networking/baserow_backend.py diff --git a/core/nodeslicer/nodes.py b/core/nodeslicer/nodes.py index 16d3695..ef9b750 100644 --- a/core/nodeslicer/nodes.py +++ b/core/nodeslicer/nodes.py @@ -393,9 +393,18 @@ class SLICENODE_GetMesh(SliceNode): obj: Any = self.resolve("Object") if not obj: return None + modifiers = {v.field: v.value_str for k, v in dict(obj.nodeslicer.modifiers).items()} + extruder = obj.nodeslicer.extruder + part_type = obj.nodeslicer.object_type + family = [obj] + (get_all_children(obj) if self.include_children else []) sc: SlicingCollection = SlicingCollection(family, obj.name) + + for so in sc.objects: + so.assign_modifiers(modifiers) + so.object_type = part_type + so.extruder = extruder return sc @@ -1142,6 +1151,61 @@ class SLICENODE_StartPrint(ExecSliceNode): return 'ok' +@register_class +@register_class +class SLICENODE_BaserowSubmit(ExecSliceNode): + bl_label = "Baserow Submit" + button_label = "Submit to Baserow" + + host: NfStringProperty(name="Host", default="https://baserow.io") # pyright: ignore[reportInvalidTypeForm] + token: NfStringProperty(name="Token", default="") # pyright: ignore[reportInvalidTypeForm] + table_id: NfStringProperty(name="Table ID", default="") # pyright: ignore[reportInvalidTypeForm] + gcode_field: NfStringProperty(name="Gcode Field", default="GCODE") # pyright: ignore[reportInvalidTypeForm] + + def init(self, context): + self._init_exec() + self.add_input("SLICESOCK_Gcode") + self.add_input("SLICESOCK_String", "Filename") + + def draw_buttons(self, context, layout): + super().draw_buttons(context, layout) + layout.prop(self, "host") + layout.prop(self, "token") + layout.prop(self, "table_id") + layout.prop(self, "gcode_field") + + def evaluate(self, operator=None): + from ...infra.networking.baserow_backend import BaserowBackend + import tempfile + from pathlib import Path + + gcode: str = resolve_input(self.inputs.get("Gcode")) + if not gcode: + if operator: operator.report({'ERROR'}, "No Gcode to send") + return 'no gcode' + + if not self.token or not self.table_id: + if operator: operator.report({'ERROR'}, "Baserow configuration incomplete") + return 'incomplete config' + + db = BaserowBackend(self.host, self.token) + + with tempfile.NamedTemporaryFile(mode='w', suffix='.gcode', delete=False) as tf: + tf.write(gcode) + temp_path = Path(tf.name) + + try: + res = db.create_record(self.table_id, {}) + record_id = int(res['id']) + db.upload_attachment(self.table_id, record_id, self.gcode_field, temp_path) + return 'ok' + except Exception as e: + if operator: operator.report({'ERROR'}, f"Baserow Error: {str(e)}") + return 'error' + finally: + if temp_path.exists(): + temp_path.unlink() + @register_class class SLICENODE_PrusaslicerSlice(ExecSliceNode): bl_label = "PrusaSlicer Slice" diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index cd274e6..51a24a1 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -185,6 +185,7 @@ class SLICENODE_MT_Menu(Menu): layout.operator("node.add_node", text="Gcode to File").type = "SLICENODE_GcodeToFile" layout.operator("node.add_node", text="Gcode to Text Block").type = "SLICENODE_GcodeToTextBlock" layout.operator("node.add_node", text="Start Print").type = "SLICENODE_StartPrint" + layout.operator("node.add_node", text="Baserow Submit").type = "SLICENODE_BaserowSubmit" layout.operator("node.add_node", text="Gcode Preview").type = "SLICENODE_GcodeToMesh" layout.operator("node.add_node", text="PrusaSlicer Slice").type = "SLICENODE_PrusaslicerSlice" layout.operator("node.add_node", text="Executor").type = "SLICENODE_Executor" diff --git a/infra/networking/baserow_backend.py b/infra/networking/baserow_backend.py new file mode 100644 index 0000000..cfe1ef7 --- /dev/null +++ b/infra/networking/baserow_backend.py @@ -0,0 +1,87 @@ +from pathlib import Path +import requests # pyright: ignore[reportMissingModuleSource] +from typing import Any + +class BaserowBackend: + def __init__(self, host: str, token: str): + self.host = host.rstrip('/') + self.token = token + self.headers = { + "Authorization": f"Token {self.token}", + "Content-Type": "application/json" + } + + def fetch(self, table_id: str, filter_formula: str | None = None) -> list[dict]: + url = f"{self.host}/api/database/rows/table/{table_id}/" + params = {"user_field_names": "true"} + rows = [] + while url: + response = requests.get(url, headers=self.headers, params=params) + response.raise_for_status() + data = response.json() + rows.extend(data.get("results", [])) + url = data.get("next") + params = {} + + if filter_formula: + # Simple client-side filtering to mimic Airtable formulas + import re + match = re.search(r"^({)?(.*?)(})?\s*=\s*['\"]?(.*?)['\"]?$", filter_formula) + if match: + _, f, _, v = match.groups() + + filtered_rows = [] + for r in rows: + actual_val = r['id'] if f in ('RECORD_ID()', 'ID') else r.get(f) + + # Direct comparison + if str(actual_val) == str(v): + filtered_rows.append(r) + continue + + # Numeric comparison fallback + try: + if float(actual_val) == float(v): + filtered_rows.append(r) + except (ValueError, TypeError): + pass + rows = filtered_rows + return rows + + def create_record(self, table_id: str, fields: dict[str, Any]) -> dict: + url = f"{self.host}/api/database/rows/table/{table_id}/" + response = requests.post(url, headers=self.headers, json=fields, params={"user_field_names": "true"}) + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + print(f"Baserow Create Error: {response.status_code} - {response.text}") + raise e + return response.json() + + def update_record(self, table_id: str, record_id: int, fields: dict[str, Any]) -> dict: + url = f"{self.host}/api/database/rows/table/{table_id}/{record_id}/" + response = requests.patch(url, headers=self.headers, json=fields, params={"user_field_names": "true"}) + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + print(f"Baserow Update Error: {response.status_code} - {response.text}") + raise e + return response.json() + + def upload_attachment(self, table_id: str, record_id: int, field_id: str, filepath: str | Path) -> Any: + # 1. Upload the file to Baserow + with open(filepath, "rb") as f: + response = requests.post( + f"{self.host}/api/user-files/upload-file/", + headers={"Authorization": f"Token {self.token}"}, + files={'file': f} + ) + response.raise_for_status() + file_name = response.json()['name'] + + # 2. Link the uploaded file to the record + url = f"{self.host}/api/database/rows/table/{table_id}/{record_id}/" + payload = {field_id: [{"name": file_name}]} + response = requests.patch(url, headers=self.headers, json=payload, params={"user_field_names": "true"}) + response.raise_for_status() + return response.json() diff --git a/infra/networking/printer_backends.py b/infra/networking/printer_backends.py index 97878c4..fa795b5 100644 --- a/infra/networking/printer_backends.py +++ b/infra/networking/printer_backends.py @@ -15,7 +15,7 @@ def with_api_state(api_state: str) -> Callable[..., Any]: from dataclasses import dataclass, field from pathlib import Path from typing import Protocol, Any, Optional -import requests +import requests # pyright: ignore[reportMissingModuleSource] import time @dataclass(frozen=True)