Click here to Skip to main content
14,389,715 members

python.net Client: A C# Library to Interact with Jupyter Kernels

Rate this:
5.00 (3 votes)
Please Sign up or sign in to vote.
5.00 (3 votes)
3 Nov 2019CPOL
A description of jupyter.net client: a C# library for interacting with Jupyter kernels

Introduction

In this article, I will present a C# library called jupyter.net client which allows to interact with a Jupyter Kernel.

Jupyter is a project that provides a framework for doing interactive computation in any programming language. The main components of this framework are reported on the figure below. The client interacts with the user asking code to execute and shows the output received from the kernel. The kernel executes the code and keeps the computation status (local variables, user functions, …). The communication between client and server takes place via ZeroMQ sockets and using the protocol described below.

Image 1

It's possible to connect multiple clients to a single kernel, but in this article, I’ll consider only the case in which one kernel is connected to one client.

The Jupyter project defines also the Notebook specification: a file format that the client can use to save in a file the source code, the results of the computation and other data.

There are many existing implementations of Jupyter clients. Here are the most used standalone applications:

  • Jupyter notebook
  • Jupyter console
  • JupyterLab
  • Nteract
  • CoCalc
  • Spyder

In addition, there is the jupyter_client library (https://pypi.org/project/jupyter-client/) to interact with a kernel programmatically in Python. The project presented here is intended to be C# alternative to this library.

Some possible uses for jupyter.net client are:

  • Create a customized frontend in C# for any scripting language. For example, you could write a custom tool for data analysis exploiting the python libraries, numpy and panda.
  • Use it as a script engine, to run a script in any language for which there is a jupyter kernel available in a C# application.

In this article, I’ll try to give an overview of the following topics:

  • How to use jupyter.net client
  • An overview of the Jupyter framework
  • How the code of jupyter.net client is structured

It will require much more than an article to explain these arguments deeply, so I’m going to provide just the key information and I will give some links to learn more. You can also look at the attached source code, which is quite simple.

jupyter.net client is available on GitHub (https://github.com/andreaschiavinato/jupyter.net_client) or as a NuGet package named JupiterNetClient.

This article will be followed by another one that will present a complete C# Windows application that allows to interact with a Jupyter kernel using jupyter.net client.

Jupyter Software Installation

There is a suite of software useful for Jupyer that can be installed, which includes:

  • The Jupyter notebook: a web application for doing interactive computation
  • The Jupyter console: a simple console application for doing interactive computation
  • The Python kernel: a kernel for the Python language, which is used by default with Jupyter notebook and Jupyter console
  • Some utilities, like jupyter-kernelspec.exe which is used in python.net client to get the available kernels.

To install it, first install python, then run python -m pip install jupyter”.

To test if you installed Jupyter correctly, you can run python -m jupyter notebook. After a while, the Jupyter Notebook web application should start.

Hello World Application

The code below is a simple C# application that executes the code print("Hello from Jupyter") on a Python kernel. To compile it, you need to import the JupiterNetClient NuGet package, and to run it, you need to install the software described in the previous section.

using JupiterNetClient;
using JupiterNetClient.Nbformat;
using System;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        //Initializing the Jupyter client
        //The constructor of JupyterBlockingClient will throw an execption
        //if the jupyter framework is not found
        //It is searched on the folders defined on the PATH system variable
        //You can also pass the folder where python.exe is located as
        //an argument of the constructor
        //(since the jupyter framework is located on the python folder)
        var client = new JupyterBlockingClient();

        //Getting available kernels
        var kernels = client.GetKernels();
        if (kernels.Count == 0)
            throw new Exception("No kernels found");

        //Connecting to the first kernel found
        Console.WriteLine($"Connecting to kernel {kernels.First().Value.spec.display_name}");
        client.StartKernel(kernels.First().Key);
        Console.WriteLine("Connected\n");

        //A callback that is executed when there is any information 
        //that needs to be shown to the user
        client.OnOutputMessage += Client_OnOutputMessage;

        //Executing some code
        client.Execute("print(\"Hello from Jupyter\")");

        //Closing the kernel
        client.Shutdown();
           
        Console.WriteLine("Press enter to exit");
        Console.ReadLine();            
    }

    private static void Client_OnOutputMessage(object sender, JupyterMessage message)
    {
        switch (message.content)
        {
            case JupyterMessage.ExecuteResultContent executeResultContent:
                Console.WriteLine($"[{executeResultContent.execution_count}] - 
                                 {executeResultContent.data[MimeTypes.TextPlain]}");
                break;

            case JupyterMessage.StreamContent streamContent:
                Console.WriteLine(streamContent.text);
                break;

            default:
                break;
        }
    } 
}

If the program runs successfully, you'll see the following output:

Connecting to kernel Python 3
Connected

Hello from Jupyter

Press enter to exit

Main Classes Description

Below are the main classes of jupyter.net client library:

Image 2

JupyterClientBase is the main class, it implements the methods to connect to a Kernel, get the available kernels, execute code, etc.

There are two versions of it that you can use: one blocking (JupyterBlockingClient) and one not blocking (JupyterClient). In short words, on the blocking version, the function that executes a code waits until the code execution is completed, conversely the non blocking variant returns immediately and raises an event on completion. This architecture is similar to the one of the official implementation of the Jupyter client (https://pypi.org/project/jupyter-client/). I found it useful to look at his code to better understand how a Jupyter client is supposed to work.

The KernelManager class is used to discover and connect to a Jupyter kernel and it is used internally in the library.

The Notebook class can be used to read or save a jupyter notebook file.

The JupyterMessage class is used to handle the messages exchanged between the Kernel and the Client.

Finding a Kernel and Connecting to It

In the following sections, I'll provide some technical details about the Jupyter framework. Let's start by looking at how a Jupyter client can connect to a kernel.

Each kernel is defined by a json file like this:

{
 "argv": [
  "python",
  "-m",
  "ipykernel_launcher",
  "-f",
  "{connection_file}"
 ],
 "display_name": "Python 3",
 "language": "python"
}

This structure is called Kernelspec and is defined here.
In my computer, those files are located in the folder C:\Users\Andrea\AppData\Local\Programs\Python\Python37\share\jupyter\kernels\, but a better way to retrieve this information is to run jupyter-kernelspec.exe list (it is located on the subfolder Scripts of the Python directory, and it is used on the function KernelManager.GetKernels()).

The args member of the kernelspec defines the command line to execute to start the kernel. Once a kernel is started, it should create another file called connection file which identifies the kernel instance and contains all the information required to connect to it (see https://jupyter-client.readthedocs.io/en/latest/kernels.html#connection-files and the code of the function KernelManager.StartKernel()).
Below is an example of connection file:

{
  "shell_port": 64656,
  "iopub_port": 64665,
  "stdin_port": 64659,
  "control_port": 64662,
  "hb_port": 64675,
  "ip": "127.0.0.1",
  "key": "5f5313c7-fb04d880c4e5756a645e1a97",
  "transport": "tcp",
  "signature_scheme": "hmac-sha256",
  "kernel_name": ""
}

The connection file contains the port number for five ZMQ sockets, used to communicate with the kernel, and a key, used to create a signature to add on all the messages.

ZeroMQ Sockets

ZeroMQ sockets are the tool used to communicate with the kernel. A good description of the ZMQ sockets is given at http://zguide.zeromq.org:

ZeroMQ (also known as ØMQ, 0MQ, or zmq) looks like an embeddable networking library but acts like a concurrency framework. It gives you sockets that carry atomic messages across various transports like in-process, inter-process, TCP, and multicast. You can connect sockets N-to-N with patterns like fan-out, pub-sub, task distribution, and request-reply. It's fast enough to be the fabric for clustered products. Its asynchronous I/O model gives you scalable multicore applications, built as asynchronous message-processing tasks. It has a score of language APIs and runs on most operating systems. ZeroMQ is from iMatix and is LGPLv3 open source.

As you can read, ZeroMQ provides different communication patterns. Below are the ones used in Jupyter:

  • Request/Response: A RESPONSE socket waits for requests from any REQUEST socket. Then the RESPONSE socket may provide a reply to the REQUEST socket
  • Publisher/Subscriber: A PUBLISHER socket is used to publish messages and other SUBSCRIBER sockets can subscribe to it to receive these messages.
  • Router/Dealer: This is a more sophisticated version of the Request/Response pattern, where the ROUTER can be seen as an asynchronous version of a REQUEST socket and the DEALER as an asynchronous version of the RESPONSE. By asynchronous, I mean that the DEALER can handle multiple requests at the same time, from different nodes. However, for the purpose of this article, it’s enough to consider this equivalent to the Request/Response.

Below is a picture showing the ZeroMQ sockets that a Kernel and a Client can use to communicate. A more detailed description is given at https://jupyter-client.readthedocs.io/en/stable/messaging.html.

Image 3

There are two main C# implementations of ZeroMQ sockets:

In this project, I’m using ZeroMQ, but for our proposes, they look equivalent.

Jupyter Protocol

The messages exchanged through these sockets use the JSON format. A message has the following fields:

  • Header: contains a unique identifier for the message, a string containing the username, a session identifier, a timestamp, the type of message and the protocol version
  • Parent_header: It’s a copy of the header of the parent cell (if any). For example, the code execution response message has as parent the code execution request message.
  • Metadata: additional metadata associated with the message. On the specification, it’s not specified clearly how to use this information, and on jupyter.net client, the metadata is not used.
  • Content: the content of the message, that depends on the message type
  • Buffers: list of binary data buffers for implementations that support binary extensions to the protocol. On jupyter.net, this information is not used.

The types of messages available at the time are:

  • execute_request: used by the client to ask the kernel to perform a certain action
  • execute_reply: used by the kernel to inform the client that the action requested has been completed
  • status: used by the kernel to communicate to the clients its status
  • display_data: used by the kernel to communicate to the clients that there is some data to show to the user
  • execute_result: used by the kernel to communicate to the client the result of a computation
  • input_request: used by the kernel to communicate to the client that it needs an input from the user;
  • execute_input: used by the kernel to broadcast to all the connected clients the code that it is being executed
  • error: used by the kernel to communicate that an error occurred during the computation

For more information, see https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-shell-router-dealer-channel.

On jupyter.net client, the class JupyterMessage has been created to handle these messages. As shown in the figure below, there is an abstract class to handle the content which has different subclasses depending on the message type.

Image 4

The interaction between client and server follows this schema:

  • The client sends to the kernel an execute_request message through the shell socket.
  • The kernel publishes on the iopub socket a status message indicating it is busy.
  • The kernel publishes on the iopub socket any required display_data/execute_result/error message, or on the stdin socket any execure_input message.
  • The kernel sends on the shell socket an execute_reply message indicating the computation has been completed.
  • The kernel publishes on the iopub socket a status message indicating it is ready again.

Commands Available

The Jupyter protocol provides the following commands that the client can use on an execute_request message:

  • Execute: execute some code
  • Introspection: provide information about a code (for instance, the type of a variable, but it’s up to the kernel to decide what information to provide)
  • Completion: provide a string to complete the current code
  • History: provides a list of the recent executed statements
  • Code completeness: indicate whether the current code can be executed as it is, or if the client should ask the user to enter one more line
  • Kernel info: provides information about the kernel
  • Kernel shutdown: closes the kernel
  • Kernel interrupt: interrupts the current computation

For each of these commands, a method on the class JupyterClient / JupyterBlokingClient is provided.

Executing Code

The sequence diagram below illustrates the messages exchanged between the Client and the Kernel to execute a code.

Image 5

The Client sends a ZeroMQ message of type execute_request on the shell socket containing the code to execute.

The kernel sends a message of type status on the IOPub socket indicating it is busy, then confirms the message has been received by sending a message of type execute_input with a copy of the code received and a progressive number to identify this statement.

Then it sends the result of the execution by sending a message of type execute_result. Some code may not produce an execute_result and instead the kernel may send a stream or a display_data message.
Then it sends a message of type execute_reply on the shell socket, indicating that the execution is completed.
Finally, it sends a status message on the IOPub socket, indicating that it is ready to process the next request.

Command Line Client

Below, I report the code of a command line client that can be used to interact with a Jupyter kernel. It is a C# version of the Jupyter console application (https://github.com/jupyter/jupyter_console).

using JupiterNetClient;
using JupiterNetClient.Nbformat;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

class Program
{
    private const string Prompt = ">>> ";
    private const string PromptWhite = "... ";
    private static JupyterBlockingClient client;
    private static Dictionary<string, KernelSpec> kernels;

    static void Main(string[] args)
    {
        client = new JupyterBlockingClient();

        kernels = client.GetKernels();
        if (kernels.Count == 0)
            throw new Exception("No kernels found");

        //Connecting to the first kernel found
        Console.WriteLine($"Connecting to kernel {kernels.First().Value.spec.display_name}");
        client.StartKernel(kernels.First().Key);

        DisplayKernelInfo(client.KernelInfo);

        client.OnOutputMessage += Client_OnOutputMessage;
        client.OnInputRequest += Client_OnInputRequest;

        //Mainlook asks code to execute and executes it.
        Console.WriteLine("\n\nEnter code to execute or Q <enter> to terminate:");        
        MainLoop(client);

        //terminating the kernel process
        Console.WriteLine("SHUTTING DOWN KERNEL");
        client.Shutdown();
    }

Below is the MainLoop procedure:

private static void MainLoop(JupyterBlockingClient client)
{
    //Using the component ReadLine, which has some nice features
    //like code completion and history support
    ReadLine.HistoryEnabled = true;
    ReadLine.AutoCompletionHandler = new AutoCompletionHandler(client);
    var enteredCode = new StringBuilder();
    var startNewCode = true;
    var lineIdent = string.Empty;
    while (true)
    {

        enteredCode.Append(ReadLine.Read
                    (startNewCode ? Prompt : PromptWhite + lineIdent));
        var code = enteredCode.ToString();
        if (code == "Q")
        {
            //When the user types Q we terminates the application
            return;
        }
        else if (string.IsNullOrWhiteSpace(code))
        {
            //No code entered, do nothing
        }
        else
        {
            //Asking the kernel if the code entered by the user
            //so far is a complete statement.
            //If not, for example because it is the first line of a function definition,
            //we ask the user to enter one more line
            var isComplete = client.IsComplete(code);
            switch (isComplete.status)
            {
                case JupyterMessage.IsCompleteStatusEnum.complete:
                    //the code is complete, execute it
                    //the results are given on the OnOutputMessage callback
                    client.Execute(code);
                    startNewCode = true;
                    break;

                case JupyterMessage.IsCompleteStatusEnum.incomplete:
                    lineIdent = isComplete.indent;
                    enteredCode.Append("\n" + lineIdent);
                    startNewCode = false;
                    break;

                case JupyterMessage.IsCompleteStatusEnum.invalid:
                case JupyterMessage.IsCompleteStatusEnum.unknown:
                    Console.WriteLine("Invalid code: " + code);
                    startNewCode = true;
                    break;
            }
        }

        if (startNewCode)
        {
            enteredCode.Clear();
        }
    }
}

The AutoCompletionHandler class is used by the Readline component to support the autocompletion:

private class AutoCompletionHandler : IAutoCompleteHandler
{
    private readonly JupyterBlockingClient _client;

    public AutoCompletionHandler(JupyterBlockingClient client)
    {
        _client = client;
    }

    public char[] Separators { get; set; } = new char[] { };

    public string[] GetSuggestions(string text, int index)
    {
        //asking the kernel to provide a list of strings to complete the current line
        var result = _client.Complete(text, text.Length);
        return result.matches
            .Select(s => text.Substring(0, result.cursor_start) + s)
            .ToArray();
    }
}

This is the callback to display the output messages:

private static void Client_OnOutputMessage(object sender, JupyterMessage message)
{
    switch (message.content)
    {
        case JupyterMessage.ExecuteInputContent executeInputContent:
            Console.WriteLine($"Executing
              [{executeInputContent.execution_count}] - {executeInputContent.code}");
            break;

        case JupyterMessage.ExecuteResultContent executeResultContent:
            Console.WriteLine($"Result
                [{executeResultContent.execution_count}] -
                 {executeResultContent.data[MimeTypes.TextPlain]}");
            break;

        case JupyterMessage.DisplayDataContent displayDataContent:
            Console.WriteLine($"Data  {displayDataContent.data}");
            break;

        case JupyterMessage.StreamContent streamContent:
            Console.WriteLine($"Stream  {streamContent.name} {streamContent.text}");
            break;

        case JupyterMessage.ErrorContent errorContent:
            Console.WriteLine($"Error  {errorContent.ename} {errorContent.evalue}");
            Console.WriteLine(errorContent.traceback);
            break;

        case JupyterMessage.ExecuteReplyContent executeReplyContent:
            Console.WriteLine($"Executed
                 [{executeReplyContent.execution_count}] - {executeReplyContent.status}");
            break;

        default:
            break;
    }
}

This is the callback to ask the user for an input:

private static void Client_OnInputRequest
          (object sender, (string prompt, bool password) e)
{
    var input = e.password
        ? ReadLine.ReadPassword(e.prompt)
        : ReadLine.Read(e.prompt);
    client.SendInputReply(input);
}

And finally, this is the method used to display information about the kernel:

private static void DisplayKernelInfo(JupyterMessage.KernelInfoReplyContent kernelInfo)
{
    Console.WriteLine("");
    Console.WriteLine(" KERNEL INFO");
    Console.WriteLine("============");
    Console.WriteLine($"Banner: {kernelInfo.banner}");
    Console.WriteLine($"Status: {kernelInfo.status}");
    Console.WriteLine($"Protocol version: {kernelInfo.protocol_version}");
    Console.WriteLine($"Implementation: {kernelInfo.implementation}");
    Console.WriteLine($"Implementation version: {kernelInfo.implementation_version}");
    Console.WriteLine($"Language name: {kernelInfo.language_info.name}");
    Console.WriteLine($"Language version: {kernelInfo.language_info.version}");
    Console.WriteLine($"Language mimetype: {kernelInfo.language_info.mimetype}");
    Console.WriteLine($"Language file_extension: {kernelInfo.language_info.file_extension}");
    Console.WriteLine($"Language pygments_lexer: {kernelInfo.language_info.pygments_lexer}");
    Console.WriteLine($"Language nbconvert_exporter:
                   {kernelInfo.language_info.nbconvert_exporter}");
}

Notebook Format

A Jupyter notebook is a Json file like the one below:

{
    "metadata": {
        "kernel_info": {
            "name": "Python 3"
        },
        "language_info": {
            "name": "python",
            "version": "3.7.2"
        }
    },
    "nbformat": 4,
    "nbformat_minor": 2,
    "cells": [{
        "execution_count": 1,
        "outputs": [{
            "execution_count": 1,
            "data": {
                "text/plain": ["4"]
            },
            "output_type": "execute_result"
        }],
        "cell_type": "code",
        "source": ["2+2"],
    }]
}

It has a metadata object containing information about the kernel and the language used. It is the responsibility of the client to make sure that any notebook file opened is compatible with the kernel in use.

Then there is a list of cell elements, that can be of one of the following types:

  • Code: contains the code to run, and may contain the outputs of the execution
  • Markdown: contains text formatted using the markdown syntax
  • Raw: special cells that contains data for the nconvert utility

The following example reads a notebook and prints its content on the screen:

using JupiterNetClient.Nbformat;
using System;

class Program
{
    static void Main(string[] args)
    {
        var nb = Notebook.ReadFromFile(@"test.ipynb"); //TODO: change the name of the file
        Console.WriteLine($"Langauge: {nb.metadata.language_info.name} 
                                      {nb.metadata.language_info.version}");
        Console.WriteLine($"Kernel: {nb.metadata.kernel_info.name}");
        Console.WriteLine($"Notebook format: {nb.nbformat}.{nb.nbformat_minor}");
        Console.WriteLine("\nContent:\n");
        foreach (var cell in nb.cells)
        {
            switch (cell)
            {
                case MarkdownCell markdownCell:                        
                    Console.WriteLine(markdownCell.source);
                    break;

                case CodeCell codeCell:
                    Console.WriteLine(codeCell.source);
                    foreach (var output in codeCell.outputs)
                    {
                        Console.Write("  " + output.output_type + ": ");
                        switch (output)
                        {
                            case StreamOutputCellOutput streamOutputCellOutput:
                                Console.WriteLine($"{streamOutputCellOutput.name} 
                                                    {streamOutputCellOutput.text}");
                                break;
                            case DisplayDataCellOutput displayDataCellOutput:
                                Console.WriteLine
                                   (displayDataCellOutput.data[MimeTypes.TextPlain]);
                                break;
                            case ExecuteResultCellOutput executeResultCellOutput:
                                Console.WriteLine
                                   (executeResultCellOutput.data[MimeTypes.TextPlain]);
                                break;
                            case ErrorCellOutput errorCellOutput:
                                Console.WriteLine($"{errorCellOutput.ename} 
                                                    {errorCellOutput.evalue}");
                                break;
                        }                            
                    }
                    break;

                case RawCell _:
                    Console.WriteLine($"(raw cell)");
                    break;
            }
        }

        Console.ReadLine();
    }
}

The following program shows how to create a simple notebook:

using JupiterNetClient;
using JupiterNetClient.Nbformat;
using System;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        var client = new JupyterBlockingClient();

        //Getting available kernels
        var kernels = client.GetKernels();
        if (kernels.Count == 0)
            throw new Exception("No kernels found");

        //Connecting to the first kernel found
        Console.WriteLine($"Connecting to kernel {kernels.First().Value.spec.display_name}");
        client.StartKernel(kernels.First().Key);
        Console.WriteLine("Connected");

        //Creating a notebook and adding a code cell
        var nb = new Notebook(client.KernelSpec, client.KernelInfo.language_info);
        var cell = nb.AddCode("print(\"Hello from Jupyter\")");

        //Setting up the callback so that the outputs are written on the notebook
        client.OnOutputMessage += (sender, message) => 
                     { if (ShouldWrite(message)) cell.AddOutputFromMessage(message); };
            
        //executing the code
        client.Execute(cell.source);

        //saving the notebook
        nb.Save("test.ipynb");
        Console.WriteLine("File test.ipynb written");

        //Closing the kernel
        client.Shutdown();

        Console.WriteLine("Press enter to exit");
        Console.ReadLine();
    }

    private static bool ShouldWrite(JupyterMessage message) =>
        message.header.msg_type == JupyterMessage.Header.MsgType.execute_result
        || message.header.msg_type == JupyterMessage.Header.MsgType.display_data
        || message.header.msg_type == JupyterMessage.Header.MsgType.stream
        || message.header.msg_type == JupyterMessage.Header.MsgType.error;
}

Executing a Python Script from a C# App and Getting the Result

You can use python.net client to run a Python script from your C# application. Unlike libraries like IronPython or Python for .NET (pythonnet), with jupyter.net client (and a Python kernel), the Python code is executed on a separated process, so it is safer because it cannot crash your application and it can eventually be executed in a remote machine. You can also easily update the python kernel without recompiling the C# application.

Below is an example that shows how python.net client can be used to run a function written in a .py file and get the result.

using JupiterNetClient;
using JupiterNetClient.Nbformat;
using System;
using System.Linq;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        var client = new JupyterBlockingClient();

        //Getting available kernels
        var kernels = client.GetKernels();
        if (kernels.Count == 0)
            throw new Exception("No kernels found");

        //Connecting to the first kernel found
        Console.WriteLine($"Connecting to kernel {kernels.First().Value.spec.display_name}");
        client.StartKernel(kernels.First().Key);
        Console.WriteLine("Connected\n");

        //Loading a script containing the following function:
        //def do_something(a):
        //  return a ** 2
        client.Execute("%run script.py");

        //Creating an event handler that stores the result 
        //of the computation in a TaskCompletionSource object 
        var promise = new TaskCompletionSource<string>();
        EventHandler<JupyterMessage> hanlder = (sender, message) =>
        {
            if (message.header.msg_type == JupyterMessage.Header.MsgType.execute_result)
            {
                var content = (JupyterMessage.ExecuteResultContent)message.content;
                promise.SetResult(content.data[MimeTypes.TextPlain]);
            }
            else if (message.header.msg_type == JupyterMessage.Header.MsgType.error)
            {
                var content = (JupyterMessage.ErrorContent)message.content;
                promise.SetException(new Exception
                     ($"Jupyter kenel error: {content.ename} {content.evalue}"));
            }
        };
        client.OnOutputMessage += hanlder;
        //calling the function do_something
        client.Execute("do_something(2)");
        //removing event handler, since the TaskCompletionSource cannot be reused
        client.OnOutputMessage -= hanlder;

        //getting the result
        try
        {
            Console.WriteLine("Result:");
            if (promise.Task.IsCompleted)
                Console.WriteLine(promise.Task.Result);
            else
                Console.WriteLine("No result received");
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }

        finally
        {
            //Closing the kernel
            client.Shutdown();
            Console.WriteLine("Press enter to exit");
            Console.ReadLine();
        }
    }
}

The expected output is:

Connecting to kernel Python 3
Connected

Result:
4
Press enter to exit

History

  • 3rd November, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Andreask84
Software Developer
Unknown
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
Article
Posted 3 Nov 2019

Tagged as

Stats

2.3K views
71 downloads
7 bookmarked