tag:blogger.com,1999:blog-67390723667497516782024-03-05T11:52:18.150+01:00Linux, Computer, Musik und was sonst noch wichtig ist...Ein paar unsortierte Gedanken über Ubuntu, Python, Computer, Musik, Linux und ein paar andere Dingenoisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.comBlogger106125tag:blogger.com,1999:blog-6739072366749751678.post-43226765297513925212023-12-12T16:39:00.003+01:002023-12-12T16:39:37.191+01:00Django: Übersetzungen von Forms im Template anzeigen<p>Neulich habe ich zum ersten Mal eines meiner mit Django erstellten Projekt übersetzt. Bis jetzt waren die komplett in englisch gehalten, jetzt habe ich eine deutsche Übersetzung hinzugefügt.</p><p>Wie Übersetzungen / Internationalisierung mit Django funktionieren ist ja ausführlich in der Doku beschrieben (<a href="https://docs.djangoproject.com/en/4.2/topics/i18n/translation/" target="_blank">Link zur Django 4.2 Doku</a>). Das hat bei mir auch alles einwandfrei funktioniert - nur wurden die Labels von Formularen aus der Datei <b>forms.py</b> im Template nicht übersetzt angezeigt.</p><p>Die <b>forms.py</b> sah so aus:</p><p></p><pre><code>from django import forms
from django.utils.translation import gettext as _
class SelectByDimensionsForm(forms.Form):
width = forms.IntegerField(label=_('Inner Width, mm'))
width_tolerance = forms.IntegerField(label=_('tolerance for width, mm'),
initial=20)
(...)</code></pre><p></p>
<p>und das Template so:</p><pre><code>{% extends 'laundercalc/base.html' %}
{% load i18n %}
{% block content %}<br />{% translate "Please enter your data in the following form:" %}
(...)
<form action="{% url 'laundercalc:manual_calc' %}" method="post">
{% csrf_token %}
{{ form.as_p }}
(...)
</form>
(...)
</code></pre><p></p><p>Während der Text aus dem Template übersetzt angezeigt wurde (wie das "<i>Please enter ...</i>") erschienen die Labels des Formular nach wie vor in Englisch. In der Übersetzungsdatei waren die Labels übersetzt hinterlegt.</p><p>Die Lösung, wie man die Übersetzung angezeigt bekommt, ist ganz einfach: man muss nur <code>gettext_lazy</code> in der <b>forms.py</b> importieren, also <code>from django.utils.translation import gettext_lazy as _</code> (statt <code>...import gettext as _</code>) verwenden.</p><p>Dann wird die Übersetzung erst abgerufen, wenn diese wirklich benötigt wird, im gegebenen Fall also, wenn das Template gerendert wird. Und dann wird es auch komplett übersetzt angezeigt.<br /></p><p>Weitere Informationen zu <code>gettext_lazy</code> sind auch in der <a href="https://docs.djangoproject.com/en/4.2/topics/i18n/translation/#lazy-translations" target="_blank">Django Dokumentation</a> zu finden.<br /></p>noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-75291242673755347872023-11-22T20:37:00.001+01:002023-11-26T20:18:30.459+01:00REST-API mit Python und hug für Pimoroni Blinkt LED Leiste erstellen<p>In diesen kleinen Projekt wird mit Hilfe des Python Webframeworks <a href="https://www.hug.rest/" target="_blank">hug</a> eine einfache webbasierte REST-API zum Ansteuern der <a href="https://shop.pimoroni.com/products/blinkt?variant=22408658695" target="_blank">Blinkt</a> LED-Leiste von Pimoroni für den Raspbery Pi erstellt.</p><p>Das ganze hat keinen speziellen Grund, es ist für mich aber eine gute Gelegenheit, mal etwas mit hug zu erstellen. Wollte ich schon länger, es hatte sich bis dato aber keine Gelegenheit ergeben.</p><h2 style="text-align: left;">Ziel</h2><p>Ziel des Programmierprojekt ist es, eine webbasierte API / Steuerung für die Blinkt-Leiste zu erstellen, genutzt werden dafür nur GET-Requests. Konkret bedeutet dies, das z.B. über den Aufruf der URL <span style="font-family: courier;">http://ip_adresse:port/color/red</span> alle acht LEDs des Blinkt auf rot zu stellen. Die LEDs der Blinkt Leiste werden von Pimoroni übrigens "Pixel" genannt, dieser Begriff wird auch im weiteren hier verwendet.<br /></p><p>Die API soll die folgenden Funktionen bieten:</p><ul style="text-align: left;"><li>alle Pixel auf eine vordefinierte Farbe stellen</li><li>Helligkeit für alle Pixel einstellen</li><li>für jedes der Pixel eine separate Farbe einstellen</li><li>für jedes Pixel eine separate Farbe einstellen, aber alle acht Farben auf einmal als Parameter übergeben <br /></li><li>allen Pixeln eine zufällig gewählte Farbe zuweisen</li><li>Blinkt Leiste ausschalten</li></ul><p>Die Farben sollen sich für diese Projekt nicht frei einstellen lassen, sondern es gibt sieben vordefinierte Farben: rot, orange, gelb, grün, blau, indigo, violett sowie weiß. Was den Farben das Regenbogens plus weiß entspricht.</p><h2 style="text-align: left;">Hardware</h2><p>Als Hardware kommen eine Raspberry Pi Zero W sowie die Pimoroni Blinkt Leiste zum Einsatz. Die Blinkt Leiste wird direkt auf die GPIO Pins des Raspberry Pi gesteckt. Natürlich lässt sich das Projekt auch mit jedem anderen Raspberry umsetzen, solange dieser die 40 GPIO Pins hat Auf ganz alte Raspis mit nur 28 Pins passt die Leiste nicht.</p><p></p><table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj53_5ZnDr25T1WNV5zvocWkS12GITB8zQbpbLUN8bQ9u3CVUNX3ZWrtQ9Rkb_1vSrlF4YG_-WNDhl-h5XzR8R_a8WiUrLaGLVRXke61_kVFGsON_dOm1pbw8AvWpFI4Muxhyphenhyphenodh8LFwUeiDUB01tSR1syV5LHz3OeMrDQ6HCG0N9Nyrm8T1sN8SKl5Eec/s800/pi_mit_blinkt.jpg" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="600" data-original-width="800" height="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj53_5ZnDr25T1WNV5zvocWkS12GITB8zQbpbLUN8bQ9u3CVUNX3ZWrtQ9Rkb_1vSrlF4YG_-WNDhl-h5XzR8R_a8WiUrLaGLVRXke61_kVFGsON_dOm1pbw8AvWpFI4Muxhyphenhyphenodh8LFwUeiDUB01tSR1syV5LHz3OeMrDQ6HCG0N9Nyrm8T1sN8SKl5Eec/s320/pi_mit_blinkt.jpg" width="320" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Pi Zero im Pibow Gehäuse mit aufgesteckter Blinkt LED Leiste<br /></td></tr></tbody></table><p></p><h2 style="text-align: left;"> Software</h2><p style="text-align: left;"></p><p>Als Software kommt zum Einsatz:</p><ul style="text-align: left;"><li>Raspberry Pi OS. Hier wird die zum Zeitpunkt des Schreibens aktuelle Version "Bookworm" verwendet, das Projekt funktioniert aber auch mit älteren Versionen.</li><li><a href="https://docs.python.org/3/library/venv.html" target="_blank">venv</a> zum Erstellen eines virtual Environments für Python, in dem das Programm später läuft</li><li>innerhalb des venvs <a href="https://packaging.python.org/en/latest/tutorials/installing-packages/">pip</a>, der Paketmanager für Python </li><li>das Python Modul für <a href="https://pypi.org/project/hug/" target="_blank">hug</a>, installiert via pip</li><li>das Python Modul für <a href="https://github.com/pimoroni/blinkt">Blinkt</a>, installiert aus den Paketquellen von Raspberry Pi OS</li><li><a href="https://docs.pylonsproject.org/projects/waitress/en/stable/index.html" target="_blank">waitress</a> als WSGI-Server, installiert via pip</li><li>systemd zum Erstellen einer Service Unit zum automatischen Starten des Server. systemd ist bei Raspberry Pi OS standardmäßig an Bord.</li></ul><p>Anstellen von waitress kann natürlich auf jeder andere WSGI-Applikationsserver genutzt werden, wie z.B. Gunicorn. Beim Entwickeln bzw. zum Testen kann man auch den Server nutzen, den hug mitbringt - habe ich auch in der Testphase gemacht.</p><h2 style="text-align: left;">Programm</h2><h3 style="text-align: left;">Vorbereitung</h3><p>Als erstes wird die benötigte Software installiert und das venv angelegt sowie aktiviert.<br /></p><p>Aus den Paketquellen von Raspberry Pi OS wird folgendes installiert:</p><p></p><div class="highlight" style="background: rgb(248, 248, 248);"><pre style="line-height: 125%;"><span style="color: #717171;">sudo apt install python3-venv</span>
<span style="color: #717171;">sudo apt install python3-blinkt</span>
</pre></div><p></p><p>Dann wird ein venv Names "hug_blinkt" angelegt. Ich habe dieses bei mir im Homeverzeichnis im Unterverzeichnis "code" angelegt, so dass der volle absolute Pfad <b>/home/noisefloor/code/hug_blinkt</b> lautet. Von Prinzip her geht aber natürlich auch jedes andere Verzeichnis, für das man ausreichende Rechte hat.<br /></p><p></p><div class="highlight" style="background: rgb(248, 248, 248);"><pre style="line-height: 125%;"><span style="color: #717171;">python3 -m venv hug_blinkt --system-site-packages</span>
</pre></div><p></p><p>Die Option <span style="font-family: courier;">--system-site-packages</span> ist deshalb notwendig, weil das Python-Modul für Blinkt via APT systemweit installiert ist, aber systemweite Pakete standardmäßig nicht in ein venv übernommen werden.<br /></p><p>Jetzt ins venv wechseln, dieses aktivieren und darin die Python-Module für hug und waitress installieren:</p><p></p><div class="highlight" style="background: rgb(248, 248, 248);"><pre style="line-height: 125%;"><span></span><span style="color: #717171;">cd hug_blinkt</span>
<span style="color: #717171;">source bin/activate</span>
<span style="color: #717171;">pip3 install hug</span>
<span style="color: #717171;">pip3 install waitress</span>
</pre></div><p></p><p>Nun ist alles bereit, um das Programm zu erstellen und starten.<br /></p><h3 style="text-align: left;">Quellcode</h3><p>Der komplette Quellcode für das Programm sieht wie folgt aus:</p>
<p><div class="highlight" style="background: rgb(248, 248, 248);"><pre style="line-height: 125%;"><span></span><span style="color: green; font-weight: bold;">import</span> <span style="color: blue; font-weight: bold;">random</span>
<span style="color: green; font-weight: bold;">import</span> <span style="color: blue; font-weight: bold;">hug</span>
<span style="color: green; font-weight: bold;">import</span> <span style="color: blue; font-weight: bold;">blinkt</span>
COLORS <span style="color: #666666;">=</span> {<span style="color: #ba2121;">'red'</span>: (<span style="color: #666666;">255</span>, <span style="color: #666666;">0</span> , <span style="color: #666666;">0</span>),
<span style="color: #ba2121;">'green'</span>: (<span style="color: #666666;">0</span>, <span style="color: #666666;">255</span>, <span style="color: #666666;">0</span>),
<span style="color: #ba2121;">'blue'</span>: (<span style="color: #666666;">0</span>, <span style="color: #666666;">0</span>, <span style="color: #666666;">255</span>),
<span style="color: #ba2121;">'yellow'</span>: (<span style="color: #666666;">255</span>, <span style="color: #666666;">255</span>, <span style="color: #666666;">0</span>),
<span style="color: #ba2121;">'orange'</span>: (<span style="color: #666666;">255</span>, <span style="color: #666666;">165</span>, <span style="color: #666666;">0</span>),
<span style="color: #ba2121;">'violet'</span>: (<span style="color: #666666;">255</span>, <span style="color: #666666;">87</span>, <span style="color: #666666;">51</span>),
<span style="color: #ba2121;">'indigo'</span>: (<span style="color: #666666;">75</span>, <span style="color: #666666;">0</span>, <span style="color: #666666;">130</span>),
<span style="color: #ba2121;">'white'</span>: (<span style="color: #666666;">255</span>, <span style="color: #666666;">255</span>, <span style="color: #666666;">255</span>),
}
<span style="color: #3d7b7b; font-style: italic;">#define short name for each color, which is the first letter of the color</span>
<span style="color: #3d7b7b; font-style: italic;">#as defined in COLORS</span>
<span style="color: #3d7b7b; font-style: italic;">#needs to be modified / removed once two colors with the same starting letter</span>
<span style="color: #3d7b7b; font-style: italic;">#are defined in COLORS, e.g. if ever purple and pink would be added.</span>
COLORS_SHORT <span style="color: #666666;">=</span> {key[<span style="color: #666666;">0</span>]:value <span style="color: green; font-weight: bold;">for</span> (key, value) <span style="color: #aa22ff; font-weight: bold;">in</span> COLORS<span style="color: #666666;">.</span>items()}
ALL_COLORS <span style="color: #666666;">=</span> COLORS <span style="color: #666666;">|</span> COLORS_SHORT
<span style="color: #aa22ff;">@hug</span><span style="color: #666666;">.</span>get(<span style="color: #ba2121;">'/color/</span><span style="color: #a45a77; font-weight: bold;">{color}</span><span style="color: #ba2121;">'</span>)
<span style="color: #aa22ff;">@hug</span><span style="color: #666666;">.</span>get(<span style="color: #ba2121;">'/color'</span>, examples<span style="color: #666666;">=</span><span style="color: #ba2121;">'color=red'</span>)
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">set_all_pixels</span>(color: <span style="color: green;">str</span>):
<span style="color: #bbbbbb;"> </span><span style="color: #ba2121; font-style: italic;">'''Sets all pixels to the the given color. The color is specified by name,</span>
<span style="color: #ba2121; font-style: italic;"> e.g. red or green. Color can also be given by shortname, e.g. y for yellow</span>
<span style="color: #ba2121; font-style: italic;"> or m for magenta.'''</span>
<span style="color: green; font-weight: bold;">if</span> <span style="color: #aa22ff; font-weight: bold;">not</span> color <span style="color: #aa22ff; font-weight: bold;">in</span> ALL_COLORS<span style="color: #666666;">.</span>keys():
<span style="color: green; font-weight: bold;">return</span> {<span style="color: #ba2121;">'error'</span>: <span style="color: #ba2121;">f'</span><span style="color: #a45a77; font-weight: bold;">{</span>color<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;"> is not a valid color. Please choose one of </span><span style="color: #aa5d1f; font-weight: bold;">\</span>
<span style="color: #a45a77; font-weight: bold;">{</span><span style="color: green;">list</span>(ALL_COLORS<span style="color: #666666;">.</span>keys())<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">'</span>}
<span style="color: green; font-weight: bold;">else</span>:
rgb_value <span style="color: #666666;">=</span> ALL_COLORS[color]
blinkt<span style="color: #666666;">.</span>set_all(rgb_value[<span style="color: #666666;">0</span>], rgb_value[<span style="color: #666666;">1</span>], rgb_value[<span style="color: #666666;">2</span>])
blinkt<span style="color: #666666;">.</span>show()
<span style="color: green; font-weight: bold;">return</span> {<span style="color: #ba2121;">'success'</span>: <span style="color: #ba2121;">f'set Pixels to color </span><span style="color: #a45a77; font-weight: bold;">{</span>color<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;"> (RBG: </span><span style="color: #a45a77; font-weight: bold;">{</span>rgb_value<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">)'</span>}
<span style="color: #aa22ff;">@hug</span><span style="color: #666666;">.</span>get(<span style="color: #ba2121;">'/color_for_pixel/</span><span style="color: #a45a77; font-weight: bold;">{pixel}</span><span style="color: #ba2121;">/</span><span style="color: #a45a77; font-weight: bold;">{color}</span><span style="color: #ba2121;">'</span>)
<span style="color: #aa22ff;">@hug</span><span style="color: #666666;">.</span>get(<span style="color: #ba2121;">'/color_for_pixel'</span>, examples<span style="color: #666666;">=</span><span style="color: #ba2121;">'pixel=2&color=magenta'</span>)
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">set_pixel_to</span>(pixel: hug<span style="color: #666666;">.</span>types<span style="color: #666666;">.</span>in_range(<span style="color: #666666;">1</span>, <span style="color: #666666;">9</span>), color: <span style="color: green;">str</span>):
<span style="color: #bbbbbb;"> </span><span style="color: #ba2121; font-style: italic;">'''Sets the given Pixel to the given color. Pixels are numbered 1 to 8 from</span>
<span style="color: #ba2121; font-style: italic;"> left to right. The color is specified by name, e.g. green or blue. Color can</span>
<span style="color: #ba2121; font-style: italic;"> also be given by shortname, e.g. y for yellow or m for magenta.'''</span>
<span style="color: green; font-weight: bold;">if</span> <span style="color: #aa22ff; font-weight: bold;">not</span> color <span style="color: #aa22ff; font-weight: bold;">in</span> ALL_COLORS<span style="color: #666666;">.</span>keys():
<span style="color: green; font-weight: bold;">return</span> {<span style="color: #ba2121;">'error'</span>: <span style="color: #ba2121;">f'</span><span style="color: #a45a77; font-weight: bold;">{</span>color<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;"> is not a valid color. Please choose one of </span><span style="color: #aa5d1f; font-weight: bold;">\</span>
<span style="color: #a45a77; font-weight: bold;">{</span><span style="color: green;">list</span>(ALL_COLORS<span style="color: #666666;">.</span>keys())<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">'</span>}
<span style="color: green; font-weight: bold;">else</span>:
pixel<span style="color: #666666;">-=1</span>
rgb_value <span style="color: #666666;">=</span> ALL_COLORS[color]
blinkt<span style="color: #666666;">.</span>set_pixel(pixel, rgb_value[<span style="color: #666666;">0</span>], rgb_value[<span style="color: #666666;">1</span>], rgb_value[<span style="color: #666666;">2</span>])
blinkt<span style="color: #666666;">.</span>show()
<span style="color: green; font-weight: bold;">return</span> {<span style="color: #ba2121;">'success'</span>: <span style="color: #ba2121;">f'set Pixel </span><span style="color: #a45a77; font-weight: bold;">{</span>pixel<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;"> to color </span><span style="color: #a45a77; font-weight: bold;">{</span>color<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;"> (RBG: </span><span style="color: #a45a77; font-weight: bold;">{</span>rgb_value<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">)'</span>}
<span style="color: #aa22ff;">@hug</span><span style="color: #666666;">.</span>get(<span style="color: #ba2121;">'/colors_for_pixels/</span><span style="color: #a45a77; font-weight: bold;">{colors}</span><span style="color: #ba2121;">'</span>)
<span style="color: #aa22ff;">@hug</span><span style="color: #666666;">.</span>get(<span style="color: #ba2121;">'/colors_for_pixels'</span>, examples<span style="color: #666666;">=</span><span style="color: #ba2121;">'colors=green,red,yellow,magenta,white,blue,y,g'</span>)
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">set_pixels_to</span>(colors: hug<span style="color: #666666;">.</span>types<span style="color: #666666;">.</span>delimited_list(<span style="color: #ba2121;">','</span>)):
<span style="color: #bbbbbb;"> </span><span style="color: #ba2121; font-style: italic;">'''Set each pixel to the given color specified for each Pixel. The colors</span>
<span style="color: #ba2121; font-style: italic;"> are specified by name, e.g. blue or yellow. In total eight colors needs</span>
<span style="color: #ba2121; font-style: italic;"> to be given, one for each Pixel. Colors must be separated by a comma , .'''</span>
<span style="color: green; font-weight: bold;">if</span> <span style="color: green;">len</span>(colors) <span style="color: #666666;">!=</span> <span style="color: #666666;">8</span>:
<span style="color: green; font-weight: bold;">return</span> {<span style="color: #ba2121;">'error'</span>: <span style="color: #ba2121;">'wrong length - exactly eight (8) colors separated by </span><span style="color: #aa5d1f; font-weight: bold;">\</span>
<span style="color: #ba2121;">a comma have to be provided'</span>}
<span style="color: green; font-weight: bold;">if</span> <span style="color: #aa22ff; font-weight: bold;">not</span> <span style="color: green;">all</span>(color<span style="color: #666666;">.</span>strip() <span style="color: #aa22ff; font-weight: bold;">in</span> ALL_COLORS <span style="color: green; font-weight: bold;">for</span> color <span style="color: #aa22ff; font-weight: bold;">in</span> colors):
<span style="color: green; font-weight: bold;">return</span> {<span style="color: #ba2121;">'error'</span>: <span style="color: #ba2121;">f'There is at least one invalid value in </span><span style="color: #a45a77; font-weight: bold;">{</span>colors<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">. </span><span style="color: #aa5d1f; font-weight: bold;">\</span>
<span style="color: #ba2121;"> Please ensure all values are one of </span><span style="color: #a45a77; font-weight: bold;">{</span>ALL_COLORS<span style="color: #666666;">.</span>keys()<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">'</span>}
_set_color_for_each_pixel(colors)
<span style="color: green; font-weight: bold;">return</span> {<span style="color: #ba2121;">'success'</span>: <span style="color: #ba2121;">f'set Pixels 1 to 8 to colors </span><span style="color: #a45a77; font-weight: bold;">{</span>colors<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">'</span>}
<span style="color: #aa22ff;">@hug</span><span style="color: #666666;">.</span>get(<span style="color: #ba2121;">'/brightness/</span><span style="color: #a45a77; font-weight: bold;">{value}</span><span style="color: #ba2121;">'</span>)
<span style="color: #aa22ff;">@hug</span><span style="color: #666666;">.</span>get(<span style="color: #ba2121;">'/brightness'</span>)
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">set_brightness</span>(value: hug<span style="color: #666666;">.</span>types<span style="color: #666666;">.</span>in_range(<span style="color: #666666;">1</span>, <span style="color: #666666;">100</span>),
examples<span style="color: #666666;">=</span><span style="color: #ba2121;">'value=50'</span>):
<span style="color: #bbbbbb;"> </span><span style="color: #ba2121; font-style: italic;">'''Sets the brightness of all Pixels to the given value. Value must be an</span>
<span style="color: #ba2121; font-style: italic;"> integer between 1 and 99'''</span>
value <span style="color: #666666;">=</span> <span style="color: green;">round</span>(value<span style="color: #666666;">/100</span>, <span style="color: #666666;">2</span>)
blinkt<span style="color: #666666;">.</span>set_brightness(value)
blinkt<span style="color: #666666;">.</span>show()
<span style="color: green; font-weight: bold;">return</span> {<span style="color: #ba2121;">'success'</span>: <span style="color: #ba2121;">f'set brightness to: </span><span style="color: #a45a77; font-weight: bold;">{</span>value<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">'</span>}
<span style="color: #aa22ff;">@hug</span><span style="color: #666666;">.</span>get(<span style="color: #ba2121;">'/randomize'</span>)
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">randomize_colors</span>():
<span style="color: #bbbbbb;"> </span><span style="color: #ba2121; font-style: italic;">'''Sets all eight pixels to a randomly chosen color from the color dict.'''</span>
colors <span style="color: #666666;">=</span> random<span style="color: #666666;">.</span>choices(<span style="color: green;">list</span>(COLORS<span style="color: #666666;">.</span>keys()), k<span style="color: #666666;">=8</span>)
_set_color_for_each_pixel(colors)
<span style="color: green; font-weight: bold;">return</span> {<span style="color: #ba2121;">'success'</span>: <span style="color: #ba2121;">'set pixels to randomly chosen colors.'</span>}
<span style="color: #aa22ff;">@hug</span><span style="color: #666666;">.</span>get(<span style="color: #ba2121;">'/show_state'</span>)
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">show_state</span>():
<span style="color: #bbbbbb;"> </span><span style="color: #ba2121; font-style: italic;">'''Shows the current state of each pixel - current RGB value and brightness.'''</span>
state <span style="color: #666666;">=</span> {}
<span style="color: green; font-weight: bold;">for</span> pixel <span style="color: #aa22ff; font-weight: bold;">in</span> <span style="color: green;">range</span>(blinkt<span style="color: #666666;">.</span>NUM_PIXELS):
value <span style="color: #666666;">=</span> blinkt<span style="color: #666666;">.</span>get_pixel(pixel)
state[<span style="color: #ba2121;">f'Pixel </span><span style="color: #a45a77; font-weight: bold;">{</span>pixel<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">'</span>] <span style="color: #666666;">=</span> <span style="color: #ba2121;">f'color: </span><span style="color: #a45a77; font-weight: bold;">{</span>value[<span style="color: #666666;">0</span>:<span style="color: #666666;">3</span>]<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">, brightness: </span><span style="color: #a45a77; font-weight: bold;">{</span>value[<span style="color: #666666;">3</span>]<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">'</span>
<span style="color: green; font-weight: bold;">return</span> state
<span style="color: #aa22ff;">@hug</span><span style="color: #666666;">.</span>get(<span style="color: #ba2121;">'/off'</span>)
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">leds_off</span>():
<span style="color: #bbbbbb;"> </span><span style="color: #ba2121; font-style: italic;">'''Turns off all pixels.'''</span>
blinkt<span style="color: #666666;">.</span>clear()
blinkt<span style="color: #666666;">.</span>show()
<span style="color: green; font-weight: bold;">return</span> {<span style="color: #ba2121;">'success'</span>: <span style="color: #ba2121;">'turned off all Pixels'</span>}
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">_set_color_for_each_pixel</span>(colors):
<span style="color: #bbbbbb;"> </span><span style="color: #ba2121; font-style: italic;">'''Helper function to set each pixel to a given color.</span>
<span style="color: #ba2121; font-style: italic;"> Expects one argument, which has to be an iterable holding the names of exactly</span>
<span style="color: #ba2121; font-style: italic;"> eight colors.'''</span>
<span style="color: green; font-weight: bold;">assert</span> <span style="color: green;">len</span>(colors)<span style="color: #666666;">==8</span>
rgb_values <span style="color: #666666;">=</span> [ALL_COLORS[color] <span style="color: green; font-weight: bold;">for</span> color <span style="color: #aa22ff; font-weight: bold;">in</span> colors]
<span style="color: green; font-weight: bold;">for</span> position, rgb_value <span style="color: #aa22ff; font-weight: bold;">in</span> <span style="color: green;">enumerate</span>(rgb_values):
blinkt<span style="color: #666666;">.</span>set_pixel(position, rgb_value[<span style="color: #666666;">0</span>], rgb_value[<span style="color: #666666;">1</span>], rgb_value[<span style="color: #666666;">2</span>])
blinkt<span style="color: #666666;">.</span>show()
</pre></div><p>
<p><span style="font-family: courier;">set_all_pixels(color: str)</span> bindet die Route <span style="font-family: courier;">color</span> an die Funktion <span style="font-family: courier;">set_all_pixels</span>. Die Route und die daran gebundene Funktion erwarten ein Parameter Namens <span style="font-family: courier;">color</span>, welches auf zwei Wege übergeben werden kann: <span style="font-family: courier;">color?color=red</span> oder <span style="font-family: courier;">color/red</span>.</p><p>Im Wörterbuch <span style="font-family: courier;">COLORS</span> werden die erlaubten Farben definiert, was, wie weiter oben bereits erwähnt, die Regenbogenfarben plus weiß sind. Außerdem wird daraus das Wörterbuch <span style="font-family: courier;">COLORS_SHORT</span> abgeleitet. Darin ist definiert, dass auch nur der Anfangsbuchstabe jeder Farbe als Farbangabe genutzt werden darf, also z.B. <span style="font-family: courier;">y</span> für <span style="font-family: courier;">yellow</span>. Alle Funktionen, denen ein oder mehrere Farben übergeben werden, prüfen, ob die Farbangabe bzw. die Farbangaben gültig sind, sprich im Wörterbuch <span style="font-family: courier;">ALL_COLORS</span> vorkommen.</p><p>Alle Funktionen, die an eine Route gebunden sind, liefern als Antwort JSON zurück. Dies ist der Standard von hug. Das Wörterbuch nach dem <span style="font-family: courier;">return</span> wird von hug in ein JSON-Objekt umgewandelt und mit dem passenden Content-Type Header versehen. hug kennt außer JSON aber natürlich auch noch andere <a href="https://www.hug.rest/website/learn/output_formats" target="_blank">Ausgabeformate</a>.</p><p>Was meines Erachtens bei hug ganz praktisch ist sind die <a href="https://hugapi.github.io/hug/documentation/TYPE_ANNOTATIONS/" target="_blank">Type Annotations</a>, welche man optional bei den Route angeben kann. Diese können ganz gewöhnlich sein wie bei <span style="font-family: courier;">color: str</span> . Aber hug kennt auch weiterführende, komplexere Annotations. Hier im Programm verwendet werden zwei weitere:<br /><span style="font-family: courier;">value: hug.types.in_range(1, 100)</span> prüft und stellt sicher, dass für <span style="font-family: courier;">value</span> nur Zahlen zwischen 1 und 99 akzeptiert werden.<br /><span style="font-family: courier;">colors: hug.types.delimited_list(',')</span> stellt sicher, dass für <span style="font-family: courier;">colors</span> nur durch ein Komma separierte Liste von Werten akzeptiert wird, wie z.B. <span style="font-family: courier;">red,yellow,blue</span>. Werte wie <span style="font-family: courier;">red;yellow;blue</span> würden zu einem Fehler führen. Nach erfolgreicher Prüfung wandelt hug die Werte praktischerweise direkt in eine Python-Liste um.<br />Eine tiefer gehende Erklärung zu den Type Annotations in hug sind <a href="https://hugapi.github.io/hug/reference/hug/types/" target="_blank">in der Doku</a> zu finden.</p><h3 style="text-align: left;">Programm testen</h3><p>Zum Testen des Programms benötigt man keinen WSGI Applikationsserver, sondern kann den in hug enthaltenen Server nutzen. Diesen ruft man mit</p><p></p><div class="highlight" style="background: rgb(248, 248, 248);"><pre style="line-height: 125%;"><span></span><span style="color: #717171;">hug -f name_des_programms.py</span>
</pre></div><p></p><p>auf. Danach ist das Programm bzw. die darüber bereit gestellt Web-API unter der Adresse <span style="font-family: courier;">127.0.0.1:8000</span> auf dem Rechner, auf dem das Programm läuft, erreichbar.</p><h3 style="text-align: left;">Deployment</h3><p>Das fertige Programm sollte zur produktiven / dauerhaften Nutzung - selbst wenn es nur im Heimnetzwerk ist - über einen WSGI-Server ausgeliefert werden, um einen stabilen Betrieb zu gewährleisten. Wie weiter oben bereits erwähnt nutze ich dafür waitress.</p><p>Der Befehl zum Starten von waitress innerhalb des venvs lautet:</p><p></p><div class="highlight" style="background: rgb(248, 248, 248);"><pre style="line-height: 125%;"><span></span><span style="color: #717171;">waitress-serve --threads=2 --listen=192.168.178.82:8001 blinkt_api:__hug_wsgi__</span>
</pre></div><p></p><p>Die Angaben für <span style="font-family: courier;">--listen</span> müssen natürlich auf die IP-Adresse des Host-Rechners angepasst werden. Ebenso können der Port sowie die Anzahl der Threads geändert werden. Der Einstiegspunkt von hug für den WSGI-Server <span style="font-family: courier;">__hug_wsgi__</span> erhält man automatisch über das <span style="font-family: courier;">import hug</span> im Skript.<br /></p><p>Wer das Programm beim Systemstart automatisch starten will kann dies auf Basis der folgenden systemd Service Unit machen:</p><p></p><div class="highlight" style="background: rgb(248, 248, 248);"><pre style="line-height: 125%;"><span></span><span style="color: green; font-weight: bold;">[Unit]</span>
<span style="color: #687822;">Description</span><span style="color: #666666;">=</span><span style="color: #ba2121;">Blinkt Server using hug as the backend</span>
<span style="color: #687822;">After</span><span style="color: #666666;">=</span><span style="color: #ba2121;">network-online.target</span>
<span style="color: green; font-weight: bold;">[Service]</span>
<span style="color: #687822;">User</span><span style="color: #666666;">=</span><span style="color: #ba2121;">DEIN_BENUTZERNAME</span>
<span style="color: #687822;">WorkingDirectory</span><span style="color: #666666;">=</span><span style="color: #ba2121;">/PFAD/ZUM/HUG/VENV</span>
<span style="color: #687822;">ExecStart</span><span style="color: #666666;">=</span><span style="color: #ba2121;">/PFAD/ZUM/HUG/VENV/bin/waitress-serve --listen=192.168.178.117:8001 --threads=2 NAME_DES_SKRIPTS:__hug_wsgi__</span>
<span style="color: #687822;">Restart</span><span style="color: #666666;">=</span><span style="color: #ba2121;">always</span>
<span style="color: green; font-weight: bold;">[Install]</span>
<span style="color: #687822;">WantedBy</span><span style="color: #666666;">=</span><span style="color: #ba2121;">multi-user.target</span>
</pre></div><p></p><p>Der Pfad zum angelegten venv muss entsprechend eingetragen werden. Da hug, waitress sowie das Python-Modul für Blinkt keine speziellen Rechte brauchen kann das ganze ohne Probleme mit Nutzerrechten laufen, Root-Rechte sind nicht notwendig.</p><h2 style="text-align: left;">abschließende Worte</h2><p>Wie schon Eingangs erwähnt habe ich das ganze Projekt primär gestartet, um mal etwas mit hug zu machen, was auch einen halbwegs produktiven Nutzen hat. Die Nutzung von hug ist ziemlich einfach und gradlinig. Ein bisschen komisch ist bei hug nur, dass die Doku auf zwei Webseiten verteilt ist, nämlich <a href="https://www.hug.rest/website/learn/">https://www.hug.rest/website/learn/</a> und <a href="https://hugapi.github.io/hug/">https://hugapi.github.io/hug/</a> </p><p>hug kann übrigens nicht nur webbasierte REST-APIs, sondern auch APIs für die Kommandozeile. Und hug hat noch wesentlich mehr Funktionen und Möglichkeiten als die hier gezeigten / genutzten.</p><p>Das hier gezeigt Programm ist natürlich auch nicht auf die Nutzung in Kombination mit der Blinkt LED-Leiste beschränkt. Das Prinzip lässt sich leicht auch auf andere Hardware übertragen, für die es ein Python-Modul zu Ansteuerung gibt.<br /></p><p>Mit der Blinkt LED Leiste lässt sich übrigens sehr gut und einfach eine DIY Ambilight Beleuchtung hinter einem Monitor, Fernseher oder ähnlichem realisieren. Dazu einfach einen Raspberry Pi Zero W mit aufgesteckter Blinkt LED-Leiste hinter einem Monitor anbringen und die Pixel der Blinkt Leiste über eine Web REST-API wie hier gezeigt ansteuern.<br /></p>noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-92076393418808963622023-11-12T20:39:00.002+01:002023-11-12T20:39:43.515+01:00GNU Nano Editor unter Windows intstallieren<p>Neulich war ich im Terminal von Windows unterwegs und wollte mal schnell eine einfache Änderung in einer Textdatei machen. Unter Linux wäre das ein Fall für <a href="https://www.nano-editor.org/ " target="_blank">den Nano Editor</a> gewesen - unter Windows gibt es nach meinem Wissen im Terminal aber keinen vorinstallierten, textbasierten Editor. Also Google befragt, was es so gibt. Und, siehe da, es gibt tatsächlich Nano nicht nur für Linux, sondern auch für Windows.</p><p>Installieren lässt sich Nano auch direkt im Terminal über eine Paketverwaltung für Windows namens <a href="https://github.com/winget-run/wingetdotrun" target="_blank">winget</a>. Das ist unter Windows 11 und den neueren Versionen von Windows 10 normalerweise vorinstalliert. Wenn nicht kann man es einfach nachinstallieren, eine Anleitung und weitere Informationen sind auf der <a href="https://learn.microsoft.com/de-de/windows/package-manager/winget/" target="_blank">Infoseite von Microsoft</a> zu finden.</p><p>Die Installation ist ganz einfach: einfach im Terminal den Befehl</p><p><code></code></p><pre><code><span style="background-color: #444444; color: #f3f3f3;">winget install -e --id GNU.Nano </span></code></pre><p></p><p>ausführen und die aktuelle Nanio-Version wird installiert.</p><p>Über winget lässt sich auch noch andere Software aus dem <a href="https://www.gnu.org/home.en.html" target="_blank">GNU Projekt</a> installieren, wie z.B. der Editor Emacs oder der textbasierte Dateimanager Midnight Commander. Eine Übersicht über die installierbare Software des GNU Projekt ist <a href="https://winget.run/pkg/GNU/" target="_blank">auf dieser Seite</a> zu finden.</p><p>Leider ist es mir (bis jetzt) nicht gelungen die Stelle herauszufinden, wo man die Konfigurationsdatei <b>.nanorc</b> ablegen muss, um Nano Konfigurationsoptionen wie automatisches Einrücken, Zeilennummern, Leerzeichen statt Tab usw. mitzugeben.</p><p>Nano ist sicherlich nicht der beste Editor für Windows, aber für einen schnellen Edit im Terminal bzw. in der Powershell, ohne diese zu verlassen, gut genug.</p><table cellpadding="0" cellspacing="0" class="tr-caption-container" style="float: left;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiy32VCoXVJD9fyYX06mupcNARJw6G8A7xKdkQGVBcbEoRGYlEu598_q5M__wxEzeEc0-Le-blu-f76VzQxNInO9VF2cbaAB5EsnBwRboxksq5uyF2MJPIFXjkT57H9Xr2NonJfeoU0cuAsPtR1W4Y_Ur0x2WFytsldlPBPrA7B-1ATcdqn65yxrWXwQxM/s1482/nano_windows.jpg" imageanchor="1" style="clear: left; margin-bottom: 1em; margin-left: auto; margin-right: auto;"><img border="0" data-original-height="791" data-original-width="1482" height="171" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiy32VCoXVJD9fyYX06mupcNARJw6G8A7xKdkQGVBcbEoRGYlEu598_q5M__wxEzeEc0-Le-blu-f76VzQxNInO9VF2cbaAB5EsnBwRboxksq5uyF2MJPIFXjkT57H9Xr2NonJfeoU0cuAsPtR1W4Y_Ur0x2WFytsldlPBPrA7B-1ATcdqn65yxrWXwQxM/s320/nano_windows.jpg" width="320" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;">Nano in der Windows Powershell<br /></td></tr></tbody></table><br /><p><br /></p><p><br /></p><p><br /></p>noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0Deutschland51.165691 10.45152622.855457163821157 -24.704724 79.475924836178848 45.607776tag:blogger.com,1999:blog-6739072366749751678.post-15428913185659909042023-10-29T17:00:00.001+01:002023-10-29T17:00:25.191+01:00ein snap Paket untersuchen<h1 id='ein-snap-paket-untersuchen'>ein snap Paket untersuchen</h1>
<p>snap Pakete bestehen aus einer einzelnen Datei mit der Dateiendung <strong>.snap</strong> . Diese Datei ist technisch gesehen ein SquashFS-Dateisystem, in das alle Dateien, die das snap enthält, verpackt sind.</p>
<p>Was in dem snap Paket enthalten ist kann man unter- / durchsuchen. Je nach dem, ob das snap ins System eingehängt ist oder nicht unterscheidet sich der Weg.</p>
<h1 id='eingehängtes-snap-paket-untersuchen'>eingehängtes snap Paket untersuchen</h1>
<p>Den Inhalt eines snap Pakets, welches ins System eingehängt ist, zu unter- / durchsuchen benötigt keine weiteren Hilfsmittel. Man navigiert einfach zum Einhängepunkt, also <strong>/snap</strong> . Dann geht man den Ordner des gewünschten snaps, z.B. <strong>firefox</strong> für das Firefox snap. In diesem Ordner befindet sich immer ein Verzeichnis <strong>current</strong> , welches immer ein Link auf die aktuelle, installiert, eingehängte Version des snaps ist. Wechselt man in dieses Verzeichnis sieht man alle Dateien und Verzeichnisse, die zum snap gehören. Durch diese Verzeichnisse kann man navigieren, sich Dateien anzeigen lassen usw. Befindet man sich z.B. im Verzeichnis <strong>/snap/firefox/current</strong> kann man sich im Terminal über </p>
<pre><code class='language-shell' lang='shell'>less firefox.desktop
</code></pre>
<p>die im snap enthaltene <a href='https://wiki.ubuntuusers.de/.desktop-Dateien/'>.desktop-Datei</a> anzeigen lassen.</p>
<h1 id='nicht-eingehängtes-snap-paket-untersuchen'>nicht eingehängtes snap Paket untersuchen</h1>
<p>Ein snap Paket muss aber nicht zwingend eingehängt sein, um es zu untersuchen. Man kann auch heruntergeladen bzw. nicht eingehängte Pakete untersuchen. Komfortable geht dies, indem man Programme aus den <a href='https://github.com/AgentD/squashfs-tools-ng'>squashfs-tools-ng </a> nutzt.</p>
<h2 id='vorbereitung-squashfs-tools-ng-installieren'>Vorbereitung: squashfs-tools-ng installieren</h2>
<p>Das Paket "squashfs-tools-ng" ist in den Paketquellen einiger Linux-Distribution enthalten. Eine Übersicht findet man auf der <a href='https://github.com/AgentD/squashfs-tools-ng#installing'>Projektseite</a>.</p>
<p>Unter Ubuntu / Debian / Raspberry Pi OS kann man das Paket über</p>
<pre><code class='language-shell' lang='shell'>sudo apt install squashfs-tools-ng
</code></pre>
<p>installieren.</p>
<h2 id='inhalt-anzeigen-lassen'>Inhalt anzeigen lassen</h2>
<p>Nach der Installation von "squashfs-tools-ng" steht unter anderem das Programm <code>rdsquashfs</code> zur Verfügung. Mit dessen Hilfe kann man sich den Inhalt eines SquashFS-Dateisystems - und damit auch eines snaps - anzeigen lassen.</p>
<p>Zuerst wechselt man in der Verzeichnis, wo das snap Paket liegt. Unter Ubuntu werden regulär installierte snaps unter <strong>/var/lib/snapd/snaps</strong> gespeichert. Liegt dort z.B. das snap <strong>core22_858.snap</strong> kann man sich über</p>
<pre><code class='language-shell' lang='shell'>sudo rdsquashfs core22_858.snap -l /
</code></pre>
<p>das Wurzelverzeichnis diese snaps anzeigen lassen. Das <code>sudo</code> ist hier notwendig, weil die snaps in <strong>/var/lib/snapdsnaps</strong> zum Besitzer und zur Gruppe <em>root</em> gehören und die Dateirechte mit <code>300</code> restriktiv gesetzt sind. Hätte man ein snap manuell heruntergeladen und z.B. im eigenen Home-Verzeichnis gespeichert, wären das Ausführen des Befehls mit <code>sudo</code> nicht notwendig.</p>
<p>Die Option <code>-l</code> des Programm <code>rdsquashfs</code> gibt an, welches Verzeichnis ausgegeben werden sollte. Möchte man sich z.B. alle ausführbaren Programme ausgeben lassen, die in diesem snap enthalten sind, ruft man den Befehl</p>
<pre><code class='language-shell' lang='shell'>sudo rdsquashfs core22_858.snap -l /usr/bin
</code></pre>
<p>auf.</p>
<p>Man kann sich mit Hilfe von <code>rdsquashfs</code> auch Dateien aus dem snap ausgeben lasse. Dazu muss man das Programm mit der Option <code>-c</code> gefolgt vom vollen Pfad und Dateinamen aufrufen. Im folgenden Beispiel würde die Datei <strong>/etc/systemd/journald.conf</strong> aus dem snap aufgelistet:</p>
<pre><code class='language-shell' lang='shell'>sudo rdsquashfs core22_858.snap -c /etc/systemd/journald.conf
</code></pre>
<p>Man kann sich auch einfach alle Dateien und Verzeichnisse aus dem snap auflisten lassen:</p>
<pre><code class='language-shell' lang='shell'>sudo rdsquashfs core22_858.snap -d
</code></pre>
<p><code>rdsquashfs</code> kennt noch einige weitere Optionen, unter anderem zum Entpacken von einzelnen oder allen Dateien aus dem snap Paket. Diese kann man sich über den Aufruf von</p>
<pre><code class='language-shell' lang='shell'>rdsquashfs -h
#oder
man rdsquashfs
</code></pre>
<p>anzeigen lasssen.</p>
<h2 id='zwei-snap-pakete-vergleichen'>zwei snap Pakete vergleichen</h2>
<p>Die "squashfs-tool-ng" beinhalten auch das Programm <code>sqfsdiff</code> mit dem sich der Inhalt von zwei SquashFS-Dateisystemen und damit auch von zwei snap Paketen vergleichen lässt. Im folgenden Beispiel würden zwei Version des core22 snaps, welche im aktuellen Verzeichnis gespeichert sind, vergleichen:</p>
<pre><code class='language-shell' lang='shell'>sudo sqfsdiff --old core22_858.snap --new core22_864.snap
</code></pre>
<p>Je nach dem, wie viele Dateien und Verzeichnisse im snap Paket sind bzw. sich geändert haben, kann die Ausgabe sehr lang sein. Es gibt verschiedene Arten von Unterschieden:</p>
<ul>
<li>Lautet eine Ausgabezeile <code>regular file ./pfad/zur datei/dateiname differs</code> bedeutet dies, dass die Datei und der Dateipfad in beiden snaps identisch ist, sich aber der Inhalt der Datei verändert hat.</li>
<li>Lautet eine Ausgabezeile <code>> /pfad/zur/datei/dateiname</code>bedeutet dies, diese diese Datei mit diesem Dateipfad in der neuen Datei enthalten ist, aber nicht in der alten.</li>
<li>Lautet eine Ausgabezeile <code>< /pfad/zur/datei/dateiname</code>bedeutet dies, diese diese Datei mit diesem Dateipfad in der alten Datei enthalten ist, aber nicht in der neue.</li>
<li>Lautet eine Ausgabezeile <code>/pfad/zur/datei/dateiname has a different link target</code> bedeutet dies, dass Datei und Dateipfad in beiden snaps enthalten ist, aber sich die Verlinkung innerhalb des SquashFS-Dateisystem geändert hat.</li>
</ul>
<p><code>sqfsdiff</code> kennt eine Reihe von Optionen, um die Vergleich einzuschränken. Die Optionen kann man sich über den Aufruf von</p>
<pre><code class='language-shell' lang='shell'>sqfsdiff -h
#oder
man sqfsdiff
</code></pre>
<p>anzeigen lassen.</p>
<h1 id='links'>Links</h1>
<h2 id='snap'>snap</h2>
<ul>
<li><a href='https://snapcraft.io/docs'>Startseite</a> der offiziellen Dokumentation zu snaps</li>
</ul>
<h2 id='squashfs'>squashfs</h2>
<ul>
<li><a href='https://de.wikipedia.org/wiki/SquashFS'>Wikipediaartikel</a> zu SquashFS</li>
<li><a href='https://tldp.org/HOWTO/SquashFS-HOWTO/whatis.html'>SquashFS Howto</a> beim The Linux Documentation Projekt</li>
<li><a href='https://docs.kernel.org/filesystems/squashfs.html'>SquashFS Dokumentation</a> des Linux Kernel</li>
</ul>
<h2 id='squashfs-tools-ng'>squashfs-tools-ng</h2>
<ul>
<li><a href='https://github.com/AgentD/squashfs-tools-ng'>Projektseite</a> von squashfs-tools-ng</li>
</ul>noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0Deutschland51.165691 10.45152622.855457163821157 -24.704724 79.475924836178848 45.607776tag:blogger.com,1999:blog-6739072366749751678.post-84480769306476525922022-12-30T17:52:00.003+01:002023-02-13T16:54:33.096+01:00ein snap ausprobieren / testweise installieren<h1 id='ein-snap-testen'>ein snap testen</h1>
<p><em><em>Hinweis</em>: das im folgenden beschriebene Vorgehen ist explizit mit Ubuntu 22.04 LTS durchgeführt worden. Da snapd, der Daemon, der für snaps zuständig ist, aber von Canonical für alle unterstützen Ubuntuversionen gleichermaßen gepflegt wird, sollte das Vorgehen auch für andere Ubuntuversion funktionieren. Ebenso sollte das Vorgehen für andere Linuxdistributionen funktionieren, auf denen snapd installiert ist.</em></p>
<p><a href='https://ubuntu.com/core/services/guide/snaps-intro'>snap</a>, das Paketformat von Canonical erlaubt es, ein Programm auszuprobieren, ohne dass das snap komplett installiert werden mus. </p>
<p>Im Vergleich zu einer normalen Installation eines snaps über snap install unterscheidet sich dieses Verfahren in vier Punkten: </p>
<ol start='' >
<li>Es werden keine Konfigurationsdateien im Homeverzeichnis des Nutzers unter <strong>~/.config</strong> angelegt - es werden aber trotzdem Dateien unter <strong>~/snap</strong> für das snap angelegt, das man testet.</li>
<li>Das snap wird bei einem snap refresh nicht aktualisiert.</li>
<li>Man könnte Dateien, die zum snap gehören, zur Laufzeit des snaps editieren und diese Änderung würden beim Start des Programms berücksichtigt. Was sich aber nicht ändern lässt ist das <a href='https://snapcraft.io/docs/snap-confinement'>Confinement</a> sowie die <a href='https://snapcraft.io/docs/interface-management'>Connections</a> eines snaps.
1.. Wenn man das Programm über die Kommandozeile startet, erhält man dort gegebenenfalls die Ausgaben des Programms wie Fehler- oder Warnmeldung. Dies kann bei der Fehlersuche hilfreich sein, wenn ein über ein snap installiertes Programm Probleme machen sollte.</li>
</ol>
<p><em>Hinweis</em>: Diese Verfahren ist dazu gedacht, Programme auszuprobieren, die noch nicht regulär installiert sind. Möchte man zwei verschiedene Versionen eines snap parallel nutzen sind Hinweis dazu im Blogartikel <a href='https://noisefloor-net.blogspot.com/2022/12/snaps-parallel-installieren.html'>snaps parallel installieren</a> zu finden. </p>
<h2 id='vorgehen'>Vorgehen</h2>
<p>Im folgenden wird das <a href='https://snapcraft.io/inkscape'>Inkscape snap</a> für das Vektorzeichenprogramm <a href='https://inkscape.org/'>Inkscape</a> als Beispiel genutzt. Die snap Version ist 10512. Für andere snaps sind einfach der Name und die Versionsnummer entsprechend anzupassen. Für alle folgenden Schritte gehe ich davon aus, dass man sich in seinem eigenen Homeverzeichnis <strong>~/</strong> befindet. </p>
<p>Die folgenden Befehle müssen alle im Terminal ausgeführt werden, teilweise sind dafür Root-Rechte notwendig. </p>
<h3 id='herunterladen-und-testweise-einbinden'>Herunterladen und testweise einbinden</h3>
<p>Zuerst lädt man das snap herunter, ohne es zu installieren, mit Hilfe des Befehls download: </p>
<p><code>$ snap download inkscape</code></p>
<p>Dabei werden zwei Dateien heruntergeladen: das snap an sich, in diesem Beispiel namens <strong>inkscape_10512.snap</strong> und die zugehörige <a href='https://snapcraft.io/docs/assertions'>Assertion-Datei</a> (Verifizierungsdatei) <strong>inkscape_10512.assert</strong>, welche die kryptographischen Schlüssel und Validierungsdaten enthält, die bei einer Installation des snaps geprüft würden. "10512" ist hierbei die Versionsnummer zur Zeit des Downloads aktuelle, stabilen snaps. Der Befehl <code>download</code> kennt auch ein Option für Channels, d.h. man kann auch eine Beta- oder Testversion herunterladen - je nach dem was für das jeweilige snap bereit gestellt wird. </p>
<p>snaps in ein <a href='https://en.wikipedia.org/wiki/SquashFS'>SquashFS</a> Dateisystem verpackt, welches zuerst in ein Verzeichnis entpackt werden muss. Das notwendige Programm Namens unsquashfs ist bei Ubuntu standardmäßig installiert (wenn zuerst nein muss über die Paketverwaltung zuerst das Paket "squashfs-tools" installiert werden): </p>
<p><code>$ unsquashfs inkscape_10512.snap</code></p>
<p>Im Verzeichnis ist jetzt ein neues Unterverzeichnis namens <strong>squashfs-root</strong> angelegt worden, welches das entpackte snap enthält. Bei Interesse kann man durch das Verzeichnis und dessen Unterverzeichnisse navigieren, um zu sehen, wo welche Dateien liegen. </p>
<p>Zum Ausprobieren von Inkscape muss man den folgenden Befehl ausführen: </p>
<p><code>$ sudo snap try squashfs-root</code> </p>
<p>Auch, wenn man sich im eigenen Homeverzeichnis befindet, sind Root-Rechte notwendig. Anschließend kann Inkscape regulär im Terminal, die GNOME-Shell bzw. Weg der jeweiligen Desktopumgebung den gestartet werden. </p>
<p>Das testweise laufende snap erscheint jetzt auch in der Liste des installierten snaps, auch wenn es nicht richtig installiert ist: </p>
<p><code>$ snap list</code></p>
<pre><code>Name Version Revision Tracking Herausgeber Hinweise
...
inkscape 1.2.2-b0a8486541-2022-12-01-cust x1 - - try
...
</code></pre>
<p>Die Revisionsnummer für snap zum Ausprobieren ist x1 und in der Spalte <em>"Hinweis"</em> ist zu sehen, dass das Inkscape snap im try-Modus ist. </p>
<p>Ein so eingebundenes snap wird auch bei einem Systemneustart wieder eingebunden, wie andere, regulär installierte snaps auch. Das deaktivieren / deinstallieren wird im folgenden Abschnitt beschrieben. </p>
<h3 id='snap-wieder-entfernen'>snap wieder entfernen</h3>
<p>Das testweise installierte snap kann wie ein reguläres snap deinstalliert werden: </p>
<p><code>$ snap remove inkscape</code></p>
<p>Danach kann man das Verzeichnis <strong>~/squashfs-root</strong> und das Verzeichnis inklusive Unterverzeichnissen <strong>~/snap/inkscape</strong> löschen. Des Weiteren kann man die heruntergeladenen Dateien <strong>inkscape_10512.snap</strong> und <strong>inkscape_10512.assert</strong> löschen. </p>
<h3 id='was-tun-wenn-versehentlich-auch-ein-produktiv-genutztes-snap-ausprobiert-wurde'>Was tun, wenn versehentlich auch ein produktiv genutztes snap ausprobiert wurde?</h3>
<p>Hat man - versehentlich oder bewusst - ein snap testweise installiert, welches in einer anderen Version schon regulär installiert war (z.B. das Firefox snap), dann wird regulär installierte gegebenenfalls durch das testweise installierte ersetzt. Dies lässt sich aber einfach rückgängig machen, im folgenden am Beispiel des Firefox snaps gezeigt: </p>
<p>Zuerst prüft man, ob noch eine reguläre Version des snap installiert ist. Dies sollte normalerweise der Fall sein, da immer mindestens eine ältere Version eines snap noch vorhanden ist: </p>
<p><code>$ snap list firefox --all</code></p>
<pre><code>...
firefox 107.0.1-1 2154 latest/stable mozilla✓ deaktiviert,try
firefox 108.0b9-1 x1 latest/stable - try
...
</code></pre>
<p>Die Ausgabe zeigt, das es die testweise installierte Version x1 und eine weitere, reguläre Version 2154 auf dem System gibt. Mit den folgenden beiden Befehlen kehrt man zur regulären Version zurück und löscht das snap der Testversion: </p>
<p><code>$ sudo snap revert firefox</code>
<code>$ sudo snap remove firefox --revision=x1</code></p>
<p>Für das vollständige Entfernen des testweise installierten Version löscht man noch die Dateien wie weiter oben im Abschnitt "snap wieder entfernen" beschrieben. </p>
<h2 id='links'>Links</h2>
<ul>
<li><a href='https://snapcraft.io/docs/snap-try'>snap try</a> Dokumentation</li>
</ul>noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-13729498802996930792022-12-27T20:21:00.001+01:002023-02-13T17:03:28.439+01:00snaps parallel installieren<h1 id='snaps-parallel-installieren'>snaps parallel installieren</h1>
<p><em>Hinweis: das im folgenden beschriebene Vorgehen ist explizit mit Ubuntu 22.04 LTS durchgeführt worden. Da snapd, der Daemon, der für snaps zuständig ist, aber von Canonical für alle unterstützen Ubuntuversionen gleichermaßen gepflegt wird, sollte das Vorgehen auch für andere Ubuntuversion funktionieren. Ebenso sollte das Vorgehen für andere Linuxdistributionen funktionieren, auf denen snapd installiert ist.</em></p>
<p><a href='https://ubuntu.com/core/services/guide/snaps-intro'>snap</a>, das Paketformat von Canonical erlaubt es, mehrere Versionen eines Programms parallel zu installieren. Die Voraussetzung ist dabei, dass die snaps aus verschiedenen <a href='https://snapcraft.io/docs/channels'>Channels</a> stammen. Verschiedene Channels können z.B. eine Extend Support Version eines Programms sein und der reguläre / aktuelle, stabile Release.</p>
<p>Verschiedene Channels sind aber in der Regel nicht die aktuelle, stabile Version ein Programms und die Betaversion, da diese bei vielen / den meisten snaps im selben <a href='https://snapcraft.io/docs/channels#heading--tracks'>Track</a> sind und damit nicht parallel installierbar</p>
<h2 id='warum-parallel-installieren'>Warum parallel installieren?</h2>
<p>Ob man ein snap in mehreren Version parallel installieren möchte (oder muss), muss man letztenslich für sich selber entschieden. Wirklich nötig ist es - zumindest in meinen Augen - wohl eher selten. Interessant ist es vielleicht für diejenigen, die zu Testzwecken mehrere Versionen eines Programms benötigen, wie z.B. den auch im folgenden als Beispiel verwendeten Firefox in der jeweils aktuellen und ESR-Version.</p>
<h2 id='risiko-einer-parallelinstallation'>Risiko einer Parallelinstallation</h2>
<p>Das Risiko kann, je nach snap, sein, dass die eine Version des snaps Konfigurationsdateien des anderen snap überschreibt oder ändert, was zu Problemen führen könnte (aber nicht muss). </p>
<h2 id='vorgehen'>Vorgehen</h2>
<p>Im folgenden wird als Beispiel das <a href='https://snapcraft.io/firefox'>Firefox</a> snap aus dem "stable/latest" (=das standardmäßig installiere) und dem "esr/latest" Track/Channel, so dass zwischen der jeweils aktuellsten und aktuellsten ESR (=Extended Support Release) Version gewechselt werden kann. Das Vorgehen ist auf andere snap und andere Channels übertragbar.</p>
<p>Der Wechsel erfolgt über Befehle, die im Terminal eingegeben werden müssen. Zum Wechsel zwischen zwei Versionen dient der Befehl</p>
<p><code>$ snap switch --channel=TRACK/CHANNEL NAME_DES_SNAPS</code></p>
<p>Nach dem Ausführen von switch muss immer anschließend noch ein </p>
<p><code>$ snap refresh</code></p>
<p>ausgeführt werden. Zum Wechsel auf den stabilen ESR-Track des Firefox führt man die Befehle</p>
<p><code>$ snap switch --channel=esr/stable firefox</code>
<code>$ snap refresh</code></p>
<p>aus. Da die ESR-Version des Firefox noch nicht installiert ist, wird diese heruntergeladen und aktiviert. Gleichzeitig wird der latest/stable Firefox deaktiviert, aber nicht deinstalliert. Beim nächsten Start des Firefox nutzt man die ESR Version.</p>
<p>Um wieder zum latest/stable Track zurück zu wechseln führt man wieder die beiden Befehle</p>
<p><code>$ snap switch --channel=lastest/stable firefox</code>
<code>$snap refresh</code></p>
<p>aus.</p>
<h2 id='einen-channel-wieder-deinstallieren'>einen Channel wieder deinstallieren </h2>
<p>Möchte man doch nur einen Channel, im folgenden latest/stable nutzen und die Programmversion des anderen deinstallieren, dann wechselt man zuerst in den zu deinstallierenden Channel</p>
<p><code>$ snap switch --channel=esr/stable firefox</code>
<code>$ snap refresh</code></p>
<p>und deinstalliert dann den Firefox</p>
<p><code>$ snap remove firefox</code>
<code>$ snap switch --channel=latest/stable firefox</code>
<code>$ snap refresh</code></p>
<p>Anschließend muss der Firefox aus dem latest/stable Channel gegebenenfalls noch aktiviert werden. Steht in der Ausgabe von</p>
<p><code>$ snap list firefox --all</code></p>
<p>in der Spalte <em>"Hinweise"</em> der Eintrag disabled, dann führt man noch den Befehl</p>
<p><code>$ snap activate firefox</code>
<code>$ snap refresh</code></p>
<p>aus.</p>noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-12267414287820078242022-06-08T18:20:00.001+02:002023-05-26T21:14:34.621+02:00Python und Geschwindigkeit auf verschiedenen Intel und AMD CPUs - und einem Apple Bionic Chip<p>Vor ein paar Jahren gab es hier im Blog einen Post, der die Geschwindigkeit eines Pythonprogramms zum Number-Crunching (in Form eines Primzahlentests) beschriebt (<a href="https://noisefloor-net.blogspot.com/2018/01/python-und-geschwindigkeit-oder.html">Link zum Artikel</a>).</p><p>Ich hatte jetzt kürzliche die Gelegenheit, die Ausführungsgeschwindigkeit auf vier verschiedenen Mobil CPUs verschiedener Hersteller und Generation zu vergleichen, nämlich:</p><ul style="text-align: left;"><li>Intel Core i5-5200U mit 2,20 GHz, 4 Kerne (<a href="https://ark.intel.com/content/www/de/de/ark/products/85212/intel-core-i55200u-processor-3m-cache-up-to-2-70-ghz.html">Link zum Datenblatt</a>)</li><li>Intel Core i7-10510U mit 1,80 GHz, 4 Kerne (<a href="https://ark.intel.com/content/www/us/en/ark/products/196449/intel-core-i710510u-processor-8m-cache-up-to-4-90-ghz.html">Link zum Datenblatt</a>)<br /></li><li>Intel Core i7-1165G7 mit 2,80 GHz, 4 Kerne (<a href="https://ark.intel.com/content/www/de/de/ark/products/208662/intel-core-i71165g7-processor-12m-cache-up-to-4-70-ghz.html">Link zum Datenblatt</a>) </li><li>AMD Ryzen 7 5700U mit 1,80 GHz, 8 Kerne (<a href="https://www.amd.com/en/product/10846">Link zum Datenblatt</a>)</li></ul><p>Die Prozessoren sind also eine zum Zeitpunkt des Schreibens des Artikels eine ca. 7 Jahre alte CPU (der Core i5-5200U, erschienen 2015), ein Core i7 Prozessor der 10. Generation (erschienen zweite Jahreshälfte 2019), ein Core i7 Prozessor der 11. Generation (erschienen 2. Jahreshälfte 2021) sowie die ebenfalls 2021 erschienene Ryzen 7 5700U CPU.</p><p>Außerdem habe ich noch einen Test auf einem iPad Air gemacht. Mehr dazu <a href="#ipad">am Ende des Artikels</a>.</p><p>Verglichen habe ich nur die Ausführungsgeschwindigkeit des Python-Programms ohne Optimierungen / Beschleunigungen, also ohne PyPy, Cython, JIT Compiler etc.</p><p>Als Betriebssystem diente für den Core i5-5200U und den AMD Ryzen 7 5700U Ubuntu 22.04 mit Python 3.10.4 aus den offiziellen Ubuntu Quelle und für die beiden Intel Core i7 Windows 11, ebenfalls mit Python 3.10.4 aus dem Windows App Store, also die Python-Version, die direkt von der PSF bereit gestellt wird.<br />Das ganze wurde immer bei ruhendem Desktop gemessen, d.h. außer dem Terminal, in dem das Python-Programm läuft, läuft kein weiteres Programm. Es wurde jedes Skript 3x ausgeführt, die weiter unten auf geführten Zeitangabe sind der Mittelwert der Ausführungszeiten.<br /></p><p>Als erste habe ich die Ausführungsgeschwindigkeit mit nur einem Prozess gemessen, das Programm sieht wie folgt aus:</p>
<p>
</p><div style="background: rgb(248, 248, 248);"><pre style="line-height: 125%;"><span></span><span style="color: green; font-weight: bold;">import</span> <span style="color: blue; font-weight: bold;">math</span>
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">time</span> <span style="color: green; font-weight: bold;">import</span> time
PRIMES <span style="color: #666666;">=</span> [
<span style="color: #666666;">112272535095293</span>,
<span style="color: #666666;">112582705942171</span>,
<span style="color: #666666;">112272535095293</span>,
<span style="color: #666666;">115280095190773</span>,
<span style="color: #666666;">115797848077099</span>,
<span style="color: #666666;">1099726899285419</span>,
<span style="color: #666666;">777777722155555333</span>,
<span style="color: #666666;">777777722155555335</span>,
<span style="color: #666666;">9999999900000001</span>,
<span style="color: #666666;">2327074306453592351</span>,
<span style="color: #666666;">2327074306453592353</span>]
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">is_prime</span>(n):
<span style="color: green; font-weight: bold;">if</span> n <span style="color: #666666;">%</span> <span style="color: #666666;">2</span> <span style="color: #666666;">==</span> <span style="color: #666666;">0</span>:
<span style="color: green; font-weight: bold;">return</span> <span style="color: green; font-weight: bold;">False</span>
sqrt_n <span style="color: #666666;">=</span> <span style="color: green;">int</span>(math<span style="color: #666666;">.</span>floor(math<span style="color: #666666;">.</span>sqrt(n)))
<span style="color: green; font-weight: bold;">for</span> i <span style="color: #aa22ff; font-weight: bold;">in</span> <span style="color: green;">range</span>(<span style="color: #666666;">3</span>, sqrt_n <span style="color: #666666;">+</span> <span style="color: #666666;">1</span>, <span style="color: #666666;">2</span>):
<span style="color: green; font-weight: bold;">if</span> n <span style="color: #666666;">%</span> i <span style="color: #666666;">==</span> <span style="color: #666666;">0</span>:
<span style="color: green; font-weight: bold;">return</span> <span style="color: green; font-weight: bold;">False</span>
<span style="color: green; font-weight: bold;">return</span> <span style="color: green; font-weight: bold;">True</span>
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">main</span>():
<span style="color: green; font-weight: bold;">for</span> number <span style="color: #aa22ff; font-weight: bold;">in</span> PRIMES:
prime <span style="color: #666666;">=</span> is_prime(number)
<span style="color: green;">print</span>(<span style="color: #ba2121;">'</span><span style="color: #a45a77; font-weight: bold;">{}</span><span style="color: #ba2121;"> is prime: </span><span style="color: #a45a77; font-weight: bold;">{}</span><span style="color: #ba2121;">'</span><span style="color: #666666;">.</span>format(number, prime))
<span style="color: green; font-weight: bold;">if</span> <span style="color: #19177c;">__name__</span> <span style="color: #666666;">==</span> <span style="color: #ba2121;">'__main__'</span>:
start <span style="color: #666666;">=</span> time()
main()
end <span style="color: #666666;">=</span> time()
<span style="color: green;">print</span>(<span style="color: #ba2121;">f'Time: </span><span style="color: #a45a77; font-weight: bold;">{</span>end <span style="color: #666666;">-</span> start<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">'</span>)
</pre></div>
<p></p><p>Die Ergebnisse sind wie folgt:</p><ul style="text-align: left;"><li>Core i5-5200U: 204 Sekunden</li><li>Core i7-10510U: 111 Sekunden</li><li>Core i7-1165G7: 68 Sekunden</li><li>Ryzen 7 5700U: 78 Sekunden</li></ul><p>Wie zu erwarten war, sind die neueren CPUs deutlich schneller. Interessant ist auch die relative große Unterschied zwischen dem Core i7 10. Generation und 11. Generation, letzterer ist etwas 60% schneller. Hier hat Intel deutlich in Sachen Leistung nachgelegt, zumindest was die Performance von nur einem Kern angeht.<br />Zweite Beobachtung: im weiter oben verlinkten Blogpost wurde der gleiche Code ebenfalls auf dem gleichen Core i5-5200U ausgeführt, nur damals unter Python 3.6. Die Geschwindigkeit war damals quasi identisch mit der hier gemessenen. Auch wenn Python (bzw. genau genommen die Referenzimplementierung CPython) einige Verbesserungen in Sachen Geschwindigkeit bekommen hat, spielen diese zumindest für dieses Szenario keine Rolle, der Code ist auf der gleichen CPU bzw. dem gleichen Rechner wie damals quasi gleich schnell.<br /></p><p>Im zweiten Test wurde das Programm modifiziert, so dass mit Hilfe des <span style="font-family: courier;">concurent.futures</span> Moduls mehrere Prozesses zum Rechnen gestartet. Da die Core i5 5200U CPU und auch die neueren Core i7 CPUs "nur" vier Kerne haben, wurde als Prozessanzahl vier gewählt, damit im Idealfall jeder Prozess auf einem Kern läuft.</p><p>Der Code sieht wie folgt aus:</p><p>
</p><div class="highlight" style="background: rgb(248, 248, 248);"><pre style="line-height: 125%;"><span></span><span style="color: green; font-weight: bold;">import</span> <span style="color: blue; font-weight: bold;">math</span>
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">time</span> <span style="color: green; font-weight: bold;">import</span> time
<span style="color: green; font-weight: bold;">import</span> <span style="color: blue; font-weight: bold;">concurrent.futures</span>
WORKERS <span style="color: #666666;">=</span> <span style="color: #666666;">4</span>
PRIMES <span style="color: #666666;">=</span> [
<span style="color: #666666;">112272535095293</span>,
<span style="color: #666666;">112582705942171</span>,
<span style="color: #666666;">112272535095293</span>,
<span style="color: #666666;">115280095190773</span>,
<span style="color: #666666;">115797848077099</span>,
<span style="color: #666666;">1099726899285419</span>,
<span style="color: #666666;">777777722155555333</span>,
<span style="color: #666666;">777777722155555335</span>,
<span style="color: #666666;">9999999900000001</span>,
<span style="color: #666666;">2327074306453592351</span>,
<span style="color: #666666;">2327074306453592353</span>]
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">is_prime</span>(n):
<span style="color: green; font-weight: bold;">if</span> n <span style="color: #666666;">%</span> <span style="color: #666666;">2</span> <span style="color: #666666;">==</span> <span style="color: #666666;">0</span>:
<span style="color: green; font-weight: bold;">return</span> <span style="color: green; font-weight: bold;">False</span>
sqrt_n <span style="color: #666666;">=</span> <span style="color: green;">int</span>(math<span style="color: #666666;">.</span>floor(math<span style="color: #666666;">.</span>sqrt(n)))
<span style="color: green; font-weight: bold;">for</span> i <span style="color: #aa22ff; font-weight: bold;">in</span> <span style="color: green;">range</span>(<span style="color: #666666;">3</span>, sqrt_n <span style="color: #666666;">+</span> <span style="color: #666666;">1</span>, <span style="color: #666666;">2</span>):
<span style="color: green; font-weight: bold;">if</span> n <span style="color: #666666;">%</span> i <span style="color: #666666;">==</span> <span style="color: #666666;">0</span>:
<span style="color: green; font-weight: bold;">return</span> <span style="color: green; font-weight: bold;">False</span>
<span style="color: green; font-weight: bold;">return</span> <span style="color: green; font-weight: bold;">True</span>
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">main</span>():
<span style="color: green; font-weight: bold;">with</span> concurrent<span style="color: #666666;">.</span>futures<span style="color: #666666;">.</span>ProcessPoolExecutor(max_workers<span style="color: #666666;">=</span>WORKERS) <span style="color: green; font-weight: bold;">as</span> executor:
<span style="color: green; font-weight: bold;">for</span> number, prime <span style="color: #aa22ff; font-weight: bold;">in</span> <span style="color: green;">zip</span>(PRIMES, executor<span style="color: #666666;">.</span>map(is_prime, PRIMES)):
<span style="color: green;">print</span>(<span style="color: #ba2121;">'</span><span style="color: #a45a77; font-weight: bold;">%d</span><span style="color: #ba2121;"> is prime: </span><span style="color: #a45a77; font-weight: bold;">%s</span><span style="color: #ba2121;">'</span> <span style="color: #666666;">%</span> (number, prime))
<span style="color: green; font-weight: bold;">if</span> <span style="color: #19177c;">__name__</span> <span style="color: #666666;">==</span> <span style="color: #ba2121;">'__main__'</span>:
<span style="color: green;">print</span>(<span style="color: #ba2121;">f'using </span><span style="color: #a45a77; font-weight: bold;">{</span>WORKERS<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;"> workers for calculation'</span>)
start <span style="color: #666666;">=</span> time()
main()
end <span style="color: #666666;">=</span> time()
<span style="color: green;">print</span>(<span style="color: #ba2121;">f'Time: </span><span style="color: #a45a77; font-weight: bold;">{</span>end <span style="color: #666666;">-</span> start<span style="color: #a45a77; font-weight: bold;">}</span><span style="color: #ba2121;">'</span>)
</pre></div><p></p><p>Die Ergebnisse für den Multiprozessansatz sind:</p><ul style="text-align: left;"><li>Core i5-5200U: 111 Sekunden</li><li>Core i7-10510U: 89 Sekunden</li><li>Core i7-1165G7: 50 Sekunden</li><li>Ryzen 7 5700U: 51 Sekunden</li></ul><p></p><p>Auch hier ist der Leistungsunterschied zwischen dem Core i7 10. und 11. Generation erheblich, der neuere ist ca. 80% schneller. Der Core i7-1168G7 und der Ryzen 7 5700U sind quasi gleich schnell.<br /></p><p>Ein zusätzlicher Test mit dem Ryzen 7 und acht Workern brachte keine weitere Beschleunigung, das Ergebnis war quasi identisch mit dem mit vier Workern. Dies liegt im gegebenen Test aber ziemlich sicher daran, dass die ersten sechs Prüfungen relativ schnell abgeschlossen sind, d.h. man zieht für die restlichen fünf Zahlen keinen echten Nutzen aus mehr Kernen, weil nicht mehr so viele Prozesse laufen.</p><p>Festzuhalten bleibt aber auch: natürlich sind neuere Prozessoren schneller, aber bringen (bei weitem) nicht so viel wie der Einsatz von schnelleren Python-Implementierungen und JIT Compilern.<br />Alleine der simple Einsatz von PyPy statt CPython auf dem "alten" Core i5 Prozessor führt den Code doppelt so schnell aus wie die Core i7 CPU der 11. Generation oder der Ryzen 7 5700U.</p><p id="ipad">Zusätzlich zu den diversen Intel und der AMD CPU habe ich den nicht-parallelen Code auf einem iPad Air 3. Generation, iOS 15.5 und Python 3.6.1 aus der <a href="http://omz-software.com/pythonista/">Pythonista</a> ausgeführt. Dieses iPad hat eine <a href="https://de.wikipedia.org/wiki/Apple_A12_Bionic">A12 Bionic CPU</a>. Der parallelisierte Code funktioniert ja leider nicht unter iOS, weil, zumindest zum Zeitpunkt des Schreibens des Artikels, ein Prozess keine weiteren Prozesse starten darf.</p><p>Der A12 Bionic braucht für den obigen Code ca. 104 Sekunden - und ist damit ca. 10% schneller der getestet Intel Core i7 der 10. Generation. Schon ganz ordentlich für einen Prozessor, der ca. ein Jahr vor dem Intel Prozessor erschienen ist, wobei allerdings auch der Takt der leistungsstärkeren Kerne des A13 mit 2,,49 GHz (deutlich) höher ist als die 1,80 GHz Core i7-10510U.</p>noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-34451716299432284132018-01-18T20:58:00.004+01:002023-05-26T21:13:50.794+02:00Python und Geschwindigkeit (oder: Beschleunigung ist ganz einfach)<p>Es heißt ja schon mal gerne "Python ist langsam". Mal abgesehen davon, dass solch pauschale Aussagen generell fragwürdig sind, ist in der Aussage ein entscheidender Fehler: Python beschreibt die (Programmier-) Sprache an sich - aber es gibt <a href="https://www.python.org/download/alternatives/">diverse Implementierung</a>, die sich in Sachen Geschwindigkeit teilweise erheblich unterscheiden.</p>
<p>Richtig ist, dass im allgemeinen "Python" mit der Referenzimplementierung CPython gleichgesetzt wird, welches die (mit Abstand) am meisten genutzte Implementierung ist. Auch verwende ich hier im Blog in der Regel das Wort Python als Synonym für CPython.</p>
<p>CPython arbeitet Code in zwei Schritten ab: zuerst wird der Quellcode in (plattformunabhängigen) Bytecode übersetzt, mit Hilfe eines Compilers. Der Bytecode wird dann in einer virtuellen Maschine (nicht zu verwechseln mit VirtualBox, VMware & Co) Anweisung für Anweisung ausgeführt. Und CPython ist in der Tat nicht auf Geschwindigkeit optimiert, der Fokus der Entwicklung liegt auf anderen Punkten.</p>
<p>Im folgenden geht es darum, wie man die Ausführung von Python-Skripten beschleunigen kann. Dazu sei noch angemerkt: Es werden im folgenden in erster Linie einige Möglichkeiten gezeigt. Es geht nicht darum, jede noch mögliche (Mikro-) Optimierung zu nutzen, um noch die ein oder andere Zehntelsekunde extra heraus zu holen.</p>
<p>Auch habe ich keine (aufwendigen) Benchmarks genutzt, sondern einfach nur einen recht rechenintensiven Code zur Prüfung von Primzahlen. Die absoluten Werte der Laufzeit des Codes sind auch sekundär, interessanter ist der relative Unterschied zwischen den verschieden Methoden der Ausführung. Alle Messungen wurden auf meinem Laptop gemacht, welcher einen Intel® Core™ i5-5200U CPU Prozessor mit 2.20 GHz und vier Kernen hat. Das Betriebssystem ist Ubuntu 16.04 (mit ruhendem Desktop, d.h. es liefen keine weiteren Programme), die CPython-Version ist Python 3.5.3, was Ubuntu 16.04 standardmäßig installiert hat.</p>
<p> Der Code ist <a href="https://docs.python.org/3.5/library/concurrent.futures.html#processpoolexecutor-example">an ein Beispiel</a> aus der Python-Doku angelehnt:</p>
<pre><code>import math
PRIMES = [
112272535095293,
112582705942171,
112272535095293,
115280095190773,
115797848077099,
1099726899285419,
777777722155555333,
777777722155555335,
9999999900000001,
2327074306453592351,
2327074306453592353]
def is_prime(n):
if n % 2 == 0:
return False
sqrt_n = int(math.floor(math.sqrt(n)))
for i in range(3, sqrt_n + 1, 2):
if n % i == 0:
return False
return True
def main():
for number in PRIMES:
prime = is_prime(number)
print('{} is prime: {}'.format(number, prime))
if __name__ == '__main__':
main()
</code></pre>
<p>Es wird also einfach nur für elf Zahlen geprüft, ob diese eine Primzahl sind (Tipp: wer diesen Code auf einem langsamen Rechner wie z.B. dem Raspberry Pi ausführen möchte, sollte die Liste kürzen und z.B. nach 777777722155555333 Schluss machen, damit der Code nicht zu lange läuft).</p>
<p> Dieser Code braucht bei mir auf meinem Laptop 3 Minuten 23 Sekunden. Das ist der Referenzwert für die im folgenden besprochenen Methoden zur Beschleunigung.</p>
<p>Natürlich lässt sich dieses Problem sehr gut parallelisieren. Dem concurrent.futures Beispiel aus der Python-Doku folgend zum Beispiel, in dem man das Rechnen auf mehrere Prozesse verteilt:</p>
<pre><code>import concurrent.futures
import math
...
def is_prime(n):
...
def main():
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
print('%d is prime: %s' % (number, prime))
if __name__ == '__main__':
main()
</code></pre>
<p>Hier werden vier parallele Prozesse gestartet und damit gerechnet.</p>
<p>Ergebnis: 2 Minuten 15 Sekunden, also etwas mehr als 1 Minute schneller.</p>
<p>Nun lassen sich aber nicht alle Programme bzw. Berechnungen so leicht parallelisieren wie das Prüfen von Primzahlen. Was auch nicht weiter schlimm ist, denn es gibt auch Methoden, nicht-parallelisierten Code zu beschleunigen.</p>
<p>Die für Python wahrscheinliche bekannteste Methode ist der Einsatz von <a href="https://web.archive.org/web/20181221031809/https://pypy.org/">PyPy</a>. PyPy ist kompatibel zu CPython, der Code wird aber von einem Just-in-Time Compiler übersetzt. Der Aufruf des Codes erfolgt einfach durch:</p>
<pre><code>$ pypy name_des_skripts.py
</code></pre>
<p>Das Skript aus dem ersten Beispiel (also ohne Parallelisierung) wird von PyPy (Version 5.10.0) in 24 Sekunden ausgeführt. D.h. die Ausführungsgeschwindigkeit ist um einen Faktor von ca. 8,5 (!) höher, nur durch Einsatz der Python-Implementierung PyPy, ohne weitere Änderungen am Code.</p>
<p> Eine weitere Möglichkeit der Beschleunigung ist der Einsatz des Just-in-Time Compilers <a href="https://numba.pydata.org/">Numba</a>. Dieser ist standardmäßig bei der Python-Distribution <a href="https://www.anaconda.com/what-is-anaconda/">Anaconda</a> an Bord. Numba kann zwar auch manuell installiert werden, dass ist aber vergleichsweise aufwendig, weswegen für den Test von Numba die Version 0.36.2 aus Anaconda (in Kombination mit Python 3.6.3) zum Einsatz kam.</p>
<p>Um Code bzw. Funktionen mit Numba zu beschleunigen, müssen diese lediglich mit einem Dekorator versehen werden:</p>
<pre><code>import math
from numba import jit
PRIMES = [
...]
@jit
def is_prime(n):
...
def main():
for number in PRIMES:
prime = is_prime(number)
print('{} is prime: {}'.format(number, prime))
if __name__ == '__main__':
main()
</code></pre>
<p>Diese unscheinbare Änderung bewirkt, dass die Funktion is_prime vom Numba Just-in-Time Compiler ausgeführt wird. Das Ergebnis ist eine Laufzeit von 19 Sekunden, also eine Beschleunigung vom eine Faktor von ca. 10,5.</p>
<p> Jetzt gibt auch die Möglichkeit, Python zu C-Code kompilieren lassen. Dazu wurden der Python-Compiler Nuitka und eine Kombination aus Cython und gcc getestet.</p>
<p><a href="http://nuitka.net/">Nuitka</a> (welches ich bis dahin noch nie verwendet hatte) hat zum Ziel, einen <i>"extremely compatible Python compiler"</i> bereit zustellen, entsprechend liegt der Entwicklungsfokus darauf, (Geschwindigkeits-) Optimierungen sollen später erst einfließen.</p>
<p>Entsprechend ist auch das Ergebnis: die Laufzeit ist 3 Minuten und 7 Sekunden, also unwesentlich schneller als CPython. Die eingesetzte Nuitka Version ist 0.5.24. Laut Aussage auf der Homepage ist Nutika-Homepage ist Nuitka bei größeren / umfangreicheren Benchmarks aber doch spürbar schneller als CPython.</p>
<p> Zweiter Testkandidat ist eine Kombination aus <a href="http://cython.org/">Cython</a> (Version 0.27.3) und dem gcc Compiler (Version 5.4.0). Dazu wurde der Code aus dem ersten Beispiel zuerst einfach mit Cython in C-Code übersetzt und dann mit gcc zu einer ausführbaren Datei kompiliert:</p>
<pre><code>$ cython prime_code_linear_cython.py --embed
$ gcc -Os -I /usr/include/python3.5m -o prime_code_linear_cython prime_code_linear_cython.c
-lpython3.5m -lpthread -lm -lutil -ldl
</code></pre>
<p>Das Ergebnis ist eine Laufzeit von 2 Minuten 39 Sekunden, also rund 1 Minute schneller als CPython. Nicht schlecht, aber für kompilierten Code auch nicht wirklich viel - jedenfalls nicht im Vergleich zu PyPy und Numba.</p>
<p> Das ganze kann man aber verbessern, wenn man von der dynamischen Typisierung, die Python standardmäßig hat, Abschied nimmt und statisch typisiert. Dadurch sind Optimierung seitens Cython möglich, welche die Ausführung (erheblich) beschleunigen. Ändert man den Code wie folgt:</p>
<pre><code>import math
PRIMES = [
112272535095293,
...]
cdef bint is_prime(long n):
cdef int sqrt_n, i
if n % 2 == 0:
return False
sqrt_n = int(math.floor(math.sqrt(n)))
for i in range(3, sqrt_n + 1, 2):
if n % i == 0:
return False
return True
def main():
cdef long number
cdef bint prime
for number in PRIMES:
prime = is_prime(number)
print('{} is prime: {}'.format(number, prime))
if __name__ == '__main__':
main()
</code></pre>
<p>und kompiliert erneut, dann liegt die Ausführungsgeschwindigkeit bei 18 Sekunden, das ist die schnellste Ausführung (wenn auch nur unwesentlich schneller als Numba).</p>
<p>Der Nachteil: Der Code funktioniert tatsächlich nur noch, wenn die Primzahlen vom Typ long sind, d.h. der Code wird unflexibler. Fügt man z.B. eine 5 in die Liste der Primzahlen ein, bricht die Übersetzung mit Cython von Python nach C mit einer Fehlermeldung ab, weil 5 vom Typ int ist (und kein long). Wer mehr Infos zu Cython und Beschleunigung mittels statischer Typisierung sucht, für den ist die Seite <a href="http://cython.readthedocs.io/en/latest/src/quickstart/cythonize.html">Faster code via static typing</a> in der Cython-Doku ein guter Startpunkt.</p>
<p> Zum Abschluss noch mal alle Ergebnisse auf einen Blick: </p>
<ul>
<li>CPython 3.5.3: 3 Minuten 23 Sekunde</li>
<li>CPython + concurrent.futures.ProcessPoolExecutor: 2 Minuten 15 Sekunden</li>
<li>PyPy 5.10.0: 24 Sekunden</li>
<li>Python 3.6.3 + Numba 0.36.2: 19 Sekunden</li>
<li>Nuitka 0.5.24: 3 Minuten 7 Sekunden</li>
<li>Cython 0.27.3 + gcc 5.4.0: 2 Minuten 39 Sekunden</li>
<li>Cython mit statischer Typisierung + gcc: 18 Sekunden</li>
</ul>
<h2>Fazit</h2>
<p> Es gibt verschiedenen Möglichkeiten rechenintensive / CPU-intensive Python-Skripte zu beschleunigen, teilweise erheblich zu beschleunigen.</p>
<p>Die einfachste Möglichkeit ist sicherlich PyPy - hier muss am Code nichts geändert werden, das Skript muss lediglich mit PyPy statt CPython aufgerufen werden. Wie auch <a href="https://de.wikipedia.org/wiki/Guido_van_Rossum">Guido van Rossum</a> (der "Erfinder" von Python) sagte: <i>“If you want your code to run faster, you should probably just use PyPy.”</i></p>
<p>Der tatsächliche Beschleunigungsfaktor des eigenen Codes kann natürlich auch noch vom Code an sich abhängen. Gegenfalls macht es dann Sinn, die verschieden Optionen - PyPy, Numba und Cython + gcc - zu prüfen, um die für sich beste herauszufinden.</p>noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-89966971105023371452017-12-17T12:07:00.000+01:002018-06-23T17:19:53.451+02:00Tkinter und Threads<div dir="ltr" style="text-align: left;" trbidi="on">
Der Raspberry Pi ist ein recht populärer Minirechner, besonders für Hobbybastler und Leute, die gerne mit elektrischen Schaltung, welche vom Rechner gesteuert werden, herumspielen.<br />
In Supportforen taucht öfters die Frage auf, wie man Messwerte von Sensoren am besten in graphischen Oberfläche (GUI) visualisiert werden können. Da Tkinter bei Python standardmäßig an Bord ist, wird dieses gerne eingesetzt (auch, wenn Qt oder GTK+ leistungsfähiger sind).<br />
<br />
Das grundsätzliche Problem: Das Auslesen von Sensoren kann, je nach Sensor, mehr oder minder lang dauern. Liest man den Sensor über eine Funktion in der GUI aus, kann es sein, dass das Warten auf den Wert des Sensor die GUI bzw. deren Mainloop blockiert. Dies kann man recht einfach dadurch um gehen, dass das Auslesen der Sensoren in einen eigenen Thread auszulesen und die Werte über eine Queue an die GUI zu senden.<br />
<br />
Der entsprechende Code kann z.B. wie folgt aussehen: <br />
<br />
<div class="highlight" style="background: #f8f8f8;">
<pre style="line-height: 125%;"><span style="color: green; font-weight: bold;">import</span> <span style="color: blue; font-weight: bold;">tkinter</span> <span style="color: green; font-weight: bold;">as</span> <span style="color: blue; font-weight: bold;">tk</span>
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">queue</span> <span style="color: green; font-weight: bold;">import</span> Queue
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">threading</span> <span style="color: green; font-weight: bold;">import</span> Thread, Event
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">time</span> <span style="color: green; font-weight: bold;">import</span> sleep
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">random</span> <span style="color: green; font-weight: bold;">import</span> random
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">generate_data</span>(my_queue, event):
<span style="color: green; font-weight: bold;">while</span> <span style="color: #aa22ff; font-weight: bold;">not</span> event<span style="color: #666666;">.</span>is_set():
data <span style="color: #666666;">=</span>{}
data[<span style="color: #ba2121;">'value_sensor_1'</span>] <span style="color: #666666;">=</span> random()
data[<span style="color: #ba2121;">'value_sensor_2'</span>] <span style="color: #666666;">=</span> random()
my_queue<span style="color: #666666;">.</span>put(data)
sleep(<span style="color: #666666;">0.7</span>)
<span style="color: green; font-weight: bold;">class</span> <span style="color: blue; font-weight: bold;">Application</span>(tk<span style="color: #666666;">.</span>Frame):
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">__init__</span>(<span style="color: green;">self</span>, my_queue, thread_kill_event, master<span style="color: #666666;">=</span><span style="color: green;">None</span>):
<span style="color: green;">super</span>()<span style="color: #666666;">.</span><span style="color: blue;">__init__</span>(master)
<span style="color: green;">self</span><span style="color: #666666;">.</span>my_queue <span style="color: #666666;">=</span> my_queue
<span style="color: green;">self</span><span style="color: #666666;">.</span>thread_kill_event <span style="color: #666666;">=</span> thread_kill_event
<span style="color: green;">self</span><span style="color: #666666;">.</span>pack()
<span style="color: green;">self</span><span style="color: #666666;">.</span>create_widgets()
<span style="color: green;">self</span><span style="color: #666666;">.</span>update_labels()
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">create_widgets</span>(<span style="color: green;">self</span>):
<span style="color: green;">self</span><span style="color: #666666;">.</span>label_sensor_1 <span style="color: #666666;">=</span> tk<span style="color: #666666;">.</span>Label(<span style="color: green;">self</span>, bg<span style="color: #666666;">=</span><span style="color: #ba2121;">'purple'</span>, height<span style="color: #666666;">=5</span>, width<span style="color: #666666;">=10</span>,
text<span style="color: #666666;">=</span><span style="color: #ba2121;">'Sensor 1'</span>)
<span style="color: green;">self</span><span style="color: #666666;">.</span>label_sensor_1<span style="color: #666666;">.</span>pack(side<span style="color: #666666;">=</span><span style="color: #ba2121;">'left'</span>)
<span style="color: green;">self</span><span style="color: #666666;">.</span>label_sensor_2 <span style="color: #666666;">=</span> tk<span style="color: #666666;">.</span>Label(<span style="color: green;">self</span>, bg<span style="color: #666666;">=</span><span style="color: #ba2121;">'yellow'</span>, height<span style="color: #666666;">=5</span>, width<span style="color: #666666;">=10</span>,
text<span style="color: #666666;">=</span><span style="color: #ba2121;">'Sensor 2'</span>)
<span style="color: green;">self</span><span style="color: #666666;">.</span>label_sensor_2<span style="color: #666666;">.</span>pack(side<span style="color: #666666;">=</span><span style="color: #ba2121;">'right'</span>)
<span style="color: green;">self</span><span style="color: #666666;">.</span>quit <span style="color: #666666;">=</span> tk<span style="color: #666666;">.</span>Button(<span style="color: green;">self</span>, text<span style="color: #666666;">=</span><span style="color: #ba2121;">'QUIT'</span>, fg<span style="color: #666666;">=</span><span style="color: #ba2121;">'red'</span>,
command<span style="color: #666666;">=</span><span style="color: green;">self</span><span style="color: #666666;">.</span>exit_cleanup)
<span style="color: green;">self</span><span style="color: #666666;">.</span>quit<span style="color: #666666;">.</span>pack(side<span style="color: #666666;">=</span><span style="color: #ba2121;">'bottom'</span>)
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">update_labels</span>(<span style="color: green;">self</span>):
<span style="color: green;">self</span><span style="color: #666666;">.</span>master<span style="color: #666666;">.</span>after(<span style="color: #666666;">500</span>, <span style="color: green;">self</span><span style="color: #666666;">.</span>update_labels)
<span style="color: green; font-weight: bold;">if</span> <span style="color: #aa22ff; font-weight: bold;">not</span> <span style="color: green;">self</span><span style="color: #666666;">.</span>my_queue<span style="color: #666666;">.</span>empty():
data <span style="color: #666666;">=</span> <span style="color: green;">self</span><span style="color: #666666;">.</span>my_queue<span style="color: #666666;">.</span>get()
<span style="color: green; font-weight: bold;">if</span> data[<span style="color: #ba2121;">'value_sensor_1'</span>] <span style="color: #666666;"><</span> <span style="color: #666666;">0.5</span>:
<span style="color: green;">self</span><span style="color: #666666;">.</span>label_sensor_1[<span style="color: #ba2121;">'bg'</span>] <span style="color: #666666;">=</span> <span style="color: #ba2121;">'purple'</span>
<span style="color: green; font-weight: bold;">else</span>:
<span style="color: green;">self</span><span style="color: #666666;">.</span>label_sensor_1[<span style="color: #ba2121;">'bg'</span>] <span style="color: #666666;">=</span> <span style="color: #ba2121;">'pink'</span>
<span style="color: green; font-weight: bold;">if</span> data[<span style="color: #ba2121;">'value_sensor_2'</span>] <span style="color: #666666;"><</span> <span style="color: #666666;">0.5</span>:
<span style="color: green;">self</span><span style="color: #666666;">.</span>label_sensor_2[<span style="color: #ba2121;">'bg'</span>] <span style="color: #666666;">=</span> <span style="color: #ba2121;">'yellow'</span>
<span style="color: green; font-weight: bold;">else</span>:
<span style="color: green;">self</span><span style="color: #666666;">.</span>label_sensor_2[<span style="color: #ba2121;">'bg'</span>] <span style="color: #666666;">=</span> <span style="color: #ba2121;">'blue'</span>
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">exit_cleanup</span>(<span style="color: green;">self</span>):
<span style="color: green;">self</span><span style="color: #666666;">.</span>thread_kill_event<span style="color: #666666;">.</span>set()
<span style="color: green;">self</span><span style="color: #666666;">.</span>master<span style="color: #666666;">.</span>destroy()
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">main</span>():
my_queue <span style="color: #666666;">=</span> Queue()
my_event <span style="color: #666666;">=</span> Event()
my_thread <span style="color: #666666;">=</span> Thread(target<span style="color: #666666;">=</span>generate_data, args<span style="color: #666666;">=</span>(my_queue, my_event, ))
my_thread<span style="color: #666666;">.</span>start()
root <span style="color: #666666;">=</span> tk<span style="color: #666666;">.</span>Tk()
app <span style="color: #666666;">=</span> Application(my_queue, my_event, master<span style="color: #666666;">=</span>root)
app<span style="color: #666666;">.</span>master<span style="color: #666666;">.</span>title(<span style="color: #ba2121;">'TKinter Thread Demo'</span>)
app<span style="color: #666666;">.</span>mainloop()
<span style="color: green; font-weight: bold;">if</span> <span style="color: #19177c;">__name__</span> <span style="color: #666666;">==</span> <span style="color: #ba2121;">'__main__'</span>:
main()
</pre>
</div>
<br />
Von Prinzip her ist das eigentlich recht einfach:<br />
Die Funktion <span style="font-family: "courier new" , "courier" , monospace;">generate_data</span> , welche in einem eigenen Thread läuft, produziert alle 0,7 Sekunden einen zufälligen Wert zwischen 0 und 1 für zwei fiktive Sensoren, schreibt die Werte in ein Dictionary, welches dann in die Queue "geschoben" wird. Die 0,7 Sekunden Wartezeit "simulieren" dabei dir Trägheit des Sensors.<br />
Die Klasse <span style="font-family: "courier new" , "courier" , monospace;">Application</span> ist die eigentliche Tkinter-Anwendung. Diese besteht nur aus zwei Labeln und einem Quit-Button.<br />
Die Methode <span style="font-family: "courier new" , "courier" , monospace;">update_labels</span> ist die, welche die Werte aus der Queue holt und in Abhängigkeit vom Wert <span style="font-family: "courier new" , "courier" , monospace;">data['value_sensor_1']</span> bzw. <span style="font-family: "courier new" , "courier" , monospace;">data['value_sensor_2']</span> die Farbe des jeweiligen Labels ändert. Wichtig ist die Zeile<br />
<br />
<div class="highlight" style="background: #f8f8f8;">
<pre style="line-height: 125%;"><span style="color: green;">self</span><span style="color: #666666;">.</span>master<span style="color: #666666;">.</span>after(<span style="color: #666666;">500</span>, <span style="color: green;">self</span><span style="color: #666666;">.</span>update_labels)
</pre>
</div>
<br />
mit der festgelegt wird, dass die Methode alle 500 Millisekunden aufgerufen wird, so dass die Labels periodisch aktualisiert werden.<br />
In der Funktion <span style="font-family: "courier new" , "courier" , monospace;">exit_cleanup</span> wird dann noch der Thread sauber beendet, indem ein Event an die Funktion <span style="font-family: "courier new" , "courier" , monospace;">generate_data</span> gesendet wird, worauf hin diese sich beendet, dann wird noch das Tkinter Fenster gekillt.<br />
<br />
Wie zu sehen ist, ist das Entkoppeln von Messwerterfassungen und Messwertvisualisierung nicht weiter schwierig. Hat man mehrere verschiedene Sensoren, lässt sich das obige Beispiel auch einfach auf mehrere Threads erweitern.</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-84613705143984956202017-09-24T21:30:00.001+02:002017-09-24T21:45:57.454+02:00Learning Concurrency in Python - Buchrezension<div dir="ltr" style="text-align: left;" trbidi="on">
Der Name des englischsprachigen Buchs ist Programm: "Learning Concurrency in Python" hat das Ziel, eine Einführung in die <a href="https://de.wikipedia.org/wiki/Nebenl%C3%A4ufigkeit" target="_blank">nebenläufige Programmierung</a> mit Python 3 zu geben.<br />
<br />
Das ganze erfolgt recht umfassend auf rund 340 Seiten. Das Buch beginnt mit einem knappen Überblick über die verschiedenen Möglichkeiten, die Python bzw. zusätzliche Python-Module in Sachen nebenläufiger Programmierung bieten. Danach folgt ein allgemeiner (Python-unabhängiger) Überblick, welche Möglichkeiten und Limitierungen die Hardware bezüglich Nebenläufigkeit haben kann.<br />
Ab dem 3. Kapitel wird es dann konkreter: es werden die Themen Threads in Python, Synchronisation, Ressourcenteilung und Kommunikation von Threads und Multiprocessing behandelt. Threads und Multiprocessing werden dabei über die jeweiligen Module <span style="font-family: "courier new" , "courier" , monospace;">threading</span> und <span style="font-family: "courier new" , "courier" , monospace;">multiprocessing</span> erläutert, als auch (etwas kürzer) über das Modul <span style="font-family: "courier new" , "courier" , monospace;">concurrent.futures</span>.<br />
Das folgenden Kapitel heißt "Event-Driven Programming" und behandelt primär das <span style="font-family: "courier new" , "courier" , monospace;">asyncio</span>-Modul, am Ende des Kapitels gibt es aber auch (relativ kurze) Abschnitte zu den Modulen Twisted und Gevent.<br />
Die folgenden beiden Kapitel behandeln "Reactive Programming" mittels der Module RxPY und PyFunctional sowie die Nutzung der GPU über die Module PyCUDA, Numba, Theano und PyOpenCL.<br />
Zum Abschluss des Buchs gibt es nochmal auf rund zehn Seiten eine Zusammenfassung inklusive der Erläuterung der Vor- und Nachteile der verschiedene Wege zur nebenläufigen Programmierung mit Python.<br />
<br />
Es werden also so ziemlich alle gängigen Bereich in Sachen Python und nebenläufiger Programmierung abgedeckt. Gut ist, dass das Buch sehr viel Bespiele enthält, welche der Autor auch alle erläutert, so dass eigentlich kaum Fragen offen bleiben. Persönlich fand ich das Kapitel zu asyncio sehr gut, da es zumindest mir (enorm) geholfen hat, dass Thema (endlich) besser zu verstehen.<br />
<br />
Trotz der sehr guten und umfassenden Erklärungen hat das Buch aber einen ziemlich großen Mangel: in vielen Listings stimmen die Einrückungen nicht. Was so viel heißt wie, dass dort, wo syntaktisch Einrückungen sein müssen, keine sind (womit der Code in der abgedruckten Form nicht lauffähig wäre). Die Fehler sind zwar leicht zu erkennen, da immer "nur" eine Ebene fehlt. Aber für ein Fachbuch zu Python, wo Einrückungen essentiell wichtig sind, ist dieser Fehler schwer zu entschuldigen.<br />
<br />
Nichts desto trotz ist das Buch durchaus empfehlenswert, wenn man einen umfassenden Überblick, Einblick und Einstieg ins Thema "concurrent programming" mit Python sucht. Um das Buch zu verstehen und effektiv nutzen zu können, sollte man fortgeschrittene Kenntnisse haben oder zumindest ein fortgeschrittener, ambitionierter Einsteiger sein (vorher braucht man wahrscheinlich so wie keine nebenläufigen Programme...).<br />
Das Buch liest sich recht flüssige und sollte auch mit "normalen" Englischkenntnissen gut zu verstehen sein.<br />
Wer sich für das Thema des Buchs interessiert, der sollte einen Blick hinein werfen. <br />
<br />
Buchinfos:<br />
<br />
Titel: Learning Concurrency in Python<br />
Autor: Elliot Forbes<br />
Verlag: Packt Publishing<br />
ISBN: <span class="label"></span><span itemprop="isbn">9781787285378</span><br />
Preis: 42,99 Euro (gedruckte Ausgabe + E-Book), 29,15 Euro (nur E-Book)<br />
Erstauflage: August 2017</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-28087454056150994352016-08-14T21:16:00.000+02:002016-08-14T21:16:04.640+02:00Motorola Moto G 3. Gen - Bilder werden im Dateimanager nicht angezeigt<div dir="ltr" style="text-align: left;" trbidi="on">
Ein Verwandter hatte sich kürzlich ein Motorola Moto G 3. Generation als Smartphone gekauft. Da dieses Moto G einen Slot für Micro-SD Karten hat, wurde direkt eine 32 GB Karte eingelegt.<br />
<br />
Das Smartphone hat dann nach dem ersten Start noch ein Update auf Android 6.0 gemacht und läuft völlig problemlos. Geschossene Fotos werden dabei auf die externe Speicherkarten gespeichert.<br />
<br />
Jetzt sollten die Fotos auf den Computer übertragen werden. Also wurde das Handy per USB-Kabel an den Computer angeschlossen. Dieser erkannte das Smartphone und die SD-Karte auch direkt. Aber auf der SD-Karte wurden keine Fotos gefunden bzw. der Ordner "DCIM/Camera" wurde erst gar nicht angezeigt. Das Problem trat gleichermaßen unter Windows 10 als auch unter Ubuntu Linux 14.04 auf. Es macht auch keinen Unterschied, ob man am Smartphone als Modus für den Datentransfer <i>"PTP"</i> oder <i>"MTP"</i> auswählt.<br />
<br />
Eine kurze Recherche im Internet zeigt, dass dieses Probleme wohl viele Nutzer haben. In einem englischsprachigen Forum wurde gesagt, dass dies ein Bug sei, der im Zusammenhang mit dem Upgrade auf Android 6.0 steht. Aber, und das ist das gut, es wurde direkt auch eine Lösung gezeigt, welche auch funktioniert hat. Diese ist im folgenden aufgeführt:<br />
<br />
<u>Hinweis</u>: Auch wenn es beim Beschreiten des Lösungswegs normalerweise nicht zu Datenverlust kommen sollte, ist es dringend anzuraten, die auf der SD-Karte befindlichen Fotos vorher zu sichern (z.B. via Google Drive, Dropbox etc.).<br />
<br />
Die Lösung des Problems besteht darin, dass man von Android zwischengespeicherte Daten löscht und somit das Betriebssystem dazu "bewegt" die Daten neu zu lesen und anzulegen. Anschließend werden die Verzeichnisses im Dateimanager auch wieder wie gewohnt angezeigt. Das ganze geht so:<br />
<br />
Man geht in die <i>"Einstellungen"</i> und wählt dort den Punkt <i>"Apps"</i>. Hier klickt man rechts oben die drei senkrechten Punkte und wählt den Menüpunkt <i>"Systemprozesse anzeigen"</i> aus.<br />
Nun sucht man in der langen Liste der Prozesse die Punkte <i>"Externer Speicher"</i> und <i>"Medienspeicher"</i>.<br />
Beide Einträge öffnet man, klickt auf das Feld <i>"Speicher"</i> und klickt dann auf <i>"Daten löschen"</i> und <i>"Cache leeren"</i> .<br />
<br />
Jetzt muss man das Smartphone noch ausschalten und dann wieder einschalten. Bevor man es an den Computer anschließt sollte ein paar Minuten (ca. 8-10) gewartet werden, damit Android die Daten neu anlegen kann (wie lange dies wirklich dauert weiß ich nicht, aber das englischsprachig Forum empfiehlt, so lange zu warten).<br />
<br />
Schließt man das Moto G 3. Gen jetzt an den Computer an und wählt unter <i>"USB zum Aufladen"</i> die Option <i>"Dateien übertragen (MTP)"</i>, dann sollte man wieder alle Ordner auf der SD-Karte sehen. Inklusive des Ordners, in dem die Fotos liegen. Jedenfalls hat es bei meinem Verwandten so einwandfrei funktioniert.</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-30613439073517659032016-01-30T22:06:00.002+01:002016-01-30T22:06:56.459+01:00Python, WTForms, JavaScript: Formulare dynamisch erweitern<div dir="ltr" style="text-align: left;" trbidi="on">
Folgender Fall: in einer Webanwendung sollen Daten über ein Formular eingegeben werden. Die Felder des Formulars sind klar definiert, aber wie viele Daten eingegeben werden sollen, ist offen.<br />
<br />
Ein Beispiel hierfür wäre z.B. eine Webanwendung für Rezepte. Es gibt Rezepte, die haben eine handvoll Zutaten. Aber es gibt auch Rezepte, die haben mehrere Dutzend Zutaten. Hier macht es also wenig Sinn, die Anzahl der Eingabefelder für das Formular fix festzulegen. Vielmehr gilt es, die Möglichkeit für den Nutzer zu schaffen, das Formular dynamisch zu erweitern.<br />
<br />
Die Zutaten hier dafür: <a href="http://www.python.org/" target="_blank">Python</a> 3.4, <a href="http://www.bottlepy.org/" target="_blank">Bottle</a> 0.12, <a href="https://github.com/wtforms/wtforms" target="_blank">WTForms</a> 2.1 , JavaScript und jQuery 2.2. Bottle als Webframework ist hier übrigens nur Mittel zum Zweck, das Beispiel lässt sich genau so mit anderen Python Webframeworks umsetzen.<br />
<br />
Das Beispiel besteht aus vier Codeteilen:<br />
<ul style="text-align: left;">
<li>der Bottle Hauptanwendung, die das Routing etc. festlegt</li>
<li>den Formularklassen, erstellt mit WTForms</li>
<li>den beiden Templates zur Dateneingabe und zur Datenausgabe</li>
</ul>
Wer das Beispiel nachstellen möchte: wie bei Bottle üblich wird erwartet, dass die Templates im Verzeichnis <b>views</b> liegen. Das benötigte jQuery wird im Verzeichnis <b>static</b> erwartet. Alternativ könnte man natürlich auch jQuery von einem der vielen CDN-Servern online laden.<br />
<br />
Die Hauptdatei sieht so aus:<br />
<br />
<div class="highlight" style="background: #f8f8f8;">
<pre style="line-height: 125%;"><span style="color: green; font-weight: bold;">import</span> <span style="color: blue; font-weight: bold;">os</span>
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">bottle</span> <span style="color: green; font-weight: bold;">import</span> route, run, template, debug, static_file, request
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">my_forms</span> <span style="color: green; font-weight: bold;">import</span> InputForm
BASE_DIR <span style="color: #666666;">=</span> os<span style="color: #666666;">.</span>path<span style="color: #666666;">.</span>dirname(os<span style="color: #666666;">.</span>path<span style="color: #666666;">.</span>dirname(__file__))
<span style="color: #aa22ff;">@route</span>(<span style="color: #ba2121;">'/ingredients'</span>)
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">index</span>():
form <span style="color: #666666;">=</span> InputForm()
<span style="color: green; font-weight: bold;">return</span> template(<span style="color: #ba2121;">'dyn_form_input_wt.html'</span>, form<span style="color: #666666;">=</span>form)
<span style="color: #aa22ff;">@route</span>(<span style="color: #ba2121;">'/ingredients'</span>, method<span style="color: #666666;">=</span><span style="color: #ba2121;">'POST'</span>)
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">post_data</span>():
form_data <span style="color: #666666;">=</span> request<span style="color: #666666;">.</span>forms
form <span style="color: #666666;">=</span> InputForm(form_data)
<span style="color: green; font-weight: bold;">if</span> <span style="color: #aa22ff; font-weight: bold;">not</span> form<span style="color: #666666;">.</span>validate():
errors <span style="color: #666666;">=</span> form<span style="color: #666666;">.</span>errors[<span style="color: #ba2121;">'ingredients'</span>]
<span style="color: green; font-weight: bold;">else</span>:
errors <span style="color: #666666;">=</span> <span style="color: green;">None</span>
ingredients <span style="color: #666666;">=</span> []
<span style="color: green; font-weight: bold;">for</span> i <span style="color: #aa22ff; font-weight: bold;">in</span> <span style="color: green;">range</span>(<span style="color: #666666;">0</span>, <span style="color: green;">int</span>(<span style="color: green;">len</span>(form_data)<span style="color: #666666;">/3</span>)):
ingredients<span style="color: #666666;">.</span>append((form_data[<span style="color: #ba2121;">'ingredients-'</span><span style="color: #666666;">+</span><span style="color: green;">str</span>(i)<span style="color: #666666;">+</span><span style="color: #ba2121;">'-description'</span>],
form_data[<span style="color: #ba2121;">'ingredients-'</span><span style="color: #666666;">+</span><span style="color: green;">str</span>(i)<span style="color: #666666;">+</span><span style="color: #ba2121;">'-unit'</span>],
form_data[<span style="color: #ba2121;">'ingredients-'</span><span style="color: #666666;">+</span><span style="color: green;">str</span>(i)<span style="color: #666666;">+</span><span style="color: #ba2121;">'-quantity'</span>]))
<span style="color: green; font-weight: bold;">return</span> template(<span style="color: #ba2121;">'output.html'</span>,
ingredients<span style="color: #666666;">=</span>ingredients,
errors<span style="color: #666666;">=</span>errors)
<span style="color: #aa22ff;">@route</span>(<span style="color: #ba2121;">'/static/<filename>'</span>)
<span style="color: green; font-weight: bold;">def</span> <span style="color: blue;">server_static</span>(filename):
<span style="color: green; font-weight: bold;">return</span> static_file(filename, root<span style="color: #666666;">=</span>os<span style="color: #666666;">.</span>path<span style="color: #666666;">.</span>join(BASE_DIR, <span style="color: #ba2121;">'static'</span>))
debug<span style="color: #666666;">=</span><span style="color: green;">True</span>
run(host<span style="color: #666666;">=</span><span style="color: #ba2121;">'localhost'</span>, port<span style="color: #666666;">=8080</span>, reloader<span style="color: #666666;">=</span><span style="color: green;">True</span>)
</pre>
</div>
<br />
Hier gibt es eigentlich nicht viel zu sagen. Eine Route liefert das Formular aus, die Route mit <span style="font-family: "courier new" , "courier" , monospace;">method='POST'</span> nimmt die Formulardaten auf und sortiert diese zurück in eine Liste. In einer realen Anwendung würde man an dieser Stelle die Daten z.B. in eine Datenbank schreiben.<br />
Wichtig ist hierbei aber, dass davon ausgegangen wird, dass die <span style="font-family: "courier new" , "courier" , monospace;">name</span>-Attribute der Formularfelder a) der Namensgebung von WTForms FieldList entsprechen - also Name des Formulars - Zähler - Name des Felds - , b) der Zähler bei Null beginnt und c) die Zählung lückenlos ist (also 0, 1, 2, 3 usw.). Dass das tatsächlich auch so ist, dafür sorgt der JavaScript in der weiter unten gezeigten Template-Datei <b>dyn_form_input_wt.html</b>.<br />
<br />
Die Datei <b>my_forms.py</b>, welche die Formularklassen enthält, sieht so aus:<br />
<br />
<div class="highlight" style="background: #f8f8f8;">
<pre style="line-height: 125%;"><span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">wtforms.form</span> <span style="color: green; font-weight: bold;">import</span> Form
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">wtforms.fields</span> <span style="color: green; font-weight: bold;">import</span> StringField, SelectField, DecimalField, FormField, \
FieldList
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">wtforms.validators</span> <span style="color: green; font-weight: bold;">import</span> NumberRange, InputRequired
UNITS <span style="color: #666666;">=</span> [(<span style="color: #ba2121;">'kg'</span>, <span style="color: #ba2121;">'kg'</span>),
(<span style="color: #ba2121;">'g'</span>, <span style="color: #ba2121;">'g'</span>),
(<span style="color: #ba2121;">'TL'</span>, <span style="color: #ba2121;">'TL'</span>),
(<span style="color: #ba2121;">'EL'</span>, <span style="color: #ba2121;">'EL'</span>),
(<span style="color: #ba2121;">'St'</span>, <span style="color: #ba2121;">'St'</span>)]
<span style="color: green; font-weight: bold;">class</span> <span style="color: blue; font-weight: bold;">IngredientForm</span>(Form):
description <span style="color: #666666;">=</span> StringField(<span style="color: #ba2121;">'description'</span>,
[InputRequired()])
unit <span style="color: #666666;">=</span> SelectField(<span style="color: #ba2121;">'unit'</span>, choices<span style="color: #666666;">=</span>UNITS)
quantity <span style="color: #666666;">=</span> DecimalField(<span style="color: #ba2121;">'quantity'</span>,
[InputRequired(), NumberRange(<span style="color: green;">min</span><span style="color: #666666;">=0</span>)],
places<span style="color: #666666;">=3</span>)
<span style="color: green; font-weight: bold;">class</span> <span style="color: blue; font-weight: bold;">InputForm</span>(Form):
ingredients <span style="color: #666666;">=</span> FieldList(FormField(IngredientForm), min_entries<span style="color: #666666;">=1</span>)
</pre>
</div>
<br />
Das ist soweit alles WTForms Standard ohne Tricks und Kniffe. In der Klasse <span style="font-family: "courier new" , "courier" , monospace;">InputForm</span> wird die <span style="font-family: "courier new" , "courier" , monospace;">IngredientForm</span> per <span style="font-family: "courier new" , "courier" , monospace;">FormField</span> zu einem Formular-Feld zusammengefasst, <span style="font-family: "courier new" , "courier" , monospace;">FieldList</span> wiederum fasst mehrere Formular-Felder zusammen. Mehr Infos dazu sind in der <a href="http://wtforms.readthedocs.org/en/2.1/fields.html?highlight=fieldlist#wtforms.fields.FormField" target="_blank">Dokumentation von WTForms</a> zu finden.<br />
<br />
Das Template <b>dyn_form_input_wt.html </b>ist für die Eingabe der Zutaten sowie das dynamische Hinzufügen und Entfernen von Eingabefeldern zuständig. Das Template sieht so aus:<br />
<br />
<div class="highlight" style="background: #f8f8f8;">
<pre style="line-height: 125%;"><span style="color: #bc7a00;"><!DOCTYPE html></span>
<span style="color: green; font-weight: bold;"><html></span>
<span style="color: green; font-weight: bold;"><head></span>
<span style="color: green; font-weight: bold;"><title></span>Dynamic Forms<span style="color: green; font-weight: bold;"></title></span>
<span style="color: green; font-weight: bold;"><script </span><span style="color: #7d9029;">src=</span><span style="color: #ba2121;">"static/jquery-2.2.0.min.js"</span><span style="color: green; font-weight: bold;">></script></span>
<span style="color: green; font-weight: bold;"></head></span>
<span style="color: green; font-weight: bold;"><body></span>
<span style="color: green; font-weight: bold;"><form</span> <span style="color: #7d9029;">action=</span><span style="color: #ba2121;">"/ingredients"</span> <span style="color: #7d9029;">method=</span><span style="color: #ba2121;">"POST"</span><span style="color: green; font-weight: bold;">></span>
<span style="color: green; font-weight: bold;"><div</span> <span style="color: #7d9029;">class=</span><span style="color: #ba2121;">"input_fields_wrap"</span><span style="color: green; font-weight: bold;">></span>
<span style="color: #408080; font-style: italic;"><!-- here goes the form fields --></span>
<span style="color: green; font-weight: bold;"></div></span>
<span style="color: green; font-weight: bold;"><button</span> <span style="color: #7d9029;">class=</span><span style="color: #ba2121;">"add_field_button"</span><span style="color: green; font-weight: bold;">></span>Add field<span style="color: green; font-weight: bold;"></button></span>
<span style="color: green; font-weight: bold;"><button</span> <span style="color: #7d9029;">class=</span><span style="color: #ba2121;">"remove_field_button"</span><span style="color: green; font-weight: bold;">></span>Remove field<span style="color: green; font-weight: bold;"></button></span>
<span style="color: green; font-weight: bold;"><input</span> <span style="color: #7d9029;">type=</span><span style="color: #ba2121;">"submit"</span><span style="color: green; font-weight: bold;">></span>
<span style="color: green; font-weight: bold;"></form></span>
<span style="color: green; font-weight: bold;"><script></span>
$(<span style="color: green;">document</span>).ready(<span style="color: green; font-weight: bold;">function</span>() {
<span style="color: green; font-weight: bold;">var</span> max_fields <span style="color: #666666;">=</span> <span style="color: #666666;">20</span>; <span style="color: #408080; font-style: italic;">//maximum input boxes allowed</span>
<span style="color: green; font-weight: bold;">var</span> wrapper <span style="color: #666666;">=</span> $(<span style="color: #ba2121;">".input_fields_wrap"</span>); <span style="color: #408080; font-style: italic;">//Fields wrapper</span>
<span style="color: green; font-weight: bold;">var</span> addButton <span style="color: #666666;">=</span> $(<span style="color: #ba2121;">".add_field_button"</span>); <span style="color: #408080; font-style: italic;">//Add button ID</span>
<span style="color: green; font-weight: bold;">var</span> removeButton <span style="color: #666666;">=</span> $(<span style="color: #ba2121;">".remove_field_button"</span>); <span style="color: #408080; font-style: italic;">//Add button ID</span>
<span style="color: green; font-weight: bold;">var</span> htmlString <span style="color: #666666;">=</span> <span style="color: #ba2121;">'<div id="input_fields_0">{{ !form.ingredients }}</div>'</span>;
<span style="color: green; font-weight: bold;">var</span> field_counter <span style="color: #666666;">=</span> <span style="color: #666666;">0</span>;
$(wrapper).append(makeString()); <span style="color: #408080; font-style: italic;">//add the first form</span>
$(addButton).click(<span style="color: green; font-weight: bold;">function</span>(e){ <span style="color: #408080; font-style: italic;">//on add input button click</span>
e.preventDefault();
<span style="color: green; font-weight: bold;">if</span>(field_counter <span style="color: #666666;"><</span> max_fields){ <span style="color: #408080; font-style: italic;">//max input box allowed</span>
field_counter<span style="color: #666666;">++</span>;
$(wrapper).append(makeString());}
<span style="color: green; font-weight: bold;">else</span> { <span style="color: green;">window</span>.alert(<span style="color: #ba2121;">'max number of ingredients reached!'</span>)}
});
$(removeButton).click(<span style="color: green; font-weight: bold;">function</span>(e){ <span style="color: #408080; font-style: italic;">//on remove input button click</span>
e.preventDefault();
<span style="color: green; font-weight: bold;">if</span>(field_counter <span style="color: #666666;">></span> <span style="color: #666666;">0</span>){ <span style="color: #408080; font-style: italic;">//make sure at least one field is there</span>
$(<span style="color: #ba2121;">'#input_fields_'</span><span style="color: #666666;">+</span>field_counter).remove();
field_counter<span style="color: #666666;">--</span>;}
<span style="color: green; font-weight: bold;">else</span> { <span style="color: green;">window</span>.alert(<span style="color: #ba2121;">'Cannot delete, one input has to remain.'</span>)}
});
<span style="color: green; font-weight: bold;">function</span> makeString() {
<span style="color: green; font-weight: bold;">var</span> myString <span style="color: #666666;">=</span> htmlString;
<span style="color: green; font-weight: bold;">return</span> myString.replace(<span style="color: #bb6688;">/0/g</span>, field_counter);
};
});
<span style="color: green; font-weight: bold;"></script></span>
<span style="color: green; font-weight: bold;"></body></span>
<span style="color: green; font-weight: bold;"></html></span>
</pre>
</div>
<br />
Wie zu sehen ist, besteht das Template aus zwei Sektionen: dem HTML-Teil und dem JavaScript Teil. Erster ist ziemlich "straight forward" und bedarf wohl keiner weiterer Erklärung, letzter macht die Hauptarbeit und liefert die Dynamik.<br />
<br />
Im HTML-Teil wird nur der "Rahmen" für das Formular angelegt, es werden aber keine Formularfelder erzeugt. Dies geschieht ausschließlich per JavaScript / jQuery.<br />
<br />
Den Ausgangspunkt bildet die Definition der Variablen <span style="font-family: "courier new" , "courier" , monospace;">htmlString</span>, in der auch das WTForms Formular gerendert wird, über <span style="font-family: "courier new" , "courier" , monospace;">{{ !form.ingredients }}</span><br />
<br />
Die Zeile <span style="font-family: "courier new" , "courier" , monospace;">$(wrapper).append(makeString());</span> erzeugt den ersten Satz Formularfelder, der Zähler <span style="font-family: "Courier New",Courier,monospace;">field_counter</span> steht hier noch auf Null.<br />
<br />
Zum Hinzufügen weitere Felder ist eine JavaScript-Funktion an den Button <i>"Add field"</i> gebunden, welche a) checkt, ob nicht die per <span style="font-family: "courier new" , "courier" , monospace;">max_fields</span> festgelegte Anzahl an Feldern schon erreicht ist, b) den Feldzähler <span style="font-family: "courier new" , "courier" , monospace;">field_counter</span> um eins erhöht und dann c) einen Satz Formularfelder hinzufügt.<br />
<br />
Zum Löschen, welches über eine an den Button <i>"Remove Fields"</i> gebundene Funktion erfolgt, ist der Ablauf ähnlich. Hier wird zuerst geprüft, ob mehr als ein Satz Formularfelder vorhanden ist. Wenn ja wird das letzte entfernt und der <span style="font-family: "courier new" , "courier" , monospace;">field_counter</span> Zähler um eins dekrementiert.<br />
<br />
Die Funktion <span style="font-family: "courier new" , "courier" , monospace;">makeString</span> ist dafür verantwortlich, dass die Nummerierung der Formularfelder (bzw. genau genommen deren Attribute wie <span style="font-family: "courier new" , "courier" , monospace;">id</span>, <span style="font-family: "courier new" , "courier" , monospace;">name</span> etc. gem. dem aktuellen Stand von <span style="font-family: "courier new" , "courier" , monospace;">field_counter</span> angepasst werden. Dazu wird einfach die String-Methode <span style="font-family: "courier new" , "courier" , monospace;">replace()</span> mit den entsprechenden Werten auf den Ausgangsstring angewendet und als neuer String zurück geliefert.<br />
<br />
Wenn das Formular abgesendet wurde, wird in diesem Beispiel hier das Template <b>output.html</b> aufgerufen, welches die eine Liste der Zutaten mit Menge und Einheit sowie mögliche Fehler ausgibt. Das Template sieht so aus:<br />
<br />
<div class="highlight" style="background: #f8f8f8;">
<pre style="line-height: 125%;"><span style="color: #bc7a00;"><!DOCTYPE html></span>
<span style="color: green; font-weight: bold;"><html></span>
<span style="color: green; font-weight: bold;"><head></span>
<span style="color: green; font-weight: bold;"><title></span>Dynamic Forms Output<span style="color: green; font-weight: bold;"></title></span>
<span style="color: green; font-weight: bold;"></head></span>
<span style="color: green; font-weight: bold;"><body></span>
<span style="color: green; font-weight: bold;"><p></span>Received ingredients from form data:<span style="color: green; font-weight: bold;"></p></span>
<span style="color: green; font-weight: bold;"><ul></span>
% for ingredient in ingredients:
<span style="color: green; font-weight: bold;"><li></span>{{ ingredient[0] }}: {{ ingredient[2] }} {{ ingredient[1] }}
% end
<span style="color: green; font-weight: bold;"></ul></span>
% if errors:
<span style="color: green; font-weight: bold;"><p></span>The form has the following errors:
<span style="color: green; font-weight: bold;"><ul></span>
% for error in errors:
<span style="color: green; font-weight: bold;"><li></span>{{ error }}
% end
<span style="color: green; font-weight: bold;"></ul></span>
% end
<span style="color: green; font-weight: bold;"></body></span>
<span style="color: green; font-weight: bold;"></html></span>
</pre>
</div>
<br />
Das Template ist hier auch nur Mittel zum Zweck, bei Produktiv-Code würde man hier sicherlich anders vorgehen.<br />
<br />
Wie hier gezeigt wurde, ist es nicht so schwierig und aufwendig, dynamisch erweiterbare Formular zu erzeugen, auch wenn diese in den ansonsten "starren" Klassen eines Python Formularframeworks definiert sind.<br />
Dazu benötigt werden aus WTForms insbesonders die Klassen <span style="font-family: "courier new" , "courier" , monospace;">FormField</span> und <span style="font-family: "courier new" , "courier" , monospace;">FieldList</span> sowie auf der Client-Seite ein bisschen JavaScript mit jQuery.<br />
<br />
<br /></div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-21619577721176652962015-10-03T21:17:00.000+02:002015-10-03T21:21:41.858+02:00systemd timer unter Raspbian<div dir="ltr" style="text-align: left;" trbidi="on">
Ende September ist eine neue Raspian Version erschienen, welche auf <a href="https://wiki.debian.org/DebianJessie" target="_blank">Debian Jessie</a> basiert. Und damit ist auch <a href="http://www.freedesktop.org/wiki/Software/systemd/" target="_blank">systemd</a> als Init-Dienst standardmäßig an Bord.<br />
<br />
Davon bekommt man als "normaler" Nutzer erst mal wenig mit, weil man in der Regel wenig "Berührung" mit dem Init-System hat.<br />
<br />
Durch den Einsatz von systemd bietet sich aber die Möglichkeit, dessen Timer zu verwenden. systemd Timer sind ein Dienst, welcher periodisch bestimmte, frei festzulegende Aktion ausführen kann - und sind damit eine Alternative zu <a href="https://de.wikipedia.org/wiki/Cron" target="_blank">cron</a>.<br />
<br />
Das wird im folgenden anhand eines Beispiels gezeig. <br />
<br />
Unter Verwendung eines systemd Timer soll das folgende Python Skript alle 5 Minuten ausgeführt werden:<br />
<pre><code>
#!/usr/bin/env python3
import sqlite3
import datetime
from sense_hat import SenseHat
s = SenseHat()
conn = sqlite3.connect('/home/pi/code/ambient_data.db')
sql = 'INSERT INTO data VALUES (?, ?, ?, ?)'
with conn:
conn.execute(sql, (s.get_temperature(),
s.get_pressure(),
s.get_humidity(),
datetime.datetime.now()))
conn.close()</code></pre>
<br />
Das Skript liest den Temperatur-, Luftdruck- und Luftfeuchtigkeitssensor eines <a href="https://www.raspberrypi.org/products/sense-hat/" target="_blank">SenseHAT</a> aus und schreibt die Daten in eine SQLite-Datenbank.<br />
Das Skript heißt <b>ambient_temperature_logger.py</b> und liegt unter <b>/home/pi/code</b>.<br />
Die zugehörige Datenbank <b>ambient_data.db</b> liegt im gleichen Verzeichnis.<br />
<br />
Damit das Skript automatisch ausgeführt wird, muss man noch zwei Dateien für systemd erstellen:<br />
<ul style="text-align: left;">
<li>eine .service Datei, welche das Skript aufruft</li>
<li>eine .timer Datei, welche die Daten für das automatische Ausführen enthält.</li>
</ul>
Beide Dateien legt man mit Root-Rechten im Verzeichnis <b>/etc/systemd/system</b> an.<br />
<br />
Die Datei <b>ambient_data_logger.service</b> bekommt folgenden Inhalt:<br />
<pre><code>
[Unit]
Description=SenseHat Ambient Data Logger
[Service]
Type=simple
ExecStart=/home/pi/code/ambient_data_logger.py</code></pre>
<br />
Der Inhalt ist wohl weitestgehend selbsterklärend:<br />
Im Abschnitt <span style="font-family: "Courier New",Courier,monospace;">[Unit]</span> steht eine Beschreibung, was passiert. Im Abschnitt <span style="font-family: "Courier New",Courier,monospace;">[Service]</span> wird bei <span style="font-family: "Courier New",Courier,monospace;">Type</span> der Typ festgelegt, was bei Skripten wie in diesem Beispiel normalerweise <span style="font-family: "Courier New",Courier,monospace;">simple</span> ist. Unter <span style="font-family: "Courier New",Courier,monospace;">ExecStart</span> wird der Befehl zum Aufruf des Skripts angegeben.<br />
<br />
Die Datei <b>ambient_data_logger.timer</b> bekommt den folgenden Inhalt:<br />
<pre><code>
[Unit]
Description=Runs the ambient_data_logger Python script every 5 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Unit=ambient_data_logger.service
[Install]
WantedBy=multi-user.target</code></pre>
<br />
Unter <span style="font-family: "Courier New",Courier,monospace;">[Unit] </span>wird wieder ein Beschreibung hinterlegt, unter <span style="font-family: "Courier New",Courier,monospace;">[Install] WantedBy</span>, welchem target der Timer zugeordnet wird. Für Skripte wie diese ist <span style="font-family: "Courier New",Courier,monospace;">multi-user.target</span> eine gute Wahl, weil der (aktiverte) Timer dann beim Systemstart mit gestartet wird.<br />
<br />
Im Abschnitt [Timer] wird festgelegt, wann was passieren soll:
<br />
<ul style="text-align: left;">
<li><span style="font-family: "Courier New",Courier,monospace;">OnBootSec</span> legt fest, wann das Skript (bzw. im Jargon von systemd die "Unit") ausgeführt werden soll - hier also nach 2 Minuten.</li>
<li><span style="font-family: "Courier New",Courier,monospace;">OnUnitActiveSec</span> legt fest, nach wie viel Minuten die Unit jedes weitere Mal ausgeführt werden soll - hier also alle 5 Minuten</li>
<li><span style="font-family: "Courier New",Courier,monospace;">Unit</span> legt fest, welche <span style="font-family: "Courier New",Courier,monospace;">.service</span>-Datei durch den Timer aufgerufen wird.</li>
</ul>
Jetzt muss man die .timer und die .service-Dateien noch ausführbar machen:<br />
<br />
<pre><code>$ sudo chmod +x ambient_data_logger.*</code></pre>
<br />
und den Timer mit den folgenden beiden Befehlen aktivieren:<br />
<br />
<pre><code>$ sudo systemctl start ambient_data_logger.timer
$ sudo systemctl enable ambient_data_logger.timer</code></pre>
<br />
Der erste Befehl startet den Timer, der zweite stellt sicher, dass der Timer auch nach einem Reboot aktiviert wird.<br />
Zum Deaktivieren des Timers ersetzt man im ersten Befehl <span style="font-family: "Courier New",Courier,monospace;">start</span> durch <span style="font-family: "Courier New",Courier,monospace;">stop</span> und im zweiten Befehl <span style="font-family: "Courier New",Courier,monospace;">enable</span> durch <span style="font-family: "Courier New",Courier,monospace;">disable</span>. <br />
<br />
Um zu prüfen, ob der Timer auch aktiv ist und läuft, kann man sich alle aktiven Timer anzeigen lassen:<br />
<br />
<pre><code>$ systemctl list-timers</code></pre>
<br />
Das Anlegen von Timer von systemd ist also ziemlich gradlinig und nicht weiter schwierig.<br />
<br />
Da systemd bei anderen Distribution (z.B. Fedora, Arch) schon länger im Einsatz ist, findet man im Internet reichlich Beispiele zu systemd und Timern. Da systemd weitestgehend unabhängig von den jeweiligen Eigenheiten der darüber liegenden Distribution ist, sind die Beispiele auch auf Raspbian übertragbar.<br />
Außerdem sind die Man-Pages zu systemd und systemd.timer recht ausführlich.</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-81097033728058477372015-08-15T19:14:00.000+02:002015-08-15T19:14:21.352+02:00Python: PIL/Pillow - Bild anzeigen mit Image.show() ( xv vs. xli vs. command vs. imagemagick)<div dir="ltr" style="text-align: left;" trbidi="on">
Möchte man sich mit Hilfe von Python ein Bild anzeigen lassen, dann geht das mit drei Zeilen Code und dem <a href="https://python-pillow.github.io/" target="_blank">Pillow-Modul</a>. Pillow ist ein aktiv entwickelter "friendly fork" von PIL (=Python Image Library). PIL bzw. Pillow sind der quasi-Standard unter Python, wenn es das Handling und Manipulieren von Grafik-Dateien geht.<br />
<br />
Die folgenden drei Codezeilen zeigen das Bild "foo.png" an:<br />
<pre><code>
>>> from PIL import Image
>>> im = Image.open('foo.png')
>>> im.show()</code></pre>
<br />
So zumindest die Theorie. In der Praxis passiert aber zumindest unter Ubuntu 14.04 und auch unter Raspbian nichts. Nach dem Aufruf von <span style="font-family: "Courier New",Courier,monospace;">im.show()</span> wird sofort wieder der Prompt des Python-Interpreters angezeigt. Keine Fehlermeldung, aber auch kein Bild.<br />
<br />
Ein Blick in die Doku von Pillow brachte dann ans Tageslicht, dass die <a href="https://pillow.readthedocs.org/reference/Image.html#PIL.Image.Image.show" target="_blank">show-Funktion</a> den Bildbetrachter <a href="https://de.wikipedia.org/wiki/XV_%28Software%29" target="_blank">xv</a> aufruft. Dieser war in den 90er Jahren des vorherigen Jahrhunderts wohl mal der de-facto Bildbetrachter auf Unix-System, ist heute jedoch (ziemlich) veraltet. Und damit auch in den meisten Distributionen wie Debian, Ubuntu etc. nicht mehr enthalten.<br />
<br />
Um trotzdem Bilder mittels <span style="font-family: "Courier New",Courier,monospace;">show()</span> anzeigen zu können, gibt es zwei alternative Wege:<br />
<br />
Einer ist, das Paket <b>imagemagick</b> zu installieren. Dieses ist für quasi alle Linux-Distributionen in den Paketquellen enthalten.<br />
Danach funktioniert der Aufruf von <span style="font-family: "Courier New",Courier,monospace;">show()</span>, es wird <span style="font-family: "Courier New",Courier,monospace;">display</span> aus dem ImageMagick-Paket zur Anzeige des Bilds verwendet. Leider ist dieser Weg in der Doku von Pillow nicht dokumentiert. Ein Blick in den Quellcode von von <span style="font-family: "Courier New",Courier,monospace;">Image</span> zeigt aber, dass Pillow auf Linux-Systemen explizit nach <span style="font-family: "Courier New",Courier,monospace;">display</span> sucht und diese - sofern vorhanden - verwendet.<br />
<br />
Eine andere Alternative ist im <a href="https://www.raspberrypi.org/forums/viewtopic.php?f=91&t=79379" target="_blank">Raspberry Pi</a> Forum zu finden. Hier wird also das Paket <b>xli</b> (xli="command line tool for viewing images in X11") installiert und ein Softlink von <span style="font-family: "Courier New",Courier,monospace;">xv</span> auf <span style="font-family: "Courier New",Courier,monospace;">xli</span> angelegt: <br />
<br />
<span style="font-family: inherit;"><code>$ sudo apt-get install xli</code></span><br />
<span style="font-family: inherit;"><code>$ cd /usr/local/bin<br />$ sudo ln -s /usr/bin/xli xv</code></span><br />
<br />
Dies funktioniert sowohl unter Raspbian als auch unter Ubuntu (und vermutlich auch bei den meisten anderen Linux-Distros, die xli in den Paketquellen haben.<br />
<br />
Die <span style="font-family: "Courier New",Courier,monospace;">show</span>-Funktion kennt das optionale Argument <span style="font-family: "Courier New",Courier,monospace;">command=</span>, mit dem der Bildbetrachter explizit vorgegeben werden. So steht es jedenfalls in der Dokumentation. Allerdings hat das auf keinem meiner Systeme funktioniert.</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-55642439456725871152015-08-03T08:52:00.000+02:002015-08-03T08:52:28.824+02:00Python: Ausrichten von Paragraphen in Platypus / ReportLab<div dir="ltr" style="text-align: left;" trbidi="on">
Paragraphen von <a href="http://www.reportlab.com/" target="_blank">Reportlabs</a> Playtypus sind dazu da, "normalen" Text aufzunehmen und dann im PDF-Dokument darzustellen. Und wie in jeder anderen Textdarstellung auch hat der Text in diesen Paragraphen natürlich auch einen Ausrichtung. Diese ist per Voreinstellung linksbündig.<br />
<br />
Nun gibt es aber auch genug Anwendungsfälle, wo man den Text nicht linksbündig ausrichten möchte. So bietet es sich bei längern Textpassagen an, den Text als <a href="https://de.wikipedia.org/wiki/Blocksatz" target="_blank">Blocksatz</a> zu formatieren, damit das rechte Textende nicht "flattert". <br />
<br />
Das ist natürlich auch mit ReportLab / Platypus auch ohne Probleme möglich. Es werden alle vier gängigen Textformatierungen unterstützt: linksbündig, rechtsbündig, zentriert und Blocksatz.<br />
<br />
Wie das geht, ist im folgenden Beispiel gezeigt. Für rechtsbündig, zentriert und Blocksatz wird einfach das bestehenden Stylesheet um neue Styles erweitert, die die entsprechenden Ausrichtung haben:<br />
<pre><code>
from reportlab.platypus import Paragraph, SimpleDocTemplate
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY
from reportlab.lib.pagesizes import A4
text = '''Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita
kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.'''
text_short = 'Lorem ipsum dolor sit amet'
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(name='RightAlign',
parent=ParagraphStyle('Normal'),
alignment=TA_RIGHT))
styles.add(ParagraphStyle(name='CenterAlign',
parent=ParagraphStyle('Normal'),
alignment=TA_CENTER))
styles.add(ParagraphStyle(name='JustifyAlign',
parent=ParagraphStyle('Normal'),
alignment=TA_JUSTIFY))
doc = SimpleDocTemplate('align_test.pdf', pagesize=A4)
story = []
story.append(Paragraph(text_short, styles['Normal']))
story.append(Paragraph(text_short, styles['RightAlign']))
story.append(Paragraph(text_short, styles['CenterAlign']))
story.append(Paragraph(text, styles['JustifyAlign']))
doc.build(story)
</code>
</pre>
Führt man den obigen Code aus, dann enthält dieser vier Paragraphen: der erste ist linksbündig ausgerichtet, der zweite rechtsbündig, der dritte zentriert und der vierte als Blocksatz.<br />
<br />
Intern verwendet ReportLab für die Ausrichtung Integer-Werte, die importierten Konstanten <span style="font-family: "Courier New",Courier,monospace;">TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY</span> stellen diese einfach nur dar, wie man leicht im interaktiven Python-Interpreter sehen kann:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">>>> from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY<br />>>> TA_CENTER<br />1<br />>>> TA_LEFT<br />0<br />>>> TA_RIGHT<br />2<br />>>> TA_JUSTIFY<br />4</span><br />
<span style="font-family: "Courier New",Courier,monospace;">>>></span><br />
<br />
Im obigen Codebeispiel könnte man also statt<br />
<pre><code>
styles.add(ParagraphStyle(name='JustifyAlign',
parent=ParagraphStyle('Normal'),
alignment=TA_JUSTIFY))
</code></pre>
auch schreiben:<br />
<pre><code>
styles.add(ParagraphStyle(name='JustifyAlign',
parent=ParagraphStyle('Normal'),
alignment=4))
</code></pre>
<br />
Das Ergebnis ist das gleich und man würde den Import der <span style="font-family: "Courier New",Courier,monospace;">TA_* </span>Konstanten "sparen".</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-13205449864316235372015-08-01T14:29:00.000+02:002015-08-01T14:29:37.450+02:00WTForms: Daten für SelectField nachträglich einfügen<div dir="ltr" style="text-align: left;" trbidi="on">
<a href="http://pythonhosted.org/WTForms/" target="_blank">WTForms</a> gehört in der Python-Welt zu den populäreren Modulen zum Generieren und Validieren von HTML-Formularen für Webanwendungen.<br />
<br />
Hier im Blogeintrag geht es aber nicht um WTForms im gesamten - die <a href="http://wtforms.readthedocs.org/en/latest/" target="_blank">offizielle Doku</a> ist ja so wie so recht ausführlich - sondern "nur" um das nachträgliche Hinzufügen von Auswahlmöglichkeiten für ein <span style="font-family: "Courier New",Courier,monospace;">SelectField</span> (=Auswahlfeld). Wobei das im folgenden gezeigt auch für das <span style="font-family: "Courier New",Courier,monospace;">RadioField</span> gilt.<br />
<br />
Vorab noch: Alle folgenden Beispiele sind mit WTForms 2.0.2 unter Python 3.4 getestet. Der Einfachheit wird alles im interaktiven Python-Interpreter auf der Kommandozeile durchgespielt. Das Vorgehen in realen Webanwendungen ist aber identisch.<br />
<br />
Eine einfaches Formular mit einem <span style="font-family: "Courier New",Courier,monospace;">SelectField</span> sieht z.B. so aus:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">>>> from wtforms import Form, StringField, SelectField<br />>>> class TestForm(Form):<br />... name = StringField('name')<br />... gender = SelectField('gender',</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> choices=[('m', 'male'),</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> ('f', 'female')])</span><br />
<span style="font-family: "Courier New",Courier,monospace;">>>> </span><br />
<br />
Zuerst werden die benötigten Klassen importiert, dann wird eine eigene Formularklasse Namens <span style="font-family: "Courier New",Courier,monospace;">TestForm</span> definiert, welche die zwei Felder <span style="font-family: "Courier New",Courier,monospace;">name</span> und <span style="font-family: "Courier New",Courier,monospace;">gender</span> hat. <span style="font-family: "Courier New",Courier,monospace;">gender</span> ist dabei ein Auswahlfeld.<br />
<br />
Die Auswahlmöglichkeiten werden über das <span style="font-family: "Courier New",Courier,monospace;">choices</span>-Attribute festgelegt. Diesem wird eine Liste von Tupeln zuwiesen, wobei das erste Element der <span style="font-family: "Courier New",Courier,monospace;">value</span> der Option ist und das zweite Element der angezeigte Text.<br />
<br />
Erzeugt man nun eine neue Instanz der Klasse<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">>>> my_form = TestForm()</span><br />
<br />
kann man sich das HTML-Markup, welche das Formular erzeugt, auch direkt auf der Kommandozeile anzeigen lassen, z.B. für <span style="font-family: "Courier New",Courier,monospace;">gender</span>:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">>>> str(my_form.gender)<br />'<select id="gender" name="gender"><option value="m">male</option><option value="f">female</option></select>'</span><br />
<span style="font-family: "Courier New",Courier,monospace;">>>></span><br />
<br />
Jetzt gibt es aber natürlich auch genug Anwendungsfälle, wo man in einer Webanwendung ein Auswahlfeld dynamisch befüllen will und somit die Werte bei der Definition der Klasse noch nicht kennt.<br />
<br />
Auch das ist natürlich mit WTForms problemlos möglich und zwar indem man das <span style="font-family: "Courier New",Courier,monospace;">choices</span>-Attribut erst später mit Daten füttert. Dies wird im folgenden Beispiel gezeigt:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">>>> class TestForm2(Form):<br />... name = StringField('name')<br />... fav_color = SelectField('favourite color')<br />... <br />>>> form2 = TestForm2()</span><br />
<br />
Wie zu sehen ist, wird für das <span style="font-family: "Courier New",Courier,monospace;">SelectField</span> kein Wert für <span style="font-family: "Courier New",Courier,monospace;">choices</span> übergeben. Folglich kann auch kein HTML generiert werden, stattdessen wird ein Fehler geworfen:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">>>> str(form2.fav_color)<br />Traceback (most recent call last):<br />...<br />TypeError: 'NoneType' object is not iterable<br />>>> </span><br />
<br />
Versorgt man das <span style="font-family: "Courier New",Courier,monospace;">choices</span>-Attribute mit Daten, dann funktioniert alles:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">>>> my_choices = [('w', 'white'),</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> ('r', 'red'),</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> ('b', 'blue')]<br />>>> form2.fav_color.choices = my_choices<br />>>> str(form2.fav_color)<br />'<select id="fav_color" name="fav_color"><option value="w">white</option><option value="r">red</option><option value="b">blue</option></select>'</span><br />
>>> <br />
<br />
In einer realen Anwendung würde die Liste natürlich nicht hard-coded im Quelltext stehen, sondern z.B. aus einer Datenbankabfrage kommen.<br />
<br />
Wie andere <span style="font-family: "Courier New",Courier,monospace;">Field</span>-Klassen auch kennt <span style="font-family: "Courier New",Courier,monospace;">SelectField</span> ebenfalls einen Default-Wert, was sich im HTML-Formular dann so darstellt, dass diese Option vorausgewählt ist.<br />
<br />
Wollte man z.B. im ersten Beispiel <span style="font-family: "Courier New",Courier,monospace;">female</span> als Default-Wert haben, dann müsste die Zeile im Code so geändert werden:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">gender = SelectField('gender',</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> choices=[('m', 'male'),</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> ('f', 'female')],</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> default='f')</span><br />
<br />
Will man beim 2. Beispiel die Farbe weiß als Default-Wert setzen, dann muss man das <span style="font-family: "Courier New",Courier,monospace;">default</span>-Attribute ebenfalls mit einem Wert versorgen:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">>>> form2.fav_color.default='w'<br />>>> str(form2.fav_color)<br />'<select id="fav_color" name="fav_color"><option value="w">white</option><option value="r">red</option><option value="b">blue</option></select>'<br />>>> form2.fav_color.default<br />'w'<br />>>></span><br />
<br />
Das alleine reicht aber nicht, wie im obigen Code zu sehen ist, hat sich am generierten HTML aber nichts geändert, obwohl der Default-Wert korrekt gesetzt ist.<br />
<br />
Damit das HTML wie gewünscht ist, muss ein weiterer Schritt durchgeführt werden, und zwar der Aufruf der <span style="font-family: "Courier New",Courier,monospace;">process</span>-Methode der Klasse: :<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">>>> form2.process()<br />>>> str(form2.fav_color)<br />'<select id="fav_color" name="fav_color"><option selected value="w">white</option><option value="r">red</option><option value="b">blue</option></select>'<br />>>></span><br />
<br />
So ist auch der Default-Wert korrekt gesetzt.</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-1633156062211850702015-07-26T21:10:00.000+02:002015-07-26T21:10:29.051+02:00Python: Tabellen in ReportLab<div dir="ltr" style="text-align: left;" trbidi="on">
<a href="http://www.reportlab.com/opensource/" target="_blank">ReportLab</a>
ist die wohl leistungsfähigste Bibliothek für Python, wenn es um das
Erzeugen von PDF-Dateien geht. Von High-Level Zugriffen via Platypus
(=Page Layout and Typography Using Scripts) bis zu Low-Level mit
direktem Zugriff auf den Canvas bietet ReportLab alles.<br />
<br />
Tabellen unterstützt Platypus mit Hilfe der <span style="font-family: "Courier New",Courier,monospace;">Table</span>-Klasse (welche ein Flowable darstellt) direkt und die Verwendung ist auch erst Mal nicht weiter schwierig.<br />
<br />
Es soll ein einfaches PDF-Dokument mit Hilfe von ReportLab gebaut werden. Das Dokument besteht aus einem Absatz Text, einer Tabelle und einem Absatz Text nach der Tabelle.<br />
<br />
Der Code dafür sieht im einfachsten Fall so aus:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">from reportlab.platypus import Paragraph, Table, SimpleDocTemplate<br />from reportlab.lib.styles import getSampleStyleSheet<br />from reportlab.lib.pagesizes import A4</span><br />
<span style="font-family: "Courier New",Courier,monospace;"><br />my_data = [['foo', 'bar', 'spam'],<br /> ['egg', 123, 'python'],<br /> ['a bit longer', 666, '9B']]<br /> <br />styles = getSampleStyleSheet()<br />doc = SimpleDocTemplate('table_test.pdf', pagesize=A4)<br />story = []<br />story.append(Paragraph('Text vor Tabelle', styles['Normal']))<br />story.append(Table(my_data))<br />story.append(Paragraph('Text nach Tabelle', styles['Normal']))<br />doc.build(story)</span><br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">my_data</span> enthält die Daten für die Tabelle, der Rest ist weitestgehend selbsterklärend.<br />
<br />
Nur das das Dokument nicht gerade "hübsch". Das Ergebnis ist im folgenden Screenshot zu sehen:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjt_wy6zBHEpCTJ38kCw4ypDP2gzAB_iwQmZKMDNlOEViwy2HpTLBvMlkuWqVpvLulGQkRHTK-n1AHKJglJ5h-LD8Ow4zoKLbxeX2G4gpcl2IZJctQirBurXIGEqXTXYOgqoJHDbDa_KwA/s1600/rl_tabelle_1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="126" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjt_wy6zBHEpCTJ38kCw4ypDP2gzAB_iwQmZKMDNlOEViwy2HpTLBvMlkuWqVpvLulGQkRHTK-n1AHKJglJ5h-LD8Ow4zoKLbxeX2G4gpcl2IZJctQirBurXIGEqXTXYOgqoJHDbDa_KwA/s320/rl_tabelle_1.png" width="320" /></a></div>
Standardmäßig werden für die Tabelle keine Rahmen gezeichnet und die Tabelle ist horizontal auf der Seite zentriert. Außerdem ist der Text sehr nah an der Tabelle.<br />
<br />
Das ist im folgenden Code korrigiert:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">from reportlab.platypus import Paragraph, Table, TableStyle, SimpleDocTemplate<br />from reportlab.lib.styles import getSampleStyleSheet<br />from reportlab.lib.pagesizes import A4<br />from reportlab.lib import colors<br /><br />my_data = [['foo', 'bar', 'spam'],<br /> ['egg', 123, 'python'],<br /> ['a bit longer', 666, '9B']]<br /> <br />styles = getSampleStyleSheet()<br />doc = SimpleDocTemplate('table_test.pdf', pagesize=A4)<br />story = []<br />story.append(Paragraph('Text vor Tabelle', styles['Normal']))<br />t = Table(my_data)<br />t.hAlign = 'LEFT'<br />t.spaceBefore = 10<br />t.spaceAfter = 10<br />t.setStyle(TableStyle(</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> [('BOX', (0,0), (-1,-1), 0.5, colors.black),</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> ('INNERGRID', (0,0), (-1,-1), 0.5, colors.black)]))<br />story.append(t)<br />story.append(Paragraph('Text nach Tabelle', styles['Normal']))<br />doc.build(story)</span><br />
<br />
Das ganze liefert als Ergebnis folgendes:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPn44JMyODrAd8Z00jzqRS7oikg0eRN6fwUWATORuBomHMcb3XQBjFAsyT96ps0WqXCvy6OjtsYU7jyv327fF-O_4iXCkSlQB8ALqGtUGBEopTWSeePv8gor3T8TtbNod7PQD_W9hVQhc/s1600/rl_tabelle_2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="164" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPn44JMyODrAd8Z00jzqRS7oikg0eRN6fwUWATORuBomHMcb3XQBjFAsyT96ps0WqXCvy6OjtsYU7jyv327fF-O_4iXCkSlQB8ALqGtUGBEopTWSeePv8gor3T8TtbNod7PQD_W9hVQhc/s320/rl_tabelle_2.png" width="320" /></a></div>
Über das Attribute <span style="font-family: "Courier New",Courier,monospace;">hAlign</span> wird die Tabelle die Ausrichtung der Tabelle bestimmt, der Wert <span style="font-family: "Courier New",Courier,monospace;">LEFT</span> richtet sie linksbündig aus. Mit den Attributen <span style="font-family: "Courier New",Courier,monospace;">spaceBefore</span> und <span style="font-family: "Courier New",Courier,monospace;">SpaceAfter</span> wird zusätzlicher Leerraum (hier: 10 px) eingefügt.<br />
<br />
Die Tabelle an sich wird über die <span style="font-family: "Courier New",Courier,monospace;">setStyle</span>-Methode formatiert, welche als Wert die <span style="font-family: "Courier New",Courier,monospace;">TableStyle</span>-Klasse übergeben bekommt. Diese erlaubt einen detaillierten Eingriff in die Formatierung der Tabelle.<br />
<br />
Die Werte für <span style="font-family: "Courier New",Courier,monospace;">TableStyle</span> werden als Liste von Tupeln übergeben. Im obigen Beispiel wird mit <span style="font-family: "Courier New",Courier,monospace;">BOX</span> ein Rahmen um die Tabelle gezeichnet und <span style="font-family: "Courier New",Courier,monospace;">INNERGRID</span> zeichnet den inneren Rahmen, beide in schwarz.<br />
Die Tupel <span style="font-family: "Courier New",Courier,monospace;">(0, 0), (-1, -1)</span> definieren den Geltungsbereich der Formatierung. <span style="font-family: "Courier New",Courier,monospace;">(0,0)</span> ist dabei die Zelle links oben, <span style="font-family: "Courier New",Courier,monospace;">(-1, -1)</span> die Zelle rechts unten - somit ist also die ganze Tabelle abgedeckt.<br />
<br />
Möchte man z.B. das innere Gitter mur für einen Teil der Zellen zeichnen und in der mittleren Zelle soll die Schrift rot sein, so ist die Codezeile wie folgt anzupassen:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">t.setStyle(TableStyle(</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> [('BOX', (0,0), (-1,-1), 0.5, colors.black</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> ('INNERGRID', (0,0), (1,1), 0.5, colors.black),</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> ('TEXTCOLOR', (1,1), (1,1), colors.red)]))</span><br />
<br />
Und das Ergebnis sieht dann so aus:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDq7g0EEgjEUnlllGtGdmnpMI9w_RU8jvqgMmGaNpaMviVdelJZpTkt9c3zGNfP5qSydSnrvr0y3R3kBZscLMemlivqLC3SV60F-QkYFE6hYTOYCQCV5ROTp9DrOg5-qeul-GM-O1oGYE/s1600/rl_tabelle_3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDq7g0EEgjEUnlllGtGdmnpMI9w_RU8jvqgMmGaNpaMviVdelJZpTkt9c3zGNfP5qSydSnrvr0y3R3kBZscLMemlivqLC3SV60F-QkYFE6hYTOYCQCV5ROTp9DrOg5-qeul-GM-O1oGYE/s1600/rl_tabelle_3.png" /></a></div>
<br />
<span style="font-family: "Courier New",Courier,monospace;">TableStyle</span> bietet eine Vielzahl weiterer Möglichkeiten, dass Aussehen der Tabelle zu beeinflussen. Eine komplette Übersicht ist im Kapitel 7 der ReportLab Dokumentation zu finden.<br />
<br />
So ist es z.B. auch möglich, Zellen innerhalb einer Spalte oder Zeile zusammenzufassen. Möchte man z.B. die drei Zellen der ersten Zeile zusammenfassen, so ist der obiger Code wie folgt zu ändern:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">...</span><br />
<span style="font-family: "Courier New",Courier,monospace;">my_data = [['Überschrift', '', ''],<br /> ['egg', 123, 'python'],<br /> ['a bit longer', 666, '9B']]</span><br />
<span style="font-family: "Courier New",Courier,monospace;">....</span><br />
<span style="font-family: "Courier New",Courier,monospace;">t.setStyle(TableStyle(</span><br />
<span style="font-family: "Courier New",Courier,monospace;"> [('BOX', (0,0), (-1,-1), 0.5, colors.black),<br /> ('INNERGRID', (0,0), (-1,-1), 0.5, colors.black),<br /> ('SPAN', (0, 0), (-1, 0))]))</span><br />
<br />
Das sieht im Ergebnis so aus:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEggOKe_SQOEjyk2tgf6zfyK8t_5AXX9tOpZ4wvknYsbGri59CHzY0yTigu7wyrxIAkxqL4IhVZ93Msm69KAEm9iIKewTHJiPr5wnciNKI4eNQcebf1Va69h7qg5uDNlMm5pUVWg3esqd_s/s1600/rl_tabelle_4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="133" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEggOKe_SQOEjyk2tgf6zfyK8t_5AXX9tOpZ4wvknYsbGri59CHzY0yTigu7wyrxIAkxqL4IhVZ93Msm69KAEm9iIKewTHJiPr5wnciNKI4eNQcebf1Va69h7qg5uDNlMm5pUVWg3esqd_s/s320/rl_tabelle_4.png" width="320" /></a></div>
Die bisher generierten Tabellen ermitteln die Breite der Spalten und Höhe der Zeilen automatisch, aber auch das kann natürlich beeinflusst werden. Möchte man z.B. eine fixe Spaltenbreite von 100 px haben, so ist im Code die Zeile wie folgt zu ändern:<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">t = Table(my_data, colWidths=100)</span><br />
<br />
Weitere Möglichkeit sind ebenfalls im Kapitel 7 der Dokumentation zu finden.<br />
<br />
In den obigen Beispielen wird der <span style="font-family: "Courier New",Courier,monospace;">Table</span>-Klasse einfach eine Liste von Strings bzw. Integern übergeben. Man kann aber auch eine Liste von Flowables übergeben, was sich anbieten, wenn man z.B. längeren Text oder Grafiken in der Tabelle unterbringen möchte.<br />
<br />
Des Weiteren gibt es noch die Klasse <span style="font-family: "Courier New",Courier,monospace;">LongTable</span>, welche anstellen von <span style="font-family: "Courier New",Courier,monospace;">Table</span> verwendet werden kann. Das Ergebnis ist laut Dokumentation das gleiche, allerdings soll <span style="font-family: "Courier New",Courier,monospace;">LongTable</span> bei sehr langen Tabelle etwas performanter sein.<br />
<br />
Wie hier gezeigt wird, ist das Generieren einer Tabelle in einem mit Platypus gebauten PDF-Dokuments in ReportLab nicht weiter schwierig. Wie bei ReportLab üblich kann man auf Wunsch aber auch sehr detailliert in die Gestaltung und das Layout der Tabelle eingreifen.<br />
<br />
Alle Beispiele aus diesem Blogbeitrag sind unter Python 3.4 und ReportLab 3.0 erstellt.</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-29672645710779887642015-07-19T20:09:00.003+02:002015-07-19T20:09:29.192+02:00Grafikfehler auf Lenovo T450 und die passende Abhilfe<div dir="ltr" style="text-align: left;" trbidi="on">
Seit ein paar Monaten nutze ich beruflich ein Lenovo T450 Thinkpad in der Varianten mit Intel Grafik (und ohne zusätzlich Nvidia Grafik).<br />
<br />
Ubuntu 14.04 läuft darunter auch ohne Probleme, nur einen kleinen "Schönheitsfehler" gab es: nach dem Login war manchmal die Beschriftung der Symbole / Dateien auf dem Desktop unvollständig. Unvollständig heißt, dass einzelne oder ganz selten auch alle Buchstaben fehlten. Das kam bei ca. 50% der Logins vor. "Meine" Abhilfe war: abmelden, neu anmelden und alles war ok - zumindest in 95% der Fälle. Selten musst ich mich auch nochmal abmelden und einloggen, damit alles war, wie es sein sollte.<br />
<br />
Auch mit dem oben beschriebenen Grafikfehler war das System uneingeschränkt benutzbar. Das Fehlen der Buchstaben trat z.B. auch im Menü der Systemeinstellung und in der Tab-Beschriftung des Firefox auf. Der Inhalt der Programmfenster an sich (z.B. die vom Firefox dargestellte Seite, die Liste der Lieder in Quod Libet etc.) war aber immer korrekt und fehlerlos.<br />
<br />
Der Grafikfehler war zwar etwas nervig, aber die "Lösung" "Ausloggen, Einloggen" war so schnell und funktionierend, dass ich nie weiter für den Grund des Problems gesucht habe.<br />
<br />
Anfang Juli hatte ich dann im <a href="https://planet.ubuntuusers.de/" target="_blank">Planet</a> von ubuntuusers.de einen Beitrag des <a href="http://www.intux.de/2015/07/das-thinkpad-e550-und-die-grafik/" target="_blank">Intux-Blogs</a> gelesen, in dem genau das gleiche Problem und eine passende Lösung für ein Thinkpad E550 beschrieben wurde.<br />
<br />
Und das ist auch genau die Lösung, die beim Thinkpad T450 mit Intel GraKa funktioniert. Das ganze geht unter Ubuntu 14.04 in vier Schritten:<br />
<ul>
<li>die Verzeichnis <b>/etc/X11/xorg.conf.d/</b> anlegen
</li>
<li>darin die Datei <b>20-intel.conf</b> anlegen
</li>
<li>die Datei mit folgendem Inhalt befüllen<br />
<pre><code>Section "Device"
Identifier "Intel Graphics"
Driver "intel"
Option "AccelMethod" "uxa"
EndSection</code></pre>
</li>
<li>abmelden und wieder anmelden
</li>
</ul>
Zumindest bei mir sind seitdem die oben beschrieben Grafikfehler nicht mehr aufgetreten.<br />
<br />
An diese Stelle nochmal Danke an den Intux-Blog und zum Abschluss nochmal der volle Link auf den Blogartikel: <a href="http://www.intux.de/2015/07/das-thinkpad-e550-und-die-grafik/" target="_blank">http://www.intux.de/2015/07/das-thinkpad-e550-und-die-grafik/ </a></div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-74816504245143125882015-06-28T19:15:00.000+02:002015-06-28T19:15:19.152+02:00Mair 1 Festival 2015, 2. Tag - Expire, Nasty, Suicidal Tendencies, Terror<div dir="ltr" style="text-align: left;" trbidi="on">
Am, 27.6.2015, dem 2. Tag des <a href="http://www.mair1-festival.de/" target="_blank">Mair 1 Festivals</a> 2015, ging es für mich so früh wie noch nie zum Mair 1, nämlich bereits um 14:30 Uhr. Der Grund: um 14.50 Uhr war der Auftritt von Expire - und den wollte ich nicht verpassen.<br />
<br />
Als ich ankam, waren auf der großen Bühne gerade (die mir bis dato völlig unbekannten) <a href="https://www.reverbnation.com/burymyregrets" target="_blank">Bury My Regrets</a> zugange. Das ist eine deutsche Hardcore Band, welche ein solides, gut anzusehende und -hörendes Konzert ablieferten.<br />
<br />
Jetzt kam <a href="https://de.wikipedia.org/wiki/Expire" target="_blank">Expire</a> auf die kleinere Bühne. Die CDs von denen gefallen mir sehr gut, also war da schon eine gewisse Erwartungshaltung für das Konzert vorhanden.<br />
Und die wurde nicht enttäuscht. Live hatte die Band genau so viel Power wie auf CD und mehr Energie als ein Dampfhammer mit Überdruck. Aufgrund der (für ein Festival) relativ frühen Stunde waren zwar noch nicht so viel Leute vor Ort, aber das vorhandene Publikum ging trotzdem voll bei dem sehr guter Auftritt von Expire mit.<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEie0gMH-nnadJ9bR43jFOQLEZr4TksTPAvKvo9wGg-WBkXm9G1DLkmxeioMlpnUJ6yTuycy6w_1FMOFPC7HzhoO5tmT7SKj4vinVRmSQ3wcbtDHZ7PGsp6lVySLpd8B11P0kusao0nzARQ/s1600/expire.jpg" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="180" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEie0gMH-nnadJ9bR43jFOQLEZr4TksTPAvKvo9wGg-WBkXm9G1DLkmxeioMlpnUJ6yTuycy6w_1FMOFPC7HzhoO5tmT7SKj4vinVRmSQ3wcbtDHZ7PGsp6lVySLpd8B11P0kusao0nzARQ/s320/expire.jpg" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Expire live auf der Bühne</td></tr>
</tbody></table>
Danach ging es dann erst Mal wieder nach Hause, weil mich dann erst so richtig wieder die Bands interessierten, die am Abend spielten.<br /><br />
Gegen 19.50 Uhr war ich dann auch wieder auf dem Festivalgelände. Zum dem Zeitpunkt waren noch Caliban auf der großen Bühne. Davon habe ich allerdings nichts gesehen, weil gerade am Eingang zum Festivalgelände eine Motocross Freestyle Show begann. Und so fliegende Motorräder aus der Nähe zu betrachten ist schon recht beeindruckend.<br />
<br />
Um 20 Uhr starteten dann <a href="https://de.wikipedia.org/wiki/Nasty_%28Band%29" target="_blank">Nasty</a> ihre Konzert. Die kannte ich bisher nur vom Namen. Die Band aus dem deutschsprachigen Teil von Belgien lieferten eine gute Show mit viel Interaktion mit dem Publikum ab. Das ging auch ziemlich mit, auch wenn ich so das Gefühl hatte, dass der entscheidende Funk nicht überspringen wollte.<br />
<br />
Um 20.50 Uhr war es dann so weit - die <a href="https://de.wikipedia.org/wiki/Suicidal_Tendencies" target="_blank">Suicidal Tendencies</a> betraten die große Bühne. Lebende Legenden, Veteranen des Hardcore, Inkarnation des Skatepunk und ich behaupte mal so was wie die heimlichen Headliner des Mair 1 2015.<br />Und: die Show war super gut, ohne wenn und aber. Mike Muir flitze eine Stunde lang wie ein angestochenes Schwein über die Bühne und auch der Rest der Band war voll dabei. Suicidal Style halt.<br />
Zum Song "Possessed to Skate" holte man sich dann noch mal schnell ca. 50 Leute aus dem Publikum auf die Bühne, um dort abzufeiern.<br />Insgesamt lieferten die Suicidal Tendencies einen tollen, kurzweiligen Auftritt.<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgssEar6KAQ4wcJqgBMg7nc402jLoDnKE1lWqYMzHmcblEV85NyhtchA_Iw4xHm2uXdd-f4BVf-05Ckxw6NjUiBVfuRS5DfUXffyyIT0O9ki4k-FM9mljGu_4Dvq_aDiTOHyVfPclINLqE/s1600/st.jpg" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="180" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgssEar6KAQ4wcJqgBMg7nc402jLoDnKE1lWqYMzHmcblEV85NyhtchA_Iw4xHm2uXdd-f4BVf-05Ckxw6NjUiBVfuRS5DfUXffyyIT0O9ki4k-FM9mljGu_4Dvq_aDiTOHyVfPclINLqE/s320/st.jpg" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">ST auf der Bühne des Mair 1</td></tr>
</tbody></table>
Auf der kleineren Bühne folgten dann <a href="http://www.terrorlahc.com/" target="_blank">Terror</a>. Die Band kannte ich bis dahin auch nur vom Namen und hatte aufgrund dessen eher Metalcore vermutet. Ist es aber nicht - <a href="https://de.wikipedia.org/wiki/Terror_%28Band%29" target="_blank">Terror</a> spielt Hardcore-Punk mit leicht metall-lastigem Sound.<br />
Aufgrund einer Rückverletzung des Sängers spielte Terror wohl in leicht veränderter Besetzung, was der Sache aber keine Abbruch tat. Es war eine energie-geladene Hardcore Show, hat mir gut gefallen. Vielleicht sollte ich mir doch mal eine CD von denen kaufen.<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0DNsOy1GPvcsuipVeT6r6Z3MB2RzEJFn5_aSqtZMbtTEsay1Q9YE_amsa8fEZrCP74zwfge374UBnAlSVYm7c6SGlU781VASypRM3HBvXWBWTKorvhU3Jp8039Oz-sFzsejJy_kbMnqQ/s1600/terror.jpg" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="180" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0DNsOy1GPvcsuipVeT6r6Z3MB2RzEJFn5_aSqtZMbtTEsay1Q9YE_amsa8fEZrCP74zwfge374UBnAlSVYm7c6SGlU781VASypRM3HBvXWBWTKorvhU3Jp8039Oz-sFzsejJy_kbMnqQ/s320/terror.jpg" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Terror live auf dem Mair 1</td></tr>
</tbody></table>
Den Abschluss des Mair 1 bildeten <a href="https://de.wikipedia.org/wiki/Millencolin" target="_blank">Millencolin</a>. Und obwohl ich ja gerne Punkrock - auch von schwedischen Bands - höre, konnte ich mit Millencolin noch nie was anfangen. Und, was soll ich sagen: das war auch hier live auf dem Mair 1 nicht anders. Drei Songs angeschaut und nach Hause gegangen. Das ist nichts für mich.<br />
<br />
Das Mair 1 war auf jeden Fall auch 2015 ein gutes Festival, was einen Besuch wert war. Ich freue mich schon auf das Mair 1 2016!</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-54336631409349453872015-06-27T10:54:00.000+02:002015-06-27T10:54:16.534+02:00Mair 1 Festival 2015, 1. Tag - Strung Out, Agnostic Front, Breakdown of Sanity<div dir="ltr" style="text-align: left;" trbidi="on">
Am 26. und 27.6.2015 war es wieder so weit: das <a href="http://www.mair1-festival.de/home/intro" target="_blank">Mair 1 Festival</a> 2015 findet statt, an der bekannten Location in der Nähe von Montabaur.<br />
<br />
Das Lineup ist wie in den Jahren zuvor schon ziemlich gut, was die Musikrichtungen Hardcore, Metalcore und Punk betrifft, auch wenn mich dieses Jahr die Bands nicht sooo ansprechen (im Vergleich zu einigen anderen Lineups aus den Vorjahren).<br />
<br />
Nichts desto trotz ging es auch dieses Jahr wieder zum Mair 1 - ist ja in der Nähe, die Atmosphäre ist immer entspannt und die Organisation sehr gut.<br />
<br />
Als ich am 1. Festivaltag ankam, spielten gerade die <a href="http://www.emilbulls.de/" target="_blank">Emil Bulls</a> auf der großen Bühne ihre letzten beiden Stücke. Klang ganz ok, mehr kann ich dazu nicht sagen.<br />
<br />
Danach kamen <a href="https://de.wikipedia.org/wiki/Breakdown_of_Sanity" target="_blank">Breakdown of Sanity</a> auf die kleinere Bühne. Diese Band war mit bis dato völlig unbekannt, aber nach den ersten paar Takten war klar: das ist wohl Metalcore mit Hang zum düsteren - also musikalisch nicht ganz so mein Fall.<br />
Die Show war gut und kam auch beim Publikum sehr gut an, aber leider war der Sound bei der ersten paar Liedern schlecht - außer der wummernden Double-Bass (der in allen Liedern ausgiebig zum Einsatz kommt) und dem Gesang war nicht viel zu hören. Zum Glück wurde das nach drei oder vier Liedern besser und der Sound war so, wie er sein sollte.<br />
<br />
Danach waren dann die Bands an der Reihe, weswegen ich heute eigentlich zum Mair 1 gefahren war: Agnostic Front und Strung Out.<br />
<br />
Pünktlich betraten <a href="https://de.wikipedia.org/wiki/Agnostic_Front" target="_blank">Agnostic Front</a> um 20.50 Uhr die große Bühne. Das Urgestein der New York Hardcore fackelte nicht lange und gab ab dem ersten Song Vollgas, so dass der Funke auch direkt auf's Publikum übersprang.<br />Und wer schon, wie Agnostic Front, seit übr 30 Jahren im Geschäft ist, der weiß natürlich auch, wie man eine gute Hardcore Show aufzieht und spielt. Und so war dieses Konzert eine kurzweilig, unterhaltsame Stunde. Der letzte Song war nochmal ein Highlight, da Agnostic Front den Ramons Song "Blitzkrieg Bop" brachten. Sehr schön.<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMHpaF0bRxp_R0pdRkKhS52GB3Xc66A6Fd3Szk2gTI0NCilHjV8d5T5XRfzgfmY_DwxPixS4qCDHI8-grC-bY0r-qDTlAdX06Gq8tmgkaPpy26Wu5StuYswkV6w5edMAE4uuSafZhkJeg/s1600/af.jpg" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="180" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMHpaF0bRxp_R0pdRkKhS52GB3Xc66A6Fd3Szk2gTI0NCilHjV8d5T5XRfzgfmY_DwxPixS4qCDHI8-grC-bY0r-qDTlAdX06Gq8tmgkaPpy26Wu5StuYswkV6w5edMAE4uuSafZhkJeg/s320/af.jpg" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Agonstic Front auf der Bühne</td></tr>
</tbody></table>
Dann waren <a href="http://www.strungout.com/" target="_blank">Strung Out</a> auf der kleineren Bühne an der Reihe. Von denen habe ich nur eine CD, das bereits 1992 erschienene Album "<a href="https://en.wikipedia.org/wiki/Suburban_Teenage_Wasteland_Blues" target="_blank">Suburban Teenage Wasteland Blues</a>". Und da Strung Out in deren Songs doch trotz kalifornischem Punk Rock einen Hang zum dunklen und melancholischen hat war ich zugegebener Maßen etwas skeptisch, wie die Show werden würde.<br />
<br />
Um es direkt zu sagen: die Skepsis war völlig unbegründet. Strung Out spielten ein mitreisendes, Enerigie-gelandenes Konzert vom ersten bis zum letzten Takt. Sehr guter Sound und auch der von der Band oft eingesetzte mehrstimmige Gesang kam live klar und druckvoll rüber.<br />Unter Strich muss ich dann auch sagen: Strung Out war mit Sicherheit einer der besten Auftritte, die ich bisher auf dem Mair 1 gesehen habe. Super gut.<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEW55dFk6w7RCgKmYbeOt1StWOT9ZSA2VxZLl-4JYJqZO_dbMbEpdl7CUsYvNfZtZcB4oHWyYHYx4o4UHqa9FK2w-4Y-vU5Vr3ZqtDjPYEO0H5t_0zLkJMOC_9jHoFaSDIL5A_CWFOa7E/s1600/so.jpg" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="180" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEW55dFk6w7RCgKmYbeOt1StWOT9ZSA2VxZLl-4JYJqZO_dbMbEpdl7CUsYvNfZtZcB4oHWyYHYx4o4UHqa9FK2w-4Y-vU5Vr3ZqtDjPYEO0H5t_0zLkJMOC_9jHoFaSDIL5A_CWFOa7E/s320/so.jpg" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Strung Out auf der Bühne</td></tr>
</tbody></table>
Den Abschluss machten dann die <a href="https://de.wikipedia.org/wiki/Guano_Apes" target="_blank">Guana Apes</a>. Musikalisch so wie so nicht mein Fall und ehrlich gesagt fand ich die Band aus der Sparte "Rock mit Hang zum Mainstream" auf dem Mair 1 auch ein wenig deplatziert. Zwei Songs habe ich mir angeschaut, bevor ich gefahren bin. Publikum war aber definitiv noch genug da, also an Popularität mangelt es den Guana Apes scheinbar nicht.<br />
<br />
Auch wenn ich ja "nur" drei Bands ganz gesehen hatte war's ein guter 1. Tag auf dem Mair 1 2015. Schon alleine wegen des tollen Konzerts von Strung Out.</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-52351328201535481972014-10-30T20:33:00.000+01:002014-10-30T20:33:06.605+01:00Erst Upgrade, dann Neuinstallation - von Ubuntu 12.04 nach 14.04<div dir="ltr" style="text-align: left;" trbidi="on">
Letztes Wochenende war es soweit: das Update von Ubuntu 12.04 auf 14.04 stand auf dem Laptop an, den ich beruflich nutze (ein Lenovo T410). Die Prozedur ist ja nicht weiter schwierig und z.B. auch im <a href="http://wiki.ubuntuusers.de/Upgrade_Precise_auf_Trusty" target="_blank">Wiki von ubuntuusers.de </a>beschrieben.<br />
<br />
Dank schnellem Internetzugang ging das ganze auch recht zügig von statten und das Upgrade lief problemlos und fehlerfrei durch.<br />
<br />
Nach dem Reboot lief auch alles rund, also alles gut. Dachte ich zumindest. Beim ersten Starten von Empathy unter Trusty war die Darstellung der Chats auf die Standardansicht zurückgesetzt, besser gefällt mir aber eine andere die Darstellung.<br />
<br />
Kein Problem, kann man ja umstellen. Nur: im Empathy-Fenster fehlte das komplette Menü <i>"Empathy"</i>, welches auch den Menüpunkt <i>"Einstellungen"</i> enthält. Da ich auf meinem privaten Laptop aber schon länger Ubuntu 14.04 installiert habe war ich mir sehr sicher, dass das Menü normalerweise da ist. Hm...<br />
<br />
Nun, es gibt schlimmeres, von daher erst Mal weiter gemacht. Aber dieses Phänomen wollte dann später doch untersucht werden. Also mal testweise den Editor <i>"gedit" </i>gestartet. Alle Menüpunkte vorhanden, aber im Menü <i>"Bearbeiten"</i> fehlte auch hier der Punkt <i>"Einstellungen"</i>. Das gleiche auch in Rhythmbox - der Menüpunkt zum erreichen der Einstellungen fehlt ebenfalls.<br />
<br />
Da mir so gar nicht danach war, diesen Fehler in den Untiefen des Systems zu suchen, habe ich kurzerhand die Installations-DVD heruntergeladen, die wenigen Daten, die noch nicht gesichert waren, gesichert und dann Ubuntu 14.04 neu installiert.<br />
<br />
Das lief - wie nicht anders zu erwarten - problemlos und das neue System war jetzt wirklich einsatzbereit. Inklusive der Einstellungs-Menüs, die sind jetzt wieder vorhanden.<br />
<br />
Die Neuinstallation ist für mich übrigens nicht weiter tragisch, da ich auf dem beruflich genutzten Laptop quasi ein "Vanilla-Ubuntu" (=keine Anpassungen) nutze. Ich habe zusätzlich nur ein paar Pakete aus den offiziellen Quellen (Apache, MySQL, CouchDB) und ein paar Python-Module installiert, sonst nichts. Und die Dateien im persönlichen Ordner (Musik, Code, Bilder, Dokumente für die Arbeit) werden so wie so regelmäßig gesichert.<br />
<br />
Wer das gleiche Problem auch schon hatten und eine Lösung ohne Neuinstallation kennt, der kann dies gerne als Kommentar zu diesem Blogbeitrag posten.<br />
<br /></div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-54499684748105645972014-10-18T21:42:00.000+02:002014-10-19T21:23:51.828+02:00Python: ListFlowables in ReportLab<div dir="ltr" style="text-align: left;" trbidi="on">
<a href="http://www.reportlab.com/opensource/" target="_blank">ReportLab</a> ist die wohl leistungsfähigste Bibliothek für Python, wenn es um das Erzeugen von PDF-Dateien geht. Von High-Level Zugriffen via Platypus (=Page Layout and Typography Using Scripts) bis zu Low-Level mit direktem Zugriff auf den Canvas bietet ReportLab alles.<br />
<br />
Allerdings hat es doch bis zu ReportLab Version 2.6 (erschienen im Oktober 2012) gedauert, bis das direkte Erzeugen von nummerierten und unnummerierten Listen möglich war. Dies war zwar vorher prinzipiell auch möglich, aber nur über Umwege (z.B. bei unnummerierten Listen durch das manuelle hinzufügen eines Bullet Points vor dem Text).<br />
<br />
Mit dem seit ReportLab 2.6 vorhandenen Flowable namens "ListFlowable" geht das aber wesentlich einfacher.<br />
<br />
Neulich, bei der Überarbeitung eines meiner älteren Python-Skripte, welches auch ReportLab einsetzt, habe ich auch auf diese Flowable umgestellt.<br />
<br />
Da die Doku von ReportLab zu diesem Thema - leider - etwas dünn ist, habe ich hier in diesem Blogebeitrag mal ein paar Sachen dazu aufgeschrieben.<br />
<br />
Grundsätzlich ist die Sache recht simpel. Nach dem Import der benötigten Module erstellt man ein ListFlowable, welches die Punkte der Liste als ListItems enthält. Der Quelltext für eine Liste mit drei Punkten sieht so aus:<br />
<br />
<div class="highlight" style="background: #f8f8f8;">
<pre style="line-height: 125%;"><span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">reportlab.lib.styles</span> <span style="color: green; font-weight: bold;">import</span> getSampleStyleSheet
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">reportlab.lib.pagesizes</span> <span style="color: green; font-weight: bold;">import</span> A4
<span style="color: green; font-weight: bold;">from</span> <span style="color: blue; font-weight: bold;">reportlab.platypus</span> <span style="color: green; font-weight: bold;">import</span> Paragraph, SimpleDocTemplate, ListFlowable, \
ListItem
styles <span style="color: #666666;">=</span> getSampleStyleSheet()
style <span style="color: #666666;">=</span> styles[<span style="color: #ba2121;">'Normal'</span>]
story <span style="color: #666666;">=</span> []
t1 <span style="color: #666666;">=</span> ListFlowable([ListItem(Paragraph(<span style="color: #ba2121;">'foo'</span>, style)),
ListItem(Paragraph(<span style="color: #ba2121;">'bar'</span>, style)),
ListItem(Paragraph(<span style="color: #ba2121;">'spamegg'</span>, style))])
story<span style="color: #666666;">.</span>append(t1)
doc <span style="color: #666666;">=</span> SimpleDocTemplate(<span style="color: #ba2121;">'testdoc.pdf'</span>, size<span style="color: #666666;">=</span>A4)
doc<span style="color: #666666;">.</span>build(story)
</pre>
</div>
<br />
So wird eine nummerierte Liste erzeugt, der Standard des ListFlowables, wenn keine weiteren Vorgaben gemacht werden.<br />
<br />
Möchte man eine unnummerierte Liste erzeugen, muss dem ListFlowable das Argument <span style="font-family: "Courier New",Courier,monospace;">bulletType</span> und <span style="font-family: "Courier New",Courier,monospace;">start</span> mitgegeben werden:<br />
<br />
<div class="highlight" style="background: #f8f8f8;">
<pre style="line-height: 125%;">t1 <span style="color: #666666;">=</span> ListFlowable([ListItem(Paragraph(<span style="color: #ba2121;">'foo'</span>, style)),
ListItem(Paragraph(<span style="color: #ba2121;">'bar'</span>, style)),
ListItem(Paragraph(<span style="color: #ba2121;">'spamegg'</span>, style))],
bulletType<span style="color: #666666;">=</span><span style="color: #ba2121;">'bullet'</span>,
start<span style="color: #666666;">=</span><span style="color: #ba2121;">'circle'</span>)
</pre>
</div>
<br />
Möchte man statt Kreisen als Bullet Points z.B. Quadrate haben, dann ersetzt man einfach <span style="font-family: "Courier New",Courier,monospace;">start='circle'</span> durch <span style="font-family: "Courier New",Courier,monospace;">start='square'</span>.<br />
<br />
Wer aus den obigen Beispielen ein PDF generiert wird feststellen, dass sowohl die Zahl als auch die Bullet Points ziemlich groß sind, größer als der Text, formatiert mit dem Beispiel-Stylesheet aus ReportLab. Fügt man noch das Argument <span style="font-family: "Courier New",Courier,monospace;">bulletFontSize</span> hinzu, gefolgt von der gewünschten Schriftgröße (also z.B. <span style="font-family: "Courier New",Courier,monospace;">bulletFontSize=8</span>), dann werden Schrift bzw. Bullet Points in der angegebenen Schriftgröße dargestellt.<br />
Alternativ kann man für unnummerierten Listen auch folgendes angeben: <span style="font-family: "Courier New",Courier,monospace;">start='bulletchar'</span>. Dann wird als Zeichen standardmäßig ein kleiner Punkt (HTML: &middot;) verwendet.<br />
<br />
Bei nummerierten Listen kann aber ReportLab aber nicht nur mit "normalen" Zahlen zählen, sondern auch:<br />
<span style="font-family: "Courier New",Courier,monospace;">bulletType='i'</span> # römische Zahlen<br />
<span style="font-family: "Courier New",Courier,monospace;">bulletType='a'</span> # Zählung: a, b , c, ...<br />
<span style="font-family: "Courier New",Courier,monospace;">bulletType='A'</span> # Zählung: A, B, C, ...<br />
<br />
Mit dem Argument <span style="font-family: "Courier New",Courier,monospace;">start</span> kann man den Beginn der Zählung bei nummerierten Listen den Beginn der Zahlung festlegen. <span style="font-family: "Courier New",Courier,monospace;">bulletType='A', start='10'</span> würde z..B. die Zählung der Liste bei <span style="font-family: "Courier New",Courier,monospace;">J</span> starten lassen.<br />
<br />
Weiterhin werden standardmäßig alle Listen linksbündig ausgerichtet. Möchte man die Listen gegenüber dem Text etwas einrücken, dann muss man die für die Aufzählungszeichen und den zugehörigen Text separat machen, wie z.B.:<br />
<br />
<div class="highlight" style="background: #f8f8f8;">
<pre style="line-height: 125%;">p1 <span style="color: #666666;">=</span> Paragraph(<span style="color: #ba2121;">'etwas Text'</span>, style)
t1 <span style="color: #666666;">=</span> ListFlowable([ListItem(Paragraph(<span style="color: #ba2121;">'foo'</span>, style), leftIndent<span style="color: #666666;">=25</span>),
ListItem(Paragraph(<span style="color: #ba2121;">'bar'</span>, style), leftIndent<span style="color: #666666;">=25</span>),
ListItem(Paragraph(<span style="color: #ba2121;">'spamegg'</span>, style),leftIndent<span style="color: #666666;">=25</span>)],
bulletType<span style="color: #666666;">=</span><span style="color: #ba2121;">'bullet'</span>,
start<span style="color: #666666;">=</span><span style="color: #ba2121;">'bulletchar'</span>,
leftIdent<span style="color: #666666;">=10</span>)
story<span style="color: #666666;">.</span>append(p1)
story<span style="color: #666666;">.</span>append(t1)
story<span style="color: #666666;">.</span>append(p1)
</pre>
</div>
<br />
Und das ListFlowable kann natürlich auch verschachtelt werden, für verschachtelte Liste. Eine Liste mit zwei Ebenen sieht z.B. so aus:<br />
<br />
<div class="highlight" style="background: #f8f8f8;">
<pre style="line-height: 125%;">t1 <span style="color: #666666;">=</span> ListFlowable([ListItem(Paragraph(<span style="color: #ba2121;">'foo'</span>, style)),
ListItem(Paragraph(<span style="color: #ba2121;">'bar'</span>, style)),
ListFlowable([ListItem(Paragraph(<span style="color: #ba2121;">'spam'</span>, style)),
ListItem(Paragraph(<span style="color: #ba2121;">'egg'</span>, style))],
bulletType<span style="color: #666666;">=</span><span style="color: #ba2121;">'a'</span>),
ListItem(Paragraph(<span style="color: #ba2121;">'more'</span>, style))])
</pre>
</div>
<br />
Ein paar weiter Tipps zum ListFlowable und dessen Formatierungsmöglichkeiten gibt es auch in einem Thread bei <a href="http://stackoverflow.com/questions/14996458/how-can-i-make-the-bullet-appear-directly-next-to-the-text-of-an-indented-list-i" target="_blank">stackoverflow</a>.</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-41710881659613646932014-07-29T15:38:00.000+02:002014-07-29T15:38:17.112+02:00Bob Mould "Beauty & Ruin" - Rezension<div dir="ltr" style="text-align: left;" trbidi="on">
Am 3.6.2014 war es mal wieder so weit: <a href="http://de.wikipedia.org/wiki/Bob_Mould" target="_blank">Bob Mould</a>, legendärer Frontmann von Hüsker Dü, hat sein 13. Studioalbum namens "Beauty & Ruin" veröffentlicht.<br />
<br />
Darauf enthalten sind 12 Tracks mit einer Gesamtspielzeit von 36 Minuten. Nach dem letzten, sehr guten Release "<a href="http://noisefloor-net.blogspot.de/2012/10/bob-mould-silver-age-rezension.html" target="_blank">Silverage</a>" aus dem Jahr 2012 ist es spannend zu sehen, ob Bob Mould nochmals so ein fulminantes Album abliefern kann.<br />
<br />
Der erste, spontante Eindruck meinerseits beim 1. Lied: "Hm, klingt ja wie Sugar" (was ja keineswegs schlecht ist). Musikalisch schwankt das ganze Album dann immer zwischen Bob Mould und Sugar, wobei auf dem Album auch zwei, drei ruhiger Stücken enthalten sind.<br />
<br />
Insgesamt ist das alles aber sehr gut zu hören und auch das ganze Album ist in sich sehr "rund". Aber: das Album hinterlässt keine bleibenden Erinnerung. Weder positiv noch negativ. Während bei "Siverage" eine Vielzahl der Lieder im Ohr bleiben, ist dies bei "Beauty & Ruin" nicht so.<br />
<br />
Nichts desto trotz macht jeder neue Hördurchgang Spaß und langweilig wird's auch nicht. Nur die Nachhaltigkeit fehlt. Von daher ein sehr gutes und solides Album von Bob Mould, was absolut hörenswert ist. Aber auch kein echtes Highlight.</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-43193488671335834002014-06-29T20:12:00.002+02:002015-06-27T10:55:59.038+02:00Mair 1 Festival 2014 2. Tag - Risk it!, Comeback Kid, Hatebreed<div dir="ltr" style="text-align: left;" trbidi="on">
28.6.2104, 2. Tag des Mair 1 Festivals 2014. Nachdem der <a href="http://noisefloor-net.blogspot.de/2014/06/mair-1-festival-2014-1-tag-walls-of.html" target="_blank">1. Tag</a> schon ziemlich gut war, waren die Gründe für den Besuch am 2. Tag die anstehenden Auftritte der Bands Risk it!, Comeback Kid und Hatebreed.<br />
<br />
Leider war an diesem Tag das Wetter schlechter, es gab mehr Regenschauern. Immerhin war es trocken, als ich gegen 20.30 Uhr auf dem Festivalgelände ankam. Auf der Hauptbühne waren gerade <a href="http://de.wikipedia.org/wiki/Silverstein" target="_blank">Silverstein</a> zu Gange. Tendenziell ruhigerer Post-Hardcore, gut vorgetragen - aber nicht wirklich mein Fall.<br />
<br />
Kurz nach 21 Uhr legten dann auch einer kleinen Bühnen am anderen Ende des Festivalgeländes <a href="http://riskit.bandcamp.com/" target="_blank">Risk it!</a> los. Eine deutsch Hardcore Band aus Dresden, welche "klassischen" Hardcore spielen. Jedenfalls habe die fünf eine sehr gute Show hingelegt, mit Energie und Überzeugung gespielt. Ein guter Einstieg in den Abend. Aber ich habe die Show (leider) nicht zu Ende schauen können, weil gegen 21.45 Uhr Comeback Kid auf der Nebenbühne spielten.<br />
<br />
<a href="http://de.wikipedia.org/wiki/Comeback_Kid" target="_blank">Comeback Kid</a> habe ich zum ersten Mal vor 6 oder 7 Jahren auf dem Mair 1 gesehen. Heute, ein paar Jahre und Alben später, haben die fünf Kanadier immer noch reichlich "Bums". Besonders der Frontmann hat unglaublich viel Energie, stürmte die ganze Show kreuz und quer über die Bühne und sang (oder schrie...) sich die Seele aus dem Leib. Kein Wunder, dass der Funke ziemlich schnell auf das Publikum übersprang und die Menge in Bewegung kam.<br />
Alles in allem ein sehr gut, energiegeladene Hardcore-Show. Sehr schön!<br />
<br />
Den Abschluss des Mair 1 2014 machten dann <a href="http://de.wikipedia.org/wiki/Hatebreed" target="_blank">Hatebreed</a>, welche 2012 schon Headliner waren. Die Band gehört ja zu den "ganz großen" der Hardcore Szene. Ohne großes Intro und viel Schnick-Schnack ging's dann los. Das Publikum war sofort dabei und Frontmann Jamey Jasta hat dabei auch durchaus Entertainer-Qualitäten und weiß, wie man das Publikum mit nimmt. Was, gepaart mit der Bühnenpräsenz der Band, zu einer entsprechend guten Stimmung im Publikum sorgt.<br />
Nur leider fing es um kurz nach 23 Uhr, nach den ersten paar Songs von Hatebreed, ziemlich an zu regnen. Was der Stimmung zwar keinen Abbruch tat, mich aber dazu bewog, dann doch schon gegen 23.30 Uhr die Segel zu streichen und Hatebreed nicht noch ganz zu Ende zu schauen.<br />
<br />
Unterm Strich auch das Mair 1 2014 ein super Festival. Ein sehr gutes Line-Up mit einigen "großen" Namen aus der Hardcore Szene, entspannte Atmosphäre und sehr gute Organisation. Wenn das Line-Up stimmt komme ich auch 2015 gerne wieder.</div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0tag:blogger.com,1999:blog-6739072366749751678.post-20345699688382248892014-06-28T16:42:00.000+02:002015-06-27T10:56:11.779+02:00Mair 1 Festival 2014 1. Tag - Walls of Jericho & Ignite<div dir="ltr" style="text-align: left;" trbidi="on">
Am 27./28.6.2014 war es wieder soweit: das <a href="http://www.mair1-festival.de/home/intro" target="_blank">Mair 1 Festival</a> fand, wie in den Jahren zuvor auch, auf dem Flugfeld von Montabaur statt. Traditionell ist das Festival Hardcore und Punk Rock lastig, wobei dieses Jahr viel mehr Hardcore als Punk am Start war. Und auch dieses Jahr waren wieder ein paar hochkarätige Bands des Genres vertreten.<br />
<br />
Während <a href="http://noisefloor-net.blogspot.de/2013/06/mair-1-festival-2862012.html" target="_blank">letztes Jahr</a> netter Weise alle Bands, die ich sehen wollte, an einem Tag gespielt hatten, verteilte es sich diese Jahr auf zwei Tage.<br />
<br />
Am Freitag, 27.6.2014 waren Walls of Jericho und danach Ignite am Start. Besonders auf WoJ war ich gespannt. Gegen 19.20 Uhr war ich vor Ort, da hatten gerade die mir bis dato unbekannten <a href="http://de.wikipedia.org/wiki/Skindred" target="_blank">Skindred</a> mit ihrem Set begonnen. Erster Eindruck: 20 Jahre zu spät. Diese Mischung aus Reggae und Metal war doch in den 90ern "in". Skindred 1998 wurden übrigens gegründet, passt also doch ;-) . Musikalisch nicht ganz mein Fall, aber durchaus gefällig anzuschauen. Wobei man sagen muss, dass es die Band bzw. besonders der Frontmann es sehr gut geschafft hat, das Publikum "mitzunehmen". Von daher war die Stimmung gut. Nur das Ende kam ein bisschen abrupt, scheinbar aufgrund eines technischen Problems des DJs vor dem letzten Song.<br />
<br />
Danach kamen Walls of Jericho auf die kleinere Bühne. Und gaben von der ersten bis zur letzten Minute Vollgas. 110% Energie, top Bühnenpräsenz und absolut mitreißend. Somit war das Publikum auch direkt dabei und ab dem 3. oder 4. Song lief auch der Circle Pit.<br />
Erstaunlich finde ich immer wieder, mit wie viel Power die Frontfrau Candace Kucsulain singen bzw. <a href="http://de.wikipedia.org/wiki/Gutturaler_Gesang" target="_blank">growlen</a> kann. Und das, obwohl sie beim Sprechen eine ganz normale, weibliche Stimme hat.<br />
Jedenfalls ein superguter Auftritt. Eine der besten Hardcore Shows, die ich bisher gesehen habe. Sehr geil!<br />
<br />
Direkt nach Walls of Jericho waren dann <a href="http://de.wikipedia.org/wiki/Ignite" target="_blank">Ignite</a> auf der Hauptbühne an der Reihe. Melodischer Hardcore mit leichtem Emo-Touch, also ab und an auch mal etwas "dunkler" / depressiver. Im direkt Vergleich zu WoJ vorher wirkten Ignite ein bisschen "weich", aber die Show war trotzdem sehr gut. Ein gute Mischung aus neueren und älteren Songs, mal schneller, mal langsamer. Auch hier war das Publikum direkt dabei und so war es eine kurzweilige, gut anzuschauende und anzuhörende Show. Sehr schön.<br />
<br />
Nun war eigentlich Schluss für mich für heute, zumal ich noch auf einer Geburtsfeier eingeladen war. Der nächste Act war "<a href="http://www.bamargera.com/" target="_blank">Bam Margera as the Fuckface Unstoppable</a>". Bam Margera ist von "Jackass" bekannt, die Band war zumindest mir bis dato ebenfalls völlig unbekannt. Also habe ich mir noch die ersten 1,5 Songs angehört. Nach einem sehr obskuren Opening ging's dann los. Klingt irgendwie nach einer Mischung aus Hardrock und Stoner Rock, hörte sich jedenfalls ganz gefällig an.<br />
<br />
Jedenfalls hatte sich der Besuch am 1. Tag schon alleine wegen der Show von Walls of Jericho gelohnt. Am 2. Tag geht's dann weiter im Risk it!, Comeback Kid und Hatebreed.<br />
<br />
<span style="font-size: x-small;">Blogbeitrag zum Besuch des 2. Tags des Festivals: <a href="http://noisefloor-net.blogspot.de/2014/06/mair-1-festival-2-tag-risk-it-comeback.html" target="_blank">Link</a> </span></div>
noisefloorhttp://www.blogger.com/profile/05145579161641978739noreply@blogger.com0