kepconfig.connection

connection exposes an server class that manages the connection information and RESTful requests for the Kepware Configuration API Library.

  1# -------------------------------------------------------------------------
  2# Copyright (c) PTC Inc. and/or all its affiliates. All rights reserved.
  3# See License.txt in the project root for
  4# license information.
  5# --------------------------------------------------------------------------
  6
  7r"""`connection` exposes an `server` class that manages the connection
  8information and RESTful requests for the Kepware Configuration API Library.
  9"""
 10
 11import json
 12import codecs
 13import datetime
 14from urllib import request, parse, error
 15from base64 import b64encode
 16from .error import KepError, KepHTTPError, KepURLError
 17import socket
 18import ssl
 19from .structures import KepServiceResponse, KepServiceStatus, _HttpDataAbstract, Filter
 20
 21
 22class server:
 23    '''A class to represent a connection to an instance of Kepware. This object is used to 
 24    create the Authentication and server parameters to taget a Kepware instance. An instance of this is 
 25    used in all configuration calls done.
 26
 27    :param host: host name or IP address
 28    :param port: port of Configuration API
 29    :param username: username to conduct "Basic Authentication"
 30    :param password: password to conduct "Basic Authentication"
 31    :param https: Sets `SSL_on` to use HTTPS connection (Default: False)
 32    :param SSL_on: Identify to use HTTPS connection (Default: False)
 33    :param SSL_ignore_hostname: During certificate validation ignore the hostname check
 34    :param SSL_trust_all_certs: (insecure) - During certificate validation trust any certificate - if True, 
 35        will "set SSL_ignore_hostname" to true
 36    :param url: base URL for the server connection
 37
 38    **Methods**
 39
 40    :meth:`reinitialize`: reinitialize the Kepware server
 41
 42    :meth:`get_transaction_log`: retrieve the Configuration API transaction logs
 43
 44    :meth:`get_event_log`: retrieve the Kepware Event Log
 45
 46    :meth:`get_audit_log`: retrieve the Kepware Audit Log
 47
 48    :meth:`get_info`: retrieve the Kepware product information
 49    
 50    :meth:`import_empty_project`: import an empty project to the Kepware server
 51
 52    :meth:`get_project_properties`: retrieve the Kepware Project Properties
 53
 54    :meth:`modify_project_properties` - modify the Kepware Project Properties
 55
 56    :meth:`service_status` - retrive service job status
 57
 58    :meth:`export_project_configuration` - export the current project configuration in JSON format
 59
 60    :meth:`save_project` - save the current project to a file
 61
 62    :meth:`load_project` - load a project from a file
 63    '''
 64    __root_url = '/config'
 65    __version_url = '/v1'
 66    __project_services_url = '/project/services'
 67    __event_log_url = '/event_log'
 68    __trans_log_url = '/log'
 69    __audit_log_url = '/audit_log'
 70
 71
 72
 73    def __init__(self,  host: str, port: int, user: str, pw: str, https: bool = False):
 74        self.host = host
 75        self.port = port
 76        self.username = user
 77        self.password = pw
 78        self.__ssl_context = ssl.create_default_context()
 79        self.__SSL_on = https
 80    
 81    @property
 82    def url(self):
 83        if self.SSL_on:
 84            proto = 'https'
 85        else:
 86            proto = 'http'
 87        return  f'{proto}://{self.host}:{self.port}{self.__root_url}{self.__version_url}'
 88    
 89    @property
 90    def SSL_on(self):
 91        return self.__SSL_on
 92    
 93    @SSL_on.setter
 94    def SSL_on(self, val):
 95        
 96        if isinstance(val, bool):
 97            self.__SSL_on = val
 98
 99    @property
100    def SSL_ignore_hostname(self):
101        return not self.__ssl_context.check_hostname
102
103    @SSL_ignore_hostname.setter
104    def SSL_ignore_hostname(self, val):
105        if isinstance(val, bool):
106            if val == True:
107                self.__ssl_context.check_hostname = False
108            else:
109                self.__ssl_context.check_hostname = True
110
111    
112    @property
113    def SSL_trust_all_certs(self):
114        if self.__ssl_context.verify_mode == ssl.CERT_NONE:
115            return True
116        else:
117            return False
118
119    @SSL_trust_all_certs.setter
120    def SSL_trust_all_certs(self, val):
121        if isinstance(val, bool):
122            if val == True:
123                if self.__ssl_context.check_hostname == True:
124                    self.__ssl_context.check_hostname = False
125                self.__ssl_context.verify_mode = ssl.CERT_NONE
126            else:
127                self.__ssl_context.verify_mode = ssl.CERT_REQUIRED
128
129
130    def get_status(self) -> dict:
131        '''Executes a health status request to the Kepware instance to report service statuses.
132
133        :return: List of data for the health status request
134
135        :raises KepHTTPError: If urllib provides an HTTPError
136        :raises KepURLError: If urllib provides an URLError
137        '''
138
139        r = self._config_get(f'{self.url}/status')
140        return r.payload
141    def get_info(self) -> dict:
142        '''Requests product information from the Kepware instance. Provides various information including
143        product name and version information.
144
145        :return: dict of data for the product information request
146        
147        :raises KepHTTPError: If urllib provides an HTTPError
148        :raises KepURLError: If urllib provides an URLError
149        '''
150        
151        r = self._config_get(f'{self.url}/about')
152        return r.payload
153
154    def reinitialize(self, job_ttl: int = None) -> KepServiceResponse:
155        '''Executes a Reinitialize service call to the Kepware instance.
156
157        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
158
159        :return: `KepServiceResponse` instance with job information
160
161        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
162        :raises KepURLError: If urllib provides an URLError
163        '''
164        url = self.url + self.__project_services_url + '/ReinitializeRuntime' 
165        try:
166            job = self._kep_service_execute(url, None, job_ttl)
167            return job
168        except Exception as err:
169            raise err
170        
171    def get_transaction_log(self, limit: int = None, start: datetime.datetime = None, end: datetime.datetime = None) -> list:
172        ''' Get the Transaction Log from the Kepware instance.
173
174        :param limit: *(optional)* number of transaction log entries to request
175        :param start: *(optional)* start time of query as `datetime.datetime` type and should be UTC
176        :param end: *(optional)* end time of query as `datetime.datetime` type and should be UTC
177
178        :raises KepHTTPError: If urllib provides an HTTPError
179        :raises KepURLError: If urllib provides an URLError
180        '''
181        query = self.__create_query(start, end, limit)
182        url = f'{self.url}{self.__trans_log_url}'
183        r = self._config_get(url, params= query)
184        return r.payload
185
186    def get_event_log(self, limit: int = None, start: datetime.datetime = None, end: datetime.datetime = None, *, options: dict = None) -> list:
187        ''' Get the Event Log from the Kepware instance.
188
189        :param limit: *(optional)* number of event log entries to request
190        :param start: *(optional)* start time of query as `datetime.datetime` type and should be UTC
191        :param end: *(optional)* end time of query as `datetime.datetime` type and should be UTC
192        :param options: *(optional)* Dict of parameters to filter, sort or pagenate the list of transactions. Options are `event`, 
193        `sortOrder`, `sortProperty`, `pageNumber`, and `pageSize`
194
195        :raises KepHTTPError: If urllib provides an HTTPError
196        :raises KepURLError: If urllib provides an URLError
197        '''
198        query = self.__create_query(start, end, limit)
199        if options is not None:
200            query = {**query, **options}
201        url = f'{self.url}{self.__event_log_url}'
202        r = self._config_get(url, params= query)
203        return r.payload
204
205    def get_audit_log(self, limit: int = None, *, filters: list[Filter] = None, options: dict = None) -> list:
206        ''' Get the Audit Log from the Kepware instance.
207
208        :param limit: *(optional)* number of event log entries to request
209        :param filters: *(optional)* list of filters that are used to control results returned from the log
210        :param options: *(optional)* Dict of parameters to filter, sort or pagenate the list of transactions. Options are `sortOrder`, 
211        `sortProperty`, `pageNumber`, and `pageSize`
212
213        :raises KepHTTPError: If urllib provides an HTTPError
214        :raises KepURLError: If urllib provides an URLError
215        '''
216        query = self.__create_filter_query(filters)
217        if limit is not None:
218            query['limit'] = limit
219        if options is not None:
220            query = {**query, **options}
221        url = f'{self.url}{self.__audit_log_url}'
222        r = self._config_get(url, params= query)
223        return r.payload
224    
225    def get_project_properties(self) -> dict:
226        ''' Get the Project Properties of the Kepware instance.
227        
228        :return: Dict of all the project properties
229
230        :raises KepHTTPError: If urllib provides an HTTPError
231        :raises KepURLError: If urllib provides an URLError
232        '''
233
234        r = self._config_get(self.url + '/project')
235        return r.payload
236    
237    def modify_project_properties(self, DATA: dict, force: bool = False) -> bool:
238        ''' Modify the Project Properties of the Kepware instance.
239
240        :param DATA: Dict of the project properties to be modified
241        :param force: *(optional)* if True, will force the configuration update to the Kepware server
242        
243        :return: True - If a "HTTP 200 - OK" is received from Kepware server
244
245        :raises KepHTTPError: If urllib provides an HTTPError
246        :raises KepURLError: If urllib provides an URLError
247        '''
248
249        prop_data = self._force_update_check(force, DATA)
250        r = self._config_update(self.url + '/project', prop_data)
251        if r.code == 200: return True 
252        else: raise KepHTTPError(r.url, r.code, r.msg, r.hdrs, r.payload)
253    
254    def import_empty_project(self) -> KepServiceResponse:
255        '''Executes JsonProjectLoad Service call to the Kepware instance with an empty project. This service 
256        imports an empty project configuration, acts like a FILE->NEW action and 
257        stop communications while the new project replaces the current project in the Kepware runtime. 
258
259        :return: `KepServiceResponse` instance with job information
260        
261        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
262        :raises KepURLError: If urllib provides an URLError
263        '''
264        return self.import_project_configuration({"project":{}})
265
266
267    def import_project_configuration(self, DATA: dict) -> KepServiceResponse:
268        '''Executes JsonProjectLoad Service call to the Kepware instance. This service imports project configuration 
269        data, expecting a complete project file in JSON/dict format. This service acts like a FILE->OPEN action and 
270        stop communications while the new project replaces the current project in the Kepware runtime. 
271    
272        :param DATA: Complete project configuration data in JSON/dict format. 
273
274        :return: `KepServiceResponse` instance with job information
275        
276        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
277        :raises KepURLError: If urllib provides an URLError
278        '''
279        url = self.url + self.__project_services_url + '/JsonProjectLoad'
280        try:
281            job = self._kep_service_execute(url, DATA)
282            return job
283        except Exception as err:
284            raise err
285        
286    def export_project_configuration(self) -> dict:
287        '''Get a complete copy of the project configuration in JSON format. This will include the same 
288        configuration that is stored when you save the project file manually.
289
290        :return: Dict of the complete project configuration
291
292        :raises KepHTTPError: If urllib provides an HTTPError
293        :raises KepURLError: If urllib provides an URLError
294        '''
295        r = self._config_get(self.url + '/project', params= {"content": "serialize"})
296        return r.payload
297    
298    def save_project(self, filename: str, password: str = None, job_ttl: int = None) -> KepServiceResponse:
299        '''Executes a ProjectSave Service call to the Kepware instance. This saves 
300        a copy of the current project file to disk. The filename
301
302        :param filename: Relative file path and name of project file including the file extension to save.
303        Location of relative project file paths:
304         
305                TKS or KEP (Windows): C:\\PROGRAMDATA\\PTC\\Thingworx Kepware Server\\V6 or 
306                                C:\\PROGRAMDATA\\Kepware\\KEPServerEX\\V6
307                TKE (Linux):    /opt/tkedge/v1/user_data
308
309
310        :param password: *(optional)* Specify a password with which to  save an encrypted project file with.  
311            This password will be required to load this project file.
312        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
313
314        :return: `KepServiceResponse` instance with job information
315
316        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
317        :raises KepURLError: If urllib provides an URLError
318        '''
319        url = self.url + self.__project_services_url + '/ProjectSave'
320        prop_data = {'servermain.PROJECT_FILENAME': filename}
321        if password != None: prop_data['servermain.PROJECT_PASSWORD'] = password
322        try:
323            job = self._kep_service_execute(url, prop_data, job_ttl)
324            return job
325        except Exception as err:
326            raise err
327
328    def load_project(self, filename: str, password: str = None, job_ttl: int = None) -> KepServiceResponse:
329        '''Executes a ProjectLoad Service call to the Kepware instance. This loads 
330        a project file to disk.
331
332        INPUTS:
333        
334        :param filename: Fully qualified or relative path and name of project file including the file extension. Absolute
335        paths required for TKS and KEP while file path is relative for TKE:
336
337                Windows - filename = C:\\filepath\\test.opf
338                Linux - filename = filepath/test.lpf - Location is /opt/tkedge/v1/user_data/filepath/test.lpf
339
340        :param password: *(optional)* Specify a password with which to load an encrypted project file.          
341        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
342
343        :return: `KepServiceResponse` instance with job information
344
345        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
346        :raises KepURLError: If urllib provides an URLError
347        '''
348        url = self.url + self.__project_services_url + '/ProjectLoad'
349        prop_data = {'servermain.PROJECT_FILENAME': filename}
350        if password != None: prop_data['servermain.PROJECT_PASSWORD'] = password
351        try:
352            job = self._kep_service_execute(url, prop_data, job_ttl)
353            return job
354        except Exception as err:
355            raise err
356
357    def get_project_backup_info(self) -> dict:
358        ''' Get the Project Backup Information of the Kepware instance.
359        
360        :return: List of all the backup projects and their properties.
361
362        :raises KepHTTPError: If urllib provides an HTTPError
363        :raises KepURLError: If urllib provides an URLError
364        '''
365
366        r = self._config_get(self.url + '/project/backups')
367        return r.payload
368
369    def backup_project(self, job_ttl: int = None) -> KepServiceResponse:
370        '''Executes a CreateBackup Service call to the Kepware instance. This saves 
371        a copy of the current project file to disk as a backup that can be retrieved.
372
373        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
374
375        :return: `KepServiceResponse` instance with job information
376
377        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
378        :raises KepURLError: If urllib provides an URLError
379        '''
380        url = self.url + self.__project_services_url + '/CreateBackup'
381        try:
382            job = self._kep_service_execute(url, TTL= job_ttl)
383            return job
384        except Exception as err:
385            raise err
386        
387    def service_status(self, resp: KepServiceResponse):
388        '''Returns the status of a service job. Used to verify if a service call
389        has completed or not.
390
391        :param resp: `KepServiceResponse` instance with job information
392
393        :return: `KepServiceStatus` instance with job status
394
395        :raises KepHTTPError: If urllib provides an HTTPError
396        :raises KepURLError: If urllib provides an URLError
397        '''
398        # need to remove part of job href
399        loc = resp.href.find(self.__root_url + self.__version_url)
400        job_url = resp.href[loc + len(self.__root_url + self.__version_url):]
401
402        r = self._config_get(self.url + job_url)
403        job = KepServiceStatus(r.payload['servermain.JOB_COMPLETE'],r.payload['servermain.JOB_STATUS'], r.payload['servermain.JOB_STATUS_MSG'])
404        return job
405
406
407    #Function used to Add an object to Kepware (HTTP POST)
408    def _config_add(self, url, DATA):
409        '''Conducts an POST method at *url* to add an object in the Kepware Configuration
410        *DATA* is required to be a properly JSON object (dict) of the item to be posted to *url* 
411        '''
412        if len(DATA) == 0:
413            err_msg = f'Error: Empty List or Dict in DATA | DATA type: {type(DATA)}'
414            raise KepError(err_msg) 
415        data = json.dumps(DATA).encode('utf-8')
416        url_obj = self.__url_validate(url)
417        q = request.Request(url_obj, data, method='POST')
418        r = self.__connect(q)
419        return r
420
421    #Function used to del an object to Kepware (HTTP DELETE)
422    def _config_del(self, url):
423        '''Conducts an DELETE method at *url* to delete an object in the Kepware Configuration'''
424        url_obj = self.__url_validate(url)
425        q = request.Request(url_obj, method='DELETE')
426        r = self.__connect(q)
427        return r
428
429    #Function used to Update an object to Kepware (HTTP PUT)
430    def _config_update(self, url, DATA = None):
431        '''Conducts an PUT method at *url* to modify an object in the Kepware Configuration.
432        *DATA* is required to be a properly JSON object (dict) of the item to be put to *url*
433        '''
434        url_obj = self.__url_validate(url)
435        if DATA == None:            
436            q = request.Request(url_obj, method='PUT')
437        else:
438            data = json.dumps(DATA).encode('utf-8')
439            q = request.Request(url_obj, data, method='PUT')
440        r = self.__connect(q)
441        return r
442
443    #Function used to Read an object from Kepware (HTTP GET) and return the JSON response
444    def _config_get(self, url, *, params = None):
445        '''
446        Conducts an GET method at *url* to retrieve an objects properties with query parameters in 
447        the Kepware Configuration.
448        '''
449        # Add parameters when necessary
450        if params is not None and params != {}:
451            qparams = parse.urlencode(params)
452            url = f'{url}?{qparams}'
453        url_obj = self.__url_validate(url)
454        q = request.Request(url_obj, method='GET')
455        r = self.__connect(q)
456        return r
457
458    
459    def _force_update_check(self, force, DATA):
460        '''
461        This will validate if the modify call needs to be forced or not. If forced, the dict DATA needs
462        to have the 'FORCE_UPDATE' property with a value of True. If forced is not requested, it is necessary
463        to provide the current 'PROJECT_ID'. If 'PROJECT_ID' is not present in DATA, this will automatically 
464        retreive it from the active server.
465        '''
466        if force == True:
467            DATA['FORCE_UPDATE'] = True
468        else:
469            # Get Project ID if it doesn't exist and if FORCE_UPDATE is existing and FALSE
470            if 'PROJECT_ID' not in DATA:
471                if 'FORCE_UPDATE' in DATA:
472                    if 'FORCE_UPDATE' == False:
473                        try:
474                            project_data = self._config_get(self.url + '/project')
475                            DATA['PROJECT_ID'] = project_data.payload['PROJECT_ID']
476                        except:
477                            #NEED TO COVER ERROR CONDITION
478                            pass
479                else:
480                    try:
481                        project_data = self._config_get(self.url + '/project')
482                        DATA['PROJECT_ID'] = project_data.payload['PROJECT_ID']
483                    except:
484                        #NEED TO COVER ERROR CONDITION
485                        pass
486        return DATA
487    # General service call handler
488    def _kep_service_execute(self, url, DATA = None, TTL = None):
489        try:
490            if TTL != None:
491                if DATA == None: DATA = {}
492                DATA["servermain.JOB_TIME_TO_LIVE_SECONDS"]= TTL
493            r = self._config_update(url, DATA)
494            job = KepServiceResponse(r.payload['code'],r.payload['message'], r.payload['href'])
495            return job
496        except KepHTTPError as err:
497            if err.code == 429:
498                job = KepServiceResponse()
499                job.code = err.code
500                job.message = err.payload
501                return job
502            else:
503                raise err
504
505# 
506# Supporting Functions
507#
508
509    # General connect call to manage HTTP responses for all methods
510    # Returns the response object for the method to handle as appropriate
511    # Raises Errors as found
512    def __connect(self,request_obj):
513        # Fill appropriate header information
514        data = _HttpDataAbstract()
515        request_obj.add_header("Authorization", "Basic %s" % self.__build_auth_str(self.username, self.password))
516        request_obj.add_header("Content-Type", "application/json")
517        request_obj.add_header("Accept", "application/json")
518        try:
519            # context is sent regardless of HTTP or HTTPS - seems to be ignored if HTTP URL
520            with request.urlopen(request_obj, context=self.__ssl_context) as server:
521                try:
522                    payload = server.read()
523                    data.payload = json.loads(codecs.decode(payload,'utf-8-sig'))
524                except:
525                    pass
526                data.code = server.code
527                data.reason = server.reason
528                return data
529        except error.HTTPError as err:
530            payload = json.loads(codecs.decode(err.read(),'utf-8-sig'))
531            # print('HTTP Code: {}\n{}'.format(err.code,payload), file=sys.stderr)
532            raise KepHTTPError(url=err.url, code=err.code, msg=err.msg, hdrs=err.hdrs, payload=payload)
533        except error.URLError as err:
534            # print('URLError: {} URL: {}'.format(err.reason, request_obj.get_full_url()), file=sys.stderr)
535            raise KepURLError(msg=err.reason, url=request_obj.get_full_url())
536
537    # Fucntion used to ensure special characters are handled in the URL
538    # Ex: Space will be turned to %20
539    def __url_validate(self, url):
540        # Configuration API does not use fragments in URL so ignore to allow # as a character
541        # Objects in Kepware can include # as part of the object names
542        parsed_url = parse.urlparse(url, allow_fragments= False)
543        # Added % for scenarios where special characters have already been escaped with %
544        updated_path = parse.quote(parsed_url.path, safe = '/%')
545
546        # If host is "localhost", force using the IPv4 loopback adapter IP address in all calls
547        # This is done to remove retries that will happen when the host resolution uses IPv6 intially
548        # Kepware currently doesn't support IPv6 and is not listening on this interface
549        if parsed_url.hostname.lower() == 'localhost':
550            ip = socket.gethostbyname(parsed_url.hostname)
551            parsed_url = parsed_url._replace(netloc='{}:{}'.format(ip, parsed_url.port))
552        
553        return parsed_url._replace(path=updated_path).geturl()
554
555    # Function used to build the basic authentication string
556    def __build_auth_str(self, username, password):
557        if isinstance(username, str):
558            username = username.encode('latin1')
559        if isinstance(password, str):
560            password = password.encode('latin1')
561        authstr = b64encode(b':'.join((username, password))).strip().decode('ascii')
562        return authstr
563    
564    # Create parameters for log queries
565    def __create_query(self, start = None, end = None, limit = None):
566        query = {}
567        if start != None and isinstance(start, datetime.datetime):
568            query['start'] = start.isoformat()
569        if end != None and isinstance(end, datetime.datetime):
570            query['end'] = end.isoformat()
571        if limit != None:
572            query['limit'] = limit
573        return query
574
575    # Create filter query for log queries
576    def __create_filter_query(self, filters: list[Filter] = None):
577        query = {}
578        if filters is None:
579            return query
580        for f in filters:
581            # Ensure we use the value of the Enum, not the Enum object itself
582            key = f"filter[{f.field.value}][{f.modifier.value}]"
583            query[key] = f.value
584        return query
class server:
 23class server:
 24    '''A class to represent a connection to an instance of Kepware. This object is used to 
 25    create the Authentication and server parameters to taget a Kepware instance. An instance of this is 
 26    used in all configuration calls done.
 27
 28    :param host: host name or IP address
 29    :param port: port of Configuration API
 30    :param username: username to conduct "Basic Authentication"
 31    :param password: password to conduct "Basic Authentication"
 32    :param https: Sets `SSL_on` to use HTTPS connection (Default: False)
 33    :param SSL_on: Identify to use HTTPS connection (Default: False)
 34    :param SSL_ignore_hostname: During certificate validation ignore the hostname check
 35    :param SSL_trust_all_certs: (insecure) - During certificate validation trust any certificate - if True, 
 36        will "set SSL_ignore_hostname" to true
 37    :param url: base URL for the server connection
 38
 39    **Methods**
 40
 41    :meth:`reinitialize`: reinitialize the Kepware server
 42
 43    :meth:`get_transaction_log`: retrieve the Configuration API transaction logs
 44
 45    :meth:`get_event_log`: retrieve the Kepware Event Log
 46
 47    :meth:`get_audit_log`: retrieve the Kepware Audit Log
 48
 49    :meth:`get_info`: retrieve the Kepware product information
 50    
 51    :meth:`import_empty_project`: import an empty project to the Kepware server
 52
 53    :meth:`get_project_properties`: retrieve the Kepware Project Properties
 54
 55    :meth:`modify_project_properties` - modify the Kepware Project Properties
 56
 57    :meth:`service_status` - retrive service job status
 58
 59    :meth:`export_project_configuration` - export the current project configuration in JSON format
 60
 61    :meth:`save_project` - save the current project to a file
 62
 63    :meth:`load_project` - load a project from a file
 64    '''
 65    __root_url = '/config'
 66    __version_url = '/v1'
 67    __project_services_url = '/project/services'
 68    __event_log_url = '/event_log'
 69    __trans_log_url = '/log'
 70    __audit_log_url = '/audit_log'
 71
 72
 73
 74    def __init__(self,  host: str, port: int, user: str, pw: str, https: bool = False):
 75        self.host = host
 76        self.port = port
 77        self.username = user
 78        self.password = pw
 79        self.__ssl_context = ssl.create_default_context()
 80        self.__SSL_on = https
 81    
 82    @property
 83    def url(self):
 84        if self.SSL_on:
 85            proto = 'https'
 86        else:
 87            proto = 'http'
 88        return  f'{proto}://{self.host}:{self.port}{self.__root_url}{self.__version_url}'
 89    
 90    @property
 91    def SSL_on(self):
 92        return self.__SSL_on
 93    
 94    @SSL_on.setter
 95    def SSL_on(self, val):
 96        
 97        if isinstance(val, bool):
 98            self.__SSL_on = val
 99
100    @property
101    def SSL_ignore_hostname(self):
102        return not self.__ssl_context.check_hostname
103
104    @SSL_ignore_hostname.setter
105    def SSL_ignore_hostname(self, val):
106        if isinstance(val, bool):
107            if val == True:
108                self.__ssl_context.check_hostname = False
109            else:
110                self.__ssl_context.check_hostname = True
111
112    
113    @property
114    def SSL_trust_all_certs(self):
115        if self.__ssl_context.verify_mode == ssl.CERT_NONE:
116            return True
117        else:
118            return False
119
120    @SSL_trust_all_certs.setter
121    def SSL_trust_all_certs(self, val):
122        if isinstance(val, bool):
123            if val == True:
124                if self.__ssl_context.check_hostname == True:
125                    self.__ssl_context.check_hostname = False
126                self.__ssl_context.verify_mode = ssl.CERT_NONE
127            else:
128                self.__ssl_context.verify_mode = ssl.CERT_REQUIRED
129
130
131    def get_status(self) -> dict:
132        '''Executes a health status request to the Kepware instance to report service statuses.
133
134        :return: List of data for the health status request
135
136        :raises KepHTTPError: If urllib provides an HTTPError
137        :raises KepURLError: If urllib provides an URLError
138        '''
139
140        r = self._config_get(f'{self.url}/status')
141        return r.payload
142    def get_info(self) -> dict:
143        '''Requests product information from the Kepware instance. Provides various information including
144        product name and version information.
145
146        :return: dict of data for the product information request
147        
148        :raises KepHTTPError: If urllib provides an HTTPError
149        :raises KepURLError: If urllib provides an URLError
150        '''
151        
152        r = self._config_get(f'{self.url}/about')
153        return r.payload
154
155    def reinitialize(self, job_ttl: int = None) -> KepServiceResponse:
156        '''Executes a Reinitialize service call to the Kepware instance.
157
158        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
159
160        :return: `KepServiceResponse` instance with job information
161
162        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
163        :raises KepURLError: If urllib provides an URLError
164        '''
165        url = self.url + self.__project_services_url + '/ReinitializeRuntime' 
166        try:
167            job = self._kep_service_execute(url, None, job_ttl)
168            return job
169        except Exception as err:
170            raise err
171        
172    def get_transaction_log(self, limit: int = None, start: datetime.datetime = None, end: datetime.datetime = None) -> list:
173        ''' Get the Transaction Log from the Kepware instance.
174
175        :param limit: *(optional)* number of transaction log entries to request
176        :param start: *(optional)* start time of query as `datetime.datetime` type and should be UTC
177        :param end: *(optional)* end time of query as `datetime.datetime` type and should be UTC
178
179        :raises KepHTTPError: If urllib provides an HTTPError
180        :raises KepURLError: If urllib provides an URLError
181        '''
182        query = self.__create_query(start, end, limit)
183        url = f'{self.url}{self.__trans_log_url}'
184        r = self._config_get(url, params= query)
185        return r.payload
186
187    def get_event_log(self, limit: int = None, start: datetime.datetime = None, end: datetime.datetime = None, *, options: dict = None) -> list:
188        ''' Get the Event Log from the Kepware instance.
189
190        :param limit: *(optional)* number of event log entries to request
191        :param start: *(optional)* start time of query as `datetime.datetime` type and should be UTC
192        :param end: *(optional)* end time of query as `datetime.datetime` type and should be UTC
193        :param options: *(optional)* Dict of parameters to filter, sort or pagenate the list of transactions. Options are `event`, 
194        `sortOrder`, `sortProperty`, `pageNumber`, and `pageSize`
195
196        :raises KepHTTPError: If urllib provides an HTTPError
197        :raises KepURLError: If urllib provides an URLError
198        '''
199        query = self.__create_query(start, end, limit)
200        if options is not None:
201            query = {**query, **options}
202        url = f'{self.url}{self.__event_log_url}'
203        r = self._config_get(url, params= query)
204        return r.payload
205
206    def get_audit_log(self, limit: int = None, *, filters: list[Filter] = None, options: dict = None) -> list:
207        ''' Get the Audit Log from the Kepware instance.
208
209        :param limit: *(optional)* number of event log entries to request
210        :param filters: *(optional)* list of filters that are used to control results returned from the log
211        :param options: *(optional)* Dict of parameters to filter, sort or pagenate the list of transactions. Options are `sortOrder`, 
212        `sortProperty`, `pageNumber`, and `pageSize`
213
214        :raises KepHTTPError: If urllib provides an HTTPError
215        :raises KepURLError: If urllib provides an URLError
216        '''
217        query = self.__create_filter_query(filters)
218        if limit is not None:
219            query['limit'] = limit
220        if options is not None:
221            query = {**query, **options}
222        url = f'{self.url}{self.__audit_log_url}'
223        r = self._config_get(url, params= query)
224        return r.payload
225    
226    def get_project_properties(self) -> dict:
227        ''' Get the Project Properties of the Kepware instance.
228        
229        :return: Dict of all the project properties
230
231        :raises KepHTTPError: If urllib provides an HTTPError
232        :raises KepURLError: If urllib provides an URLError
233        '''
234
235        r = self._config_get(self.url + '/project')
236        return r.payload
237    
238    def modify_project_properties(self, DATA: dict, force: bool = False) -> bool:
239        ''' Modify the Project Properties of the Kepware instance.
240
241        :param DATA: Dict of the project properties to be modified
242        :param force: *(optional)* if True, will force the configuration update to the Kepware server
243        
244        :return: True - If a "HTTP 200 - OK" is received from Kepware server
245
246        :raises KepHTTPError: If urllib provides an HTTPError
247        :raises KepURLError: If urllib provides an URLError
248        '''
249
250        prop_data = self._force_update_check(force, DATA)
251        r = self._config_update(self.url + '/project', prop_data)
252        if r.code == 200: return True 
253        else: raise KepHTTPError(r.url, r.code, r.msg, r.hdrs, r.payload)
254    
255    def import_empty_project(self) -> KepServiceResponse:
256        '''Executes JsonProjectLoad Service call to the Kepware instance with an empty project. This service 
257        imports an empty project configuration, acts like a FILE->NEW action and 
258        stop communications while the new project replaces the current project in the Kepware runtime. 
259
260        :return: `KepServiceResponse` instance with job information
261        
262        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
263        :raises KepURLError: If urllib provides an URLError
264        '''
265        return self.import_project_configuration({"project":{}})
266
267
268    def import_project_configuration(self, DATA: dict) -> KepServiceResponse:
269        '''Executes JsonProjectLoad Service call to the Kepware instance. This service imports project configuration 
270        data, expecting a complete project file in JSON/dict format. This service acts like a FILE->OPEN action and 
271        stop communications while the new project replaces the current project in the Kepware runtime. 
272    
273        :param DATA: Complete project configuration data in JSON/dict format. 
274
275        :return: `KepServiceResponse` instance with job information
276        
277        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
278        :raises KepURLError: If urllib provides an URLError
279        '''
280        url = self.url + self.__project_services_url + '/JsonProjectLoad'
281        try:
282            job = self._kep_service_execute(url, DATA)
283            return job
284        except Exception as err:
285            raise err
286        
287    def export_project_configuration(self) -> dict:
288        '''Get a complete copy of the project configuration in JSON format. This will include the same 
289        configuration that is stored when you save the project file manually.
290
291        :return: Dict of the complete project configuration
292
293        :raises KepHTTPError: If urllib provides an HTTPError
294        :raises KepURLError: If urllib provides an URLError
295        '''
296        r = self._config_get(self.url + '/project', params= {"content": "serialize"})
297        return r.payload
298    
299    def save_project(self, filename: str, password: str = None, job_ttl: int = None) -> KepServiceResponse:
300        '''Executes a ProjectSave Service call to the Kepware instance. This saves 
301        a copy of the current project file to disk. The filename
302
303        :param filename: Relative file path and name of project file including the file extension to save.
304        Location of relative project file paths:
305         
306                TKS or KEP (Windows): C:\\PROGRAMDATA\\PTC\\Thingworx Kepware Server\\V6 or 
307                                C:\\PROGRAMDATA\\Kepware\\KEPServerEX\\V6
308                TKE (Linux):    /opt/tkedge/v1/user_data
309
310
311        :param password: *(optional)* Specify a password with which to  save an encrypted project file with.  
312            This password will be required to load this project file.
313        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
314
315        :return: `KepServiceResponse` instance with job information
316
317        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
318        :raises KepURLError: If urllib provides an URLError
319        '''
320        url = self.url + self.__project_services_url + '/ProjectSave'
321        prop_data = {'servermain.PROJECT_FILENAME': filename}
322        if password != None: prop_data['servermain.PROJECT_PASSWORD'] = password
323        try:
324            job = self._kep_service_execute(url, prop_data, job_ttl)
325            return job
326        except Exception as err:
327            raise err
328
329    def load_project(self, filename: str, password: str = None, job_ttl: int = None) -> KepServiceResponse:
330        '''Executes a ProjectLoad Service call to the Kepware instance. This loads 
331        a project file to disk.
332
333        INPUTS:
334        
335        :param filename: Fully qualified or relative path and name of project file including the file extension. Absolute
336        paths required for TKS and KEP while file path is relative for TKE:
337
338                Windows - filename = C:\\filepath\\test.opf
339                Linux - filename = filepath/test.lpf - Location is /opt/tkedge/v1/user_data/filepath/test.lpf
340
341        :param password: *(optional)* Specify a password with which to load an encrypted project file.          
342        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
343
344        :return: `KepServiceResponse` instance with job information
345
346        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
347        :raises KepURLError: If urllib provides an URLError
348        '''
349        url = self.url + self.__project_services_url + '/ProjectLoad'
350        prop_data = {'servermain.PROJECT_FILENAME': filename}
351        if password != None: prop_data['servermain.PROJECT_PASSWORD'] = password
352        try:
353            job = self._kep_service_execute(url, prop_data, job_ttl)
354            return job
355        except Exception as err:
356            raise err
357
358    def get_project_backup_info(self) -> dict:
359        ''' Get the Project Backup Information of the Kepware instance.
360        
361        :return: List of all the backup projects and their properties.
362
363        :raises KepHTTPError: If urllib provides an HTTPError
364        :raises KepURLError: If urllib provides an URLError
365        '''
366
367        r = self._config_get(self.url + '/project/backups')
368        return r.payload
369
370    def backup_project(self, job_ttl: int = None) -> KepServiceResponse:
371        '''Executes a CreateBackup Service call to the Kepware instance. This saves 
372        a copy of the current project file to disk as a backup that can be retrieved.
373
374        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
375
376        :return: `KepServiceResponse` instance with job information
377
378        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
379        :raises KepURLError: If urllib provides an URLError
380        '''
381        url = self.url + self.__project_services_url + '/CreateBackup'
382        try:
383            job = self._kep_service_execute(url, TTL= job_ttl)
384            return job
385        except Exception as err:
386            raise err
387        
388    def service_status(self, resp: KepServiceResponse):
389        '''Returns the status of a service job. Used to verify if a service call
390        has completed or not.
391
392        :param resp: `KepServiceResponse` instance with job information
393
394        :return: `KepServiceStatus` instance with job status
395
396        :raises KepHTTPError: If urllib provides an HTTPError
397        :raises KepURLError: If urllib provides an URLError
398        '''
399        # need to remove part of job href
400        loc = resp.href.find(self.__root_url + self.__version_url)
401        job_url = resp.href[loc + len(self.__root_url + self.__version_url):]
402
403        r = self._config_get(self.url + job_url)
404        job = KepServiceStatus(r.payload['servermain.JOB_COMPLETE'],r.payload['servermain.JOB_STATUS'], r.payload['servermain.JOB_STATUS_MSG'])
405        return job
406
407
408    #Function used to Add an object to Kepware (HTTP POST)
409    def _config_add(self, url, DATA):
410        '''Conducts an POST method at *url* to add an object in the Kepware Configuration
411        *DATA* is required to be a properly JSON object (dict) of the item to be posted to *url* 
412        '''
413        if len(DATA) == 0:
414            err_msg = f'Error: Empty List or Dict in DATA | DATA type: {type(DATA)}'
415            raise KepError(err_msg) 
416        data = json.dumps(DATA).encode('utf-8')
417        url_obj = self.__url_validate(url)
418        q = request.Request(url_obj, data, method='POST')
419        r = self.__connect(q)
420        return r
421
422    #Function used to del an object to Kepware (HTTP DELETE)
423    def _config_del(self, url):
424        '''Conducts an DELETE method at *url* to delete an object in the Kepware Configuration'''
425        url_obj = self.__url_validate(url)
426        q = request.Request(url_obj, method='DELETE')
427        r = self.__connect(q)
428        return r
429
430    #Function used to Update an object to Kepware (HTTP PUT)
431    def _config_update(self, url, DATA = None):
432        '''Conducts an PUT method at *url* to modify an object in the Kepware Configuration.
433        *DATA* is required to be a properly JSON object (dict) of the item to be put to *url*
434        '''
435        url_obj = self.__url_validate(url)
436        if DATA == None:            
437            q = request.Request(url_obj, method='PUT')
438        else:
439            data = json.dumps(DATA).encode('utf-8')
440            q = request.Request(url_obj, data, method='PUT')
441        r = self.__connect(q)
442        return r
443
444    #Function used to Read an object from Kepware (HTTP GET) and return the JSON response
445    def _config_get(self, url, *, params = None):
446        '''
447        Conducts an GET method at *url* to retrieve an objects properties with query parameters in 
448        the Kepware Configuration.
449        '''
450        # Add parameters when necessary
451        if params is not None and params != {}:
452            qparams = parse.urlencode(params)
453            url = f'{url}?{qparams}'
454        url_obj = self.__url_validate(url)
455        q = request.Request(url_obj, method='GET')
456        r = self.__connect(q)
457        return r
458
459    
460    def _force_update_check(self, force, DATA):
461        '''
462        This will validate if the modify call needs to be forced or not. If forced, the dict DATA needs
463        to have the 'FORCE_UPDATE' property with a value of True. If forced is not requested, it is necessary
464        to provide the current 'PROJECT_ID'. If 'PROJECT_ID' is not present in DATA, this will automatically 
465        retreive it from the active server.
466        '''
467        if force == True:
468            DATA['FORCE_UPDATE'] = True
469        else:
470            # Get Project ID if it doesn't exist and if FORCE_UPDATE is existing and FALSE
471            if 'PROJECT_ID' not in DATA:
472                if 'FORCE_UPDATE' in DATA:
473                    if 'FORCE_UPDATE' == False:
474                        try:
475                            project_data = self._config_get(self.url + '/project')
476                            DATA['PROJECT_ID'] = project_data.payload['PROJECT_ID']
477                        except:
478                            #NEED TO COVER ERROR CONDITION
479                            pass
480                else:
481                    try:
482                        project_data = self._config_get(self.url + '/project')
483                        DATA['PROJECT_ID'] = project_data.payload['PROJECT_ID']
484                    except:
485                        #NEED TO COVER ERROR CONDITION
486                        pass
487        return DATA
488    # General service call handler
489    def _kep_service_execute(self, url, DATA = None, TTL = None):
490        try:
491            if TTL != None:
492                if DATA == None: DATA = {}
493                DATA["servermain.JOB_TIME_TO_LIVE_SECONDS"]= TTL
494            r = self._config_update(url, DATA)
495            job = KepServiceResponse(r.payload['code'],r.payload['message'], r.payload['href'])
496            return job
497        except KepHTTPError as err:
498            if err.code == 429:
499                job = KepServiceResponse()
500                job.code = err.code
501                job.message = err.payload
502                return job
503            else:
504                raise err
505
506# 
507# Supporting Functions
508#
509
510    # General connect call to manage HTTP responses for all methods
511    # Returns the response object for the method to handle as appropriate
512    # Raises Errors as found
513    def __connect(self,request_obj):
514        # Fill appropriate header information
515        data = _HttpDataAbstract()
516        request_obj.add_header("Authorization", "Basic %s" % self.__build_auth_str(self.username, self.password))
517        request_obj.add_header("Content-Type", "application/json")
518        request_obj.add_header("Accept", "application/json")
519        try:
520            # context is sent regardless of HTTP or HTTPS - seems to be ignored if HTTP URL
521            with request.urlopen(request_obj, context=self.__ssl_context) as server:
522                try:
523                    payload = server.read()
524                    data.payload = json.loads(codecs.decode(payload,'utf-8-sig'))
525                except:
526                    pass
527                data.code = server.code
528                data.reason = server.reason
529                return data
530        except error.HTTPError as err:
531            payload = json.loads(codecs.decode(err.read(),'utf-8-sig'))
532            # print('HTTP Code: {}\n{}'.format(err.code,payload), file=sys.stderr)
533            raise KepHTTPError(url=err.url, code=err.code, msg=err.msg, hdrs=err.hdrs, payload=payload)
534        except error.URLError as err:
535            # print('URLError: {} URL: {}'.format(err.reason, request_obj.get_full_url()), file=sys.stderr)
536            raise KepURLError(msg=err.reason, url=request_obj.get_full_url())
537
538    # Fucntion used to ensure special characters are handled in the URL
539    # Ex: Space will be turned to %20
540    def __url_validate(self, url):
541        # Configuration API does not use fragments in URL so ignore to allow # as a character
542        # Objects in Kepware can include # as part of the object names
543        parsed_url = parse.urlparse(url, allow_fragments= False)
544        # Added % for scenarios where special characters have already been escaped with %
545        updated_path = parse.quote(parsed_url.path, safe = '/%')
546
547        # If host is "localhost", force using the IPv4 loopback adapter IP address in all calls
548        # This is done to remove retries that will happen when the host resolution uses IPv6 intially
549        # Kepware currently doesn't support IPv6 and is not listening on this interface
550        if parsed_url.hostname.lower() == 'localhost':
551            ip = socket.gethostbyname(parsed_url.hostname)
552            parsed_url = parsed_url._replace(netloc='{}:{}'.format(ip, parsed_url.port))
553        
554        return parsed_url._replace(path=updated_path).geturl()
555
556    # Function used to build the basic authentication string
557    def __build_auth_str(self, username, password):
558        if isinstance(username, str):
559            username = username.encode('latin1')
560        if isinstance(password, str):
561            password = password.encode('latin1')
562        authstr = b64encode(b':'.join((username, password))).strip().decode('ascii')
563        return authstr
564    
565    # Create parameters for log queries
566    def __create_query(self, start = None, end = None, limit = None):
567        query = {}
568        if start != None and isinstance(start, datetime.datetime):
569            query['start'] = start.isoformat()
570        if end != None and isinstance(end, datetime.datetime):
571            query['end'] = end.isoformat()
572        if limit != None:
573            query['limit'] = limit
574        return query
575
576    # Create filter query for log queries
577    def __create_filter_query(self, filters: list[Filter] = None):
578        query = {}
579        if filters is None:
580            return query
581        for f in filters:
582            # Ensure we use the value of the Enum, not the Enum object itself
583            key = f"filter[{f.field.value}][{f.modifier.value}]"
584            query[key] = f.value
585        return query

A class to represent a connection to an instance of Kepware. This object is used to create the Authentication and server parameters to taget a Kepware instance. An instance of this is used in all configuration calls done.

Parameters
  • host: host name or IP address
  • port: port of Configuration API
  • username: username to conduct "Basic Authentication"
  • password: password to conduct "Basic Authentication"
  • https: Sets SSL_on to use HTTPS connection (Default: False)
  • SSL_on: Identify to use HTTPS connection (Default: False)
  • SSL_ignore_hostname: During certificate validation ignore the hostname check
  • SSL_trust_all_certs: (insecure) - During certificate validation trust any certificate - if True, will "set SSL_ignore_hostname" to true
  • url: base URL for the server connection

Methods

reinitialize(): reinitialize the Kepware server

get_transaction_log(): retrieve the Configuration API transaction logs

get_event_log(): retrieve the Kepware Event Log

get_audit_log(): retrieve the Kepware Audit Log

get_info(): retrieve the Kepware product information

import_empty_project(): import an empty project to the Kepware server

get_project_properties(): retrieve the Kepware Project Properties

modify_project_properties() - modify the Kepware Project Properties

service_status() - retrive service job status

export_project_configuration() - export the current project configuration in JSON format

save_project() - save the current project to a file

load_project() - load a project from a file

server(host: str, port: int, user: str, pw: str, https: bool = False)
74    def __init__(self,  host: str, port: int, user: str, pw: str, https: bool = False):
75        self.host = host
76        self.port = port
77        self.username = user
78        self.password = pw
79        self.__ssl_context = ssl.create_default_context()
80        self.__SSL_on = https
host
port
username
password
url
SSL_on
SSL_ignore_hostname
SSL_trust_all_certs
def get_status(self) -> dict:
131    def get_status(self) -> dict:
132        '''Executes a health status request to the Kepware instance to report service statuses.
133
134        :return: List of data for the health status request
135
136        :raises KepHTTPError: If urllib provides an HTTPError
137        :raises KepURLError: If urllib provides an URLError
138        '''
139
140        r = self._config_get(f'{self.url}/status')
141        return r.payload

Executes a health status request to the Kepware instance to report service statuses.

Returns

List of data for the health status request

Raises
  • KepHTTPError: If urllib provides an HTTPError
  • KepURLError: If urllib provides an URLError
def get_info(self) -> dict:
142    def get_info(self) -> dict:
143        '''Requests product information from the Kepware instance. Provides various information including
144        product name and version information.
145
146        :return: dict of data for the product information request
147        
148        :raises KepHTTPError: If urllib provides an HTTPError
149        :raises KepURLError: If urllib provides an URLError
150        '''
151        
152        r = self._config_get(f'{self.url}/about')
153        return r.payload

Requests product information from the Kepware instance. Provides various information including product name and version information.

Returns

dict of data for the product information request

Raises
  • KepHTTPError: If urllib provides an HTTPError
  • KepURLError: If urllib provides an URLError
def reinitialize(self, job_ttl: int = None) -> kepconfig.structures.KepServiceResponse:
155    def reinitialize(self, job_ttl: int = None) -> KepServiceResponse:
156        '''Executes a Reinitialize service call to the Kepware instance.
157
158        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
159
160        :return: `KepServiceResponse` instance with job information
161
162        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
163        :raises KepURLError: If urllib provides an URLError
164        '''
165        url = self.url + self.__project_services_url + '/ReinitializeRuntime' 
166        try:
167            job = self._kep_service_execute(url, None, job_ttl)
168            return job
169        except Exception as err:
170            raise err

Executes a Reinitialize service call to the Kepware instance.

Parameters
  • job_ttl: (optional) Determines the number of seconds a job instance will exist following completion.
Returns

KepServiceResponse instance with job information

Raises
  • KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
  • KepURLError: If urllib provides an URLError
def get_transaction_log( self, limit: int = None, start: datetime.datetime = None, end: datetime.datetime = None) -> list:
172    def get_transaction_log(self, limit: int = None, start: datetime.datetime = None, end: datetime.datetime = None) -> list:
173        ''' Get the Transaction Log from the Kepware instance.
174
175        :param limit: *(optional)* number of transaction log entries to request
176        :param start: *(optional)* start time of query as `datetime.datetime` type and should be UTC
177        :param end: *(optional)* end time of query as `datetime.datetime` type and should be UTC
178
179        :raises KepHTTPError: If urllib provides an HTTPError
180        :raises KepURLError: If urllib provides an URLError
181        '''
182        query = self.__create_query(start, end, limit)
183        url = f'{self.url}{self.__trans_log_url}'
184        r = self._config_get(url, params= query)
185        return r.payload

Get the Transaction Log from the Kepware instance.

Parameters
  • limit: (optional) number of transaction log entries to request
  • start: (optional) start time of query as datetime.datetime type and should be UTC
  • end: (optional) end time of query as datetime.datetime type and should be UTC
Raises
  • KepHTTPError: If urllib provides an HTTPError
  • KepURLError: If urllib provides an URLError
def get_event_log( self, limit: int = None, start: datetime.datetime = None, end: datetime.datetime = None, *, options: dict = None) -> list:
187    def get_event_log(self, limit: int = None, start: datetime.datetime = None, end: datetime.datetime = None, *, options: dict = None) -> list:
188        ''' Get the Event Log from the Kepware instance.
189
190        :param limit: *(optional)* number of event log entries to request
191        :param start: *(optional)* start time of query as `datetime.datetime` type and should be UTC
192        :param end: *(optional)* end time of query as `datetime.datetime` type and should be UTC
193        :param options: *(optional)* Dict of parameters to filter, sort or pagenate the list of transactions. Options are `event`, 
194        `sortOrder`, `sortProperty`, `pageNumber`, and `pageSize`
195
196        :raises KepHTTPError: If urllib provides an HTTPError
197        :raises KepURLError: If urllib provides an URLError
198        '''
199        query = self.__create_query(start, end, limit)
200        if options is not None:
201            query = {**query, **options}
202        url = f'{self.url}{self.__event_log_url}'
203        r = self._config_get(url, params= query)
204        return r.payload

Get the Event Log from the Kepware instance.

Parameters
  • limit: (optional) number of event log entries to request
  • start: (optional) start time of query as datetime.datetime type and should be UTC
  • end: (optional) end time of query as datetime.datetime type and should be UTC
  • options: (optional) Dict of parameters to filter, sort or pagenate the list of transactions. Options are event, sortOrder, sortProperty, pageNumber, and pageSize
Raises
  • KepHTTPError: If urllib provides an HTTPError
  • KepURLError: If urllib provides an URLError
def get_audit_log( self, limit: int = None, *, filters: list[kepconfig.structures.Filter] = None, options: dict = None) -> list:
206    def get_audit_log(self, limit: int = None, *, filters: list[Filter] = None, options: dict = None) -> list:
207        ''' Get the Audit Log from the Kepware instance.
208
209        :param limit: *(optional)* number of event log entries to request
210        :param filters: *(optional)* list of filters that are used to control results returned from the log
211        :param options: *(optional)* Dict of parameters to filter, sort or pagenate the list of transactions. Options are `sortOrder`, 
212        `sortProperty`, `pageNumber`, and `pageSize`
213
214        :raises KepHTTPError: If urllib provides an HTTPError
215        :raises KepURLError: If urllib provides an URLError
216        '''
217        query = self.__create_filter_query(filters)
218        if limit is not None:
219            query['limit'] = limit
220        if options is not None:
221            query = {**query, **options}
222        url = f'{self.url}{self.__audit_log_url}'
223        r = self._config_get(url, params= query)
224        return r.payload

Get the Audit Log from the Kepware instance.

Parameters
  • limit: (optional) number of event log entries to request
  • filters: (optional) list of filters that are used to control results returned from the log
  • options: (optional) Dict of parameters to filter, sort or pagenate the list of transactions. Options are sortOrder, sortProperty, pageNumber, and pageSize
Raises
  • KepHTTPError: If urllib provides an HTTPError
  • KepURLError: If urllib provides an URLError
def get_project_properties(self) -> dict:
226    def get_project_properties(self) -> dict:
227        ''' Get the Project Properties of the Kepware instance.
228        
229        :return: Dict of all the project properties
230
231        :raises KepHTTPError: If urllib provides an HTTPError
232        :raises KepURLError: If urllib provides an URLError
233        '''
234
235        r = self._config_get(self.url + '/project')
236        return r.payload

Get the Project Properties of the Kepware instance.

Returns

Dict of all the project properties

Raises
  • KepHTTPError: If urllib provides an HTTPError
  • KepURLError: If urllib provides an URLError
def modify_project_properties(self, DATA: dict, force: bool = False) -> bool:
238    def modify_project_properties(self, DATA: dict, force: bool = False) -> bool:
239        ''' Modify the Project Properties of the Kepware instance.
240
241        :param DATA: Dict of the project properties to be modified
242        :param force: *(optional)* if True, will force the configuration update to the Kepware server
243        
244        :return: True - If a "HTTP 200 - OK" is received from Kepware server
245
246        :raises KepHTTPError: If urllib provides an HTTPError
247        :raises KepURLError: If urllib provides an URLError
248        '''
249
250        prop_data = self._force_update_check(force, DATA)
251        r = self._config_update(self.url + '/project', prop_data)
252        if r.code == 200: return True 
253        else: raise KepHTTPError(r.url, r.code, r.msg, r.hdrs, r.payload)

Modify the Project Properties of the Kepware instance.

Parameters
  • DATA: Dict of the project properties to be modified
  • force: (optional) if True, will force the configuration update to the Kepware server
Returns

True - If a "HTTP 200 - OK" is received from Kepware server

Raises
  • KepHTTPError: If urllib provides an HTTPError
  • KepURLError: If urllib provides an URLError
def import_empty_project(self) -> kepconfig.structures.KepServiceResponse:
255    def import_empty_project(self) -> KepServiceResponse:
256        '''Executes JsonProjectLoad Service call to the Kepware instance with an empty project. This service 
257        imports an empty project configuration, acts like a FILE->NEW action and 
258        stop communications while the new project replaces the current project in the Kepware runtime. 
259
260        :return: `KepServiceResponse` instance with job information
261        
262        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
263        :raises KepURLError: If urllib provides an URLError
264        '''
265        return self.import_project_configuration({"project":{}})

Executes JsonProjectLoad Service call to the Kepware instance with an empty project. This service imports an empty project configuration, acts like a FILE->NEW action and stop communications while the new project replaces the current project in the Kepware runtime.

Returns

KepServiceResponse instance with job information

Raises
  • KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
  • KepURLError: If urllib provides an URLError
def import_project_configuration(self, DATA: dict) -> kepconfig.structures.KepServiceResponse:
268    def import_project_configuration(self, DATA: dict) -> KepServiceResponse:
269        '''Executes JsonProjectLoad Service call to the Kepware instance. This service imports project configuration 
270        data, expecting a complete project file in JSON/dict format. This service acts like a FILE->OPEN action and 
271        stop communications while the new project replaces the current project in the Kepware runtime. 
272    
273        :param DATA: Complete project configuration data in JSON/dict format. 
274
275        :return: `KepServiceResponse` instance with job information
276        
277        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
278        :raises KepURLError: If urllib provides an URLError
279        '''
280        url = self.url + self.__project_services_url + '/JsonProjectLoad'
281        try:
282            job = self._kep_service_execute(url, DATA)
283            return job
284        except Exception as err:
285            raise err

Executes JsonProjectLoad Service call to the Kepware instance. This service imports project configuration data, expecting a complete project file in JSON/dict format. This service acts like a FILE->OPEN action and stop communications while the new project replaces the current project in the Kepware runtime.

Parameters
  • DATA: Complete project configuration data in JSON/dict format.
Returns

KepServiceResponse instance with job information

Raises
  • KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
  • KepURLError: If urllib provides an URLError
def export_project_configuration(self) -> dict:
287    def export_project_configuration(self) -> dict:
288        '''Get a complete copy of the project configuration in JSON format. This will include the same 
289        configuration that is stored when you save the project file manually.
290
291        :return: Dict of the complete project configuration
292
293        :raises KepHTTPError: If urllib provides an HTTPError
294        :raises KepURLError: If urllib provides an URLError
295        '''
296        r = self._config_get(self.url + '/project', params= {"content": "serialize"})
297        return r.payload

Get a complete copy of the project configuration in JSON format. This will include the same configuration that is stored when you save the project file manually.

Returns

Dict of the complete project configuration

Raises
  • KepHTTPError: If urllib provides an HTTPError
  • KepURLError: If urllib provides an URLError
def save_project( self, filename: str, password: str = None, job_ttl: int = None) -> kepconfig.structures.KepServiceResponse:
299    def save_project(self, filename: str, password: str = None, job_ttl: int = None) -> KepServiceResponse:
300        '''Executes a ProjectSave Service call to the Kepware instance. This saves 
301        a copy of the current project file to disk. The filename
302
303        :param filename: Relative file path and name of project file including the file extension to save.
304        Location of relative project file paths:
305         
306                TKS or KEP (Windows): C:\\PROGRAMDATA\\PTC\\Thingworx Kepware Server\\V6 or 
307                                C:\\PROGRAMDATA\\Kepware\\KEPServerEX\\V6
308                TKE (Linux):    /opt/tkedge/v1/user_data
309
310
311        :param password: *(optional)* Specify a password with which to  save an encrypted project file with.  
312            This password will be required to load this project file.
313        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
314
315        :return: `KepServiceResponse` instance with job information
316
317        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
318        :raises KepURLError: If urllib provides an URLError
319        '''
320        url = self.url + self.__project_services_url + '/ProjectSave'
321        prop_data = {'servermain.PROJECT_FILENAME': filename}
322        if password != None: prop_data['servermain.PROJECT_PASSWORD'] = password
323        try:
324            job = self._kep_service_execute(url, prop_data, job_ttl)
325            return job
326        except Exception as err:
327            raise err

Executes a ProjectSave Service call to the Kepware instance. This saves a copy of the current project file to disk. The filename

Parameters
  • filename: Relative file path and name of project file including the file extension to save. Location of relative project file paths:

    TKS or KEP (Windows): C:\PROGRAMDATA\PTC\Thingworx Kepware Server\V6 or 
                    C:\PROGRAMDATA\Kepware\KEPServerEX\V6
    TKE (Linux):    /opt/tkedge/v1/user_data
    
  • password: (optional) Specify a password with which to save an encrypted project file with.
    This password will be required to load this project file.

  • job_ttl: (optional) Determines the number of seconds a job instance will exist following completion.
Returns

KepServiceResponse instance with job information

Raises
  • KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
  • KepURLError: If urllib provides an URLError
def load_project( self, filename: str, password: str = None, job_ttl: int = None) -> kepconfig.structures.KepServiceResponse:
329    def load_project(self, filename: str, password: str = None, job_ttl: int = None) -> KepServiceResponse:
330        '''Executes a ProjectLoad Service call to the Kepware instance. This loads 
331        a project file to disk.
332
333        INPUTS:
334        
335        :param filename: Fully qualified or relative path and name of project file including the file extension. Absolute
336        paths required for TKS and KEP while file path is relative for TKE:
337
338                Windows - filename = C:\\filepath\\test.opf
339                Linux - filename = filepath/test.lpf - Location is /opt/tkedge/v1/user_data/filepath/test.lpf
340
341        :param password: *(optional)* Specify a password with which to load an encrypted project file.          
342        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
343
344        :return: `KepServiceResponse` instance with job information
345
346        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
347        :raises KepURLError: If urllib provides an URLError
348        '''
349        url = self.url + self.__project_services_url + '/ProjectLoad'
350        prop_data = {'servermain.PROJECT_FILENAME': filename}
351        if password != None: prop_data['servermain.PROJECT_PASSWORD'] = password
352        try:
353            job = self._kep_service_execute(url, prop_data, job_ttl)
354            return job
355        except Exception as err:
356            raise err

Executes a ProjectLoad Service call to the Kepware instance. This loads a project file to disk.

INPUTS:

Parameters
  • filename: Fully qualified or relative path and name of project file including the file extension. Absolute paths required for TKS and KEP while file path is relative for TKE:

    Windows - filename = C:\filepath\test.opf
    Linux - filename = filepath/test.lpf - Location is /opt/tkedge/v1/user_data/filepath/test.lpf
    
  • password: (optional) Specify a password with which to load an encrypted project file.

  • job_ttl: (optional) Determines the number of seconds a job instance will exist following completion.
Returns

KepServiceResponse instance with job information

Raises
  • KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
  • KepURLError: If urllib provides an URLError
def get_project_backup_info(self) -> dict:
358    def get_project_backup_info(self) -> dict:
359        ''' Get the Project Backup Information of the Kepware instance.
360        
361        :return: List of all the backup projects and their properties.
362
363        :raises KepHTTPError: If urllib provides an HTTPError
364        :raises KepURLError: If urllib provides an URLError
365        '''
366
367        r = self._config_get(self.url + '/project/backups')
368        return r.payload

Get the Project Backup Information of the Kepware instance.

Returns

List of all the backup projects and their properties.

Raises
  • KepHTTPError: If urllib provides an HTTPError
  • KepURLError: If urllib provides an URLError
def backup_project(self, job_ttl: int = None) -> kepconfig.structures.KepServiceResponse:
370    def backup_project(self, job_ttl: int = None) -> KepServiceResponse:
371        '''Executes a CreateBackup Service call to the Kepware instance. This saves 
372        a copy of the current project file to disk as a backup that can be retrieved.
373
374        :param job_ttl: *(optional)* Determines the number of seconds a job instance will exist following completion.
375
376        :return: `KepServiceResponse` instance with job information
377
378        :raises KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
379        :raises KepURLError: If urllib provides an URLError
380        '''
381        url = self.url + self.__project_services_url + '/CreateBackup'
382        try:
383            job = self._kep_service_execute(url, TTL= job_ttl)
384            return job
385        except Exception as err:
386            raise err

Executes a CreateBackup Service call to the Kepware instance. This saves a copy of the current project file to disk as a backup that can be retrieved.

Parameters
  • job_ttl: (optional) Determines the number of seconds a job instance will exist following completion.
Returns

KepServiceResponse instance with job information

Raises
  • KepHTTPError: If urllib provides an HTTPError (If not HTTP code 202 [Accepted] or 429 [Too Busy] returned)
  • KepURLError: If urllib provides an URLError
def service_status(self, resp: kepconfig.structures.KepServiceResponse):
388    def service_status(self, resp: KepServiceResponse):
389        '''Returns the status of a service job. Used to verify if a service call
390        has completed or not.
391
392        :param resp: `KepServiceResponse` instance with job information
393
394        :return: `KepServiceStatus` instance with job status
395
396        :raises KepHTTPError: If urllib provides an HTTPError
397        :raises KepURLError: If urllib provides an URLError
398        '''
399        # need to remove part of job href
400        loc = resp.href.find(self.__root_url + self.__version_url)
401        job_url = resp.href[loc + len(self.__root_url + self.__version_url):]
402
403        r = self._config_get(self.url + job_url)
404        job = KepServiceStatus(r.payload['servermain.JOB_COMPLETE'],r.payload['servermain.JOB_STATUS'], r.payload['servermain.JOB_STATUS_MSG'])
405        return job

Returns the status of a service job. Used to verify if a service call has completed or not.

Parameters
  • resp: KepServiceResponse instance with job information
Returns

KepServiceStatus instance with job status

Raises
  • KepHTTPError: If urllib provides an HTTPError
  • KepURLError: If urllib provides an URLError