# HG changeset patch # User Edouard Tisserant <edouard@beremiz.fr> # Date 1738923675 -3600 # Node ID 79aa1772f4911d4c52b840870e2bf4dadd315d79 # Parent 9e59bb5ad9e1bd1d2faa195dc0784618042789b8 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. diff -r 9e59bb5ad9e1 -r 79aa1772f491 py_ext/pous.xml --- 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"/> diff -r 9e59bb5ad9e1 -r 79aa1772f491 py_ext/py_ext.py --- 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): diff -r 9e59bb5ad9e1 -r 79aa1772f491 py_ext/py_ext_rt.py --- /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() +