Channel Scripting

When the standard communication of bots does not fit your needs, use communicationscripts. Instruction to make a channel script:

  • There must be channel in bots-monitor (or make a new one)
  • Make a communicationscript with the same name as the channelID
  • Place the communicationscript in bots/usersys/communicationscripts/channelid.py

Use cases

  • Communication method not provided by bots
  • Existing communication needs customization
  • Call external program to write edi message to your ERP system.
  • Additional requirements: Eg. use partner name or order number in output file name.
  • Control archive file naming

Types of communication scripts

  1. Small user exits: at certain places in normal communication a user script is called. Examples of small user exits
  2. Subclass: take-over of (parts of) communication script: user script subclasses existing communication type.
  3. Communication type communicationscript. Bots tries to do the bots-handling of files, you provide the communication details. Examples of communication type ‘communicationscript’

Small User Exits

Some examples of small user exists are below:

Example to Filter email attachments: Some edi-partners send signatures etc in their email. Script does a simple check if incoming attachment starts with ‘UNB’. (Note: Bots treats any text in the email body as another “attachment”)

def accept_incoming_attachment(channeldict,ta,charset,content,contenttype,*args,**kwargs):
    if 'UNB' in content[0:50]:
        return True   #attachments is OK
    else:
        return False  #skip this attachment

Example to Set email subject: Some edi-partners send signatures etc in their email. By default bots uses a number for emails. Sometimes you want a more meaningfull subject.

def subject(channeldict,ta,subjectstring,content,*args,**kwargs):
    ta.synall()        #needed to get access to attributes of object ta (eg ta.frompartner)
    return 'EDI messages from ' + ta.frompartner + '_' + subjectstring

Exapmle to Name archive file same as input file: Not needed for bots 3.x where you can do this via setting in bots.ini

import os
import bots.botslib as botslib

def archivename(channeldict,idta,filename,*args,**kwargs):
     taparent=botslib.OldTransaction(idta=idta)
     ta_list = botslib.trace_origin(ta=taparent,where={'status':EXTERNIN})
     archivename = os.path.basename(ta_list[0].filename)
     return archivename

Example to Set the archive path: Path root is set in channel. Add sub-dir per date, then sub-dir per channel under it.

import time
import bots.botslib as botslib

def archivepath(channeldict,*args,**kwargs):
    archivepath = botslib.join(channeldict['archivepath'],time.strftime('%Y%m%d'),channeldict['idchannel'])
    return archivepath

Example to Partners in the output file name: Not needed for bots 3.x where you can do this via file name in GUI.

def filename(channeldict,filename,ta,*args,**kwargs):
    ta.synall()        #needed to get access to attributes of object ta (eg ta.frompartner)
    return ta.frompartner + '_' + ta.topartner + '_' + filename

Example to Name the output file from botskey: botskey can be set in grammar or mapping, eg. from customer’s order number. If no botskey is found, the default file naming method will be used. Syntax must contain ‘merge’:False. Not needed for bots 3.x where you can do this via file name in GUI.

def filename(channeldict,filename,ta,*args,**kwargs):
    ta.synall()
    if ta.botskey:
        return filename + ta.botskey
    else:
        return filename

Example to Name the output file same as input file: Syntax must contain merge:False. Not needed for bots 3.x where you can do this via file name in GUI.

import os
import bots.botslib as botslib

def filename(channeldict,filename,ta,*args,**kwargs):
    ta_list = botslib.trace_origin(ta=ta,where={'status':EXTERNIN})
    filename_in = os.path.basename(ta_list[0].filename) # just filename, remove path
    return filename + filename_in

Subclassing

It is possible to overwrite bots communication methods completely. This is done using python subclassing. Again, as with all communication scripting there should be a file in usersys/communicationscripts with the same name as the channel (and extension .py)

Example 1: In this case communication-type of the channel is ‘file’. Bots will check the communication-script file if there is a class called ‘file’ and use that. The class ‘file’ subclasses the standard ‘file’ method of bots.

import bots.communication as communication

class file(communication.file):
    def connect(self,*args,**kwargs):
        #do the preparing work
        print 'in connect method'

Example 2: In this case communication-type of the channel is ‘ftp’. The class ‘ftp’ subclasses the standard ‘ftp’ method of bots. The ‘outcommunicate’ method of the ftp class is taken over with this implementation.

import bots.communication as communication
import bots.botslib as botslib
from bots.botsconfig import *

class ftp(communication.ftp):
    @botslib.log_session
    def outcommunicate(self,*args,**kwargs):
        #get right filename_mask & determine if fixed name (append) or files with unique names
        filename_mask = self.channeldict['filename'] if self.channeldict['filename'] else '*'
        if '{overwrite}' in filename_mask:
            filename_mask = filename_mask.replace('{overwrite}','')
            mode = 'STOR '
        else:
            mode = 'APPE '
        for row in botslib.query('''SELECT idta,filename,numberofresends
                                    FROM ta
                                    WHERE idta>%(rootidta)s
                                      AND status=%(status)s
                                      AND statust=%(statust)s
                                      AND tochannel=%(tochannel)s
                                        ''',
                                    {'tochannel':self.channeldict['idchannel'],'rootidta':self.rootidta,
                                    'status':FILEOUT,'statust':OK}):
            try:
                ta_from = botslib.OldTransaction(row['idta'])
                ta_to = ta_from.copyta(status=EXTERNOUT)
                tofilename = self.filename_formatter(filename_mask,ta_from)
                if self.channeldict['ftpbinary']:
                    fromfile = botslib.opendata(row['filename'], 'rb')
                    self.session.storbinary(mode + tofilename, fromfile)
                else:
                    fromfile = botslib.opendata(row['filename'], 'r')
                    self.session.storlines(mode + tofilename, fromfile)
                fromfile.close()
            except:
                txt = botslib.txtexc()
                ta_to.update(statust=ERROR,errortext=txt,filename='ftp:/'+posixpath.join(self.dirpath,tofilename),numberofresends=row['numberofresends']+1)
            else:
                ta_to.update(statust=DONE,filename='ftp:/'+posixpath.join(self.dirpath,tofilename),numberofresends=row['numberofresends']+1)
            finally:
                ta_from.update(statust=DONE)

Example 3: In this case communication-type of the channel is ‘ftp’ or ‘sftp’. The class ‘ftp’ subclasses the standard ‘ftp’ method of bots. The ‘disconnect’ method of the ftp class is taken over with this implementation. The bots channel should be configured to upload either to a ‘tmp’ sub-directory, or with a ‘.tmp’ extension. This function renames the files once uploads are complete, this preventing the recipient from processing partial files.

'''
For safety when uploading to ftp servers, it is a good idea to rename/move
files once complete. This prevents the receiver processing partial files.
When all files have been sent and before the session is disconnected, the
files are renamed so the receiver can process them.

Two methods are available:
 1. Append extension ".tmp" to the channel filename
    This method is simpler, but the receiver may still process the
    .tmp files if it does not look for specific extensions to process.
 2. Append subdirectory "/tmp" to the channel path
    This requires an extra directory created on the server, you may not
    be authorised to do this.

Subclassing of ftp.disconnect. Import this to your communicationscript (ftp or sftp as required):
    from _ftp_rename import ftp
    from _ftp_rename import sftp

Mike Griffin  4/09/2013

'''

import bots.communication as communication
import bots.botslib as botslib
import bots.botsglobal as botsglobal

class ftp(communication.ftp):
    def disconnect(self,*args,**kwargs):

        # rename files to remove .tmp extensions
        if self.channeldict['filename'].endswith('.tmp'):
            for f in self.session.nlst():
                if f.endswith('.tmp'):
                    try:
                        self.session.rename(f,f[:-4])
                    except:
                        pass

        # rename files from tmp subdirectory to parent directory
        if self.channeldict['path'].endswith('/tmp'):
            for f in self.session.nlst():
                try:
                    self.session.rename(f,'../%s' %f)
                except:
                    pass

        try:
            self.session.quit()
        except:
            self.session.close()
        botslib.settimeout(botsglobal.ini.getint('settings','globaltimeout',10))

class sftp(communication.sftp):
    def disconnect(self,*args,**kwargs):

        # rename files to remove .tmp extensions
        if self.channeldict['filename'].endswith('.tmp'):
            for f in self.session.listdir('.'):
                if f.endswith('.tmp'):
                    try:
                        self.session.rename(f,f[:-4])
                    except:
                        pass

        # rename files from tmp subdirectory to parent directory
        if self.channeldict['path'].endswith('/tmp'):
            for f in self.session.listdir('.'):
                try:
                    self.session.rename(f,'../%s' %f)
                except:
                    pass

        self.session.close()
        self.transport.close()

Example 4: In this case communication-type of the channel is ‘ftp’. The class ‘ftp’ subclasses the standard ‘ftp’ method of bots. The ‘disconnect’ method of the ftp class is taken over with this implementation. This provides a way to submit a remote command to the ftp server, for example to run a program on that server. The bots channel is configured with the command in the ‘parameters’ field.

'''
Before disconnecting, send a remote command
Channel "parameters" holds the command to send

Subclassing of ftp.disconnect. Import this to your communicationscript:
    from _ftp_remote_command import ftp

Mike Griffin  13/09/2013
'''

import bots.communication as communication
import bots.botsglobal as botsglobal

class ftp(communication.ftp):
    def disconnect(self,*args,**kwargs):

        # send remote command to ftp server
        botsglobal.logger.info('Send remote command: %s',self.channeldict['parameters'])
        self.session.sendcmd('RCMD %s' %self.channeldict['parameters'])

        try:
            self.session.quit()
        except:
            self.session.close()
        botslib.settimeout(botsglobal.ini.getint('settings','globaltimeout',10))

Communication type communicationscript

In this case, the channel must be configured with Type: communicationscript. In the communicationscript some functions will be called:

  • connect (required)
  • main (optional, ‘main’ should handle files one by one)
  • disconnect (optional)

Different ways of working:

  1. For incoming files (bots receives the files):
    • Connect puts all files in a directory, there is no ‘main’ function. bots can remove the files (if you use the remove switch of the channel). See example 1.
    • Connect only builds the connection, main is a generator that passes the messages one by one (using yield). bots can remove the files (if you use the remove switch of the channel). See example 2.
  2. For outgoing files (bots sends the files):
    • No main function: the processing of all the files can be done in disconnect. bots can remove the files (if you use the remove switch of the channel). See example 3.
    • If there is a main function: the main function is called by bots after writing each file. bots can remove the files (if you use the remove switch of the channel). See example 4.

Example 1: incoming files via external program all at once

Calls an external program. Think eg of a specific communication module for a VAN. All files are received at once to a folder, then processed like a normal file channel.

import subprocess

def connect(channeldict,*args,**kwargs):
    subprocess.call(['C:/Program files/my VAN/comms-module.exe','-receive'])

Example 2: incoming files via external program one by one

TODO: make a valid example using yield. main is a generator.

import subprocess

def connect(channeldict,*args,**kwargs):
    ''' function does nothing but it is required.'''
    pass

def main(channeldict,*args,**kwargs):

    yield ?

Example 3: outgoing files via external program all at once

Calls an external program. Think eg of a specific communication module for a VAN. In this example the ‘disconnect’ script is called after all files are written to directory; in disconnect all files are passed to external communication-module.

import subprocess
import os

def connect(channeldict,*args,**kwargs):
    ''' function does nothing but it is required.'''
    pass

def disconnect(channeldict,*args,**kwargs):
    subprocess.call(['C:/Program files/my VAN/comms-module.exe','-send',os.path.join(channeldict['path'],'\*.xml'])

Example 4: outgoing files via external program one by one

Calls an external program. Think eg of a specific communication module for a VAN. In this example the ‘main’ script is called for each outgoing file.

import subprocess

def connect(channeldict,*args,**kwargs):
    ''' function does nothing but it is required.'''
    pass

def main(channeldict,filename,ta,*args,**kwargs):
    subprocess.call(['C:/Program files/my VAN/comms-module.exe','-send',filename])

Example 5: outgoing files to a printer

Send data (eg. ZPL code to print fancy labels) directly to a Windows configured printer. The printer can be defined in Windows either as “Generic/Text Only” or with the proper driver, because this script just sends raw data, bypassing the driver.

Dependencies: Requires pywin32
import os
import win32print
import bots.transform as transform

def connect(channeldict,*args,**kwargs):
    ''' function does nothing but it is required.'''
    pass

def main(channeldict,filename,ta,*args,**kwargs):

    # set printer values required
    ta.synall()
    printer = transform.partnerlookup(ta.topartner,'attr1')
    jobname = ta.botskey

    # read the output file
    with open(filename,'r') as content_file:
        content = content_file.read()

    # send data to the printer
    hPrinter = win32print.OpenPrinter(printer)
    hJob = win32print.StartDocPrinter(hPrinter,1,(jobname,None,'RAW'))
    win32print.WritePrinter(hPrinter,content)
    win32print.EndDocPrinter(hPrinter)
    win32print.ClosePrinter(hPrinter)