diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index cd2b035698..0aa7f7113e 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1453,7 +1453,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return # --- tolerance check --- - tolerance = 1e-5 #to provide a little wiggle room + tolerance = 1e-5 # to provide a little wiggle room if layer2_bases and (max(layer2_bases) - min(layer2_bases)) > tolerance: min_base = min(layer2_bases) max_base = max(layer2_bases) diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py index 15d21be340..63906ba25d 100644 --- a/src/bonsai/bonsai/bim/module/tester/operator.py +++ b/src/bonsai/bonsai/bim/module/tester/operator.py @@ -55,26 +55,26 @@ class IfcTesterWebSocketServer: self.site = None self.loop = None self.shutdown_event = None - + # Register namespace self.sio.register_namespace(IfcTesterNamespace("/ifctester")) - + # Add health check route self.app.router.add_get("/health", self.health_check) - + async def health_check(self, request): return web.Response(text="OK", content_type="text/plain") - + async def start_server(self): self.loop = asyncio.get_event_loop() self.shutdown_event = asyncio.Event() - + self.runner = web.AppRunner(self.app) await self.runner.setup() self.site = web.TCPSite(self.runner, "127.0.0.1", self.port) await self.site.start() print(f"IfcTester WebSocket server started on 127.0.0.1:{self.port}") - + try: # Wait for shutdown signal await self.shutdown_event.wait() @@ -82,35 +82,35 @@ class IfcTesterWebSocketServer: print("WebSocket server received cancellation") finally: await self._cleanup() - + async def _cleanup(self): try: # Disconnect all clients print("Shutting down SocketIO...") - + try: await self.sio.shutdown() except Exception as e: print(f"Error shutting down socketio: {e}") - + # Stop the web server if self.site: print("Stopping web server...") await asyncio.wait_for(self.site.stop(), timeout=2.0) self.site = None - + # Clean up the runner if self.runner: print("Cleaning up runner...") await asyncio.wait_for(self.runner.cleanup(), timeout=2.0) self.runner = None - + print("IfcTester WebSocket server stopped") except asyncio.TimeoutError: print("Websocket server cleanup timed out, forcing shutdown") except Exception as e: print(f"Error during websocket cleanup: {e}") - + def stop_server(self): if self.loop and self.shutdown_event and not self.shutdown_event.is_set(): try: @@ -135,18 +135,13 @@ class IfcTesterNamespace(socketio.AsyncNamespace): try: request_id = data.get("id") ids_string = data.get("ids") - + if not request_id: - await self.emit("error", { - "error": "No request ID provided" - }, room=sid) + await self.emit("error", {"error": "No request ID provided"}, room=sid) return - + if not ids_string: - await self.emit("error", { - "id": request_id, - "error": "No IDS XML string provided" - }, room=sid) + await self.emit("error", {"id": request_id, "error": "No IDS XML string provided"}, room=sid) return print(f"Processing IDS audit request {request_id}") @@ -154,30 +149,23 @@ class IfcTesterNamespace(socketio.AsyncNamespace): # Check if IFC is loaded in Bonsai ifc = tool.Ifc.get() if not ifc: - await self.emit("error", { - "id": request_id, - "error": "No IFC model is currently loaded in Bonsai" - }, room=sid) + await self.emit( + "error", {"id": request_id, "error": "No IFC model is currently loaded in Bonsai"}, room=sid + ) return # Parse IDS from string try: ids = ifctester.ids.from_string(ids_string) except Exception as e: - await self.emit("error", { - "id": request_id, - "error": f"Failed to parse IDS XML: {str(e)}" - }, room=sid) + await self.emit("error", {"id": request_id, "error": f"Failed to parse IDS XML: {str(e)}"}, room=sid) return # Validate IFC against IDS try: ids.validate(ifc) except Exception as e: - await self.emit("error", { - "id": request_id, - "error": f"Validation failed: {str(e)}" - }, room=sid) + await self.emit("error", {"id": request_id, "error": f"Validation failed: {str(e)}"}, room=sid) return # Generate reports @@ -193,28 +181,21 @@ class IfcTesterNamespace(socketio.AsyncNamespace): html_report = html_reporter.to_string() # Send results back - await self.emit("audit_result", { - "id": request_id, - "json_report": json_report, - "html_report": html_report - }, room=sid) - + await self.emit( + "audit_result", {"id": request_id, "json_report": json_report, "html_report": html_report}, room=sid + ) + print(f"Successfully processed IDS audit request {request_id}") except Exception as e: - await self.emit("error", { - "id": request_id, - "error": f"Failed to generate reports: {str(e)}" - }, room=sid) + await self.emit("error", {"id": request_id, "error": f"Failed to generate reports: {str(e)}"}, room=sid) except Exception as e: print(f"Error processing audit request: {str(e)}") import traceback + print(traceback.format_exc()) - await self.emit("error", { - "id": request_id, - "error": f"Internal server error: {str(e)}" - }, room=sid) + await self.emit("error", {"id": request_id, "error": f"Internal server error: {str(e)}"}, room=sid) async def on_ping(self, sid, data): await self.emit("pong", {"timestamp": data.get("timestamp")}, room=sid) @@ -318,9 +299,9 @@ class StartIfcTesterWebapp(bpy.types.Operator): def execute(self, context): global webapp_process, websocket_server_thread, websocket_app - + props = tool.Tester.get_tester_props() - + if webapp_process is not None or websocket_server_thread is not None: self.report({"WARNING"}, "IfcTester webapp is already running") return {"CANCELLED"} @@ -328,19 +309,21 @@ class StartIfcTesterWebapp(bpy.types.Operator): try: import ifctester.webapp.serve except ImportError: - self.report({"ERROR"}, "IfcTester webapp not available. Please ensure the latest version of ifctester is installed.") + self.report( + {"ERROR"}, "IfcTester webapp not available. Please ensure the latest version of ifctester is installed." + ) return {"CANCELLED"} webapp_port = self.find_free_port() websocket_port = self.find_free_port() - + # Get the path to the serve.py module webapp_serve_path = ifctester.webapp.serve.__file__ - + try: # Start the websocket server in a thread websocket_app = IfcTesterWebSocketServer(websocket_port) - + def run_websocket_server(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) @@ -350,34 +333,35 @@ class StartIfcTesterWebapp(bpy.types.Operator): print(f"WebSocket server error: {e}") finally: loop.close() - + websocket_server_thread = threading.Thread(target=run_websocket_server, daemon=True) websocket_server_thread.start() - + # Start the Flask server as subprocess - webapp_process = subprocess.Popen([ - sys.executable, webapp_serve_path, - "--host", "127.0.0.1", - "--port", str(webapp_port) - ]) - + webapp_process = subprocess.Popen( + [sys.executable, webapp_serve_path, "--host", "127.0.0.1", "--port", str(webapp_port)] + ) + # Update properties props.webapp_server_port = webapp_port props.websocket_server_port = websocket_port props.webapp_is_running = True - + # Wait a moment for servers to start, then open browser def delayed_open_browser(): import time + time.sleep(1.5) webbrowser.open(f"http://127.0.0.1:{webapp_port}?bonsai_server={websocket_port}") - + browser_thread = threading.Thread(target=delayed_open_browser, daemon=True) browser_thread.start() - - self.report({"INFO"}, f"IfcTester webapp started at http://127.0.0.1:{webapp_port} (Websocket: {websocket_port})") + + self.report( + {"INFO"}, f"IfcTester webapp started at http://127.0.0.1:{webapp_port} (Websocket: {websocket_port})" + ) return {"FINISHED"} - + except Exception as e: # Clean up on error if webapp_process: @@ -387,13 +371,13 @@ class StartIfcTesterWebapp(bpy.types.Operator): # The websocket server will be cleaned up when the thread ends websocket_server_thread = None websocket_app = None - + self.report({"ERROR"}, f"Failed to start servers: {str(e)}") return {"CANCELLED"} def find_free_port(self): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('', 0)) + s.bind(("", 0)) s.listen(1) port = s.getsockname()[1] return port @@ -406,15 +390,15 @@ class StopIfcTesterWebapp(bpy.types.Operator): def execute(self, context): global webapp_process, websocket_server_thread, websocket_app - + props = tool.Tester.get_tester_props() - + if webapp_process is None and websocket_server_thread is None: self.report({"WARNING"}, "No IfcTester servers are running") return {"CANCELLED"} errors = [] - + # Stop webapp server if webapp_process: try: @@ -426,34 +410,34 @@ class StopIfcTesterWebapp(bpy.types.Operator): errors.append(f"Error stopping webapp server: {str(e)}") finally: webapp_process = None - + # Stop websocket server if websocket_app and websocket_server_thread: try: print("Stopping WebSocket server...") - + # Signal shutdown using thread-safe method websocket_app.stop_server() - + # Wait for the websocket thread to finish websocket_server_thread.join(timeout=5) - + except Exception as e: errors.append(f"Error during websocket shutdown: {str(e)}") finally: websocket_app = None websocket_server_thread = None - + # Update properties props.webapp_server_port = 0 props.websocket_server_port = 0 props.webapp_is_running = False - + if errors: self.report({"WARNING"}, f"IfcTester webapp and server stopped with errors: {'; '.join(errors)}") else: self.report({"INFO"}, "IfcTester webapp and server stopped") - + return {"FINISHED"} @@ -464,11 +448,11 @@ class OpenIfcTesterWebapp(bpy.types.Operator): def execute(self, context): props = tool.Tester.get_tester_props() - + if not props.webapp_is_running: self.report({"ERROR"}, "IfcTester webapp is not running. Please start it first.") return {"CANCELLED"} - + webbrowser.open(f"http://127.0.0.1:{props.webapp_server_port}?bonsai_server={props.websocket_server_port}") return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/tester/ui.py b/src/bonsai/bonsai/bim/module/tester/ui.py index cd7a5977e5..8defa5df9d 100644 --- a/src/bonsai/bonsai/bim/module/tester/ui.py +++ b/src/bonsai/bonsai/bim/module/tester/ui.py @@ -70,14 +70,14 @@ class BIM_PT_tester(Panel): row = self.layout.row() row.operator("bim.execute_ifc_tester") - + self.layout.separator() - + # IfcTester Webapp controls if props.webapp_is_running: row = self.layout.row() row.label(text=f"Webapp: {props.webapp_server_port} | Server: {props.websocket_server_port}") - + row = self.layout.row(align=True) row.operator("bim.stop_ifc_tester_webapp") row.operator("bim.open_ifc_tester_webapp", icon="URL", text="") diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index d3a07e800e..308c0b4e85 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -1059,7 +1059,7 @@ class BIM_OT_enum_property_search(bpy.types.Operator): # though it would fit perfectly. if self.dummy_name: self.layout.label(text=f"Current: {self.dummy_name}") - + self.layout.prop_search(self, "dummy_name", self, "collection_names", text=self.prop_name) def execute(self, context): diff --git a/src/ifctester/webapp/__init__.py b/src/ifctester/webapp/__init__.py index 02853f3dbc..ef963b43d7 100644 --- a/src/ifctester/webapp/__init__.py +++ b/src/ifctester/webapp/__init__.py @@ -1,3 +1,3 @@ from .serve import app -__all__ = ['app'] \ No newline at end of file +__all__ = ["app"] diff --git a/src/ifctester/webapp/public/worker/api.py b/src/ifctester/webapp/public/worker/api.py index c1eec3ee2c..20791f162e 100644 --- a/src/ifctester/webapp/public/worker/api.py +++ b/src/ifctester/webapp/public/worker/api.py @@ -9,6 +9,7 @@ from ifctester.ids import Ids, IdsXmlValidationError, get_schema # https://github.com/buildingSMART/IDS/blob/9914d568c7ac037acd97e58a0d16e9f93c3e3416/Schema/ids.xsd#L232 ifc_schemas = ["IFC2X3", "IFC4", "IFC4X3_ADD2"] + def get_predefined_types_for_entity(schema_name, entity_name): """Get a list of predefined types for a given entity.""" @@ -18,50 +19,57 @@ def get_predefined_types_for_entity(schema_name, entity_name): entity = schema.declaration_by_name(entity_name) except: return [] - + if not entity or not entity.as_entity(): print(f"Entity {entity_name} not found") return [] - + entity = entity.as_entity() predefined_type_attr = None - + # Check all attributes for "PredefinedType" for attr in entity.all_attributes(): if attr.name() == "PredefinedType": predefined_type_attr = attr break - + if not predefined_type_attr: return [] - + param_type = predefined_type_attr.type_of_attribute() - + if param_type.as_named_type(): enum_decl = param_type.as_named_type().declared_type() if enum_decl.as_enumeration_type(): return enum_decl.as_enumeration_type().enumeration_items() - + return [] + def get_all_entity_classes(schema_name): """Get all IFC entity classes in the given schema.""" - + schema = ifcopenshell.schema_by_name(schema_name) entities = [] - + for entity in schema.entities(): entities.append(entity.name()) - + # Sort alphabetically entities.sort() return entities + def get_all_data_types(schema_name): """Get all data types in the given schema.""" schema = ifcopenshell.schema_by_name(schema_name) - return {d.name(): ifcopenshell.util.attribute.get_primitive_type(d) for d in schema.declarations() if d.as_type_declaration()} + return { + d.name(): ifcopenshell.util.attribute.get_primitive_type(d) + for d in schema.declarations() + if d.as_type_declaration() + } + def get_entity_attributes(schema_name, entity_name): """Get all attributes for a given entity.""" @@ -72,40 +80,44 @@ def get_entity_attributes(schema_name, entity_name): entity = schema.declaration_by_name(entity_name) except: return [] - + if not entity or not entity.as_entity(): print(f"Entity {entity_name} not found") return [] - + entity = entity.as_entity() attributes = [] for attr in entity.all_attributes(): - attributes.append({ - "name": attr.name(), - # "type": attr.type_of_attribute() # TODO Types of attribute - }) - + attributes.append( + { + "name": attr.name(), + # "type": attr.type_of_attribute() # TODO Types of attribute + } + ) + return attributes -def get_applicable_psets(schema_name, entity_name, predefined_type = ""): + +def get_applicable_psets(schema_name, entity_name, predefined_type=""): """Get all applicable property and quantity sets for a given entity.""" pset_qto = ifcopenshell.util.pset.PsetQto(schema_name) pset_names = pset_qto.get_applicable_names(entity_name, predefined_type) - + return pset_names + def get_all_psets(schema_name): """Get all property sets and quantity sets defined in an IFC schema""" pset_qto = ifcopenshell.util.pset.PsetQto(schema_name) result = {} - + for template_file in pset_qto.templates: for pset_template in template_file.by_type("IfcPropertySetTemplate"): pset_name = pset_template.Name properties = [] - + # Get property templates for this pset if pset_template.HasPropertyTemplates: for prop_template in pset_template.HasPropertyTemplates: @@ -113,7 +125,7 @@ def get_all_psets(schema_name): "name": prop_template.Name, # "description": prop_template.Description } - + # Extract type information if prop_template.is_a("IfcSimplePropertyTemplate"): if prop_template.PrimaryMeasureType: @@ -121,61 +133,62 @@ def get_all_psets(schema_name): else: prop_info["type"] = str(prop_template.TemplateType) elif prop_template.is_a("IfcComplexPropertyTemplate"): - prop_info["type"] = None # Complex properties are not supported + prop_info["type"] = None # Complex properties are not supported else: prop_info["type"] = None - + properties.append(prop_info) result[pset_name] = properties - + return result + def get_material_categories(): - return ['concrete', 'steel', 'aluminium', 'block', 'brick', 'stone', 'wood', 'glass', 'gypsum', 'plastic', 'earth'] + return ["concrete", "steel", "aluminium", "block", "brick", "stone", "wood", "glass", "gypsum", "plastic", "earth"] + def get_standard_classification_systems(): return { - 'BB/SfB (3/4 cijfers)': {'source': 'Regie der Gebouwen', 'tokens': ['.']}, - 'BIMTypeCode': {'source': 'BIMStockholm', 'tokens': None}, - 'Common Arrangement of Work Sections (CAWS)': {'source': 'NBS', 'tokens': ['/']}, - 'CBI Classification - Level 2': {'source': 'Masterspec', 'tokens': None}, - 'CBI Classification - Level 4': {'source': 'Masterspec', 'tokens': None}, - 'Rumsfunktionskoder CC001 - 001': {'source': 'BIMAlliance', 'tokens': ['-']}, - 'CCS': {'source': 'Molio', 'tokens': None}, - 'CCTB': {'source': 'CCT-Bâtiments', 'tokens': ['.']}, - 'Funktionskoder Regionservice CD001 - 001': {'source': 'BIMAlliance', 'tokens': None}, - 'Rumsfunktion Blekinge CD002 - 001': {'source': 'BIMAlliance', 'tokens': None}, - 'EcoQuaestor Codetabel': {'source': 'EcoQuaestor', 'tokens': ['.', '-']}, - 'GuBIMclass CA': {'source': 'GuBIMClass', 'tokens': ['.']}, - 'GuBIMclass ES': {'source': 'GuBIMClass', 'tokens': ['.']}, - 'MasterFormat': {'source': 'CSI', 'tokens': [' ', '.']}, - 'NATSPEC Worksections': {'source': 'NATSPEC', 'tokens': None}, - 'NBS Create': {'source': 'NBS', 'tokens': ['_', '/']}, - 'NL/SfB (4 cijfers)': {'source': 'BIMLoket', 'tokens': ['.']}, - 'NS 3451 - Bygningsdelstabell': {'source': 'Standard Norge', 'tokens': None}, - 'OmniClass': {'source': 'OmniClass', 'tokens': ['-', ' ']}, - 'ÖNORM 6241-2': {'source': 'freeBIM 2', 'tokens': None}, - 'RICS NRM1': {'source': 'RICS', 'tokens': ['.']}, - 'RICS NRM3': {'source': 'RICS', 'tokens': ['.']}, - 'SFG20': {'source': 'SFG20', 'tokens': ['-']}, - 'SINAPI': {'source': 'Caixa', 'tokens': ['/']}, - 'STABU-Element': {'source': 'STABU', 'tokens': ['.']}, - 'TALO 2000 Building Component Classification': {'source': 'Rakennustieto', 'tokens': ['.']}, - 'TALO 2000 Hankenimikkeistö': {'source': 'Rakennustieto', 'tokens': ['.']}, - 'Uniclass': {'source': 'RIBA Enterprises Ltd', 'tokens': ['_']}, - 'Uniclass 2015': {'source': 'RIBA Enterprises Ltd', 'tokens': ['_']}, - 'UniFormat': {'source': 'UniFormat', 'tokens': ['.']}, - 'Uniformat': {'source': 'UniFormat', 'tokens': ['.']}, - 'VMSW': {'source': 'VMSW', 'tokens': ['.']} + "BB/SfB (3/4 cijfers)": {"source": "Regie der Gebouwen", "tokens": ["."]}, + "BIMTypeCode": {"source": "BIMStockholm", "tokens": None}, + "Common Arrangement of Work Sections (CAWS)": {"source": "NBS", "tokens": ["/"]}, + "CBI Classification - Level 2": {"source": "Masterspec", "tokens": None}, + "CBI Classification - Level 4": {"source": "Masterspec", "tokens": None}, + "Rumsfunktionskoder CC001 - 001": {"source": "BIMAlliance", "tokens": ["-"]}, + "CCS": {"source": "Molio", "tokens": None}, + "CCTB": {"source": "CCT-Bâtiments", "tokens": ["."]}, + "Funktionskoder Regionservice CD001 - 001": {"source": "BIMAlliance", "tokens": None}, + "Rumsfunktion Blekinge CD002 - 001": {"source": "BIMAlliance", "tokens": None}, + "EcoQuaestor Codetabel": {"source": "EcoQuaestor", "tokens": [".", "-"]}, + "GuBIMclass CA": {"source": "GuBIMClass", "tokens": ["."]}, + "GuBIMclass ES": {"source": "GuBIMClass", "tokens": ["."]}, + "MasterFormat": {"source": "CSI", "tokens": [" ", "."]}, + "NATSPEC Worksections": {"source": "NATSPEC", "tokens": None}, + "NBS Create": {"source": "NBS", "tokens": ["_", "/"]}, + "NL/SfB (4 cijfers)": {"source": "BIMLoket", "tokens": ["."]}, + "NS 3451 - Bygningsdelstabell": {"source": "Standard Norge", "tokens": None}, + "OmniClass": {"source": "OmniClass", "tokens": ["-", " "]}, + "ÖNORM 6241-2": {"source": "freeBIM 2", "tokens": None}, + "RICS NRM1": {"source": "RICS", "tokens": ["."]}, + "RICS NRM3": {"source": "RICS", "tokens": ["."]}, + "SFG20": {"source": "SFG20", "tokens": ["-"]}, + "SINAPI": {"source": "Caixa", "tokens": ["/"]}, + "STABU-Element": {"source": "STABU", "tokens": ["."]}, + "TALO 2000 Building Component Classification": {"source": "Rakennustieto", "tokens": ["."]}, + "TALO 2000 Hankenimikkeistö": {"source": "Rakennustieto", "tokens": ["."]}, + "Uniclass": {"source": "RIBA Enterprises Ltd", "tokens": ["_"]}, + "Uniclass 2015": {"source": "RIBA Enterprises Ltd", "tokens": ["_"]}, + "UniFormat": {"source": "UniFormat", "tokens": ["."]}, + "Uniformat": {"source": "UniFormat", "tokens": ["."]}, + "VMSW": {"source": "VMSW", "tokens": ["."]}, } + def ids_from_xml_string(xml: str, validate: bool = False) -> Ids: try: decode = get_schema().decode( - xml, strip_namespaces=True, namespaces={ - "": "http://standards.buildingsmart.org/IDS" - } + xml, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"} ) except XMLSchemaValidationError as e: raise IdsXmlValidationError(e, "Provided XML appears to be invalid. See details above.") - return Ids().parse(decode) \ No newline at end of file + return Ids().parse(decode) diff --git a/src/ifctester/webapp/serve.py b/src/ifctester/webapp/serve.py index c7356e5378..87e92930f3 100644 --- a/src/ifctester/webapp/serve.py +++ b/src/ifctester/webapp/serve.py @@ -22,11 +22,12 @@ from flask import Flask, send_from_directory, send_file app = Flask(__name__) + def get_static_folder(): base_dir = os.path.dirname(__file__) - dist_dir = os.path.join(base_dir, 'dist') - www_dir = os.path.join(base_dir, 'www') - + dist_dir = os.path.join(base_dir, "dist") + www_dir = os.path.join(base_dir, "www") + if os.path.exists(dist_dir) and os.path.isdir(dist_dir): return dist_dir elif os.path.exists(www_dir) and os.path.isdir(www_dir): @@ -34,36 +35,42 @@ def get_static_folder(): else: return dist_dir + STATIC_FOLDER = get_static_folder() -@app.route('/') -def index(): - return send_file(os.path.join(STATIC_FOLDER, 'index.html')) -@app.route('/') +@app.route("/") +def index(): + return send_file(os.path.join(STATIC_FOLDER, "index.html")) + + +@app.route("/") def static_files(filename): return send_from_directory(STATIC_FOLDER, filename) -@app.route('/assets/') + +@app.route("/assets/") def assets(filename): - return send_from_directory(os.path.join(STATIC_FOLDER, 'assets'), filename) + return send_from_directory(os.path.join(STATIC_FOLDER, "assets"), filename) + @app.errorhandler(404) def not_found(error): - return send_file(os.path.join(STATIC_FOLDER, 'index.html')) + return send_file(os.path.join(STATIC_FOLDER, "index.html")) + if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Start IfcTester webapp') - parser.add_argument('--host', default='127.0.0.1', help='Host to bind to (default: 127.0.0.1)') - parser.add_argument('--port', type=int, default=5000, help='Port to bind to (default: 5000)') - parser.add_argument('--debug', action='store_true', help='Enable debug mode') - parser.add_argument('--dist-dir', default=STATIC_FOLDER, help='Directory containing built files') - + parser = argparse.ArgumentParser(description="Start IfcTester webapp") + parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)") + parser.add_argument("--port", type=int, default=5000, help="Port to bind to (default: 5000)") + parser.add_argument("--debug", action="store_true", help="Enable debug mode") + parser.add_argument("--dist-dir", default=STATIC_FOLDER, help="Directory containing built files") + args = parser.parse_args() - + STATIC_FOLDER = args.dist_dir - + print(f"Serving IfcTester webapp from: {STATIC_FOLDER}") print(f"Server running at: http://{args.host}:{args.port}") - - app.run(host=args.host, port=args.port, debug=args.debug) \ No newline at end of file + + app.run(host=args.host, port=args.port, debug=args.debug)