From 741c8153da831a7d6ed00a6f9de3056d20187500 Mon Sep 17 00:00:00 2001 From: Kevin Vu te Laar Date: Thu, 13 Mar 2025 13:28:59 +0100 Subject: [PATCH] First Python-Wrapper Documentation Signed-off-by: Kevin Vu te Laar --- docs/node/clients/python_wrapper.md | 931 ++++++++++++++++++++++++++++ 1 file changed, 931 insertions(+) create mode 100644 docs/node/clients/python_wrapper.md diff --git a/docs/node/clients/python_wrapper.md b/docs/node/clients/python_wrapper.md new file mode 100644 index 0000000..a4b3c47 --- /dev/null +++ b/docs/node/clients/python_wrapper.md @@ -0,0 +1,931 @@ +# Python-Wrapper Documentation: + +>Important Preface: +All of the documentation and testing done regarding the VILLASnode Python-Wrapper +is based upon the signal_v2 node provided by VILLASnode. +Node specific functions are implemented on a node to node basis and +therefore may exert different behavior. + +1. [VILLASnode functions exposed by the C-API](#1.-villasnode-functions-exposed-by-the-c-api) + - [Functions to set up a node or modify its state](#functions-to-set-up-a-node-or-modify-its-state) + - [Functions to extract node specific information](#functions-to-extract-node-specific-information) + - [Functions related to data transfer](#functions-related-to-data-transfer) +2. [Using the C-API with the Python-Wrapper](#2.-using-the-c-api-with-the-python-wrapper) +3. [Bugs](#3.-bugs) +4. [Improvements](#4.-improvements) +5. [Contributing to the Python-Wrapper](#5.-contributing-to-the-python-wrapper) +6. [Installation](#6.-installation) + +## 1. VILLASnode functions exposed by the C-API +``` +#Functions to set up up a node or modify its state + + +vnode * node_new(const char *id_str, const char *json_str); + +int node_check(vnode *n); +int node_prepare(vnode *n); +int node_start(vnode *n); +int node_stop(vnode *n); +int node_pause(vnode *n); +int node_resume(vnode *n); +int node_restart(vnode *n); +int node_destroy(vnode *n); + +#Functions to extract node specific information + +bool node_is_valid_name(const char *name); +bool node_is_enabled(const vnode *n); +const char *node_name(vnode *n); +const char *node_name_short(vnode *n); +const char *node_name_full(vnode *n); +const char *node_details(vnode *n); +unsigned node_input_signals_max_cnt(vnode *n); +unsigned node_output_signals_max_cnt(vnode *n); +const char *node_to_json_str(vnode *n); +unsigned sample_length(vsample *smp); + +#Functions related to data transfer + +int node_reverse(vnode *n); +int node_read(vnode *n, vsample **smps, unsigned cnt); +int node_write(vnode *n, vsample **smps, unsigned cnt); +int node_poll_fds(vnode *n, int fds[]); +int node_netem_fds(vnode *n, int fds[]); +vsample *sample_alloc(unsigned len); +void sample_decref(vsample *smp); +vsample *sample_pack(unsigned seq, struct timespec *ts_origin, + struct timespec *ts_received, unsigned len, + double *values); +void sample_unpack(vsample *s, unsigned *seq, struct timespec *ts_origin, + struct timespec *ts_received, int *flags, unsigned *len, + double *values); +int memory_init(int hugepages); +``` +--- +### Functions to set up a node or modify its state + + +`node_new(const char *id_str, const char *json_str)` takes two strings as input parameters. +- `id_str:` identification string (uuid - 36 characters long + 1 character for null termination). +If not provided, resulting in the nullptr, a random uuid is created and assigned to the node by VILLASnode. +It can be retrieved by different functions like [node_name_full()](#node_name_full()). + +- `json_str:` the string containing a valid json configuration + +Invalid json configurations will throw an error. + +**Further:** valid VILLASnode configurations can be either checked for with `node_check()` +if the configuration has been accepted by VILLASnode ***or*** may fail completely when +a wrong configurations is used to create a node, resulting in a runtime error. +The required json format to configure nodes can be found [here](https://villas.fein-aachen.org/docs/node/nodes/). + +***Paths or the VILLASdaemon are not used for the Python-Wrapper instance. +Therefore only the node configurations need to be considered.*** + +
+ Creating a Node + +``` +import uuid +import villas_node as vn + +# some valid json config +config = { + ... +} + +# config is a singular node configuration +# creating an uuid - optional +id = str(uuid.uuid4()) + +#invalid uuid's are discarded and a new random one is generated by VILLASnode +#node = vn.node_new(config) does not work, some input for the uuid is necessary +#node = vn.node_new("", config) +#node = vn.node_new("0", config) + +node = vn.node_new(id, config) + +# config is a list of node configurations +nodes = {} +# creating new nodes, accessible by name +for name, content in data.items(): + #dictionary to extract the name of each node + obj = {name: content} + + # read inner configuration/json object to create a node + config = json.dumps(obj, indent=2) + id = str(uuid.uuid4()) + + nodes[name] = vn.node_new(id, config) + +``` +
+ +--- + +`int node_check(vnode *n)` checks the in and output signals of a node and sets the node state to **checked** + +`int node_prepare(vnode *n)` sets up the in and output signals of a node and sets the node state **prepared** + +`int node_start(vnode *n)` starts a node and sets the node state **started** + +`int node_stop(vnode *n)` stops a node, can be (re-)started and but resumed + +`int node_pause(vnode *n)` pauses a node, can be resumed and stopped + +`int node_resume(vnode *n)` resumes a paused node, does not work if the node stopped + +`int node_restart(vnode *n)` restarts a stopped node + + +`int node_destroy(vnode *n)` deletes/destroys a node, can not be used again leaving the node pointer [dangling](#dangling_pointer_comment) (do not use (the pointer)) + +- `vnode *n` a node pointer that can be created by [node_new()](#node_new()) + +The functions have return codes on success or failure. +In other words either `-1`, `0`, `1` is returned depending on either: +- unchanged `-1` +- success `0` +- failure `1` + +An example: in case node_start() is used on a node that has already been started `1` is returned. +This can be used for branching. +
+ All of these functions can be used like this + +``` +# assuming node is a valid node +node = ... + +node_prepare(node) +node_check(node) +status = node_start(node) +node_stop(node) +node_pause(node) +node_resume(node) +node_restart(node) +node_destroy(node) + +# branching +if (status == -1): + print("Node already started!") +if (status == 0): + print("Node started!") +if (status == 1): + print("Starting the node failed!") +``` +
+ +--- +### Functions to extract node specific information + +`bool node_is_valid_name(const char *name)` + +`bool node_is_enabled(const vnode *n)` + +`const char *node_name(vnode *n)` returns the node name + +`const char *node_name_short(vnode *n)` [not working](#node_name_short) + +`const char *node_name_full(vnode *n)` returns name and details of the node +>output structure of the details: +node_name\: uuid=\, +#in.signals=<.../...>, +#out.signals<.../...>, +#in.hooks=..., +#out.hooks=..., +in.vectorize=..., +out.vectorize=..., +out.netem=..., +layer=..., +in.address=, +out.address + +`const char *node_details(vnode *n)` returns less details than node_name_full() +>layer=..., +in.address=\, +out.address=\ + +`unsigned node_input_signals_max_cnt(vnode *n)` returns input signal count + +`unsigned node_output_signals_max_cnt(vnode *n)` returns output signal count + +`const char *node_to_json_str(vnode *n)` returns node config in string format + +`unsigned sample_length(vsample *smp)` returns the length of the samples stored in a sample object + +
+ All of these functions can be used like this + +``` +#assuming node is a valid node +name = "some name" +node = ... + +node_is_valid_name(name) +node_is_enabled(node) +node_name(node) +node_name_short(node) +nodfe_name_full(node) +node_details(node) +node_input_signals_max_cnt(node) +node_output_signals_max_cnt(node) +node_to_json_str(node) + +#sample_length() requires a sample handle +#this can be a sample stored in an array +samples = smps_array(1) +samples[0] = sample_alloc(i) #i should be the sample length +... + +sample_length(samples[0]) + +#or a sample created manually with sample_pack() + +sample = sample_pack(...) + +sample_length(sample) +``` +
+ +--- +`json_t *node_to_json(const vnode *n):` returns a [node configuration](#node_to_json_wrong_format) of the node + +The node configuration returned is either of type: +- `none` +- `int` +- `float` +- `bool` +- `string` + +the following returned types contain the ones above: +- `dictionary` +- `list` + +--- +### Functions related to data transfer + +`int node_reverse(vnode *n)` swap in and output signals of a node + +`int node_read(vnode *n, vsample **smps, unsigned cnt)` reads from the node's storage/buffer + +- `vnode *n` the pointer to a node +- `vsample **smps` is a pointer to a data structure that can hold samples + +Since this is impossible without a wrapper class such as the [sample holding array](#2.-implementation-notes) to do natively with a python data structure, the samples array has to be used for this. + +- `unsigned cnt` the amount of samples to read or write + +Some nodes like the signal generator (v2) node can only read one sample at a time. Especially when using things such as rt (realtime) mode. + +`int node_write(vnode *n, vsample **smps, unsigned cnt)` writes to a node's storage/buffer + +The same as `node_read()` above. + +`int node_poll_fds(vnode *n, int fds[])` + +`int node_netem_fds(vnode *n, int fds[])` + +`vsample *sample_alloc(unsigned len)` allocates a single sample + +`void sample_decref(vsample *smp)` decrement and delete sample pointer + +The sample is deleted and deallocated if it has no pointers pointing to it. + +`vsample *sample_pack(unsigned seq, struct timespec *ts_origin, + struct timespec *ts_received, unsigned len, + double *values)` creates a sample manually + +`void sample_unpack(vsample *s, unsigned *seq, struct timespec *ts_origin, + struct timespec *ts_received, int *flags, unsigned *len, + double *values)` + +`int memory_init(int hugepages)` initializes memory system with **hugepages** + +All of these functions can be used like in [this example](#rw_test). + +--- + +### 2. Using the C-API with the Python-Wrapper + +The wrapper exposes all of the C-API functions of VILLASnode. + +All functions except for: +``` +int node_poll_fds() +int node_netem_fds() +vsample *sample_pack() +void sample_unpack() +int memory_init() +``` + +have been tested and work, except for: + +``` +node_name_short() +node_restart() +node_stop() +``` + +which may cause unexpected behavior. + + +--- +### An example creating a signal, socket and file nodes to read from a configuration file, write the data to a file and send over a socket. + +This will be moved to a Lab in the future. +Further there will be also another Lab of the Python-Wrapper that leverages a VILLASnode openDSS node type, which is still being implemented. + +
+ The configuration file may look like this + +``` +{ + "send_socket": { + "type": "socket", + "format": "protobuf", + "layer": "udp", + "in": { + "address": "127.0.0.1:65532", + "signals": [ + { + "name": "voltage", + "type": "float", + "unit": "V" + }, + { + "name": "current", + "type": "float", + "unit": "A" + } + ] + }, + "out": { + "address": "127.0.0.1:65533", + "netem": { + "enabled": false + }, + "multicast": { + "enabled": false + } + + } + }, + "intmdt_socket": { + "type": "socket", + "format": "protobuf", + "layer": "udp", + "in": { + "address": "127.0.0.1:65533", + "signals": [ + { + "name": "voltage", + "type": "float", + "unit": "V" + }, + { + "name": "current", + "type": "float", + "unit": "A" + } + ] + }, + "out": { + "address": "127.0.0.1:65534", + "netem": { + "enabled": false + }, + "multicast": { + "enabled": false + } + } + }, + "recv_socket": { + "type": "socket", + "format": "protobuf", + "layer": "udp", + "in": { + "address": "127.0.0.1:65534", + "signals": [ + { + "name": "voltage", + "type": "float", + "unit": "V" + }, + { + "name": "current", + "type": "float", + "unit": "A" + } + ] + }, + "out": { + "address": "127.0.0.1:65535", + "netem": { + "enabled": false + }, + "multicast": { + "enabled": false + } + } + }, + "sig_gen_file" :{ + "type": "file", + "format": "villas.human", +"uri": "/path/to/sig_gen.log", + "in": { + "epoch_mode": "wait", + "signals": [ + { + "name": "voltage", + "type": "float", + "unit": "V" + }, + { + "name": "current", + "type": "float", + "unit": "A" + } + ] + } + }, + "recv_socket_file" :{ + "type": "file", + "format": "villas.human", + "uri": "/path/to/recv_socket.log", + "in": { + "epoch_mode": "wait", + "signals": [ + { + "name": "voltage", + "type": "float", + "unit": "V" + }, + { + "name": "current", + "type": "float", + "unit": "A" + } + ], + "hooks": [ + { + "type": "print", + "format": "villas.human" + } + ] + } + }, + "signal_generator": { + "type": "signal.v2", + "limit": 100, + "rate": 10, + "in": { + "signals": [ + { + "amplitude": 2, + "name": "voltage", + "phase": 90, + "signal": "sine", + "type": "float", + "unit": "V" + }, + { + "amplitude": 1, + "name": "current", + "phase": 0, + "signal": "sine", + "type": "float", + "unit": "A" + } + ], + "hooks": [ + { + "type": "print", + "format": "villas.human" + } + ] + } + } +} +``` +
+ + +**Example code taken from the Wrapper Unit tests and slightly modified:** +``` +import json +import uuid +import villas_node as vn + +# the configuration comprises nodes with the type and name: +# +# signal generator node (v2): "signal_generator" +# socket nodes: "send_socket", "intmdt_socket", "recv_socket" +# file nodes: "sig_gen_file", "recv_socket_file" + +with open('/path/to/config/file.json', 'r') as f: + data = json.load(f) + f.close() + +# list to read and create multiple nodes from a file +test_nodes = {} +for name, content in data.items(): + #dictionary to extract the name of each node + obj = {name: content} + + # forward inner configuration to create a node + config = json.dumps(obj, indent=2) + id = str(uuid.uuid4()) + + #creating new nodes, accesible by name + test_nodes[name] = vn.node_new(id, config) + +# verifying the node configurations and starting them +for node in test_nodes.values(): + if (vn.node_check(node)): + raise RuntimeError(f"Failed to verify node configuration") + if (vn.node_prepare(node)): + raise RuntimeError(f"Failed to verify {vn.node_name(node)} node configuration") + vn.node_start(node) + +# declare Arrays with that can hold 1, 100 and 100 samples respectively +send_smpls = vn.smps_array(1) +intmdt_smpls = vn.smps_array(100) +recv_smpls = vn.smps_array(100) + +for i in range(0,100): + # allocate memory for samples to be stored with two signal values per Sample + send_smpls[0] = vn.sample_alloc(2) + intmdt_smpls[i] = vn.sample_alloc(2) + recv_smpls[i] = vn.sample_alloc(2) + + # generate signals and send over send socket, write to file + # signal nodes can only create one Sample at a time + vn.node_read(test_nodes["signal_generator"], send_smpls, 1) + vn.node_write(test_nodes["send_socket"], send_smpls, 1) + vn.node_write(test_nodes["sig_gen_file"], send_smpls, 1) + +# write intermediary signals to file (100 at once) +vn.node_read(test_nodes["intmdt_socket"], intmdt_smpls, 100) +vn.node_write(test_nodes["intmdt_socket"], intmdt_smpls, 100) + +# write receive socket signals to file (100 at once) +vn.node_read(test_nodes["recv_socket"], recv_smpls, 100) +vn.node_write(test_nodes["recv_socket_file"], recv_smpls, 100) +``` + +___ +## 3. Bugs + +This is a small collection of known bugs. If any other ones are encountered, you are encouraged to open an [issue](#https://github.com/VILLASframework/node/issues). + +--- +Hooks are a big issue. +Not every hook works properly, some cause undefined behavior or even segmentation faults. + +Hooks within the json config have no exclusive place to be defined in. +Let's consider what would happen if you defined the same hook for print in: +- `"in":{...}` +- `"out": {...}` +- `"hooks": {[...]}` +
+ Hooks example + +``` +{ + "some_node": { + ... + "in":{ + "...": { + ... + }, + ... + "hooks": {[ + { + #some hook + }, + ... + ]} + }, + "out":{ + "...": { + ... + }, + ... + "hooks": {[ + { + #some hook + }, + ... + ]} + }, + "hooks": {[ + { + #some hook + }, + ... + ]} + } +} +``` +
+ +--- + +node_write_short() bugged, appears to be trying to print a free()'d (json?)object. + +--- +## 4. Improvements + +This is a small collection of known improvements that could be made. If any other suggestions arise, you are encouraged +to either open an [issue](#https://github.com/VILLASframework/node/issues) or contact the code authors. + +--- +Fix [bugs](#-bugs). + +Convenience functions/bindings in the wrapper for ease of use (one example would be the automatic sample_decref()). + +Implement sample_alloc() for an array slice? for loops in python are terribly slow. + +Reduce the amount of calls between Python/C/C++. + + +Ensure that the [dangling pointer](#dangling_pointer) is a nullptr. + + +It does not seem like vectorize has much sensible use... + +--- +## 5. Contributing to the Python-Wrapper + +This is a small guide about what was used to create the Python-Wrapper to help with understanding how the wrapper works +and therefore also help with developing for it. + +### Implementation Notes + +For Samples to be able to be used and stored in Python, it was necessary to +create a data structure that can store them. +This was done with a simple Array implementation that can hold the type +`void ***` equivalent to `vsample **`, which is cast to `vsample *` when used by bindings for singular samples. These are then cast to `villas::node::Sample *`. +- `void ***:` list to a set of samples stored in a data structure +- `vsample **:` indicative of samples to be used with this data structure +- `vsample *:` indicative of a singular sample to be used +- `villas::node::Sample ` or `Sample *:` cast to the sample + + +The Sample Array automatically uses sample_decref() on a sample, if its entry is overwritten or for all the Samples it holds as long as they do exist (differ from the nullptr) and the Array is deleted/destroyed. +This makes the use easier and less annoying in python. +Further this reduces the API calls between C/C++ and Python. + +For node_to_json() to return a json object, as the json library in Python itself would do, a wrapper function had to be created to translate the possible json return types to the same ones [Python would translate them to](#json_wrapper). +This function is implemented recursively and unfortunately const can not be used, as the for each function expects a non const json_t *. +Using a recursive function should be fine, as json objects are not typically that deep. +Further it is only used by the tool itself to generate a valid json_t translated python object, thus it is not exposed to the wrapper and can not be used to create a "configuration bomb". + +
+ Json-Wrapper for C/C++/Python + +``` +#include +#include + +namespace py = pybind11; + +py::object json_to_py_json(json_t *json) { + switch(json_typeof(json)) { + case JSON_NULL: + return py::none(); + + case JSON_INTEGER: + return py::int_(json_integer_value(json)); + + case JSON_REAL: + return py::float_(json_real_value(json)); + + case JSON_TRUE: + return py::bool_(json_string_value(json)); + + case JSON_FALSE: + return py::bool_(json_boolean_value(json)); + + case JSON_STRING: + return py::str(json_string_value(json)); + + case JSON_OBJECT: { + py::dict dict; + const char *key; + json_t *value; + + json_object_foreach(json, key, value) { + dict[py::str(key)] = json_to_py_json(value); + } + return dict; + } + + case JSON_ARRAY: { + py::list list(json_array_size(json)); + size_t index; + json_t *value; + + json_array_foreach(json, index, value) { + list[index] = json_to_py_json(value); + } + return list; + } + + default: + throw std::runtime_error("Unsupported JSON type"); + } +} +``` +
+ +--- +### Pybind11 (and why not SIP) + +Late in the process, I've realized that not declaring +``` +extern "C"{ +#include +} +``` +but rather +``` +#include +``` +has been the cause for not being able to link bindings properly to libvillas. +This may be, next to some shared issues with Pybind11, the main cause that caused me to abandon SIP. +Other problems include Pybind11 or SIP not being able to deal with pointers of type `void **` or `void ***`. +Pybind11 as well as SIP can only handle `void *` pointers natively. +Nevertheless this does not directly pose an issue, as void pointers can simply be cast to any other type within the implemented functions. + +**An example:** + +``` +typedef void *vnode; + +PYBIND11_MODULE(villas_node, m) { + ... + #binding for 'node_start()' + m.def("node_start", + + #lambda function using a pointer of type 'void *' + [](void *n) -> int { + return node_start((vnode *)n); #casting pointer to type 'vnode *' + }); + ... +} + +#C-API of VILLASnode called by the node_start() binding +int node_start(vnode *n) { + auto *nc = (Node *)n; #'vnode *' cast to the Node type + return nc->start(); +} +``` + +SIP being originally developed to be used with PyQt may be a mighty tool, but there is too much unneccessary overhead required to learn in comparison to Pybind11, causing SIP to be not as straight forward and "barebones" as Pybind11. +Since writing bindings with SIP does not directly leverage the Python C API, but rather uses tools to translate and at the same time optimize everything, you have to rely on SIP being flawless. + +This is where Pybind11 has the advantage of being a header only library, essentially being Macros that directly use the Python C API. This gives you more direct control as for how your code works and interacts with C/C++/Python. +Especially Pybind11 being the more lightweight solution, considering dependencies, installation and integration, makes it more favorable to use. + +--- +### Some Pybind11 basics: + +The [example above](#node_start()) already shows how Pybind11 is used for the most part. +For convenience: everything used to create the Wrapper bindings with Pybind11 is in the following documented briefly. +The full Pybind11 Documentation can be found [here](https://pybind11.readthedocs.io/en/stable/index.html). + +### Module Definitions + +`PYBIND11_MODULE(module_name, m)` + +- `module_name:` module_name +- `m:` module identifier to define module bindings + +`m.doc() = "":` docstring for the Python module + +`m.def()` takes different inputs to define Bindings separated by `,` + +- `"":` defines the binding name as found in the module imported into Python + +- `(param1, param2, ...)[] -> { #code }:` lambda function to define the implementation if not already done elsewhere. +If already defined `&function_name` automatically uses this implementation for the binding. +Return types can be of C/C++ standard or STL-Types and will be translated to corresponding Python Types. +However you can also define Python return types [directly](#node_to_json()), +this can be as plain as a generic Python Object `pybind11::object`. +Pointers are returned within [capsules](https://docs.python.org/3/c-api/capsule.html) to Python and can therefore not be used directly within Python, for the most part, without having a wrapped function that can make use of them. + +- `optional: pybind11::return_value_policy:::` automatically determined, but not always as to the desired behaviour. +Can be explicitly defined with `:` +`take_ownership`, `copy`, `move`, `reference`, `reference_internal`, `automatic`, `automatic_reference` +Further documentation can be found [here](https://pybind11.readthedocs.io/en/stable/advanced/functions.html). + +- `optional: :` docstring for a binding +### Classes +Can be implemented like you would implement them in C++. +These are extended by the Python object types through Pybind11. +Classes exposed to Python have to be defined with [bindings](#class_definition). + +**These are as follows:** +`pybind11::class_(module_identifier, "Python object name")` +- class_name has to match the C++ defined class name +- module identifier can be chosen freely, [in this given example](#node_start()) it would be `m` +- `"Python object name"` sets the name of the class becoming available as a Python object + +Further definitions are extended by `.`. + +`.def()` for classes follows the same pattern as `module_identifier.def()` for the bindings. +It is however important to define e.g. the **constructor**. +**Set** and **get functions can be implemented, which will always be +called upon writing or reading to or from an object of the class. + +- `pybind::init()` is an essential argument for the constructor +- `.def("__getitem__", ...)` +- `.def("__setitem__", ...)` + +`"__getitem__"` and `"__setitem__"` are predefined by Python. +In other words: these names have to be used in order to define the +get and set functions for the Python object. + +
+ Array Class Bindings as example + +``` +py::class_(m, "SamplesArray") + .def(py::init(), py::arg("len")) + .def("size", &Array::size) + .def("__getitem__", [](Array &a, unsigned int idx) { + if (idx >= a.size()) { + throw py::index_error("Index out of bounds"); + } + return a[idx]; + }) + .def("__setitem__", [](Array &a, unsigned int idx, void *smp) { + if (idx >= a.size()) { + throw py::index_error("Index out of bounds"); + } + if (a[idx]) { + sample_decref(a[idx]); + } + a[idx] = (vsample *)smp; + }); +``` +
+ +--- +Pointer return types are returned to Python as [capsules](https://docs.python.org/3/c-api/capsule.html) (addresses can be seen and dereferenced, but are essentially useless in Python). +Capsules are a "safe" way of having pointers in Python and using them with wrapped C++ functions. + +Further, capsules can be used to ensure lifetime constraints, if you use them as a return object. You can use a lambda function to return a capsule with a defined destructor that is executed when the object is garbage collected and thus deleted by Python. + +
+ Example for a capsule return + +``` +#node_new() if it just accepted proper configurations as per VILLASnode documentation +m.def("node_new", [](const char *id_str, const char *json_str) -> py::capsule { + json_error_t err; + uuid_t id; + + uuid_parse(id_str, id); + auto *json = json_loads(json_str, 0, &err); + + void *it = json_object_iter(json); + json_t *inner = json_object_iter_value(it); + + // create node with name + auto node = (vnode *)villas::node::NodeFactory::make(json_object_iter_value(it), id, json_object_iter_key(it)); + + return py::capsule(node, [](void *p) { + auto *vnode = (vnode *)(p); + if (vnode != nullptr){ + node_destroy(vnode); + } + }) + }); +``` +
+ +Function parameters in Pybind11 are strict, a wrong type will cause a function call to fail and a runtime error to be caused. + +--- +### 6. Installation + +There are two recommended methods to install the VILLASnode Python-Wrapper. + + +1. Using one of the compatible Docker Containers which can be found [in the VILLASnode repository](https://github.com/VILLASframework/node/tree/python-wrapper/packaging/docker) + or can be installed [as is described here](../../installation.md). The Fedora container, which is also the development + container would be recommended first and foremost. +2. Build VILLASnode from source [as is described here](../installation.md) and make sure to have all of the necessary + dependencies installed. + +**The requirements for the Python-Wrapper differ from the Versions listed in 2.** + +| Package | Version | Purpose | License | +| --- | --- | --- | --- | +| [CMake](http://cmake.org/) | >= 3.15 | for generating the build-system | BSD 3 | +| [pybind11](https://github.com/pybind/pybind11) | >= 2.13 | for building the Python-Wrapper | BSD 3 | +| [python](https://python.org/) | >= 3.7 | building and using the Python-Wrapper | PSFL |