pyapi FEATURE <edit-config> implementation

ncRPCEditConfig() method of the Session to send <edit-config> to the
NETCONF server.
diff --git a/python/examples/editconfig.py b/python/examples/editconfig.py
new file mode 100755
index 0000000..f405156
--- /dev/null
+++ b/python/examples/editconfig.py
@@ -0,0 +1,64 @@
+#!/usr/bin/python3
+
+import sys
+import os
+import getpass
+import libyang as ly
+import netconf2 as nc
+
+def interactive_auth(name, instruct, prompt, data):
+	print(name)
+	return getpass.getpass(prompt)
+
+def password_auth(user, host, data):
+	return getpass.getpass((user if user else os.getlogin()) + '@' + host + ' password : ')
+
+def hostkey_check(hostname, state, keytype, hexa, priv):
+	return True
+
+#
+# get know where to connect
+#
+host = input("hostname: ")
+try:
+	port = int(input("port    : "))
+except:
+	port = 0;
+user = input("username: ")
+
+#
+# set SSH settings
+#
+if user:
+	ssh = nc.SSH(username=user)
+else:
+	ssh = nc.SSH()
+ssh.setAuthInteractiveClb(interactive_auth)
+ssh.setAuthPasswordClb(password_auth)
+ssh.setAuthHostkeyCheckClb(hostkey_check)
+
+#
+# create NETCONF session to the server
+#
+try:
+	session = nc.Session(host, port, ssh)
+except Exception as e:
+	print(e)
+	sys.exit(1)
+
+# prepare config content as string or data tree
+tm = session.context.get_module("turing-machine")
+# config = "<turing-machine xmlns=\"http://example.net/turing-machine\"><transition-function><delta><label>left summand</label><input><state>0</state></input></delta></transition-function></turing-machine>"
+config = ly.Data_Node(session.context, "/turing-machine:turing-machine/transition-function/delta[label='left summand']/input/state", "5", 0, 0)
+
+# perform <edit-config> and print result
+try:
+        session.rpcEditConfig(nc.DATASTORE_RUNNING, config)
+except nc.ReplyError as e:
+        reply = {'success':False, 'error': []}
+        for err in e.args[0]:
+                reply['error'].append(json.loads(str(err)))
+        print(json.dumps(reply))
+        sys.exit(1)
+
+# print(data.print_mem(ly.LYD_XML, ly.LYP_FORMAT | ly.LYP_WITHSIBLINGS))
diff --git a/python/examples/get.py b/python/examples/get.py
index 5fa0bc4..9ccfb73 100755
--- a/python/examples/get.py
+++ b/python/examples/get.py
@@ -13,6 +13,9 @@
 def password_auth(user, host, data):
 	return getpass.getpass((user if user else os.getlogin()) + '@' + host + ' password : ')
 
+def hostkey_check(hostname, state, keytype, hexa, priv):
+        return True
+
 #
 # get know where to connect
 #
@@ -32,6 +35,7 @@
 	ssh = nc.SSH()
 ssh.setAuthInteractiveClb(interactive_auth)
 ssh.setAuthPasswordClb(password_auth)
+ssh.setAuthHostkeyCheckClb(hostkey_check)
 
 #
 # create NETCONF session to the server
diff --git a/python/netconf.c b/python/netconf.c
index e51e532..5b990fc 100644
--- a/python/netconf.c
+++ b/python/netconf.c
@@ -240,6 +240,18 @@
     PyModule_AddIntConstant(nc, "DATASTORE_STARTUP", NC_DATASTORE_STARTUP);
     PyModule_AddIntConstant(nc, "DATASTORE_CANDIDATE", NC_DATASTORE_CANDIDATE);
 
+    PyModule_AddIntConstant(nc, "RPC_EDIT_ERROPT_STOP", NC_RPC_EDIT_ERROPT_STOP);
+    PyModule_AddIntConstant(nc, "RPC_EDIT_ERROPT_CONTINUE", NC_RPC_EDIT_ERROPT_CONTINUE);
+    PyModule_AddIntConstant(nc, "RPC_EDIT_ERROPT_ROLLBACK", NC_RPC_EDIT_ERROPT_ROLLBACK);
+
+    PyModule_AddIntConstant(nc, "RPC_EDIT_TESTOPT_TESTSET", NC_RPC_EDIT_TESTOPT_TESTSET);
+    PyModule_AddIntConstant(nc, "RPC_EDIT_TESTOPT_SET", NC_RPC_EDIT_TESTOPT_SET);
+    PyModule_AddIntConstant(nc, "RPC_EDIT_TESTOPT_TEST", NC_RPC_EDIT_TESTOPT_TEST);
+
+    PyModule_AddIntConstant(nc, "RPC_EDIT_DFLTOP_MERGE", NC_RPC_EDIT_DFLTOP_MERGE);
+    PyModule_AddIntConstant(nc, "RPC_EDIT_DFLTOP_REPLACE", NC_RPC_EDIT_DFLTOP_REPLACE);
+    PyModule_AddIntConstant(nc, "RPC_EDIT_DFLTOP_NONE", NC_RPC_EDIT_DFLTOP_NONE);
+
     /* init libnetconf exceptions for use in clb_print() */
     libnetconf2Error = PyErr_NewExceptionWithDoc("netconf2.Error",
                     "Error passed from the underlying libnetconf2 library.",
diff --git a/python/rpc.c b/python/rpc.c
index ec7bd69..92109e7 100644
--- a/python/rpc.c
+++ b/python/rpc.c
@@ -31,6 +31,11 @@
 extern PyObject *libnetconf2Error;
 extern PyObject *libnetconf2ReplyError;
 
+static const char *ncds2str[] = {NULL, "config", "url", "running", "startup", "candidate"};
+const char *rpcedit_dfltop2str[] = {NULL, "merge", "replace", "none"};
+const char *rpcedit_testopt2str[] = {NULL, "test-then-set", "set", "test-only"};
+const char *rpcedit_erropt2str[] = {NULL, "stop-on-error", "continue-on-error", "rollback-on-error"};
+
 static struct nc_reply *
 rpc_send_recv(struct nc_session *session, struct nc_rpc *rpc)
 {
@@ -190,3 +195,109 @@
 
     return process_reply_data(reply);
 }
+
+PyObject *
+ncRPCEditConfig(ncSessionObject *self, PyObject *args, PyObject *keywords)
+{
+    static char *kwlist[] = {"datastore", "data", "defop", "testopt", "erropt", NULL};
+    struct lyd_node *data = NULL, *node, *content_tree = NULL;
+    char *content_str = NULL;
+    const struct lys_module *ietfnc;
+    NC_DATASTORE datastore;
+    NC_RPC_EDIT_DFLTOP defop = 0;
+    NC_RPC_EDIT_TESTOPT testopt = 0;
+    NC_RPC_EDIT_ERROPT erropt = 0;
+    PyObject *content_o = NULL, *py_lyd_node;
+    struct nc_rpc *rpc;
+    struct nc_reply *reply;
+
+    ietfnc = ly_ctx_get_module(self->ctx, "ietf-netconf", NULL, 1);
+    if (!ietfnc) {
+        PyErr_SetString(libnetconf2Error, "Missing \"ietf-netconf\" schema in the context.");
+        return NULL;
+    }
+
+    if (!PyArg_ParseTupleAndKeywords(args, keywords, "iO|iii:ncRPCEditConfig", kwlist, &datastore, &content_o, &defop, &testopt, &erropt)) {
+        return NULL;
+    }
+
+    if (PyUnicode_Check(content_o)) {
+            content_str = PyUnicode_AsUTF8(content_o);
+    } else if (!strcmp(Py_TYPE(content_o)->tp_name, "Data_Node")) {
+        py_lyd_node = PyObject_CallMethod(content_o, "C_lyd_node", NULL);
+        if (!SWIG_IsOK(SWIG_Python_ConvertPtr(py_lyd_node, (void**)&content_tree, SWIG_Python_TypeQuery("lyd_node *"), SWIG_POINTER_DISOWN))) {
+            PyErr_SetString(PyExc_TypeError, "Invalid object representing <edit-config> content. Data_Node is accepted.");
+            goto error;
+        }
+    } else if (content_o != Py_None) {
+        PyErr_SetString(PyExc_TypeError, "Invalid object representing <edit-config> content. String or Data_Node is accepted.");
+        goto error;
+    }
+
+    data = lyd_new(NULL, ietfnc, "edit-config");
+    node = lyd_new(data, ietfnc, "target");
+    node = lyd_new_leaf(node, ietfnc, ncds2str[datastore], NULL);
+    if (!node) {
+        goto error;
+    }
+
+    if (defop) {
+        node = lyd_new_leaf(data, ietfnc, "default-operation", rpcedit_dfltop2str[defop]);
+        if (!node) {
+            goto error;
+        }
+    }
+
+    if (testopt) {
+        node = lyd_new_leaf(data, ietfnc, "test-option", rpcedit_testopt2str[testopt]);
+        if (!node) {
+            goto error;
+        }
+    }
+
+    if (erropt) {
+        node = lyd_new_leaf(data, ietfnc, "error-option", rpcedit_erropt2str[erropt]);
+        if (!node) {
+            goto error;
+        }
+    }
+
+    if (content_str) {
+        if (!content_str[0] || (content_str[0] == '<')) {
+            node = lyd_new_anydata(data, ietfnc, "config", content_str, LYD_ANYDATA_SXML);
+        } else {
+            node = lyd_new_leaf(data, ietfnc, "url", content_str);
+        }
+    } else if (content_tree) {
+        node = lyd_new_anydata(data, ietfnc, "config", content_tree, LYD_ANYDATA_DATATREE);
+    }
+    if (!node) {
+        goto error;
+    }
+
+    rpc = nc_rpc_act_generic(data, NC_PARAMTYPE_FREE);
+    data = NULL;
+    if (!rpc) {
+        goto error;
+    }
+
+    reply = rpc_send_recv(self->session, rpc);
+    nc_rpc_free(rpc);
+    if (!reply) {
+        goto error;
+    }
+    if (reply->type != NC_RPL_OK) {
+        if (reply->type == NC_RPL_ERROR) {
+            RAISE_REPLY_ERROR(reply);
+        } else {
+            PyErr_SetString(libnetconf2Error, "Unexpected reply received.");
+        }
+        goto error;
+    }
+
+    Py_RETURN_NONE;
+
+error:
+    lyd_free(data);
+    return NULL;
+}
diff --git a/python/rpc.h b/python/rpc.h
index 262325e..9eb95da 100644
--- a/python/rpc.h
+++ b/python/rpc.h
@@ -21,6 +21,7 @@
 
 PyObject *ncRPCGet(ncSSHObject *self, PyObject *args, PyObject *keywords);
 PyObject *ncRPCGetConfig(ncSSHObject *self, PyObject *args, PyObject *keywords);
+PyObject *ncRPCEditConfig(ncSSHObject *self, PyObject *args, PyObject *keywords);
 
 #ifdef __cplusplus
 }
diff --git a/python/session.c b/python/session.c
index 1e78b24..e11b033 100644
--- a/python/session.c
+++ b/python/session.c
@@ -487,10 +487,14 @@
      "Send NETCONF <get> operation on the Session.\n\n"
      "ncRPCGet(subtree=None, xpath=None)\n"
      ":returns: Reply from the server.\n"},
-     {"rpcGetConfig", (PyCFunction)ncRPCGetConfig, METH_VARARGS | METH_KEYWORDS,
-      "Send NETCONF <get-config> operation on the Session.\n\n"
-      "ncRPCGetConfig(datastore, subtree=None, xpath=None)\n"
-      ":returns: Reply from the server.\n"},
+    {"rpcGetConfig", (PyCFunction)ncRPCGetConfig, METH_VARARGS | METH_KEYWORDS,
+     "Send NETCONF <get-config> operation on the Session.\n\n"
+     "ncRPCGetConfig(datastore, subtree=None, xpath=None)\n"
+     ":returns: Reply from the server.\n"},
+    {"rpcEditConfig", (PyCFunction)ncRPCEditConfig, METH_VARARGS | METH_KEYWORDS,
+     "Send NETCONF <edit-config> operation on the Session.\n\n"
+     "ncRPCEditConfig(datastore, data, defop=None, testopt=None, erropt=None)\n"
+     ":returns: None\n"},
     {NULL}  /* Sentinel */
 };