Package: avista-digital-exchange-sdk
Avista Digital Exchange SDKThis package allows you to access the Avista Digital Exchange and perform a subset of its features programmatically. All that you need is a personal authentication token that can be generated atenergy.collaboratives.io.PyPi listingGitHub RepositoryNote: This document contains HTML tags at each section header to support internal document references in the PyPi markdown viewer. Viewthis readme_renderer issuefor more information.Table of ContentsNote: Internal document links are not working when viewing this document in PyPi because of thisreadme_rendererissueAvista Digital Exchange SDKTable of ContentsGetting StartedAvistaDigitalExchange FunctionsgetUserInfodataStorelistDataStoresgetDataStoregetDataStoreDirectorygetDataStoreFileMetadownloadDataStoreFileuploadFileToDataStoredeleteDataStoreFileiotgetEndpointcreateEndpointcreateModellistEndpointLastValuesqueryByTimeRangepublishupdateEndpointPropertiesdataCapturestartCapturestopCapturepublishDatasubscribeToDataTypesUserOrganizationDataStoreMethodscdlspwduploadFiledownloadFiledeleteFileDataStoreDirectoryMethodsprintContentsDataStoreFileDigitalTwinModelModelPropertyModelTelemetryIotEndpointEndpointPropertyEndpointTelemetryDxTypes.CaptureDataRecordInputDxTypes.PublishCaptureDataResultDxTypes.PublishCaptureDataSuccessfulRecordDxTypes.PublishCaptureDataFailedRecordDxTypes.StartCaptureResultDxTypes.StopCaptureResultTroubleshootingDevelopmentDeploymentGenerating Python types and GraphQL Client from GraphQL SchemaFix for py-graphql-mapper modulewebsocketclient / websocket-client submoduleResourcesGetting StartedInstall the python package.pip3installavista-digital-exchange-sdkEnsure you have the latest version.pip3install--upgradeavista-digital-exchange-sdkImport the module in your python script.Initialize the module with your authentication token.  The authentication token can be a user authentication token, or an iot endpoint authentication token.  If an endpoint token is used, only iot operations can be performed.fromavista_digital_exchange_sdkimportAvistaDigitalExchangedigitalExchange=AvistaDigitalExchange("tokenvalue")# Alternatively, instantiate with debug mode enabled# digitalExchange = AvistaDigitalExchange("tokenvalue", True)AvistaDigitalExchange FunctionsgetUserInfoRetrieves your user information.ParametersNoneReturn TypeUserExampleuser=digitalExchange.getUserInfo()dataStorelistDataStoresLists the data stores you own.ParametersNoneReturn Type[DataStore]ExampledataStores=digitalExchange.dataStores.listDataStores()getDataStoreGets a data store object. The object can be interacted with as a command line utility to navigate the data store. SeeDataStore.ParametersdataStoreId :  str, required
    The id of the data store.Return TypeDataStoreExampledataStore=digitalExchange.dataStores.getDataStore("dataStoreId.1234")dataStore.ls()dataStore.cd("dirname1")getDataStoreDirectoryGets the directory metadata and contents.ParametersdataStoreDirectoryId :  str, required
    The id of the directory.Return TypeDataStoreDirectoryExampledir=digitalExchange.dataStores.getDataStoreDirectory("dataStoreDirectoryId.1234")getDataStoreFileMetaGets the metadata of a file.ParametersdataStoreFileId :  str, required
    The id of the file.Return TypeDataStoreFileExamplefile=digitalExchange.dataStores.getDataStoreFileMeta("dataStoreFileId.1234")downloadDataStoreFileDownloads a copy of the file to the local file system.ParametersdataStoreFileId :  str, required
    The id of the file.
writeLocation :  str, required
    Path where the file should be written. If writeLocation is a directory, the filename will be its original name.Return TypeDataStoreFileExamplefile=digitalExchange.dataStores.downloadDataStoreFile("dataStoreFileId.1234","./")uploadFileToDataStoreUploads a local file to a data store.ParametersdataStoreId :  str, required
    The id of the data store you are writing to.
dataStoreDirectoryId :  str, required
    The id of the directory to place the file in.
localPath :  str, required
    The path of the file to be uploaded.
name :  str, optional
    A name for the file if naming different than the local file name.
description :  str, optional
    A description of the file.Return TypeDataStoreFileExamplefile=digitalExchange.dataStores.uploadFileToDataStore("dataStoreId.1234","dataStoreDirectoryId.1234","./testFile.txt")deleteDataStoreFileRemoves a file from the data store and deletes it from the Digital Exchange.ParametersdataStoreFileId :  str, required
    The id of the file to delete.Return TypeDataStoreFileExamplefile=digitalExchange.dataStores.deleteDataStoreFile("dataStoreFileId.1234")iotgetEndpointGets information about the iot endpoint.  Includes the digital twin data model.ParametersiotEndpointId :  str, requiredReturn TypeIotEndpointExampleiotEndpointId="iotEndpointId.1234"endpoint=digitalExchange.iot.getEndpoint(iotEndpointId)forpropertyinendpoint.properties:print(f"Property{property.name}has type{property.schemaType}.")fortelemetryinendpoint.telemetry:print(f"Telemetry attribute{telemetry.name}has type{telemetry.schemaType}.")createEndpointCreates a new IoT Endpoint within an existing IoT Hub.ParametersiotHubId :  str, required
    The hub in which to create the endpoint in.
modelId :  str, required
    The digital twin model for this endpoint to use.
name :  str, required
    The name of the iot endpoint.
description :  str, optionalReturn TypeIotEndpointExampleiotHubId="iotHubId.1234"modelId="modelId.4321"name="Temperature Sensor"description="The sensor on top of the house"endpoint=digitalExchange.iot.createEndpoint(iotHubId,modelId,name,description)createModelCreates a digital twin data model.Parametersname :  str, required
    Name should not contain spaces or special characters. Alphanumeric characters only.
description :  str, optional
properties :  [dict], required
    Array of dictionaries representing the properties to associate with the model. See https://learn.microsoft.com/en-us/azure/digital-twins/concepts-models#:~:text=the%20following%20fields%3A-,Property,-%2D%20Properties%20are%20data for more information on model properties.
    The expected dictionary keys are as follows:
        - name : str, required
            Name of the property
        - description : str, optional
            Description of the property
        - schemaType : str, required
            The variable type of the property. Valid values are "integer", "double", "string", "boolean", "dateTime", and "duration"
        - defaultValue : str, optional
            Wrap the value, regardless of schemaType, in a string
        - writable : bool, optional
            Determines if the proeprty value can be updated. Default value is False

telemetry :  [dict], required
    Array of dictionaries representing the telemetry variables to associate with the model. See https://learn.microsoft.com/en-us/azure/digital-twins/concepts-models#:~:text=and%20telemetry%20below.-,Telemetry,-%2D%20Telemetry%20fields%20represent for more information on model telemetry.
    The expected dictionary keys are as follows:
        - name : str, required
            Name of the telemetry
        - description : str, optional
            Description of the telemetry
        - schemaType : str, required
            The variable type of the telemetry. Valid values are "integer", "double", "string", "boolean", "dateTime", and "duration"Return TypeDigitalTwinModelExampleproperties=[{"name":"Longitude","description":"the last known longitude of the device","schemaType":"double","defaultValue":"-117.426048","writable":True},{"name":"Latitude","description":"the last known latitude of the device","schemaType":"double","defaultValue":"47.658779","writable":True}]telemetry=[{"name":"Temperature","description":"Temp from the sensor in celsius","schemaType":"double"},{"name":"Humidity","description":"Humidity from the sensor","schemaType":"double"}]model=digitalExchange.iot.createModel("MyFirstModel","A simple example digital twin model",properties,telemetry)listEndpointLastValuesQueries for the last known values of the endpoint's attributes.ParametersiotEndpointId :  str, required
resultFileWriteLocation :  str, optional
    If you would like the results stored in a file, include a name or path here. 
    Results will be in JSON format.Return Type[dict][
    {
        'attributeName': 'name'
        'lastValue': 'value',
        'timestamp': 'time'
    }
]ExampleiotEndpointId="iotEndpointId.1234"result=digitalExchange.iot.listEndpointLastValues(iotEndpointId)# Example iteration through resultforentryinresult:attributeName=entry["attributeName"]lastValue=entry["lastValue"]timestamp=entry["timestamp"]queryByTimeRangeQueries the endpoint data using attribute and time filters, and writes the results to a file.You must provide a time range and the attributeNames that you wish to query. The method automatically
iterates over all pagination tokens so the result will be compiled upon completion. When the compiled result is available, it will be written to the local file system.ParametersiotEndpointId :  str, required
    The id of the endpoint.
attributeNames :  [str], required
    The attributes to include in the query result.
startTime :  ISO8601 str, required
    The beginning of the time interval.
    Example: "2022-09-27T10:22:45.000Z"
endTime: ISO8601 str, required
    The end of the time interval.
    Example: "2022-10-27T10:22:45.000Z"
resultFileWriteLocation: str, required
    The location to store the result file on the local file system. If a directory is provided but not a file name, the file will be saved as result.csv in the specified directory.Return TypeBool indicating success or errorExampleresult=digitalExchange.iot.queryByTimeRange("iotEndpointId.1234",["SOC","V","V - Setpoint"],"2022-09-25T20:13:04.465Z","2022-09-26T20:58:04.465Z","./")publishPublish telemetry values for an endpoint.  One or more data records can be written at one time.ParametersiotEndpointId :  str, required
    The id of the endpoint.
data :  [dict], required
    Array of data records to write.  Each dict should contain the record's timestamp (defaults to current milliseconds), timeUnit ("MILLISECONDS", "SECONDS", "MICROSECONDS", "NANOSECONDS", "ISO8601") ("MILLISECONDS" by default), and a dicitonary of attribute name, value pairs. Example shown below.
    ISO8601 timestamps should match format yyyy-MM-ddTHH:mm:ss.SSSZReturn TypeDictionary shown below{
    'recordsWritten': [
        {
            'timestamp': '1670008999', 
            'iotEndpointId': 'iotEndpointId.1234',
            'attributes': [
                {
                    'name': 'speed',
                    'value': '24.5'
                }
            ]
        }
    ],
    'recordsFailed': [
        {
            'record': {
                'timestamp': '1670007999',
                'iotEndpointId': 'iotEndpointId.1234',
                'attributes': [
                    {
                        'name': 'speed',
                        'value': '24.5'
                    }
                ]
            },
            'errors': [
                {
                    'errorType': 'DUPLICATE_RECORD',
                    'errorMessage': 'A record already exists for this endpoint at the given timestamp.'
                }
            ]
        }
    ]
}Example# Create the input data arrayinputData=[{"timestamp":1670008999,"timeUnit":"MILLISECONDS",# Optional - "MILLISECONDS" by default"attributes":{"speed":"40.2","altitude":"2413.0"}},{"timestamp":"2022-12-15T12:45:31.055Z","timeUnit":"ISO8601","attributes":{"speed":"40.2","altitude":"2413.0"}}]# Write the recordsresult=digitalExchange.iot.publish('iotEndpointId.1234',inputData)recordsWritten=result.recordsWrittenfailedRecords=result.failedRecordsupdateEndpointPropertiesUpdate the properties (state variables) for an endpoint.ParametersiotEndpointId :  str, required
    The id of the endpoint.
properties :  dict, required
    The properties to update and the new values. See example below for expected dict structure.Return TypeIotEndpointExampleproperties={'moving':False}digitalExchange.iot.updateEndpointProperties("iotEndpointId.1234",properties)dataCapturestartCaptureCommands a Data Capture to begin collecting data. Capture must be in 'Ready' state for startCapture to succeed.ParameterscaptureId :  str, required
    The id of the Data Capture.Return TypeDxTypes.StartCaptureResultExampleimportasynciofromavista_digital_exchange_sdkimportAvistaDigitalExchange# NOTE: The capture must be in 'READY' state to be started.captureId=CAPTURE_IDauthenticationToken=AUTHENTICATION_TOKENasyncdefmain():# Create an instance of the AvistaDigitalExchange SDK# You may use a user authentication token or the authentication token of the Data CapturedigitalExchange=AvistaDigitalExchange(authenticationToken)print("Instantiated AvistaDigitalExchange instance with authentication token")try:print("Calling dataCapture.startCapture")result=awaitdigitalExchange.dataCapture.startCapture(captureId=captureId)except:print("dataCapture.startCapture failed")if__name__=="__main__":asyncio.run(main())stopCaptureCommands a Data Capture to stop collecting data. Capture must be in 'CAPTURING' state for stopCapture to succeed.ParameterscaptureId :  str, required
    The id of the Data Capture.Return TypeDxTypes.StopCaptureResultExampleimportasynciofromavista_digital_exchange_sdkimportAvistaDigitalExchange# NOTE: The capture must be in 'CAPTURING' state to be stopped.captureId=CAPTURE_IDauthenticationToken=AUTHENTICATION_TOKENasyncdefmain():# Create an instance of the AvistaDigitalExchange SDK# You may use a user authentication token or the authentication token of the Data CapturedigitalExchange=AvistaDigitalExchange(authenticationToken)print("Instantiated AvistaDigitalExchange instance with authentication token")try:print("Calling dataCapture.stopCapture")result=awaitdigitalExchange.dataCapture.stopCapture(captureId=captureId)except:print("dataCapture.stopCapture failed")if__name__=="__main__":asyncio.run(main())publishDataPublish attribute values to an active Data Capture.ParameterscaptureId :  str, required
    The id of the Data Capture.
data :  [DxTypes.CaptureDataRecordInput], required
    Array of data records to write.  Each dict should contain the record's timestamp, timeUnit ("MILLISECONDS"), and a dicitonary of attributeId, value pairs. Example shown below.Return TypeDxTypes.PublishCaptureDataResultExampleimportasyncioimporttimefromavista_digital_exchange_sdkimportAvistaDigitalExchange,DxTypesauthenticationToken=AUTHENTICATION_TOKEN_VALUEcaptureId=YOUR_CAPTURE_ID# Create an instance of the AvistaDigitalExchange SDK# You may use a user authentication token or the authentication token of the Data CapturedigitalExchange=AvistaDigitalExchange(authenticationToken)print("Instantiated AvistaDigitalExchange instance with authentication token")# Specify the capture you are publishing data to.# NOTE: The capture must be in Capturing state to accept datadefgetCurrentMilliseconds():returnint(f'{time.time()*1000}'.split('.')[0])asyncdefpublishDataExample():# Prepare the data to be published# Replace ATTRIBUTE_ID and ATTRIBUTE_VALUEattributeValueMap={ATTRIBUTE_ID:ATTRIBUTE_VALUE,ATTRIBUTE_ID:ATTRIBUTE_VALUE,ATTRIBUTE_ID:ATTRIBUTE_VALUE,}data=[DxTypes.CaptureDataRecordInput(timestamp=f'{getCurrentMilliseconds()}',timeUnit="MILLISECONDS",attributeValues=attributeValueMap)]try:print("Calling dataCapture.publishData")result=awaitdigitalExchange.dataCapture.publishData(captureId=captureId,data=data)except:print("dataCapture.publishData failed")asyncio.run(publishDataExample())subscribeToDataListen for data being published to a Data Capture in realtime.ParameterscaptureId :  str, required
    The id of the Data Capture.Return TypeAsyncIterator[DxTypes.PublishCaptureDataResult]Exampleimportasynciofromavista_digital_exchange_sdkimportAvistaDigitalExchangeauthenticationToken=AUTHENTICATION_TOKENcaptureId=CAPTURE_IDasyncdefmain():# Create an instance of the AvistaDigitalExchange SDK# You may use a user authentication token or the authentication token of the Data CapturedigitalExchange=AvistaDigitalExchange(authenticationToken)print("Instantiated AvistaDigitalExchange instance with authentication token")try:print("Calling dataCapture.subscribeToData")# The for-loop will run each time a data publish event is received.asyncforresultindigitalExchange.dataCapture.subscribeToData(captureId=captureId):print("DataCapture.subscribeToData: Data received.")print("Do something with data...")exceptExceptionaserror:print(f"dataCapture.subscribeToData failed with error:{error}")raiseerrorif__name__=="__main__":asyncio.run(main())TypesUserStores basic information about a Digital Exchange user.PropertiesuserId : str
    The user's unique id number.
firstName : str
lastName : str
organization : Organization
    The organization that the user is a member of.OrganizationStores basic information about a Digital Exchange member organization.Propertiesname : str
organizationId : strDataStoreA Data Store Service object.PropertiesdataStoreId: str
name: str
description: str
ownerUserId: str
homeDirectoryId: str
    The dataStoreDirectoryId of the Data Store's home directoryMethodscdChange the working directory.Parameterspath: str, required
  The path to the desired directory.Exampleds = digitalExchange.getDataStore("dataStoreId.1234")
ds.cd("docs")lsList the contents of the working directory.Parameterspath: str, optional
  The path to the desired directory.Exampleds = digitalExchange.getDataStore("dataStoreId.1234")
ds.ls("./")pwdPrints the abosulte path of the working directory.ParametersnoneExampleds = digitalExchange.getDataStore("dataStoreId.1234")
ds.pwd()uploadFileUpload a file to the data store in the working directory.ParameterslocalFilePath: str, required
  The location of the file on your local file system.
name: str, optional
  What the file should be named in the data store.
description: string, optional
  A brief description of the file and it's contents.Exampleds = digitalExchange.getDataStore("dataStoreId.1234")
ds.uploadFile("./exampleFile.json", "remoteFileName.json", "A file containing a basic json object.")downloadFileDownload the specified file.Parametersfilename: str, required
  Name of the remote file in the current directory.
writeLocation: str, optional
  The location to store the downloaded file. If a filename is not given it will be saved with it's remote name.Exampleds = digitalExchange.getDataStore("dataStoreId.1234")
ds.downloadFile("remoteFileName.json", "./desiredLocalFileName.json")deleteFileDeletes the file from the working directory.Parametersfilename: str, required
  The name of the file to delete.Exampleds = digitalExchange.getDataStore("dataStoreId.1234")
ds.deleteFile("remoteFileName.json")DataStoreDirectoryA directory in a Data Store file storage hierarchy.PropertiesdataStoreDirectoryId: str
dataStoreId: str
    The Data Store this directory belongs to.
name: str
    The name of the directory.
homeDirectory: Boolean
    Indicator of whether this directory is the home directory or a subdirectory.
parentDirectoryId: str
    The directory this directory is found within.
directories: [DataStoreDirectory]
    This directory's contents - directories.
files: [DataStoreFile]
    This directory's contents - files.MethodsprintContentsLists all of the directory contents (directories and files), along with their ids.ParametersNoneExampledir = digitalExchange.getDataStoreDirectory("dataStoreDirectoryId.1234")
dir.printContents()DataStoreFileA file belonging to a Data Store.PropertiesdataStoreFileId: str
dataStoreId: str
    The Data Store this file belongs to.
dataStoreDirectoryId: str
    The directory that the file belongs to.
name: str
    The filename without the file extension.
description: str
fileExtension: str
storageSizeBytes: int
lastModified: str
contentType: strDigitalTwinModelPropertiesmodelId: str
    Id of the model
ownerUserId: str
    UserId of the owner of the model
displayName: str
    The name of the model
description: str
    Description of the model
properties: [ModelProperty]
    All properties associated with the model
telemetry: [ModelTelemetry]
    All telemetry associated with the modelModelPropertyData field that represents a state variable.(DTDL Property).Propertiesname: str
    Name of the property variable.
description: str
schemaType: str : "integer", "double", "string", "boolean", "dateTime", "duration"
    The type of the variable.
defaultValue: str
    The original value of the property.
writable: bool
    Indicates if the value of the property can be updated.ModelTelemetryData field representing measurements or events. Values are stored in a time series database.(DTDL Telemetry).Propertiesname: str
    Name of the telmetry.
description: str
schemaType: str : "integer", "double", "string", "boolean", "dateTime", "duration"
    The type of the variable.IotEndpointPropertiesiotEndpointId: str
iotHubId: str
    The iot hub the endpoint belongs to.
modelId: str
    The model the endpoint uses.
name: str
    The name of the endpoint.
description: str
properties: [EndpointProperty]
    The properties and their values. Properties are associated through the digital twin model used by the endpoint.
telemetry: [EndpointTelemetry]
    The telemetry attributes of the endpoint. Telemetry variables are associated through the digital twin model used by the endpoint.EndpointPropertyData field that represents a state of the endpoint. No historical values.(DTDL Property).Propertiesname: str
    Name of the property.
description: str
schemaType: str : "integer", "double", "string", "boolean", "dateTime", "duration"
    The type of the variable.
writable: bool
    Indicates if the value of the property can be updated.
value: str
    The current value of the property. All variable types are wrapped in a string.
timestamp: str
    Timestamp (in milliseconds) that the current value was written.EndpointTelemetryData field representing measurements or events. Values are stored in a time series database. Telemetry values must be queried for separately (queryByTimeRange, listEndpointLastValues).(DTDL Telemetry).Propertiesname: str
    Name of the telemetry.
description: str
schemaType: str : "integer", "double", "string", "boolean", "dateTime", "duration"
    The type of the variable.DxTypes.CaptureDataRecordInputA data record that is to be published to a Data Capture.Propertiestimestamp: str
    The timestamp of the data record
timeUnit: str
    The format of the timestamp
attributeValues: dict
    A dictionary containing the data to be written for the given timestmap. Key values should be attributeIds that map to the relevant attribute values.DxTypes.PublishCaptureDataResultResult object for the DataCapture.publishData operation.Propertiessuccess: bool
    Indicates if the action was successful
captureId: str
    The captureId of the Data Capture published to.
recordsWritten: [DxTypes.PublishCaptureDataSuccessfulRecord] | None
    An array of input data records that were successfully written.
recordsFailed: [DxTypes.PublishCaptureDataFailedRecord] | None    
    An array of input data records that were not written.
error: str | None
    An error message if a problem was encountered.DxTypes.PublishCaptureDataSuccessfulRecordA record that was published and written to a Data Capture.Propertiestimestamp: str
    The timestamp of the data record
timeUnit: str
    The format of the timestamp
attributeId: str
attributeValue: strDxTypes.PublishCaptureDataFailedRecordA record that was published but not written to a Data Capture.Propertiestimestamp: str
    The timestamp of the data record
timeUnit: str
    The format of the timestamp
attributeId: str
attributeValue: str
message: str
    Explains why the data record failed.DxTypes.StartCaptureResultResult object for the DataCapture.startCapture operation.Propertiessuccess: bool
    Indicates if the action was successful
captureId: str
    The captureId of the Data Capture being started.
startedAt: str | None
    The timestamp at which the Data Capture was started (if successful).
error: str | None
    An error message if a problem was encountered.DxTypes.StopCaptureResultResult object for the DataCapture.stopCapture operation.Propertiessuccess: bool
    Indicates if the action was successful
captureId: str
    The captureId of the Data Capture being stopped.
stoppedAt: str | None
    The timestamp at which the Data Capture was stopped (if successful).
error: str | None
    An error message if a problem was encountered.TroubleshootingDevelopmentClone the repository with commandgit clone https://github.com/Avista-Digital-Innovation/avista-digital-exchange-sdk.git.Use VS Code with the Python extension to utilize formatting and code completion.Deployment related code is in the root directory and the package code is found insrc/avista_digital_exchange_sdk.DeploymentFollow the steps below to build and push the new package version to PyPi.(Python packaging reference used)StepsUpdateCHANGELOG.mdwith new release notes.UpdateREADME.mdif necessary.PyPi deploymentUpdate the package version inpyproject.toml. Followthis versioning methodFrom the root directory of the repository:Runpython3 -m buildRunpython3 -m twine upload dist/*to upload the new build to PyPi.Use username__token__Use yourPyPi authentication tokenas the password.Test that the new version is available in pip by runningpip3 install --upgrade avista-digital-exchange-sdk.Push changes to git.Merge changes tomain.Create a release branch from main with the namerelease/YYYY_MM_DD_vXX.XX.XXwhere XX.XX.XX is the new version number, and YYYY_MM_DD is the date the version was deployed.Generating Python types and GraphQL Client from GraphQL SchemaSee instructions in graphql_codegen/README.mdFix for py-graphql-mapper moduleThe plugin was failing to assign the correct type for attributes of a schema type that were lists of other custom schema types (ie. IotEndpoint contains array of Properties and Telemetry).I edited the functioninit_type(obj, fieldType, fieldName) in the file gql_init.py from commit bc0888b. There are conditions for different fieldTypes. I edited the condition if the field is a list. Changed
'''
elif fieldType == list or (hasattr(fieldType, 'origin') and fieldType.origin== list):
'''
to
'''
elif fieldType == list or (hasattr(fieldType, 'origin') and fieldType.origin== list) or "gql_types.list" in str(fieldType):
'''websocketclient / websocket-client submoduleTo mitigate an issue when usingimport websocketwhen both websocket and websocket-client packages are installed, we added a git submodule to reference websocket-client directly. Once the SDK moves away from the subscriptionClient.py usage, this can be removed.ResourcesAvista Digital ExchangePyPi SDK project listingGitHub project repositoryPython package deployment tutorialPyPi API Token