Py_ext: add CSV write by String FB + refactoring
authorEdouard Tisserant <edouard@beremiz.fr>
Fri, 07 Feb 2025 11:21:15 +0100 (5 weeks ago)
changeset 4105 79aa1772f491
parent 4104 9e59bb5ad9e1
child 4106 6a82b8b7564b
Py_ext: add CSV write by String FB + refactoring

- CSV_WRITE_BY_STR can create file from scratch
- Moved python runtime code from a string to a separate file.
py_ext/pous.xml
py_ext/py_ext.py
py_ext/py_ext_rt.py
--- a/py_ext/pous.xml	Fri Feb 07 10:52:09 2025 +0100
+++ b/py_ext/pous.xml	Fri Feb 07 11:21:15 2025 +0100
@@ -1,7 +1,7 @@
 <?xml version='1.0' encoding='utf-8'?>
 <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.plcopen.org/xml/tc6_0201" xmlns:xhtml="http://www.w3.org/1999/xhtml" xsi:schemaLocation="http://www.plcopen.org/xml/tc6_0201">
   <fileHeader companyName="Beremiz" productName="Beremiz" productVersion="0.0" creationDateTime="2008-12-14T16:53:26"/>
-  <contentHeader name="Beremiz non-standard POUs library" modificationDateTime="2024-12-06T15:13:47">
+  <contentHeader name="Beremiz non-standard POUs library" modificationDateTime="2025-02-03T14:57:54">
     <coordinateInfo>
       <fbd>
         <scaling x="8" y="8"/>
@@ -17,6 +17,512 @@
   <types>
     <dataTypes/>
     <pous>
+      <pou name="csv_write_by_string" pouType="functionBlock">
+        <interface>
+          <outputVars>
+            <variable name="ACK">
+              <type>
+                <BOOL/>
+              </type>
+            </variable>
+            <variable name="ERROR">
+              <type>
+                <BOOL/>
+              </type>
+            </variable>
+            <variable name="RESULT">
+              <type>
+                <string/>
+              </type>
+            </variable>
+          </outputVars>
+          <inputVars>
+            <variable name="FILE_NAME">
+              <type>
+                <string/>
+              </type>
+            </variable>
+            <variable name="ROW">
+              <type>
+                <string/>
+              </type>
+            </variable>
+            <variable name="COLUMN">
+              <type>
+                <string/>
+              </type>
+            </variable>
+            <variable name="CONTENT">
+              <type>
+                <string/>
+              </type>
+            </variable>
+            <variable name="SAVE">
+              <type>
+                <BOOL/>
+              </type>
+            </variable>
+          </inputVars>
+          <localVars>
+            <variable name="py_eval0">
+              <type>
+                <derived name="python_eval"/>
+              </type>
+            </variable>
+            <variable name="R_TRIG1">
+              <type>
+                <derived name="R_TRIG"/>
+              </type>
+            </variable>
+            <variable name="R_TRIG2">
+              <type>
+                <derived name="R_TRIG"/>
+              </type>
+            </variable>
+            <variable name="csv_refresh0">
+              <type>
+                <derived name="csv_refresh"/>
+              </type>
+            </variable>
+          </localVars>
+        </interface>
+        <body>
+          <FBD>
+            <inVariable localId="8" executionOrderId="0" height="27" width="112" negated="false">
+              <position x="384" y="128"/>
+              <connectionPointOut>
+                <relPosition x="112" y="16"/>
+              </connectionPointOut>
+              <expression>'CSVWrStr("'</expression>
+            </inVariable>
+            <inVariable localId="52" executionOrderId="0" height="32" width="112" negated="false">
+              <position x="216" y="296"/>
+              <connectionPointOut>
+                <relPosition x="112" y="16"/>
+              </connectionPointOut>
+              <expression>CONTENT</expression>
+            </inVariable>
+            <comment localId="29" height="40" width="232">
+              <position x="64" y="32"/>
+              <content>
+                <xhtml:p><![CDATA[Generate python code line]]></xhtml:p>
+              </content>
+            </comment>
+            <block localId="40" width="104" height="80" typeName="python_eval" instanceName="py_eval0" executionOrderId="0">
+              <position x="552" y="480"/>
+              <inputVariables>
+                <variable formalParameter="TRIG">
+                  <connectionPointIn>
+                    <relPosition x="0" y="32"/>
+                    <connection refLocalId="46" formalParameter="Q">
+                      <position x="552" y="512"/>
+                      <position x="360" y="512"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="CODE">
+                  <connectionPointIn>
+                    <relPosition x="0" y="64"/>
+                    <connection refLocalId="41">
+                      <position x="552" y="544"/>
+                      <position x="520" y="544"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="ACK">
+                  <connectionPointOut>
+                    <relPosition x="104" y="32"/>
+                  </connectionPointOut>
+                </variable>
+                <variable formalParameter="RESULT">
+                  <connectionPointOut>
+                    <relPosition x="104" y="64"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <continuation name="Code" localId="41" height="24" width="128">
+              <position x="392" y="528"/>
+              <connectionPointOut>
+                <relPosition x="128" y="16"/>
+              </connectionPointOut>
+            </continuation>
+            <inVariable localId="42" height="24" width="64" executionOrderId="0" negated="false">
+              <position x="208" y="496"/>
+              <connectionPointOut>
+                <relPosition x="64" y="16"/>
+              </connectionPointOut>
+              <expression>SAVE</expression>
+            </inVariable>
+            <outVariable localId="43" height="32" width="40" executionOrderId="0" negated="false">
+              <position x="736" y="400"/>
+              <connectionPointIn>
+                <relPosition x="0" y="16"/>
+                <connection refLocalId="40" formalParameter="ACK">
+                  <position x="736" y="416"/>
+                  <position x="688" y="416"/>
+                  <position x="688" y="512"/>
+                  <position x="656" y="512"/>
+                </connection>
+              </connectionPointIn>
+              <expression>ACK</expression>
+            </outVariable>
+            <outVariable localId="44" height="24" width="64" executionOrderId="0" negated="false">
+              <position x="688" y="584"/>
+              <connectionPointIn>
+                <relPosition x="0" y="8"/>
+                <connection refLocalId="40" formalParameter="RESULT">
+                  <position x="688" y="592"/>
+                  <position x="672" y="592"/>
+                  <position x="672" y="544"/>
+                  <position x="656" y="544"/>
+                </connection>
+              </connectionPointIn>
+              <expression>RESULT</expression>
+            </outVariable>
+            <block localId="46" typeName="R_TRIG" instanceName="R_TRIG1" executionOrderId="0" height="48" width="64">
+              <position x="296" y="480"/>
+              <inputVariables>
+                <variable formalParameter="CLK">
+                  <connectionPointIn>
+                    <relPosition x="0" y="32"/>
+                    <connection refLocalId="42">
+                      <position x="296" y="512"/>
+                      <position x="272" y="512"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="Q">
+                  <connectionPointOut>
+                    <relPosition x="64" y="32"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <block localId="33" typeName="LEFT" executionOrderId="0" height="64" width="56">
+              <position x="736" y="512"/>
+              <inputVariables>
+                <variable formalParameter="IN">
+                  <connectionPointIn>
+                    <relPosition x="0" y="32"/>
+                    <connection refLocalId="40" formalParameter="RESULT">
+                      <position x="736" y="544"/>
+                      <position x="656" y="544"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="L">
+                  <connectionPointIn>
+                    <relPosition x="0" y="56"/>
+                    <connection refLocalId="35">
+                      <position x="736" y="568"/>
+                      <position x="724" y="568"/>
+                      <position x="724" y="560"/>
+                      <position x="712" y="560"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="OUT">
+                  <connectionPointOut>
+                    <relPosition x="56" y="32"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <block localId="34" typeName="EQ" executionOrderId="0" height="72" width="64">
+              <position x="880" y="512"/>
+              <inputVariables>
+                <variable formalParameter="IN1">
+                  <connectionPointIn>
+                    <relPosition x="0" y="32"/>
+                    <connection refLocalId="33" formalParameter="OUT">
+                      <position x="880" y="544"/>
+                      <position x="792" y="544"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN2">
+                  <connectionPointIn>
+                    <relPosition x="0" y="56"/>
+                    <connection refLocalId="36">
+                      <position x="880" y="568"/>
+                      <position x="848" y="568"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="OUT">
+                  <connectionPointOut>
+                    <relPosition x="64" y="32"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <inVariable localId="35" executionOrderId="0" height="24" width="24" negated="false">
+              <position x="688" y="552"/>
+              <connectionPointOut>
+                <relPosition x="24" y="8"/>
+              </connectionPointOut>
+              <expression>1</expression>
+            </inVariable>
+            <inVariable localId="36" executionOrderId="0" height="32" width="40" negated="false">
+              <position x="808" y="552"/>
+              <connectionPointOut>
+                <relPosition x="40" y="16"/>
+              </connectionPointOut>
+              <expression>'#'</expression>
+            </inVariable>
+            <block localId="37" typeName="R_TRIG" instanceName="R_TRIG2" executionOrderId="0" height="48" width="64">
+              <position x="736" y="456"/>
+              <inputVariables>
+                <variable formalParameter="CLK">
+                  <connectionPointIn>
+                    <relPosition x="0" y="32"/>
+                    <connection refLocalId="40" formalParameter="ACK">
+                      <position x="736" y="488"/>
+                      <position x="688" y="488"/>
+                      <position x="688" y="512"/>
+                      <position x="656" y="512"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="Q">
+                  <connectionPointOut>
+                    <relPosition x="64" y="32"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <block localId="39" typeName="AND" executionOrderId="0" height="72" width="64">
+              <position x="984" y="456"/>
+              <inputVariables>
+                <variable formalParameter="IN1">
+                  <connectionPointIn>
+                    <relPosition x="0" y="32"/>
+                    <connection refLocalId="37" formalParameter="Q">
+                      <position x="984" y="488"/>
+                      <position x="800" y="488"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN2" negated="true">
+                  <connectionPointIn>
+                    <relPosition x="0" y="56"/>
+                    <connection refLocalId="34" formalParameter="OUT">
+                      <position x="984" y="512"/>
+                      <position x="974" y="512"/>
+                      <position x="974" y="544"/>
+                      <position x="944" y="544"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="OUT">
+                  <connectionPointOut>
+                    <relPosition x="64" y="32"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <block localId="53" typeName="csv_refresh" instanceName="csv_refresh0" executionOrderId="0" width="104" height="48">
+              <position x="1112" y="456"/>
+              <inputVariables>
+                <variable formalParameter="TRIG">
+                  <connectionPointIn>
+                    <relPosition x="0" y="32"/>
+                    <connection refLocalId="39" formalParameter="OUT">
+                      <position x="1112" y="488"/>
+                      <position x="1048" y="488"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables/>
+            </block>
+            <outVariable localId="54" executionOrderId="0" width="56" height="32" negated="false">
+              <position x="1096" y="528"/>
+              <connectionPointIn>
+                <relPosition x="0" y="16"/>
+                <connection refLocalId="34" formalParameter="OUT">
+                  <position x="1096" y="544"/>
+                  <position x="944" y="544"/>
+                </connection>
+              </connectionPointIn>
+              <expression>ERROR</expression>
+            </outVariable>
+            <block localId="7" typeName="CONCAT" executionOrderId="0" height="240" width="67">
+              <position x="536" y="112"/>
+              <inputVariables>
+                <variable formalParameter="IN1">
+                  <connectionPointIn>
+                    <relPosition x="0" y="32"/>
+                    <connection refLocalId="8">
+                      <position x="536" y="144"/>
+                      <position x="496" y="144"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN2">
+                  <connectionPointIn>
+                    <relPosition x="0" y="56"/>
+                    <connection refLocalId="2">
+                      <position x="536" y="168"/>
+                      <position x="328" y="168"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN3">
+                  <connectionPointIn>
+                    <relPosition x="0" y="80"/>
+                    <connection refLocalId="10">
+                      <position x="536" y="192"/>
+                      <position x="496" y="192"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN4">
+                  <connectionPointIn>
+                    <relPosition x="0" y="104"/>
+                    <connection refLocalId="3">
+                      <position x="536" y="216"/>
+                      <position x="328" y="216"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN5">
+                  <connectionPointIn>
+                    <relPosition x="0" y="128"/>
+                    <connection refLocalId="12">
+                      <position x="536" y="240"/>
+                      <position x="496" y="240"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN6">
+                  <connectionPointIn>
+                    <relPosition x="0" y="152"/>
+                    <connection refLocalId="4">
+                      <position x="536" y="264"/>
+                      <position x="328" y="264"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN7">
+                  <connectionPointIn>
+                    <relPosition x="0" y="176"/>
+                    <connection refLocalId="1">
+                      <position x="536" y="288"/>
+                      <position x="496" y="288"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN8">
+                  <connectionPointIn>
+                    <relPosition x="0" y="200"/>
+                    <connection refLocalId="52">
+                      <position x="536" y="312"/>
+                      <position x="328" y="312"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+                <variable formalParameter="IN9">
+                  <connectionPointIn>
+                    <relPosition x="0" y="224"/>
+                    <connection refLocalId="14">
+                      <position x="536" y="336"/>
+                      <position x="496" y="336"/>
+                    </connection>
+                  </connectionPointIn>
+                </variable>
+              </inputVariables>
+              <inOutVariables/>
+              <outputVariables>
+                <variable formalParameter="OUT">
+                  <connectionPointOut>
+                    <relPosition x="67" y="32"/>
+                  </connectionPointOut>
+                </variable>
+              </outputVariables>
+            </block>
+            <inVariable localId="2" executionOrderId="0" height="32" width="112" negated="false">
+              <position x="216" y="152"/>
+              <connectionPointOut>
+                <relPosition x="112" y="16"/>
+              </connectionPointOut>
+              <expression>FILE_NAME</expression>
+            </inVariable>
+            <inVariable localId="10" executionOrderId="0" height="24" width="112" negated="false">
+              <position x="384" y="184"/>
+              <connectionPointOut>
+                <relPosition x="112" y="8"/>
+              </connectionPointOut>
+              <expression>'","'</expression>
+            </inVariable>
+            <inVariable localId="3" executionOrderId="0" height="32" width="112" negated="false">
+              <position x="216" y="200"/>
+              <connectionPointOut>
+                <relPosition x="112" y="16"/>
+              </connectionPointOut>
+              <expression>ROW</expression>
+            </inVariable>
+            <inVariable localId="12" executionOrderId="0" height="24" width="112" negated="false">
+              <position x="384" y="224"/>
+              <connectionPointOut>
+                <relPosition x="112" y="16"/>
+              </connectionPointOut>
+              <expression>'","'</expression>
+            </inVariable>
+            <inVariable localId="4" executionOrderId="0" height="32" width="112" negated="false">
+              <position x="216" y="248"/>
+              <connectionPointOut>
+                <relPosition x="112" y="16"/>
+              </connectionPointOut>
+              <expression>COLUMN</expression>
+            </inVariable>
+            <inVariable localId="14" executionOrderId="0" height="24" width="112" negated="false">
+              <position x="384" y="320"/>
+              <connectionPointOut>
+                <relPosition x="112" y="16"/>
+              </connectionPointOut>
+              <expression>'")'</expression>
+            </inVariable>
+            <connector name="Code" localId="19" height="24" width="128">
+              <position x="656" y="136"/>
+              <connectionPointIn>
+                <relPosition x="0" y="8"/>
+                <connection refLocalId="7" formalParameter="OUT">
+                  <position x="656" y="144"/>
+                  <position x="603" y="144"/>
+                </connection>
+              </connectionPointIn>
+            </connector>
+            <inVariable localId="1" executionOrderId="0" height="24" width="112" negated="false">
+              <position x="384" y="272"/>
+              <connectionPointOut>
+                <relPosition x="112" y="16"/>
+              </connectionPointOut>
+              <expression>'","'</expression>
+            </inVariable>
+          </FBD>
+        </body>
+      </pou>
       <pou name="_csv_update" pouType="functionBlock">
         <interface>
           <externalVars>
@@ -1058,7 +1564,7 @@
               <connectionPointOut>
                 <relPosition x="176" y="16"/>
               </connectionPointOut>
-              <expression>'pyext_csv_reload()'</expression>
+              <expression>'CSVReload()'</expression>
             </inVariable>
             <block localId="15" typeName="csv_refresh" instanceName="csv_refresh0" executionOrderId="0" width="104" height="64">
               <position x="568" y="32"/>
@@ -1930,9 +2436,9 @@
               </outputVariables>
             </block>
             <inVariable localId="8" executionOrderId="0" height="24" width="160" negated="false">
-              <position x="352" y="112"/>
-              <connectionPointOut>
-                <relPosition x="160" y="8"/>
+              <position x="352" y="104"/>
+              <connectionPointOut>
+                <relPosition x="160" y="16"/>
               </connectionPointOut>
               <expression>'CSVWrInt("'</expression>
             </inVariable>
@@ -1944,9 +2450,9 @@
               <expression>FILE_NAME</expression>
             </inVariable>
             <inVariable localId="10" executionOrderId="0" height="24" width="112" negated="false">
-              <position x="504" y="152"/>
-              <connectionPointOut>
-                <relPosition x="112" y="16"/>
+              <position x="504" y="160"/>
+              <connectionPointOut>
+                <relPosition x="112" y="8"/>
               </connectionPointOut>
               <expression>'",'</expression>
             </inVariable>
@@ -1958,9 +2464,9 @@
               <expression>ROW</expression>
             </inVariable>
             <inVariable localId="12" executionOrderId="0" height="24" width="112" negated="false">
-              <position x="504" y="208"/>
-              <connectionPointOut>
-                <relPosition x="112" y="8"/>
+              <position x="504" y="200"/>
+              <connectionPointOut>
+                <relPosition x="112" y="16"/>
               </connectionPointOut>
               <expression>','</expression>
             </inVariable>
@@ -1972,9 +2478,9 @@
               <expression>COLUMN</expression>
             </inVariable>
             <inVariable localId="51" executionOrderId="0" height="24" width="112" negated="false">
-              <position x="504" y="256"/>
-              <connectionPointOut>
-                <relPosition x="112" y="8"/>
+              <position x="504" y="248"/>
+              <connectionPointOut>
+                <relPosition x="112" y="16"/>
               </connectionPointOut>
               <expression>',"'</expression>
             </inVariable>
@@ -1986,18 +2492,12 @@
               <expression>CONTENT</expression>
             </inVariable>
             <inVariable localId="14" executionOrderId="0" height="24" width="112" negated="false">
-              <position x="504" y="304"/>
+              <position x="504" y="296"/>
               <connectionPointOut>
                 <relPosition x="112" y="16"/>
               </connectionPointOut>
               <expression>'")'</expression>
             </inVariable>
-            <comment localId="28" height="48" width="520">
-              <position x="48" y="400"/>
-              <content>
-                <xhtml:p><![CDATA[Execute python code on change or globally when CSV is updated]]></xhtml:p>
-              </content>
-            </comment>
             <comment localId="29" height="40" width="232">
               <position x="64" y="32"/>
               <content>
@@ -2005,9 +2505,9 @@
               </content>
             </comment>
             <connector name="Code" localId="30" height="24" width="128">
-              <position x="856" y="112"/>
+              <position x="856" y="104"/>
               <connectionPointIn>
-                <relPosition x="0" y="8"/>
+                <relPosition x="0" y="16"/>
                 <connection refLocalId="7" formalParameter="OUT">
                   <position x="856" y="120"/>
                   <position x="752" y="120"/>
--- a/py_ext/py_ext.py	Fri Feb 07 10:52:09 2025 +0100
+++ b/py_ext/py_ext.py	Fri Feb 07 11:21:15 2025 +0100
@@ -1,186 +1,21 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
-# This file is part of Beremiz, a Integrated Development Environment for
-# programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
+# This file is part of Beremiz IDE
 #
-# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
+# Copyright (C) 2013: Laurent BESSARD
 # Copyright (C) 2017: Andrey Skvortsov
+# Copyright (C) 2025: Edouard TISSERANT
 #
 # See COPYING file for copyrights details.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-
 
 import os
 from POULibrary import POULibrary
 from py_ext.PythonFileCTNMixin import PythonFileCTNMixin
 import util.paths as paths
 
-pyext_python_lib_code = """
 
-import csv
-from collections import OrderedDict
-
-csv_int_files = {}
-def CSVRdInt(fname, rowidx, colidx):
-    \"\"\"
-    Return value at row/column pointed by integer indexes
-    Assumes data starts at first row and first column, no headers.
-    \"\"\"
-    global csv_int_files
-    data = csv_int_files.get(fname, None)
-    if data is None:
-        data = list()
-        try:
-            csvfile = open(fname, 'rt', encoding='utf-8')
-        except IOError:
-            return "#FILE_NOT_FOUND"
-        try:
-            dialect = csv.Sniffer().sniff(csvfile.read(1024))
-            csvfile.seek(0)
-            reader = csv.reader(csvfile, dialect)
-            for row in reader:
-                data.append(row)
-        except csv.Error as e:
-            return "#CSV_ERROR"
-        finally:
-            csvfile.close()
-        csv_int_files[fname] = data
-
-    try:
-        row = data[rowidx]
-        if not row and rowidx == len(data)-1:
-            raise IndexError
-    except IndexError:
-        return "#ROW_NOT_FOUND"
-
-    try:
-        return row[colidx]
-    except IndexError:
-        return "#COL_NOT_FOUND"
-
-
-csv_str_files = {}
-def CSVRdStr(fname, rowname, colname):
-    \"\"\"
-    Return value at row/column pointed by a pair of names as string
-    Assumes first row is column headers and first column is row name.
-    \"\"\"
-    global csv_str_files
-    entry = csv_str_files.get(fname, None)
-    if entry is None:
-        data = dict()
-        try:
-            csvfile = open(fname, 'rt', encoding='utf-8')
-        except IOError:
-            return "#FILE_NOT_FOUND"
-        try:
-            dialect = csv.Sniffer().sniff(csvfile.read(1024))
-            csvfile.seek(0)
-            reader = csv.reader(csvfile, dialect)
-            headers = dict([(name, index) for index, name in enumerate(reader.__next__()[1:])])
-            for row in reader:
-                data[row[0]] = row[1:]
-        except csv.Error:
-            return "#CSV_ERROR"
-        finally:
-            csvfile.close()
-        csv_str_files[fname] = (headers, data)
-    else:
-        headers, data = entry
-
-    try:
-        row = data[rowname]
-    except KeyError:
-        return "#ROW_NOT_FOUND"
-
-    try:
-        colidx = headers[colname]
-    except KeyError:
-        return "#COL_NOT_FOUND"
-
-    try:
-        return row[colidx]
-    except IndexError:
-        return "#COL_NOT_FOUND"
-
-
-def CSVWrInt(fname, rowidx, colidx, content):
-    \"\"\"
-    Update value at row/column pointed by integer indexes
-    Assumes data starts at first row and first column, no headers.
-    \"\"\"
-
-    global csv_int_files
-    dialect = None
-    data = csv_int_files.get(fname, None)
-    if data is None:
-        data = list()
-        try:
-            csvfile = open(fname, 'rt', encoding='utf-8')
-        except IOError:
-            return "#FILE_NOT_FOUND"
-        try:
-            dialect = csv.Sniffer().sniff(csvfile.read(1024))
-            csvfile.seek(0)
-            reader = csv.reader(csvfile, dialect)
-            for row in reader:
-                data.append(row)
-        except csv.Error as e:
-            return "#CSV_ERROR"
-        finally:
-            csvfile.close()
-        csv_int_files[fname] = data
-
-    try:
-        if rowidx == len(data):
-            row = []
-            data.append(row)
-        else:
-            row = data[rowidx]
-    except IndexError:
-        return "#ROW_NOT_FOUND"
-
-    try:
-        if rowidx > 0 and colidx >= len(data[0]):
-            raise IndexError
-        if colidx >= len(row):
-            row.extend([""] * (colidx - len(row)) + [content])
-        else:
-            row[colidx] = content
-    except IndexError:
-        return "#COL_NOT_FOUND"
-
-    try:
-        wfile = open(fname, 'wt')
-        writer = csv.writer(wfile) if not(dialect) else csv.writer(wfile, dialect)
-        for row in data:
-            writer.writerow(row)
-    finally:
-        wfile.close()
-
-    return "OK"
-
-
-def pyext_csv_reload():
-    global csv_int_files, csv_str_files
-    csv_int_files.clear()
-    csv_str_files.clear()
-
-"""
+pyext_python_lib_code = open(paths.AbsNeighbourFile(__file__, "py_ext_rt.py"), "r").read()
 
 
 class PythonLibrary(POULibrary):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/py_ext/py_ext_rt.py	Fri Feb 07 11:21:15 2025 +0100
@@ -0,0 +1,297 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz Runtime
+#
+# Copyright (C) 2013: Laurent BESSARD
+# Copyright (C) 2017: Andrey Skvortsov
+# Copyright (C) 2025: Edouard Tisserant
+#
+# See COPYING file for copyrights details.
+
+import csv
+from collections import OrderedDict
+
+csv_int_files = {}
+cvs_int_changed = set()
+csv_str_files = {}
+cvs_str_changed = set()
+
+class Entry():
+    def __init__(self, *args):
+        self.args = args
+    def __call__(self):
+        return self.args
+
+def _CSV_int_Load(fname):
+    global csv_int_files
+    entry = csv_int_files.get(fname, None)
+    if entry is None:
+        data = list()
+        csvfile = open(fname, 'rt', encoding='utf-8')
+
+        try:
+            dialect = csv.Sniffer().sniff(csvfile.read(1024))
+            csvfile.seek(0)
+            reader = csv.reader(csvfile, dialect)
+            for row in reader:
+                data.append(row)
+        finally:
+            csvfile.close()
+        entry = Entry(fname, dialect, data)
+        csv_int_files[fname] = entry
+    return entry
+
+
+def _CSV_str_Load(fname):
+    global csv_str_files
+    entry = csv_str_files.get(fname, None)
+    if entry is None:
+        data = []
+        csvfile = open(fname, 'rt', encoding='utf-8')
+
+        try:
+            dialect = csv.Sniffer().sniff(csvfile.read(1024))
+            csvfile.seek(0)
+            reader = csv.reader(csvfile, dialect)
+            first_row = reader.__next__()
+            data.append(first_row)
+            col_headers = OrderedDict([(name, index+1) for index, name 
+                                        in enumerate(first_row[1:])])
+            max_row_len = len(first_row)
+            row_headers = OrderedDict()
+            for index, row in enumerate(reader):
+                row_headers[row[0]] = index+1
+                data.append(row)
+                max_row_len = max(max_row_len, len(row))
+        finally:
+            csvfile.close()
+        entry = Entry(fname, dialect, col_headers, row_headers, max_row_len, data)
+        csv_str_files[fname] = entry
+    return entry
+
+
+def _CSV_str_Create(fname):
+    global csv_str_files
+    data = [[]] # start with an empty row, acounting for header row
+    dialect = None
+    col_headers = OrderedDict()
+    row_headers = OrderedDict()
+    max_row_len = 1  # set to one initialy, accounting for header column
+    entry = Entry(fname, dialect, col_headers, row_headers, max_row_len, data)
+    csv_str_files[fname] = entry
+    return entry
+
+
+def _CSV_Save_data(fname, dialect, data):
+    try:
+        wfile = open(fname, 'wt')
+        writer = csv.writer(wfile) if not(dialect) else csv.writer(wfile, dialect)
+        for row in data:
+            writer.writerow(row)
+    finally:
+        wfile.close()
+
+def _CSV_int_Save(entry):
+    fname, dialect, data = entry()
+    _CSV_Save_data(fname, dialect, data)
+
+
+def _CSV_str_Save(entry):
+    fname, dialect, col_headers, row_headers, max_row_len, data = entry()
+    _CSV_Save_data(fname, dialect, data)
+
+
+_already_registered_cb = False
+def _CSV_OnIdle_callback():
+    global _already_registered_cb, cvs_int_changed, cvs_str_changed
+    _already_registered_cb = False
+    while len(cvs_int_changed):
+        entry = cvs_int_changed.pop()
+        _CSV_int_Save(entry)
+
+    while len(cvs_str_changed):
+        entry = cvs_str_changed.pop()
+        _CSV_str_Save(entry)
+
+
+def _CSV_register_OnIdle_callback():
+    global _already_registered_cb
+    if not _already_registered_cb:
+        OnIdle.append(_CSV_OnIdle_callback)
+        _already_registered_cb = True
+
+
+def _CSV_int_modified(entry):
+    global cvs_int_changed
+    cvs_int_changed.add(entry)
+    _CSV_register_OnIdle_callback()
+    
+
+def _CSV_str_modified(entry):
+    global cvs_str_changed
+    cvs_str_changed.add(entry)
+    _CSV_register_OnIdle_callback()
+
+
+def CSVRdInt(fname, rowidx, colidx):
+    """
+    Return value at row/column pointed by integer indexes
+    Assumes data starts at first row and first column, no headers.
+    """
+
+    try:
+        _fname, _dialect, data = _CSV_int_Load(fname)()
+    except IOError:
+        return "#FILE_NOT_FOUND"
+    except csv.Error as e:
+        return "#CSV_ERROR"
+
+    try:
+        row = data[rowidx]
+        if not row and rowidx == len(data)-1:
+            raise IndexError
+    except IndexError:
+        return "#ROW_NOT_FOUND"
+
+    try:
+        return row[colidx]
+    except IndexError:
+        return "#COL_NOT_FOUND"
+
+
+def CSVRdStr(fname, rowname, colname):
+    """
+    Return value at row/column pointed by a pair of names as string
+    Assumes first row is column headers and first column is row name.
+    """
+
+    if not rowname:
+        return "#INVALID_ROW"
+    if not colname:
+        return "#INVALID_COLUMN"
+
+    try:
+        fname, dialect, col_headers, row_headers, max_row_len, data = _CSV_str_Load(fname)()
+    except IOError:
+        return "#FILE_NOT_FOUND"
+    except csv.Error:
+        return "#CSV_ERROR"
+
+    try:
+        rowidx = row_headers[rowname]
+    except KeyError:
+        return "#ROW_NOT_FOUND"
+
+    try:
+        colidx = col_headers[colname]
+    except KeyError:
+        return "#COL_NOT_FOUND"
+
+    try:
+        return data[rowidx][colidx]
+    except IndexError:
+        return "#COL_NOT_FOUND"
+
+
+def CSVWrInt(fname, rowidx, colidx, content):
+    """
+    Update value at row/column pointed by integer indexes
+    Assumes data starts at first row and first column, no headers.
+    """
+
+    try:
+        entry = _CSV_int_Load(fname)
+    except IOError:
+        return "#FILE_NOT_FOUND"
+    except csv.Error as e:
+        return "#CSV_ERROR"
+
+    fname, dialect, data = entry()
+    try:
+        if rowidx == len(data):
+            row = []
+            data.append(row)
+        else:
+            row = data[rowidx]
+    except IndexError:
+        return "#ROW_NOT_FOUND"
+
+    try:
+        if rowidx > 0 and colidx >= len(data[0]):
+            raise IndexError
+        if colidx >= len(row):
+            row.extend([""] * (colidx - len(row)) + [content])
+        else:
+            row[colidx] = content
+    except IndexError:
+        return "#COL_NOT_FOUND"
+
+    _CSV_int_modified(entry)
+
+    return "OK"
+
+
+def CSVWrStr(fname, rowname, colname, content):
+    """
+    Update value at row/column pointed by a pair of names as string.
+    Assumes first row is column headers and first column is row name.
+    """
+
+    if not rowname:
+        return "#INVALID_ROW"
+    if not colname:
+        return "#INVALID_COLUMN"
+
+    try:
+        entry = _CSV_str_Load(fname)
+    except IOError:
+        entry = _CSV_str_Create(fname)
+    except csv.Error:
+        return "#CSV_ERROR"
+
+    fname, dialect, col_headers, row_headers, max_row_len, data = entry()
+    try:
+        rowidx = row_headers[rowname]
+        row = data[rowidx]
+    except KeyError:
+        # create a new row with appropriate header
+        row = [rowname]
+        # put it at the end
+        rowidx = len(data)
+        data.append(row)
+        row_headers[rowname] = rowidx
+
+    try:
+        colidx = col_headers[colname]
+    except KeyError:
+        # adjust col headers content
+        first_row = data[0] 
+        first_row += [""]*(max_row_len - len(first_row)) + [rowname]
+        # create a new column
+        colidx = col_headers[colname] = max_row_len
+        max_row_len = max_row_len + 1
+
+    try:
+        row[colidx] = content
+    except IndexError:
+        # create a new cell
+        row += [""]*(colidx - len(row)) + [content]
+
+    _CSV_str_modified(entry)
+
+    return "OK"
+
+
+def CSVReload():
+    global csv_int_files, csv_str_files, cvs_int_changed, cvs_str_changed
+
+    # Force saving modified CSV files
+    _CSV_OnIdle_callback()
+
+    # Wipe data model
+    csv_int_files.clear()
+    csv_str_files.clear()
+    cvs_int_changed.clear()
+    cvs_str_changed.clear()
+