--- /dev/null
+graph.crt
+config.json
+*-briefbogen.pdf
--- /dev/null
+{
+ "name": "graph.example.com",
+ "unix_url": "unix:///var/lib/graph/var/run/graphserver.sock",
+ "tls_url": "tls://graph.example.com:4339"
+}
--- /dev/null
+# Python Graph API
+
+This is a minimal Python API for the Graph.
+It works with Python 2 as well as Python 3.
+
+## Prerequisites
+
+The Graph API uses [msgpack](https://msgpack.org/).
+So, the Python binding for it has to be installed.
+
+For Debian-based distributions these can be done by:
+```sh
+# apt install python-msgpack python3-msgpack
+```
+
+With pip, it can be done by:
+```sh
+# pip install msgpack
+```
+
+In order to use TLS connections to graphs, a client certificate chain has to
+be available in a file.
+
+## Usage
+
+A graph connection is initialised by giving a URI and optionally the filename
+of the certificate chain to the constructor:
+```python
+>>> import graph
+>>> gc = graph.Connection('tls://graph.de.screwerk.com:4437', 'graph.crt')
+```
+
+Then all functions of the Graph API can be called on the connection object:
+```python
+>>> graphmodul_guid = gc.attributsknoten('graphmodul_name', 'graph')
+>>> knotens = gc.listeattribute(graphmodul_guid, 'knoten', 'typ, dokumentation, anzahlattribute', 'typ asc')
+```
+
+Observe that all string data returned by the API are bytestrings (even the
+keys in `*attribute` results) and have to be decoded to be used as Python
+strings:
+```python
+>>> for knoten in knotens.values():
+... print("{}: {} ({})".format(knoten[b'typ'].decode(), knoten[b'dokumentation'].decode(), knoten[b'anzahlattribute'].decode()))
+...
+aktion: <p>Auszuführende Aktion</p>
+ (5)
+aktionfunktion: <p>Auszuführende Funktion, die an einem Knoten hinterlegt werden und von Änderung an lokalen Attributen getriggert werden kann.</p>
+ (22)
+attribut: <p>Allgemeines Merkmal</p>
+ (16)
+[…]
+```
+
+## Test script
+
+The `test.py` script reads the configuration of a main graph from
+`config.json`, queries this main graph for remote graphs configured in it,
+prints a summary of all graph modules in the main graph and all remote graphs
+and downloads stationeries in all the graphs if they are configured there.
--- /dev/null
+from collections import OrderedDict
+from socket import socket, AF_UNIX, AF_INET, SOCK_STREAM
+from ssl import SSLContext, PROTOCOL_TLSv1
+from struct import pack, unpack
+try:
+ from urllib.parse import urlparse
+except ImportError:
+ from urlparse import urlparse
+
+from msgpack import packb, unpackb
+
+
+class Connection:
+ def __init__(self, url, crt_filename=''):
+ self.__url = url
+ self.__crt_filename = crt_filename
+ self.__cid = 0
+ self.__sock = False
+
+ def __del__(self):
+ if self.__sock:
+ self.__sock.close()
+
+ def __init(self):
+ res = urlparse(self.__url)
+ if res.scheme == 'unix':
+ self.__sock = socket(AF_UNIX, SOCK_STREAM)
+ self.__sock.connect(res.path)
+ if res.scheme == 'tcp':
+ self.__sock = socket(AF_INET, SOCK_STREAM)
+ self.__sock.connect((res.hostname, res.port))
+ if res.scheme == 'tls':
+ ssl_ctx = SSLContext(PROTOCOL_TLSv1)
+ ssl_ctx.load_cert_chain(self.__crt_filename)
+ self.__sock = socket(AF_INET, SOCK_STREAM)
+ self.__sock = ssl_ctx.wrap_socket(self.__sock)
+ self.__sock.connect((res.hostname, res.port))
+ self.__read()
+
+ def __read(self):
+ size = self.__sock.recv(4)
+ size = unpack('<L', size)[0]
+ mesg = b''
+ while len(mesg) < size:
+ to_be_read = size - len(mesg)
+ mesg += self.__sock.recv(to_be_read)
+ return unpackb(
+ mesg,
+ object_pairs_hook=lambda pair_list: OrderedDict(pair_list),
+ raw=True)
+
+ def __write(self, mesg):
+ mesg = packb(mesg, use_bin_type=False)
+ size = pack('<L', len(mesg))
+ self.__sock.sendall(size)
+ self.__sock.sendall(mesg)
+
+ def __encode(self, struct):
+ if isinstance(struct, dict):
+ return {self.__encode(key): self.__encode(value)
+ for key, value in struct.items()}
+ elif isinstance(struct, list):
+ return [self.__encode(element)
+ for element in struct]
+ elif isinstance(struct, str):
+ return struct.encode()
+ else:
+ return struct
+
+ def __call(self, method, params):
+ if self.__cid == 0:
+ self.__init()
+ self.__cid += 1
+ req = {b'jsonrpc': b'2.0',
+ b'method': self.__encode(method),
+ b'params': self.__encode(params),
+ b'id': self.__cid}
+ self.__write(req)
+ res = self.__read()
+ if b'jsonrpc' not in res or res[b'jsonrpc'] != req[b'jsonrpc']:
+ raise Exception('Not a JSON-RPC 2.0 response!')
+ if b'error' in res:
+ err = res[b'error'].decode()
+ raise Exception('JSON-RPC: Remote error: {0}'.format(err))
+ if b'id' not in res or res[b'id'] != req[b'id']:
+ raise Exception('JSON-RPC id missing or invalid')
+ return res[b'result']
+
+ def __getattr__(self, attr):
+ return lambda *args: self.__call(attr, args)
--- /dev/null
+#!/usr/bin/python3 -Bu
+# (also works in Python 2)
+import json
+import os
+import textwrap
+
+import graph
+
+crt_filename = os.path.dirname(os.path.abspath(__file__)) + '/graph.crt'
+config_filename = os.path.dirname(os.path.abspath(__file__)) + '/config.json'
+with open(config_filename) as config_file:
+ config = json.load(config_file)
+
+wrapper = textwrap.TextWrapper()
+wrapper.width = 80
+wrapper.initial_indent = ' '
+wrapper.subsequent_indent = ' '
+
+# Test if main graph is reachable by Unix domain socket:
+try:
+ graphs = [(config['name'], config['unix_url'])]
+ gc = graph.Connection(config['unix_url'])
+ gc.attributsknoten('knoten_typ', 'knoten')
+except Exception:
+ graphs = [(config['name'], config['tls_url'])]
+ gc = graph.Connection(config['tls_url'], crt_filename)
+
+# All remote graphs, sorted by url:
+remotegraphs = gc.alleattribute(
+ 'remotegraph',
+ 'name, url',
+ 'url',
+ 'name != :l and name != :g',
+ {':l': 'local', ':g': config['name']})
+for remotegraph_guid, remotegraph in remotegraphs.items():
+ graphs.append((remotegraph[b'name'].decode(),
+ remotegraph[b'url'].decode()))
+
+# For all graphs, print name and url and list of installed graph modules:
+for name, url in graphs:
+ print(name + ': ' + url)
+ gc = graph.Connection(url, crt_filename)
+
+ graphmodule = gc.alleattribute('graphmodul', 'name', 'name')
+ graphmodule = ', '.join([gm[b'name'].decode()
+ for gm in graphmodule.values()])
+ print(wrapper.fill(graphmodule))
+
+ aa_guid = gc.attributsknoten('auftragsabwicklung_name',
+ 'auftragsabwicklung')
+ if aa_guid:
+ aa_briefbogen = gc.attribut(aa_guid, 'auftragsabwicklung_briefbogen')
+ with open(name + '-briefbogen.pdf', 'wb') as pdf_file:
+ pdf_file.write(aa_briefbogen)
+ print(' ' + name + '-briefbogen.pdf written')