Alexis Métaireau - codehttps://blog.notmyidea.org/2024-02-22T00:00:00+01:00Changing the primary key of a model in Django2024-02-22T00:00:00+01:002024-02-22T00:00:00+01:00tag:blog.notmyidea.org,2024-02-22:/changing-the-primary-key-of-a-model-in-django.html<p>I had to change the primary key of a django model, and I wanted to create a
migration for this.</p>
<p>The previous model was using django <a href="https://
docs.djangoproject.com/en/5.0/topics/db/models/#automatic-primary-key-fields">automatic primary key fields</a></p>
<p>I firstly changed the model to include the new <code>uuid</code> field, and added the <code>id</code>
field (the old primary key …</p><p>I had to change the primary key of a django model, and I wanted to create a
migration for this.</p>
<p>The previous model was using django <a href="https://
docs.djangoproject.com/en/5.0/topics/db/models/#automatic-primary-key-fields">automatic primary key fields</a></p>
<p>I firstly changed the model to include the new <code>uuid</code> field, and added the <code>id</code>
field (the old primary key), like this:</p>
<div class="highlight"><pre><span></span><code> <span class="n">uuid</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">UUIDField</span><span class="p">(</span>
<span class="n">unique</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">uuid</span><span class="o">.</span><span class="n">uuid4</span><span class="p">,</span> <span class="n">editable</span><span class="o">=</span><span class="kc">False</span>
<span class="p">)</span>
<span class="nb">id</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">IntegerField</span><span class="p">(</span><span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</code></pre></div>
<p>Then I created the migration, it:</p>
<ul>
<li>Adds a new <code>uuid</code> field/column in the database</li>
<li>Iterate over the existing items in the table, and generates an uuid for them</li>
<li>Change the old primary key to a different type</li>
<li>Drop the old index</li>
<li>Mark the new uuid as a primary key.</li>
</ul>
<p>To generate the migrations I did <code>django-admin makemigrations</code>, and iterated on
it. Here is the migration I ended up with:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">uuid</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">migrations</span><span class="p">,</span> <span class="n">models</span>
<span class="k">class</span> <span class="nc">Migration</span><span class="p">(</span><span class="n">migrations</span><span class="o">.</span><span class="n">Migration</span><span class="p">):</span>
<span class="n">dependencies</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="s2">"umap"</span><span class="p">,</span> <span class="s2">"0017_migrate_to_openstreetmap_oauth2"</span><span class="p">),</span>
<span class="p">]</span>
<span class="n">operations</span> <span class="o">=</span> <span class="p">[</span>
<span class="c1"># Add the new uuid field</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">AddField</span><span class="p">(</span>
<span class="n">model_name</span><span class="o">=</span><span class="s2">"datalayer"</span><span class="p">,</span>
<span class="n">name</span><span class="o">=</span><span class="s2">"uuid"</span><span class="p">,</span>
<span class="n">field</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">UUIDField</span><span class="p">(</span>
<span class="n">default</span><span class="o">=</span><span class="n">uuid</span><span class="o">.</span><span class="n">uuid4</span><span class="p">,</span> <span class="n">editable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">serialize</span><span class="o">=</span><span class="kc">False</span>
<span class="p">),</span>
<span class="p">),</span>
<span class="c1"># Generate UUIDs for existing records</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">RunSQL</span><span class="p">(</span><span class="s2">"UPDATE umap_datalayer SET uuid = gen_random_uuid()"</span><span class="p">),</span>
<span class="c1"># Remove the primary key constraint</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">RunSQL</span><span class="p">(</span><span class="s2">"ALTER TABLE umap_datalayer DROP CONSTRAINT umap_datalayer_pk"</span><span class="p">),</span>
<span class="c1"># Drop the "id" primary key…</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">AlterField</span><span class="p">(</span>
<span class="s2">"datalayer"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">"id"</span><span class="p">,</span> <span class="n">field</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">IntegerField</span><span class="p">(</span><span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="p">),</span>
<span class="c1"># … to put it back on the "uuid"</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">AlterField</span><span class="p">(</span>
<span class="n">model_name</span><span class="o">=</span><span class="s2">"datalayer"</span><span class="p">,</span>
<span class="n">name</span><span class="o">=</span><span class="s2">"uuid"</span><span class="p">,</span>
<span class="n">field</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">UUIDField</span><span class="p">(</span>
<span class="n">default</span><span class="o">=</span><span class="n">uuid</span><span class="o">.</span><span class="n">uuid4</span><span class="p">,</span>
<span class="n">editable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">unique</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">serialize</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">),</span>
<span class="p">]</span>
</code></pre></div>
<h2 id="generating-uuids-in-pure-python">Generating UUIDs in pure python</h2>
<p>The uuid generation can also be done with pure python, like this. It works with all databases, but might be slower. Use it with <code>migrations.RunPython()</code>.</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span> <span class="nf">gen_uuid</span><span class="p">(</span><span class="n">apps</span><span class="p">,</span> <span class="n">schema_editor</span><span class="p">):</span>
<span class="n">DataLayer</span> <span class="o">=</span> <span class="n">apps</span><span class="o">.</span><span class="n">get_model</span><span class="p">(</span><span class="s2">"umap"</span><span class="p">,</span> <span class="s2">"DataLayer"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">DataLayer</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">():</span>
<span class="n">row</span><span class="o">.</span><span class="n">uuid</span> <span class="o">=</span> <span class="n">uuid</span><span class="o">.</span><span class="n">uuid4</span><span class="p">()</span>
<span class="n">row</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">update_fields</span><span class="o">=</span><span class="p">[</span><span class="s2">"uuid"</span><span class="p">])</span>
</code></pre></div>
<h2 id="getting-the-constraint-name">Getting the constraint name</h2>
<p>One of the things that took me some time is to have a way to get the constraint name before removing it. I wanted to do this with the Django <span class="caps">ORM</span>, but I didn’t find how. So here is how in plain <span class="caps">SQL</span>. This only works with PostgreSQL, though.</p>
<div class="highlight"><pre><span></span><code><span class="n">migrations</span><span class="o">.</span><span class="n">RunSQL</span><span class="p">(</span><span class="s2">"""</span>
<span class="s2">DO $$</span>
<span class="s2">BEGIN</span>
<span class="s2"> EXECUTE 'ALTER TABLE umap_datalayer DROP CONSTRAINT ' || (</span>
<span class="s2"> SELECT indexname</span>
<span class="s2"> FROM pg_indexes</span>
<span class="s2"> WHERE tablename = 'umap_datalayer' AND indexname LIKE '%pkey'</span>
<span class="s2"> );</span>
<span class="s2">END $$;</span>
<span class="s2">"""</span><span class="p">),</span>
</code></pre></div>Using uuids in URLs in a Django app2024-02-22T00:00:00+01:002024-02-22T00:00:00+01:00tag:blog.notmyidea.org,2024-02-22:/using-uuids-in-urls-in-a-django-app.html<p>After adding a regexp for uuids (which are quite hard to regexp for), I
discovered that Django <a href="https://docs.djangoproject.com/
en/5.0/topics/http/urls/#path-converters">offers path converters</a>, making this a piece of cake.</p>
<p>I was using old school <code>re_path</code> paths in my <code>urls.py</code>, but it’s possible to
replace them with <code>path</code>, like this:</p>
<div class="highlight"><pre><span></span><code><span class="n">url_patterns</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">path …</span></code></pre></div><p>After adding a regexp for uuids (which are quite hard to regexp for), I
discovered that Django <a href="https://docs.djangoproject.com/
en/5.0/topics/http/urls/#path-converters">offers path converters</a>, making this a piece of cake.</p>
<p>I was using old school <code>re_path</code> paths in my <code>urls.py</code>, but it’s possible to
replace them with <code>path</code>, like this:</p>
<div class="highlight"><pre><span></span><code><span class="n">url_patterns</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">path</span><span class="p">(</span>
<span class="s2">"datalayer/<int:map_id>/<uuid:pk>/"</span><span class="p">,</span>
<span class="n">views</span><span class="o">.</span><span class="n">DataLayerView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span>
<span class="n">name</span><span class="o">=</span><span class="s2">"datalayer_view"</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">)</span>
</code></pre></div>
<p>A few default path converters are defined (str, int, slug, uuid, path), but it’s
also possible to define your own, as specified in the docs.</p>Adding collaboration on uMap, third update2024-02-12T00:00:00+01:002024-02-12T00:00:00+01:00tag:blog.notmyidea.org,2024-02-12:/adding-collaboration-on-umap-third-update.html<p>I’ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still
with the goal of bringing real-time collaboration to the maps. I’m not there
yet, but I’ve made some progress that I will relate here.</p>
<h2 id="javascript-modules">JavaScript modules</h2>
<p>uMap has been there <a href="https://github.com/
umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time
when <span class="caps">ES6 …</span></p><p>I’ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still
with the goal of bringing real-time collaboration to the maps. I’m not there
yet, but I’ve made some progress that I will relate here.</p>
<h2 id="javascript-modules">JavaScript modules</h2>
<p>uMap has been there <a href="https://github.com/
umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time
when <span class="caps">ES6</span> <a href="https://fr.wikipedia.org/wiki/ECMAScript">wasn’t out there yet</a>.</p>
<p>At that time, it wasn’t possible to use JavaScript modules, nor modern JavaScript
syntax. The project stayed with these requirements for a long time, in order to support
people with old browsers. But as time goes on, we now have access to more browser features,
and it’s now possible to use modules!</p>
<p>The team has been working hard on bringing modules to the mix. It
wasn’t a piece of cake, but the result is here: we’re <a href="https://github.com/umap-project/umap/pull/1463/files">now able to use modern
JavaSript modules</a> and we
are now more confident <a href="https://github.com/umap-project/umap/commit/65f1cdd6b4569657ef5e219d9b377fec85c41958">about which features of the browser we can use or
not</a>.</p>
<hr>
<p>I then spent some time trying to integrate existing CRDTs like
Automerge and <span class="caps">YJS</span> in our project. These two libs are unfortunately expecting us to
use a bundler, which we aren’t currently.</p>
<p>uMap is plain old JavaScript, and as such is not using react or any other framework. The way
I see this is that it makes it possible to have something “close to the
metal” (if that means anything when it comes to web development).</p>
<p>As a result, we’re not tied to the development pace of these frameworks, and have more
control on what we do (read “it’s easier to debug”).</p>
<p>So, after making tweaks and learning how “modules”, “requires” and “bundling”
are working, I ultimately decided to take a break from this path, to work on the
wiring with uMap. After all, CRDTs might not even be the way forward for us.</p>
<h2 id="internals">Internals</h2>
<p>After some time with the head under the water, I’m now able to better
understand the big picture, and I’m not getting lost in the details like I was at first.</p>
<p>Let me try to summarize what I’ve learned.</p>
<p>uMap appears to be doing a lot of different things, but in the end it’s:</p>
<ul>
<li>Using <a href="https://leafletjs.com/">Leaflet.js</a> to render <em>features</em> on the map ;</li>
<li>Using <a href="https://github.com/Leaflet/Leaflet.Editable">Leaflet Editable</a> to edit
complex shapes, like polylines, polygons, and to draw markers ;</li>
<li>Using the <a href="https://github.com/yohanboniface/Leaflet.FormBuilder">Formbuilder</a>
to expose a way for the users to edit the features, and the data of the map</li>
<li>Serializing the layers to and from <a href="https://geojson.org/">GeoJSON</a>. That’s
what’s being sent to and received from the server.</li>
<li>Providing different layer types (marker cluster, chloropleth, etc) to display
the data in different ways.</li>
</ul>
<h3 id="naming-matters">Naming matters</h3>
<p>There is some naming overlap between the different projects we’re using, and
it’s important to have these small clarifications in mind:</p>
<h4 id="leaflet-layers-and-umap-features">Leaflet layers and uMap features</h4>
<p><strong>In Leaflet, everything is a layer</strong>. What we call <em>features</em> in geoJSON are
leaflet layers, and even a (uMap) layer is a layer. We need to be extra careful
what are our inputs and outputs in this context.</p>
<p>We actually have different layers concepts: the <em>datalayer</em> and the different
kind of layers (chloropleth, marker cluster, etc). A datalayer, is (as you can
guess) where the data is stored. It’s what uMap serializes. It contains the
features (with their properties). But that’s the trick: these features are named
<em>layers</em> by Leaflet.</p>
<h4 id="geojson-and-leaflet">GeoJSON and Leaflet</h4>
<p>We’re using GeoJSON to share data with the server, but we’re using Leaflet
internally. And these two have different way of naming things.</p>
<p>The different geometries are named differently (a leaflet <code>Marker</code> is a GeoJSON
<code>Point</code>), and their coordinates are stored differently: Leaflet stores <code>lat,
long</code> where GeoJSON stores <code>long, lat</code>. Not a big deal, but it’s a good thing
to know.</p>
<p>Leaflet stores data in <code>options</code>, where GeoJSON stores it in <code>properties</code>.</p>
<h3 id="this-is-not-reactive-programming">This is not reactive programming</h3>
<p>I was expecting the frontend to be organised similarly to Elm apps (or React
apps): a global state and a data flow (<a href="https:// react-redux.js.org/
introduction/getting-started">a la redux</a>), with events changing the data that will trigger
a rerendering of the interface.</p>
<p>Things work differently for us: different components can write to the map, and
get updated without being centralized. It’s just a different paradigm.</p>
<h2 id="a-syncing-proof-of-concept">A syncing proof of concept</h2>
<p>With that in mind, I started thinking about a simple way to implement syncing. </p>
<p>I left aside all the thinking about how this would relate with CRDTs. It can
be useful, but later. For now, I “just” want to synchronize two maps. I want a
proof of concept to do informed decisions.</p>
<h3 id="syncing-map-properties">Syncing map properties</h3>
<p>I started syncing map properties. Things like the name of the map, the default
color and type of the marker, the description, the default zoom level, etc.</p>
<p>All of these are handled by “the formbuilder”. You pass it an object, a list of
properties and a callback to call when an update happens, and it will build for
you form inputs.</p>
<p>Taken from the documentation (and simplified):</p>
<div class="highlight"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">[</span><span class="s1">'name'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">'BlurInput'</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">'display name'</span><span class="p">}],</span>
<span class="w"> </span><span class="p">[</span><span class="s1">'maxZoom'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">'BlurIntInput'</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">'max zoom'</span><span class="p">}],</span>
<span class="w"> </span><span class="p">[</span><span class="s1">'minZoom'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">'BlurIntInput'</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">'min zoom'</span><span class="p">}],</span>
<span class="w"> </span><span class="p">[</span><span class="s1">'attribution'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">'BlurInput'</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">'attribution'</span><span class="p">}],</span>
<span class="w"> </span><span class="p">[</span><span class="s1">'tms'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">'CheckBox'</span><span class="p">,</span><span class="w"> </span><span class="nx">helpText</span><span class="o">:</span><span class="w"> </span><span class="s1">'TMS format'</span><span class="p">}]</span>
<span class="p">];</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">builder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">FormBuilder</span><span class="p">(</span><span class="nx">myObject</span><span class="p">,</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">callback</span><span class="o">:</span><span class="w"> </span><span class="nx">myCallback</span><span class="p">,</span>
<span class="w"> </span><span class="nx">callbackContext</span><span class="o">:</span><span class="w"> </span><span class="k">this</span>
<span class="p">});</span>
</code></pre></div>
<p>In uMap, the formbuilder is used for every form you see on the right panel. Map
properties are stored in the <code>map</code> object.</p>
<p>We want two different clients work together. When one changes the value of a
property, the other client needs to be updated, and update its interface.</p>
<p>I’ve started by creating a mapping of property names to rerender-methods, and
added a method <code>renderProperties(properties)</code> which updates the interface,
depending on the properties passed to it.</p>
<p>We now have two important things:</p>
<ol>
<li>Some code getting called each time a property is changed ;</li>
<li>A way to refresh the right interface when a property is changed.</li>
</ol>
<p>In other words, from one client we can send the message to the other client,
which will be able to rerender itself.</p>
<p>Looks like a plan.</p>
<h2 id="websockets">Websockets</h2>
<p>We need a way for the data to go from one side to the other. The easiest
way is probably websockets.</p>
<p>Here is a simple code which will relay messages from one websocket to the other
connected clients. It’s not the final code, it’s just for demo puposes.</p>
<p>A basic way to do this on the server side is to use python’s
<a href="https://websockets.readthedocs.io/">websockets</a> library.</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">asyncio</span>
<span class="kn">import</span> <span class="nn">websockets</span>
<span class="kn">from</span> <span class="nn">websockets.server</span> <span class="kn">import</span> <span class="n">serve</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="c1"># Just relay all messages to other connected peers for now</span>
<span class="n">CONNECTIONS</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span>
<span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">async</span> <span class="k">for</span> <span class="n">message</span> <span class="ow">in</span> <span class="n">websocket</span><span class="p">:</span>
<span class="c1"># recompute the peers-list at the time of message-sending.</span>
<span class="c1"># doing so beforehand would miss new connections</span>
<span class="n">peers</span> <span class="o">=</span> <span class="n">CONNECTIONS</span> <span class="o">-</span> <span class="p">{</span><span class="n">websocket</span><span class="p">}</span>
<span class="n">websockets</span><span class="o">.</span><span class="n">broadcast</span><span class="p">(</span><span class="n">peers</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
<span class="k">finally</span><span class="p">:</span>
<span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span>
<span class="n">message</span> <span class="o">=</span> <span class="k">await</span> <span class="n">websocket</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span>
<span class="n">event</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
<span class="c1"># The first event should always be 'join'</span>
<span class="k">assert</span> <span class="n">event</span><span class="p">[</span><span class="s2">"kind"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"join"</span>
<span class="k">await</span> <span class="n">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
<span class="k">async</span> <span class="k">with</span> <span class="n">serve</span><span class="p">(</span><span class="n">handler</span><span class="p">,</span> <span class="s2">"localhost"</span><span class="p">,</span> <span class="mi">8001</span><span class="p">):</span>
<span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Future</span><span class="p">()</span> <span class="c1"># run forever</span>
<span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</code></pre></div>
<p>On the client side, it’s fairly easy as well. I won’t even cover it here.</p>
<p>We now have a way to send data from one client to the other.
Let’s consider the actions we do as “verbs”. For now, we’re just updating
properties values, so we just need the <code>update</code> verb.</p>
<h2 id="code-architecture">Code architecture</h2>
<p>We need different parts:</p>
<ul>
<li>the <strong>transport</strong>, which connects to the websockets, sends and receives messages.</li>
<li>the <strong>message sender</strong> to relat local messages to the other party.</li>
<li>the <strong>message receiver</strong> that’s being called each time we receive a message.</li>
<li>the <strong>sync engine</strong> which glues everything together</li>
<li>Different <strong>updaters</strong>, which knows how to apply received messages, the goal being
to update the interface in the end.</li>
</ul>
<p>When receiving a message it will be routed to the correct updater, which will
know what to do with it.</p>
<p>In our case, its fairly simple: when updating the <code>name</code> property, we send a
message with <code>name</code> and <code>value</code>. We also need to send along some additional
info: the <code>subject</code>.</p>
<p>In our case, it’s <code>map</code> because we’re updating map properties.</p>
<p>When initializing the <code>map</code>, we’re initializing the <code>SyncEngine</code>, like this:</p>
<div class="highlight"><pre><span></span><code><span class="c1">// inside the map</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">umap</span><span class="p">.</span><span class="nx">SyncEngine</span><span class="p">(</span><span class="k">this</span><span class="p">)</span>
<span class="c1">// Then, when we need to send data to the other party</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncEngine</span><span class="p">()</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncSubject</span><span class="p">()</span>
<span class="nx">syncEngine</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">subject</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span>
</code></pre></div>
<p>The code on the other side of the wire is simple enough: when you receive the
message, change the data and rerender the properties:</p>
<div class="highlight"><pre><span></span><code><span class="k">this</span><span class="p">.</span><span class="nx">updateObjectValue</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">,</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span>
<span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">.</span><span class="nx">renderProperties</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span>
</code></pre></div>
<h2 id="syncing-features">Syncing features</h2>
<p>At this stage I was able to sync the properties of the map. A
small victory, but not the end of the trip.</p>
<p>The next step was to add syncing for features: markers, polygon and polylines,
alongside their properties.</p>
<p>All of these features have a uMap class representation (which extends Leaflets
ones). All of them share some code in the <code>FeatureMixin</code> class.</p>
<p>That seems a good place to do the changes.</p>
<p>I did a few changes:</p>
<ul>
<li>Each feature now has an identifier, so clients know they’re talking about the
same thing. This identifier is also stored in the database when saved.</li>
<li>I’ve added an <code>upsert</code> verb, because we don’t have any way (from the
interface) to make a distinction between the creation of a new feature and
its modification. The way we intercept the creation of a feature (or its
update) is to use Leaflet Editable’s <code>editable:drawing:commit</code> event. We just
have to listen to it and then send the appropriate messages !</li>
</ul>
<p>After some giggling around (ah, everybody wants to create a new protocol !) I
went with reusing GeoJSON. It allowed me to have a better understanding of how
Leaflet is using latlongs. That’s a multi-dimensional array, with variable
width, depending on the type of geometry and the number of shapes in each of these.</p>
<p>Clearly not something I want to redo, so I’m now reusing some Leaflet code, which handles this serialization for me.</p>
<p>I’m now able to sync different types of features with their properties.</p>
<video controls width="80%">
<source src="/images/umap/sync-features.webm" type="video/webm">
</video>
<p>Point properties are also editable, using the already-existing table editor. I
was expecting this to require some work but it’s just working without more changes.</p>
<h2 id="whats-next">What’s next ?</h2>
<p>I’m able to sync map properties, features and their properties, but I’m not
yet syncing layers. That’s the next step! I also plan to make some pull
requests with the interesting bits I’m sure will go in the final implementation:</p>
<ul>
<li>Adding ids to features, so we have a way to refer to them.</li>
<li>Having a way to map properties with how they render the interface, the <code>renderProperties</code> bits.</li>
</ul>
<p>When this demo will be working, I’ll probably spend some time updating it with the latest changes (umap is moving a lot these weeks).
I will probably focus on how to integrate websockets in the server side, and then will see how to leverage (maybe) some magic from CRDTs, if we need it.</p>
<p>See you for the next update!</p>Returning objects from an arrow function2024-02-08T00:00:00+01:002024-02-08T00:00:00+01:00tag:blog.notmyidea.org,2024-02-08:/returning-objects-from-an-arrow-function.html<p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p>
<p>Turns out it’s not possible to return directly objects from inside the arrow function because they’re confused as statements.</p>
<p>This is <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#function_body">covered by <span class="caps">MDN</span></a>.</p>
<p>To …</p><p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p>
<p>Turns out it’s not possible to return directly objects from inside the arrow function because they’re confused as statements.</p>
<p>This is <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#function_body">covered by <span class="caps">MDN</span></a>.</p>
<p>To return an object, I had to <strong>put it inside parenthesis, like this</strong>:</p>
<div class="highlight"><pre><span></span><code><span class="nx">latlngs</span><span class="p">.</span><span class="nx">map</span><span class="p">(({</span><span class="w"> </span><span class="nx">lat</span><span class="p">,</span><span class="w"> </span><span class="nx">lng</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">lat</span><span class="p">,</span><span class="w"> </span><span class="nx">lng</span><span class="w"> </span><span class="p">}))</span>
</code></pre></div>Format an USB disk from the command-line on MacOSX2023-12-25T00:00:00+01:002023-12-25T00:00:00+01:00tag:blog.notmyidea.org,2023-12-25:/format-an-usb-disk-from-the-command-line-on-macosx.html<div class="highlight"><pre><span></span><code>sudo<span class="w"> </span>diskutil<span class="w"> </span>unmountDisk<span class="w"> </span>/dev/disk5
sudo<span class="w"> </span>diskutil<span class="w"> </span>eraseDisk<span class="w"> </span><span class="s2">"MS-DOS FAT32"</span><span class="w"> </span>Brocolis<span class="w"> </span>/dev/disk
</code></pre></div>Rescuing a broken asahi linux workstation2023-12-08T00:00:00+01:002023-12-08T00:00:00+01:00tag:blog.notmyidea.org,2023-12-08:/rescuing-a-broken-asahi-linux-workstation.html<p>On my main machine, I’m currently using <a href="asahilinux.org/">Asahi Linux</a> (on a macbook m1). I’ve recently broken my system, which wasn’t able to boot because of a broken <code>/etc/fstab</code>.</p>
<p>On my previous setups, I was able to easily plug an usb key and boot to it to …</p><p>On my main machine, I’m currently using <a href="asahilinux.org/">Asahi Linux</a> (on a macbook m1). I’ve recently broken my system, which wasn’t able to boot because of a broken <code>/etc/fstab</code>.</p>
<p>On my previous setups, I was able to easily plug an usb key and boot to it to solve my issues, but here I wasn’t sure how to deal with it.</p>
<p>After playing a bit (without much luck) with <a href="https://github.com/leifliddy/fedora-macos-asahi-qemu/">qemu and vagrant</a>, someone pointed me to the right direction: using alpine linux.</p>
<p>Here’s what I did to solve my broken install:</p>
<p>First, install this alpine linux on a key.</p>
<p><a href="https://dev.alpinelinux.org/~mps/m1/m1-usb-alpine-install.img.xz">Download the iso image here</a>, and copy it to a key. I’m not sure why, but <code>dd</code> didn’t work for me, and I ended up using another tool to create the usb from the iso. </p>
<div class="highlight"><pre><span></span><code><span class="c1"># When booting, press a key to enter u-boot. Then:</span>
env<span class="w"> </span><span class="nb">set</span><span class="w"> </span>boot_efi_bootmgr
run<span class="w"> </span>bootcmd_usb0
</code></pre></div>
<p>Which should get you a session. When connected, do the following:</p>
<div class="highlight"><pre><span></span><code><span class="c1"># to find the parition you want to mount, marked EFI something</span>
lsblk<span class="w"> </span>-f
mount<span class="w"> </span><span class="nv">label</span><span class="o">=</span><span class="s2">"EFI - FEDOR"</span><span class="w"> </span>/mnt
<span class="c1"># Install the wifi firmware</span>
<span class="nb">cd</span><span class="w"> </span>/lib/firmware
tar<span class="w"> </span>xvf<span class="w"> </span>/mnt/vendor/firmware.tar
/root/update-vendor-firmware
rm<span class="w"> </span>/etc/modprobe.d/blacklist-brcmfmac.conf
modprobe<span class="w"> </span>brcmfmac
<span class="c1"># Connect to the wifi</span>
/etc/init.d/iwd<span class="w"> </span>start
iwctl
</code></pre></div>
<p>In my case, I wanted to mount a btrfs filesystem to fix something inside.</p>
<div class="highlight"><pre><span></span><code>apk<span class="w"> </span>add<span class="w"> </span>btrfs-progs
<span class="nb">echo</span><span class="w"> </span>btrfs<span class="w"> </span>>><span class="w"> </span>/etc/modules
modprobe<span class="w"> </span>btrfs
mount<span class="w"> </span><span class="nv">LABEL</span><span class="o">=</span><span class="s2">"fedora"</span><span class="w"> </span>/opt/fedora
</code></pre></div>
<p>I then could access the filesystem, and made a fix to it.</p>
<hr>
<p>Resources:</p>
<ul>
<li>https://arvanta.net/alpine/install-alpine-m1/</li>
<li>https://arvanta.net/alpine/iwd-howto/</li>
<li>https://wiki.alpinelinux.org/wiki/Btrfs</li>
</ul>Using pelican to track my worked and volunteer hours2023-11-23T00:00:00+01:002023-11-23T00:00:00+01:00tag:blog.notmyidea.org,2023-11-23:/using-pelican-to-track-my-worked-and-volunteer-hours.html<p>I was tracking my hours in Datasette (<a href="https://blog.notmyidea.org/using-datasette-for-tracking-my-professional-activity.html">article</a> and <a href="https://blog.notmyidea.org/deploying-and-customizing-datasette.html">follow-up</a>), but I wasn’t really happy with the editing process.</p>
<p>I’ve seen <a href="https://larlet.fr/david">David</a> notes, which made me want to do something similar.</p>
<p>I’m consigning everything in markdown files and as such, was already keeping track of everything this …</p><p>I was tracking my hours in Datasette (<a href="https://blog.notmyidea.org/using-datasette-for-tracking-my-professional-activity.html">article</a> and <a href="https://blog.notmyidea.org/deploying-and-customizing-datasette.html">follow-up</a>), but I wasn’t really happy with the editing process.</p>
<p>I’ve seen <a href="https://larlet.fr/david">David</a> notes, which made me want to do something similar.</p>
<p>I’m consigning everything in markdown files and as such, was already keeping track of everything this way already. Tracking my hours should be simple otherwise I might just oversee it. So I hacked something together with <a href="https://github.com/getpelican/pelican">pelican</a> (the software I wrote for this blog).</p>
<p><img alt="A graph showing the worked hours and volunteer hours" src="/images/pelican/worklog.png"></p>
<p>It’s doing the following:</p>
<ol>
<li>Defines a specific format for my worklog entries</li>
<li>Parses them (using a regexp), does some computation and ;</li>
<li>Uses a specific template to display a graph and progress bar.</li>
</ol>
<h2 id="reading-information-from-the-titles">Reading information from the titles</h2>
<p>I actually took the format I’ve been already using in my log, and enhanced it a bit.
Basically, the files look likes this (I’m writing in french):</p>
<div class="highlight"><pre><span></span><code>---
title: My project
<span class="gu">total_days: 25</span>
<span class="gu">---</span>
<span class="gu">## Mardi 23 Novembre 2023 (9h, 5/5)</span>
What I did this day.
I can include [<span class="nt">links</span>](<span class="na">https://domain.tld</span>) and whatever I want.
It won't be processed.
<span class="gu">## Lundi 22 Novembre 2023 (8h rémunérées, 2h bénévoles, 4/5)</span>
Something else.
</code></pre></div>
<p>Basically, the second titles (h2) are parsed, and should have the following structure:
<code>{day_of_week} {day} {month} {year} ({worked_hours}(, optional {volunteer_hours}), {fun_rank})</code></p>
<p>The goal here is to retrieve all of this, so I asked ChatGPT for a regexp and iterated on the result which got me:</p>
<div class="highlight"><pre><span></span><code><span class="n">pattern</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span>
<span class="w"> </span><span class="sa">r</span><span class="sd">"""</span>
<span class="sd"> (\w+)\s+ # Day name</span>
<span class="sd"> (\d{1,2})\s+ # Day number</span>
<span class="sd"> ([\wéû]+)\s+ # Month name</span>
<span class="sd"> (\d{4})\s+ # Year</span>
<span class="sd"> \(</span>
<span class="sd"> (\d{1,2})h # Hours (mandatory)</span>
<span class="sd"> (?:\s+facturées)? # Optionally 'facturées', if not present, assume hours are 'facturées'</span>
<span class="sd"> (?:,\s*(\d{1,2})h\s*bénévoles)? # Optionally 'volunteer hours 'bénévoles'</span>
<span class="sd"> ,? # An optional comma</span>
<span class="sd"> \s* # Optional whitespace</span>
<span class="sd"> (?:fun\s+)? # Optionally 'fun' (text) followed by whitespace</span>
<span class="sd"> (\d)/5 # Happiness rating (mandatory, always present)</span>
<span class="sd"> \) # Closing parenthesis</span>
<span class="sd"> """</span><span class="p">,</span>
<span class="n">re</span><span class="o">.</span><span class="n">VERBOSE</span> <span class="o">|</span> <span class="n">re</span><span class="o">.</span><span class="n">UNICODE</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div>
<h2 id="the-markdown-preprocessor">The markdown preprocessor</h2>
<p>I’m already using a custom pelican plugin, which makes it possible to have pelican behave exactly the way I want. For instance, it’s getting the date from the filesystem.</p>
<p>I just had to add some features to it. The way I’m doing this is by <a href="https://docs.getpelican.com/en/3.6.2/plugins.html#how-to-create-a-new-reader">using a custom Markdown reader</a>, on which I add extensions and custom processors.</p>
<p>In my case, I added a preprocessor which will only run when we are handling the worklog. It makes it possible to change what’s being read, before the markdown lib actually transforms it to <span class="caps">HTML</span>.</p>
<p>Here is the code for it:</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">WorklogPreprocessor</span><span class="p">(</span><span class="n">Preprocessor</span><span class="p">):</span>
<span class="n">pattern</span> <span class="o">=</span> <span class="s2">"the regexp we've seen earlier"</span>
<span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">lines</span><span class="p">):</span>
<span class="n">new_lines</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">lines</span><span class="p">:</span>
<span class="k">if</span> <span class="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"##"</span><span class="p">):</span>
<span class="n">match</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pattern</span><span class="p">,</span> <span class="n">line</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">match</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Unable to parse worklog title"</span><span class="p">,</span> <span class="n">line</span><span class="p">)</span>
<span class="p">(</span>
<span class="n">day_of_week</span><span class="p">,</span>
<span class="n">day</span><span class="p">,</span>
<span class="n">month</span><span class="p">,</span>
<span class="n">year</span><span class="p">,</span>
<span class="n">payed_hours</span><span class="p">,</span>
<span class="n">volunteer_hours</span><span class="p">,</span>
<span class="n">happiness</span><span class="p">,</span>
<span class="p">)</span> <span class="o">=</span> <span class="n">match</span><span class="o">.</span><span class="n">groups</span><span class="p">()</span>
<span class="n">volunteer_hours</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">volunteer_hours</span><span class="p">)</span> <span class="k">if</span> <span class="n">volunteer_hours</span> <span class="k">else</span> <span class="mi">0</span>
<span class="n">payed_hours</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">payed_hours</span><span class="p">)</span>
<span class="n">happiness</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">happiness</span><span class="p">)</span>
<span class="n">date</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">strptime</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">day</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">month</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">year</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"</span><span class="si">%d</span><span class="s2"> %B %Y"</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="n">date</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%Y-%m-</span><span class="si">%d</span><span class="s2">"</span><span class="p">)]</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">"payed_hours"</span><span class="p">:</span> <span class="n">payed_hours</span><span class="p">,</span>
<span class="s2">"volunteer_hours"</span><span class="p">:</span> <span class="n">volunteer_hours</span><span class="p">,</span>
<span class="s2">"happyness"</span><span class="p">:</span> <span class="n">happiness</span><span class="p">,</span>
<span class="p">}</span>
<span class="c1"># Replace the line with just the date</span>
<span class="n">new_lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">"## 🗓️ </span><span class="si">{</span><span class="n">day_of_week</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">day</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">month</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">year</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">new_lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">line</span><span class="p">)</span>
<span class="k">return</span> <span class="n">new_lines</span>
</code></pre></div>
<p>It does the following when it encounters a h2 line:</p>
<ul>
<li>try to parse it</li>
<li>store the data locally</li>
<li>replace the line with a simpler version</li>
<li>If if doesn’t work, error out.</li>
</ul>
<p>I’ve also added some computations on top of it, which makes it possible to display a percentage of completion for the project, if “payed_hours” was present in the metadata, and makes it use a specific template (see later).</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span> <span class="nf">compute_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">metadata</span><span class="p">):</span>
<span class="n">done_hours</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">([</span><span class="n">item</span><span class="p">[</span><span class="s2">"payed_hours"</span><span class="p">]</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">data</span><span class="o">.</span><span class="n">values</span><span class="p">()])</span>
<span class="n">data</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span>
<span class="n">data</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">data</span><span class="p">,</span>
<span class="n">done_hours</span><span class="o">=</span><span class="n">done_hours</span><span class="p">,</span>
<span class="n">template</span><span class="o">=</span><span class="s2">"worklog"</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">if</span> <span class="s2">"total_days"</span> <span class="ow">in</span> <span class="n">metadata</span><span class="p">:</span>
<span class="n">total_hours</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">metadata</span><span class="p">[</span><span class="s2">"total_days"</span><span class="p">])</span> <span class="o">*</span> <span class="mi">7</span>
<span class="n">data</span><span class="o">.</span><span class="n">update</span><span class="p">(</span>
<span class="nb">dict</span><span class="p">(</span>
<span class="n">total_hours</span><span class="o">=</span><span class="n">total_hours</span><span class="p">,</span>
<span class="n">percentage</span><span class="o">=</span><span class="nb">round</span><span class="p">(</span><span class="n">done_hours</span> <span class="o">/</span> <span class="n">total_hours</span> <span class="o">*</span> <span class="mi">100</span><span class="p">),</span>
<span class="p">)</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">data</span>
</code></pre></div>
<h2 id="plugging-this-with-pelican">Plugging this with pelican</h2>
<p>Here’s the code for extending a custom reader, basically adding a pre-processor and adding back its data in the document metadata:</p>
<div class="highlight"><pre><span></span><code><span class="n">is_worklog</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">source_path</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">match</span><span class="p">(</span><span class="s2">"pages/worklog"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">is_worklog</span><span class="p">:</span>
<span class="n">worklog</span> <span class="o">=</span> <span class="n">WorklogPreprocessor</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_md</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_md</span><span class="o">.</span><span class="n">preprocessors</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">worklog</span><span class="p">,</span> <span class="s2">"worklog"</span><span class="p">,</span> <span class="mi">20</span><span class="p">)</span>
<span class="c1"># process the markdown, and then</span>
<span class="k">if</span> <span class="n">is_worklog</span><span class="p">:</span>
<span class="n">metadata</span><span class="p">[</span><span class="s2">"worklog"</span><span class="p">]</span> <span class="o">=</span> <span class="n">worklog</span><span class="o">.</span><span class="n">compute_data</span><span class="p">(</span><span class="n">metadata</span><span class="p">)</span>
</code></pre></div>
<h2 id="adding-a-graph">Adding a graph</h2>
<p>Okay, everything is parsed, but it’s not yet displayed on the pages. I’m using <a href="https://vega.github.io/vega-lite/docs/">vega-lite</a> to display a graph.</p>
<p>Here is my template for this (stored in <code>template/worklog.html</code>), it’s doing a stacked bar chart with my data.</p>
<div class="highlight"><pre><span></span><code><span class="kd">const</span><span class="w"> </span><span class="nx">spec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"$schema"</span><span class="o">:</span><span class="w"> </span><span class="s2">"https://vega.github.io/schema/vega-lite/v5.json"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"width"</span><span class="o">:</span><span class="w"> </span><span class="mf">500</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"height"</span><span class="o">:</span><span class="w"> </span><span class="mf">200</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"data"</span><span class="o">:</span><span class="w"> </span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"name"</span><span class="o">:</span><span class="w"> </span><span class="s2">"table"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"values"</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span><span class="o">%</span><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="nx">date</span><span class="p">,</span><span class="w"> </span><span class="nx">item</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nx">page</span><span class="p">.</span><span class="nx">metadata</span><span class="p">.</span><span class="nx">worklog</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">items</span><span class="p">()</span><span class="w"> </span><span class="o">%</span><span class="p">}</span>
<span class="w"> </span><span class="p">{</span><span class="s2">"date"</span><span class="o">:</span><span class="w"> </span><span class="s2">"{{ date }}"</span><span class="p">,</span><span class="w"> </span><span class="s2">"series"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Rémunéré"</span><span class="p">,</span><span class="w"> </span><span class="s2">"count"</span><span class="o">:</span><span class="w"> </span><span class="p">{{</span><span class="w"> </span><span class="nx">item</span><span class="p">[</span><span class="s1">'payed_hours'</span><span class="p">]</span><span class="w"> </span><span class="p">}}},</span>
<span class="w"> </span><span class="p">{</span><span class="s2">"date"</span><span class="o">:</span><span class="w"> </span><span class="s2">"{{ date }}"</span><span class="p">,</span><span class="w"> </span><span class="s2">"series"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Bénévole"</span><span class="p">,</span><span class="w"> </span><span class="s2">"count"</span><span class="o">:</span><span class="w"> </span><span class="p">{{</span><span class="w"> </span><span class="nx">item</span><span class="p">[</span><span class="s1">'volunteer_hours'</span><span class="p">]</span><span class="w"> </span><span class="p">}}},</span>
<span class="w"> </span><span class="p">{</span><span class="o">%</span><span class="w"> </span><span class="nx">endfor</span><span class="w"> </span><span class="o">%</span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">,</span>
<span class="w"> </span><span class="s2">"mark"</span><span class="o">:</span><span class="w"> </span><span class="s2">"bar"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"encoding"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"x"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"timeUnit"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="s2">"unit"</span><span class="o">:</span><span class="w"> </span><span class="s2">"dayofyear"</span><span class="p">,</span><span class="w"> </span><span class="s2">"step"</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="p">},</span>
<span class="w"> </span><span class="s2">"field"</span><span class="o">:</span><span class="w"> </span><span class="s2">"date"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"axis"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="s2">"format"</span><span class="o">:</span><span class="w"> </span><span class="s2">"%d/%m"</span><span class="p">},</span>
<span class="w"> </span><span class="s2">"title"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Date"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"step"</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="p">,</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="s2">"y"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"aggregate"</span><span class="o">:</span><span class="w"> </span><span class="s2">"sum"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"field"</span><span class="o">:</span><span class="w"> </span><span class="s2">"count"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"title"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Heures"</span><span class="p">,</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="s2">"color"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"field"</span><span class="o">:</span><span class="w"> </span><span class="s2">"series"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"scale"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"domain"</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Bénévole"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Rémunéré"</span><span class="p">],</span>
<span class="w"> </span><span class="s2">"range"</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"#e7ba52"</span><span class="p">,</span><span class="w"> </span><span class="s2">"#1f77b4"</span><span class="p">]</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="s2">"title"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Type d'heures"</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">};</span>
<span class="w"> </span><span class="nx">vegaEmbed</span><span class="p">(</span><span class="s2">"#vis"</span><span class="p">,</span><span class="w"> </span><span class="nx">spec</span><span class="p">)</span>
<span class="w"> </span><span class="c1">// result.view provides access to the Vega View API</span>
<span class="w"> </span><span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">result</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">result</span><span class="p">))</span>
<span class="w"> </span><span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">console</span><span class="p">.</span><span class="nx">warn</span><span class="p">);</span>
</code></pre></div>
<p>I’ve also added a small progress bar, made with unicode, which looks like this.</p>
<div class="highlight"><pre><span></span><code>▓▓░░░░░░░░ 29% (51h / 175 prévues)
</code></pre></div>
<p>Here is the code for it:</p>
<div class="highlight"><pre><span></span><code><span class="cp">{%</span> <span class="k">if</span> <span class="s2">"total_days"</span> <span class="k">in</span> <span class="nv">page.metadata.keys</span><span class="o">()</span> <span class="cp">%}</span>
<span class="w"> </span><span class="cp">{%</span> <span class="k">set</span> <span class="nv">percentage</span> <span class="o">=</span> <span class="nv">page.metadata.worklog</span><span class="o">[</span><span class="s1">'percentage'</span><span class="o">]</span> <span class="cp">%}</span>
<span class="w"> </span><span class="cp">{%</span> <span class="k">set</span> <span class="nv">total_blocks</span> <span class="o">=</span> <span class="m">10</span> <span class="cp">%}</span>
<span class="w"> </span><span class="cp">{%</span> <span class="k">set</span> <span class="nv">percentage_value</span> <span class="o">=</span> <span class="o">(</span><span class="nv">percentage</span> <span class="o">/</span> <span class="m">100.0</span><span class="o">)</span> <span class="cp">%}</span>
<span class="w"> </span><span class="cp">{%</span> <span class="k">set</span> <span class="nv">full_blocks</span> <span class="o">=</span> <span class="o">((</span><span class="nv">percentage_value</span> <span class="o">*</span> <span class="nv">total_blocks</span><span class="o">)</span> <span class="o">|</span> <span class="nf">round</span><span class="o">(</span><span class="m">0</span><span class="o">,</span> <span class="s1">'floor'</span><span class="o">)</span> <span class="o">)</span> <span class="o">|</span> <span class="nf">int</span> <span class="cp">%}</span>
<span class="w"> </span><span class="cp">{%</span> <span class="k">set</span> <span class="nv">empty_blocks</span> <span class="o">=</span> <span class="nv">total_blocks</span> <span class="o">-</span> <span class="nv">full_blocks</span> <span class="cp">%}</span>
<span class="w"> </span><span class="nt"><div</span><span class="w"> </span><span class="na">class=</span><span class="s">"progressbar"</span><span class="nt">></span>
<span class="w"> </span><span class="c">{# Display full blocks #}</span>
<span class="w"> </span><span class="cp">{%</span> <span class="k">for</span> <span class="nv">i</span> <span class="k">in</span> <span class="nv">range</span><span class="o">(</span><span class="nv">full_blocks</span><span class="o">)</span> <span class="cp">%}</span>▓<span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
<span class="w"> </span><span class="c">{# Display empty blocks #}</span>
<span class="w"> </span><span class="cp">{%</span> <span class="k">for</span> <span class="nv">i</span> <span class="k">in</span> <span class="nv">range</span><span class="o">(</span><span class="nv">empty_blocks</span><span class="o">)</span> <span class="cp">%}</span>░<span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
<span class="w"> </span><span class="cp">{{</span> <span class="nv">percentage</span> <span class="cp">}}</span>%<span class="w"> </span>(<span class="cp">{{</span> <span class="nv">page.metadata.worklog</span><span class="o">[</span><span class="s1">'done_hours'</span><span class="o">]</span> <span class="cp">}}</span>h<span class="w"> </span>/<span class="w"> </span><span class="cp">{{</span> <span class="nv">page.metadata.worklog</span><span class="o">[</span><span class="s1">'total_hours'</span><span class="o">]</span> <span class="cp">}}</span><span class="w"> </span>prévues)
<span class="w"> </span><span class="nt"></div></span>
</code></pre></div>Adding Real-Time Collaboration to uMap, second week2023-11-21T00:00:00+01:002023-11-21T00:00:00+01:00tag:blog.notmyidea.org,2023-11-21:/adding-real-time-collaboration-to-umap-second-week.html<p>I continued working on <a href="https://github.com/umap-project/umap/">uMap</a>, an open-source map-making tool to create and share customizable maps, based on Open Street Map data.</p>
<p>Here is a summary of what I did:</p>
<ul>
<li>I reviewed, rebased and made some minor changes to <a href="https://github.com/umap-project/umap/pull/772">a pull request which makes it possible to merge geojson features together …</a></li></ul><p>I continued working on <a href="https://github.com/umap-project/umap/">uMap</a>, an open-source map-making tool to create and share customizable maps, based on Open Street Map data.</p>
<p>Here is a summary of what I did:</p>
<ul>
<li>I reviewed, rebased and made some minor changes to <a href="https://github.com/umap-project/umap/pull/772">a pull request which makes it possible to merge geojson features together</a> ;</li>
<li>I’ve explored around the idea of using SQLite inside the browser, for two reasons : it could make it possible to use the <a href="https://www.gaia-gis.it/fossil/libspatialite/index">Spatialite</a> extension, and it might help us to implement a <span class="caps">CRDT</span> with <a href="https://github.com/vlcn-io/cr-sqlite">cr-sqlite</a> ;</li>
<li>I learned a lot about the <span class="caps">SIG</span> field. This is a wide ecosystem with lots of moving parts, which I understand a bit better now.</li>
</ul>
<h2 id="the-optimistic-merge-approach">The optimistic-merge approach</h2>
<p>There were an open pull request implementing an “optimistic merge”. We spent some time together with Yohan to understand what the pull request is doing, discuss it and made a few changes.</p>
<p>Here’s the logic of the changes:</p>
<ol>
<li>On the server-side, we detect if we have a conflict between the incoming changes and what’s stored on the server (is the last document save fresher than the <code>IF-UNMODIFIED-SINCE</code> header we get ?) ;</li>
<li>In case of conflict, find back the reference document in the history (let’s name this the “local reference”) ;</li>
<li>Merge the 3 documents together, that is :</li>
<li>Find what the the incoming changes are, by comparing the incoming doc to the local reference.</li>
<li>Re-apply the changes on top of the latest doc.</li>
</ol>
<p>One could compare this logic to what happens when you do a <code>git rebase</code>. Here is some pseudo-code:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span> <span class="nf">merge_features</span><span class="p">(</span><span class="n">reference</span><span class="p">:</span> <span class="nb">list</span><span class="p">,</span> <span class="n">latest</span><span class="p">:</span> <span class="nb">list</span><span class="p">,</span> <span class="n">incoming</span><span class="p">:</span> <span class="nb">list</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""Finds the changes between reference and incoming, and reapplies them on top of latest."""</span>
<span class="k">if</span> <span class="n">latest</span> <span class="o">==</span> <span class="n">incoming</span><span class="p">:</span>
<span class="k">return</span> <span class="n">latest</span>
<span class="n">reference_removed</span><span class="p">,</span> <span class="n">incoming_added</span> <span class="o">=</span> <span class="n">get_difference</span><span class="p">(</span><span class="n">reference</span><span class="p">,</span> <span class="n">incoming</span><span class="p">)</span>
<span class="c1"># Ensure that items changed in the reference weren't also changed in the latest.</span>
<span class="k">for</span> <span class="n">removed</span> <span class="ow">in</span> <span class="n">reference_removed</span><span class="p">:</span>
<span class="k">if</span> <span class="n">removed</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">latest</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ConflictError</span>
<span class="n">merged</span> <span class="o">=</span> <span class="n">copy</span><span class="p">(</span><span class="n">latest</span><span class="p">)</span>
<span class="c1"># Reapply the changes on top of the latest.</span>
<span class="k">for</span> <span class="n">removed</span> <span class="ow">in</span> <span class="n">reference_removed</span><span class="p">:</span>
<span class="n">merged</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">removed</span><span class="p">)</span>
<span class="k">for</span> <span class="n">added</span> <span class="ow">in</span> <span class="n">incoming_added</span><span class="p">:</span>
<span class="n">merged</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">added</span><span class="p">)</span>
<span class="k">return</span> <span class="n">merged</span>
</code></pre></div>
<p>The pull request is not ready yet, as I still want to add tests with real data, and enhance the naming, but that’s a step in the right direction :-)</p>
<h2 id="using-sqlite-in-the-browser">Using SQLite in the browser</h2>
<p>At the moment, (almost) everything is stored on the server side as GeoJSON files. They are simple to use, to read and to write, and having them on the storage makes it easy to handle multiple revisions.</p>
<p>I’ve been asked to challenge this idea for a moment. What if we were using some other technology to store the data? What would that give us? What would be the challenges?</p>
<p>I went with SQLite, just to see what this would mean.</p>
<ul>
<li>SQLite is originally not made to work on a web browser, but thanks to Web Assembly, it’s possible to use it. It’s not <strong>that</strong> big, but the library weights 2Mb.</li>
<li>With projects such as <a href="https://github.com/vlcn-io/cr-sqlite"><span class="caps">CR</span>-SQLite</a>, you get a way to add CRDTs on top of SQLite databases. Meaning that the clients could send their changes to other clients or to the server, and that it would be easy to integrate ;</li>
<li>The clients could retrieve just some part of the data to the server (e.g. by specifying a bounding box), which gives it the possibility to not load everything in memory if that’s not needed.</li>
</ul>
<p>I wanted to see how it would work, and what would be the challenges around this technology. I wrote a small application with it. Turns out writing to a local in-browser SQLite works.</p>
<p>Here is what it would look like:</p>
<ul>
<li>Each client will get a copy of the database, alongside a version ;</li>
<li>When clients send changes, you can just send the data since the last version ;</li>
<li>Databases can be merged without loosing data, the operations done in <span class="caps">SQL</span> will trigger writes to a specific table, which will be used as a <span class="caps">CRDT</span>.</li>
</ul>
<p>I’m not sure SQLite by itself is useful here. It sure is fun, but I don’t see what we get in comparison with a more classical <span class="caps">CRDT</span> approach, except complexity. The technology is still quite young and rough to the edges, and uses Rust and WebASM, which are still strange beasts to me. </p>
<h2 id="related-projects-in-the-sig-field">Related projects in the <span class="caps">SIG</span> field</h2>
<p>Here are some interesting projects I’ve found this week :</p>
<ul>
<li><a href="https://allartk.github.io/leaflet.offline/">Leaflet.offline</a> allows to store the tiles offline ;</li>
<li><a href="https://github.com/mapbox/geojson-vt">geojson-vt</a> uses the concept of “vector tiles” I didn’t know about. Tiles can return binary or vectorial data, which can be useful to just get the data in one specific bounding box This allows us for instance to store GeoJSON in vector tiles.</li>
<li><a href="https://github.com/mapbox/mapbox-gl-js">mapbox-gl-js</a> makes it possible to render <span class="caps">SIG</span>-related data using WebGL (no connection with Leaflet)</li>
<li><a href="https://github.com/BenjaminVadant/leaflet-ugeojson">leaflet-ugeojson</a> and <a href="https://github.com/jieter/Leaflet.Sync">leaflet.Sync</a> allows multiple people to share the same view on a map.</li>
</ul>
<p>Two libraries seems useful for us:</p>
<ul>
<li><a href="https://github.com/ATran31/Leaflet-GeoSSE">Leaflet-GeoSSE</a> makes it possible to use <span class="caps">SSE</span> (Server Sent Events) to update local data. It uses events (create, update, delete) and keys in the GeoJSON features..</li>
<li><a href="https://github.com/perliedman/leaflet-realtime">Leaflet Realtime</a> does something a bit similar, but doesn’t take care of the transport. It’s meant to be used to track remote elements (a <span class="caps">GPS</span> tracker for instance)</li>
</ul>
<p>I’m noting that:</p>
<ul>
<li>In the two libraries, unique identifiers are added to the <code>features</code> to allow for updates.</li>
<li>None of these libraries makes it possible to track local changes. That’s what’s left to find.</li>
</ul>
<h2 id="how-to-transport-the-data">How to transport the data?</h2>
<p>One of the related subjects is transportation of the data between the client and the server. When we’ll get the local changes, we’ll need to find a way to send this data to the other clients, and ultimately to the server.</p>
<p>There are multiple ways to do this, and I spent some time trying to figure out the pros and cons of each approach. Here is a list:</p>
<ul>
<li><strong>WebRTC, the <span class="caps">P2P</span> approach</strong>. You let the clients talk to each other. I’m not sure where the server fits in this scenario. I’ve yet to figure-out how this works out in real-case scenarii, where you’re working behind a <span class="caps">NAT</span>, for instance. Also, what’s the requirement on <span class="caps">STUN</span> / Turn servers, etc. </li>
<li>Using <strong>WebSockets</strong> seems nice at the first glance, but I’m concerned about the resources this could take on the server. The requirement we have on “real-time” is not that big (e.g. if it’s not immediate, it’s okay).</li>
<li>Using <strong>Server Sent Events</strong> is another way to solve this, it seems lighter on the client and on the server. The server still needs to keep connexion opens, but I’ve found some proxies which will do that for you, so it would be something to put between the uMap server and the <span class="caps">HTTP</span> server.</li>
<li><strong>Polling</strong> means less connexion open, but also that the server will need to keep track of the messages the clients have to get. It’s easily solvable with a Redis queue for instance.</li>
</ul>
<p>All of these scenarii are possible, and each of them has pros and cons. I’ll be working on a document this week to better understand what’s hidden behind each of these, so we can ultimately make a choice.</p>
<h3 id="server-sent-events-sse">Server-Sent Events (<span class="caps">SSE</span>)</h3>
<p>Here are some notes about <span class="caps">SSE</span>. I’ve learned that:</p>
<ul>
<li><span class="caps">SSE</span> makes it so that server connections never ends (so it consumes a process?)</li>
<li>There is a library in Django for this, named <a href="https://github.com/fanout/django-eventstream">django-eventstream</a></li>
<li><a href="https://channels.readthedocs.io/en/latest/">Django channels</a> aims at using <span class="caps">ASGI</span> for certain parts of the app.</li>
<li>You don’t have to handle all this in Django. It’s possible to delegate it to <a href="https://github.com/fastly/pushpin">pushpin</a>, a proxy, using <a href="https://github.com/fanout/django-grip">django-grip</a></li>
</ul>
<p>It’s questioning me in terms of infrastructure changes.</p>Importing a PostgreSQL dump under a different database name2023-11-20T00:00:00+01:002023-11-20T00:00:00+01:00tag:blog.notmyidea.org,2023-11-20:/importing-a-postgresql-dump-under-a-different-database-name.html<p>For <a href="https://chariotte.fr">Chariotte</a>, I’ve had to do an import from one system to the other. I had no control on the export I received. It contained the database name and the ACLs, which I had to change to match the ones on the new system.</p>
<h2 id="decrypting-the-dump">Decrypting the dump</h2>
<p>First off …</p><p>For <a href="https://chariotte.fr">Chariotte</a>, I’ve had to do an import from one system to the other. I had no control on the export I received. It contained the database name and the ACLs, which I had to change to match the ones on the new system.</p>
<h2 id="decrypting-the-dump">Decrypting the dump</h2>
<p>First off, the import I received was encrypted, so I had to decrypt it. It took me some time to figure out that both my private <strong>and public</strong> keys needed to be imported to the pgp. Once that was done, I could decrypt with</p>
<div class="highlight"><pre><span></span><code><span class="c1"># Decrypt the file</span>
gpg<span class="w"> </span>--decrypt<span class="w"> </span>hb_chariotte_prod.pgdump.asc<span class="w"> </span>><span class="w"> </span>hb_chariotte_prod.pgdump
<span class="c1"># Upload it to the server with scp</span>
scp<span class="w"> </span>hb_chariotte_prod.pgdump<span class="w"> </span>chariotte:.
</code></pre></div>
<h2 id="importing-while-changing-acls-and-database-name">Importing while changing ACLs and database name</h2>
<p>On the server, here is the command to change the name of the database and the user. The file I received was using the so-called “custom” format, which is not editable with a simple editor, so you have to export it to <span class="caps">SQL</span> first, and then edit it before running the actual queries.</p>
<div class="highlight"><pre><span></span><code><span class="c1"># Convert to SQL, then replace the table name with the new one, and finally run the SQL statements.</span>
pg_restore<span class="w"> </span>-C<span class="w"> </span>-f<span class="w"> </span>-<span class="w"> </span>hb_chariotte_prod.pgdump<span class="w"> </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span><span class="s1">'s/hb_chariotte_prod/chariotte_temp/g'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>psql<span class="w"> </span>-U<span class="w"> </span>chariotte_temp<span class="w"> </span>-d<span class="w"> </span>chariotte_temp<span class="w"> </span>-h<span class="w"> </span>yourhost
</code></pre></div>Deploying and customizing datasette2023-11-12T00:00:00+01:002023-11-12T00:00:00+01:00tag:blog.notmyidea.org,2023-11-12:/deploying-and-customizing-datasette.html<p>First, create the venv and install everything</p>
<div class="highlight"><pre><span></span><code><span class="c1"># Create and activate venv</span>
python3<span class="w"> </span>-m<span class="w"> </span>venv<span class="w"> </span>venv
<span class="nb">source</span><span class="w"> </span>venv/bin/activate
<span class="c1"># Install datasette…</span>
pip<span class="w"> </span>install<span class="w"> </span>datasette
<span class="c1"># … and the plugins</span>
datasette<span class="w"> </span>install<span class="w"> </span>datasette-render-markdown<span class="w"> </span>datasette-dashboards<span class="w"> </span>datasette-dateutil
</code></pre></div>
<p>I was curious how much all of this was weighting. <span class="caps">30MB</span> seems pretty reasonable to me.</p>
<div class="highlight"><pre><span></span><code><span class="c1"># All of …</span></code></pre></div><p>First, create the venv and install everything</p>
<div class="highlight"><pre><span></span><code><span class="c1"># Create and activate venv</span>
python3<span class="w"> </span>-m<span class="w"> </span>venv<span class="w"> </span>venv
<span class="nb">source</span><span class="w"> </span>venv/bin/activate
<span class="c1"># Install datasette…</span>
pip<span class="w"> </span>install<span class="w"> </span>datasette
<span class="c1"># … and the plugins</span>
datasette<span class="w"> </span>install<span class="w"> </span>datasette-render-markdown<span class="w"> </span>datasette-dashboards<span class="w"> </span>datasette-dateutil
</code></pre></div>
<p>I was curious how much all of this was weighting. <span class="caps">30MB</span> seems pretty reasonable to me.</p>
<div class="highlight"><pre><span></span><code><span class="c1"># All of this weights 30Mb</span>
du<span class="w"> </span>-sh<span class="w"> </span>venv
30M<span class="w"> </span>venv
</code></pre></div>
<h2 id="adding-authentication">Adding authentication</h2>
<p>Datasette doesn’t provide authentication by default, so <a href="https://docs.datasette.io/en/stable/authentication.html">you have to use a plugin for this</a>. I’ll be using <a href="https://github.com/simonw/datasette-auth-github">Github authentication</a> for now as it seems simple to add:</p>
<div class="highlight"><pre><span></span><code>pip install datasette-auth-github
</code></pre></div>
<p>I’ve had to create a new github application and export the variables to my server, and add some configuration to my <code>metadata.yaml</code> file:</p>
<div class="highlight"><pre><span></span><code><span class="nt">allow</span><span class="p">:</span>
<span class="w"> </span><span class="nt">gh_login</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">almet</span>
<span class="nt">plugins</span><span class="p">:</span>
<span class="w"> </span><span class="nt">datasette-auth-github</span><span class="p">:</span>
<span class="w"> </span><span class="nt">client_id</span><span class="p">:</span>
<span class="w"> </span><span class="s">"$env"</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">GITHUB_CLIENT_ID</span>
<span class="w"> </span><span class="nt">client_secret</span><span class="p">:</span>
<span class="w"> </span><span class="s">"$env"</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">GITHUB_CLIENT_SECRET</span>
</code></pre></div>
<p>If that’s useful to you, here is <a href="https://gitlab.com/almet/timetracker-datasette-deploy">the git repository</a> I’m deploying to my server.</p>
<h2 id="using-templates">Using templates</h2>
<p>Okay, I now want to be able to send an <span class="caps">URL</span> to the people I’m working with, on which they can see what I’ve been doing, and what I’ve been using my time on.</p>
<p>It was pretty simple to do, and kind of weird to basically do what I’ve been doing back in the days for my first <span class="caps">PHP</span> applications : put <span class="caps">SQL</span> statements in the templates ! heh.</p>
<p>I’ve added a template with what I want to do. It has the side-effect of being able to propose a read-only view to a private database.</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">h1</span><span class="p">></span>{{project}}
{% for row in sql("SELECT SUM(CAST(duration AS REAL)) as total_hours FROM journal WHERE project = '" + project + "';", database="db") %}
({{ row["total_hours"] }} heures)
{% endfor %}
<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"><</span><span class="nt">dl</span><span class="p">></span>
{% for row in sql("select date, CAST(duration AS REAL) as duration, content from journal where project = '" + project + "' order by date DESC", database="db") %}
<span class="p"><</span><span class="nt">dt</span><span class="p">></span>{{ row["date"] }} ({{ row["duration"] }} heures)<span class="p"></</span><span class="nt">dt</span><span class="p">></span>
<span class="p"><</span><span class="nt">dd</span><span class="p">></span>{{ render_markdown(row["content"]) }}<span class="p"></</span><span class="nt">dd</span><span class="p">></span>
{% endfor %}
<span class="p"></</span><span class="nt">dl</span><span class="p">></span>
</code></pre></div>
<p>Which looks like this :</p>
<p><img alt="Alt text" src="/images/datasette/custom-template.png"></p>Adding Real-Time Collaboration to uMap, first week2023-11-11T00:00:00+01:002023-11-11T00:00:00+01:00tag:blog.notmyidea.org,2023-11-11:/adding-real-time-collaboration-to-umap-first-week.html<p>Last week, I’ve been lucky to start working on <a href="https://github.com/umap-project/umap/">uMap</a>, an open-source map-making tool to create and share customizable maps, based on Open Street Map data.</p>
<p>My goal is to add real-time collaboration to uMap, but <strong>we first want to be sure to understand the issue correctly</strong>. There are …</p><p>Last week, I’ve been lucky to start working on <a href="https://github.com/umap-project/umap/">uMap</a>, an open-source map-making tool to create and share customizable maps, based on Open Street Map data.</p>
<p>My goal is to add real-time collaboration to uMap, but <strong>we first want to be sure to understand the issue correctly</strong>. There are multiple ways to solve this, so one part of the journey is to understand the problem properly (then, we’ll be able to chose the right path forward).</p>
<p>Part of the work is documenting it, so expect to see some blog posts around this in the future.</p>
<h2 id="installation">Installation</h2>
<p>I’ve started by installing uMap on my machine, made it work and read the codebase. uMap is written in Python and Django, and using old school Javascript, specifically using the Leaflet library for <span class="caps">SIG</span>-related interface.</p>
<p>Installing uMap was simple. On a mac:</p>
<ol>
<li>Create the venv and activate it</li>
</ol>
<div class="highlight"><pre><span></span><code>python3<span class="w"> </span>-m<span class="w"> </span>venv<span class="w"> </span>venv
<span class="nb">source</span><span class="w"> </span>venv/bin/activate
pip<span class="w"> </span>install<span class="w"> </span>-e<span class="w"> </span>.
</code></pre></div>
<ol>
<li>Install the deps : <code>brew install postgis</code> (this will take some time to complete)</li>
</ol>
<div class="highlight"><pre><span></span><code>createuser<span class="w"> </span>umap
createdb<span class="w"> </span>umap<span class="w"> </span>-O<span class="w"> </span>umap
psql<span class="w"> </span>umap<span class="w"> </span>-c<span class="w"> </span><span class="s2">"CREATE EXTENSION postgis"</span>
</code></pre></div>
<ol>
<li>Copy the default config with <code>cp umap/settings/local.py.sample umap.conf</code></li>
</ol>
<div class="highlight"><pre><span></span><code><span class="c1"># Copy the default config to umap.conf</span>
cp<span class="w"> </span>umap/settings/local.py.sample<span class="w"> </span>umap.conf
<span class="nb">export</span><span class="w"> </span><span class="nv">UMAP_SETTINGS</span><span class="o">=</span>~/dev/umap/umap.conf
make<span class="w"> </span>install
make<span class="w"> </span>installjs
make<span class="w"> </span>vendors
umap<span class="w"> </span>migrate
umap<span class="w"> </span>runserver
</code></pre></div>
<h2 id="and-youre-done">And you’re done!</h2>
<p>On Arch Linux, I had to do some changes, but all in all it was simple:</p>
<div class="highlight"><pre><span></span><code>createuser<span class="w"> </span>umap<span class="w"> </span>-U<span class="w"> </span>postgres
createdb<span class="w"> </span>umap<span class="w"> </span>-O<span class="w"> </span>umap<span class="w"> </span>-U<span class="w"> </span>postgres
psql<span class="w"> </span>umap<span class="w"> </span>-c<span class="w"> </span><span class="s2">"CREATE EXTENSION postgis"</span><span class="w"> </span>-Upostgres
</code></pre></div>
<p>Depending on your installation, you might need to change the <span class="caps">USER</span> that connects the database.</p>
<p>The configuration could look like this:</p>
<div class="highlight"><pre><span></span><code><span class="n">DATABASES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">"default"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"ENGINE"</span><span class="p">:</span> <span class="s2">"django.contrib.gis.db.backends.postgis"</span><span class="p">,</span>
<span class="s2">"NAME"</span><span class="p">:</span> <span class="s2">"umap"</span><span class="p">,</span>
<span class="s2">"USER"</span><span class="p">:</span> <span class="s2">"postgres"</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<h2 id="how-its-currently-working">How it’s currently working</h2>
<p>With everything working on my machine, I took some time to read the code and understand
the current code base.</p>
<p>Here are my findings :</p>
<ul>
<li>uMap is currently using a classical client/server architecture where :</li>
<li>The server is here mainly to handle access rights, store the data and send it over to the clients.</li>
<li>The actual rendering and modifications of the map are directly done in JavaScript, on the clients.</li>
</ul>
<p>The data is split in multiple layers. At the time of writing, concurrent writes to the same layers are not possible, as one edit would potentially overwrite the other. It’s possible to have concurrent edits on different layers, though.</p>
<p>When a change occurs, <a href="https://github.com/umap-project/umap/blob/c16a01778b4686a562d97fde1cfd3433777d7590/umap/views.py#L917-L948">each <code>DataLayer</code> is sent by the client to the server</a>.</p>
<ul>
<li>The data is updated on the server.</li>
<li><strong>If the data has been modified by another client</strong>, an <code>HTTP 422 (Unprocessable Entity)</code> status is returned, which makes it possible to detect conflicts. The users are prompted about it, and asked if they want to overwrite the changes.</li>
<li>The files are stored as geojson files on the server as <code>{datalayer.pk}_{timestamp}.geojson</code>. <a href="https://github.com/umap-project/umap/blob/c16a01778b4686a562d97fde1cfd3433777d7590/umap/models.py#L426-L433">A history of the last changes is preserved</a> (The default settings preserves the last 10 revisions).</li>
<li>The data is stored <a href="https://github.com/umap-project/umap/blob/c16a01778b4686a562d97fde1cfd3433777d7590/umap/static/umap/js/umap.js#L158-L163">in a Leaflet object</a> and <a href="https://github.com/umap-project/umap/blob/c16a01778b4686a562d97fde1cfd3433777d7590/umap/static/umap/js/umap.js#L1095:L1095">backups are made manually</a> (it does not seem that changes are saved automatically).</li>
</ul>
<h3 id="data">Data</h3>
<p>Each layer consists of:</p>
<ul>
<li>On one side are the properties (matching the <code>_umap_options</code>), and on the other, the geojson data (the Features key).</li>
<li>Each feature is composed of three keys:</li>
<li><strong>geometry</strong>: the actual geo object</li>
<li><strong>properties</strong>: the data associated with it</li>
<li><strong>style</strong>: just styling information which goes with it, if any.</li>
</ul>
<p><img alt="JSON representation of the umap options" src="/images/umap/umap-options.png">
<img alt="JSON representation of the umap features" src="/images/umap/umap-features.png"></p>
<h2 id="real-time-collaboration-the-different-approaches">Real-time collaboration : the different approaches</h2>
<p>Behind the “real-time collaboration” name, we have :</p>
<ol>
<li>The <strong>streaming of the changes to the clients</strong>: when you’re working with other persons on the same map, you can see their edits at the moment they happen.</li>
<li>The ability to handle <strong>concurrent changes</strong>: some changes can happen on the same data concurrently. In such a case, we need to merge them together and be able to </li>
<li><strong>Offline editing</strong>: in some cases, one needs to map data but doesn’t have access to a network. Changes happen on a local device and is then synced with other devices / the server ;</li>
</ol>
<p><em>Keep in mind these notes are just food for toughs, and that other approaches might be discovered on the way</em></p>
<p>I’ve tried to come up with the different approaches I can follow in order to add the collaboration
features we want.</p>
<ul>
<li><strong><span class="caps">JSON</span> Patch and <span class="caps">JSON</span> Merge Patch</strong>: Two specifications by the <span class="caps">IETF</span> which define a format for generating and using diffs on json files. In this scenario, we could send the diffs from the clients to the server, and let it merge everything.</li>
<li><strong>Using CRDTs</strong>: Conflict-Free Resolution Data Types are one of the other options we have lying around. The technology has been used mainly to solve concurrent editing on text documents (like <a href="https://github.com/ether/etherpad-lite">etherpad-lite</a>), but should work fine on trees.</li>
</ul>
<h3 id="json-patch-and-json-merge-patch"><span class="caps">JSON</span> Patch and <span class="caps">JSON</span> Merge Patch</h3>
<p>I’ve stumbled on two <span class="caps">IETF</span> specifications for <a href="https://datatracker.ietf.org/doc/html/rfc6902"><span class="caps">JSON</span> Patch</a> and <a href="https://datatracker.ietf.org/doc/html/rfc7396"><span class="caps">JSON</span> Merge Patch</a> which respectively define how <span class="caps">JSON</span> diffs could be defined and applied.</p>
<p>There are multiple libraries for this, and at least one for <a href="https://github.com/OpenDataServices/json-merge-patch">Python</a>, <a href="https://docs.rs/json-patch/latest/json_patch/">Rust</a> and <a href="https://www.npmjs.com/package/json-merge-patch"><span class="caps">JS</span></a>.</p>
<p>It’s even <a href="https://redis.io/commands/json.merge/">supported by the Redis database</a>, which might come handy in case we want to stream the changes with it.</p>
<p>If you’re making edits to the map without changing all the data all the time, it’s possible to generate diffs. For instance, let’s take this simplified data (it’s not valid geojson, but it should be enough for testing):</p>
<p>source.json</p>
<div class="highlight"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">"features"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">"key"</span><span class="p">:</span><span class="w"> </span><span class="s2">"value"</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">],</span>
<span class="w"> </span><span class="nt">"not_changed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"whatever"</span>
<span class="p">}</span>
</code></pre></div>
<p>And now let’s add a new object right after the first one :</p>
<p>destination.geojson</p>
<div class="highlight"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">"features"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">"key"</span><span class="p">:</span><span class="w"> </span><span class="s2">"value"</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">"key"</span><span class="p">:</span><span class="w"> </span><span class="s2">"another-value"</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">],</span>
<span class="w"> </span><span class="nt">"not_changed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"whatever"</span>
<span class="p">}</span>
</code></pre></div>
<p>If we generate a diff:</p>
<div class="highlight"><pre><span></span><code><span class="n">pipx</span> <span class="n">install</span> <span class="n">json</span><span class="o">-</span><span class="n">merge</span><span class="o">-</span><span class="n">patch</span>
<span class="n">json</span><span class="o">-</span><span class="n">merge</span><span class="o">-</span><span class="n">patch</span> <span class="n">create</span><span class="o">-</span><span class="n">patch</span> <span class="n">source</span><span class="o">.</span><span class="n">json</span> <span class="n">destination</span><span class="o">.</span><span class="n">json</span>
<span class="p">{</span>
<span class="s2">"features"</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="s2">"key"</span><span class="p">:</span> <span class="s2">"value"</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="s2">"key"</span><span class="p">:</span> <span class="s2">"another-value"</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">}</span>
</code></pre></div>
<p>Multiple things to note here:</p>
<ol>
<li>It’s a valid <span class="caps">JSON</span> object</li>
<li>It doesn’t reproduce the <code>not_changed</code> key</li>
<li>But… I was expecting to see only the new item to show up. Instead, we are getting two items here, because it’s replacing the “features” key with everything inside.</li>
</ol>
<p>This is actually what <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.1">the specification defines</a>:</p>
<blockquote>
<p>4.1. add</p>
<p>The “add” operation performs one of the following functions,
depending upon what the target location references:</p>
<p>o If the target location specifies an array index, a new value is
inserted into the array at the specified index.</p>
<p>o If the target location specifies an object member that does not
already exist, a new member is added to the object</p>
<p>o <strong>If the target location specifies an object member that does exist,
that member’s value is replaced.</strong></p>
</blockquote>
<p>It seems too bad for us, as this will happen each time a new feature is added to the feature collection.</p>
<p>It’s not working out of the box, but we could probably hack something together by having all features defined by a unique id, and send this to the server. We wouldn’t be using vanilla <code>geojson</code> files though, but adding some complexity on top of it.</p>
<p>At this point, I’ve left this here and went to experiment with the other ideas. After all, the goal here is not (yet) to have something functional, but to clarify how the different options would play off.</p>
<h3 id="using-crdts">Using CRDTs</h3>
<p>I’ve had a look at the two main CRDTs implementation that seem to get traction these days : <a href="https://automerge.org/">Automerge</a> and <a href="https://github.com/yjs/yjs">Yjs</a>.</p>
<p>I’ve first tried to make Automerge work with Python, but the <a href="https://github.com/automerge/automerge-py">Automerge-py</a> repository is outdated now and won’t build. I realized at this point that we might not even need a python implementation: </p>
<p>In this scenario, the server could just stream the changes from one client to the other, and the <span class="caps">CRDT</span> will guarantee that the structures will be similar on both clients. It’s handy because it means we won’t have to implement the <span class="caps">CRDT</span> logic on the server side. </p>
<p>Let’s do some JavaScript, then. A simple Leaflet map would look like this:</p>
<div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="nx">L</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'leaflet'</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="s1">'leaflet/dist/leaflet.css'</span><span class="p">;</span>
<span class="c1">// Initialize the map and set its view to our chosen geographical coordinates and a zoom level:</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">map</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="s1">'map'</span><span class="p">).</span><span class="nx">setView</span><span class="p">([</span><span class="mf">48.1173</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mf">1.6778</span><span class="p">],</span><span class="w"> </span><span class="mf">13</span><span class="p">);</span>
<span class="c1">// Add a tile layer to add to our map, in this case using Open Street Map</span>
<span class="nx">L</span><span class="p">.</span><span class="nx">tileLayer</span><span class="p">(</span><span class="s1">'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">maxZoom</span><span class="o">:</span><span class="w"> </span><span class="kt">19</span><span class="p">,</span>
<span class="w"> </span><span class="nx">attribution</span><span class="o">:</span><span class="w"> </span><span class="s1">'© OpenStreetMap contributors'</span>
<span class="p">}).</span><span class="nx">addTo</span><span class="p">(</span><span class="nx">map</span><span class="p">);</span>
<span class="c1">// Initialize a GeoJSON layer and add it to the map</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">geojsonFeature</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"type"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Feature"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"properties"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"name"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Initial Feature"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"popupContent"</span><span class="o">:</span><span class="w"> </span><span class="s2">"This is where the journey begins!"</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="s2">"geometry"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"type"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Point"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"coordinates"</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="o">-</span><span class="mf">0.09</span><span class="p">,</span><span class="w"> </span><span class="mf">51.505</span><span class="p">]</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">};</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">geojsonLayer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">geoJSON</span><span class="p">(</span><span class="nx">geojsonFeature</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">onEachFeature</span><span class="o">:</span><span class="w"> </span><span class="kt">function</span><span class="w"> </span><span class="p">(</span><span class="nx">feature</span><span class="p">,</span><span class="w"> </span><span class="nx">layer</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">feature</span><span class="p">.</span><span class="nx">properties</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nx">feature</span><span class="p">.</span><span class="nx">properties</span><span class="p">.</span><span class="nx">popupContent</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">layer</span><span class="p">.</span><span class="nx">bindPopup</span><span class="p">(</span><span class="nx">feature</span><span class="p">.</span><span class="nx">properties</span><span class="p">.</span><span class="nx">popupContent</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}).</span><span class="nx">addTo</span><span class="p">(</span><span class="nx">map</span><span class="p">);</span>
<span class="c1">// Add new features to the map with a click</span>
<span class="kd">function</span><span class="w"> </span><span class="nx">onMapClick</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">newFeature</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"type"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Feature"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"properties"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"name"</span><span class="o">:</span><span class="w"> </span><span class="s2">"New Feature"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"popupContent"</span><span class="o">:</span><span class="w"> </span><span class="s2">"You clicked the map at "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">e</span><span class="p">.</span><span class="nx">latlng</span><span class="p">.</span><span class="nx">toString</span><span class="p">()</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="s2">"geometry"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"type"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Point"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"coordinates"</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="nx">e</span><span class="p">.</span><span class="nx">latlng</span><span class="p">.</span><span class="nx">lng</span><span class="p">,</span><span class="w"> </span><span class="nx">e</span><span class="p">.</span><span class="nx">latlng</span><span class="p">.</span><span class="nx">lat</span><span class="p">]</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">};</span>
<span class="w"> </span><span class="c1">// Add the new feature to the geojson layer</span>
<span class="w"> </span><span class="nx">geojsonLayer</span><span class="p">.</span><span class="nx">addData</span><span class="p">(</span><span class="nx">newFeature</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">map</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">'click'</span><span class="p">,</span><span class="w"> </span><span class="nx">onMapClick</span><span class="p">);</span>
</code></pre></div>
<p>Nothing fancy here, just a map which adds markers when you click. Now let’s add automerge:</p>
<p>We add a bunch of imports, the goal here will be to sync between tabs of the same browser. Automerge <a href="https://automerge.org/blog/2023/11/06/automerge-repo/">announced an automerge-repo</a> library to help with all the wiring-up, so let’s try it out!</p>
<div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">DocHandle</span><span class="p">,</span><span class="w"> </span><span class="nx">isValidAutomergeUrl</span><span class="p">,</span><span class="w"> </span><span class="nx">Repo</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'@automerge/automerge-repo'</span>
<span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">BroadcastChannelNetworkAdapter</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'@automerge/automerge-repo-network-broadcastchannel'</span>
<span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">IndexedDBStorageAdapter</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"@automerge/automerge-repo-storage-indexeddb"</span>
<span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">v4</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">uuidv4</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'uuid'</span><span class="p">;</span>
</code></pre></div>
<p>These were just import. Don’t bother too much. The next section does the following:</p>
<ul>
<li>Instantiate an “automerge repo”, which helps to send the right messages to the other peers if needed ;</li>
<li>Add a mechanism to create and initialize a repository if needed,</li>
<li>or otherwise look for an existing one, based on a hash passed in the <span class="caps">URI</span>.</li>
</ul>
<div class="highlight"><pre><span></span><code><span class="c1">// Add an automerge repository. Sync to </span>
<span class="kd">const</span><span class="w"> </span><span class="nx">repo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">Repo</span><span class="p">({</span>
<span class="w"> </span><span class="nx">network</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="ow">new</span><span class="w"> </span><span class="nx">BroadcastChannelNetworkAdapter</span><span class="p">()],</span>
<span class="w"> </span><span class="nx">storage</span><span class="o">:</span><span class="w"> </span><span class="kt">new</span><span class="w"> </span><span class="nx">IndexedDBStorageAdapter</span><span class="p">(),</span>
<span class="p">});</span>
<span class="c1">// Automerge-repo exposes an handle, which is mainly a wrapper around the library internals.</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">handle</span><span class="o">:</span><span class="w"> </span><span class="kt">DocHandle</span><span class="o"><</span><span class="nx">unknown</span><span class="o">></span>
<span class="kd">const</span><span class="w"> </span><span class="nx">rootDocUrl</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nb">document</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">hash</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mf">1</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span>
<span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">isValidAutomergeUrl</span><span class="p">(</span><span class="nx">rootDocUrl</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">handle</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">repo</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">rootDocUrl</span><span class="p">);</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">doc</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">handle</span><span class="p">.</span><span class="nx">doc</span><span class="p">();</span>
<span class="w"> </span><span class="c1">// Once we've found the data in the browser, let's add the features to the geojson layer.</span>
<span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">values</span><span class="p">(</span><span class="nx">doc</span><span class="p">.</span><span class="nx">features</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">feature</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">geojsonLayer</span><span class="p">.</span><span class="nx">addData</span><span class="p">(</span><span class="nx">feature</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">handle</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">repo</span><span class="p">.</span><span class="nx">create</span><span class="p">()</span>
<span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">handle</span><span class="p">.</span><span class="nx">doc</span><span class="p">();</span>
<span class="w"> </span><span class="nx">handle</span><span class="p">.</span><span class="nx">change</span><span class="p">(</span><span class="nx">doc</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">doc</span><span class="p">.</span><span class="nx">features</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{});</span>
<span class="p">}</span>
</code></pre></div>
<p>Let’s change the <code>onMapClick</code> function:</p>
<div class="highlight"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">onMapClick</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">uuid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">uuidv4</span><span class="p">();</span>
<span class="w"> </span><span class="c1">// ... What was there previously</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">newFeature</span><span class="p">[</span><span class="s2">"properties"</span><span class="p">][</span><span class="s2">"id"</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">uuid</span><span class="p">;</span>
<span class="w"> </span><span class="c1">// Add the new feature to the geojson layer.</span>
<span class="w"> </span><span class="c1">// Here we use the handle to do the change.</span>
<span class="w"> </span><span class="nx">handle</span><span class="p">.</span><span class="nx">change</span><span class="p">(</span><span class="nx">doc</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">doc</span><span class="p">.</span><span class="nx">features</span><span class="p">[</span><span class="nx">uuid</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">newFeature</span><span class="p">});</span>
<span class="p">}</span>
</code></pre></div>
<p>And on the other side of the logic, let’s listen to the changes:</p>
<div class="highlight"><pre><span></span><code><span class="nx">handle</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">"change"</span><span class="p">,</span><span class="w"> </span><span class="p">({</span><span class="nx">doc</span><span class="p">,</span><span class="w"> </span><span class="nx">patches</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// "patches" is a list of all the changes that happened to the tree.</span>
<span class="w"> </span><span class="c1">// Because we're sending JS objects, a lot of patches events are being sent.</span>
<span class="w"> </span><span class="c1">// </span>
<span class="w"> </span><span class="c1">// Filter to only keep first-level events (we currently don't want to reflect</span>
<span class="w"> </span><span class="c1">// changes down the tree — yet)</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"patches"</span><span class="p">,</span><span class="w"> </span><span class="nx">patches</span><span class="p">);</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">inserted</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">patches</span><span class="p">.</span><span class="nx">filter</span><span class="p">(({</span><span class="nx">path</span><span class="p">,</span><span class="w"> </span><span class="nx">action</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span><span class="nx">path</span><span class="p">[</span><span class="mf">0</span><span class="p">]</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"features"</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nx">path</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mf">2</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nx">action</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"put"</span><span class="p">)</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="nx">inserted</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(({</span><span class="nx">path</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">uuid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">path</span><span class="p">[</span><span class="mf">1</span><span class="p">];</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">newFeature</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">doc</span><span class="p">.</span><span class="nx">features</span><span class="p">[</span><span class="nx">uuid</span><span class="p">];</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Adding a new feature at position </span><span class="si">${</span><span class="nx">uuid</span><span class="si">}</span><span class="sb">`</span><span class="p">)</span>
<span class="w"> </span><span class="nx">geojsonLayer</span><span class="p">.</span><span class="nx">addData</span><span class="p">(</span><span class="nx">newFeature</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="p">});</span>
</code></pre></div>
<p>And… It’s working, here is a little video capture of two tabs working together :-)</p>
<video controls preload="none" width="100%"
poster="https://nuage.b.delire.party/s/kpP9ijfqabmKxnr">
<source src="https://nuage.b.delire.party/s/kpP9ijfqabmKxnr/download"
type="video/mp4">
</video>
<p>It’s very rough, but the point was mainly to see how the library can be used, and what the <span class="caps">API</span> looks like. I’ve found that :</p>
<ul>
<li>The <code>patches</code> object that’s being sent to the <code>handle.on</code> subscribers is very chatty: it contains all the changes, and I have to filter it to get what I want.</li>
<li>I was expecting the objects to be sent on one go, but it’s creating an operation for each change. For instance, setting a new object to a key will result in multiple events, as it will firstly create the object, and the populate it.</li>
<li>Here I need to keep track of all the edits, but I’m not sure how that will work out with for instance the offline use-case (or with limited connectivity). That’s what I’m going to find out next week, I guess :-)</li>
<li>The team behind Automerge is very welcoming, and was prompt to answer me when needed.</li>
<li>There seem to be another <span class="caps">API</span> <code>Automerge.getHistory()</code>, and <code>Automerge.diff()</code> to get a patch between the different docs, which might prove more helpful than getting all the small patches.</li>
</ul>
<p>We’ll figure that out next week, I guess!</p>Using Datasette for tracking my professional activity2023-11-11T00:00:00+01:002023-11-11T00:00:00+01:00tag:blog.notmyidea.org,2023-11-11:/using-datasette-for-tracking-my-professional-activity.html<p>I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project <a href="https://datasette.io">Datasette</a> before.</p>
<p>As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …</p><p>I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project <a href="https://datasette.io">Datasette</a> before.</p>
<p>As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time I’ve worked on such and such project. A discussion with <a href="https://thom4.net/">Thomas</a> made me realize it would be nice to track all this in a spreadsheet of some sort, which I was doing until today.</p>
<p>Spreadsheets are nice, but they don’t play well with rich content, and doing graphs with them is kind of tricky. So I went ahead and setup everything in Datasette.</p>
<p>First of all, I’ve imported my <code>.csv</code> file into a sqlite database: </p>
<div class="highlight"><pre><span></span><code>sqlite3<span class="w"> </span>-csv<span class="w"> </span>-header<span class="w"> </span>db.sqlite<span class="w"> </span><span class="s2">".import journal.csv journal"</span>
</code></pre></div>
<p>Then, I used <a href="https://sqlite-utils.datasette.io/en/stable/">sqlite-utils</a> to do some tidying and changed the columns names:</p>
<div class="highlight"><pre><span></span><code><span class="c1"># Rename a column</span>
sqlite-utils<span class="w"> </span>transform<span class="w"> </span>journal<span class="w"> </span>--rename<span class="w"> </span><span class="s2">"quoi ?"</span><span class="w"> </span>content
<span class="c1"># Make everything look similar</span>
sqlite-utils<span class="w"> </span>update<span class="w"> </span>db.sqlite<span class="w"> </span>journal<span class="w"> </span>project<span class="w"> </span><span class="s1">'value.replace("Umap", "uMap")'</span>
</code></pre></div>
<p>Here is my database schema:</p>
<div class="highlight"><pre><span></span><code>sqlite-utils<span class="w"> </span>schema<span class="w"> </span>db.sqlite
CREATE<span class="w"> </span>TABLE<span class="w"> </span><span class="s2">"journal"</span><span class="w"> </span><span class="o">(</span>
<span class="w"> </span><span class="o">[</span>date<span class="o">]</span><span class="w"> </span>TEXT,
<span class="w"> </span><span class="o">[</span>project<span class="o">]</span><span class="w"> </span>TEXT,
<span class="w"> </span><span class="o">[</span>duration<span class="o">]</span><span class="w"> </span>TEXT,
<span class="w"> </span><span class="o">[</span>where<span class="o">]</span><span class="w"> </span>TEXT,
<span class="w"> </span><span class="o">[</span>content<span class="o">]</span><span class="w"> </span>TEXT,
<span class="w"> </span><span class="o">[</span>paid_work<span class="o">]</span><span class="w"> </span>INTEGER
<span class="o">)</span><span class="p">;</span>
</code></pre></div>
<p>And then installed datasette, with a few plugins, and ran it:</p>
<div class="highlight"><pre><span></span><code>pipx<span class="w"> </span>install<span class="w"> </span>datasette
datasette<span class="w"> </span>install<span class="w"> </span>datasette-render-markdown<span class="w"> </span>datasette-write-ui<span class="w"> </span>datasette-dashboards<span class="w"> </span>datasette-dateutil
</code></pre></div>
<p>I then came up with a few <span class="caps">SQL</span> queries which are useful:</p>
<p>How much I’ve worked per project:</p>
<div class="highlight"><pre><span></span><code><span class="n">sqlite</span><span class="o">-</span><span class="n">utils</span><span class="w"> </span><span class="n">db</span><span class="p">.</span><span class="n">sqlite</span><span class="w"> </span><span class="ss">"SELECT project, SUM(CAST(duration AS REAL)) as total_duration FROM journal GROUP BY project;"</span>
<span class="p">[</span><span class="err">{</span><span class="ss">"project"</span><span class="p">:</span><span class="w"> </span><span class="ss">"Argos"</span><span class="p">,</span><span class="w"> </span><span class="ss">"total_duration"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"project"</span><span class="p">:</span><span class="w"> </span><span class="ss">"IDLV"</span><span class="p">,</span><span class="w"> </span><span class="ss">"total_duration"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"project"</span><span class="p">:</span><span class="w"> </span><span class="ss">"Notmyidea"</span><span class="p">,</span><span class="w"> </span><span class="ss">"total_duration"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"project"</span><span class="p">:</span><span class="w"> </span><span class="ss">"Sam"</span><span class="p">,</span><span class="w"> </span><span class="ss">"total_duration"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"project"</span><span class="p">:</span><span class="w"> </span><span class="ss">"uMap"</span><span class="p">,</span><span class="w"> </span><span class="ss">"total_duration"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">]</span>
</code></pre></div>
<p>How much I’ve worked per week, in total (I’ve redacted the results for privacy):</p>
<div class="highlight"><pre><span></span><code><span class="n">sqlite</span><span class="o">-</span><span class="n">utils</span><span class="w"> </span><span class="n">db</span><span class="p">.</span><span class="n">sqlite</span><span class="w"> </span><span class="ss">"SELECT strftime('%Y-W%W', date) AS week, SUM(CAST(duration AS REAL)) AS hours FROM journal GROUP BY week ORDER BY week;"</span>
<span class="p">[</span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W21"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W22"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W23"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W25"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W29"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W37"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W39"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W40"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W41"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W42"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W44"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">,</span>
<span class="w"> </span><span class="err">{</span><span class="ss">"week"</span><span class="p">:</span><span class="w"> </span><span class="ss">"2023-W45"</span><span class="p">,</span><span class="w"> </span><span class="ss">"hours"</span><span class="p">:</span><span class="w"> </span><span class="n">XX</span><span class="err">}</span><span class="p">]</span>
</code></pre></div>
<p>I then created a quick dashboard using <a href="https://github.com/rclement/datasette-dashboards">datasette-dashboard</a>, which looks like this:</p>
<p><img alt="Capture d'écran du dashboard, heures par semaine" src="/images/datasette/hours-per-week.png">
<img alt="Capture d'écran du dashboard, heures par projet" src="/images/datasette/hours-per-project.png"></p>
<p>Using this configuration:</p>
<div class="highlight"><pre><span></span><code><span class="nt">plugins</span><span class="p">:</span>
<span class="w"> </span><span class="nt">datasette-render-markdown</span><span class="p">:</span>
<span class="w"> </span><span class="nt">columns</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">"content"</span>
<span class="w"> </span><span class="nt">datasette-dashboards</span><span class="p">:</span>
<span class="w"> </span><span class="nt">my-dashboard</span><span class="p">:</span>
<span class="w"> </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Notmyidea</span>
<span class="w"> </span><span class="nt">filters</span><span class="p">:</span>
<span class="w"> </span><span class="nt">project</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Projet</span>
<span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">select</span>
<span class="w"> </span><span class="nt">db</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">db</span>
<span class="w"> </span><span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">SELECT DISTINCT project FROM journal WHERE project IS NOT NULL ORDER BY project ASC</span>
<span class="w"> </span><span class="nt">layout</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="p p-Indicator">[</span><span class="nv">hours-per-project</span><span class="p p-Indicator">]</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="p p-Indicator">[</span><span class="nv">entries</span><span class="p p-Indicator">]</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="p p-Indicator">[</span><span class="nv">hours-per-week</span><span class="p p-Indicator">]</span>
<span class="w"> </span><span class="nt">charts</span><span class="p">:</span>
<span class="w"> </span><span class="nt">hours-per-project</span><span class="p">:</span>
<span class="w"> </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Nombre d'heures par projet</span>
<span class="w"> </span><span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">SELECT project, SUM(CAST(duration AS REAL)) as total FROM journal GROUP BY project;</span>
<span class="w"> </span><span class="nt">db</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">db</span>
<span class="w"> </span><span class="nt">library</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">vega-lite</span>
<span class="w"> </span><span class="nt">display</span><span class="p">:</span>
<span class="w"> </span><span class="nt">mark</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">{</span><span class="nt"> type</span><span class="p">:</span><span class="w"> </span><span class="nv">arc</span><span class="p p-Indicator">,</span><span class="nt"> tooltip</span><span class="p">:</span><span class="w"> </span><span class="nv">true</span><span class="w"> </span><span class="p p-Indicator">}</span>
<span class="w"> </span><span class="nt">encoding</span><span class="p">:</span>
<span class="w"> </span><span class="nt">color</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">{</span><span class="nt"> field</span><span class="p">:</span><span class="w"> </span><span class="nv">project</span><span class="p p-Indicator">,</span><span class="nt"> type</span><span class="p">:</span><span class="w"> </span><span class="nv">nominal</span><span class="w"> </span><span class="p p-Indicator">}</span>
<span class="w"> </span><span class="nt">theta</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">{</span><span class="nt"> field</span><span class="p">:</span><span class="w"> </span><span class="nv">total</span><span class="p p-Indicator">,</span><span class="nt"> type</span><span class="p">:</span><span class="w"> </span><span class="nv">quantitative</span><span class="w"> </span><span class="p p-Indicator">}</span>
<span class="w"> </span><span class="nt">hours-per-week</span><span class="p">:</span>
<span class="w"> </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Heures par semaine</span>
<span class="w"> </span><span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">SELECT strftime('%Y-W%W', date) AS week, SUM(CAST(duration AS REAL)) AS hours FROM journal GROUP BY week ORDER BY week;</span>
<span class="w"> </span><span class="nt">db</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">db</span>
<span class="w"> </span><span class="nt">library</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">vega-lite</span>
<span class="w"> </span><span class="nt">display</span><span class="p">:</span>
<span class="w"> </span><span class="nt">mark</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">{</span><span class="nt"> type</span><span class="p">:</span><span class="w"> </span><span class="nv">bar</span><span class="p p-Indicator">,</span><span class="nt"> tooltip</span><span class="p">:</span><span class="w"> </span><span class="nv">true</span><span class="w"> </span><span class="p p-Indicator">}</span>
<span class="w"> </span><span class="nt">encoding</span><span class="p">:</span>
<span class="w"> </span><span class="nt">x</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">{</span><span class="nt"> field</span><span class="p">:</span><span class="w"> </span><span class="nv">week</span><span class="p p-Indicator">,</span><span class="nt"> type</span><span class="p">:</span><span class="w"> </span><span class="nv">ordinal</span><span class="p p-Indicator">}</span>
<span class="w"> </span><span class="nt">y</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">{</span><span class="nt"> field</span><span class="p">:</span><span class="w"> </span><span class="nv">hours</span><span class="p p-Indicator">,</span><span class="nt"> type</span><span class="p">:</span><span class="w"> </span><span class="nv">quantitative</span><span class="w"> </span><span class="p p-Indicator">}</span>
<span class="w"> </span><span class="nt">entries</span><span class="p">:</span>
<span class="w"> </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Journal</span>
<span class="w"> </span><span class="nt">db</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">db</span>
<span class="w"> </span><span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">SELECT * FROM journal WHERE TRUE [[ AND project = :project ]] ORDER BY date DESC</span>
<span class="w"> </span><span class="nt">library</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">table</span>
<span class="w"> </span><span class="nt">display</span><span class="p">:</span>
</code></pre></div>
<p>And ran datasette with:</p>
<div class="highlight"><pre><span></span><code>datasette<span class="w"> </span>db.sqlite<span class="w"> </span>--root<span class="w"> </span>--metadata<span class="w"> </span>metadata.yaml
</code></pre></div>Using DISTINCT in Parent-Child Relationships2023-10-18T00:00:00+02:002023-10-18T00:00:00+02:00tag:blog.notmyidea.org,2023-10-18:/using-distinct-in-parent-child-relationships.html<p>Let’s say you have a model defined like this, with a Parent and a Child table:</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">Parent</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
<span class="n">__tablename__</span> <span class="o">=</span> <span class="s2">"parent"</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">childs</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="s2">"Child"</span><span class="p">]]</span> <span class="o">=</span> <span class="n">relationship</span><span class="p">(</span><span class="n">back_populates</span><span class="o">=</span><span class="s2">"parent"</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Child</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
<span class="n">__tablename__</span> <span class="o">=</span> <span class="s2">"child"</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">parent_id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column …</span></code></pre></div><p>Let’s say you have a model defined like this, with a Parent and a Child table:</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">Parent</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
<span class="n">__tablename__</span> <span class="o">=</span> <span class="s2">"parent"</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">childs</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="s2">"Child"</span><span class="p">]]</span> <span class="o">=</span> <span class="n">relationship</span><span class="p">(</span><span class="n">back_populates</span><span class="o">=</span><span class="s2">"parent"</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Child</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
<span class="n">__tablename__</span> <span class="o">=</span> <span class="s2">"child"</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">parent_id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s2">"parent.id"</span><span class="p">))</span>
<span class="n">parent</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="s2">"Parent"</span><span class="p">]</span> <span class="o">=</span> <span class="n">relationship</span><span class="p">(</span><span class="n">back_populates</span><span class="o">=</span><span class="s2">"children"</span><span class="p">)</span>
<span class="n">born_at</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="n">datetime</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">()</span>
</code></pre></div>
<p>I’ve tried many ways, with complex subqueries and the like, before finding out <a href="https://www.sqlitetutorial.net/sqlite-distinct/">the <span class="caps">DISTINCT</span> <span class="caps">SQL</span> statement</a>.</p>
<p>So, if you want to retrieve the parent with it’s more recent child, you can do it like this:</p>
<div class="highlight"><pre><span></span><code><span class="n">results</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">db</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">Parent</span><span class="p">,</span> <span class="n">Child</span><span class="p">)</span>
<span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">Child</span><span class="p">)</span>
<span class="o">.</span><span class="n">distinct</span><span class="p">(</span><span class="n">Parent</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
<span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="n">Parent</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="n">desc</span><span class="p">(</span><span class="n">Child</span><span class="o">.</span><span class="n">born_at</span><span class="p">))</span>
<span class="o">.</span><span class="n">all</span><span class="p">()</span>
<span class="p">)</span>
</code></pre></div>Convert string to duration2023-10-11T00:00:00+02:002023-10-11T00:00:00+02:00tag:blog.notmyidea.org,2023-10-11:/convert-string-to-duration.html<p>I found myself wanting to convert a string to a duration (int), for some configuration.</p>
<p>Something you can call like this:</p>
<div class="highlight"><pre><span></span><code><span class="n">string_to_duration</span><span class="p">(</span><span class="s2">"1d"</span><span class="p">,</span> <span class="n">target</span><span class="o">=</span><span class="s2">"days"</span><span class="p">)</span>
<span class="n">string_to_duration</span><span class="p">(</span><span class="s2">"1d"</span><span class="p">,</span> <span class="n">target</span><span class="o">=</span><span class="s2">"hours"</span><span class="p">)</span>
<span class="n">string_to_duration</span><span class="p">(</span><span class="s2">"3m"</span><span class="p">,</span> <span class="n">target</span><span class="o">=</span><span class="s2">"hours"</span><span class="p">)</span>
<span class="n">string_to_duration</span><span class="p">(</span><span class="s2">"3m"</span><span class="p">,</span> <span class="n">target</span><span class="o">=</span><span class="s2">"minutes"</span><span class="p">)</span>
</code></pre></div>
<p>The code :</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Literal</span>
<span class="k">def</span> <span class="nf">string_to_duration</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">target</span><span class="p">:</span> <span class="n">Literal</span><span class="p">[</span><span class="s2">"days …</span></code></pre></div><p>I found myself wanting to convert a string to a duration (int), for some configuration.</p>
<p>Something you can call like this:</p>
<div class="highlight"><pre><span></span><code><span class="n">string_to_duration</span><span class="p">(</span><span class="s2">"1d"</span><span class="p">,</span> <span class="n">target</span><span class="o">=</span><span class="s2">"days"</span><span class="p">)</span>
<span class="n">string_to_duration</span><span class="p">(</span><span class="s2">"1d"</span><span class="p">,</span> <span class="n">target</span><span class="o">=</span><span class="s2">"hours"</span><span class="p">)</span>
<span class="n">string_to_duration</span><span class="p">(</span><span class="s2">"3m"</span><span class="p">,</span> <span class="n">target</span><span class="o">=</span><span class="s2">"hours"</span><span class="p">)</span>
<span class="n">string_to_duration</span><span class="p">(</span><span class="s2">"3m"</span><span class="p">,</span> <span class="n">target</span><span class="o">=</span><span class="s2">"minutes"</span><span class="p">)</span>
</code></pre></div>
<p>The code :</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Literal</span>
<span class="k">def</span> <span class="nf">string_to_duration</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">target</span><span class="p">:</span> <span class="n">Literal</span><span class="p">[</span><span class="s2">"days"</span><span class="p">,</span> <span class="s2">"hours"</span><span class="p">,</span> <span class="s2">"minutes"</span><span class="p">]):</span>
<span class="w"> </span><span class="sd">"""Convert a string to a number of hours, days or minutes"""</span>
<span class="n">num</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="s2">""</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">filter</span><span class="p">(</span><span class="nb">str</span><span class="o">.</span><span class="n">isdigit</span><span class="p">,</span> <span class="n">value</span><span class="p">)))</span>
<span class="c1"># It's not possible to convert from a smaller unit to a greater one:</span>
<span class="c1"># - hours and minutes cannot be converted to days</span>
<span class="c1"># - minutes cannot be converted to hours</span>
<span class="k">if</span> <span class="p">(</span><span class="n">target</span> <span class="o">==</span> <span class="s2">"days"</span> <span class="ow">and</span> <span class="p">(</span><span class="s2">"h"</span> <span class="ow">in</span> <span class="n">value</span> <span class="ow">or</span> <span class="s2">"m"</span> <span class="ow">in</span> <span class="n">value</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"mo"</span><span class="p">,</span> <span class="s2">""</span><span class="p">)))</span> <span class="ow">or</span> <span class="p">(</span>
<span class="n">target</span> <span class="o">==</span> <span class="s2">"hours"</span> <span class="ow">and</span> <span class="s2">"m"</span> <span class="ow">in</span> <span class="n">value</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"mo"</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
<span class="p">):</span>
<span class="n">msg</span> <span class="o">=</span> <span class="p">(</span>
<span class="s2">"Durations cannot be converted from a smaller to a greater unit. "</span>
<span class="sa">f</span><span class="s2">"(trying to convert '</span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">' to </span><span class="si">{</span><span class="n">target</span><span class="si">}</span><span class="s2">)"</span>
<span class="p">)</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
<span class="c1"># Consider we're converting to minutes, do the eventual multiplication at the end.</span>
<span class="k">if</span> <span class="s2">"h"</span> <span class="ow">in</span> <span class="n">value</span><span class="p">:</span>
<span class="n">num</span> <span class="o">=</span> <span class="n">num</span> <span class="o">*</span> <span class="mi">60</span>
<span class="k">elif</span> <span class="s2">"d"</span> <span class="ow">in</span> <span class="n">value</span><span class="p">:</span>
<span class="n">num</span> <span class="o">=</span> <span class="n">num</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">24</span>
<span class="k">elif</span> <span class="s2">"w"</span> <span class="ow">in</span> <span class="n">value</span><span class="p">:</span>
<span class="n">num</span> <span class="o">=</span> <span class="n">num</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">7</span>
<span class="k">elif</span> <span class="s2">"mo"</span> <span class="ow">in</span> <span class="n">value</span><span class="p">:</span>
<span class="n">num</span> <span class="o">=</span> <span class="n">num</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">30</span> <span class="c1"># considers 30d in a month</span>
<span class="k">elif</span> <span class="s2">"y"</span> <span class="ow">in</span> <span class="n">value</span><span class="p">:</span>
<span class="n">num</span> <span class="o">=</span> <span class="n">num</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">365</span>
<span class="k">elif</span> <span class="s2">"m"</span> <span class="ow">in</span> <span class="n">value</span><span class="p">:</span>
<span class="n">num</span> <span class="o">=</span> <span class="n">num</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Invalid duration value"</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
<span class="k">if</span> <span class="n">target</span> <span class="o">==</span> <span class="s2">"hours"</span><span class="p">:</span>
<span class="n">num</span> <span class="o">=</span> <span class="n">num</span> <span class="o">/</span> <span class="mi">60</span>
<span class="k">elif</span> <span class="n">target</span> <span class="o">==</span> <span class="s2">"days"</span><span class="p">:</span>
<span class="n">num</span> <span class="o">=</span> <span class="n">num</span> <span class="o">/</span> <span class="mi">60</span> <span class="o">/</span> <span class="mi">24</span>
<span class="k">return</span> <span class="n">num</span>
</code></pre></div>llm command-line tips2023-09-27T00:00:00+02:002023-09-27T00:00:00+02:00tag:blog.notmyidea.org,2023-09-27:/llm-command-line-tips.html<p>I’m using <a href="https://llm.datasette.io">llm</a> more and more, and today I had to find back prompts I used in the past. Here is a command I’ve been using, which allows me to filter the results based on what I want. It leverages <a href="https://sqlutils.datasette.io">sql-utils</a>, a cli tool which is able to …</p><p>I’m using <a href="https://llm.datasette.io">llm</a> more and more, and today I had to find back prompts I used in the past. Here is a command I’ve been using, which allows me to filter the results based on what I want. It leverages <a href="https://sqlutils.datasette.io">sql-utils</a>, a cli tool which is able to talk to a <span class="caps">SQLITE</span> database and answer in json, and <a href="https://github.com/jqlang/jq">jq</a> a command-line tool capable of doing requests for json.</p>
<p>All in all, it’s pretty satisfying to use. I finally got a simple way to query databases! I’m also using <a href="https://github.com/charmbracelet/glow">glow</a>, which is capable of transforming markdown into a better version on the terminal.</p>
<div class="highlight"><pre><span></span><code>sqlite-utils<span class="w"> </span><span class="s2">"</span><span class="k">$(</span>llm<span class="w"> </span>logs<span class="w"> </span>path<span class="k">)</span><span class="s2">"</span><span class="w"> </span><span class="s2">"SELECT * FROM responses WHERE prompt LIKE '%search%'"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'.[].response'</span><span class="w"> </span>-r<span class="w"> </span><span class="p">|</span><span class="w"> </span>glow
</code></pre></div>
<p>Which got me a colored response :-)</p>Setting up a IRC Bouncer with ZNC2023-09-27T00:00:00+02:002023-09-27T00:00:00+02:00tag:blog.notmyidea.org,2023-09-27:/setting-up-a-irc-bouncer-with-znc.html<p>It’s been a while since I’ve used <span class="caps">IRC</span>, but I needed to connect to it today to discuss around <a href="https://docs.peewee-orm.com">Peewee</a>.</p>
<p>The main issue with <span class="caps">IRC</span> is that you need to be connected to see the answer, and to get the context of the conversation. Unless… you set up …</p><p>It’s been a while since I’ve used <span class="caps">IRC</span>, but I needed to connect to it today to discuss around <a href="https://docs.peewee-orm.com">Peewee</a>.</p>
<p>The main issue with <span class="caps">IRC</span> is that you need to be connected to see the answer, and to get the context of the conversation. Unless… you set up a bouncer.</p>
<p>The bouncer is named <a href="https://znc.in"><span class="caps">ZNC</span></a>, and the <span class="caps">IRC</span> client I use is <a href="https://weechat.org">Weechat</a>.</p>
<p>So, that’s what I did:</p>
<h2 id="installation-of-znc">Installation of <span class="caps">ZNC</span></h2>
<div class="highlight"><pre><span></span><code>apt<span class="w"> </span>install<span class="w"> </span>znc
sudo<span class="w"> </span>-u<span class="w"> </span>_znc<span class="w"> </span>/usr/bin/znc<span class="w"> </span>--datadir<span class="o">=</span>/var/lib/znc<span class="w"> </span>--makeconf
sudo<span class="w"> </span>systemctl<span class="w"> </span><span class="nb">enable</span><span class="w"> </span>znc
</code></pre></div>
<p>You can answer the questions asked by <code>--makeconf</code>, it will generate you a configuration file like this (stored in <code>/var/lib/znc/configurations/znc.conf</code>):</p>
<div class="highlight"><pre><span></span><code>AnonIPLimit<span class="w"> </span>=<span class="w"> </span>10
AuthOnlyViaModule<span class="w"> </span>=<span class="w"> </span>false
ConfigWriteDelay<span class="w"> </span>=<span class="w"> </span>0
ConnectDelay<span class="w"> </span>=<span class="w"> </span>5
HideVersion<span class="w"> </span>=<span class="w"> </span>false
LoadModule<span class="w"> </span>=<span class="w"> </span>webadmin
MaxBufferSize<span class="w"> </span>=<span class="w"> </span>500
ProtectWebSessions<span class="w"> </span>=<span class="w"> </span>true
SSLCertFile<span class="w"> </span>=<span class="w"> </span>/var/lib/znc/znc.pem
SSLDHParamFile<span class="w"> </span>=<span class="w"> </span>/var/lib/znc/znc.pem
SSLKeyFile<span class="w"> </span>=<span class="w"> </span>/var/lib/znc/znc.pem
ServerThrottle<span class="w"> </span>=<span class="w"> </span>30
Version<span class="w"> </span>=<span class="w"> </span>1.8.2
<span class="nt"><Listener</span><span class="w"> </span><span class="err">listener0</span><span class="nt">></span>
<span class="w"> </span>AllowIRC<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span>AllowWeb<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span>IPv4<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span>IPv6<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span>Port<span class="w"> </span>=<span class="w"> </span>6697
<span class="w"> </span>SSL<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span>URIPrefix<span class="w"> </span>=<span class="w"> </span>/
<span class="nt"></Listener></span>
<span class="nt"><User</span><span class="w"> </span><span class="err">alexis</span><span class="nt">></span>
<span class="w"> </span>Admin<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span>Allow<span class="w"> </span>=<span class="w"> </span>*
<span class="w"> </span>AltNick<span class="w"> </span>=<span class="w"> </span>alexis_
<span class="w"> </span>AppendTimestamp<span class="w"> </span>=<span class="w"> </span>false
<span class="w"> </span>AuthOnlyViaModule<span class="w"> </span>=<span class="w"> </span>false
<span class="w"> </span>AutoClearChanBuffer<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span>AutoClearQueryBuffer<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span>BindHost<span class="w"> </span>=<span class="w"> </span>skate.notmyidea.org
<span class="w"> </span>ChanBufferSize<span class="w"> </span>=<span class="w"> </span>50
<span class="w"> </span>DenyLoadMod<span class="w"> </span>=<span class="w"> </span>false
<span class="w"> </span>DenySetBindHost<span class="w"> </span>=<span class="w"> </span>false
<span class="w"> </span>Ident<span class="w"> </span>=<span class="w"> </span>alexis
<span class="w"> </span>JoinTries<span class="w"> </span>=<span class="w"> </span>10
<span class="w"> </span>LoadModule<span class="w"> </span>=<span class="w"> </span>chansaver
<span class="w"> </span>LoadModule<span class="w"> </span>=<span class="w"> </span>controlpanel
<span class="w"> </span>MaxJoins<span class="w"> </span>=<span class="w"> </span>0
<span class="w"> </span>MaxNetworks<span class="w"> </span>=<span class="w"> </span>1
<span class="w"> </span>MaxQueryBuffers<span class="w"> </span>=<span class="w"> </span>50
<span class="w"> </span>MultiClients<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span>Nick<span class="w"> </span>=<span class="w"> </span>alexis
<span class="w"> </span>NoTrafficTimeout<span class="w"> </span>=<span class="w"> </span>180
<span class="w"> </span>PrependTimestamp<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span>QueryBufferSize<span class="w"> </span>=<span class="w"> </span>50
<span class="w"> </span>QuitMsg<span class="w"> </span>=<span class="w"> </span>See<span class="w"> </span>you<span class="w"> </span>:)
<span class="w"> </span>RealName<span class="w"> </span>=<span class="w"> </span>N/A
<span class="w"> </span>StatusPrefix<span class="w"> </span>=<span class="w"> </span>*
<span class="w"> </span>TimestampFormat<span class="w"> </span>=<span class="w"> </span>[%H:%M:%S]
<span class="w"> </span><span class="nt"><Network</span><span class="w"> </span><span class="err">liberachat</span><span class="nt">></span>
<span class="w"> </span>FloodBurst<span class="w"> </span>=<span class="w"> </span>9
<span class="w"> </span>FloodRate<span class="w"> </span>=<span class="w"> </span>2.00
<span class="w"> </span>IRCConnectEnabled<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span>JoinDelay<span class="w"> </span>=<span class="w"> </span>0
<span class="w"> </span>LoadModule<span class="w"> </span>=<span class="w"> </span>simple_away
<span class="w"> </span>RealName<span class="w"> </span>=<span class="w"> </span>N/A
<span class="w"> </span>Server<span class="w"> </span>=<span class="w"> </span>irc.libera.chat<span class="w"> </span>+6697
<span class="w"> </span>TrustAllCerts<span class="w"> </span>=<span class="w"> </span>false
<span class="w"> </span>TrustPKI<span class="w"> </span>=<span class="w"> </span>true
<span class="w"> </span><span class="nt"><Chan</span><span class="w"> </span><span class="err">#peewee</span><span class="nt">></span>
<span class="w"> </span><span class="nt"></Chan></span>
<span class="w"> </span><span class="nt"></Network></span>
<span class="w"> </span><span class="nt"><Pass</span><span class="w"> </span><span class="err">password</span><span class="nt">></span>
<span class="w"> </span>Hash<span class="w"> </span>=<span class="w"> </span>REDACTED
<span class="w"> </span>Method<span class="w"> </span>=<span class="w"> </span>SHA256
<span class="w"> </span>Salt<span class="w"> </span>=<span class="w"> </span>REDACTED
<span class="w"> </span><span class="nt"></Pass></span>
<span class="nt"></User></span>
</code></pre></div>
<p>You can access a web interface on the exposed port. I had to make a change in my Firefox configuration, in <code>about:config</code>, set <code>network.security.ports.banned.override</code> to <code>6697</code>, otherwise, Firefox prevents you from connecting to these ports (which might actually be a good idea).</p>
<h2 id="weechat-configuration">Weechat configuration</h2>
<p>Now, to use this in weechat, here are some useful commands. First, get the fingerprint of the <span class="caps">SSL</span> certificate generated on your server:</p>
<div class="highlight"><pre><span></span><code>cat<span class="w"> </span>/var/log/znc/znc.pem<span class="w"> </span><span class="p">|</span><span class="w"> </span>openssl<span class="w"> </span>x509<span class="w"> </span>-sha512<span class="w"> </span>-fingerprint<span class="w"> </span>-noout<span class="w"> </span><span class="p">|</span><span class="w"> </span>tr<span class="w"> </span>-d<span class="w"> </span><span class="s1">':'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>tr<span class="w"> </span><span class="s1">'A-Z'</span><span class="w"> </span><span class="s1">'a-z'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>cut<span class="w"> </span>-d<span class="w"> </span><span class="o">=</span><span class="w"> </span>-f<span class="w"> </span><span class="m">2</span>
</code></pre></div>
<p>Then, in weechat :</p>
<div class="highlight"><pre><span></span><code>/server add znc host/6697 -tls -username=<username> -password=<yourpass> -autoconnect
/set irc.server.znc.tls_fingerprint <fingerprint-goes-here>
/connect znc
</code></pre></div>
<p>And you should be all set!</p>
<p>Resources : <a href="https://wiki.znc.in/Weechat">The <span class="caps">ZNC</span> Wiki on Weechat</a> and the <a href="https://wiki.debian.org/ZNC">Debian page on <span class="caps">ZNC</span></a></p>How to run the vigogne model locally2023-09-22T00:00:00+02:002023-09-22T00:00:00+02:00tag:blog.notmyidea.org,2023-09-22:/how-to-run-the-vigogne-model-locally.html
<p><a href="https://github.com/bofenghuang/vigogne">Vigogne</a> is a <span class="caps">LLM</span> model based on <span class="caps">LLAMA2</span>, but trained with french data. As I’m working mostly in french, it might be useful. The current models that I can get locally are in english.</p>
<p>The information I’ve found online are scarse and not so easy to follow, so …</p>
<p><a href="https://github.com/bofenghuang/vigogne">Vigogne</a> is a <span class="caps">LLM</span> model based on <span class="caps">LLAMA2</span>, but trained with french data. As I’m working mostly in french, it might be useful. The current models that I can get locally are in english.</p>
<p>The information I’ve found online are scarse and not so easy to follow, so here is a step by step tutorial you can follow. I’m using <a href="https://pipenv.pypa.io/en/latest/">pipenv</a> almost everywhere now, it’s so easy :-)</p>
<div class="highlight"><pre><span></span><code>llm<span class="w"> </span>install<span class="w"> </span>-U<span class="w"> </span>llm-llama-cpp
wget<span class="w"> </span>https://huggingface.co/TheBloke/Vigogne-2-7B-Chat-GGUF/resolve/main/vigogne-2-7b-chat.Q4_K_M.gguf
llm<span class="w"> </span>llama-cpp<span class="w"> </span>add-model<span class="w"> </span>vigogne-2-7b-chat.Q4_K_M.gguf<span class="w"> </span>-a<span class="w"> </span>vigogne
llm<span class="w"> </span>models<span class="w"> </span>default<span class="w"> </span>vigogne
</code></pre></div>Creating a simple command line to post snippets on Gitlab2023-09-18T00:00:00+02:002023-09-18T00:00:00+02:00tag:blog.notmyidea.org,2023-09-18:/creating-a-simple-command-line-to-post-snippets-on-gitlab.html
<p>I’m trying to get away from Github, and one thing that I find useful is the <a href="https://gist.github.com">gist</a> utility they’re providing. Seems that gitlab provides a similar tool.</p>
<p>You can use it using <a href="https://python-gitlab.readthedocs.io/">python-gitlab</a>:</p>
<div class="highlight"><pre><span></span><code>pipx<span class="w"> </span>install<span class="w"> </span>python-gitlab
</code></pre></div>
<p>And then :</p>
<div class="highlight"><pre><span></span><code>gitlab<span class="w"> </span>snippet<span class="w"> </span>create<span class="w"> </span>--title<span class="o">=</span><span class="s2">"youpi"</span><span class="w"> </span>--file-name<span class="o">=</span><span class="s2">"snip.py"</span><span class="w"> </span>--content<span class="w"> </span>snip …</code></pre></div>
<p>I’m trying to get away from Github, and one thing that I find useful is the <a href="https://gist.github.com">gist</a> utility they’re providing. Seems that gitlab provides a similar tool.</p>
<p>You can use it using <a href="https://python-gitlab.readthedocs.io/">python-gitlab</a>:</p>
<div class="highlight"><pre><span></span><code>pipx<span class="w"> </span>install<span class="w"> </span>python-gitlab
</code></pre></div>
<p>And then :</p>
<div class="highlight"><pre><span></span><code>gitlab<span class="w"> </span>snippet<span class="w"> </span>create<span class="w"> </span>--title<span class="o">=</span><span class="s2">"youpi"</span><span class="w"> </span>--file-name<span class="o">=</span><span class="s2">"snip.py"</span><span class="w"> </span>--content<span class="w"> </span>snip.py<span class="w"> </span>--visibility<span class="o">=</span><span class="s2">"public"</span>
</code></pre></div>
<p>I now wanted a small bash script which will just get the name of the file and infer all the parameters. I asked <span class="caps">GPT</span>-4, and iterated on its answer.</p>
<p>Here’s the resulting bash script:</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/bin/bash</span>
<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span><span class="w"> </span><span class="o">]</span>
<span class="k">then</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"Please provide a filename"</span>
<span class="w"> </span><span class="nb">exit</span><span class="w"> </span><span class="m">1</span>
<span class="k">fi</span>
<span class="nv">file</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="nv">base</span><span class="o">=</span><span class="k">$(</span>basename<span class="w"> </span><span class="s2">"</span><span class="nv">$file</span><span class="s2">"</span><span class="k">)</span>
<span class="nv">title</span><span class="o">=</span><span class="s2">"</span><span class="nv">$base</span><span class="s2">"</span>
<span class="nv">visibility</span><span class="o">=</span><span class="s2">"public"</span>
<span class="c1"># Use `cat` to fetch the content of the file</span>
<span class="nv">content</span><span class="o">=</span><span class="k">$(</span>cat<span class="w"> </span><span class="s2">"</span><span class="nv">$file</span><span class="s2">"</span><span class="k">)</span>
<span class="nv">result</span><span class="o">=</span><span class="k">$(</span>gitlab<span class="w"> </span>snippet<span class="w"> </span>create<span class="w"> </span>--title<span class="o">=</span><span class="s2">"</span><span class="nv">$title</span><span class="s2">"</span><span class="w"> </span>--file-name<span class="o">=</span><span class="s2">"</span><span class="nv">$title</span><span class="s2">"</span><span class="w"> </span>--content<span class="o">=</span><span class="s2">"</span><span class="nv">$content</span><span class="s2">"</span><span class="w"> </span>--visibility<span class="o">=</span><span class="s2">"</span><span class="nv">$visibility</span><span class="s2">"</span><span class="k">)</span>
<span class="nv">id</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$result</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>awk<span class="w"> </span><span class="s1">'/id: / { print $2 }'</span><span class="k">)</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">"https://gitlab.com/-/snippets/</span><span class="nv">$id</span><span class="s2">"</span>
</code></pre></div>
<p>I can now do <code>snip README.md</code> and that will create the snippet for me :-)</p>Creating an online space to share markdown files2023-09-17T00:00:00+02:002023-09-17T00:00:00+02:00tag:blog.notmyidea.org,2023-09-17:/creating-an-online-space-to-share-markdown-files.html
<p>I wanted to create a space on my server where I can upload markdown files and have them rendered directly, for them to be shared with other people.</p>
<p>I stumbled on <a href="https://github.com/ukarim/ngx_markdown_filter_module">the markdown module for nginx</a> which does exactly what I want, but seemed to ask for compilation of nginx …</p>
<p>I wanted to create a space on my server where I can upload markdown files and have them rendered directly, for them to be shared with other people.</p>
<p>I stumbled on <a href="https://github.com/ukarim/ngx_markdown_filter_module">the markdown module for nginx</a> which does exactly what I want, but seemed to ask for compilation of nginx, which wasn’t exactly what I wanted in terms of maintainability (it would make it complicated to update it)</p>
<p>I then thought that the <a href="https://caddyserver.com/">Caddy</a> server does that by default, and so I’ve tested it out. Turns out it’s not, but it offers ways to do this thanks to its template mecanism.</p>
<p>It also, <a href="https://caddyserver.com/docs/automatic-https">setups automatically and transparently <span class="caps">SSL</span> certificates</a> for you (using Let’s Encrypt!), so I wanted to have a look.</p>
<p>Here is the Caddy configuration file I’m now using :</p>
<div class="highlight"><pre><span></span><code>md.notmyidea.org {
root <span class="gs">* /home/caddy/md.notmyidea.org</span>
<span class="gs"> rewrite *</span> /index.html
file_server
templates
encode zstd gzip
}
</code></pre></div>
<p>And the template:</p>
<div class="highlight"><pre><span></span><code>{{$pathParts := splitList "/" .OriginalReq.URL.Path}}
{{$markdownFilename := default "index" (slice $pathParts 1 | join "/")}}
{{if not (fileExists $markdownFilename)}}
{{httpError 404}}
{{end}}
{{$markdownFile := (include $markdownFilename | splitFrontMatter)}}
<span class="cp"><!DOCTYPE html></span>
<span class="p"><</span><span class="nt">html</span><span class="p">></span>
<span class="p"><</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">title</span><span class="p">></span>{{ $markdownFilename }}<span class="p"></</span><span class="nt">title</span><span class="p">></span>
<span class="p"></</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span><span class="p">></span>
{{ markdown $markdownFile.Body }}
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">></span>
</code></pre></div>
<p>This is a minimalistic version, but it works :-)</p>Conversion d’un fichier svg en favicon.ico2023-09-13T00:00:00+02:002023-09-13T00:00:00+02:00tag:blog.notmyidea.org,2023-09-13:/conversion-dun-fichier-svg-en-faviconico.html
<p>Il y a plusieurs sites qui permettent de faire ça automatiquement, mais j’aime bien faire les choses depuis mon terminal, voici donc une commande qui permet de faire ça simplement, en utilisant <a href="https://imagemagick.org/">ImageMagick</a>. Merci à <a href="https://gist.github.com/azam/3b6995a29b9f079282f3">ce gist</a></p>
<div class="highlight"><pre><span></span><code>convert<span class="w"> </span>-density<span class="w"> </span>256x256<span class="w"> </span>-background<span class="w"> </span>transparent<span class="w"> </span>favicon.svg<span class="w"> </span>-define<span class="w"> </span>icon:auto-resize<span class="w"> </span>-colors<span class="w"> </span><span class="m">256 …</span></code></pre></div>
<p>Il y a plusieurs sites qui permettent de faire ça automatiquement, mais j’aime bien faire les choses depuis mon terminal, voici donc une commande qui permet de faire ça simplement, en utilisant <a href="https://imagemagick.org/">ImageMagick</a>. Merci à <a href="https://gist.github.com/azam/3b6995a29b9f079282f3">ce gist</a></p>
<div class="highlight"><pre><span></span><code>convert<span class="w"> </span>-density<span class="w"> </span>256x256<span class="w"> </span>-background<span class="w"> </span>transparent<span class="w"> </span>favicon.svg<span class="w"> </span>-define<span class="w"> </span>icon:auto-resize<span class="w"> </span>-colors<span class="w"> </span><span class="m">256</span><span class="w"> </span>favicon.ico
</code></pre></div>Découverte de nouveaux outils pour le développement: LLM, Helix et plus2023-09-12T00:00:00+02:002023-09-12T00:00:00+02:00tag:blog.notmyidea.org,2023-09-12:/decouverte-de-nouveaux-outils-pour-le-developpement-llm-helix-et-plus.html
<h2 id="llm"><span class="caps">LLM</span></h2>
<ul>
<li><a href="https://localai.io/model-compatibility/">LocalAI</a> permet de faire tourner des modèles en local avec la même <span class="caps">API</span> <span class="caps">HTTP</span> que celle d’OpenAI</li>
<li><a href="https://github.com/bofenghuang/vigogne">Le modèle Vigogne</a> est un modèle entrainé (<em>fine-tuned</em>) avec des données en Français. Notamment <a href="https://huggingface.co/bofenghuang/vigogne-2-7b-chat/tree/v1.0">ce modèle</a>qui prends <span class="caps">LLAMA2</span> en entrée.</li>
<li><a href="https://python.langchain.com/docs/get_started/introduction.html">LangChain</a> semble être un framework pour travailler avec les différents …</li></ul>
<h2 id="llm"><span class="caps">LLM</span></h2>
<ul>
<li><a href="https://localai.io/model-compatibility/">LocalAI</a> permet de faire tourner des modèles en local avec la même <span class="caps">API</span> <span class="caps">HTTP</span> que celle d’OpenAI</li>
<li><a href="https://github.com/bofenghuang/vigogne">Le modèle Vigogne</a> est un modèle entrainé (<em>fine-tuned</em>) avec des données en Français. Notamment <a href="https://huggingface.co/bofenghuang/vigogne-2-7b-chat/tree/v1.0">ce modèle</a>qui prends <span class="caps">LLAMA2</span> en entrée.</li>
<li><a href="https://python.langchain.com/docs/get_started/introduction.html">LangChain</a> semble être un framework pour travailler avec les différents concepts utiles. A voir.</li>
</ul>
<p>Pour la première fois, j’ai commencé à utiliser un peu plus l’outil <a href="https://llm.datasette.io">llm</a> pour m’aider dans les tâches de programmation. </p>
<p>!! warning
J’utilise actuellement par défaut le modèle en ligne d’OpenAI “<span class="caps">GTP4</span>”, à travers leur <span class="caps">API</span>. Cela me pose des problèmes éthiques, mais mon approche est pour le moment de voir le type de résultats que j’obtiens pour ensuite comparer avec des modèles locaux type <span class="caps">LLAMA2</span>.</p>
<p>Deux choses que j’ai trouvées utiles :</p>
<div class="highlight"><pre><span></span><code>git<span class="w"> </span>diff<span class="w"> </span><span class="p">|</span><span class="w"> </span>llm<span class="w"> </span><span class="s2">"write me a commit message"</span>
git<span class="w"> </span>diff<span class="w"> </span><span class="p">|</span><span class="w"> </span>llm<span class="w"> </span><span class="s2">"find a branch name"</span>
</code></pre></div>
<p>Qui peuvent faciliter la vie plutôt que de chercher à décrire des choses manuellement (le “branch-name”, je le trouve particulièrement utile)</p>
<p>J’ai aussi trouvé l’idée intéressante d’automatiser l’écrite de tests, avec pytest. Bien sur ça ne fait pas tout, mais ça donne un template de départ qui est intéressant :</p>
<div class="highlight"><pre><span></span><code>cat<span class="w"> </span>code/test_file.py<span class="w"> </span><span class="p">|</span><span class="w"> </span>llm<span class="w"> </span><span class="s2">"write tests for this, using pytest"</span>
</code></pre></div>
<p>J’ai par exemple demandé à llm de me lister 20 titres pour cet article :</p>
<div class="highlight"><pre><span></span><code>cat<span class="w"> </span>~/dev/til.notmyidea.org/content/<span class="k">$(</span>date<span class="w"> </span>+%F<span class="k">)</span>.md<span class="w"> </span><span class="p">|</span><span class="w"> </span>llm<span class="w"> </span><span class="s2">"Liste moi 20 titres pour cet article. Quelque chose de simple"</span>
<span class="m">1</span>.<span class="w"> </span><span class="s2">"Découverte de nouveaux outils pour le développement: LLM, Helix et plus"</span>
<span class="m">2</span>.<span class="w"> </span><span class="s2">"Explorer les modèles LLM pour la génération de code"</span>
<span class="m">3</span>.<span class="w"> </span><span class="s2">"L'amélioration du développement grâce à LLM et Helix"</span>
<span class="m">4</span>.<span class="w"> </span><span class="s2">"Introduction à l'éditeur Helix et à LLM"</span>
<span class="m">5</span>.<span class="w"> </span><span class="s2">"Boostez votre productivité de développement avec LLM et Helix"</span>
<span class="m">6</span>.<span class="w"> </span><span class="s2">"Comment j'intègre LLM et Helix dans mon flux de travail de développement"</span>
<span class="m">7</span>.<span class="w"> </span><span class="s2">"Les avantages de l'utilisation de LLM pour la programmation"</span>
<span class="m">8</span>.<span class="w"> </span><span class="s2">"Approches innovantes pour le développement de code avec LLM"</span>
<span class="m">9</span>.<span class="w"> </span><span class="s2">"L'avenir du développement de code avec LLM et Helix"</span>
<span class="m">10</span>.<span class="w"> </span><span class="s2">"Optimisation des tâches de programmation avec LLM"</span>
<span class="m">11</span>.<span class="w"> </span><span class="s2">"Ouvrir de nouvelles possibilités de codage avec LLM et Helix"</span>
<span class="m">12</span>.<span class="w"> </span><span class="s2">"Utiliser des modèles LLM pour améliorer la qualité du code"</span>
</code></pre></div>
<h2 id="helix">Helix</h2>
<p>Je me mets à tester un peu plus l’éditeur Helix. Je suis un habitué de Vim pour toutes les petites modifications que je fais depuis mon terminal, et Helix semble intéressant.</p>
<ul>
<li><code>x</code>pour sélectionner une ligne (<code>xd</code> pour sélectionner et supprimer)</li>
<li><code>ma</code> pour sélectionner “around” quelque chose. <code>maw</code> pour sélectionner le mot</li>
<li><code>c</code> pour effacer la sélection et passer en mode insertion.</li>
</ul>
<h2 id="divers">Divers</h2>
<blockquote>
<p>J’ai fait confiance, j’ai appris.
— <a href="https://d%C3%A9tour.studio">Thomas</a></p>
</blockquote>
<p>J’aime beaucoup ce que ça dit. Faire confiance est peut-être nécessaire, même si on est déçu au final, on aura au moins appris. Ça me touche.</p>Running the Gitlab CI locally2023-08-19T00:00:00+02:002023-08-19T00:00:00+02:00tag:blog.notmyidea.org,2023-08-19:/running-the-gitlab-ci-locally.html
<p>Sometimes, I need to change how the continuous integration is setup, and I find
myself pushing to a branch to test if my changes are working. Oftentimes, it
takes me multiple commits to find the correct configuration, which is… suboptimal.</p>
<p>I discovered today <a href="https://github.com/firecow/gitlab-ci-local">Gitlab <span class="caps">CI</span>
local</a> which makes it possible …</p>
<p>Sometimes, I need to change how the continuous integration is setup, and I find
myself pushing to a branch to test if my changes are working. Oftentimes, it
takes me multiple commits to find the correct configuration, which is… suboptimal.</p>
<p>I discovered today <a href="https://github.com/firecow/gitlab-ci-local">Gitlab <span class="caps">CI</span>
local</a> which makes it possible to
run the <span class="caps">CI</span> actions locally, without having to push to the remote <span class="caps">CI</span>. The same
thing exists for <a href="https://github.com/nektos/act">Microsoft Github</a>.</p>
<p>Under the hood, it’s using Docker, so you need to have it running on your
system, but once it’s done, you just have to issue a simple command to see the
results. Very helpful :-)</p>
<p>Here is an example :</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>gitlab-ci-local<span class="w"> </span><span class="nb">test</span>
parsing<span class="w"> </span>and<span class="w"> </span>downloads<span class="w"> </span>finished<span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="m">41</span><span class="w"> </span>ms
<span class="nb">test</span><span class="w"> </span>starting<span class="w"> </span>python:3.8-alpine<span class="w"> </span><span class="o">(</span><span class="nb">test</span><span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>copied<span class="w"> </span>to<span class="w"> </span>docker<span class="w"> </span>volumes<span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="m">4</span>.05<span class="w"> </span>s
<span class="nb">test</span><span class="w"> </span>$<span class="w"> </span>apk<span class="w"> </span>update<span class="w"> </span><span class="o">&&</span><span class="w"> </span>apk<span class="w"> </span>add<span class="w"> </span>make<span class="w"> </span>libsass<span class="w"> </span>gcc<span class="w"> </span>musl-dev<span class="w"> </span>g++
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>fetch<span class="w"> </span>https://dl-cdn.alpinelinux.org/alpine/v3.18/main/aarch64/APKINDEX.tar.gz
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>fetch<span class="w"> </span>https://dl-cdn.alpinelinux.org/alpine/v3.18/community/aarch64/APKINDEX.tar.gz
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>v3.18.3-55-g2ee93b9273a<span class="w"> </span><span class="o">[</span>https://dl-cdn.alpinelinux.org/alpine/v3.18/main<span class="o">]</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>v3.18.3-56-g4a3b0382caa<span class="w"> </span><span class="o">[</span>https://dl-cdn.alpinelinux.org/alpine/v3.18/community<span class="o">]</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>OK:<span class="w"> </span><span class="m">19939</span><span class="w"> </span>distinct<span class="w"> </span>packages<span class="w"> </span>available
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">1</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>libgcc<span class="w"> </span><span class="o">(</span><span class="m">12</span>.2.1_git20220924-r10<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">2</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>libstdc++<span class="w"> </span><span class="o">(</span><span class="m">12</span>.2.1_git20220924-r10<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">3</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>libstdc++-dev<span class="w"> </span><span class="o">(</span><span class="m">12</span>.2.1_git20220924-r10<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">4</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>zstd-libs<span class="w"> </span><span class="o">(</span><span class="m">1</span>.5.5-r4<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">5</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>binutils<span class="w"> </span><span class="o">(</span><span class="m">2</span>.40-r7<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">6</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>libgomp<span class="w"> </span><span class="o">(</span><span class="m">12</span>.2.1_git20220924-r10<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">7</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>libatomic<span class="w"> </span><span class="o">(</span><span class="m">12</span>.2.1_git20220924-r10<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">8</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>gmp<span class="w"> </span><span class="o">(</span><span class="m">6</span>.2.1-r3<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">9</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>isl26<span class="w"> </span><span class="o">(</span><span class="m">0</span>.26-r1<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">10</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>mpfr4<span class="w"> </span><span class="o">(</span><span class="m">4</span>.2.0_p12-r0<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">11</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>mpc1<span class="w"> </span><span class="o">(</span><span class="m">1</span>.3.1-r1<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">12</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>gcc<span class="w"> </span><span class="o">(</span><span class="m">12</span>.2.1_git20220924-r10<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">13</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>musl-dev<span class="w"> </span><span class="o">(</span><span class="m">1</span>.2.4-r1<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">14</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>libc-dev<span class="w"> </span><span class="o">(</span><span class="m">0</span>.7.2-r5<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">15</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>g++<span class="w"> </span><span class="o">(</span><span class="m">12</span>.2.1_git20220924-r10<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">16</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>libsass<span class="w"> </span><span class="o">(</span><span class="m">3</span>.6.5-r0<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">(</span><span class="m">17</span>/17<span class="o">)</span><span class="w"> </span>Installing<span class="w"> </span>make<span class="w"> </span><span class="o">(</span><span class="m">4</span>.4.1-r1<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Executing<span class="w"> </span>busybox-1.36.1-r2.trigger
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>OK:<span class="w"> </span><span class="m">246</span><span class="w"> </span>MiB<span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="m">55</span><span class="w"> </span>packages
<span class="nb">test</span><span class="w"> </span>$<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>-r<span class="w"> </span>requirements.txt
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>pelican
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>pelican-4.8.0-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">1</span>.4<span class="w"> </span>MB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">1</span>.4/1.4<span class="w"> </span>MB<span class="w"> </span><span class="m">539</span>.9<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>markdown
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>Markdown-3.4.4-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">94</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">94</span>.2/94.2<span class="w"> </span>kB<span class="w"> </span><span class="m">540</span>.1<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>typogrify
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>typogrify-2.0.7.tar.gz<span class="w"> </span><span class="o">(</span><span class="m">12</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Preparing<span class="w"> </span>metadata<span class="w"> </span><span class="o">(</span>setup.py<span class="o">)</span>:<span class="w"> </span>started
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Preparing<span class="w"> </span>metadata<span class="w"> </span><span class="o">(</span>setup.py<span class="o">)</span>:<span class="w"> </span>finished<span class="w"> </span>with<span class="w"> </span>status<span class="w"> </span><span class="s1">'done'</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>pelican-search
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>pelican_search-1.1.0-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">6</span>.6<span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>pelican-neighbors
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>pelican_neighbors-1.2.0-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">16</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>pelican-webassets
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>pelican_webassets-2.0.0-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">5</span>.8<span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>libsass
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>libsass-0.22.0.tar.gz<span class="w"> </span><span class="o">(</span><span class="m">316</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">316</span>.3/316.3<span class="w"> </span>kB<span class="w"> </span><span class="m">552</span>.1<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Preparing<span class="w"> </span>metadata<span class="w"> </span><span class="o">(</span>setup.py<span class="o">)</span>:<span class="w"> </span>started
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Preparing<span class="w"> </span>metadata<span class="w"> </span><span class="o">(</span>setup.py<span class="o">)</span>:<span class="w"> </span>finished<span class="w"> </span>with<span class="w"> </span>status<span class="w"> </span><span class="s1">'done'</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>docutils><span class="o">=</span><span class="m">0</span>.16
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>docutils-0.20.1-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">572</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">572</span>.7/572.7<span class="w"> </span>kB<span class="w"> </span><span class="m">549</span>.2<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>rich><span class="o">=</span><span class="m">10</span>.1
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>rich-13.5.2-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">239</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">239</span>.7/239.7<span class="w"> </span>kB<span class="w"> </span><span class="m">485</span>.3<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>jinja2><span class="o">=</span><span class="m">2</span>.7
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>Jinja2-3.1.2-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">133</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">133</span>.1/133.1<span class="w"> </span>kB<span class="w"> </span><span class="m">342</span>.6<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>pytz><span class="o">=</span><span class="m">2020</span>.1
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>pytz-2023.3-py2.py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">502</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">502</span>.3/502.3<span class="w"> </span>kB<span class="w"> </span><span class="m">547</span>.3<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>pygments><span class="o">=</span><span class="m">2</span>.6
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>Pygments-2.16.1-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">1</span>.2<span class="w"> </span>MB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">1</span>.2/1.2<span class="w"> </span>MB<span class="w"> </span><span class="m">551</span>.4<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>unidecode><span class="o">=</span><span class="m">1</span>.1
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>Unidecode-1.3.6-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">235</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">235</span>.9/235.9<span class="w"> </span>kB<span class="w"> </span><span class="m">554</span>.2<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>blinker><span class="o">=</span><span class="m">1</span>.4
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>blinker-1.6.2-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">13</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>python-dateutil><span class="o">=</span><span class="m">2</span>.8
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>python_dateutil-2.8.2-py2.py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">247</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">247</span>.7/247.7<span class="w"> </span>kB<span class="w"> </span><span class="m">235</span>.7<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>feedgenerator><span class="o">=</span><span class="m">1</span>.9
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>feedgenerator-2.1.0-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">21</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>importlib-metadata><span class="o">=</span><span class="m">4</span>.4
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>importlib_metadata-6.8.0-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">22</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>smartypants><span class="o">=</span><span class="m">1</span>.8.3
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>smartypants-2.0.1-py2.py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">9</span>.9<span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>rtoml<<span class="m">0</span>.10.0,><span class="o">=</span><span class="m">0</span>.9.0
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>rtoml-0.9.0-cp38-cp38-musllinux_1_1_aarch64.whl<span class="w"> </span><span class="o">(</span><span class="m">846</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">846</span>.2/846.2<span class="w"> </span>kB<span class="w"> </span><span class="m">503</span>.7<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>webassets<<span class="m">3</span>.0,><span class="o">=</span><span class="m">2</span>.0
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>webassets-2.0-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">142</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">142</span>.9/142.9<span class="w"> </span>kB<span class="w"> </span><span class="m">551</span>.8<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>zipp><span class="o">=</span><span class="m">0</span>.5
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>zipp-3.16.2-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">7</span>.2<span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>MarkupSafe><span class="o">=</span><span class="m">2</span>.0
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl<span class="w"> </span><span class="o">(</span><span class="m">30</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>six><span class="o">=</span><span class="m">1</span>.5
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>six-1.16.0-py2.py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">11</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>markdown-it-py><span class="o">=</span><span class="m">2</span>.2.0
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>markdown_it_py-3.0.0-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">87</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<span class="w"> </span><span class="m">87</span>.5/87.5<span class="w"> </span>kB<span class="w"> </span><span class="m">561</span>.7<span class="w"> </span>kB/s<span class="w"> </span>eta<span class="w"> </span><span class="m">0</span>:00:00
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>typing-extensions<<span class="m">5</span>.0,><span class="o">=</span><span class="m">4</span>.0.0
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>typing_extensions-4.7.1-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">33</span><span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Collecting<span class="w"> </span>mdurl~<span class="o">=</span><span class="m">0</span>.1
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Downloading<span class="w"> </span>mdurl-0.1.2-py3-none-any.whl<span class="w"> </span><span class="o">(</span><span class="m">10</span>.0<span class="w"> </span>kB<span class="o">)</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Building<span class="w"> </span>wheels<span class="w"> </span><span class="k">for</span><span class="w"> </span>collected<span class="w"> </span>packages:<span class="w"> </span>typogrify,<span class="w"> </span>libsass
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Building<span class="w"> </span>wheel<span class="w"> </span><span class="k">for</span><span class="w"> </span>typogrify<span class="w"> </span><span class="o">(</span>setup.py<span class="o">)</span>:<span class="w"> </span>started
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Building<span class="w"> </span>wheel<span class="w"> </span><span class="k">for</span><span class="w"> </span>typogrify<span class="w"> </span><span class="o">(</span>setup.py<span class="o">)</span>:<span class="w"> </span>finished<span class="w"> </span>with<span class="w"> </span>status<span class="w"> </span><span class="s1">'done'</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Created<span class="w"> </span>wheel<span class="w"> </span><span class="k">for</span><span class="w"> </span>typogrify:<span class="w"> </span><span class="nv">filename</span><span class="o">=</span>typogrify-2.0.7-py2.py3-none-any.whl<span class="w"> </span><span class="nv">size</span><span class="o">=</span><span class="m">13452</span><span class="w"> </span><span class="nv">sha256</span><span class="o">=</span>4ce329903e807671102eab7fd2bc49765b6efc3a4ae68c82053318b62789083c
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Stored<span class="w"> </span><span class="k">in</span><span class="w"> </span>directory:<span class="w"> </span>/root/.cache/pip/wheels/0b/e9/98/c888501e8dd2166da059e4f8418694de9b50b48a7192712be9
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Building<span class="w"> </span>wheel<span class="w"> </span><span class="k">for</span><span class="w"> </span>libsass<span class="w"> </span><span class="o">(</span>setup.py<span class="o">)</span>:<span class="w"> </span>started
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Building<span class="w"> </span>wheel<span class="w"> </span><span class="k">for</span><span class="w"> </span>libsass<span class="w"> </span><span class="o">(</span>setup.py<span class="o">)</span>:<span class="w"> </span>still<span class="w"> </span>running...
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Building<span class="w"> </span>wheel<span class="w"> </span><span class="k">for</span><span class="w"> </span>libsass<span class="w"> </span><span class="o">(</span>setup.py<span class="o">)</span>:<span class="w"> </span>finished<span class="w"> </span>with<span class="w"> </span>status<span class="w"> </span><span class="s1">'done'</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Created<span class="w"> </span>wheel<span class="w"> </span><span class="k">for</span><span class="w"> </span>libsass:<span class="w"> </span><span class="nv">filename</span><span class="o">=</span>libsass-0.22.0-cp38-abi3-linux_aarch64.whl<span class="w"> </span><span class="nv">size</span><span class="o">=</span><span class="m">13710320</span><span class="w"> </span><span class="nv">sha256</span><span class="o">=</span>3dcb4ce97c1aafc179a6343e0f312c17df88e56c4eb647ab54b09ead5ee00b92
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Stored<span class="w"> </span><span class="k">in</span><span class="w"> </span>directory:<span class="w"> </span>/root/.cache/pip/wheels/95/64/fa/47638d5037df216387cdc168e9871d5d9851fc995d636bd108
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Successfully<span class="w"> </span>built<span class="w"> </span>typogrify<span class="w"> </span>libsass
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Installing<span class="w"> </span>collected<span class="w"> </span>packages:<span class="w"> </span>webassets,<span class="w"> </span>smartypants,<span class="w"> </span>pytz,<span class="w"> </span>zipp,<span class="w"> </span>unidecode,<span class="w"> </span>typogrify,<span class="w"> </span>typing-extensions,<span class="w"> </span>six,<span class="w"> </span>rtoml,<span class="w"> </span>pygments,<span class="w"> </span>mdurl,<span class="w"> </span>MarkupSafe,<span class="w"> </span>libsass,<span class="w"> </span>feedgenerator,<span class="w"> </span>docutils,<span class="w"> </span>blinker,<span class="w"> </span>python-dateutil,<span class="w"> </span>markdown-it-py,<span class="w"> </span>jinja2,<span class="w"> </span>importlib-metadata,<span class="w"> </span>rich,<span class="w"> </span>markdown,<span class="w"> </span>pelican,<span class="w"> </span>pelican-webassets,<span class="w"> </span>pelican-search,<span class="w"> </span>pelican-neighbors
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Successfully<span class="w"> </span>installed<span class="w"> </span>MarkupSafe-2.1.3<span class="w"> </span>blinker-1.6.2<span class="w"> </span>docutils-0.20.1<span class="w"> </span>feedgenerator-2.1.0<span class="w"> </span>importlib-metadata-6.8.0<span class="w"> </span>jinja2-3.1.2<span class="w"> </span>libsass-0.22.0<span class="w"> </span>markdown-3.4.4<span class="w"> </span>markdown-it-py-3.0.0<span class="w"> </span>mdurl-0.1.2<span class="w"> </span>pelican-4.8.0<span class="w"> </span>pelican-neighbors-1.2.0<span class="w"> </span>pelican-search-1.1.0<span class="w"> </span>pelican-webassets-2.0.0<span class="w"> </span>pygments-2.16.1<span class="w"> </span>python-dateutil-2.8.2<span class="w"> </span>pytz-2023.3<span class="w"> </span>rich-13.5.2<span class="w"> </span>rtoml-0.9.0<span class="w"> </span>six-1.16.0<span class="w"> </span>smartypants-2.0.1<span class="w"> </span>typing-extensions-4.7.1<span class="w"> </span>typogrify-2.0.7<span class="w"> </span>unidecode-1.3.6<span class="w"> </span>webassets-2.0<span class="w"> </span>zipp-3.16.2
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>WARNING:<span class="w"> </span>Running<span class="w"> </span>pip<span class="w"> </span>as<span class="w"> </span>the<span class="w"> </span><span class="s1">'root'</span><span class="w"> </span>user<span class="w"> </span>can<span class="w"> </span>result<span class="w"> </span><span class="k">in</span><span class="w"> </span>broken<span class="w"> </span>permissions<span class="w"> </span>and<span class="w"> </span>conflicting<span class="w"> </span>behaviour<span class="w"> </span>with<span class="w"> </span>the<span class="w"> </span>system<span class="w"> </span>package<span class="w"> </span>manager.<span class="w"> </span>It<span class="w"> </span>is<span class="w"> </span>recommended<span class="w"> </span>to<span class="w"> </span>use<span class="w"> </span>a<span class="w"> </span>virtual<span class="w"> </span>environment<span class="w"> </span>instead:<span class="w"> </span>https://pip.pypa.io/warnings/venv
<span class="nb">test</span><span class="w"> </span>>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">[</span>notice<span class="o">]</span><span class="w"> </span>A<span class="w"> </span>new<span class="w"> </span>release<span class="w"> </span>of<span class="w"> </span>pip<span class="w"> </span>is<span class="w"> </span>available:<span class="w"> </span><span class="m">23</span>.0.1<span class="w"> </span>-><span class="w"> </span><span class="m">23</span>.2.1
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="o">[</span>notice<span class="o">]</span><span class="w"> </span>To<span class="w"> </span>update,<span class="w"> </span>run:<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>--upgrade<span class="w"> </span>pip
<span class="nb">test</span><span class="w"> </span>$<span class="w"> </span>make<span class="w"> </span>publish
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span><span class="s2">"pelican"</span><span class="w"> </span><span class="s2">"/gcl-builds/content"</span><span class="w"> </span>-o<span class="w"> </span><span class="s2">"/gcl-builds/public"</span><span class="w"> </span>-s<span class="w"> </span><span class="s2">"/gcl-builds/publishconf.py"</span>
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>Done:<span class="w"> </span>Processed<span class="w"> </span><span class="m">5</span><span class="w"> </span>articles,<span class="w"> </span><span class="m">0</span><span class="w"> </span>drafts,<span class="w"> </span><span class="m">0</span><span class="w"> </span>hidden<span class="w"> </span>articles,<span class="w"> </span><span class="m">2</span><span class="w"> </span>pages,<span class="w"> </span><span class="m">0</span><span class="w"> </span>hidden<span class="w"> </span>pages
<span class="nb">test</span><span class="w"> </span>><span class="w"> </span>and<span class="w"> </span><span class="m">0</span><span class="w"> </span>draft<span class="w"> </span>pages<span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="m">0</span>.50<span class="w"> </span>seconds.
<span class="nb">test</span><span class="w"> </span>finished<span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="m">6</span><span class="w"> </span>min
PASS<span class="w"> </span><span class="nb">test</span>
</code></pre></div>ArchLinux et mise à jour du keyring2023-08-18T00:00:00+02:002023-08-18T00:00:00+02:00tag:blog.notmyidea.org,2023-08-18:/archlinux-et-mise-a-jour-du-keyring.html
<p>Pour les mises à jour Arch, j’utilise <a href="https://github.com/Jguer/yay">yay</a>. Je
ne fais les mises à jour que de manière semi-régulière, et parfois après une
longue période je me retrouve avec des soucis de clé qui ne sont plus à jour ou manquantes.</p>
<p>Avec une utilisation fréquente du système, aucun problème …</p>
<p>Pour les mises à jour Arch, j’utilise <a href="https://github.com/Jguer/yay">yay</a>. Je
ne fais les mises à jour que de manière semi-régulière, et parfois après une
longue période je me retrouve avec des soucis de clé qui ne sont plus à jour ou manquantes.</p>
<p>Avec une utilisation fréquente du système, aucun problème ne se pose car un
service s’occupe de faire la mise à jour des clés de manière automatique.</p>
<p>Pour résoudre le souci, il suffit de mettre à jour le paquet
<code>archlinux-keyring</code>, comme décrit <a href="https://wiki.archlinux.org/title/Pacman/Package_signing">dans la page Wiki qui va
bien</a>.</p>
<div class="highlight"><pre><span></span><code>sudo pacman -S archlinux-keyring
</code></pre></div>Python packaging with Hatch, pipx and Zsh environment variables2023-08-17T00:00:00+02:002023-08-17T00:00:00+02:00tag:blog.notmyidea.org,2023-08-17:/python-packaging-with-hatch-pipx-and-zsh-environment-variables.html
<p>It’s been a while I didn’t packaged something new. I recently remembered an old
package of mine that needed some attention :
<a href="https://gitlab.com/almet/debts">debts</a>. It’s now time to package it, so I
discovered <a href="https://hatch.pypa.io/">hatch</a></p>
<p>hatch new —init</p>
<p>This does the heavy-lifting for you, actually porting the <code>setup.py</code> files …</p>
<p>It’s been a while I didn’t packaged something new. I recently remembered an old
package of mine that needed some attention :
<a href="https://gitlab.com/almet/debts">debts</a>. It’s now time to package it, so I
discovered <a href="https://hatch.pypa.io/">hatch</a></p>
<p>hatch new —init</p>
<p>This does the heavy-lifting for you, actually porting the <code>setup.py</code> files to the
new way of packaging with python (with a <code>pyproject.toml</code> file)</p>
<p>Then <code>hatch shell</code> will create a development environment, install dependencies,
check the <code>pyproject.toml</code> file in one command, and give you a shell to test
whatever you need to test.</p>
<h2 id="isolating-system-packages">Isolating system packages</h2>
<p>I discovered that <a href="https://github.com/pypa/pipx">pipx</a> is a convenient way to
install user-facing applications on my system. I use multiple virtual
environments for my different projects, but not for the install that are used system-wide.</p>
<p>pipx seems to solve this, and avoid using <code>sudo pip install x</code>.</p>
<h2 id="manipulating-env-variables-with-zsh">Manipulating env variables with Zsh</h2>
<p>I use <a href="https://www.zsh.org/">Zsh</a> as my main shell for years, and I just
discovered that it’s possible to manipulate environment variables in an easy way.</p>
<p>If you’re like me, you never remember how to add something to your path. You
can actually use <code>+=</code>, like this:</p>
<div class="highlight"><pre><span></span><code><span class="nv">path</span><span class="o">+=(</span><span class="s1">'/Users/alexis/.local/bin'</span><span class="o">)</span>
<span class="nb">export</span><span class="w"> </span>PATH
</code></pre></div>Profiling and speeding up Django and Pytest2023-08-16T00:00:00+02:002023-08-16T00:00:00+02:00tag:blog.notmyidea.org,2023-08-16:/profiling-and-speeding-up-django-and-pytest.html
<p><a href="https://yaal.coop/">Éloi</a> made <a href="https://github.com/spiral-project/ihatemoney/issues/1214">a pull request on
IHateMoney</a> to
speedup the tests, with some great tooling for pytest that I wasn’t aware of:</p>
<ul>
<li><a href="https://pypi.org/project/pytest-xdist/">pytest-xdist</a> allows to run tests in
parallel, using <code>-n auto</code></li>
<li><a href="https://pypi.org/project/pytest-profiling/">pytest-profiling</a> makes it easy
to get the call stack and time the function calls that take most …</li></ul>
<p><a href="https://yaal.coop/">Éloi</a> made <a href="https://github.com/spiral-project/ihatemoney/issues/1214">a pull request on
IHateMoney</a> to
speedup the tests, with some great tooling for pytest that I wasn’t aware of:</p>
<ul>
<li><a href="https://pypi.org/project/pytest-xdist/">pytest-xdist</a> allows to run tests in
parallel, using <code>-n auto</code></li>
<li><a href="https://pypi.org/project/pytest-profiling/">pytest-profiling</a> makes it easy
to get the call stack and time the function calls that take most of the time.</li>
<li>You can them analyse the <code>.prof</code> files with
<a href="https://pypi.org/project/snakeviz/">Snakeviz</a></li>
</ul>
<p>So, I spent some time using these on the tests for <a href="https://chariotte.fr">La
Chariotte</a>, because they were slow.</p>
<p>I found two things :</p>
<ul>
<li>Login calls are costly in the test, and it’s possible to speed things up ;</li>
<li>On my machine, calls to resolve my hostname were slow, using 5s during the
tests for a lookup that wasn’t even useful.</li>
</ul>
<h2 id="changing-the-hashing-algorithm-to-speedup-tests">Changing the hashing algorithm to speedup tests</h2>
<p>By default, Django uses a slow (but secure !) hashing mechanism for checking
the user credentials. In the tests, we don’t need this security, but we need
the speed.</p>
<p>Changing them to use <span class="caps">MD5</span> turns out to be a way to greatly speed them up! Here
is how to do it with a pytest fixture :</p>
<div class="highlight"><pre><span></span><code><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span><span class="p">(</span><span class="n">autouse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">password_hasher_setup</span><span class="p">(</span><span class="n">settings</span><span class="p">):</span>
<span class="c1"># Use a weaker password hasher during tests, for speed</span>
<span class="n">settings</span><span class="o">.</span><span class="n">PASSWORD_HASHERS</span> <span class="o">=</span> <span class="p">[</span>
<span class="s2">"django.contrib.auth.hashers.MD5PasswordHasher"</span><span class="p">,</span>
<span class="p">]</span>
</code></pre></div>
<h2 id="speeding-dns-lookups">Speeding <span class="caps">DNS</span> lookups</h2>
<p>I’m currently using a MacOSX machine, and for for whatever reason, the local
lookup was not configured properly on my machine. I don’t think I did anything
specific to get this wrong, so it might be your case too. Calls to resolve the
local domain were tooking 5s.</p>
<p>If the answer to <code>scutil --get LocalHostName</code>, <code>hostname</code> and <code>scutil --get
HostName</code> differ, then you might be in this case. Here is the fix :</p>
<div class="highlight"><pre><span></span><code>sudo<span class="w"> </span>scutil<span class="w"> </span>--set<span class="w"> </span>HostName<span class="w"> </span><YourHostName>
</code></pre></div>Installation de Mosquitto, InfluxDB, Telegraf et Grafana2022-08-29T00:00:00+02:002022-08-29T00:00:00+02:00tag:blog.notmyidea.org,2022-08-29:/installation-de-mosquitto-influxdb-telegraf-et-grafana.html<p>Récemment, on a m’a demandé un petit coup de main pour aider à l’installation d’une pile logicielle qui permet de stocker des données temporelles et en faire des graphiques.</p>
<p>Voici donc quelques notes prises durant l’installation du système, concues pour que des personnes qui n’y …</p><p>Récemment, on a m’a demandé un petit coup de main pour aider à l’installation d’une pile logicielle qui permet de stocker des données temporelles et en faire des graphiques.</p>
<p>Voici donc quelques notes prises durant l’installation du système, concues pour que des personnes qui n’y connaissent pas grand chose puissent s’y retrouver.</p>
<p>L’objectif, c’est d’avoir des cartes Arduino qui envoient des données de manière régulière sur un système qui va nous permettre de les stocker et d’en faire des graphiques.</p>
<p>Pour ça, nous allons utiliser :</p>
<ul>
<li>Un <em>Broker</em> <a href="https://mosquitto.org/">Mosquitto</a> qui va permettre de receptionner les données depuis les différents <em>clients</em>, puis de les dispatcher à qui en a besoin ;</li>
<li>Une base de données <a href="https://www.influxdata.com/products/influxdb-overview/">InfluxDB</a>, qui permet de stocker des données temporelles ;</li>
<li>Un <em>agent</em> <a href="https://www.influxdata.com/time-series-platform/telegraf/">Telegraf</a> qui va prendre les données du broker et les stocker dans la base de données InfluxDB.</li>
<li><a href="https://grafana.com/">Grafana</a>, une <em>application web</em> qui permet de visualiser les données stockées dans InfluxDB.</li>
</ul>
<p>Voici donc un document qui résume les étapes que j’ai suivies pour mettre en place les différents élements utiles :</p>
<h2 id="installer-et-se-connecter-au-serveur">Installer et se connecter au serveur</h2>
<p>Dans notre cas, on est passé par un <span class="caps">VPS</span> chez <span class="caps">OVH</span>, qui tourne sous <a href="https://www.debian.org/">Debian 11</a>, qui a le mérite d’être une distribution Linux stable, reconnue et (relativement) simple à utiliser.</p>
<p>Dans un terminal, vous pouvez vous connecter en utilisant la ligne de commande suivante :</p>
<p><em>Les lignes suivantes sont des lignes d’invite de commande, on les rencontre assez souvent dans les documentations sur le web. Le signe <code>$</code> signifie le début de la ligne de commande. Le signe <code>#</code> signifie le début des commentaires.</em></p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>ssh<span class="w"> </span>utilisateur@adresseip
</code></pre></div>
<p>Une fois connecté, on va mettre à jour les logiciels qui sont présents sur le serveur. </p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>sudo<span class="w"> </span>apt<span class="w"> </span>update<span class="w"> </span><span class="c1"># mise à jour des dépôts (la liste des logiciels).</span>
$<span class="w"> </span>sudo<span class="w"> </span>apt<span class="w"> </span>upgrade<span class="w"> </span><span class="c1"># mise à jour des logiciels.</span>
</code></pre></div>
<h2 id="configurer-les-dns">Configurer les <span class="caps">DNS</span></h2>
<p>Nous allons avoir besoin de deux sous domaines qui redirigent vers le serveur. Bien sur, il faut adapter <code>ndd.tld</code> et le remplacer par votre nom de domaine :</p>
<ol>
<li>moquitto.ndd.tld</li>
<li>graphs.ndd.tld</li>
</ol>
<p>Pour faire ça, chez <span class="caps">OVH</span> ça se passe dans la console de « <span class="caps">OVH</span> Cloud », direction « Noms de domaines », et puis il faut rajouter deux enregistrements de type « A » qui pointent vers l’adresse <span class="caps">IP</span> du serveur.</p>
<p>En temps normal, l’adresse <span class="caps">IP</span> vous est fournie par <span class="caps">OVH</span>. Si vous avez un doute, vous pouvez l’obtenir depuis le serveur avec la commande <code>ip a</code>.</p>
<h2 id="installer-mosquitto">Installer Mosquitto</h2>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>sudo<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> </span>mosquitto<span class="w"> </span><span class="c1"># installation depuis les dépots officiels</span>
</code></pre></div>
<p>Une fois installé, <a href="https://mosquitto.org/documentation/authentication-methods/">il faut sécuriser l’installation avec un utilisateur et un mot de passe</a>.</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>sudo<span class="w"> </span>mosquitto_passwd<span class="w"> </span>-c<span class="w"> </span>/etc/mosquitto/passwd<span class="w"> </span><username>
</code></pre></div>
<p>Ensuite dans le fichier de configuration il faut spécifier où est le fichier qui contient les mots de passe. Pour éditer je recommande l’utilisation de l’éditeur de texte <code>nano</code>. </p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>sudo<span class="w"> </span>nano<span class="w"> </span>/etc/mosquitto/mosquitto.conf
</code></pre></div>
<p>Voici les lignes à rajouter :</p>
<div class="highlight"><pre><span></span><code>listener 1883
password_file /etc/mosquitto/passwd
</code></pre></div>
<p>Puis il faut relancer le service mosquitto :</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>sudo<span class="w"> </span>systemctl<span class="w"> </span>restart<span class="w"> </span>mosquitto
</code></pre></div>
<p>Avant de pouvoir utiliser mosquitto, il faut <a href="https://docs.ovh.com/gb/en/dedicated/firewall-network/">régler le firewall</a> de chez <span class="caps">OVH</span> pour qu’il accepte de laisser passer les messages pour le broker <span class="caps">MQTT</span>.</p>
<p>Il faut ajouter une règle dans le Firewall qui laisse passer toutes les connections <span class="caps">TCP</span>, avec l’option « établie ».</p>
<h3 id="verifions-que-tout-fonctionne-comme-prevu">Vérifions que tout fonctionne comme prévu :</h3>
<p>Dans une console, écoutons…</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>mosquitto_sub<span class="w"> </span>-h<span class="w"> </span>mosquitto.ndd.tld<span class="w"> </span>-p<span class="w"> </span><span class="m">1883</span><span class="w"> </span>-u<span class="w"> </span><username><span class="w"> </span>-P<span class="w"> </span><password><span class="w"> </span>-t<span class="w"> </span>topic
</code></pre></div>
<p>Et dans une autre envoyons un message :</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>mosquitto_pub<span class="w"> </span>-h<span class="w"> </span>mosquitto.ndd.tld<span class="w"> </span>-p<span class="w"> </span><span class="m">1883</span><span class="w"> </span>-u<span class="w"> </span><username><span class="w"> </span>-P<span class="w"> </span><password><span class="w"> </span>-t<span class="w"> </span>topic<span class="w"> </span>-m<span class="w"> </span><span class="m">30</span>
</code></pre></div>
<p>Vous deviez voir « 30 » apparaitre dans la première console. Si c’est bon, tout fonctionne !</p>
<h2 id="installation-dinfluxdb-et-telegraf">Installation d’InfluxDB et Telegraf</h2>
<p>Coup de bol, InfluxDB propose directement des packets pour Debian, sur leur dépot, qu’il faut donc ajouter en suivant ces quelques lignes :</p>
<div class="highlight"><pre><span></span><code>sudo<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> </span>-y<span class="w"> </span>gnupg2<span class="w"> </span>curl<span class="w"> </span>wget
wget<span class="w"> </span>-qO-<span class="w"> </span>https://repos.influxdata.com/influxdb.key<span class="w"> </span><span class="p">|</span><span class="w"> </span>sudo<span class="w"> </span>apt-key<span class="w"> </span>add<span class="w"> </span>-
<span class="nb">echo</span><span class="w"> </span><span class="s2">"deb https://repos.influxdata.com/debian </span><span class="k">$(</span>lsb_release<span class="w"> </span>-cs<span class="k">)</span><span class="s2"> stable"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>sudo<span class="w"> </span>tee<span class="w"> </span>/etc/apt/sources.list.d/influxdb.list
sudo<span class="w"> </span>apt<span class="w"> </span>update
</code></pre></div>
<p>Puis <code>sudo apt install influxdb telegraf</code> pour l’installer.</p>
<p>Ensuite, vous pouvez le lancer maintenant et indiquer au système de le lancer tout seul au démarrage :</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>sudo<span class="w"> </span>systemctl<span class="w"> </span><span class="nb">enable</span><span class="w"> </span>--now<span class="w"> </span>influxdb
$<span class="w"> </span>sudo<span class="w"> </span>systemctl<span class="w"> </span><span class="nb">enable</span><span class="w"> </span>--now<span class="w"> </span>telegraf
</code></pre></div>
<h3 id="configuration-de-telegraf">Configuration de Telegraf</h3>
<p>Telegraf permet de faire le lien entre les messages envoyés sur le broker <span class="caps">MQTT</span> et la base de données InfluxDB.</p>
<p>Ici, il faut rentrer un peu plus dans le vif du sujet, et ça dépends des messages que vous avez à stocker.</p>
<p>Dans notre cas, nous avons trois types de messages :</p>
<ul>
<li>/BatVoltage, int</li>
<li>/Temperature, int</li>
<li>/<span class="caps">GPS</span>, string</li>
</ul>
<p>Voici un fichier de configuration, qui reste à modifier en fonction des données.</p>
<div class="highlight"><pre><span></span><code><span class="k">[global_tags]</span>
<span class="k">[agent]</span>
<span class="w"> </span><span class="na">interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"10s"</span>
<span class="w"> </span><span class="na">round_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">true</span>
<span class="w"> </span><span class="na">metric_batch_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">1000</span>
<span class="w"> </span><span class="na">metric_buffer_limit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">10000</span>
<span class="w"> </span><span class="na">collection_jitter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"0s"</span>
<span class="w"> </span><span class="na">flush_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"10s"</span>
<span class="w"> </span><span class="na">flush_jitter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"0s"</span>
<span class="w"> </span><span class="na">precision</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"0s"</span>
<span class="w"> </span><span class="na">hostname</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">""</span>
<span class="w"> </span><span class="na">omit_hostname</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">false</span>
<span class="k">[[outputs.influxdb]]</span>
<span class="w"> </span><span class="na">urls</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">["http://127.0.0.1:8086"]</span>
<span class="w"> </span><span class="na">database</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"telegraf"</span>
<span class="k">[[inputs.mqtt_consumer]]</span>
<span class="w"> </span><span class="na">servers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">["tcp://127.0.0.1:1883"]</span>
<span class="w"> </span><span class="na">name_override</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"mqtt_consumer_float"</span>
<span class="w"> </span><span class="na">topics</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">[</span>
<span class="w"> </span><span class="na">"Topic/BatVoltage",</span>
<span class="w"> </span><span class="na">"Topic/Temperature",</span>
<span class="w"> </span><span class="na">]</span>
<span class="w"> </span><span class="na">username</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"<username>"</span>
<span class="w"> </span><span class="na">password</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"<password>"</span>
<span class="w"> </span><span class="na">data_format</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"value"</span>
<span class="w"> </span><span class="na">data_type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"integer"</span>
</code></pre></div>
<h2 id="installation-de-grafana">Installation de Grafana</h2>
<div class="highlight"><pre><span></span><code>sudo<span class="w"> </span>apt-get<span class="w"> </span>install<span class="w"> </span>-y<span class="w"> </span>apt-transport-https
sudo<span class="w"> </span>apt-get<span class="w"> </span>install<span class="w"> </span>-y<span class="w"> </span>software-properties-common<span class="w"> </span>wget
sudo<span class="w"> </span>wget<span class="w"> </span>-q<span class="w"> </span>-O<span class="w"> </span>/usr/share/keyrings/grafana.key<span class="w"> </span>https://packages.grafana.com/gpg.key
<span class="nb">echo</span><span class="w"> </span><span class="s2">"deb [signed-by=/usr/share/keyrings/grafana.key] https://packages.grafana.com/oss/deb stable main"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>sudo<span class="w"> </span>tee<span class="w"> </span>-a<span class="w"> </span>/etc/apt/sources.list.d/grafana.list
sudo<span class="w"> </span>apt<span class="w"> </span>update
sudo<span class="w"> </span>apt-get<span class="w"> </span>install<span class="w"> </span>grafana
sudo<span class="w"> </span>/bin/systemctl<span class="w"> </span>daemon-reload
sudo<span class="w"> </span>/bin/systemctl<span class="w"> </span><span class="nb">enable</span><span class="w"> </span>grafana-server
sudo<span class="w"> </span>/bin/systemctl<span class="w"> </span>start<span class="w"> </span>grafana-server
</code></pre></div>
<h3 id="nginx">Nginx</h3>
<div class="highlight"><pre><span></span><code>sudo apt install nginx certbot python3-certbot-nginx
</code></pre></div>
<p>Puis il faut créer un fichier de configuration dans <code>/etc/nginx/sites-enabled/graphs.ndd.tld</code> avec le contenu suivant :</p>
<div class="highlight"><pre><span></span><code><span class="k">map</span><span class="w"> </span><span class="nv">$http_upgrade</span><span class="w"> </span><span class="nv">$connection_upgrade</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kn">default</span><span class="w"> </span><span class="s">upgrade</span><span class="p">;</span>
<span class="w"> </span><span class="kn">''</span><span class="w"> </span><span class="s">close</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">upstream</span><span class="w"> </span><span class="s">grafana</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kn">server</span><span class="w"> </span><span class="n">localhost</span><span class="p">:</span><span class="mi">3000</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">server</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kn">listen</span><span class="w"> </span><span class="mi">80</span><span class="p">;</span>
<span class="w"> </span><span class="kn">server_name</span><span class="w"> </span><span class="s">graphs.ndd.tld</span><span class="p">;</span>
<span class="w"> </span><span class="kn">root</span><span class="w"> </span><span class="s">/usr/share/nginx/html</span><span class="p">;</span>
<span class="w"> </span><span class="kn">index</span><span class="w"> </span><span class="s">index.html</span><span class="w"> </span><span class="s">index.htm</span><span class="p">;</span>
<span class="w"> </span><span class="kn">location</span><span class="w"> </span><span class="s">/</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kn">proxy_set_header</span><span class="w"> </span><span class="s">Host</span><span class="w"> </span><span class="nv">$http_host</span><span class="p">;</span>
<span class="w"> </span><span class="kn">proxy_pass</span><span class="w"> </span><span class="s">http://grafana</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="c1"># Proxy Grafana Live WebSocket connections.</span>
<span class="w"> </span><span class="kn">location</span><span class="w"> </span><span class="s">/api/live/</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kn">proxy_http_version</span><span class="w"> </span><span class="mi">1</span><span class="s">.1</span><span class="p">;</span>
<span class="w"> </span><span class="kn">proxy_set_header</span><span class="w"> </span><span class="s">Upgrade</span><span class="w"> </span><span class="nv">$http_upgrade</span><span class="p">;</span>
<span class="w"> </span><span class="kn">proxy_set_header</span><span class="w"> </span><span class="s">Connection</span><span class="w"> </span><span class="nv">$connection_upgrade</span><span class="p">;</span>
<span class="w"> </span><span class="kn">proxy_set_header</span><span class="w"> </span><span class="s">Host</span><span class="w"> </span><span class="nv">$http_host</span><span class="p">;</span>
<span class="w"> </span><span class="kn">proxy_pass</span><span class="w"> </span><span class="s">http://grafana</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>Une fois ces fichiers de configuration en place, il faut penser à la mise en place du <span class="caps">SSL</span> qui permet d’avoir une connexion sécurisée (https).</p>
<p>Il suffit de lancer cette ligne de commande et de suivre les questions posées :</p>
<div class="highlight"><pre><span></span><code>sudo certbot --nginx
</code></pre></div>
<p>Voilà ! A ce moment là, tout doit être fonctionnel, il ne reste plus qu’à configurer le Grafana pour grapher les données enregistrées dans InfluxDB.</p>Groupement d’achats & partage d’expérience2018-03-03T00:00:00+01:002018-03-03T00:00:00+01:00tag:blog.notmyidea.org,2018-03-03:/groupement-dachats-partage-dexperience.html<p>Il y a quelques années, on s’est motivé entre copains copines pour créer un groupement d’achat.</p>
<p>L’idée est simple:</p>
<ul>
<li>commander en gros, pour faire baisser les prix</li>
<li>se passer d’intermédiaires et favoriser les circuits courts</li>
<li>aller à la rencontre des producteurs locaux et échanger</li>
</ul>
<p>Notre groupement …</p><p>Il y a quelques années, on s’est motivé entre copains copines pour créer un groupement d’achat.</p>
<p>L’idée est simple:</p>
<ul>
<li>commander en gros, pour faire baisser les prix</li>
<li>se passer d’intermédiaires et favoriser les circuits courts</li>
<li>aller à la rencontre des producteurs locaux et échanger</li>
</ul>
<p>Notre groupement dessert actuellement 18 foyers et une 60aine de personnes.</p>
<p>Au fur et à mesure de la vie du groupement, on a développé quelques outils pour se simplifier la vie. Voici un retour d’expérience et quelques astuces / outils, au cas où l’envie vous prenne à vous aussi :)</p>
<h1 id="organisation">Organisation</h1>
<p>On organise environs trois ou quatre distributions par an. Le <em>modus operandi</em> est le suivant:</p>
<ul>
<li>chaque product·eur·rice à un·e référent·e, qui s’occupe de faire le lien;</li>
<li>une personne est désignée pour coordonner la distribution;</li>
<li>4 semaines avant la distribution, les référent·e·s mettent à jour les prix / produits dans le tableau de commandes;·e·</li>
<li>3 semaines avant la distribution, les commandes sont ouvertes;</li>
<li>2 semaines avant la distribution, les commandes sont closes;</li>
<li>Les référent·e·s ont ensuite deux semaines pour récupérer les commandes pour la distribution</li>
</ul>
<h1 id="quels-produits">Quels produits ?</h1>
<p>On essaye d’avoir uniquement des produits qui se conservent (on a également quelques autres produits plus frais, mais avec d’autres modalités).</p>
<p>Entre autres: bières, légumes secs, conserves, jus, miel, pâtes, semoule, café, vinaigres, pommes de terre, oignons, huiles, farines.</p>
<p>On essaye de faire du local puis du bio au plus proche plutôt que de trouver nécessairement les prix les plus bas. C’est une discussion qui revient assez
souvent, et donc un point à évoquer lors de la création pour avoir une posture
claire sur le sujet (tout le monde n’est pas animé par la même éthique !).</p>
<h1 id="paiements">Paiements</h1>
<p>Pour les paiements, on utilise autant que possible des chèques. Chaque référent·e paye la·le product·rice·eur en son nom, et lui demande d’attendre la date de la distribution pour l’encaissement. La plupart des producteurs acceptent d’être payés sous quinzaine.</p>
<p>Le jour de la distribution, tout le monde apporte son chéquier. Nous avons mis
en place une moulinette qui s’occupe de faire la répartition des chèques automatiquement, chaque membre se retrouve à faire en moyenne un ou deux chèques.</p>
<p>Chaque référent·e est ainsi remboursé·e de la somme avancée, et chaque
membre du groupement d’achat paye ce qu’il doit payer. Nous n’avons
volontairement pas de structure juridique et pas de compte en banque. Les
paiements s’effectuent directement entre nous.</p>
<h1 id="transports">Transports</h1>
<p>Chaque référent·e commande les produits, puis ensuite s’occupe de les rapatrier. À Rennes, on a la chance d’avoir pas mal de producteurs aux alentours, donc c’est assez simple.</p>
<p>Le mieux est de ramener les produits juste un peu avant la distribution au lieu de distribution, ça permet d’éviter de les stocker trop longtemps, et d’éviter aux producteurs d’attendre trop longtemps avant d’encaisser les chèques.</p>
<p>Pour les grosses commandes, les voitures se remplissent bien, mais ma petite Clio suffit, que ce soit dit !</p>
<h1 id="la-distribution">La distribution</h1>
<p>Un peu en amont de la distribution, il faut organiser l’espace. Des tas par membre sont constitués pour faciliter les choses le jour de la distribution.</p>
<p>Le jour même, on se retrouve, on charge ses marchandises, on échange quelques chèques et on papote ! On en profite pour:</p>
<ul>
<li>discuter de la date de la prochaine distribution;</li>
<li>trouver une nouvelle personne pour la coordonner;</li>
<li>discuter de nouveaux produits;</li>
<li>refaire le monde;</li>
<li>changer de référents pour les producteurs.</li>
</ul>
<p>Et c’est reparti pour un tour ;)</p>
<h1 id="nos-outils">Nos outils</h1>
<p>On utilise un tableur en ligne pour partager les prix et prendre les commandes. On a essayé d’utiliser <em>ethercalc</em> au début mais ça ne fonctionnait pas pour nous à l’époque (trop de petits bugs). On a donc préféré utiliser Google docs (ouch).</p>
<p>Il est d’ailleurs possible d’y intégrer de nouvelles fonctionnalités assez facilement, du coup Fred et Rémy ont planché sur un moyen d’automatiser la répartition des chèques (qu’on faisait dans un premier temps à la main - assez péniblement).</p>
<p>Le système n’est pas parfait mais fonctionne quand même assez bien !</p>
<p>Quelques ressources, donc:</p>
<ul>
<li><a href="https://gist.github.com/almet/8c77fafc9e487c02ded852ec4a91ae16">le code pour faire la répartition des chèques</a></li>
<li><a href="https://docs.google.com/spreadsheets/d/1bnPRSvf2Q2RDxKerWnEqUyJjuCFePnVMq6pWo8LeA_k/edit?usp=sharing">une version « à remplir » de notre tableau de commandes</a> (le mieux est d’en faire une copie !).</li>
</ul>
<p>Bon groupement d’achat ;)</p>Webnotes2018-02-25T00:00:00+01:002018-02-25T00:00:00+01:00tag:blog.notmyidea.org,2018-02-25:/webnotes.html<p>Quand je navigue en ligne, j’aime bien prendre des notes sur ce que je lis. C’est utile pour les retrouver plus tard. Il existe quelques outils pour ce genre de cas, mais j’ai vraiment eu du mal à trouver un outil qui faisais ce que je voulais …</p><p>Quand je navigue en ligne, j’aime bien prendre des notes sur ce que je lis. C’est utile pour les retrouver plus tard. Il existe quelques outils pour ce genre de cas, mais j’ai vraiment eu du mal à trouver un outil qui faisais ce que je voulais, de la manière que je voulais, c’est à dire:</p>
<ul>
<li>enregistrer une sélection de texte ainsi que son contexte: heure, site web.</li>
<li>fonctionner sur Firefox;</li>
<li>stocker mes notes à un endroit que je contrôle (ce sont mes données, après tout !)</li>
<li>rester en dehors de mon chemin: je suis en train de lire, pas en train d’organiser mes notes.</li>
<li>automatiquement partager les notes sur une page web.</li>
</ul>
<p>J’ai donc pris un peu de temps pour fabriquer mon outil de prises de notes, que j’ai baptisé « Webnotes ». C’est <a href="https://addons.mozilla.org/en-US/firefox/addon/wwebnotes/">une extension Firefox</a>, qui se configure assez simplement, et qui stocke les données dans une instance de <a href="http://kinto-storage.org/">Kinto</a>.</p>
<p><img src="https://github.com/almet/webnotes/blob/master/webnotes.gif?raw=true" /></p>
<p>C’est aussi simple que sélectionner du texte, faire « clic droit » puis « save as webnote », entrer un tag et le tour est joué !</p>
<p>Mes notes sont disponibles <a href="https://notes.notmyidea.org">sur notes.notmyidea.org</a>, et voici <a href="https://github.com/almet/webnotes">le lien vers les sources</a>, si ça vous intéresse de regarder comment ça fonctionne !</p>Comment est-ce que vous générez vos formulaires ?2016-05-31T00:00:00+02:002016-05-31T00:00:00+02:00tag:blog.notmyidea.org,2016-05-31:/comment-est-ce-que-vous-generez-vos-formulaires.html<p><span class="caps">TL</span>; <span class="caps">DR</span>: Je viens à peine de <em>releaser</em> la première version d’un service de génération de formulaires.
Allez jeter un coup d’œil sur <a href="https://www.fourmilieres.net">https://www.fourmilieres.net</a></p>
<p><em>En février 2012, je parlais ici <a href="https://blog.notmyidea.org/carto-forms.html">d’un service de génération de formulaires</a>.
Depuis, pas mal d’eau à coulé sous …</em></p><p><span class="caps">TL</span>; <span class="caps">DR</span>: Je viens à peine de <em>releaser</em> la première version d’un service de génération de formulaires.
Allez jeter un coup d’œil sur <a href="https://www.fourmilieres.net">https://www.fourmilieres.net</a></p>
<p><em>En février 2012, je parlais ici <a href="https://blog.notmyidea.org/carto-forms.html">d’un service de génération de formulaires</a>.
Depuis, pas mal d’eau à coulé sous les ponts, on est passé par pas mal d’étapes pour
finalement arriver à une première version de ce service de génération de
formulaires (à la </em>google forms<em>).</em></p>
<p>En tant qu’organisateurs d’évènements (petits et gros), je me retrouve souvent
dans une situation ou je dois créer des formulaires pour recueillir des
informations. Actuellement, la meilleure solution disponible est <em>Google Forms</em>,
mais celle ci à plusieurs problèmes, à commencer par le fait que le code n’est
pas libre et que les données sont stockées chez Google.</p>
<p>La plupart du temps, le besoin est assez simple: je veux spécifier quelques
questions, et donner un lien à mes amis pour qu’ils puissent y répondre.
Je reviens ensuite plus tard pour voir la liste des réponses apportées.</p>
<p><img alt="Capture de l'interface de création du formulaire" src="https://blog.notmyidea.org/images/formbuilder/formbuilder-build.png"></p>
<h2 id="fonctionnalites">Fonctionnalités</h2>
<p>Il existe pas mal de solutions techniques qui essayent de répondre à la même
problématique, mais la plupart d’entre elles sont assez souvent compliquées,
nécessitent de se créer un compte, et/ou ne vous laisse pas la main libre sur
les données générées, voire le code est assez difficile à faire évoluer ou à déployer.</p>
<p>Je voulais donc quelque chose de simple à utiliser <em>et</em> pour les créateurs de
formulaires <em>et</em> pour les utilisateurs finaux. Pas de chichis, juste quelques
vues, et des URLs à sauvegarder une fois l’opération terminée.</p>
<p><img alt="Capture de l'écran avec les URLs générées" src="https://blog.notmyidea.org/images/formbuilder/formbuilder-created.png">
<img alt="Capture d'écran d'un exemple de formulaire" src="https://blog.notmyidea.org/images/formbuilder/formbuilder-form.png"></p>
<h3 id="pas-de-compte">Pas de compte</h3>
<p>Vous n’avez pas besoin d’avoir un compte sur le site pour commencer à l’utiliser.
Vous créez simplement un nouveau formulaire puis envoyez le lien à vos amis pour
qu’eux puissent à leur tour le remplir.</p>
<p><img alt="Capture de la page d'accueil, ou aucun compte n'est requis" src="https://blog.notmyidea.org/images/formbuilder/formbuilder-welcome.png"></p>
<h3 id="gardez-la-main-sur-vos-donnees">Gardez la main sur vos données</h3>
<p>Une fois que vous avez récupéré les réponses à vos questions, vous pouvez
récupérer les données sur votre machines dans un fichier <code>.csv</code>.</p>
<p><img alt="Capture de la page de resultats, il est possible de télécharger en CSV." src="https://blog.notmyidea.org/images/formbuilder/formbuilder-results.png"></p>
<h3 id="api"><span class="caps">API</span></h3>
<p>L’ensemble des données sont en fait stockées dans <a href="https://kinto.readthedocs.org">Kinto</a>
qui est interrogeable très facilement en <span class="caps">HTTP</span>. Ce qui fait qu’il est très facile de
réutiliser les formulaires que vous avez construits (ou leurs réponses) depuis
d’autres outils.</p>
<h3 id="auto-hebergeable">Auto-hébergeable</h3>
<p>Un des objectifs de ce projet est de vous redonner la main sur vos données.
Bien sur, vous pouvez utiliser l’instance qui est mise à votre disposition sur
<a href="https://www.fourmilieres.net">wwww.fourmilieres.net</a>, mais vous pouvez
également l’héberger vous même très
simplement, et vous êtes d’ailleurs fortement encouragés à le faire ! Notre
objectif n’est pas de stocker l’ensemble des formulaires du monde, mais de
(re)donner le contrôle aux utilisateurs !</p>
<h2 id="on-commence-petit">On commence petit…</h2>
<p>Cette <em>release</em> n’est (bien sur) pas parfaite, et il reste encore pas mal de
travail sur cet outil, mais je pense qu’il s’agit d’une base de travail
intéressante pour un futur où Google n’a pas la main sur toutes nos données.</p>
<p>La liste des champs supportés est pour l’instant assez faible (Texte court,
Texte long, Oui/Non, choix dans une liste) mais elle à vocation à s’étendre, en
fonction des besoins de chacun.</p>
<p>J’ai d’ailleurs créé <a href="https://www.fourmilieres.net/#/form/cfd878264cec4ed2">un formulaire pour que vous puissiez me faire part de vos
retours</a>, n’hésitez pas !</p>
<h2 id="et-euh-comment-ca-marche">Et, euh, comment ça marche ?</h2>
<p>Le <em>formbuilder</em>, comme j’aime l’appeler se compose en fin de compte de deux
parties distinctes:</p>
<ul>
<li><a href="https://kinto.readthedocs.org">Kinto</a>, un service qui stocke
des données coté serveur et qui les expose via des <strong>APIs <span class="caps">HTTP</span></strong></li>
<li><a href="https://github.com/kinto/formbuilder">Le formbuilder</a>, une application
JavaScript qui ne tourne que coté client (dans votre navigateur) qui permet
de construire les formulaires et d’envoyer les données sur les <em>APIs</em> coté serveur.</li>
</ul>
<p>Au niveau de la <em>stack</em> technique, le <strong>formbuilder</strong> est codé en ReactJS. Un
des points techniques intéressants du projet est qu’il génère en fin de compte du
<a href="http://jsonschema.net/"><span class="caps">JSON</span> Schema</a>, un format de validation de données <em><span class="caps">JSON</span></em>.</p>
<p>Donc, reprenons! Vous arrivez sur la page d’accueil puis cliquez sur
“Create a new form”, puis vous vous retrouvez face à une interface ou vous pouvez
ajouter des champs de formulaire. Une fois ce travail effectué, vous appuyez sur
“Create the form”.</p>
<ul>
<li>Le <span class="caps">JSON</span> Schema est alors envoyé au serveur Kinto, qui l’utilisera pour valider
les données qu’il recevra par la suite.</li>
<li>Ce <span class="caps">JSON</span> Schema sera aussi utilisé lors de l’affichage du formulaire aux
personnes qui le remplissent.</li>
<li>Un jeton d’accès est généré et ajouté à l’<span class="caps">URL</span>, il s’agit de l’identifiant du formulaire.</li>
<li>Un second jeton d’accès administrateur et généré, il vous faut le garder de
coté pour avoir accès aux réponses.</li>
</ul>
<p>Bref, en espérant que ça vous serve ! Un petit pas dans la direction des données
rendues à leurs utilisateurs !</p>Avez vous confiance en SSL?2016-03-25T00:00:00+01:002016-03-25T00:00:00+01:00tag:blog.notmyidea.org,2016-03-25:/avez-vous-confiance-en-ssl.html<p>Dans le cadre <a href="http://autodefense-numerique.readthedocs.org/en/latest/">des ateliers d’autodéfense numérique</a>,
j’ai passé un peu de temps à creuser sur l’utilisation de <span class="caps">SSL</span> puisque
contrairement à ce que la plupart des personnes ont encore tendance à croire,
le petit cadenas (qui prouve qu’une connexion <span class="caps">SSL</span> est en cours) n’est …</p><p>Dans le cadre <a href="http://autodefense-numerique.readthedocs.org/en/latest/">des ateliers d’autodéfense numérique</a>,
j’ai passé un peu de temps à creuser sur l’utilisation de <span class="caps">SSL</span> puisque
contrairement à ce que la plupart des personnes ont encore tendance à croire,
le petit cadenas (qui prouve qu’une connexion <span class="caps">SSL</span> est en cours) n’est
<strong>absolument</strong> pas suffisant.</p>
<p>Allez hop, c’est parti pour:</p>
<ul>
<li>un tour d’horizon du fonctionnement de SSl</li>
<li>quelques moyens contourner cette “protection” en faisant une attaque en pratique</li>
<li>un tour des solutions existantes actuellement et de pourquoi je ne les trouve
pas vraiment satisfaisantes.</li>
</ul>
<h2 id="comment-fonctionne-ssl">Comment fonctionne <span class="caps">SSL</span>?</h2>
<p>Pour expliquer les problèmes de <span class="caps">SSL</span>, j’ai d’abord besoin d’expliquer comment
tout ça fonctionne.</p>
<p><span class="caps">SSL</span> repose sur l’utilisation de certificats, qui sont générés par des autorités
de certification (<em>Certificate Authority</em> que je nomme <em><span class="caps">CA</span></em> dans la suite de l’article).</p>
<p>Les certificats <span class="caps">SSL</span> permettent deux choses:</p>
<ul>
<li>De garantir que les communications entre les navigateurs (vous) et les sites
Web ne sont connues que du détenteur du certificat du site et de vous même.</li>
<li>De garantir que le site sur lequel vous vous connectez est bien celui que
vous imaginez.</li>
</ul>
<p>Le navigateur, lors d’une visite d’un site, va télécharger le certificat
associé puis vérifier que le certificat en question a bien été généré par un
des <em><span class="caps">CA</span></em> en qui il a confiance.</p>
<p>Imaginons maintenant qu’une des <em><span class="caps">CA</span></em> essaye de savoir ce qui s’échange entre
mon navigateur et le site de ma banque (protégé par <span class="caps">SSL</span>). Comment cela se
passerait il ?</p>
<p>N’importe quel <em><span class="caps">CA</span></em> peut donc générer des certificats pour n’importe quel site,
et le navigateur vérifierait, lui, que le certificat a bien été généré par une
<em><span class="caps">CA</span></em>.</p>
<p>Tout cela ne poserait pas de soucis si les <em><span class="caps">CA</span></em> étaient gérés de manière fiable,
mais il s’agit d’un travail compliqué, et certains <em><span class="caps">CA</span></em> ont par le passé montré
des faiblesses.</p>
<p>Par exemple, <a href="https://en.wikipedia.org/wiki/DigiNotar">DigiNotar</a> (un <em><span class="caps">CA</span></em> des Pays-Bas)
a été compromise et les attaquant.e.s ont pu générer des certificats <span class="caps">SSL</span>
frauduleux, ce qui leur a permis d’attaquer des sites tels que Facebook ou GMail.</p>
<p>Vous pouvez retrouver une liste des risques et menaces autour des <em><span class="caps">CA</span></em> <a href="http://wiki.cacert.org/Risk/History">sur le
wiki de CACert</a>.</p>
<h2 id="attaque-de-lhomme-du-milieu-avec-ssl">Attaque de l’homme du milieu avec <span class="caps">SSL</span></h2>
<p>A force de dire que c’était très facile à faire, j’ai eu envie d’essayer
d’espionner des connections protégées par <span class="caps">SSL</span>, et effectivement c’est
carrément flippant tellement c’est simple.</p>
<p>En l’espace de quelques minutes, il est possible de faire une <em>attaque de
l’homme du milieu</em> en utilisant par exemple un outil nommé <a href="http://docs.mitmproxy.org/en/stable">mitm-proxy</a>.</p>
<p>Pour déchiffrer l’ensemble du trafic <span class="caps">SSL</span>, j’ai simplement eu à lancer quelques
commandes et avoir un <em><span class="caps">CA</span></em> dans lequel le navigateur de la victime a confiance.
Je l’ai ajouté dans le navigateur cible pour simuler que je l’avais déjà
(c’est le cas si un des 1200 <span class="caps">CA</span> se fait pirater, ce qui me semble une surface
d’attaque assez large).</p>
<p>Je les colle ici si ça vous intéresse:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>sudo<span class="w"> </span>aptitude<span class="w"> </span>install<span class="w"> </span>mitmproxy
$<span class="w"> </span>mitm-proxy<span class="w"> </span>-T<span class="w"> </span>--host
</code></pre></div>
<p>Il faut faire croire à votre victime que vous êtes la passerelle vers
l’extérieur et à la passerelle que vous êtes la victime:</p>
<div class="highlight"><pre><span></span><code>arpspoof<span class="w"> </span>-i<span class="w"> </span>wlan0<span class="w"> </span>-t<span class="w"> </span>victime<span class="w"> </span>gateway
arpspoof<span class="w"> </span>-i<span class="w"> </span>wlan0<span class="w"> </span>-t<span class="w"> </span>gateway<span class="w"> </span>victime
</code></pre></div>
<p>Puis dire à notre fausse passerelle de rediriger le trafic des ports 80 et 443
vers notre proxy:</p>
<div class="highlight"><pre><span></span><code>sudo<span class="w"> </span>sysctl<span class="w"> </span>-w<span class="w"> </span>net.ipv4.ip_forward<span class="o">=</span><span class="m">1</span>
sudo<span class="w"> </span>iptables<span class="w"> </span>-t<span class="w"> </span>nat<span class="w"> </span>-A<span class="w"> </span>PREROUTING<span class="w"> </span>-i<span class="w"> </span>wlan0<span class="w"> </span>-p<span class="w"> </span>tcp<span class="w"> </span>--dport<span class="w"> </span><span class="m">443</span><span class="w"> </span>-j<span class="w"> </span>REDIRECT<span class="w"> </span>--to-port<span class="w"> </span><span class="m">4443</span>
sudo<span class="w"> </span>iptables<span class="w"> </span>-t<span class="w"> </span>nat<span class="w"> </span>-A<span class="w"> </span>PREROUTING<span class="w"> </span>-i<span class="w"> </span>wlan0<span class="w"> </span>-p<span class="w"> </span>tcp<span class="w"> </span>--dport<span class="w"> </span><span class="m">80</span><span class="w"> </span>-j<span class="w"> </span>REDIRECT<span class="w"> </span>--to-port<span class="w"> </span><span class="m">4443</span>
</code></pre></div>
<p>Et paf, <strong>on voit tout ce qui passe entre la machine et le serveur <span class="caps">SSL</span></strong>. On peut
d’ailleurs même imaginer faire tourner ces quelques commandes sur un
raspberry pi, pour aller encore plus vite…</p>
<h3 id="key-pinning-dans-les-navigateurs">Key-pinning dans les navigateurs</h3>
<p>Actuellement, n’importe quel <em><span class="caps">CA</span></em> peut générer des certificats pour
n’importe quel site, et c’est en grande partie ce qui pose souci. Une des
manières de faire évoluer la situation est d’épingler les certificats de
certains sites directement dans les navigateurs.</p>
<p>Cette approche a le mérite de fonctionner très bien <a href="https://dxr.mozilla.org/mozilla-central/source/security/manager/ssl/StaticHPKPins.h?from=StaticHPKPins.h">pour un petit nombre de
sites critiques (Google, Facebook, etc)</a>.</p>
<h3 id="http-public-key-pinning-hpkp"><span class="caps">HTTP</span> Public Key Pinning (<span class="caps">HPKP</span>)</h3>
<p><a href="https://developer.mozilla.org/en/docs/Web/Security/Public_Key_Pinning"><em><span class="caps">HTTP</span> Public Key Pinning</em></a>
est également une solution de <em>pinning</em> qui permet d’établir une confiance lors
de la première connexion avec le site. C’est ce qu’on appelle du <em>Trust on First
Use</em> ou <em><span class="caps">TOFU</span></em>.</p>
<p>Le navigateur va alors mettre ces informations dans un cache et vérifiera que
les certificats correspondent bien lors des prochaines visites.</p>
<p><em><span class="caps">HPKP</span></em> est disponible dans Firefox depuis Janvier 2015 et dans Chrome
depuis Octobre 2015.</p>
<h3 id="certificate-transparency-des-journaux-auditables">Certificate transparency: des journaux auditables</h3>
<p>Une autre approche est celle proposée par <em>certificate transparency</em>:</p>
<blockquote>
<p>Certificate Transparency aims to remedy these certificate-based threats by
making the issuance and existence of <span class="caps">SSL</span> certificates open to scrutiny by
domain owners, CAs, and domain users.</p>
<p>— <a href="https://www.certificate-transparency.org/what-is-ct">Certificate Transparency</a></p>
</blockquote>
<p>Autrement dit, avec ce système les <em><span class="caps">CA</span></em> doivent rendre public le fait qu’ils
aient signé de nouveaux certificats intermédiaires. La signature est ajoutée à
un journal sur lequel il n’est possible que d’écrire.</p>
<p>Les navigateurs vont alors vérifier que les certificats utilisés sont bien des
certificats qui ont été ajoutés au journal.</p>
<p>Ici, toute l’intelligence est dans la vérification de ces journaux, qui
permettent donc de valider/invalider des certificats racines ou intermédiaires.</p>
<p>Il me semble donc qu’il serait possible d’ajouter un certificat frauduleux le
temps d’une attaque (et celui ci serait détecté et supprimé ensuite).</p>
<p><em>Certificate-Transparency</em> n’est donc pas une solution contre une écoute
globale mise en place par les gouvernements par exemple.</p>
<p>Si vous lisez bien l’anglais, je vous invite à aller lire
<a href="http://security.stackexchange.com/a/52838">cette description du problème et de la solution</a>
que je trouve très bien écrite.</p>
<h3 id="dane-dnssec"><span class="caps">DANE</span> + <span class="caps">DNSSEC</span></h3>
<blockquote>
<p>The <span class="caps">DANE</span> working group has developed a framework for securely
retrieving keying information from the <span class="caps">DNS</span> [<span class="caps">RFC6698</span>]. This
framework allows secure storing and looking up server public key
information in the <span class="caps">DNS</span>. This provides a binding between a domain
name providing a particular service and the key that can be used
to establish encrypted connection to that service.</p>
<p>— <a href="https://datatracker.ietf.org/wg/dane/charter/">Dane <span class="caps">WG</span></a></p>
</blockquote>
<p>Une autre solution est appelée “<span class="caps">DANE</span>” et repose par dessus le protocole
<em><span class="caps">DNSSEC</span></em>.</p>
<p>Je connais assez mal <em><span class="caps">DNSSEC</span></em> donc j’ai passé un peu de temps à lire des
documents. L’impression finale que ça me laisse est que le problème est
exactement le même que pour <span class="caps">SSL</span>: un certain nombre de personnes détiennent les
clés et toute la sécurité repose sur cette confiance. Or il est possible que
ces clés soient détenues par des personnes non dignes de confiance.</p>
<blockquote>
<p>Secure <span class="caps">DNS</span> (<span class="caps">DNSSEC</span>) uses cryptographic digital signatures signed with a
trusted public key certificate to determine the authenticity of data.
— https://en.wikipedia.org/wiki/DNS_spoofing</p>
</blockquote>
<p>Et aussi:</p>
<blockquote>
<p>It is widely believed[1] that securing the <span class="caps">DNS</span> is critically important for
securing the Internet as a whole, but deployment of <span class="caps">DNSSEC</span> specifically has
been hampered (As of 22 January 2010) by several difficulties:</p>
<ul>
<li>The need to design a backward-compatible standard that can scale to the
size of the Internet</li>
<li>Prevention of “zone enumeration” (see below) where desired</li>
<li>Deployment of <span class="caps">DNSSEC</span> implementations across a wide variety of <span class="caps">DNS</span> servers
and resolvers (clients)</li>
<li>Disagreement among implementers over who should own the top-level domain
root keys Overcoming the perceived complexity of <span class="caps">DNSSEC</span> and <span class="caps">DNSSEC</span> deployment</li>
</ul>
</blockquote>
<h2 id="solutions-basees-sur-la-blockchain">Solutions basées sur la blockchain</h2>
<p>Une dernière piste semble être l’utilisation de la <em>blockchain</em> pour distribuer
des clés par site.</p>
<p>La solution <em>DNSChain</em> me paraissait tout d’abord un bon point de départ mais
la lecture de <a href="https://www.indolering.com/okturtles-dnschain-unblock-us">quelques critiques</a>
et interventions du développeur du projet m’ont fait changer d’avis.</p>
<p>Reste encore la piste de <em>Namecoin Control</em> que je n’ai pas encore creusée.
Peut-être pour un prochain billet. Toute piste de réflexion est bien sur la
bienvenue sur ces sujets!</p>Retours sur un atelier ZeroNet2016-03-17T00:00:00+01:002016-03-17T00:00:00+01:00tag:blog.notmyidea.org,2016-03-17:/retours-sur-un-atelier-zeronet.html<p>Mardi dernier se tenait <a href="http://biblio.insa-rennes.fr/crypto">une <em>cryptoparty</em></a> dans les locaux de l’<span class="caps">INSA</span> de Rennes.</p>
<p>L’évènement s’étant rempli au delà de toutes les espérances, on m’a proposé de
venir y tenir un atelier, que j’ai proposé sur <a href="https://zeronet.io">ZeroNet</a>, un
petit projet fort sympathique qui pourrait devenir une …</p><p>Mardi dernier se tenait <a href="http://biblio.insa-rennes.fr/crypto">une <em>cryptoparty</em></a> dans les locaux de l’<span class="caps">INSA</span> de Rennes.</p>
<p>L’évènement s’étant rempli au delà de toutes les espérances, on m’a proposé de
venir y tenir un atelier, que j’ai proposé sur <a href="https://zeronet.io">ZeroNet</a>, un
petit projet fort sympathique qui pourrait devenir une nouvelle manière de
distribuer le Web, permettant notamment d’éviter la censure.</p>
<p>Avant toute autre chose, merci énormément à l’équipe de la bibliothèque de
l’<span class="caps">INSA</span> pour l’organisation de cet évènement qui à une réelle portée politique.</p>
<h2 id="un-peu-dhistoire">Un peu d’histoire</h2>
<p>Il me semble que Tim Bernes Lee (l’inventeur du Web) avait prévu le Web comme un
protocole décentralisé. Chacun hébergerait ses données et les servirait aux
autres, qui pourraient alors y accéder.</p>
<p>Avec ce fonctionnement, impossible alors d’accéder à des sites si leur auteur
n’est pas en ligne. Qu’à cela ne tienne, on s’est mis à avoir des machines qui
restent connectées au réseau 24 heures par jour. Et puis une machine ne
suffisant plus, on a eu des fermes de machines dans des <em>data centers</em> etc afin
de supporter les milliers d’utilisateurs des sites.</p>
<h2 id="un-web-decentralise">Un Web décentralisé</h2>
<p>ZeroNet permet (entre autres) de répondre à ce problème en proposant une manière alternative de <strong>distribuer le Web</strong>, en pair à pair. Lors d’une visite d’un site:</p>
<ol>
<li>Vous contactez un <em>tracker</em> BitTorrent pour connaitre la liste des autres
visiteurs du site (les <em>pairs</em>).</li>
<li>Vous demandez aux <em>pairs</em> de vous donner les fichiers du site.</li>
<li>Vous validez que les fichiers servis sont bien les bons (en vérifiant la
signature attachée).</li>
</ol>
<p>N’importe quel visiteur devient alors un <em>pair</em>, qui sert le site aux autres visiteurs.</p>
<p>Parmi les nombreux avantages de cette approche, je note particulièrement que:</p>
<ul>
<li>Il est très difficile de censurer un site — Il est sur l’ensemble des machines
des visiteurs.</li>
<li>Les attaques par <em>fingerprinting</em> sont impossibles: le navigateur Web se
connecte à un serveur <em>proxy</em> local.</li>
<li>Vous détenez directement vos données et (par design) ne les donnez pas à des
silos (Facebook, Google, etc.)</li>
</ul>
<p>Si vous êtes interessés par une démonstration rapide, j’ai enregistré une vidéo
de 10 minutes où je parle en anglais avec une voix très grave.</p>
<video controls="" src="http://alexis.notmyidea.org/zeronet.webm" width=800></video>
<h2 id="atelier">Atelier</h2>
<p>Pour l’atelier, j’ai choisi de faire une présentation rapide du projet (<a href="https://blog.notmyidea.org/docs/zeronet-presentation-fr.pdf">j’ai
traduit les slides</a> anglais
pour l’occasion — <a href="https://docs.google.com/presentation/d/158C_-V1ueNaaKHMBMBgGOVhunb9xrXzB3hC_g1N53c0/edit?usp=sharing">accès aux sources</a>)
avant d’installer ZeroNet sur les machines et de l’utiliser pour publier un site.</p>
<h3 id="partager-sur-le-reseau-local">Partager sur le réseau local</h3>
<p>Nous avons eu des soucis à cause du réseau (un peu congestionné) sur lequel
les ports utilisés pour la discussion entre <em>pairs</em> étaient fermés. Il est bien
sur possible de faire tourner le tout de manière indépendante du reste du réseau,
mais je n’avais pas prévu le coup.</p>
<p>Voici donc comment faire pour contourner le souci:</p>
<ol>
<li>Installer et lancer un <em>tracker</em> BitTorrent (De manière surprenante,
<a href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=685575">rien n’est packagé pour debian pour l’instant</a>)
J’ai choisi d’installer <a href="http://erdgeist.org/arts/software/opentracker/#build-instructions">OpenTracker</a></li>
<li>Ensuite lancer ZeroNet avec des options spécifiques.</li>
</ol>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>python<span class="w"> </span>zeronet.py<span class="w"> </span>--trackers<span class="w"> </span>udp://localhost:6969<span class="w"> </span>--ip_external<span class="w"> </span><span class="m">192</span>.168.43.207
$<span class="w"> </span>python<span class="w"> </span>zeronet.py<span class="w"> </span>--trackers<span class="w"> </span>udp://192.168.43.207:6969<span class="w"> </span>--ip_external<span class="w"> </span><span class="m">192</span>.168.43.172
</code></pre></div>
<p>Il est nécessaire de spécifier l’adresse <span class="caps">IP</span> externe que chaque nœud expose pour
éviter qu’elle n’essaye d’aller la trouver par elle même: nous voulons l’adresse
du réseau local, et non pas l’adresse internet.</p>
<p>La prochaine fois je tenterais de venir avec un HotSpot Wifi et un tracker
BitTorrent dans la poche!</p>
<h2 id="questions-reponses">Questions / Réponses</h2>
<p>Il y avait quelques questions intéressantes auxquelles je n’ai pas toujours su
répondre sur le moment. Après quelques recherches, je rajoute des détails ici.</p>
<h3 id="torrent-tor-breche-de-secu">Torrent + Tor = brèche de sécu ?</h3>
<p>Il me semblait avoir entendu parler de problèmes de <em>dé-anonymisation</em>
<a href="https://hal.inria.fr/file/index/docid/471556/filename/TorBT.pdf">lors de l’utilisation de BitTorrent par dessus Tor</a>.</p>
<blockquote>
<p>Dans certains cas, certains clients torrents (uTorrent, BitSpirit, etc)
écrivent directement votre adresse <span class="caps">IP</span> dans l’information qui est envoyée
au tracker et/ou aux autres pairs.
— https://blog.torproject.org/blog/bittorrent-over-tor-isnt-good-idea</p>
</blockquote>
<p><a href="https://github.com/HelloZeroNet/ZeroNet/issues/274">Ce n’est pas le cas de ZeroNet</a>, ce qui évacue le souci.</p>
<h3 id="zeromail-cest-lent-non">ZeroMail, c’est lent non ?</h3>
<p>Une des applications de démo, <em>ZeroMail</em>, propose un mécanisme qui permet de
s’envoyer des messages chiffrés sur un réseau pair à pair. L’approche choisie
est de chiffrer les messages avec la clé du destinataire et de le mettre dans
un <em>pot commun</em>. Tout le monde essaye de déchiffrer tous les messages, mais ne
peut déchiffrer que les siens.</p>
<p>Cela permet de ne <strong>pas</strong> fuiter de méta-données, <a href="https://blog.notmyidea.org/les-problemes-de-pgp.html">à l’inverse de <span class="caps">PGP</span></a>.</p>
<p>Je n’ai en fait pas de réponse claire à donner à cette question: l’auteur de
ZeroNet me disait que <span class="caps">10MB</span> (la limite de taille d’un site, par défaut)
correspondait à beaucoup de place pour stocker des messages, et qu’il était
possible de supprimer les anciens messages une fois qu’ils sont lus par exemple.</p>
<p>Une autre solution à laquelle je pensait était de créer un <em>ZeroSite</em> pour
chaque récipient, mais on connait à ce moment là le nombre de messages qu’un
utilisateur peut recevoir.</p>
<p>Je vois plusieurs problèmes avec le design actuel de ZeroMail (il me semble
assez facile d’y faire un déni de service par exemple). A creuser.</p>
<h3 id="comment-heberger-des-tres-gros-sites">Comment héberger des très gros sites ?</h3>
<p>Par exemple, comment faire pour héberger Wikipedia ?</p>
<p>Il semble que la meilleure manière de faire serait de séparer Wikipedia en
un tas de petites ressources (par catégorie par ex.). Les gros médias pourraient
être considérés optionnels (et donc téléchargés uniquement à la demande)</p>
<h3 id="est-ce-quon-a-vraiment-besoin-dun-tracker">Est-ce qu’on à vraiment besoin d’un tracker ?</h3>
<p>Le support d’une <span class="caps">DHT</span> <a href="https://github.com/HelloZeroNet/ZeroNet/issues/57">est souhaité</a>,
mais pour l’instant pas encore implémenté. L’utilisation de la <span class="caps">DHT</span> BitTorrent
n’est pas une option puisque <a href="https://github.com/HelloZeroNet/ZeroNet/issues/57">Tor ne supporte pas <span class="caps">UDP</span></a>.</p>Service de nuages : Garantir l’intégrité des données via des signatures2016-03-01T00:00:00+01:002016-03-01T00:00:00+01:00tag:blog.notmyidea.org,2016-03-01:/service-de-nuages-garantir-lintegrite-des-donnees-via-des-signatures-fr.html<p class="first last">Comment garantir l’intégrité des données en utilisant les signatures.</p>
<p><em>Cet article est repris depuis le blog « Service de Nuages » de mon équipe à Mozilla</em></p>
<p>Dans le cadre du projet <a class="reference external" href="https://wiki.mozilla.org/Firefox/Go_Faster">Go Faster</a>, nous souhaitons distribuer des
mises à jour de parties de <em>Firefox</em> de manière séparée des mises à jour majeures
(qui ont lieu toutes les 6 semaines).</p>
<p>Les données que nous souhaitons mettre à jour sur les clients sont multiples.
Entre autres, nous souhaitons gérer <a class="reference external" href="https://blog.mozilla.org/security/2015/03/03/revoking-intermediate-certificates-introducing-onecrl/">la mise à jour des listes de révocation
(<span class="caps">CRL</span>) de certificats <span class="caps">SSL</span></a>.</p>
<p>Il est évidemment nécessaire de s’assurer que les données qui sont téléchargées
sur les client sont légitimes : que personne ne tente d’invalider des
certificats alors qu’ils sont valides, et que l’ensemble des mises à jour sont
bel et bien récupérées sur le client.</p>
<p>La signature garantit qu’une mise à jour contient tous les enregistrements, mais il
est toujours possible de bloquer l’accès au service (par exemple avec le <em>china
great firewall</em>).</p>
<p>Ce mécanisme fonctionne pour les listes de certificats à révoquer, mais pas
uniquement. Nous comptons réutiliser ce même fonctionnement dans le futur pour
la mise à jour d’autres parties de Firefox, et vous pouvez également en tirer
parti pour d’autres cas d’utilisation.</p>
<p>Nous souhaitons utiliser <a class="reference external" href="https://kinto.readthedocs.org">Kinto</a> afin
de distribuer ces jeux de données. Un des avantages est que l’on peut
facilement <em>cacher</em> les collections derrière un <span class="caps">CDN</span>.</p>
<p>Par contre, nous ne souhaitons pas que les clients fassent
confiance aveuglément, ni au serveur Kinto, ni au <span class="caps">CDN</span>.</p>
<p>Effectivement, un attaquant, contrôlant l’un ou l’autre, pourrait
alors envoyer les mises à jour qu’il souhaite à l’ensemble des clients
ou supprimer des certificats révoqués. Imaginez le carnage !</p>
<p>Afin de résoudre ce problème, considérons les conditions suivantes:</p>
<ul class="simple">
<li>La personne qui a le pouvoir de mettre à jour les <span class="caps">CRL</span> (<em>l’updater</em>)
a accès à une cle de signature (ou mieux, <a class="reference external" href="https://fr.wikipedia.org/wiki/Hardware_Security_Module">un <span class="caps">HSM</span></a>) qui lui permet de
signer la collection;</li>
<li>Le pendant public de ce certificat est stocké et distribué dans Firefox;</li>
<li>Le <em>hashing</em> et la <em>signature</em> sont faits côté client pour éviter certains
vecteurs d’attaque (si un attaquant a la main sur le serveur Kinto par exemple).</li>
</ul>
<p>Le chiffrement à sens unique, aussi appellé <em>hashing</em> est un moyen de toujours
obtenir le même résultat à partir de la même entrée.</p>
<div class="section" id="premier-envoi-de-donnees-sur-kinto">
<h2>Premier envoi de données sur Kinto</h2>
<p>L’ensemble des données est récupéré depuis une source <em>sécurisée</em> puis mis dans
une collection <span class="caps">JSON</span>. Chaque élément contient un identifiant unique généré sur
le client.</p>
<p>Par exemple, un enregistrement peut ressembler à :</p>
<div class="highlight"><pre><span></span><span class="p">{</span><span class="s2">"id"</span><span class="o">:</span><span class="w"> </span><span class="s2">"b7dded96-8df0-8af8-449a-8bc47f71b4c4"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"fingerprint"</span><span class="o">:</span><span class="w"> </span><span class="s2">"11:D5:D2:0A:9A:F8:D9:FC:23:6E:5C:5C:30:EC:AF:68:F5:68:FB:A3"</span><span class="p">}</span>
</pre></div>
<p>Le <em>hash</em> de la collection est ensuite calculé, signé puis envoyé au serveur
(voir plus bas pour les détails).</p>
<p>La signature est déportée sur un service qui ne s’occupe que de ça, puisque la
sécurité du certificat qui s’occupe des signatures est extrêmement importante.</p>
</div>
<div class="section" id="comment-verifier-l-integrite-des-donnees">
<h2>Comment vérifier l’intégrité des données ?</h2>
<p>Premièrement, il faut récupérer l’ensemble des enregistrements présents sur
le serveur, ainsi que le <em>hash</em> et la signature associée.</p>
<p>Ensuite, vérifier la signature du <em>hash</em>, pour s’assurer que celui-ci provient
bien d’un tiers de confiance.</p>
<p>Finalement, recalculer le <em>hash</em> localement et valider qu’il correspond bien à
celui qui a été signé.</p>
</div>
<div class="section" id="ajouter-de-nouvelles-donnees">
<h2>Ajouter de nouvelles données</h2>
<p>Pour l’ajout de nouvelles données, il est nécessaire de s’assurer que les
données que l’on a localement sont valides avant de faire quoi que ce soit d’autre.</p>
<p>Une fois ces données validées, il suffit de procéder comme la première fois, et
d’envoyer à nouveau le <em>hash</em> de la collection au serveur.</p>
</div>
<div class="section" id="comment-calculer-ce-hash">
<h2>Comment calculer ce hash ?</h2>
<p>Pour calculer le <em>hash</em> de la collection, il est nécessaire :</p>
<ol class="arabic simple">
<li>D’ordonner l’ensemble des éléments de la collection (par leur id) ;</li>
<li>Pour chaque élément, sérialiser les champs qui nous intéressent (les
concaténer clé + valeur)</li>
<li>Calculer le <em>hash</em> depuis la sérialisation.</li>
</ol>
<p>Nous sommes encore incertains de la manière dont le hash va être calculé. Les <a class="reference external" href="https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41"><span class="caps">JSON</span> Web Signature</a> semblent
une piste intéressante. En attendant, une implementation naïve en python
pourrait ressembler à ceci :</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">json</span>
<span class="kn">import</span> <span class="nn">hashlib</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span><span class="s2">"id"</span><span class="p">:</span> <span class="s2">"b7dded96-8df0-8af8-449a-8bc47f71b4c4"</span><span class="p">,</span>
<span class="s2">"fingerprint"</span><span class="p">:</span> <span class="s2">"11:D5:D2:0A:9A:F8:D9:FC:23:6E:5C:5C:30:EC:AF:68:F5:68:FB:A3"</span><span class="p">},</span>
<span class="p">{</span><span class="s2">"id"</span><span class="p">:</span> <span class="s2">"dded96b7-8f0d-8f8a-49a4-7f771b4c4bc4"</span><span class="p">,</span>
<span class="s2">"fingerprint"</span><span class="p">:</span> <span class="s2">"33:6E:5C:5C:30:EC:AF:68:F5:68:FB:A3:11:D5:D2:0A:9A:F8:D9:FC"</span><span class="p">}]</span>
<span class="n">m</span> <span class="o">=</span> <span class="n">hashlib</span><span class="o">.</span><span class="n">sha256</span><span class="p">()</span>
<span class="n">m</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="kc">True</span><span class="p">))</span>
<span class="n">collection_hash</span> <span class="o">=</span> <span class="n">m</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">()</span>
</pre></div>
</div>
Let’s Encrypt + HAProxy2016-02-11T00:00:00+01:002016-02-11T00:00:00+01:00tag:blog.notmyidea.org,2016-02-11:/lets-encrypt-haproxy.html<p><em>Note : Cet article n’est plus à jour. Il est maintenant (2018) possible d’installer des certificats <span class="caps">SSL</span> Let’s Encrypt d’une manière beaucoup plus simple, en utilisant certbot (et le plugin nginx <code>certbot --nginx</code>).</em></p>
<blockquote>
<p>It’s time for the Web to take a big step forward in terms …</p></blockquote><p><em>Note : Cet article n’est plus à jour. Il est maintenant (2018) possible d’installer des certificats <span class="caps">SSL</span> Let’s Encrypt d’une manière beaucoup plus simple, en utilisant certbot (et le plugin nginx <code>certbot --nginx</code>).</em></p>
<blockquote>
<p>It’s time for the Web to take a big step forward in terms of security
and privacy. We want to see <span class="caps">HTTPS</span> become the default. Let’s Encrypt
was built to enable that by making it as easy as possible to get and
manage certificates.</p>
<p>— <a href="https://letsencrypt.org/">Let’s Encrypt</a></p>
</blockquote>
<p>Depuis début Décembre, la nouvelle <em>autorité de certification</em> Let’s
Encrypt est passée en version <em>Beta</em>. Les certificats <span class="caps">SSL</span> sont un moyen
de 1. chiffrer la communication entre votre navigateur et le serveur et
2. un moyen d’être sur que le site Web auquel vous accédez est celui
auquel vous pensez vous connecter (pour éviter des <a href="https://fr.wikipedia.org/wiki/Attaque_de_l'homme_du_milieu">attaques de l’homme
du milieu</a>).</p>
<p>Jusqu’à maintenant, il était nécessaire de payer une entreprise pour
faire en sorte d’avoir des certificats qui évitent d’avoir ce genre
d’erreurs dans vos navigateurs:</p>
<p><img alt="Message de firefox lorsque une connexion n'est pas
sécurisée." src="%7Bfilename%7D/static/unsecure-connection.png"></p>
<p>Maintenant, grâce à Let’s Encrypt il est possible d’avoir des
certificats <span class="caps">SSL</span> <strong>gratuits</strong>, ce qui représente un grand pas en avant
pour la sécurité de nos communications.</p>
<p>Je viens de mettre en place un procédé (assez simple) qui permet de
configurer votre serveur pour générer des certificats <span class="caps">SSL</span> valides avec
Let’s Encrypt et le répartiteur de charge
<a href="http://www.haproxy.org/">HAProxy</a>.</p>
<p>Je me suis basé pour cet article sur
d’<a href="https://blog.infomee.fr/p/letsencrypt-haproxy">autres</a>
<a href="http://blog.victor-hery.com/article22/utiliser-let-s-encrypt-avec-haproxy">articles</a>,
dont je vous recommande la lecture pour un complément d’information.</p>
<h2 id="validation-des-domaines-par-lets-encrypt">Validation des domaines par Let’s Encrypt</h2>
<p>Je vous passe les détails d’installation du client de Let’s Encrypt, qui
sont <a href="https://github.com/letsencrypt/letsencrypt#installation">très bien expliqués sur leur
documentation</a>.</p>
<p>Une fois installé, vous allez taper une commande qui va ressembler à:</p>
<div class="highlight"><pre><span></span><code>letsencrypt-auto certonly --renew-by-default
--webroot -w /home/www/letsencrypt-requests/ \
-d hurl.kinto-storage.org \
-d forums.kinto-storage.org
</code></pre></div>
<p>Le <em>webroot</em> est l’endroit ou les preuves de détention du domaine vont
être déposées.</p>
<p>Lorsque les serveurs de Let’s Encrypt vont vouloir vérifier que vous
êtes bien à l’origine des demandes de certificats, ils vont envoyer une
requête <span class="caps">HTTP</span> sur <code>http://domaine.org/.well-known/acme-challenge</code>, ou il
voudra trouver des informations qu’il aura généré via la commande
<code>letsencrypt-auto</code>.</p>
<p>J’ai choisi de faire une règle dans haproxy pour diriger toutes les
requêtes avec le chemin <code>.well-known/acme-challenge</code> vers un <em>backend</em>
nginx qui sert des fichiers statiques (ceux contenus dans
<code>/home/www/letsencrypt-requests/</code>).</p>
<p>Voici la section de la configuration de HAProxy (et <a href="https://github.com/almet/infra/blob/master/haproxy/haproxy.cfg#L63-L72">la configuration
complete</a>
si ça peut être utile):</p>
<div class="highlight"><pre><span></span><code>frontend http
bind 0.0.0.0:80
mode http
default_backend nginx_server
acl letsencrypt_check path_beg /.well-known/acme-challenge
use_backend letsencrypt_backend if letsencrypt_check
redirect scheme https code 301 if !{ ssl_fc } !letsencrypt_check
backend letsencrypt_backend
http-request set-header Host letsencrypt.requests
dispatch 127.0.0.1:8000
</code></pre></div>
<p>Et celle de <span class="caps">NGINX</span>:</p>
<div class="highlight"><pre><span></span><code>server {
listen 8000;
server_name letsencrypt.requests;
root /home/www/letsencrypt-requests;
}
</code></pre></div>
<h2 id="installation-des-certificats-dans-haproxy">Installation des certificats dans HAProxy</h2>
<p>Vos certificats <span class="caps">SSL</span> devraient être générés dans <code>/etc/letsencrypt/live</code>,
mais ils ne sont pas au format attendu par haproxy. Rien de grave, la
commande suivant convertit l’ensemble des certificats en une version
compatible avec HAProxy:</p>
<div class="highlight"><pre><span></span><code>cat /etc/letsencrypt/live/domaine.org/privkey.pem /etc/letsencrypt/live/domaine.org/fullchain.pem > /etc/ssl/letsencrypt/domaine.org.pem
</code></pre></div>
<p>Et ensuite dans la configuration de haproxy, pour le (nouveau)
<em>frontend</em> https:</p>
<div class="highlight"><pre><span></span><code>bind 0.0.0.0:443 ssl no-sslv3 crt /etc/ssl/letsencrypt
</code></pre></div>
<p>Faites bien attention à avoir un <em>frontend</em> https pour tous vos sites en
<span class="caps">HTTPS</span>. <a href="https://github.com/almet/infra/blob/master/haproxy/haproxy.cfg#L38-L60">Pour moi cela ressemble à
ça</a>.</p>
<p>Une fois tout ceci fait, redémarrez votre service haproxy et zou !</p>
<h2 id="automatisation">Automatisation</h2>
<p>Pour automatiser un peu tout ça, j’ai choisi de faire ça comme suit:</p>
<ul>
<li>Un fichier domaine dans <code>letsencrypt/domains/domain.org</code> qui
contient le script <code>letsencrypt</code>.</li>
<li>Un fichier d’installation de certificats dans
<code>letsencrypt/install-certs.sh</code> qui s’occupe d’installer les
certificats déjà générés.</li>
</ul>
<p>Et voila ! <a href="https://github.com/almet/infra/">Le tout est dans un dépot
github</a>, si jamais ça peut vous servir,
tant mieux !</p>Ateliers d’autodéfense numérique2016-01-14T00:00:00+01:002016-01-14T00:00:00+01:00tag:blog.notmyidea.org,2016-01-14:/ateliers-dautodefense-numerique.html<p>Il y a huit mois, je me rendais compte de l’importance du choix des
outils pour faire face à la surveillance généralisée, et notamment en
rapport au chiffrement des données. Une de mes envies de l’époque était
l’animation d’ateliers.</p>
<blockquote>
<p>Je compte donc:</p>
<ul>
<li>Organiser des ateliers de …</li></ul></blockquote><p>Il y a huit mois, je me rendais compte de l’importance du choix des
outils pour faire face à la surveillance généralisée, et notamment en
rapport au chiffrement des données. Une de mes envies de l’époque était
l’animation d’ateliers.</p>
<blockquote>
<p>Je compte donc:</p>
<ul>
<li>Organiser des ateliers de sensibilisation aux outils de
communication, envers mes proches;</li>
<li>Utiliser la communication chiffrée le plus souvent possible, au
moins pour rendre le déchiffrement des messages plus longue,
“noyer le poisson”.</li>
</ul>
<p>— <a href="http://blog.notmyidea.org/chiffrement.html">Chiffrement</a></p>
</blockquote>
<p>J’ai mis un peu de temps à mettre le pied à l’étrier, mais je ressors
finalement du premier atelier que j’ai co-animé avec geb, auprès d’un
public de journalistes.</p>
<p>Pour cette première édition l’idée était à la fois d’aller à la
rencontre d’un public que je connais mal, de leur donner des outils pour
solutionner les problèmes auxquels ils font parfois face, et de me faire
une idée de ce que pouvait être un atelier sur l’autodéfense numérique.</p>
<p>L’objectif pour ce premier atelier était de:</p>
<ol>
<li>Échanger autour des besoins et <strong>faire ressortir des histoires</strong> ou
le manque d’outillage / connaissances à posé problème, dans des
situations concrètes;</li>
<li>Se rendre compte des “conduites à risque”, <strong>faire peur</strong> aux
personnes formées pour qu’elles se rendent compte de l’état actuel
des choses;</li>
<li><strong>Proposer des solutions concrètes</strong> aux problèmes soulevés, ainsi
que le minimum de connaissance théorique pour les appréhender.</li>
</ol>
<h2 id="1-faire-ressortir-les-problemes">1. Faire ressortir les problèmes</h2>
<p>Afin de faire ressortir les problèmes, nous avons choisi de constituer
des petits groupes de discussion, afin de faire des “Groupes d’Interview
Mutuels”, ou “<span class="caps">GIM</span>”:</p>
<blockquote>
<p>l’animateur invite les participants à se regrouper par trois, avec des
personnes qu’on connaît moins puis invite chacun à livrer une
expérience vécue en lien avec le thème de la réunion et les deux
autres à poser des questions leur permettant de bien saisir ce qui a
été vécu.</p>
<p>— «<a href="http://www.scoplepave.org/pour-s-ecouter">Pour s’écouter</a>», <span class="caps">SCOP</span>
Le Pavé.</p>
</blockquote>
<p>De ces <em>GIMs</em> nous avons pu ressortir quelques histoires, gravitant
autour de:</p>
<ul>
<li><strong>La protection des sources (d’information)</strong>: Comment faire pour
aider quelqu’un à faire “fuiter” des données depuis l’intérieur
d’une entreprise ?</li>
<li><strong>Le chiffrement de ses données</strong>: Comment éviter de faire “fuiter”
des données importantes lors d’une perquisition de matériel ?</li>
</ul>
<h2 id="2-faire-peur">2. Faire peur</h2>
<p>Un des premiers objectifs est de faire peur, afin que tout le monde se
rende compte à quel point il est facile d’accéder à certaines données.
<a href="http://blog.barbayellow.com/">Grégoire</a> m’avait conseillé quelques
petites accroches qui ont ma foi bien marché:</p>
<p>J’ai demandé aux présent.e.s de:</p>
<ul>
<li>donner leur mot de passe à voix haute devant les autres: a priori
personne ne le fera;</li>
<li>venir se connecter à leur compte email depuis mon ordinateur. J’ai
piégé une personne, qui est venu pour taper son mot de passe.</li>
</ul>
<p>Cela à été un bon moyen de parler de l’importance des traces que l’on
peut laisser sur un ordinateur, et de la confiance qu’il faut avoir dans
le matériel que l’on utilise, à fortiori si ce ne sont pas les vôtres.</p>
<p>Pour continuer à leur faire peur, après une brève explication de ce
qu’est <span class="caps">SSL</span> nous avons montré comment il était facile de scruter le
réseau à la recherche de mots de passe en clair.</p>
<h2 id="3-proposer-des-solutions-concretes">3. Proposer des solutions concrêtes</h2>
<p>Une fois que tout le monde avait pleinement pris sonscience des
problématiques et n’osait plus utiliser son ordinateur ou son
téléphone, on à commencé à parler de quelques solutions. Plusieurs
approches étaient possibles ici, nous avons choisi de présenter quelques
outils qui nous semblaient répondre aux attentes:</p>
<ul>
<li>On a expliqué ce qu’était <a href="https://tails.boum.org">Tails</a>, et
comment l’utiliser et le dupliquer.</li>
<li>On a pu faire un tour des outils existants sur Tails, notamment
autour de l’<em>anonymisation</em> de fichiers et la suppression effective
de contenus.</li>
<li>Certaines personnes ont pu créer une clé tails avec la persistance
de configurée.</li>
<li>Nous nous sommes connectés au réseau
<a href="https://www.torproject.org">Tor</a> et testé que nos adresses <span class="caps">IP</span>
changeaient bien à la demande.</li>
<li>Nous avons utilisé <a href="https://crypto.cat">CryptoCat</a> par dessus Tor,
afin de voir comment avoir une conversation confidentielle dans
laquelle il est possible d’échanger des fichiers.</li>
</ul>
<h2 id="retours">Retours</h2>
<p>D’une manière générale, pour une formation de trois heures et demi, je
suis assez content de l’exercice, et de l’ensemble des sujets que nous
avons pu couvrir. Il y a beaucoup de place pour l’amélioration,
notamment en amont (j’avais par exemple oublié d’amener avec moi
suffisamment de clés <span class="caps">USB</span> pour utiliser Tails).</p>
<p>La plupart des retours qu’on a pu avoir jusqu’à maintenant sont
positifs, et il y a l’envie d’aller plus loin sur l’ensemble de ces sujets.</p>
<h2 id="la-suite">La suite</h2>
<p>Il y a beaucoup de sujets que nous n’avons pas abordés, ou uniquement
survolés, à cause du manque de temps disponible. Idéalement, il faudrait
au moins une journée entière pour couvrir quelques sujets plus en détail
(on peut imaginer avoir une partie théorique le matin et une partie
pratique l’après-midi par exemple).</p>
<p>J’ai choisi volontairement de ne pas aborder le chiffrement des messages
via <span class="caps">PGP</span> parce que <a href="%7Bfilename%7D2015.05.pgp-problemes.rst">je pense que la protection que ce média propose n’est
pas suffisante</a>, mais je suis
en train de revenir sur ma décision: il pourrait être utile de présenter
l’outil, à minima, en insistant sur certaines de ses faiblesses.</p>
<p>Un compte twitter à été créé recemment autour des crypto-party à Rennes,
si vous êtes interessés, <a href="https://twitter.com/CryptoPartyRNS">allez jeter un coup
d’œil</a>!</p>
<p>Je n’ai pas trouvé de ressources disponibles par rapport à des plans de
formation sur le sujet, j’ai donc décidé de publier les nôtres, afin de
co-construire avec d’autres des plans de formation.</p>
<p>Ils sont pour l’instant disponibles <a href="http://autodefense-numerique.readthedocs.org/en/latest/">sur Read The
Docs</a>. Tous les
retours sont évidemment les bienvenus !</p>Le mail doit-il mourir ?2015-11-24T00:00:00+01:002015-11-24T00:00:00+01:00tag:blog.notmyidea.org,2015-11-24:/le-mail-doit-il-mourir.html<p>J’utilise quotidiennement le protocole email, tant bien que mal, tout en sachant que l’ensemble de mes messages passent en clair sur le réseau pour la plupart de mes conversations, puisque trop peu de monde utilise le chiffrement des messages.</p>
<p>Et même si j’arrive à convaincre certains de …</p><p>J’utilise quotidiennement le protocole email, tant bien que mal, tout en sachant que l’ensemble de mes messages passent en clair sur le réseau pour la plupart de mes conversations, puisque trop peu de monde utilise le chiffrement des messages.</p>
<p>Et même si j’arrive à convaincre certains de mes proches à installer <span class="caps">PGP</span>, je ne suis pas satisfait du résultat: les méta-données (qui contacte qui à quel
moment, et pour lui dire quoi) transitent de toute manière, elles, en clair, à la vue de tous.</p>
<p>Ce problème est lié directement au protocole email: il est <em>necessaire</em> de faire fuiter ces meta-données (au moins le destinataire) pour avoir un protocole
mail fonctionnel.</p>
<p>Le mail répond à un besoin de communication asynchrone qui permet des conversations plus réfléchies qu’un simple chat (miaou). Il est tout à fait possible d’utiliser certaines technologies existantes afin de construire le futur de l’email, pour lequel:</p>
<ul>
<li>Les méta-données seraient chiffrées — Il n’est pas possible de savoir qui
communique avec qui, et quand;</li>
<li>Le chiffrement serait fort (et protégé d’une phrase de passe ?);</li>
<li>La fuite d’une clé de chiffrement utilisée dans un échange ne permette pas de
déchiffrer l’ensemble des échanges (forward secrecy);</li>
<li>Il ne soit pas possible de réutiliser les données comme preuve pour
incriminer l’emmeteur du message (deniability);</li>
</ul>
<p>Avec au moins ces besoins en tête, il semble qu’une revue de l’ensemble des projets existants pointe du doigt vers <a href="https://github.com/agl/pond">pond</a>, ou vers <a href="https://www.whispersystems.org">Signal</a>.</p>
<p>Malheureusement, Pond est le projet d’une seule personne, qui veut plutôt utiliser ce code comme démonstration du concept en question.</p>Web distribution signing2015-10-12T00:00:00+02:002015-10-12T00:00:00+02:00tag:blog.notmyidea.org,2015-10-12:/web-distribution-signing.html<p><em>I’m not a crypto expert, nor pretend to be one. These are thoughts I
want to share with the crypto community to actually see if any solution
exists to solve this particular problem.</em></p>
<p>One <a href="http://www.tonyarcieri.com/whats-wrong-with-webcrypto">often pointed</a> flaw in
web-based cryptographic applications is the fact that there is no way …</p><p><em>I’m not a crypto expert, nor pretend to be one. These are thoughts I
want to share with the crypto community to actually see if any solution
exists to solve this particular problem.</em></p>
<p>One <a href="http://www.tonyarcieri.com/whats-wrong-with-webcrypto">often pointed</a> flaw in
web-based cryptographic applications is the fact that there is no way to
trust online software distributions. Put differently, you don’t actually
trust the software authors but are rather trusting the software
distributors and certificate authorities (CAs).</p>
<p>I’ve been talking with a few folks in the past months about that and
they suggested me to publish something to discuss the matter. So here I come!</p>
<h2 id="the-problem-attack-vectors">The problem (Attack vectors)</h2>
<p>Let’s try to describe a few potential attacks:</p>
<p><em>Application Authors</em> just released a new version of their open source
web crypto messaging application. An <em>Indie Hoster</em> installs it on their
servers so a wide audience can actually use it.</p>
<p>Someone alters the files on <em>Indie Hoster</em> servers, effectively
replacing them with other <em>altered files</em> with less security properties
/ a backdoor. This someone could either be an <em>Evil Attacker</em> which
found its way trough, the <em>Indie Hoster</em> or a <span class="caps">CDN</span> which delivers the files,</p>
<p>Trusted <em>Certificate Authorities</em> (“governments” or “hacking team”) can
also trick the User Agents (i.e. Firefox) into thinking they’re talking
to <em>Indie Hoster</em> even though they’re actually talking to a different server.</p>
<p><strong>Altered files</strong> are then being served to the User Agents, and <em>Evil
Attacker</em> now has a way to actually attack the end users.</p>
<h2 id="problem-mitigation">Problem Mitigation</h2>
<p>Part of the problem is solved by the recently introduced <a href="https://w3c.github.io/webappsec/specs/subresourceintegrity/">Sub Resource
Integrity</a>
(<span class="caps">SRI</span>). To quote them: “[it] defines a mechanism by which user agents
may verify that a fetched resource has been delivered without unexpected manipulation.”.</p>
<p><span class="caps">SRI</span> is a good start, but isn’t enough: it ensures the assets (JavaScript
files, mainly) loaded from a specific <span class="caps">HTML</span> page are the ones the author
of the <span class="caps">HTML</span> page intends. However, <span class="caps">SRI</span> doesn’t allow the User Agent to
ensure the <span class="caps">HTML</span> page is the one he wants.</p>
<p>In other words, we miss a way to create trust between <em>Application
Authors</em> and <em>User Agents</em>. The User-Agent currently has to trust the
<em>Certificate Authorities</em> and the delivery (<em>Indie Hoster</em>).</p>
<p>For desktop software distribution: <em>Crypto Experts</em> audit the software,
sign it somehow and then this signature can be checked locally during
installation or runtime. It’s not automated, but at least it’s possible.</p>
<p>For web applications, we don’t have such a mechanism, but it should be
possible. Consider the following:</p>
<ul>
<li><em>App Authors</em> publish a new version of their software; They provide
a hash of each of their distributed files (including the <span class="caps">HTML</span> files);</li>
<li><em>Crypto Experts</em> audit these files and sign the hashes somehow;</li>
<li><em>User Agents</em> can chose to trust some specific <em>Crypto Experts</em>;</li>
<li>When a <em>User Agent</em> downloads files, it checks if they’re signed by
a trusted party.</li>
</ul>
<h2 id="chosing-who-you-trust">Chosing who you trust</h2>
<p>In terms of user experience, handling certificates is hard, and that’s
where the community matters. Distributions such as
<a href="https://tails.boom.org">Tails</a> could chose who they trust to verify the
files, and issue warnings / refuse to run the application in case files
aren’t verified.</p>
<p>But, as highligted earlier, CAs are hard to trust. A new instance of the
same <span class="caps">CA</span> system wouldn’t make that much differences, expect the fact that
distributions could ship with a set of trusted authorities (for which
revocation would still need to be taken care of).</p>
<blockquote>
<p>[…] users are vulnerable to MitM attacks by the authority, which
can vouch for, or be coerced to vouch for, false keys. This weakness
has been highlighted by recent <span class="caps">CA</span> scandals. Both schemes can also be
attacked if the authority does not verify keys before vouching for them.</p>
<p>— <a href="http://cacr.uwaterloo.ca/techreports/2015/cacr2015-02.pdf">SoK : Secure
Messaging</a>;</p>
</blockquote>
<p>It seems that some other systems could allow for something more reliable:</p>
<blockquote>
<p>Melara et al proposed <span class="caps">CONIKS</span>, using a series of chained commitments to
Merkle prefix trees to build a key directory […] for which
individual users can efficiently verify the consistency of their own
entry in the directory without relying on a third party.</p>
<p>This “self- auditing log” approach makes the system partially have no
auditing required (as general auditing of non-equivocation is still
required) and also enables the system to be privacy preserving as the
entries in the directory need not be made public. This comes at a mild
bandwidth cost not reflected in our table, estimated to be about 10
kilobytes per client per day for self-auditing.</p>
<p>— <a href="http://cacr.uwaterloo.ca/techreports/2015/cacr2015-02.pdf">SoK : Secure
Messaging</a>;</p>
</blockquote>
<p>Now, I honestly have no idea if this thing solves the whole problem, and
I’m pretty sure this design has many security problems attached to it.</p>
<p>However, that’s a problem I would really like to see solved one day, so
here the start of the discussion, don’t hesitate to <a href="/pages/about.html">get in
touch</a>!</p>
<h2 id="addendum">Addendum</h2>
<p>It seems possible to increase the level a user has in a Web Application
by adding indicators in the User-Agent. For instance, when using an
application that’s actually signed by someone considered trustful by the
User-Agent (or the distributor of the User-Agent), a little green icon
could be presented to the User, so they know that they can be confident
about this.</p>
<p>A bit like User-Agents do for <span class="caps">SSL</span>, but for the actual signature of the
files being viewed.</p>Service de nuages : Pourquoi avons-nous fait Cliquet ?2015-07-14T00:00:00+02:002015-07-14T00:00:00+02:00tag:blog.notmyidea.org,2015-07-14:/pourquoi-cliquet<p class="first last">Basé sur Pyramid, Cliquet est un projet qui permet de se concentrer sur l’essentiel
lors de la conception d’APIs.</p>
<p><em>Cet article est repris depuis le blog « Service de Nuages » de mon équipe à Mozilla</em></p>
<p><strong>tldr; Cliquet est un toolkit Python pour construire des APIs, qui implémente
les bonnes pratiques en terme de mise en production et de protocole <span class="caps">HTTP</span>.</strong></p>
<div class="section" id="les-origines">
<h2>Les origines</h2>
<p>L’objectif pour le premier trimestre 2015 était de construire un service de
stockage et de <a class="reference external" href="https://blog.notmyidea.org/service-de-nuages-fr.html">synchronisation de listes de lecture</a>.</p>
<p>Au démarrage du projet, nous avons tenté de rassembler toutes les bonnes pratiques
et recommandations, venant de différentes équipes et surtout des derniers projets déployés.</p>
<p>De même, nous voulions tirer parti du protocole de <em>Firefox Sync</em>, robuste et éprouvé,
pour la synchronisation des données «offline».</p>
<p>Plutôt qu’écrire un <a class="reference external" href="http://blog.octo.com/en/design-a-rest-api/">énième</a>
<a class="reference external" href="http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api">article</a> de blog,
nous avons préféré les rassembler dans ce qu’on a appellé «un protocole».</p>
<p>Comme pour l’architecture envisagée nous avions deux projets à construire, qui
devaient obéir globalement à ces mêmes règles, nous avons décidé de mettre en
commun l’implémentation de ce protocole et de ces bonnes pratiques dans un «toolkit».</p>
<p><em>Cliquet</em> est né.</p>
<img alt="Cliquet logo" class="align-center" src="https://blog.notmyidea.org/images/cliquet/cliquet-logo.png" />
<div class="section" id="les-intentions">
<h3>Les intentions</h3>
<blockquote class="epigraph">
Quelle structure <span class="caps">JSON</span> pour mon <span class="caps">API</span> ? Quelle syntaxe pour filtrer la liste
via la querystring ? Comment gérer les écritures concurrentes ?
Et synchroniser les données dans mon application cliente ?</blockquote>
<p>Désormais, quand un projet souhaite bénéficier d’une <span class="caps">API</span> <span class="caps">REST</span> pour stocker et consommer
des données, il est possible d’utiliser le <strong>protocole <span class="caps">HTTP</span></strong> proposé
et de se concentrer sur l’essentiel. Cela vaut aussi pour les clients, où
la majorité du code d’interaction avec le serveur est réutilisable.</p>
<blockquote class="epigraph">
Comment pouvons-nous vérifier que le service est opérationnel ? Quels indicateurs StatsD ?
Est-ce que Sentry est bien configuré ? Comment déployer une nouvelle version
sans casser les applications clientes ?</blockquote>
<p>Comme <em>Cliquet</em> fournit tout ce qui est nécessaire pour être conforme avec les
exigences de la <strong>mise en production</strong>, le passage du prototype au service opérationnel
est très rapide ! De base le service répondra aux attentes en terme supervision, configuration,
déploiement et dépréciation de version. Et si celles-ci évoluent, il suffira
de faire évoluer le toolkit.</p>
<blockquote class="epigraph">
Quel backend de stockage pour des documents <span class="caps">JSON</span> ? Comment faire si l’équipe
de production impose PostgreSQL ? Et si on voulait passer à Redis ou en
mémoire pour lancer les tests ?</blockquote>
<p>En terme d’implémentation, nous avons choisi de <strong>fournir des abstractions</strong>.
En effet, nous avions deux services dont le coeur consistait
à exposer un <em><span class="caps">CRUD</span></em> en <em><span class="caps">REST</span></em>, persistant des données <span class="caps">JSON</span> dans un backend.
Comme <em>Pyramid</em> et <em>Cornice</em> ne fournissent rien de tout prêt pour ça,
nous avons voulu introduire des classes de bases pour abstraire les notions
de resource <span class="caps">REST</span> et de backend de stockage.</p>
<p>Dans le but de tout rendre optionnel et «pluggable», <strong>tout est configurable</strong>
depuis le fichier <tt class="docutils literal">.ini</tt> de l’application. Ainsi tous les projets qui utilisent
le toolkit se déploieront de la même manière : seuls quelques éléments de configuration
les distingueront.</p>
<img alt="Une réunion à Paris..." class="align-center" src="https://blog.notmyidea.org/images/cliquet/cliquet-notes-whiteboard.jpg" />
</div>
</div>
<div class="section" id="le-protocole">
<h2>Le protocole</h2>
<blockquote class="epigraph">
Est-ce suffisant de parler d’«<span class="caps">API</span> <span class="caps">REST</span>» ? Est-ce bien nécessaire de
relire la spec <span class="caps">HTTP</span> à chaque fois ? Pourquoi réinventer un protocole complet
à chaque fois ?</blockquote>
<p>Quand nous développons un (micro)service Web, nous dépensons généralement beaucoup
trop d’énergie à (re)faire des choix (arbitraires).</p>
<p>Nul besoin de lister ici tout ce qui concerne la dimension
de la spécification <span class="caps">HTTP</span> pure, qui nous impose le format des headers,
le support de <span class="caps">CORS</span>, la négocation de contenus (types mime), la différence entre
authentification et autorisation, la cohérence des code status…</p>
<p>Les choix principaux du protocole concernent surtout :</p>
<ul class="simple">
<li><strong>Les resources <span class="caps">REST</span></strong> : Les deux URLs d’une resource (pour la collection
et les enregistrements) acceptent des verbes et des headers précis.</li>
<li><strong>Les formats</strong> : le format et la structure <span class="caps">JSON</span> des réponses est imposé, ainsi
que la pagination des listes ou la syntaxe pour filtrer/trier les resources via la <a class="reference external" href="https://en.wikipedia.org/wiki/Query_string">querystring</a>.</li>
<li><strong>Les timestamps</strong> : un numéro de révision qui s’incrémente à chaque opération
d’écriture sur une collection d’enregistrements.</li>
<li><strong>La synchronisation</strong> : une série de leviers pour récupérer et renvoyer des
changements sur les données, sans perte ni collision, en utilisant les timestamps.</li>
<li><strong>Les permissions</strong> : les droits d’un utilisateur sur une collection ou un enregistrement
(<em>encore frais et sur le point d’être documenté</em>) <a class="footnote-reference" href="#footnote-1" id="footnote-reference-1">[1]</a>.</li>
<li><strong>Opérations par lot</strong>: une <span class="caps">URL</span> qui permet d’envoyer une série de requêtes
décrites en <span class="caps">JSON</span> et d’obtenir les réponses respectives.</li>
</ul>
<p>Dans la dimension opérationnelle du protocole, on trouve :</p>
<ul class="simple">
<li><strong>La gestion de version</strong> : cohabitation de plusieurs versions en production,
avec alertes dans les entêtes pour la fin de vie des anciennes versions.</li>
<li><strong>Le report des requêtes</strong> : entêtes interprétées par les clients, activées en cas de
maintenance ou de surchage, pour ménager le serveur.</li>
<li><strong>Le canal d’erreurs</strong> : toutes les erreurs renvoyées par le serveur ont le même
format <span class="caps">JSON</span> et ont un numéro précis.</li>
<li><strong>Les utilitaires</strong> : URLs diverses pour répondre aux besoins exprimés par
l’équipe d’administrateurs (monitoring, metadonnées, paramètres publiques).</li>
</ul>
<p>Ce protocole est une compilation des bonnes pratiques pour les APIs <span class="caps">HTTP</span> (<em>c’est notre métier !</em>),
des conseils des administrateurs système dont c’est le métier de mettre à disposition des services
pour des millions d’utilisateurs et des retours d’expérience de l’équipe
de <em>Firefox Sync</em> pour la gestion de la concurrence et de l’«offline-first».</p>
<p>Il est <a class="reference external" href="http://cliquet.readthedocs.org/en/latest/api/index.html">documenté en détail</a>.</p>
<p>Dans un monde idéal, ce protocole serait versionné, et formalisé dans une <span class="caps">RFC</span>.
En rêve, il existerait même plusieurs implémentations avec des codes différentes
(Python, Go, Node, etc.). <a class="footnote-reference" href="#footnote-2" id="footnote-reference-2">[2]</a></p>
<table class="docutils footnote" frame="void" id="footnote-1" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-1">[1]</a></td><td>Voir notre <a class="reference external" href="https://blog.notmyidea.org/service-de-nuages-la-gestion-des-permissions-fr.html">article dédié sur les permissions</a></td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="footnote-2" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-2">[2]</a></td><td>Rappel: nous sommes une toute petite équipe !</td></tr>
</tbody>
</table>
</div>
<div class="section" id="le-toolkit">
<h2>Le toolkit</h2>
<div class="section" id="choix-techniques">
<h3>Choix techniques</h3>
<p><em>Cliquet</em> implémente le protocole en Python (<em>2.7, 3.4+, pypy</em>), avec <a class="reference external" href="http://trypyramid.com/">Pyramid</a> <a class="footnote-reference" href="#footnote-3" id="footnote-reference-3">[3]</a>.</p>
<p><strong>Pyramid</strong> est un framework Web qui va prendre en charge tout la partie <span class="caps">HTTP</span>,
et qui s’avère pertinent aussi bien pour des petits projets que des plus ambitieux.</p>
<p><strong>Cornice</strong> est une extension de <em>Pyramid</em>, écrite en partie par Alexis et Tarek,
qui permet d’éviter d’écrire tout le code <em>boilerplate</em> quand on construit une
<span class="caps">API</span> <span class="caps">REST</span> avec Pyramid.</p>
<p>Avec <em>Cornice</em>, on évite de réécrire à chaque fois le code qui va
cabler les verbes <span class="caps">HTTP</span> aux méthodes, valider les entêtes, choisir le sérialiseur
en fonction des entêtes de négociation de contenus, renvoyer les codes <span class="caps">HTTP</span>
rigoureux, gérer les entêtes <span class="caps">CORS</span>, fournir la validation <span class="caps">JSON</span> à partir de schémas…</p>
<p><strong>Cliquet</strong> utilise les deux précédents pour implémenter le protocole et fournir
des abstractions, mais on a toujours <em>Pyramid</em> et <em>Cornice</em> sous la main pour
aller au delà de ce qui est proposé !</p>
<table class="docutils footnote" frame="void" id="footnote-3" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-3">[3]</a></td><td>Au tout début nous avons commencé une implémentation avec <em>Python-Eve</em>
(Flask), mais n’étions pas satisfaits de l’approche pour la configuration
de l’<span class="caps">API</span>. En particulier du côté magique.</td></tr>
</tbody>
</table>
</div>
<div class="section" id="concepts">
<h3>Concepts</h3>
<p>Bien évidemment, les concepts du toolkit reflètent ceux du protocole mais il y
a des éléments supplémentaires:</p>
<ul class="simple">
<li><strong>Les backends</strong> : abstractions pour le stockage, le cache et les permissions
(<em>ex. PostgreSQL, Redis, en-mémoire, …</em>)</li>
<li><strong>La supervision</strong> : logging <span class="caps">JSON</span> et indicateurs temps-réel (<em>StatsD</em>) pour suivre les
performances et la santé du service.</li>
<li><strong>La configuration</strong> : chargement de la configuration depuis les variables
d’environnement et le fichier <tt class="docutils literal">.ini</tt></li>
<li><strong>La flexibilité</strong> : dés/activation ou substitution de la majorité des composants
depuis la configuration.</li>
<li><strong>Le profiling</strong> : utilitaires de développement pour trouver les <a class="reference external" href="https://fr.wiktionary.org/wiki/goulet_d%E2%80%99%C3%A9tranglement">goulets
d’étranglement</a>.</li>
</ul>
<img alt="Cliquet concepts" class="align-center" src="https://blog.notmyidea.org/images/cliquet/cliquet-concepts.png" />
<p>Proportionnellement, l’implémentation du protocole pour les resources <span class="caps">REST</span> est
la plus volumineuse dans le code source de <em>Cliquet</em>.
Cependant, comme nous l’avons décrit plus haut, <em>Cliquet</em> fournit tout un
ensemble d’outillage et de bonnes pratiques, et reste
donc tout à fait pertinent pour n’importe quel type d’<span class="caps">API</span>, même sans
manipulation de données !</p>
<p>L’objectif de la boîte à outils est de faire en sorte qu’un développeur puisse constuire
une application simplement, en étant sûr qu’elle réponde aux exigeances de la
mise en production, tout en ayant la possibilité de remplacer certaines parties
au fur et à mesure que ses besoins se précisent.</p>
<p>Par exemple, la persistence fournie par défault est <em>schemaless</em> (e.g <em><span class="caps">JSONB</span></em>),
mais rien n’empêcherait d’implémenter le stockage dans un modèle relationnel.</p>
<p>Comme les composants peuvent être remplacés depuis la configuration, il est
tout à fait possible d’étendre <em>Cliquet</em> avec des notions métiers ou des
codes exotiques ! Nous avons posé quelques idées dans <a class="reference external" href="http://cliquet.readthedocs.org/en/latest/ecosystem.html">la documentation
de l’éco-système</a>.</p>
<p>Dans les prochaines semaines, nous allons introduire la notion d’«évènements» (ou signaux),
qui permettraient aux extensions de s’interfacer beaucoup plus proprement.</p>
<p>Nous attachons beaucoup d’importance à la clareté du code, la pertinence des
<em>patterns</em>, des tests et de la documentation. Si vous avez des commentaires,
des critiques ou des interrogations, n’hésitez pas à <a class="reference external" href="https://github.com/mozilla-services/cliquet/issues">nous en faire part</a> !</p>
</div>
</div>
<div class="section" id="cliquet-a-l-action">
<h2>Cliquet, à l’action.</h2>
<p>Nous avons écrit un <a class="reference external" href="http://cliquet.readthedocs.org/en/latest/quickstart.html">guide de démarrage</a>,
qui n’exige pas de connaître <em>Pyramid</em>.</p>
<p>Pour illustrer la simplicité et les concepts, voici quelques extraits !</p>
<div class="section" id="etape-1">
<h3>Étape 1</h3>
<p>Activer <em>Cliquet</em>:</p>
<div class="highlight"><pre><span></span><span class="hll"><span class="kn">import</span> <span class="nn">cliquet</span>
</span><span class="kn">from</span> <span class="nn">pyramid.config</span> <span class="kn">import</span> <span class="n">Configurator</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">global_config</span><span class="p">,</span> <span class="o">**</span><span class="n">settings</span><span class="p">):</span>
<span class="n">config</span> <span class="o">=</span> <span class="n">Configurator</span><span class="p">(</span><span class="n">settings</span><span class="o">=</span><span class="n">settings</span><span class="p">)</span>
<span class="hll"> <span class="n">cliquet</span><span class="o">.</span><span class="n">initialize</span><span class="p">(</span><span class="n">config</span><span class="p">,</span> <span class="s1">'1.0'</span><span class="p">)</span>
</span> <span class="k">return</span> <span class="n">config</span><span class="o">.</span><span class="n">make_wsgi_app</span><span class="p">()</span>
</pre></div>
<p>À partir de là, la plupart des outils de <em>Cliquet</em> sont activés et accessibles.</p>
<p>Par exemple, les URLs <em>hello</em> (<tt class="docutils literal">/v1/</tt>) ou <em>supervision</em> (<tt class="docutils literal">/v1/__heartbeat__</tt>).
Mais aussi les backends de stockage, de cache, etc.
qu’il est possible d’utiliser dans des vues classiques <em>Pyramid</em> ou <em>Cornice</em>.</p>
</div>
<div class="section" id="etape-2">
<h3>Étape 2</h3>
<p>Ajouter des vues:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">global_config</span><span class="p">,</span> <span class="o">**</span><span class="n">settings</span><span class="p">):</span>
<span class="n">config</span> <span class="o">=</span> <span class="n">Configurator</span><span class="p">(</span><span class="n">settings</span><span class="o">=</span><span class="n">settings</span><span class="p">)</span>
<span class="n">cliquet</span><span class="o">.</span><span class="n">initialize</span><span class="p">(</span><span class="n">config</span><span class="p">,</span> <span class="s1">'1.0'</span><span class="p">)</span>
<span class="hll"> <span class="n">config</span><span class="o">.</span><span class="n">scan</span><span class="p">(</span><span class="s2">"myproject.views"</span><span class="p">)</span>
</span> <span class="k">return</span> <span class="n">config</span><span class="o">.</span><span class="n">make_wsgi_app</span><span class="p">()</span>
</pre></div>
<p>Pour définir des resources <span class="caps">CRUD</span>, il faut commencer par définir un schéma,
avec <em>Colander</em>, et ensuite déclarer une resource:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">cliquet</span> <span class="kn">import</span> <span class="n">resource</span><span class="p">,</span> <span class="n">schema</span>
<span class="k">class</span> <span class="nc">BookmarkSchema</span><span class="p">(</span><span class="n">schema</span><span class="o">.</span><span class="n">ResourceSchema</span><span class="p">):</span>
<span class="n">url</span> <span class="o">=</span> <span class="n">schema</span><span class="o">.</span><span class="n">URL</span><span class="p">()</span>
<span class="hll"><span class="nd">@resource</span><span class="o">.</span><span class="n">register</span><span class="p">()</span>
</span><span class="hll"><span class="k">class</span> <span class="nc">Bookmark</span><span class="p">(</span><span class="n">resource</span><span class="o">.</span><span class="n">BaseResource</span><span class="p">):</span>
</span><span class="hll"> <span class="n">mapping</span> <span class="o">=</span> <span class="n">BookmarkSchema</span><span class="p">()</span>
</span></pre></div>
<p>Désormais, la resource <span class="caps">CRUD</span> est disponible sur <tt class="docutils literal">/v1/bookmarks</tt>, avec toutes
les fonctionnalités de synchronisation, filtrage, tri, pagination, timestamp, etc.
De base les enregistrements sont privés, par utilisateur.</p>
<div class="highlight"><pre><span></span><span class="err">$</span><span class="w"> </span><span class="err">h</span><span class="kc">tt</span><span class="err">p</span><span class="w"> </span><span class="err">GET</span><span class="w"> </span><span class="s2">"http://localhost:8000/v1/bookmarks"</span>
<span class="err">HTTP/</span><span class="mf">1.1</span><span class="w"> </span><span class="mi">200</span><span class="w"> </span><span class="err">OK</span>
<span class="err">...</span>
<span class="p">{</span>
<span class="w"> </span><span class="nt">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://cliquet.readthedocs.org"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cc103eb5-0c80-40ec-b6f5-dad12e7d975e"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"last_modified"</span><span class="p">:</span><span class="w"> </span><span class="mi">1437034418940</span><span class="p">,</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</pre></div>
</div>
<div class="section" id="etape-3">
<h3>Étape 3</h3>
<p>Évidemment, il est possible choisir les <span class="caps">URLS</span>, les verbes <span class="caps">HTTP</span> supportés, de modifier
des champs avant l’enregistrement, etc.</p>
<div class="highlight"><pre><span></span><span class="hll"><span class="nd">@resource</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">collection_path</span><span class="o">=</span><span class="s1">'/user/bookmarks'</span><span class="p">,</span>
</span><span class="hll"> <span class="n">record_path</span><span class="o">=</span><span class="s1">'/user/bookmarks/{{id}}'</span><span class="p">,</span>
</span><span class="hll"> <span class="n">collection_methods</span><span class="o">=</span><span class="p">(</span><span class="s1">'GET'</span><span class="p">,))</span>
</span><span class="k">class</span> <span class="nc">Bookmark</span><span class="p">(</span><span class="n">resource</span><span class="o">.</span><span class="n">BaseResource</span><span class="p">):</span>
<span class="n">mapping</span> <span class="o">=</span> <span class="n">BookmarkSchema</span><span class="p">()</span>
<span class="hll"> <span class="k">def</span> <span class="nf">process_record</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">new</span><span class="p">,</span> <span class="n">old</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span><span class="hll"> <span class="k">if</span> <span class="n">old</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">new</span><span class="p">[</span><span class="s1">'device'</span><span class="p">]</span> <span class="o">!=</span> <span class="n">old</span><span class="p">[</span><span class="s1">'device'</span><span class="p">]:</span>
</span><span class="hll"> <span class="n">device</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'User-Agent'</span><span class="p">)</span>
</span><span class="hll"> <span class="n">new</span><span class="p">[</span><span class="s1">'device'</span><span class="p">]</span> <span class="o">=</span> <span class="n">device</span>
</span><span class="hll"> <span class="k">return</span> <span class="n">new</span>
</span></pre></div>
<p><a class="reference external" href="http://cliquet.readthedocs.org/en/latest/reference/resource.html">Plus d’infos dans la documentation dédiée</a> !</p>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">Il est possible de définir des resources sans validation de schema.
<a class="reference external" href="https://github.com/mozilla-services/kinto/blob/master/kinto/views/records.py">Voir le code source de Kinto</a>.</p>
</div>
</div>
<div class="section" id="etape-4-optionelle">
<h3>Étape 4 (optionelle)</h3>
<p>Utiliser les abstractions de <em>Cliquet</em> dans une vue <em>Cornice</em>.</p>
<p>Par exemple, une vue qui utilise le backend de stockage:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">cliquet</span> <span class="kn">import</span> <span class="n">Service</span>
<span class="n">score</span> <span class="o">=</span> <span class="n">Service</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">"score"</span><span class="p">,</span>
<span class="n">path</span><span class="o">=</span><span class="s1">'/score/</span><span class="si">{game}</span><span class="s1">'</span><span class="p">,</span>
<span class="n">description</span><span class="o">=</span><span class="s2">"Store game score"</span><span class="p">)</span>
<span class="nd">@score</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">schema</span><span class="o">=</span><span class="n">ScoreSchema</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">post_score</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="n">collection_id</span> <span class="o">=</span> <span class="s1">'scores-'</span> <span class="o">+</span> <span class="n">request</span><span class="o">.</span><span class="n">match_dict</span><span class="p">[</span><span class="s1">'game'</span><span class="p">]</span>
<span class="n">user_id</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">authenticated_userid</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">validated</span> <span class="c1"># c.f. Cornice.</span>
<span class="hll"> <span class="n">storage</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">registry</span><span class="o">.</span><span class="n">storage</span>
</span><span class="hll"> <span class="n">record</span> <span class="o">=</span> <span class="n">storage</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">collection_id</span><span class="p">,</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
</span> <span class="k">return</span> <span class="n">record</span>
</pre></div>
</div>
</div>
<div class="section" id="vos-retours">
<h2>Vos retours</h2>
<p>N’hésitez pas à nous faire part de vos retours ! Cela vous a donné envie
d’essayer ? Vous connaissez un outil similaire ?
Y-a-t-il des points qui ne sont pas clairs ? Manque de cas d’utilisation concrets ?
Certains aspects mal pensés ? Trop contraignants ? Trop de magie ? Overkill ?</p>
<p>Nous prenons tout.</p>
<div class="section" id="points-faibles">
<h3>Points faibles</h3>
<p>Nous sommes très fiers de ce que nous avons construit, en relativement peu
de temps. Et comme nous l’exposions dans l’article précédent (plus accessible), il y a du potentiel !</p>
<p>Cependant, nous sommes conscients d’un certain nombre de points
qui peuvent être vus comme des faiblesses.</p>
<ul class="simple">
<li><strong>La documentation d’<span class="caps">API</span></strong> : actuellement, nous n’avons pas de solution pour qu’un
projet qui utilise <em>Cliquet</em> puisse intégrer facilement toute
<a class="reference external" href="http://cliquet.readthedocs.org/en/latest/api/index.html">la documentation de l’<span class="caps">API</span></a> obtenue.</li>
<li><strong>La documentation</strong> : il est très difficile d’organiser la documentation, surtout
quand le public visé est aussi bien débutant qu’expérimenté. Nous sommes probablement
victimes du «<a class="reference external" href="https://en.wikipedia.org/wiki/Curse_of_knowledge">curse of knowledge</a>».</li>
<li><strong>Le protocole</strong> : on sent bien qu’on va devoir versionner le protocole. Au
moins pour le désolidariser des versions de <em>Cliquet</em>, si on veut aller au
bout de la philosophie et de l’éco-système.</li>
<li><strong>Le conservatisme</strong> : Nous aimons la stabilité et la robustesse. Mais surtout
nous ne sommes pas tout seuls et devons nous plier aux contraintes de la mise
en production ! Cependant, nous avons très envie de faire de l’async avec Python 3 !</li>
<li><strong>Publication de versions</strong> : le revers de la médaille de la factorisation. Il
arrive qu’on préfère faire évoluer le toolkit (e.g. ajouter une option) pour
un point précis d’un projet. En conséquence, on doit souvent releaser les
projets en cascade.</li>
</ul>
</div>
<div class="section" id="quelques-questions-courantes">
<h3>Quelques questions courantes</h3>
<blockquote>
Pourquoi Python ?</blockquote>
<p>On prend beaucoup de plaisir à écrire du Python, et le calendrier annoncé
initialement était très serré: pas question de tituber avec une code
mal maitrisée !</p>
<p>Et puis, après avoir passé près d’un an sur un projet Node.js, l’équipe avait
bien envie de refaire du Python.</p>
<blockquote>
Pourquoi pas Django ?</blockquote>
<p>On y a pensé, surtout parce qu’il y a plusieurs fans de <em>Django <span class="caps">REST</span> Framework</em>
dans l’équipe.</p>
<p>On l’a écarté principalement au profit de la légèreté et la modularité de
<em>Pyramid</em>.</p>
<blockquote>
Pourquoi pas avec un framework asynchrone en Python 3+ ?</blockquote>
<p>Pour l’instant nos administrateurs système nous imposent des déploiements en
Python 2.7, à notre grand désarroi /o\</p>
<p>Pour <em>Reading List</em>, nous <a class="reference external" href="https://github.com/mozilla-services/readinglist/blob/1.7.0/readinglist/__init__.py#L19-L26">avions activé</a>
<em>gevent</em>.</p>
<p>Puisque l’approche consiste à implémenter un protocole bien déterminé, nous n’excluons
pas un jour d’écrire un <em>Cliquet</em> en <em>aiohttp</em> ou <em>Go</em> si cela s’avèrerait pertinent.</p>
<blockquote>
Pourquoi pas <span class="caps">JSON</span>-<span class="caps">API</span> ?</blockquote>
<p>Comme nous l’expliquions au retour des APIdays,
<span class="caps">JSON</span>-<span class="caps">API</span> est une spécification qui rejoint plusieurs de nos intentions.</p>
<p>Quand nous avons commencé le protocole, nous ne connaissions pas <span class="caps">JSON</span>-<span class="caps">API</span>.
Pour l’instant, comme notre proposition est beaucoup plus minimaliste, le
rapprochement n’a <a class="reference external" href="https://github.com/mozilla-services/cliquet/issues/254">pas dépassé le stade de la discussion</a>.</p>
<blockquote>
Est-ce que Cliquet est un framework <span class="caps">REST</span> pour Pyramid ?</blockquote>
<p>Non.</p>
<p>Au delà des classes de resources <span class="caps">CRUD</span> de Cliquet, qui implémentent un
protocole bien précis, il faut utiliser Cornice ou Pyramid directement.</p>
<blockquote>
Est-ce que Cliquet est suffisamment générique pour des projets hors Mozilla ?</blockquote>
<p>Premièrement, nous faisons en sorte que tout soit contrôlable depuis la
configuration <tt class="docutils literal">.ini</tt> pour permettre la dés/activation ou substitution des composants.</p>
<p>Si le protocole <span class="caps">HTTP</span>/<span class="caps">JSON</span> des resources <span class="caps">CRUD</span> vous satisfait,
alors Cliquet est probablement le plus court chemin pour construire une
application qui tient la route.</p>
<p>Mais l’utilisation des resources <span class="caps">CRUD</span> est facultative, donc Cliquet reste pertinent
si les bonnes pratiques en terme de mise en production ou les abstractions fournies
vous paraissent valables !</p>
<p>Cliquet reste un moyen simple d’aller très vite pour mettre sur pied
une application Pyramid/Cornice.</p>
<blockquote>
Est-ce que les resources <span class="caps">JSON</span> supporte les modèles relationnels complexes ?</blockquote>
<p>La couche de persistence fournie est très simple, et devrait
répondre à la majorité des cas d’utilisation où les données n’ont pas de relations.</p>
<p>En revanche, il est tout à fait possible de bénéficier de tous les aspects
du protocole en utilisant une classe <tt class="docutils literal">Collection</tt> maison, qui se chargerait
elle de manipuler les relations.</p>
<p>Le besoin de relations pourrait être un bon prétexte pour implémenter le
protocole avec Django <span class="caps">REST</span> Framework :)</p>
<blockquote>
Est-il possible de faire ci ou ça avec Cliquet ?</blockquote>
<p>Nous aimerions collecter des besoins pour écrire un ensemble de «recettes/tutoriels». Mais
pour ne pas travailler dans le vide, nous aimerions <a class="reference external" href="https://github.com/mozilla-services/cliquet/issues">connaitre vos idées</a> !
(<em>ex. brancher l’authentification Github, changer le format du logging <span class="caps">JSON</span>, stocker des
données cartographiques, …</em>)</p>
<blockquote>
Est-ce que Cliquet peut manipuler des fichiers ?</blockquote>
<p><a class="reference external" href="https://github.com/mozilla-services/cliquet/issues/236">Nous l’envisageons</a>,
mais pour l’instant nous attendons que le besoin survienne en interne pour se lancer.</p>
<p>Si c’est le cas, le protocole utilisé sera <a class="reference external" href="http://remotestorage.io/">Remote Storage</a>,
afin notamment de s’intégrer dans l’éco-système grandissant.</p>
<blockquote>
Est-ce que la fonctionnalité X va être implémentée ?</blockquote>
<p><em>Cliquet</em> est déjà bien garni. Plutôt qu’implémenter la fonctionnalité X,
il y a de grandes chances que nous agissions pour s’assurer que les abstractions
et les mécanismes d’extension fournis permettent de l’implémenter sous forme d’extension.</p>
</div>
</div>
Service de nuages : Perspectives pour l’été2015-07-07T00:00:00+02:002015-07-07T00:00:00+02:00tag:blog.notmyidea.org,2015-07-07:/service-de-nuages-perspectives-pour-lete-fr.html<p class="first last">Le travail en cours et les fonctionnalités à venir pour les prochains mois.</p>
<p><em>Cet article est repris depuis le blog « Service de Nuages » de mon équipe à Mozilla</em></p>
<p>Mozilla a pour coutume d’organiser régulièrement des semaines de travail où tous les employés
sont réunis physiquement. Pour cette dernière édition, nous avons pu retrouver
nos collègues du monde entier à <a class="reference external" href="http://www.openstreetmap.org/node/268148288#map=4/50.12/-122.95">Whistler, en Colombie Britannique au Canada</a> !</p>
<img alt="«All Hands» talk about Lego, by @davidcrob - CC0" class="align-center" src="https://blog.notmyidea.org/images/whistler/whistler-talks.jpg" />
<p>Ce fût l’occasion pour notre équipe de se retrouver, et surtout de partager notre
vision et nos idées dans le domaine du stockage, afin de collecter des cas d’utilisation pour
notre solution <a class="reference external" href="https://kinto.readthedocs.org">Kinto</a>.</p>
<p>Dans cet article, nous passons en revue les pistes que nous avons pour
les prochains mois.</p>
<div class="section" id="ateliers-et-promotion">
<h2>Ateliers et promotion</h2>
<p>Nicolas a présenté <a class="reference external" href="https://github.com/mozilla-services/kinto.js">Kinto.js</a> dans un atelier dédié, avec comme support de
présentation le <a class="reference external" href="http://kintojs.readthedocs.org/en/latest/tutorial/">tutorial d’introduction</a>.</p>
<p>L’application résultante, pourtant toute simple, permet d’appréhender les
concepts de synchronisation de Kinto. Le tout sans installation prélable,
puisque Rémy a mis en place un <a class="reference external" href="https://kinto.dev.mozaws.net/v1/">serveur de dev effacé tous les jours</a>.</p>
<p>Nous avions mis un point d’honneur à faire du Vanilla.<span class="caps">JS</span>, déjà pour éviter les
combats de clochers autour des frameworks, mais aussi pour mettre en évidence qu’avec
<span class="caps">HTML5</span> et <span class="caps">ES6</span>, on n’était plus aussi démunis qu’il y a quelques années.</p>
<p>Ce petit atelier nous a permis de nous rendre compte qu’on avait encore de
grosses lacunes en terme de documentation, surtout en ce qui concerne
l’éco-système et la vision globale des projets (Kinto, Kinto.js, Cliquet, …).
Nous allons donc faire de notre mieux pour combler ce manque.</p>
<img alt="Kinto.js workshop - CC0" class="align-center" src="https://blog.notmyidea.org/images/whistler/whistler-workshop.jpg" />
</div>
<div class="section" id="mozilla-payments">
<h2>Mozilla Payments</h2>
<p>Comme <a class="reference external" href="http://www.servicedenuages.fr/la-gestion-des-permissions">décrit précédemment</a>, nous avons mis en place un système de permissions pour répondre aux besoins de suivi des paiements et abonnements.</p>
<p>Pour ce projet, Kinto sera utilisé depuis une application Django, via un client Python.</p>
<p>Maintenant que les développements ont été livrés, il faut transformer l’essai, réussir l’intégration, l’hébergement et la montée en puissance. La solution doit être livrée à la fin de l’année.</p>
<div class="section" id="a-venir">
<h3>À venir</h3>
<p>Nous aimerions en profiter pour implémenter une fonctionnalité qui nous tient à coeur : la construction de la liste des enregistrements accessibles en lecture sur une collection partagée.</p>
<img alt="Whistler Alta Lake - CC0" class="align-center" src="https://blog.notmyidea.org/images/whistler/whistler-lake.jpg" />
</div>
</div>
<div class="section" id="firefox-os-et-stockage">
<h2>Firefox <span class="caps">OS</span> et stockage</h2>
<p>Nous avons eu beaucoup d’échanges avec l’équipe de Firefox <span class="caps">OS</span>, avec qui nous avions
déjà eu l’occasion de collaborer, pour le <a class="reference external" href="https://github.com/mozilla-services/msisdn-gateway">serveur d’identification BrowserID par <span class="caps">SMS</span></a> et pour <a class="reference external" href="https://github.com/mozilla-services/loop-server">Firefox Hello</a>.</p>
<div class="section" id="in-app-sync">
<h3>In-App sync</h3>
<p>Kinto, la solution simple promue pour la synchronisation de données dans les applications
Firefox <span class="caps">OS</span> ? La classe ! C’est ce qu’on avait en tête depuis longtemps, déjà à
l’époque avec <a class="reference external" href="http://daybed.readthedocs.org/">Daybed</a>. Voici donc une belle opportunité à saisir !</p>
<p>Il va falloir expliciter les limitations et hypothèses simplificatrices de notre
solution, surtout en termes de gestion de la concurrence. Nous sommes persuadés
que ça colle avec la plupart des besoins, mais il ne faudrait pas décevoir :)</p>
<p>Le fait que <a class="reference external" href="https://github.com/daleharvey">Dale</a>, un des auteurs de <a class="reference external" href="http://pouchdb.com/">PouchDB</a> et <a class="reference external" href="https://github.com/michielbdejong">Michiel de Jong</a>, un des auteurs de <a class="reference external" href="http://remotestorage.io/">Remote Storage</a>, nous aient encouragés sur nos premiers pas nous a bien motivé !</p>
</div>
<div class="section" id="cut-the-rope">
<h3>Cut the Rope</h3>
<p>Kinto devrait être mis à profit pour synchroniser les paramètres et les scores
du <a class="reference external" href="http://mozilla.cuttherope.net/">jeu</a>. Un premier exercice et une première vitrine sympas !</p>
</div>
<div class="section" id="syncto">
<h3>« SyncTo »</h3>
<p><a class="reference external" href="https://docs.services.mozilla.com/storage/apis-1.5.html">Firefox Sync</a> est la solution qui permet de synchroniser les données de Firefox (favoris, extensions, historique, complétion des formulaires, mots de passe, …) entre plusieurs périphériques, de manière chiffrée.</p>
<p>L’implémentation du client en JavaScript est relativement complexe et date un peu maintenant.
Le code existant n’est pas vraiment portable dans <em>Firefox <span class="caps">OS</span></em> et les tentatives de réécriture
n’ont pas abouti.</p>
<p>Nous souhaitons implémenter un pont entre <em>Kinto</em> et <em>Firefox Sync</em>, de manière
à pouvoir utiliser le client <em>Kinto.js</em>, plus simple et plus moderne, pour récupérer
les contenus et les stocker dans IndexedDB. Le delta à implémenter côté serveur est faible car nous nous étions
inspirés du protocole déjà éprouvé de Sync. Côté client, il s’agira surtout de
câbler l’authentification BrowserId et la Crypto.</p>
<p>Alexis a sauté sur l’occasion pour commencer l’écriture d’<a class="reference external" href="https://github.com/mozilla-services/syncclient">un client python pour Firefox Sync</a>, qui servira de brique de base pour l’écriture du service.</p>
</div>
<div class="section" id="cloud-storage">
<h3>Cloud Storage</h3>
<p>Eden Chuang et Sean Lee ont présenté les avancées sur l’intégration de services de stockages
distants (<em>DropBox, Baidu Yun</em>) dans <em>Firefox <span class="caps">OS</span></em>. Actuellement, leur preuve de
concept repose sur <a class="reference external" href="https://fr.wikipedia.org/wiki/Filesystem_in_Userspace"><span class="caps">FUSE</span></a>.</p>
<p>Nous avons évidemment en tête d’introduire la notion de fichiers attachés dans
<em>Kinto</em>, en implémentant la specification
<a class="reference external" href="https://tools.ietf.org/html/draft-dejong-remotestorage-05">*Remote Storage*</a>,
mais pour l’instant les cas d’utilisations ne se sont pas encore présentés officiellement.</p>
</div>
<div class="section" id="a-venir-1">
<h3>À venir</h3>
<p>Nous serons probablement amenés à introduire la gestion de la concurrence dans
le client <span class="caps">JS</span>, en complément de ce qui a été fait sur le serveur, pour permettre
les écritures simultanées et synchronisation en tâche de fond.</p>
<p>Nous sommes par ailleurs perpétuellement preneurs de vos retours — et bien
entendu de vos contributions — tant sur le code <a class="reference external" href="https://github.com/mozilla-services/kinto/">serveur</a>
que <a class="reference external" href="https://github.com/mozilla-services/kinto.js/">client</a> !</p>
<img alt="Firefox OS Cloud Storage Presentation - CC0" class="align-center" src="https://blog.notmyidea.org/images/whistler/whistler-cloud-storage.jpg" />
</div>
</div>
<div class="section" id="contenus-applicatifs-de-firefox">
<h2>Contenus applicatifs de Firefox</h2>
<p>Aujourd’hui Firefox a un cycle de release de six semaines. Un des objectifs
consiste à désolidariser certains contenus applicatifs de ces cycles
relativement longs (ex. <em>règles de securité, dictionnaires, traductions, …</em>) <a class="footnote-reference" href="#footnote-1" id="footnote-reference-1">[1]</a>.</p>
<p>Il s’agit de données <span class="caps">JSON</span> et binaire qui doivent être versionnées et synchronisées par
les navigateurs (<em>lecture seule</em>).</p>
<p>Il y a plusieurs outils officiels qui existent pour gérer ça (<em>Balrog</em>, <em>Shavar</em>, …),
et pour l’instant, aucun choix n’a été fait. Mais lors des conversations avec
l’équipe en charge du projet, ce fût vraiment motivant de voir que même pour
ce genre de besoins internes, <em>Kinto</em> est tout aussi pertinent !</p>
<table class="docutils footnote" frame="void" id="footnote-1" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-1">[1]</a></td><td>La bonne nouvelle c’est que toutes les fonctionnalités <em>third-party</em> qui ont
été intégrées récemment vont redevenir des <em>add-ons</em> \o/.</td></tr>
</tbody>
</table>
<img alt="Landscape - CC0" class="align-center" src="https://blog.notmyidea.org/images/whistler/whistler-landscape.jpg" />
</div>
<div class="section" id="awesome-bar">
<h2>Awesome bar</h2>
<p>L’équipe <em>Firefox Labs</em>, le laboratoire qui élève des pandas roux en éprouvette,
serait vraiment intéressé par notre solution, notamment pour abreuver en données
un prototype pour améliorer <em>Awesome bar</em>, qui fusionnerait <span class="caps">URL</span>, historique et recherche.</p>
<p>Nous ne pouvons pas en dire beaucoup plus pour l’instant, mais les fonctionnalités
de collections d’enregistrements partagées entre utilisateurs de <em>Kinto</em>
correspondent parfaitement à ce qui est envisagé pour le futur du navigateur :)</p>
<div class="section" id="a-venir-2">
<h3>À venir</h3>
<p>Nous serons donc probablement amenés, avant de la fin de l’année, à introduire des
fonctionnalités d’indexation et de recherche <em>full-text</em> (comprendre <em>ElasticSearch</em>).
Cela rejoint nos plans précédents, puisque c’est quelque chose que nous avions dans
<em>Daybed</em>, et qui figurait sur notre feuille de route !</p>
<img alt="Firefox Labs Meeting - CC0" class="align-center" src="https://blog.notmyidea.org/images/whistler/whistler-labs.jpg" />
</div>
</div>
<div class="section" id="browser-html">
<h2>Browser.html</h2>
<p>L’équipe <em>Recherche</em> explore les notions de plateforme, et travaille notamment
sur l’implémentation d’un navigateur en <span class="caps">JS</span>/<span class="caps">HTML</span> avec <em>React</em>:
<a class="reference external" href="https://github.com/mozilla/browser.html">browser.html</a></p>
<p><em>Kinto</em> correspond parfaitement aux attentes
de l’équipe pour synchroniser les données associées à un utilisateur.</p>
<p>Il pourrait s’agir de données de navigation (comme Sync), mais aussi de collections
d’enregistrements diverses, comme par exemple les préférences du navigateur
ou un équivalent à <em>Alexa.com Top 500</em> pour fournir la complétion d’<span class="caps">URL</span> sans
interroger le moteur de recherche.</p>
<p>L’exercice pourrait être poussé jusqu’à la synchronisation d’états <em>React</em>
entre périphériques (par exemple pour les onglets).</p>
<div class="section" id="a-venir-3">
<h3>À venir</h3>
<p>Si <em>browser.html</em> doit stocker des données de navigation, il faudra ajouter
des fonctionnalités de chiffrement sur le client <span class="caps">JS</span>. Ça tombe bien, c’est un
sujet passionant, et <a class="reference external" href="http://www.w3.org/TR/WebCryptoAPI/">il y a plusieurs standards</a> !</p>
<p>Pour éviter d’interroger le serveur à intervalle régulier afin de synchroniser les
changements, l’introduction des <a class="reference external" href="https://w3c.github.io/push-api/">*push notifications*</a> semble assez naturelle.
Il s’agirait alors de la dernière pierre qui manque à l’édifice pour obtenir
un «<em>Mobile/Web backend as a service</em>» complet.</p>
<img alt="Roadmap - CC0" class="align-center" src="https://blog.notmyidea.org/images/whistler/whistler-roadmap.jpg" />
</div>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>Nous sommes dans une situation idéale, puisque ce que nous avions imaginé
sur <a class="reference external" href="https://github.com/mozilla-services/kinto/wiki/Roadmap">notre feuille de route</a> correspond à ce qui nous est demandé par les
différentes équipes.</p>
<p>L’enjeu consiste maintenant à se coordonner avec tout le monde, ne pas décevoir,
tenir la charge, continuer à améliorer et à faire la promotion du produit, se concentrer
sur les prochaines étapes et embarquer quelques contributeurs à nos cotés pour
construire une solution libre, générique, simple et auto-hébergeable pour le stockage
de données sur le Web :)</p>
<img alt="Friday Night Party - CC0" class="align-center" src="https://blog.notmyidea.org/images/whistler/whistler-top-roof.jpg" />
</div>
Service de nuages : Achievement unlocked2015-06-01T00:00:00+02:002015-06-01T00:00:00+02:00tag:blog.notmyidea.org,2015-06-01:/service-de-nuages-achievement-unlocked-fr.html<p class="first last">Après près de 3 mois intensifs, nous venons de <em>tagguer</em> Cliquet 2.0
et Kinto 1.0 !</p>
<p><em>Cet article est repris depuis le blog « Service de Nuages » de mon équipe à Mozilla</em></p>
<p>Aujourd’hui, c’est jour de fête : nous venons de publier Cliquet <strong>2.0</strong>
<a class="footnote-reference" href="#footnote-1" id="footnote-reference-1">[1]</a> et Kinto <strong>1.0</strong> <a class="footnote-reference" href="#footnote-2" id="footnote-reference-2">[2]</a>.</p>
<blockquote class="epigraph">
<p>L’aboutissement de 3 années de R&D !</p>
<p class="attribution">—Rémy</p>
</blockquote>
<p><a class="reference external" href="https://kinto.readthedocs.org/en/latest/">Kinto</a> est un service pour
stocker, synchroniser et partager des données arbitraires, attachées à un
compte Firefox (mais le système d’authentification est <em>pluggable</em>).</p>
<p><a class="reference external" href="https://cliquet.readthedocs.org/en/latest/">Cliquet</a> est une boite à outils pour faciliter l’implémentation de
micro-services <span class="caps">HTTP</span> tels que les APIs <em><span class="caps">REST</span></em> ayant des besoins de synchronisation.</p>
<p>Vous pouvez lire plus à propos des raisons qui nous ont poussé à proposer cette nouvelle solution
et de notre ambition sur <a class="reference external" href="http://www.servicedenuages.fr/eco-systeme-et-stockage-generique.html">http://www.servicedenuages.fr/eco-systeme-et-stockage-generique.html</a></p>
<p>Nous sommes fiers du travail que nous avons pu réaliser durant ces derniers
mois sur ces deux projets. Bien que la plupart du travail que nous ayons
réalisé pour le serveur de liste de lecture (Reading List) ait pu être utilisé,
beaucoup de parties ont été repensées et nous avons introduit des
fonctionnalités que l’on attendait depuis longtemps, comme la gestion des permissions.</p>
<p>Bien sur, exactement comme après un ré-aménagement de salon, on ne peut
s’empêcher de voir toutes les choses qui doivent toujours être améliorées,
notamment sur la documentation et les performances.</p>
<p>On peut déjà entrevoir à quoi l’écosystème va ressembler, et c’est prometteur.
Il y a déjà un client JavaScript <a class="footnote-reference" href="#footnote-3" id="footnote-reference-3">[3]</a> dont l’objectif est de synchroniser les
données locales du navigateur avec une instance de Kinto.</p>
<p>N’hésitez vraiment pas à nous solliciter pour discuter avec vous si vous avez
des problématiques proches : nous accueillons avec plaisir toutes sortes de
retours, que ce soit à propos du code, de la documentation, de la sécurité de
la solution ou de la manière de communiquer avec le monde extérieur. Si vous
souhaitez nous contacter, vous pouvez laisser un commentaire ici ou nous
contacter sur le canal <a class="reference external" href="irc://irc.mozilla.org/#storage">#storage</a> sur le réseau <span class="caps">IRC</span> de Mozilla.</p>
<p>Et ce n’est que le début ! Le futur se dessine dans notre feuille de route
<a class="footnote-reference" href="#footnote-4" id="footnote-reference-4">[4]</a>.</p>
<table class="docutils footnote" frame="void" id="footnote-1" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-1">[1]</a></td><td><strong>Cliquet</strong> est une boite à outils pour faciliter l’implémentation de
microservices <span class="caps">HTTP</span> tels que les APIs <em><span class="caps">REST</span></em> ayant des besoins de
synchronisation.</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="footnote-2" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-2">[2]</a></td><td><strong>Kinto</strong> est un service pour stocker, synchroniser et partager des données
arbitraires, attachées à un compte Firefox (mais le système d’authentification
est <em>pluggable</em>).</td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="footnote-3" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-3">[3]</a></td><td>Cliquetis, la bibliothèque JavaScript pour consommer l’<span class="caps">API</span> <span class="caps">HTTP</span> de Kinto —
<a class="reference external" href="https://github.com/mozilla-services/cliquetis">https://github.com/mozilla-services/cliquetis</a></td></tr>
</tbody>
</table>
<table class="docutils footnote" frame="void" id="footnote-4" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-4">[4]</a></td><td>La feuille de route de Kinto: <a class="reference external" href="https://github.com/mozilla-services/kinto/wiki/roadmap">https://github.com/mozilla-services/kinto/wiki/roadmap</a></td></tr>
</tbody>
</table>
Service de nuages : Stocker et interroger les permissions avec Kinto2015-05-26T00:00:00+02:002015-05-26T00:00:00+02:00tag:blog.notmyidea.org,2015-05-26:/service-de-nuages-stocker-et-interroger-les-permissions-avec-kinto-fr.html<p class="first last">Comment faire pour stocker et interroger la base de données au sujet des permissions avec Kinto ?</p>
<p><em>Cet article est repris depuis le blog « Service de Nuages » de mon équipe à Mozilla</em></p>
<p><strong>tl;dr: On a maintenant un super système de permission mais comment faire pour stocker et interroger ces permissions de manière efficace ?</strong></p>
<div class="section" id="la-problematique">
<h2>La problématique</h2>
<p>Maintenant que nous avons défini un modèle de gestion des permissions
sur les objets qui nous satisfait, le problème est de stocker ces
permissions de manière efficace afin de pouvoir autoriser ou interdire
l’accès à un objet pour la personne qui fait la requête.</p>
<p>Chaque requête sur notre <span class="caps">API</span> va générer une ou plusieurs demandes
d’accès, il faut donc que la réponse soit très rapide sous peine
d’impacter la vélocité du service.</p>
</div>
<div class="section" id="obtenir-la-liste-des-principals-d-un-utilisateur">
<h2>Obtenir la liste des “principals” d’un utilisateur</h2>
<p>Les <em>principals</em> de l’utilisateur correspondent à son <tt class="docutils literal">user_id</tt>
ainsi qu’à la liste des identifiants des groupes dans lesquels il a
été ajouté.</p>
<p>Pour éviter de recalculer les <em>principals</em> de l’utilisateur à chaque
requête, le mieux reste de maintenir une liste des <em>principals</em> par utilisateur.</p>
<p>Ainsi lorsqu’on ajoute un utilisateur à un groupe, il faut bien penser
à ajouter le groupe à la liste des <em>principals</em> de l’utilisateur.</p>
<p>Ça se complexifie lorsqu’on ajoute un groupe à un groupe.</p>
<p>Dans un premier temps interdire l’ajout d’un groupe à un groupe est
une limitation qu’on est prêts à accepter pour simplifier le modèle.</p>
<p>L’avantage de maintenir la liste des <em>principals</em> d’un utilisateur
lors de la modification de cette liste c’est qu’elle est déjà
construite lors des lectures, qui sont dans notre cas plus fréquentes
que les écritures.</p>
<p>Cela nécessite de donner un identifiant unique aux groupes pour tous
les <em>buckets</em>.</p>
<p>Nous proposons de de les nommer avec leur <span class="caps">URI</span>:
<tt class="docutils literal">/buckets/blog/groups/moderators</tt></p>
</div>
<div class="section" id="obtenir-la-liste-des-principals-d-un-ace">
<h2>Obtenir la liste des “principals” d’un <span class="caps">ACE</span></h2>
<blockquote>
Rappel, un “<span class="caps">ACE</span>” est un <em>Access Control Entry</em>, un des éléments
d’une <span class="caps">ACL</span> (e.g. <em>modifier un enregistrement</em>).</blockquote>
<p>Avec le <a class="reference external" href="https://blog.notmyidea.org/service-de-nuages-la-gestion-des-permissions-fr.html">système de permissions choisi</a>, les permissions d’un
objet héritent de celle de l’objet parent.</p>
<p>Par exemple, avoir le droit d’écriture sur un <em>bucket</em> permet la
création des permissions et la modification de tous ses records.</p>
<p>Ce qui veut dire que pour obtenir la liste complète des <em>principals</em>
ayant une permission sur un objet, il faut regarder à plusieurs endroits.</p>
<p>Rémy a <a class="reference external" href="https://gist.github.com/Natim/77c8f61c1d42e476cef8#file-permission-py-L9-L52">décrit dans un gist la liste d’héritage de chaque permission</a>.</p>
<p>Prenons l’exemple de l’ajout d’un record dans une collection.</p>
<p>Le droit <tt class="docutils literal">records:create</tt> est obtenu si l’on a l’un des droits suivants:</p>
<ul class="simple">
<li><tt class="docutils literal">bucket:write</tt></li>
<li><tt class="docutils literal">collection:write</tt></li>
<li><tt class="docutils literal">records:create</tt></li>
</ul>
<p>Notre première idée était de stocker les permissions sur chaque objet
et de maintenir la liste exhaustive des permissions lors d’une
modification d’<span class="caps">ACL</span>. Cependant cela nécessitait de construire cette
liste lors de l’ajout d’un objet et de mettre à jour tout l’arbre lors
de sa suppression. (<em>Je vous laisse imaginer le nombre d’opérations
nécessaires pour ajouter un administrateur sur un *bucket</em> contenant
1000 collections avec 100000 records chacune.*)</p>
<p>La solution que nous avons désormais adoptée consiste à stocker les
<em>principals</em> de chaque <em><span class="caps">ACE</span></em> (<em>qui</em> a le droit de faire telle action
sur l’objet), et de faire l’union des <em><span class="caps">ACE</span></em> hérités, afin de les
croiser avec les <em>principals</em> de l’utilisateur :</p>
<blockquote>
(<span class="caps">ACE</span>(object, permission) ∪ inherited_ACE) ∩ <span class="caps">PRINCIPALS</span>(user)</blockquote>
<p>Par exemple l’<span class="caps">ACE</span>: <tt class="docutils literal">/buckets/blog/collections/article:records:create</tt> hérite de
l’<span class="caps">ACE</span> <tt class="docutils literal">/buckets/blog/collections/article:write</tt> et de <tt class="docutils literal">/buckets/blog:write</tt> :</p>
<blockquote>
(<span class="caps">ACE</span>(/buckets/blog/collections/article:records:create) ∪ <span class="caps">ACE</span>(/buckets/blog/collections/article:write) ∪ <span class="caps">ACE</span>(/buckets/blog:write)) ∩ <span class="caps">PRINCIPALS</span>(‘fxa:alexis’)</blockquote>
</div>
<div class="section" id="recuperer-les-donnees-de-l-utilisateur">
<h2>Récupérer les données de l’utilisateur</h2>
<p>La situation se corse lorsqu’on souhaite limiter la liste des
<em>records</em> d’une collection à ceux accessibles pour l’utilisateur, car
on doit faire cette intersection pour tous les <em>records</em>.</p>
<p>Une première solution est de regarder si l’utilisateur est mentionné
dans les <em><span class="caps">ACL</span>*s du *bucket</em> ou de la <em>collection</em>:</p>
<p>Ensuite, si ce n’est pas le cas, alors on filtre les <em>records</em> pour
lesquels les <em>principals</em> correspondent à ceux de l’utilisateur.</p>
<div class="highlight"><pre><span></span><span class="n">principals</span> <span class="o">=</span> <span class="n">get_user_principals</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
<span class="n">can_read_all</span> <span class="o">=</span> <span class="n">has_read_perms</span><span class="p">(</span><span class="n">bucket_id</span><span class="p">,</span> <span class="n">collection_id</span><span class="p">,</span>
<span class="n">principals</span><span class="p">)</span>
<span class="k">if</span> <span class="n">can_read_all</span><span class="p">:</span>
<span class="n">records</span> <span class="o">=</span> <span class="n">get_all_records</span><span class="p">(</span><span class="n">bucket_id</span><span class="p">,</span> <span class="n">collection_id</span><span class="p">,</span>
<span class="n">filters</span><span class="o">=</span><span class="p">[</span><span class="o">...</span><span class="p">])</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">records</span> <span class="o">=</span> <span class="n">filter_read_records</span><span class="p">(</span><span class="n">bucket_id</span><span class="p">,</span> <span class="n">collection_id</span><span class="p">,</span>
<span class="n">principals</span><span class="o">=</span><span class="n">principals</span><span class="p">,</span>
<span class="n">filters</span><span class="o">=</span><span class="p">[</span><span class="o">...</span><span class="p">])</span>
</pre></div>
<p>Il faudra faire quelque chose de similaire pour la suppression
multiple, lorsqu’un utilisateur souhaitera supprimer des
enregistrements sur lesquels il a les droits de lecture mais pas d’écriture.</p>
</div>
<div class="section" id="le-modele-de-donnees">
<h2>Le modèle de données</h2>
<p>Pour avoir une idée des requêtes dans un backend <span class="caps">SQL</span>, voyons un peu ce
que donnerait le modèle de données.</p>
<div class="section" id="le-format-des-id">
<h3>Le format des <span class="caps">ID</span></h3>
<p>Utiliser des <span class="caps">URI</span> comme identifiant des objets présente de nombreux
avantages (lisibilité, unicité, cohérence avec les URLs)</p>
<ul class="simple">
<li>bucket: <tt class="docutils literal">/buckets/blog</tt></li>
<li>groupe: <tt class="docutils literal">/buckets/blog/group/moderators</tt></li>
<li>collection: <tt class="docutils literal">/buckets/blog/collections/articles</tt></li>
<li>record: <tt class="docutils literal"><span class="pre">/buckets/blog/collections/articles/records/02f3f76f-7059-4ae4-888f-2ac9824e9200</span></tt></li>
</ul>
</div>
<div class="section" id="les-tables">
<h3>Les tables</h3>
<p>Pour le stockage des principals et des permissions:</p>
<div class="highlight"><pre><span></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">user</span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w"> </span><span class="n">principals</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">[]);</span>
<span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">perms</span><span class="p">(</span><span class="n">ace</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w"> </span><span class="n">principals</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">[]);</span>
</pre></div>
<p>La table <em>perms</em> va associer des <em>principals</em> à chaque <em><span class="caps">ACE</span></em> (e.g.“/buckets/blog:write“).</p>
<p>Pour le stockage des données:</p>
<div class="highlight"><pre><span></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">object</span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w"> </span><span class="n">parent_id</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w"> </span><span class="k">data</span><span class="w"> </span><span class="n">JSONB</span><span class="p">,</span>
<span class="w"> </span><span class="n">write_principals</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">[],</span><span class="w"> </span><span class="n">read_principals</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">[]);</span>
</pre></div>
<p>La colonne <em>parent_id</em> permet de savoir à qui appartient l’objet
(e.g. groupe d’un <em>bucket</em>, collection d’un <em>bucket</em>, <em>record</em> d’une
collection, …).</p>
</div>
<div class="section" id="exemple-d-utilisateur">
<h3>Exemple d’utilisateur</h3>
<div class="highlight"><pre><span></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="k">user</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">principals</span><span class="p">)</span>
<span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="s1">'fxa:alexis'</span><span class="p">,</span><span class="w"> </span><span class="s1">'{}'</span><span class="p">);</span>
<span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="k">user</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">principals</span><span class="p">)</span>
<span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="s1">'fxa:natim'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'{"/buckets/blog/groups/moderators"}'</span><span class="p">);</span>
</pre></div>
</div>
<div class="section" id="exemple-d-objets">
<h3>Exemple d’objets</h3>
<div class="section" id="bucket">
<h4>Bucket</h4>
<div class="highlight"><pre><span></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="k">object</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="k">type</span><span class="p">,</span><span class="w"> </span><span class="n">parent_id</span><span class="p">,</span><span class="w"> </span><span class="k">data</span><span class="p">,</span>
<span class="w"> </span><span class="n">read_principals</span><span class="p">,</span><span class="w"> </span><span class="n">write_principals</span><span class="p">)</span>
<span class="k">VALUES</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="s1">'/buckets/blog'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'bucket'</span><span class="p">,</span>
<span class="w"> </span><span class="k">NULL</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'{"name": "blog"}'</span><span class="p">::</span><span class="n">JSONB</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'{}'</span><span class="p">,</span><span class="w"> </span><span class="s1">'{"fxa:alexis"}'</span><span class="p">);</span>
</pre></div>
</div>
<div class="section" id="group">
<h4>Group</h4>
<div class="highlight"><pre><span></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="k">object</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="k">type</span><span class="p">,</span><span class="w"> </span><span class="n">parent_id</span><span class="p">,</span><span class="w"> </span><span class="k">data</span><span class="p">,</span>
<span class="w"> </span><span class="n">read_principals</span><span class="p">,</span><span class="w"> </span><span class="n">write_principals</span><span class="p">)</span>
<span class="k">VALUES</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="s1">'/buckets/blog/groups/moderators'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'group'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'/buckets/blog'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'{"name": "moderators", "members": ['</span><span class="n">fxa</span><span class="p">:</span><span class="n">natim</span><span class="s1">']}'</span><span class="p">::</span><span class="n">JSONB</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'{}'</span><span class="p">,</span><span class="w"> </span><span class="s1">'{}'</span><span class="p">);</span>
</pre></div>
<p>Ce groupe peut être gére par <tt class="docutils literal">fxa:alexis</tt> puisqu’il a la permission
<tt class="docutils literal">write</tt> dans le <em>bucket</em> parent.</p>
</div>
<div class="section" id="collection">
<h4>Collection</h4>
<div class="highlight"><pre><span></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="k">object</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="k">type</span><span class="p">,</span><span class="w"> </span><span class="n">parent_id</span><span class="p">,</span><span class="w"> </span><span class="k">data</span><span class="p">,</span>
<span class="w"> </span><span class="n">read_principals</span><span class="p">,</span><span class="w"> </span><span class="n">write_principals</span><span class="p">)</span>
<span class="k">VALUES</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="s1">'/buckets/blog/collections/articles'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'collection'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'/buckets/blog'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'{"name": "article"}'</span><span class="p">::</span><span class="n">JSONB</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'{"system.Everyone"}'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'{"/buckets/blog/groups/moderators"}'</span><span class="p">);</span>
</pre></div>
<p>Cette collection d’articles peut être lue par tout le monde,
et gérée par les membres du groupe <tt class="docutils literal">moderators</tt>, ainsi que
<tt class="docutils literal">fxa:alexis</tt>, via le <em>bucket</em>.</p>
</div>
<div class="section" id="records">
<h4>Records</h4>
<div class="highlight"><pre><span></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="k">object</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="k">type</span><span class="p">,</span><span class="w"> </span><span class="n">parent_id</span><span class="p">,</span><span class="w"> </span><span class="k">data</span><span class="p">,</span>
<span class="w"> </span><span class="n">read_principals</span><span class="p">,</span><span class="w"> </span><span class="n">write_principals</span><span class="p">)</span>
<span class="k">VALUES</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="s1">'/buckets/blog/collections/articles/records/02f3f76f-7059-4ae4-888f-2ac9824e9200'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'record'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'/buckets/blog/collections/articles'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'{"name": "02f3f76f-7059-4ae4-888f-2ac9824e9200",</span>
<span class="s1"> "title": "Stocker les permissions", ...}'</span><span class="p">::</span><span class="n">JSONB</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'{}'</span><span class="p">,</span><span class="w"> </span><span class="s1">'{}'</span><span class="p">);</span>
</pre></div>
</div>
</div>
<div class="section" id="interroger-les-permissions">
<h3>Interroger les permissions</h3>
<div class="section" id="obtenir-la-liste-des-principals-d-un-ace-1">
<h4>Obtenir la liste des “principals” d’un <span class="caps">ACE</span></h4>
<p>Comme vu plus haut, pour vérifier une permission, on fait l’union des
<em>principals</em> requis par les objets hérités, et on teste leur
intersection avec ceux de l’utilisateur:</p>
<div class="highlight"><pre><span></span><span class="k">WITH</span><span class="w"> </span><span class="n">required_principals</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="k">unnest</span><span class="p">(</span><span class="n">principals</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">p</span>
<span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">perms</span>
<span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">ace</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="s1">'/buckets/blog:write'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'/buckets/blog:read'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'/buckets/blog/collections/article:write'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'/buckets/blog/collections/article:read'</span><span class="p">)</span>
<span class="w"> </span><span class="p">),</span>
<span class="w"> </span><span class="n">user_principals</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="k">unnest</span><span class="p">(</span><span class="n">principals</span><span class="p">)</span>
<span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">user</span>
<span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'fxa:natim'</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span>
<span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">user_principals</span><span class="w"> </span><span class="n">a</span>
<span class="w"> </span><span class="k">INNER</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">required_principals</span><span class="w"> </span><span class="n">b</span>
<span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">a</span><span class="p">.</span><span class="n">p</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">b</span><span class="p">.</span><span class="n">p</span><span class="p">;</span>
</pre></div>
</div>
<div class="section" id="filtrer-les-objets-en-fonction-des-permissions">
<h4>Filtrer les objets en fonction des permissions</h4>
<p>Pour filtrer les objets, on fait une simple intersection de liste
(<em>merci PostgreSQL</em>):</p>
<div class="highlight"><pre><span></span><span class="k">SELECT</span><span class="w"> </span><span class="k">data</span>
<span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">object</span><span class="w"> </span><span class="n">o</span><span class="p">,</span><span class="w"> </span><span class="k">user</span><span class="w"> </span><span class="n">u</span>
<span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'record'</span>
<span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">parent_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'/buckets/blog/collections/article'</span>
<span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">read_principals</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">principals</span><span class="w"> </span><span class="k">OR</span>
<span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">write_principals</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">principals</span><span class="p">)</span>
<span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'fxa:natim'</span><span class="p">;</span>
</pre></div>
<p>Les listes s’indexent bien, notamment grâce aux <a class="reference external" href="http://www.postgresql.org/docs/current/static/indexes-types.html">index <span class="caps">GIN</span></a>.</p>
</div>
</div>
<div class="section" id="avec-redis">
<h3>Avec Redis</h3>
<p><em>Redis</em> présente plusieurs avantages pour ce genre de
problématiques. Notamment, il gère les <em>set</em> nativement (listes de
valeurs uniques), ainsi que les opérations d’intersection et d’union.</p>
<p>Avec <em>Redis</em> on peut écrire l’obtention des <em>principals</em> pour un <em><span class="caps">ACE</span></em>
comme cela :</p>
<div class="highlight"><pre><span></span>SUNIONSTORE temp_perm:/buckets/blog/collections/articles:write permission:/buckets/blog:write permission:/buckets/blog/collections/articles:write
SINTER temp_perm:/buckets/blog/collections/articles:write principals:fxa:alexis
</pre></div>
<ul class="simple">
<li><tt class="docutils literal"><span class="caps">SUNIONSTORE</span></tt> permet de créer un set contenant les éléments de
l’union de tous les set suivants. Dans notre cas on le nomme
<tt class="docutils literal"><span class="pre">temp_perm:/buckets/blog/collections/articles:write</span></tt> et il contient
l’union des sets d’ACLs suivants:
- <tt class="docutils literal"><span class="pre">permission:/buckets/blog:write</span></tt>
- <tt class="docutils literal"><span class="pre">permission:/buckets/blog/collections/articles:write</span></tt></li>
<li><tt class="docutils literal"><span class="caps">SINTER</span></tt> retourne l’intersection de tous les sets passés en paramètres dans notre cas :
- <tt class="docutils literal"><span class="pre">temp_perm:/buckets/blog/collections/articles:write</span></tt>
- <tt class="docutils literal">principals:fxa:alexis</tt></li>
</ul>
<p>Plus d’informations sur :
- <a class="reference external" href="http://redis.io/commands/sinter">http://redis.io/commands/sinter</a>
- <a class="reference external" href="http://redis.io/commands/sunionstore">http://redis.io/commands/sunionstore</a></p>
<p>Si le set résultant de la commande <tt class="docutils literal"><span class="caps">SINTER</span></tt> n’est pas vide, alors
l’utilisateur possède la permission.</p>
<p>On peut ensuite supprimer la clé temporaire <tt class="docutils literal">temp_perm</tt>.</p>
<p>En utilisant <tt class="docutils literal"><span class="caps">MULTI</span></tt> on peut <a class="reference external" href="https://gist.github.com/Natim/77c8f61c1d42e476cef8#file-permission-py-L117-L124">même faire tout cela au sein d’une
transaction</a>
et garantir ainsi l’intégrité de la requête.</p>
</div>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>La solution a l’air simple mais nous a demandé beaucoup de réflexion
en passant par plusieurs propositions.</p>
<p>L’idée finale est d’avoir :</p>
<ul class="simple">
<li>Un backend spécifique permettant de stocker les <em>principals</em> des
utilisateurs et des <em><span class="caps">ACE</span></em> (e.g. avec les sets Redis) ;</li>
<li>La liste des principals read et write sur la table des objets.</li>
</ul>
<p>C’est dommage d’avoir le concept de permissions à deux endroits, mais
cela permet de connaître rapidement la permission d’un utilisateur sur
un objet et également de pouvoir récupérer tous les objets d’une
collection pour un utilisateur si celui-ci n’a pas accès à tous les
records de la collection, ou toutes les collections du bucket.</p>
</div>
Les problèmes de PGP2015-05-25T00:00:00+02:002015-05-25T00:00:00+02:00tag:blog.notmyidea.org,2015-05-25:/les-problemes-de-pgp.html<blockquote>
<p>Flip a bit in the communication between sender and recipient and they
will experience decryption or verification errors. How high are the
chances they will start to exchange the data in the clear rather than
trying to hunt down the man in the middle?</p>
<p>— <a href="http://secushare.org/PGP">http://secushare.org/<span class="caps">PGP</span></a></p>
</blockquote>
<p>Une fois …</p><blockquote>
<p>Flip a bit in the communication between sender and recipient and they
will experience decryption or verification errors. How high are the
chances they will start to exchange the data in the clear rather than
trying to hunt down the man in the middle?</p>
<p>— <a href="http://secushare.org/PGP">http://secushare.org/<span class="caps">PGP</span></a></p>
</blockquote>
<p>Une fois passé l’euphorie du “il faut utiliser <span class="caps">PGP</span> pour l’ensemble de
nos communications”, j’ai réalisé lors de discussions que <span class="caps">PGP</span> avait
plusieurs problèmes, parmi ceux-ci:</p>
<ul>
<li>Les <em>meta données</em> (y compris le champ “sujet” de la conversation)
sont quand même échangées en clair (il est possible de savoir qu’un
message à été échangé entre telle et telle personne, a telle date);</li>
<li><span class="caps">PGP</span> se base sur un protocole de communication qui est lui non
chiffré, et il est donc facile de soit se tromper, soit dégrader le
mode de conversation vers une méthode non chiffrée;</li>
<li>Il est facile de connaître votre réseau social avec <span class="caps">PGP</span>, puisque
tout le principe est de signer les clés des personnes dont vous
validez l’identité;</li>
<li>En cas de fuite de votre clé privée, tous les messages que vous avez
chiffrés avec elle sont compromis. On dit que <span class="caps">PGP</span> ne fournit pas de
<em>forward secrecy</em>;</li>
<li>La découverte de la clé de pairs se passe souvent <em>en clair</em>, sans
utiliser une connexion “sécurisée” (<span class="caps">HTTPS</span>). Tout le monde peut donc
voir ces échanges et savoir de qui vous cherchez la clé;</li>
<li>Les discussions de groupes sont très difficiles: il faut chiffrer
pour chacun des destinataires (ou que ceux-ci partagent une paire de clés).</li>
</ul>
<p>Je suis en train de creuser à propos les alternatives à <span class="caps">PGP</span>, par exemple
<a href="https://pond.imperialviolet.org/">Pond</a>, qui lui ne construit pas par
dessus un standard déjà établi, et donc n’hérite pas de ses défauts
(mais pas non plus de son réseau déjà établi).</p>
<p>En attendant, quelques bonnes pratiques sur <span class="caps">PGP</span> ;)</p>
<h2 id="bonnes-pratiques">Bonnes pratiques</h2>
<p>Il est en fait assez facile d’utiliser <span class="caps">PGP</span> de travers. Riseup à fait <a href="https://help.riseup.net/en/security/message-security/openpgp/best-practices">un
excellent
guide</a>
qui explique comment configurer son installation correctement.</p>
<ul>
<li>J’en ai déjà parlé, mais il faut absolument choisir des phrases de
passes suffisamment longues. Pas facile de les retenir, mais
indispensable. Vous pouvez aussi avoir un document chiffré avec une
clé que vous ne mettez jamais en ligne, qui contiens ces phrases de
passe, au cas ou vous les oubliez.</li>
<li>Générez des clés <span class="caps">RSA</span> de 4096 bits, en utilisant sha512;</li>
<li>Il faut utiliser une date d’expiration de nos clés suffisamment
proche (2 ans). Il est possible de repousser cette date si
nécessaire, par la suite.</li>
</ul>
<p>Parmi les choses les plus frappantes que j’ai rencontrées:</p>
<ul>
<li>Utiliser le <em>flag</em> –hidden-recipient avec <span class="caps">PGP</span> pour ne pas dévoiler
qui est le destinataire du message;</li>
<li>Ne pas envoyer les messages de brouillons sur votre serveur, ils le
seraient en clair !;</li>
<li>Utilisez <span class="caps">HPKS</span> pour communiquer avec les serveurs de clés, sinon tout
le trafic est en clair.</li>
</ul>
<p>Le <a href="https://bitmask.net/">projet Bitmask</a> vise lui à rendre les outils
de chiffrement d’échanges de messages et de <span class="caps">VPN</span> simples à utiliser,
encore quelque chose à regarder.</p>
<p>Enfin bref, y’a du taf.</p>Simplifier les preuves d’identités2015-05-11T00:00:00+02:002015-05-11T00:00:00+02:00tag:blog.notmyidea.org,2015-05-11:/simplifier-les-preuves-didentites.html
<ul>
<li>headline<br>
Qu’est-ce que Keybase.io et comment essayent-ils de simplifier la
création de preuves d’identité.</li>
</ul>
<p>L’un des problèmes non réellement résolu actuellement quant au
chiffrement des échanges est lié à l’authenticité des clés. Si quelqu’un
décide de publier une clé en mon nom, et en …</p>
<ul>
<li>headline<br>
Qu’est-ce que Keybase.io et comment essayent-ils de simplifier la
création de preuves d’identité.</li>
</ul>
<p>L’un des problèmes non réellement résolu actuellement quant au
chiffrement des échanges est lié à l’authenticité des clés. Si quelqu’un
décide de publier une clé en mon nom, et en utilisant mon adresse email,
cela lui est assez facile.</p>
<p>Il est donc nécessaire d’avoir des moyens de prouver que la clé publique
que j’utilise est réellement la mienne.</p>
<p>Traditionnellement, il est nécessaire de faire signer ma clé publique
par d’autres personnes, via une rencontre en personne ou des échanges
hors du réseau. C’est par exemple ce qui est réalisé lors des <a href="https://fr.wikipedia.org/wiki/Key_signing_party">Key
Signing parties</a>.</p>
<p>Une manière simple d’effectuer ces vérifications serait, en plus de
donner son adresse email, sa signature de clé, ou a minima de donner un
mot clé pour valider que les échanges proviennent bien de la bonne personne.</p>
<p><span class="caps">PGP</span> propose un mécanisme de signature des clés d’autrui, une fois celles
ci validées, ce qui permet de placer sa confiance dans les signataires
de la clé.</p>
<p><a href="https://keybase.io">Keybase.io</a> est un service qui vise à rendre la
création de ces preuves plus facile, en partant du principe qu’il est
possible d’utiliser différents moyens afin de prouver l’identité des
personnes. Par exemple, leurs comptes Twitter, GitHub ou leurs noms de
domaines. De la même manière qu’il est possible de signer (valider) les
clés de nos amis, il est possible de les “tracker” selon le jargon de keybase.</p>
<p>Donc, en somme, <em>Keybase.io</em> est un annuaire, qui tente de rendre plus
facile la création de preuves. Bien.</p>
<h2 id="quelques-points-dombre">Quelques points d’ombre</h2>
<p>Il s’agit d’une <em>startup</em> américaine, domiciliée dans le Delaware, qui
se trouve être un des paradis fiscaux qui <a href="https://fr.wikipedia.org/wiki/Delaware">est connu pour être un
paradis fiscal au coeur même des
États-Unis</a>. Je ne veux pas
faire de raccourcis trop rapides, bien évidemment, alors <a href="https://github.com/keybase/keybase-issues/issues/1569">j’ai ouvert un
ticket sur GitHub pour en savoir
plus</a> (après
tout, le fait d’être un paradis fiscal permet peut-être d’échapper à
certaines lois sur la requêtes de données). D’autant plus étonnant, la
startup n’a pour l’instant <a href="https://github.com/keybase/keybase-issues/issues/788">pas de <em>business
model</em></a> (ce qui en
un sens est assez rassurant, même si on peut se poser la question de
pourquoi faire une startup dans ces cas là).</p>
<p>Le service (bien qu’en Alpha), n’est pas mis à disposition sous licence
libre, ce qui pour l’instant empêche quiconque de créer son propre
serveur Keybase. <a href="https://github.com/keybase/">Une partie des composants, cependant, le sont (open
source)</a>.</p>
<p>J’ai du mal à croire en des initiatives qui veulent sauver le monde,
mais dans leur coin, je ne comprends pas pourquoi il n’y à pas de
documentation sur comment monter son propre serveur, ou comment les
aider à travailler sur la fédération. Mais bon, c’est pour l’instant une
initiative encore fraîche, et je lui laisse le bénéfice du doute.</p>
<p>Sur le long terme, une infrastructure comme <em>Keybase.io</em>, devra
évidemment être
<a href="https://github.com/keybase/keybase-issues/issues/162">distribuée</a>.</p>
<blockquote>
<p>We’ve been talking about a total decentralization, but we have to
solve a couple things, synchronization in particular. Right now
someone can mirror us and a client can trust a mirror just as easily
as the server at keybase.io, but there needs to be a way of announcing
proofs to any server and having them cooperate with each other. We’d
be so happy to get this right.</p>
<p>— <a href="http://chris.beams.io/posts/keybase/">Chris Coyne, co-founder of Keybase</a></p>
</blockquote>
<p>Afin de se “passer” de leur service centralisé, les preuves générées
(qui sont la force du système qu’ils mettent en place) pourraient être
exportées sur des serveurs de clés existants. C’est quelque chose
<a href="https://github.com/keybase/keybase-issues/issues/890">qu’ils souhaitent réaliser
.</a>.</p>
<p>Bref, une initiative quand même importante et utile, même si elle
soulève des questions qui méritent qu’on s’y attarde un brin.</p>
<p>Par ailleurs, <a href="https://leap.se/nicknym">d’autres projets qui visent des objectifs
similaires</a> existent, via le projet <span class="caps">LEAP</span>, mais
je n’ai pas encore creusé.</p>Phrases de passe et bonnes pratiques2015-05-09T00:00:00+02:002015-05-09T00:00:00+02:00tag:blog.notmyidea.org,2015-05-09:/phrases-de-passe-et-bonnes-pratiques.html
<ul>
<li>headline<br>
Communiquer de manière chiffrée n’est pas aisée, et nécessite de
mémoriser des phrases de passes complexes. Comment s’en sortir ?</li>
</ul>
<blockquote>
<p>Au contraire des autres mots de passe, les mots de passe
cryptographiques ont specifiquement besoin d’être longs et extremement
difficiles à deviner. La raison est qu’un …</p></blockquote>
<ul>
<li>headline<br>
Communiquer de manière chiffrée n’est pas aisée, et nécessite de
mémoriser des phrases de passes complexes. Comment s’en sortir ?</li>
</ul>
<blockquote>
<p>Au contraire des autres mots de passe, les mots de passe
cryptographiques ont specifiquement besoin d’être longs et extremement
difficiles à deviner. La raison est qu’un ordinateur (ou un cluster de
plusieurs ordinateurs) peut être programmé pour faire des trillions
d’essais de manière automatique. Si le mot de passe choisi est trop
faible ou construit d’une manière trop prédictible, cette attaque par
la force pourrait se revéler fructueuse en essayant toutes les possibilités.</p>
<p>— <a href="https://www.eff.org/wp/defending-privacy-us-border-guide-travelers-carrying-digital-devices">The Electronic Frontier
Foundation</a>
(traduction de mon fait)</p>
</blockquote>
<p>Comprendre les concepts et l’écosystème qui permettent d’avoir une vie
numérique chiffrée n’est pas quelque chose d’aisé.
<a href="https://emailselfdefense.fsf.org/fr/">Plusieurs</a>
<a href="http://www.controle-tes-donnees.net/outils/GnuPG.html">guides</a> ont été
écrits à ce propos, et pour autant je me rends compte que naïvement il
est possible de mal utiliser les outils existants.</p>
<blockquote>
<p>Utilisez un <em>bon</em> mot de passe pour votre session utilisateur et une
<em>bonne</em> phrase de passe pour proteger votre clé privée. Cette phrase
de passe est la partie la plus fragile de tout le système.</p>
<p>— La page de manuel de <span class="caps">GPG</span>.</p>
</blockquote>
<p>Une phrase de passe devrait:</p>
<ul>
<li>Être suffisamment longue pour être difficile à deviner;</li>
<li>Ne pas être une citation connue (littérature, livres sacrés etc);</li>
<li>Difficile à deviner même pour vos proches;</li>
<li>Facile à se souvenir et à taper;</li>
<li>être unique et non partagée entre différents sites / applications etc.</li>
</ul>
<p>Une des techniques consiste à utiliser des mots du dictionnaire,
sélectionnés de manière aléatoire, puis modifiés.</p>
<p><img alt="Trough 20 years of effort, we've succesfully trained everyone to use passwords that are hard for humans to remember, but easy for computers to guess" src="https://imgs.xkcd.com/comics/password_strength.png"></p>
<p>Micah Lee <a href="https://github.com/micahflee/passphrases">travaille également sur un
outil</a> qui vise à rendre la
mémorisation des phrases de passe plus aisée, de par leur répétition
avec des pauses de plus en plus longues.</p>
<p>Oui, ce n’est pas aussi simple que ce qu’il y parait. Pour ma part, j’ai
une copie en local de mes clés, dans un fichier chiffré avec une autre
clé que j’ai généré pour l’occasion et que je ne partagerait pas. J’ai
par ailleurs <a href="https://github.com/jamessan/vim-gnupg">configuré</a> mon
éditeur de texte pour pouvoir chiffrer les documents textes par défaut.</p>
<p>J’ai donc regénéré une nouvelle fois mes clés de travail et
personnelles, en utilisant des phrases de passe plus complexes.</p>
<p>Reste encore la question de la sauvegarde de ces clés privées de manière
chiffrée, que je n’ai pas encore résolue. Bref, tout cela me semble bien
compliqué pour réussir à l’expliquer à des novices, qui pour certains ne
sont même pas sur de l’intérêt de la chose.</p>Service de nuages : La gestion des permissions2015-05-01T00:00:00+02:002015-05-01T00:00:00+02:00tag:blog.notmyidea.org,2015-05-01:/service-de-nuages-la-gestion-des-permissions-fr.html<p class="first last">Démystification du vocabulaire des permissions et proposition d’implémentation pour Kinto</p>
<p><em>Cet article est repris depuis le blog « Service de Nuages » de mon équipe à Mozilla</em></p>
<p>Dans le cadre de la création d’un service de stockage de données personnelles
(Kinto), la gestion des permissions est un des gros challenges : qui doit avoir
accès à quoi, et comment le définir ?</p>
<p><strong>tl;dr: Quelques retours sur le vocabulaire des systèmes de permission et sur nos idées pour l’implementation des permissions dans un stockage générique.</strong></p>
<div class="section" id="la-problematique">
<h2>La problématique</h2>
<p>La problématique est simple : des données sont stockées en ligne, et il
faut un moyen de pouvoir les partager avec d’autres personnes.</p>
<p>En regardant les cas d’utilisations, on se rend compte qu’on a plusieurs types
d’utilisateurs :</p>
<ul class="simple">
<li>les utilisateurs “finaux” (vous) ;</li>
<li>les applications qui interagissent en leurs noms.</li>
</ul>
<p>Tous les intervenants n’ont donc pas les mêmes droits : certains doivent
pouvoir lire, d’autres écrire, d’autres encore créer de nouveaux
enregistrements, et le contrôle doit pouvoir s’effectuer de manière fine : il
doit être possible de lire un enregistrement mais pas un autre, par exemple.</p>
<p>Nous sommes partis du constat que les solutions disponibles n’apportaient pas
une réponse satisfaisante à ces besoins.</p>
</div>
<div class="section" id="un-probleme-de-vocabulaire">
<h2>Un problème de vocabulaire</h2>
<p>Le principal problème rencontré lors des réflexions fût le vocabulaire.</p>
<p>Voici ci-dessous une explication des différents termes.</p>
<div class="section" id="le-concept-de-principal">
<h3>Le concept de « principal »</h3>
<p>Un <em>principal</em>, en sécurité informatique, est une entité qui peut être
authentifiée par un système informatique. <a class="footnote-reference" href="#footnote-1" id="footnote-reference-1">[1]</a> En Français il s’agit
du « commettant », l’acteur qui commet l’action (oui, le terme est conceptuel !)</p>
<p>Il peut s’agir aussi bien d’un individu, d’un ordinateur, d’un
service ou d’un groupe regroupant l’une de ces entités, ce qui
est plus large que le classique « <em>user id</em> ».</p>
<p>Les permissions sont alors associées à ces <em>principals</em>.</p>
<p>Par exemple, un utilisateur est identifié de manière unique lors de la
connexion par le système d’authentification dont le rôle est de
définir une liste de <em>principals</em> pour l’utilisateur se connectant.</p>
<table class="docutils footnote" frame="void" id="footnote-1" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-1">[1]</a></td><td>Pour en savoir plus sur les <em>principals</em> :
<a class="reference external" href="https://en.wikipedia.org/wiki/Principal_%28computer_security%29">https://en.wikipedia.org/wiki/Principal_%28computer_security%29</a></td></tr>
</tbody>
</table>
</div>
<div class="section" id="la-difference-entre-role-et-groupe">
<h3>La différence entre rôle et groupe</h3>
<p>De but en blanc, il n’est pas évident de définir précisément la
différence entre ces deux concepts qui permettent d’associer
des permissions à un groupe de <em>principals</em>. <a class="footnote-reference" href="#footnote-2" id="footnote-reference-2">[2]</a></p>
<p>La différence est principalement sémantique. Mais on peut y voir une
différence dans la « direction » de la relation entre les deux concepts.</p>
<ul class="simple">
<li>Un rôle est une liste de permissions que l’on associe à un <em>principal</em>.</li>
<li>Un groupe est une liste de <em>principals</em> que l’on peut associer à une permission.</li>
</ul>
<table class="docutils footnote" frame="void" id="footnote-2" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-2">[2]</a></td><td>Plus d’informations :
<a class="reference external" href="http://stackoverflow.com/questions/7770728/group-vs-role-any-real-difference">http://stackoverflow.com/questions/7770728/group-vs-role-any-real-difference</a></td></tr>
</tbody>
</table>
</div>
<div class="section" id="la-difference-entre-permission-acl-ace">
<h3>La différence entre permission, <span class="caps">ACL</span>, <span class="caps">ACE</span></h3>
<blockquote class="epigraph">
<p>Une <span class="caps">ACL</span> est une liste d’Access Control Entry (<span class="caps">ACE</span>) ou entrée de contrôle d’accès
donnant ou supprimant des droits d’accès à une personne ou un groupe.</p>
<p class="attribution">—<a class="reference external" href="https://fr.wikipedia.org/wiki/Access_Control_List">https://fr.wikipedia.org/wiki/Access_Control_List</a></p>
</blockquote>
<p>Je dirais même plus, dans notre cas, « à un <em>principal</em> ». Par exemple:</p>
<div class="highlight"><pre><span></span>create_record: alexis,remy,tarek
</pre></div>
<p>Cet <span class="caps">ACE</span> donne la permission <tt class="docutils literal">create</tt> sur l’objet <tt class="docutils literal">record</tt> aux
utilisateurs Tarek, Rémy et Alexis.</p>
</div>
</div>
<div class="section" id="la-delegation-de-permissions">
<h2>La délégation de permissions</h2>
<p>Imaginez l’exemple suivant, où un utilisateur stocke deux types de données en
ligne :</p>
<ul class="simple">
<li>des contacts ;</li>
<li>une liste de tâches à faire qu’il peut associer à ses contacts.</li>
</ul>
<p>L’utilisateur a tous les droits sur ses données.</p>
<p>Cependant il utilise deux applications qui doivent elles avoir un accès restreint :</p>
<ul class="simple">
<li>une application de gestion des contacts à qui il souhaite déléguer
la gestion intégrale de ses contacts : <tt class="docutils literal">contacts:write</tt> ;</li>
<li>une application de gestion des tâches à qui il souhaite déléguer la
gestion des tâches : <tt class="docutils literal">contacts:read tasks:write</tt></li>
</ul>
<p>Il souhaite que son application de contacts ne puisse pas accéder à
ses tâches et que son application de tâches ne puisse pas modifier ses
contacts existants, juste éventuellement en créer de nouveaux.</p>
<p>Il lui faut donc un moyen de déléguer certains de ses droits à un tiers (l’application).</p>
<p>C’est précisément le rôle des <a class="reference external" href="http://tools.ietf.org/html/rfc6749#page-23">scopes OAuth2</a>.</p>
<p>Lors de la connexion d’un utilisateur, une fenêtre lui demande quels
accès il veut donner à l’application qui va agir en son nom.</p>
<p>Le service aura ensuite accès à ces <em>scopes</em> en regardant le jeton
d’authentification utilisé. Cette information doit être
considérée comme une entrée utilisateur (c’est à dire qu’on ne peut
pas lui faire confiance). Il s’agit de ce que l’utilisateur souhaite.</p>
<p>Or, il est également possible que l’utilisateur n’ait pas accès aux données
qu’il demande. Le service doit donc utiliser deux niveaux de permissions :
celles de l’utilisateur, et celles qui ont été déléguées à l’application.</p>
</div>
<div class="section" id="espace-de-noms">
<h2>Espace de noms</h2>
<p>Dans notre implémentation initiale de <em>Kinto</em> (notre service de stockage en
construction), l’espace de nom était implicite : les données stockées étaient
nécessairement celles de l’utilisateur connecté.</p>
<p>Les données d’un utilisateur étaient donc cloisonnées et impossible à partager.</p>
<p>L’utilisation d’espaces de noms est une manière simple de gérer le partage des données.</p>
<p>Nous avons choisi de reprendre le modèle de GitHub et de Bitbucket, qui
utilisent les « organisations » comme espaces de noms.</p>
<p>Dans notre cas, il est possible de créer des “buckets”, qui correspondent à ces
espaces de noms. Un bucket est un conteneur de collections et de groupes utilisateurs.</p>
<p>Les ACLs sur ces collections peuvent être attribuées à certains groupes du
<em>bucket</em> ainsi qu’à d’autres <em>principals</em> directement.</p>
</div>
<div class="section" id="notre-proposition-d-api">
<h2>Notre proposition d’<span class="caps">API</span></h2>
<div class="section" id="les-objets-manipules">
<h3>Les objets manipulés</h3>
<p>Pour mettre en place la gestion des permissions, nous avons identifié les
objets suivants :</p>
<table border="1" class="docutils">
<colgroup>
<col width="23%" />
<col width="77%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head">Objet</th>
<th class="head">Description</th>
</tr>
</thead>
<tbody valign="top">
<tr><td><strong>bucket</strong></td>
<td>On peut les voir comme des espaces de noms. Ils
permettent d’avoir différentes collections portant
le même nom mais stockées dans différents <em>buckets</em> de
manière à ce que les données soient distinctes.</td>
</tr>
<tr><td><strong>collection</strong></td>
<td>Une liste d’enregistrements.</td>
</tr>
<tr><td><strong>record</strong></td>
<td>Un enregistrement d’une collection.</td>
</tr>
<tr><td><strong>group</strong></td>
<td>Un groupe de commetants (« <em>principals</em> »).</td>
</tr>
</tbody>
</table>
<p>Pour la définition des ACLs, il y a une hiérarchie et les objets « héritent » des
ACLs de leur parents :</p>
<div class="highlight"><pre><span></span> +---------------+
| Bucket |
+---------------+
+----->+ - id +<---+
| | - permissions | |
| +---------------+ |
| |
| |
| |
| |
| |
+---+-----------+ +------+---------+
| Collection | | Group |
+---------------+ +----------------+
| - id | | - id |
| - permissions | | - members |
+------+--------+ | - permissions |
^ +----------------+
|
|
+------+---------+
| Record |
+----------------+
| - id |
| - data |
| - permissions |
+----------------+
</pre></div>
</div>
<div class="section" id="les-permissions">
<h3>Les permissions</h3>
<p>Pour chacun de ces objets nous avons identifié les permissions suivantes :</p>
<table border="1" class="docutils">
<colgroup>
<col width="23%" />
<col width="77%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head">Permission</th>
<th class="head">Description</th>
</tr>
</thead>
<tbody valign="top">
<tr><td><strong>read</strong></td>
<td>La permission de lire le contenu de
l’objet et de ses sous-objets.</td>
</tr>
<tr><td><strong>write</strong></td>
<td>La permission de modifier et
d’administrer un objet et ses sous-
objets. La permission <em>write</em> permet la
lecture, modification et suppression
d’un objet ainsi que la gestion de ses
permissions.</td>
</tr>
<tr><td><strong>create</strong></td>
<td>La permission de créer le sous-objet
spécifié. Par exemple:
<tt class="docutils literal">collections:create</tt></td>
</tr>
</tbody>
</table>
<p>À chaque permission spécifiée sur un objet est associée une liste de
<em>principals</em>.</p>
<p>Dans le cas de la permission <tt class="docutils literal">create</tt> on est obligé de spécifier
l’objet enfant en question car un objet peut avoir plusieurs types
d’enfants. Par exemple : <tt class="docutils literal">collections:create</tt>, <tt class="docutils literal">groups:create</tt>.</p>
<p>Nous n’avons pour l’instant pas de permission pour <cite>delete</cite> et <cite>update</cite>,
puisque nous n’avons pas trouvé de cas d’utilisation qui les nécessitent.
Quiconque avec le droit d’écriture peut donc supprimer un enregistrement.</p>
<p>Les permissions d’un objet sont héritées de son parent. Par exemple,
un enregistrement créé dans une collection accessible à tout le monde
en lecture sera lui aussi accessible à tout le monde.</p>
<p>Par conséquent, les permissions sont cumulées. Autrement dit, il n’est pas
possible qu’un objet ait des permissions plus restrictives que son parent.</p>
<p>Voici la liste exhaustive des permissions :</p>
<table border="1" class="docutils">
<colgroup>
<col width="21%" />
<col width="32%" />
<col width="47%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head">Objet</th>
<th class="head">Permissions associées</th>
<th class="head">Commentaire</th>
</tr>
</thead>
<tbody valign="top">
<tr><td>Configuration
(.ini)</td>
<td><cite>buckets:create</cite></td>
<td>Les <em>principals</em> ayant le droit
de créer un bucket sont définis
dans la configuration du serveur.
(<em>ex. utilisateurs authentifiés</em>)</td>
</tr>
<tr><td rowspan="4"><tt class="docutils literal">bucket</tt></td>
<td><cite>write</cite></td>
<td>C’est en quelque sorte le droit
d’administration du <em>bucket</em>.</td>
</tr>
<tr><td><cite>read</cite></td>
<td>C’est le droit de lire le contenu
de tous les objets du <em>bucket</em>.</td>
</tr>
<tr><td><cite>collections:create</cite></td>
<td>Permission de créer des
collections dans le <em>bucket</em>.</td>
</tr>
<tr><td><cite>groups:create</cite></td>
<td>Permission de créer des groupes
dans le <em>bucket</em>.</td>
</tr>
<tr><td rowspan="3"><tt class="docutils literal">collection</tt></td>
<td><cite>write</cite></td>
<td>Permission d’administrer tous les
objets de la collection.</td>
</tr>
<tr><td><cite>read</cite></td>
<td>Permission de consulter tous les
objets de la collection.</td>
</tr>
<tr><td><cite>records:create</cite></td>
<td>Permission de créer des nouveaux
enregistrement dans la collection.</td>
</tr>
<tr><td rowspan="2"><tt class="docutils literal">record</tt></td>
<td><cite>write</cite></td>
<td>Permission de modifier ou de
partager l’enregistrement.</td>
</tr>
<tr><td><cite>read</cite></td>
<td>Permission de consulter
l’enregistrement.</td>
</tr>
<tr><td rowspan="2"><tt class="docutils literal">group</tt></td>
<td><cite>write</cite></td>
<td>Permission d’administrer le
groupe</td>
</tr>
<tr><td><cite>read</cite></td>
<td>Permission de consulter les
membres du groupe.</td>
</tr>
</tbody>
</table>
</div>
<div class="section" id="les-principals">
<h3>Les « <em>principals</em> »</h3>
<p>Les acteurs se connectant au service de stockage peuvent s’authentifier.</p>
<p>Ils reçoivent alors une liste de <em>principals</em> :</p>
<ul class="simple">
<li><tt class="docutils literal">Everyone</tt>: le <em>principal</em> donné à tous les acteurs (authentifiés ou pas) ;</li>
<li><tt class="docutils literal">Authenticated</tt>: le <em>principal</em> donné à tous les acteurs authentifiés ;</li>
<li>un <em>principal</em> identifiant l’acteur, par exemple <tt class="docutils literal">fxa:32aa95a474c984d41d395e2d0b614aa2</tt></li>
</ul>
<p>Afin d’éviter les collisions d’identifiants, le <em>principal</em> de l’acteur dépend
de son type d’authentification (<tt class="docutils literal">system</tt>, <tt class="docutils literal">basic</tt>, <tt class="docutils literal">ipaddr</tt>, <tt class="docutils literal">hawk</tt>,
<tt class="docutils literal">fxa</tt>) et de son identifiant (unique par acteur).</p>
<p>En fonction du <em>bucket</em> sur lequel se passe l’action, les groupes dont
fait partie l’utilisateur sont également ajoutés à sa liste de
<tt class="docutils literal">principals</tt>. <tt class="docutils literal">group:moderators</tt> par exemple.</p>
<p>Ainsi, si Bob se connecte avec <em>Firefox Accounts</em> sur le <em>bucket</em>
<tt class="docutils literal">servicedenuages_blog</tt> dans lequel il fait partie du groupe
<tt class="docutils literal">moderators</tt>, il aura la liste de <em>principals</em> suivante :
<tt class="docutils literal">Everyone, Authenticated, fxa:32aa95a474c984d41d395e2d0b614aa2, group:moderators</tt></p>
<p>Il est donc possible d’assigner une permission à Bob en utilisant l’un de
ces quatre <em>principals</em>.</p>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">Le <em>principal</em> <tt class="docutils literal"><userid></tt> dépend du <em>back-end</em> d’authentification (e.g.
<tt class="docutils literal">github:leplatrem</tt>).</p>
</div>
</div>
<div class="section" id="quelques-exemples">
<h3>Quelques exemples</h3>
<p><strong>Blog</strong></p>
<table border="1" class="docutils">
<colgroup>
<col width="35%" />
<col width="18%" />
<col width="46%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head">Objet</th>
<th class="head">Permissions</th>
<th class="head">Principals</th>
</tr>
</thead>
<tbody valign="top">
<tr><td><tt class="docutils literal">bucket:blog</tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal"><span class="pre">fxa:<blog</span> owner id></tt></td>
</tr>
<tr><td rowspan="2"><tt class="docutils literal">collection:articles</tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal">group:moderators</tt></td>
</tr>
<tr><td><tt class="docutils literal">read</tt></td>
<td><tt class="docutils literal">Everyone</tt></td>
</tr>
<tr><td><tt class="docutils literal">record:569e28r98889</tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal"><span class="pre">fxa:<co-author</span> id></tt></td>
</tr>
</tbody>
</table>
<p><strong>Wiki</strong></p>
<table border="1" class="docutils">
<colgroup>
<col width="35%" />
<col width="18%" />
<col width="46%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head">Object</th>
<th class="head">Permissions</th>
<th class="head">Principals</th>
</tr>
</thead>
<tbody valign="top">
<tr><td><tt class="docutils literal">bucket:wiki</tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal"><span class="pre">fxa:<wiki</span> administrator id></tt></td>
</tr>
<tr><td rowspan="2"><tt class="docutils literal">collection:articles</tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal">Authenticated</tt></td>
</tr>
<tr><td><tt class="docutils literal">read</tt></td>
<td><tt class="docutils literal">Everyone</tt></td>
</tr>
</tbody>
</table>
<p><strong>Sondages</strong></p>
<table border="1" class="docutils">
<colgroup>
<col width="34%" />
<col width="31%" />
<col width="35%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head">Objet</th>
<th class="head">Permissions</th>
<th class="head">Principals</th>
</tr>
</thead>
<tbody valign="top">
<tr><td rowspan="2"><tt class="docutils literal">bucket:poll</tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal"><span class="pre">fxa:<admin</span> id></tt></td>
</tr>
<tr><td><tt class="docutils literal">collection:create</tt></td>
<td><tt class="docutils literal">Authenticated</tt></td>
</tr>
<tr><td rowspan="2"><tt class="docutils literal"><span class="pre">collection:<poll</span> id></tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal"><span class="pre">fxa:<poll</span> author id></tt></td>
</tr>
<tr><td><tt class="docutils literal">record:create</tt></td>
<td><tt class="docutils literal">Everyone</tt></td>
</tr>
</tbody>
</table>
<p><strong>Cartes colaboratives</strong></p>
<table border="1" class="docutils">
<colgroup>
<col width="34%" />
<col width="31%" />
<col width="35%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head">Objet</th>
<th class="head">Permissions</th>
<th class="head">Principals</th>
</tr>
</thead>
<tbody valign="top">
<tr><td rowspan="2"><tt class="docutils literal">bucket:maps</tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal"><span class="pre">fxa:<admin</span> id></tt></td>
</tr>
<tr><td><tt class="docutils literal">collection:create</tt></td>
<td><tt class="docutils literal">Authenticated</tt></td>
</tr>
<tr><td rowspan="2"><tt class="docutils literal"><span class="pre">collection:<map</span> id></tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal"><span class="pre">fxa:<map</span> author id></tt></td>
</tr>
<tr><td><tt class="docutils literal">read</tt></td>
<td><tt class="docutils literal">Everyone</tt></td>
</tr>
<tr><td><tt class="docutils literal"><span class="pre">record:<record</span> id></tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal"><span class="pre">fxa:<maintainer</span> id></tt>
(<em>ex. event staff member
maintaining venues</em>)</td>
</tr>
</tbody>
</table>
<p><strong>Plateformes</strong></p>
<p>Bien sûr, il y a plusieurs façons de modéliser les cas d’utilisation typiques.
Par exemple, on peut imaginer une plateforme de wikis (à la wikia.com), où les
wikis sont privés par défaut et certaines pages peuvent être rendues publiques :</p>
<table border="1" class="docutils">
<colgroup>
<col width="32%" />
<col width="30%" />
<col width="38%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head">Objet</th>
<th class="head">Permissions</th>
<th class="head">Principals</th>
</tr>
</thead>
<tbody valign="top">
<tr><td rowspan="3"><tt class="docutils literal">bucket:freewiki</tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal"><span class="pre">fxa:<administrator</span> id></tt></td>
</tr>
<tr><td><tt class="docutils literal">collection:create</tt></td>
<td><tt class="docutils literal">Authenticated</tt></td>
</tr>
<tr><td><tt class="docutils literal">group:create</tt></td>
<td><tt class="docutils literal">Authenticated</tt></td>
</tr>
<tr><td rowspan="2"><tt class="docutils literal"><span class="pre">collection:<wiki</span> id></tt></td>
<td><tt class="docutils literal">write</tt></td>
<td><tt class="docutils literal"><span class="pre">fxa:<wiki</span> owner id></tt>,
<tt class="docutils literal"><span class="pre">group:<editors</span> id></tt></td>
</tr>
<tr><td><tt class="docutils literal">read</tt></td>
<td><tt class="docutils literal"><span class="pre">group:<readers</span> id></tt></td>
</tr>
<tr><td><tt class="docutils literal"><span class="pre">record:<page</span> id></tt></td>
<td><tt class="docutils literal">read</tt></td>
<td><tt class="docutils literal">Everyone</tt></td>
</tr>
</tbody>
</table>
</div>
<div class="section" id="l-api-http">
<h3>L’<span class="caps">API</span> <span class="caps">HTTP</span></h3>
<p>Lors de la création d’un objet, l’utilisateur se voit
attribué la permission <tt class="docutils literal">write</tt> sur l’objet :</p>
<div class="highlight"><pre><span></span><span class="nf">PUT</span> <span class="nn">/v1/buckets/servicedenuages_blog</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Authorization</span><span class="o">:</span> <span class="l">Bearer 0b9c2625dc21ef05f6ad4ddf47c5f203837aa32ca42fced54c2625dc21efac32</span>
<span class="na">Accept</span><span class="o">:</span> <span class="l">application/json</span>
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{
"id": "servicedenuages_blog",
"permissions": {
"write": ["fxa:49d02d55ad10973b7b9d0dc9eba7fdf0"]
}
}
</pre></div>
<p>Il est possible d’ajouter des permissions à l’aide de <tt class="docutils literal"><span class="caps">PATCH</span></tt> :</p>
<div class="highlight"><pre><span></span><span class="nf">PATCH</span> <span class="nn">/v1/buckets/servicedenuages_blog/collections/articles</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Authorization</span><span class="o">:</span> <span class="l">Bearer 0b9c2625dc21ef05f6ad4ddf47c5f203837aa32ca42fced54c2625dc21efac32</span>
<span class="na">Accept</span><span class="o">:</span> <span class="l">application/json</span>
{
"permissions": {
"read": ["+system.Everyone"]
}
}
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{
"id": "servicedenuages_blog",
"permissions": {
"write": ["fxa:49d02d55ad10973b7b9d0dc9eba7fdf0"],
"read": ["system.Everyone"]
}
}
</pre></div>
<p>Pour le <tt class="docutils literal"><span class="caps">PATCH</span></tt> nous utilisons la syntaxe préfixée par un <tt class="docutils literal">+</tt> ou
par un <tt class="docutils literal">-</tt> pour ajouter ou enlever des <em>principals</em> sur un <span class="caps">ACL</span>.</p>
<p>Il est également possible de faire un <tt class="docutils literal"><span class="caps">PUT</span></tt> pour réinitialiser les ACLs,
sachant que le <tt class="docutils literal"><span class="caps">PUT</span></tt> va ajouter l’utilisateur courant à la
liste automatiquement mais qu’il pourra se retirer avec un <tt class="docutils literal"><span class="caps">PATCH</span></tt>.
Ajouter l’utilisateur courant permet d’éviter les situations où plus
personne n’a accès aux données.</p>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">La permission <tt class="docutils literal">create</tt> est valable pour <tt class="docutils literal"><span class="caps">POST</span></tt> mais aussi pour <tt class="docutils literal"><span class="caps">PUT</span></tt>
lorsque l’enregistrement n’existe pas.</p>
</div>
</div>
<div class="section" id="le-cas-specifique-des-donnees-utilisateurs">
<h3>Le cas spécifique des données utilisateurs</h3>
<p>Une des fonctionnalités actuelles de <em>Kinto</em> est de pouvoir gérer des
collections d’enregistrements par utilisateur.</p>
<p>Sous <em>*nix</em> il est possible, pour une
application, de sauvegarder la configuration de l’utilisateur courant
dans son dossier personnel sans se soucier de l’emplacement sur
le disque en utilisant <tt class="docutils literal">~/</tt>.</p>
<p>Dans notre cas si une application souhaite sauvegarder les contacts d’un
utilisateur, elle peut utiliser le raccourci <tt class="docutils literal">~</tt> pour faire référence au
<em>bucket</em> <strong>personnel</strong> de l’utilisateur : <tt class="docutils literal"><span class="pre">/buckets/~/collections/contacts</span></tt></p>
<p>Cette <span class="caps">URL</span> retournera le code <tt class="docutils literal"><span class="caps">HTTP</span> 307</tt> vers le <em>bucket</em> de l’utilisateur courant :</p>
<div class="highlight"><pre><span></span><span class="nf">POST</span> <span class="nn">/v1/buckets/~/collections/contacts/records</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span>
{
"name": "Rémy",
"emails": ["remy@example.com"],
"phones": ["+330820800800"]
}
HTTP/1.1 307 Temporary Redirect
Location: /v1/buckets/fxa:49d02d55ad10973b7b9d0dc9eba7fdf0/collections/contacts/records
</pre></div>
<p>Ainsi il est tout à fait possible à Alice de partager ses contacts
avec Bob. Il lui suffit pour cela de donner la permission <tt class="docutils literal">read</tt> à
Bob sur sa collection et de donner l’<span class="caps">URL</span> complète
<tt class="docutils literal">/v1/buckets/fxa:49d02d55ad10973b7b9d0dc9eba7fdf0/collections/contacts/records</tt>
à Bob.</p>
</div>
<div class="section" id="la-delegation-des-permissions">
<h3>La délégation des permissions</h3>
<p>Dans le cas de <em>Kinto</em>, nous avons défini un format pour restreindre les
permissions via les scopes OAuth2:
<tt class="docutils literal"><span class="pre">storage:<bucket_id>:<collection_id>:<permissions_list></span></tt>.</p>
<p>Ainsi, si on reprend l’exemple précédent de la liste de tâches, il est possible pour
Bob de créer un token OAuth spécifique avec les <em>scopes</em> suivants :
<tt class="docutils literal">profile storage:todolist:tasks:write <span class="pre">storage:~:contacts:read+records:create</span></tt></p>
<p>Donc, bien que Bob a la permission <tt class="docutils literal">write</tt> sur ses contacts,
l’application utilisant ce token pourra uniquement lire les contacts
existants et en ajouter de nouveaux.</p>
<p>Une partie de la complexité est donc de réussir à présenter ces <em>scopes</em> de
manière lisible à l’utilisateur, afin qu’il choisisse quelles permissions
donner aux applications qui agissent en son nom.</p>
<p>Voilà où nous en sommes de notre réflexion !</p>
<p>Si vous avez des choses à ajouter, des points de désaccord ou autres
réflexions, n’hésitez pas à nous interrompre pendant qu’il est encore temps !</p>
</div>
</div>
Eco-système et stockage générique2015-04-30T00:00:00+02:002015-04-30T00:00:00+02:00tag:blog.notmyidea.org,2015-04-30:/eco-systeme-et-stockage-generique.html
<p><strong>tl;dr Nous devons construire un service de suivi de paiements, et nous
hésitons à continuer à nous entêter avec notre propre solution de stockage/synchronisation.</strong></p>
<p>Comme nous l’écrivions <a href="https://blog.notmyidea.org/service-de-nuages-fr.html">dans l’article
précédent</a>, nous
souhaitons construire une solution de stockage générique. On refait
<a href="http://daybed.readthedocs.org">Daybed</a> chez Mozilla !</p>
<p>Notre objectif est …</p>
<p><strong>tl;dr Nous devons construire un service de suivi de paiements, et nous
hésitons à continuer à nous entêter avec notre propre solution de stockage/synchronisation.</strong></p>
<p>Comme nous l’écrivions <a href="https://blog.notmyidea.org/service-de-nuages-fr.html">dans l’article
précédent</a>, nous
souhaitons construire une solution de stockage générique. On refait
<a href="http://daybed.readthedocs.org">Daybed</a> chez Mozilla !</p>
<p>Notre objectif est simple: permettre aux développeurs d’application,
internes à Mozilla ou du monde entier, de faire persister et
synchroniser facilement des données associées à un utilisateur.</p>
<div id="storage-specs">
Les aspects de l’architecture qui nous semblent incontournables:
</div>
<ul>
<li>La solution doit reposer sur un protocole, et non sur une
implémentation ;</li>
<li>L’auto-hébergement de l’ensemble doit être simplissime ;</li>
<li>L’authentification doit être <em>pluggable</em>, voire décentralisée
(OAuth2, FxA, Persona) ;</li>
<li>Les enregistrements doivent pouvoir être validés par le serveur ;</li>
<li>Les données doivent pouvoir être stockées dans n’importe quel
backend ;</li>
<li>Un système de permissions doit permettre de protéger des
collections, ou de partager des enregistrements de manière fine ;</li>
<li>La résolution de conflits doit pouvoir avoir lieu sur le serveur ;</li>
<li>Le client doit être pensé «*offline-first*» ;</li>
<li>Le client doit pouvoir réconcilier les données simplement ;</li>
<li>Le client doit pouvoir être utilisé aussi bien dans le navigateur
que côté serveur ;</li>
<li>Tous les composants se doivent d´être simples et substituables facilement.</li>
</ul>
<p>La première question qui nous a été posée fût «*Pourquoi vous
n’utilisez pas PouchDB ou Remote Storage ?*»</p>
<h2 id="remote-storage">Remote Storage</h2>
<p>Remote Storage est un standard ouvert pour du stockage par utilisateur.
<a href="http://tools.ietf.org/html/draft-dejong-remotestorage-04">La
specification</a>
se base sur des standards déjà existants et éprouvés: Webfinger, OAuth
2, <span class="caps">CORS</span> et <span class="caps">REST</span>.</p>
<p>L’<span class="caps">API</span> est simple, des <a href="http://blog.cozycloud.cc/news/2014/08/12/when-unhosted-meets-cozy-cloud/">projets prestigieux
l’utilisent</a>.
Il y a plusieurs <a href="https://github.com/jcoglan/restore">implémentations</a>
du serveur, et il existe <a href="https://www.npmjs.com/package/remotestorage-server">un squelette
Node</a> pour
construire un serveur sur mesure.</p>
<p><img alt="Remote Storage widget" src="%7Bfilename%7D/images/remotestorage-widget.png"></p>
<p>Le client
<a href="https://github.com/remotestorage/remotestorage.js/">remoteStorage.js</a>
permet d’intégrer la solution dans les applications Web. Il se charge du
«store local», du cache, de la synchronization, et fournit un widget qui
permet aux utilisateurs des applications de choisir le serveur qui
recevra les données (via Webfinger).</p>
<p><a href="https://github.com/michielbdejong/ludbud">ludbud</a>, la version épurée de
<em>remoteStorage.js</em>, se limite à l’abstraction du stockage distant. Cela
permettrait à terme, d’avoir une seule bibliothèque pour stocker dans un
serveur <em>remoteStorage</em>, <em>ownCloud</em> ou chez les méchants comme <em>Google
Drive</em> ou <em>Dropbox</em>.</p>
<p>Au premier abord, la spécification correspond à ce que nous voulons accomplir:</p>
<ul>
<li>La philosophie du protocole est saine;</li>
<li>L’éco-système est bien fichu;</li>
<li>La vision politique colle: redonner le contrôle des données aux
utilisateurs (voir <a href="http://unhosted.org/">unhosted</a>);</li>
<li>Les choix techniques compatibles avec ce qu’on a commencé (<span class="caps">CORS</span>,
<span class="caps">REST</span>, OAuth 2);</li>
</ul>
<p>En revanche, vis à vis de la manipulation des données, il y a plusieurs
différences avec ce que nous souhaitons faire:</p>
<ul>
<li>L’<span class="caps">API</span> suit globalement une métaphore «fichiers» (dossier/documents),
plutôt que «données» (collection/enregistrements) ;</li>
<li>Il n’y a pas de validation des enregistrements selon un schéma (même
si <a href="https://remotestorage.io/doc/code/files/baseclient/types-js.html">certaines
implémentations</a>
du protocole le font) ;</li>
<li>Il n’y a pas la possibilité de trier/filtrer les enregistrements
selon des attributs ;</li>
<li>Les permissions <a href="https://groups.google.com/forum/#!topic/unhosted/5_NOGq8BPTo">se limitent à
privé/public</a>
(et <a href="https://github.com/remotestorage/spec/issues/58#issue-27249452">l’auteur envisage plutôt un modèle à la
Git</a>)[1] ;</li>
</ul>
<p>En résumé, il semblerait que ce que nous souhaitons faire avec le
stockage d’enregistrements validés est complémentaire avec <em>Remote
Storage</em>.</p>
<p>Si des besoins de persistence orientés «fichiers» se présentent, a
priori nous aurions tort de réinventer les solutions apportées par cette
spécification. Il y a donc de grandes chances que nous l´intégrions à
terme, et que <em>Remote Storage</em> devienne une facette de notre solution.</p>
<h2 id="pouchdb">PouchDB</h2>
<p><a href="http://pouchdb.com/">PouchDB</a> est une bibliothèque JavaScript qui
permet de manipuler des enregistrements en local et de les synchroniser
vers une base distante.</p>
<div class="highlight"><pre><span></span><code><span class="n">javascript</span>
<span class="k">var</span><span class="w"> </span><span class="n">db</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">new</span><span class="w"> </span><span class="n">PouchDB</span><span class="p">(</span><span class="s1">'dbname'</span><span class="p">);</span>
<span class="n">db</span><span class="o">.</span><span class="n">put</span><span class="p">({</span>
<span class="w"> </span><span class="n">_id</span><span class="p">:</span><span class="w"> </span><span class="s1">'dave@gmail.com'</span><span class="p">,</span>
<span class="w"> </span><span class="n">name</span><span class="p">:</span><span class="w"> </span><span class="s1">'David'</span><span class="p">,</span>
<span class="w"> </span><span class="n">age</span><span class="p">:</span><span class="w"> </span><span class="mi">68</span>
<span class="p">});</span>
<span class="n">db</span><span class="o">.</span><span class="n">replicate</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="s1">'http://example.com/mydb'</span><span class="p">);</span>
</code></pre></div>
<p>Le projet a le vent en poupe, bénéficie de nombreux contributeurs,
l’éco-système est très riche et l’adoption par des projets <a href="https://github.com/hoodiehq/wip-hoodie-store-on-pouchdb">comme
Hoodie</a> ne fait
que confirmer la pertinence de l’outil pour les développeurs frontend.</p>
<p><em>PouchDB</em> gère un « store » local, dont la persistence est abstraite et
<a href="http://pouchdb.com/2014/07/25/pouchdb-levels-up.html">repose sur</a> l’<span class="caps">API</span>
<a href="https://github.com/level/levelup#relationship-to-leveldown">LevelDown</a>
pour persister les données dans <a href="https://github.com/Level/levelup/wiki/Modules#storage-back-ends">n’importe quel
backend</a>.</p>
<p>Même si <em>PouchDB</em> adresse principalement les besoins des applications
«*offline-first*», il peut être utilisé aussi bien dans le navigateur
que côté serveur, via Node.</p>
<h3 id="synchronisation">Synchronisation</h3>
<p>La synchronisation (ou réplication) des données locales s’effectue sur
un <a href="http://couchdb.apache.org/">CouchDB</a> distant.</p>
<p>Le projet <a href="https://github.com/pouchdb/pouchdb-server">PouchDB Server</a>
implémente l’<span class="caps">API</span> de CouchDB en NodeJS. Comme <em>PouchDB</em> est utilisé, on
obtient un service qui se comporte comme un <em>CouchDB</em> mais qui stocke
ses données n’importe où, dans un <em>Redis</em> ou un <em>PostgreSQL</em> par exemple.</p>
<p>La synchronisation est complète. Autrement dit, tous les enregistrements
qui sont sur le serveur se retrouvent synchronisés dans le client. Il
est possible de filtrer les collections synchronisées, mais cela <a href="http://pouchdb.com/2015/04/05/filtered-replication.html">n’a
pas pour objectif de sécuriser l’accès aux
données</a>.</p>
<p>L’approche recommandée pour cloisonner les données par utilisateur
consiste à créer <a href="https://github.com/nolanlawson/pouchdb-authentication#some-people-can-read-some-docs-some-people-can-write-those-same-docs">une base de données par
utilisateur</a>.</p>
<p>Ce n’est pas forcément un problème, CouchDB <a href="https://mail-archives.apache.org/mod_mbox/couchdb-user/201401.mbox/%3C52CEB873.7080404@ironicdesign.com%3E">supporte des centaines de
milliers de bases sans
sourciller</a>.
Mais selon les cas d’utilisation, le cloisement n’est pas toujours
facile à déterminer (par rôle, par application, par collection, …).</p>
<h2 id="le-cas-dutilisation-payments">Le cas d’utilisation « Payments »</h2>
<p><img alt="Put Payments Here -- Before the Internet - CC-NC-SA Katy Silberger
https://www.flickr.com/photos/katysilbs/11163812186" src="%7Bfilename%7D/images/put-payments.jpg"></p>
<p>Dans les prochaines semaines, nous devrons mettre sur pied un prototype
pour tracer l’historique des paiements et abonnements d’un utilisateur.</p>
<p>Le besoin est simple:</p>
<ul>
<li>l’application « Payment » enregistre les paiements et abonnements
d’un utilisateur pour une application donnée;</li>
<li>l’application « Donnée » interroge le service pour vérifier qu’un
utilisateur a payé ou est abonné;</li>
<li>l’utilisateur interroge le service pour obtenir la liste de tous ses abonnements.</li>
</ul>
<p>Seule l’application « Payment » a le droit de créer/modifier/supprimer
des enregistrements, les deux autres ne peuvent que consulter en lecture seule.</p>
<p>Une application donnée ne peut pas accéder aux paiements des autres
applications, et un utilisateur ne peut pas accéder aux paiements des
autres utilisateurs.</p>
<h3 id="avec-remotestorage">Avec RemoteStorage</h3>
<p><img alt="Remote Love - CC-BY-NC Julie
https://www.flickr.com/photos/mamajulie2008/2609549461" src="%7Bfilename%7D/images/remote-love.jpg"></p>
<p>Clairement, l’idée de <em>RemoteStorage</em> est de dissocier l’application
executée, et les données créées par l’utilisateur avec celle-ci.</p>
<p>Dans notre cas, c’est l’application « Payment » qui manipule des données
concernant un utilisateur. Mais celles-ci ne lui appartiennent pas
directement: certes un utilisateur doit pouvoir les supprimer, surtout
pas en créer ou les modifier!</p>
<p>La notion de permissions limitée à privé/publique ne suffit pas dans ce
cas précis.</p>
<h3 id="avec-pouchdb">Avec PouchDB</h3>
<p>Il va falloir créer une <em>base de données</em> par utilisateur, afin d’isoler
les enregistrements de façon sécurisée. Seule l’application « Payment »
aura tous les droits sur les databases.</p>
<p>Mais cela ne suffit pas.</p>
<p>Il ne faut pas qu’une application puisse voir les paiements des autres
applications, donc il va aussi falloir recloisonner, et créer une <em>base
de données</em> par application.</p>
<p>Quand un utilisateur voudra accéder à l’ensemble de ses paiements, il
faudra agréger les <em>databases</em> de toutes les applications. Quand
l’équipe marketing voudra faire des statistiques sur l’ensemble des
applications, il faudra agrégér des centaines de milliers de
<em>databases</em>.</p>
<p>Ce qui est fort dommage, puisqu’il est probable que les paiements ou
abonnements d’un utilisateur pour une application se comptent sur les
doigts d’une main. Des centaines de milliers de bases contenant moins de
5 enregistrements ?</p>
<p>De plus, dans le cas de l’application « Payment », le serveur est
implémenté en Python. Utiliser un wrapper JavaScript comme le fait
<a href="https://pythonhosted.org/Python-PouchDB/">python-pouchdb</a> cela ne nous
fait pas trop rêver.</p>
<h2 id="un-nouvel-eco-systeme">Un nouvel éco-système ?</h2>
<p><img alt="Wagon wheel - CC-BY-NC-SA arbyreed
https://www.flickr.com/photos/19779889@N00/16161808220" src="%7Bfilename%7D/images/wagon-wheel.jpg"></p>
<p>Évidemment, quand on voit la richesse des projets <em>PouchDB</em> et <em>Remote
Storage</em> et la dynamique de ces communautés, il est légitime d’hésiter
avant de développer une solution alternative.</p>
<p>Quand nous avons créé le serveur <em>Reading List</em>, nous l’avons construit
avec <a href="http://cliquet.readthedocs.org/">Cliquet</a>, ce fût l’occasion de
mettre au point <a href="http://cliquet.readthedocs.org/en/latest/api/">un protocole très
simple</a>, fortement
inspiré de <a href="http://en.wikipedia.org/wiki/Firefox_Sync">Firefox Sync</a>,
pour faire de la synchronisation d’enregistrements.</p>
<p>Et si les clients <em>Reading List</em> ont pu être implémentés en quelques
semaines, que ce soit en JavaScript, Java (Android) et <span class="caps">ASM</span> (Add-on
Firefox), c’est que le principe «*offline first*» du service est trivial.</p>
<h3 id="les-compromis">Les compromis</h3>
<p>Évidemment, nous n’avons pas la prétention de concurrencer <em>CouchDB</em>.
Nous faisons plusieurs concessions:</p>
<ul>
<li>De base, les collections d’enregistrements sont cloisonnées par utilisateur;</li>
<li>Pas d’historique des révisions;</li>
<li>Pas de diff sur les enregistrements entre révisions;</li>
<li>De base, pas de résolution de conflit automatique;</li>
<li>Pas de synchronisation par flux (<em>streams</em>);</li>
</ul>
<p>Jusqu’à preuve du contraire, ces compromis excluent la possibilité
d’implémenter un <a href="https://github.com/pouchdb/pouchdb/blob/master/lib/adapters/http/http.js#L721-L946">adapter
PouchDB</a>
pour la synchronisation avec le protocole <span class="caps">HTTP</span> de <em>Cliquet</em>.</p>
<p>Dommage puisque capitaliser sur l’expérience client de <em>PouchDB</em> au
niveau synchro client semble être une très bonne idée.</p>
<p>En revanche, nous avons plusieurs fonctionnalités intéressantes:</p>
<ul>
<li>Pas de map-reduce;</li>
<li>Synchronisation partielle et/ou ordonnée et/ou paginée ;</li>
<li>Le client choisit, via des headers, d’écraser la donnée ou de
respecter la version du serveur ;</li>
<li>Un seul serveur à déployer pour N applications ;</li>
<li>Auto-hébergement simplissime ;</li>
<li>Le client peut choisir de ne pas utiliser de « store local » du tout ;</li>
<li>Dans le client <span class="caps">JS</span>, la gestion du « store local » sera externalisée
(on pense à <a href="https://github.com/mozilla/localForage">LocalForage</a> ou
<a href="https://github.com/dfahlander/Dexie.js">Dexie.js</a>) ;</li>
</ul>
<p>Et, on répond au reste des <a href="#storage-specs">specifications mentionnées au début de
l’article</a> !</p>
<h3 id="les-arguments-philosophiques">Les arguments philosophiques</h3>
<p>Il est <a href="http://en.wikipedia.org/wiki/Law_of_the_instrument">illusoire de penser qu’on peut tout faire avec un seul
outil</a>.</p>
<p>Nous avons d’autres cas d’utilisations dans les cartons qui semblent
correspondre au scope de <em>PouchDB</em> (<em>pas de notion de permissions ou de
partage, environnement JavaScript, …</em>). Nous saurons en tirer profit
quand cela s’avèrera pertinent !</p>
<p>L’éco-système que nous voulons construire tentera de couvrir les cas
d’utilisation qui sont mal adressés par <em>PouchDB</em>. Il se voudra:</p>
<ul>
<li>Basé sur notre protocole très simple ;</li>
<li>Minimaliste et multi-usages (<em>comme la fameuse <span class="caps">2CV</span></em>) ;</li>
<li>Naïf (<em>pas de rocket science</em>) ;</li>
<li>Sans magie (<em>explicite et facile à réimplémenter from scratch</em>) ;</li>
</ul>
<p><a href="http://cliquet.readthedocs.org/en/latest/rationale.html">La philosophie et les fonctionnalités du toolkit python
Cliquet</a> seront
bien entendu à l’honneur :)</p>
<p>Quant à <em>Remote Storage</em>, dès que le besoin se présentera, nous serons
très fier de rejoindre l’initiative, mais pour l’instant cela nous
paraît risqué de démarrer en tordant la solution.</p>
<h3 id="les-arguments-pratiques">Les arguments pratiques</h3>
<p>Avant d’accepter de déployer une solution à base de <em>CouchDB</em>, les <em>ops</em>
de Mozilla vont nous demander de leur prouver par A+B que ce n’est pas
faisable avec les stacks qui sont déjà rodées en interne (i.e. MySQL,
Redis, PostgreSQL).</p>
<p>De plus, on doit s’engager sur une pérennité d’au moins 5 ans pour les
données. Avec <em>Cliquet</em>, en utilisant le backend PostgreSQL, les données
sont persistées à plat dans un <a href="https://github.com/mozilla-services/cliquet/blob/40aa33/cliquet/storage/postgresql/schema.sql#L14-L28">schéma PostgreSQL tout
bête</a>.
Ce qui ne sera pas le cas d’un adapteur LevelDown qui va manipuler des
notions de révisions éclatées dans un schéma clé-valeur.</p>
<p>Si nous basons le service sur <em>Cliquet</em>, comme c’est le cas avec
<a href="http://kinto.readthedocs.org">Kinto</a>, tout le travail d’automatisation
de la mise en production (<em>monitoring, builds <span class="caps">RPM</span>, Puppet…</em>) que nous
avons fait pour <em>Reading List</em> est complètement réutilisable.</p>
<p>De même, si on repart avec une stack complètement différente, nous
allons devoir recommencer tout le travail de rodage, de profiling et
d’optimisation effectué au premier trimestre.</p>
<h2 id="les-prochaines-etapes">Les prochaines étapes</h2>
<p>Et il est encore temps de changer de stratégie :) Nous aimerions avoir
un maximum de retours ! C’est toujours une décision difficile à
prendre… <code></appel à troll></code></p>
<ul>
<li>Tordre un éco-système existant vs. constuire sur mesure ;</li>
<li>Maîtriser l’ensemble vs. s’intégrer ;</li>
<li>Contribuer vs. refaire ;</li>
<li>Guider vs. suivre.</li>
</ul>
<p>Nous avons vraiment l’intention de rejoindre l’initiative
<a href="https://nobackend.org/">no-backend</a>, et ce premier pas n’exclue pas que
nous convergions à terme ! Peut-être que nous allons finir par rendre
notre service compatible avec <em>Remote Storage</em>, et peut-être que
<em>PouchDB</em> deviendra plus agnostique quand au protocole de synchronisation…</p>
<p><img alt="XKCD — Standards
https://xkcd.com/927/" src="%7Bfilename%7D/images/standards.png"></p>
<p>Utiliser ce nouvel écosystème pour le projet « Payments » va nous
permettre de mettre au point un système de permissions (<em>probablement
basé sur les scopes OAuth</em>) qui correspond au besoin exprimé. Et nous
avons bien l’intention de puiser dans <a href="http://blog.daybed.io/daybed-revival.html">notre expérience avec Daybed sur
le sujet</a>.</p>
<p>Nous extrairons aussi le code des clients implémentés pour <em>Reading
List</em> afin de faire un client JavaScript minimaliste.</p>
<p>En partant dans notre coin, nous prenons plusieurs risques:</p>
<ul>
<li>réinventer une roue dont nous n’avons pas connaissance ;</li>
<li>échouer à faire de l’éco-système <em>Cliquet</em> un projet communautaire ;</li>
<li>échouer à positionner <em>Cliquet</em> dans la niche des cas non couverts
par PouchDB :)</li>
</ul>
<p>Comme <a href="http://pouchdb.com/2015/04/05/filtered-replication.html">le dit Giovanni
Ornaghi</a>:</p>
<blockquote>
<p>Rolling out your set of webservices, push notifications, or background
services might give you more control, but at the same time it will
force you to engineer, write, test, and maintain a whole new ecosystem.</p>
</blockquote>
<p>C’est justement l’éco-système dont est responsable l’équipe <em>Mozilla
Cloud Services</em>!</p>
<ol>
<li>Il existe le <a href="https://sharesome.5apps.com/">projet Sharesome</a> qui
permet de partager publiquement des ressources de son <em>remote
Storage</em>.</li>
</ol>Service de nuages !2015-04-01T00:00:00+02:002015-04-01T00:00:00+02:00tag:blog.notmyidea.org,2015-04-01:/service-de-nuages-fr.html<p class="first last">Retour sur le premier trimestre 2015: Readinglist, Kinto, Cliquet.</p>
<p><em>Cet article est repris depuis le blog « Service de Nuages » de mon équipe à Mozilla</em></p>
<p>Pas mal de changements depuis le début de l’année pour l’équipe
«cloud-services» francophone!</p>
<p>Tout d’abord, nouvelle importante, l’équipe s’étoffe avec des profils assez
complémentaires: <a class="reference external" href="https://nicolas.perriault.net/">n1k0</a> et <a class="reference external" href="http://mathieu-leplatre.info">Mathieu</a> sont venus prêter main forte à <a class="reference external" href="http://ziade.org/">Tarek</a>, <a class="reference external" href="http://natim.ionyse.com">Rémy</a> et <a class="reference external" href="http://notmyidea.org">Alexis</a>.</p>
<p>Le début de l’année a vu le lancement de <a class="reference external" href="https://www.mozilla.org/en-US/firefox/hello/">Firefox Hello</a> ce qui nous a permis de passer
à l’échelle <a class="reference external" href="https://github.com/mozilla-services/loop-server">le serveur</a>,
écrit en Node.js®, pour l’occasion.</p>
<div class="section" id="un-serveur-de-listes-de-lecture">
<h2>Un serveur de listes de lecture</h2>
<p>En parallèle, un projet de <a class="reference external" href="https://readinglist.readthedocs.org">synchronisation de liste de lecture</a> (<em>Reading List</em>) a vu le jour. L’idée
étant de pouvoir marquer des pages “à lire pour plus tard” et de continuer la
lecture sur n’importe quel périphérique synchronisé (Firefox pour Android ou
Firefox Desktop). Un équivalent libre à <a class="reference external" href="http://getpocket.com">Pocket</a> en quelque sorte, qu’il est
possible d’héberger soit-même.</p>
<img alt="Capture d'écran de Firefox nightly avec readinglist." src="https://blog.notmyidea.org/images/readinglist-screenshot.png" />
<p>Pour le construire, nous aurions pu réutiliser <a class="reference external" href="https://github.com/mozilla-services/server-syncstorage">Firefox Sync</a>, après tout
c’est un service de synchronisation de données très robuste, construit avec <a class="reference external" href="http://cornice.readthedocs.org/">Cornice</a>.
Mais seulement, <em>Sync</em> n’a pas été pensé pour garantir la pérennité des données,
et la marche était trop haute pour changer ça en profondeur.</p>
<p>Nous aurions pu aussi nous contenter de faire une énième application qui expose
une <span class="caps">API</span> et persiste des données dans une base de données.</p>
<p>Mais cette nouvelle petite équipe n’est pas là par hasard :)</p>
</div>
<div class="section" id="la-daybed-team">
<h2>La «Daybed Team»</h2>
<p>On partage une vision: un service générique de stockage de données ! Peut-être
que ça vous rappelle <a class="reference external" href="https://daybed.io">un certain projet nommé Daybed</a> ?
Pour les applications clientes, JavaScript, mobiles ou autres, l’utilisation de
ce service doit être un jeu d’enfant ! L’application gère ses données
localement (aka offline-first), et synchronise à la demande.</p>
<p>Ici, le cœur du serveur <em>Reading List</em> est justement une <span class="caps">API</span> “<span class="caps">CRUD</span>” (Create,
Retrieve, Update, Delete), qui gère de la synchronisation et de
l’authentification. Nous avons donc pris le parti de faire une <span class="caps">API</span> “simple”,
avec le moins de spécificités possible, qui poserait les bases d’un service
générique. Notamment parce qu’il y a d’autres projets dans la même trempe qui vont suivre.</p>
<p>Pas mal d’expérience ayant été accumulée au sein de l’équipe, avec d’une part la
création de <em>Firefox Sync</em>, et d’autre part avec <em>Daybed</em>, notre side-project, nous
tentons de ne pas reproduire les mêmes erreurs, tout en gardant les concepts
qui ont fait leurs preuves.</p>
<p>Par exemple, nous avons conservé le mécanisme de collections d’enregistrements
et de <em>timestamp</em> de <em>Sync</em>. Comme ces problématiques sont récurrentes, voire
incontournables, nous avons décidé de reprendre le protocole de synchronisation,
de l’étendre légèrement et surtout de le dissocier du projet de listes de lecture.</p>
</div>
<div class="section" id="le-mecanisme-qui-force-a-aller-de-l-avant">
<h2>Le mécanisme qui force à aller de l’avant</h2>
<p>Comme première pierre à l’édifice, nous avons donné naissance au projet
<a class="reference external" href="https://cliquet.readthedocs.org">Cliquet</a>, dont l’idée principale est de
fournir une implémentation de ce protocole en python, tout en factorisant
l’ensemble de nos bonnes pratiques (pour la prod notamment).</p>
<img alt="Logo du projet Cliquet" class="align-right" src="https://blog.notmyidea.org/images/cliquet/cliquet-logo.png" />
<p>L’avantage d’avoir un protocole plutôt qu’un monolithe, c’est que si vous
préférez Asyncio, io.js ou Go, on vous encouragera à publier votre
implémentation alternative !</p>
<p>Avec <em>Cliquet</em>, le code du serveur liste de lecture consiste principalement
à définir un schéma pour les enregistrements, puis à forcer des valeurs de
champs sur certains appels. Cela réduit ce projet à quelques dizaines de lignes
de code.</p>
<p>Quant au futur service de stockage générique, <a class="reference external" href="http://kinto.readthedocs.org">le projet</a> en est encore à ses balbutiements mais c’est
bel et bien en route ! Il permet déjà d’être branché comme backend de stockage
dans une application <em>Cliquet</em>, et ça <a class="reference external" href="https://github.com/mozilla-services/kinto/blob/0.2.1/kinto/views/collection.py">implémenté en 20 lignes de code</a>!</p>
<p>Ah, et cette fois, nous ne construirons les fonctionnalités qu’à partir des
besoins concrets qui surviennent. Ça paraît tout bête, mais sur <em>Daybed</em> on
l’a pas vu venir :)</p>
<p>Dans les prochains articles, nous avons prévu de décrire les bonnes pratiques
rassemblées dans le protocole (ou <em>Cliquet</em>), certains points techniques précis
et de vous présenter notre vision via des exemples et tutoriaux.</p>
<p>À bientôt, donc !</p>
</div>
What’s Hawk and how to use it?2014-07-31T00:00:00+02:002014-07-31T00:00:00+02:00tag:blog.notmyidea.org,2014-07-31:/whats-hawk-and-how-to-use-it.html
<p>At Mozilla, we recently had to implement <a href="https://github.com/hueniverse/hawk">the Hawk authentication
scheme</a> for a number of projects,
and we came up creating two libraries to ease integration into pyramid
and node.js apps.</p>
<p>But maybe you don’t know Hawk.</p>
<p>Hawk is a relatively new technology, crafted by one of the …</p>
<p>At Mozilla, we recently had to implement <a href="https://github.com/hueniverse/hawk">the Hawk authentication
scheme</a> for a number of projects,
and we came up creating two libraries to ease integration into pyramid
and node.js apps.</p>
<p>But maybe you don’t know Hawk.</p>
<p>Hawk is a relatively new technology, crafted by one of the original
<a href="https://en.wikipedia.org/wiki/OAuth">OAuth</a> specification authors, that
intends to replace the 2-legged OAuth authentication scheme using a
simpler approach.</p>
<p>It is an authentication scheme for <span class="caps">HTTP</span>, built around <a href="https://en.wikipedia.org/wiki/Hmac"><span class="caps">HMAC</span>
digests</a> of requests and responses.</p>
<p>Every authenticated client request has an Authorization header
containing a <span class="caps">MAC</span> (Message Authentication Code) and some additional
metadata, then each server response to authenticated requests contains a
Server-Authorization header that authenticates the response, so the
client is sure it comes from the right server.</p>
<h2 id="exchange-of-the-hawk-id-and-hawk-key">Exchange of the hawk id and hawk key</h2>
<p>To sign the requests, a client needs to retrieve a token id and a token
key from the server.</p>
<p>Hawk itself does not define how these credentials should be exchanged
between the server and the client. The excellent team behind <a href="http://accounts.firefox.com">Firefox
Accounts</a> put together a scheme to do that,
which acts like the following:</p>
<div class="note">
<div class="admonition-title">
Note
</div>
All this derivation crazyness might seem a bit complicated, but don’t
worry, we put together some libraries that takes care of that for you
automatically.
If you are not interested into these details, you can directly jump to
the next section to see how to use the libraries.
</div>
<p>When your server application needs to send you the credentials, it will
return it inside a specific Hawk-Session-Token header. This token can be
derived to split this string in two values (hawk id and hawk key) that
you will use to sign your next requests.</p>
<p>In order to get the hawk credentials, you’ll need to:</p>
<p>First, do an <a href="http://en.wikipedia.org/wiki/HKDF"><span class="caps">HKDF</span> derivation</a> on the
given session token. You’ll need to use the following parameters:</p>
<div class="highlight"><pre><span></span><code>key_material = HKDF(hawk_session, "", 'identity.mozilla.com/picl/v1/sessionToken', 32*2)
</code></pre></div>
<div class="note">
<div class="admonition-title">
Note
</div>
The `identity.mozilla.com/picl/v1/sessionToken` is a reference to this
way of deriving the credentials, not an actual <span class="caps">URL</span>.
</div>
<p>Then, the key material you’ll get out of the <span class="caps">HKDF</span> need to be separated
into two parts, the first 32 hex caracters are the hawk id, and the next
32 ones are the hawk key.</p>
<p>Credentials:</p>
<div class="highlight"><pre><span></span><code>javascript
credentials = {
'id': keyMaterial[0:32],
'key': keyMaterial[32:64],
'algorithm': 'sha256'
}
</code></pre></div>
<h2 id="httpie">Httpie</h2>
<p>To showcase APIs in the documentation, I like to use
<a href="https://github.com/jakubroztocil/httpie">httpie</a>, a curl-replacement
with a nicer <span class="caps">API</span>, built around <a href="http://python-requests.org">the python requests
library</a>.</p>
<p>Luckily, HTTPie allows you to plug different authentication schemes for
it, so <a href="https://github.com/mozilla-services/requests-hawk">I wrote a
wrapper</a> around
<a href="https://github.com/kumar303/mohawk">mohawk</a> to add hawk support to the
requests lib.</p>
<p>Doing hawk requests in your terminal is now as simple as:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>requests-hawk<span class="w"> </span>httpie
$<span class="w"> </span>http<span class="w"> </span>GET<span class="w"> </span>localhost:5000/registration<span class="w"> </span>--auth-type<span class="o">=</span>hawk<span class="w"> </span>--auth<span class="o">=</span><span class="s1">'id:key'</span>
</code></pre></div>
<p>In addition, it will help you to craft requests using the requests library:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">requests</span>
<span class="kn">from</span> <span class="nn">requests_hawk</span> <span class="kn">import</span> <span class="n">HawkAuth</span>
<span class="n">hawk_auth</span> <span class="o">=</span> <span class="n">HawkAuth</span><span class="p">(</span>
<span class="n">credentials</span><span class="o">=</span><span class="p">{</span><span class="s1">'id'</span><span class="p">:</span> <span class="nb">id</span><span class="p">,</span> <span class="s1">'key'</span><span class="p">:</span> <span class="n">key</span><span class="p">,</span> <span class="s1">'algorithm'</span><span class="p">:</span> <span class="s1">'sha256'</span><span class="p">})</span>
<span class="n">requests</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">"/url"</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="n">hawk_auth</span><span class="p">)</span>
</code></pre></div>
<p>Alternatively, if you don’t have the token id and key, you can pass the
hawk session token I talked about earlier and the lib will take care of
the derivation for you:</p>
<div class="highlight"><pre><span></span><code><span class="n">hawk_auth</span> <span class="o">=</span> <span class="n">HawkAuth</span><span class="p">(</span>
<span class="n">hawk_session</span><span class="o">=</span><span class="n">resp</span><span class="o">.</span><span class="n">headers</span><span class="p">[</span><span class="s1">'hawk-session-token'</span><span class="p">],</span>
<span class="n">server_url</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">server_url</span>
<span class="p">)</span>
<span class="n">requests</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">"/url"</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="n">hawk_auth</span><span class="p">)</span>
</code></pre></div>
<h2 id="integrate-with-python-pyramid-apps">Integrate with python pyramid apps</h2>
<p>If you’re writing pyramid applications, you’ll be happy to learn that
<a href="https://www.rfk.id.au/blog/">Ryan Kelly</a> put together a library that
makes Hawk work as an Authentication provider for them. I’m chocked how
simple it is to use it.</p>
<p>Here is a demo of how we implemented it for Daybed:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">pyramid_hawkauth</span> <span class="kn">import</span> <span class="n">HawkAuthenticationPolicy</span>
<span class="n">policy</span> <span class="o">=</span> <span class="n">HawkAuthenticationPolicy</span><span class="p">(</span><span class="n">decode_hawk_id</span><span class="o">=</span><span class="n">get_hawk_id</span><span class="p">)</span>
<span class="n">config</span><span class="o">.</span><span class="n">set_authentication_policy</span><span class="p">(</span><span class="n">authn_policy</span><span class="p">)</span>
</code></pre></div>
<p>The get_hawk_id function is a function that takes a request and a
tokenid and returns a tuple of (token_id, token_key).</p>
<p>How you want to store the tokens and retrieve them is up to you. The
default implementation (e.g. if you don’t pass a decode_hawk_id
function) decodes the key from the token itself, using a master secret
on the server (so you don’t need to store anything).</p>
<h2 id="integrate-with-nodejs-express-apps">Integrate with node.js Express apps</h2>
<p>We had to implement Hawk authentication for two node.js projects and
finally came up factorizing everything in a library for express, named
<a href="https://github.com/mozilla-services/express-hawkauth">express-hawkauth</a>.</p>
<p>In order to plug it in your application, you’ll need to use it as a middleware:</p>
<div class="highlight"><pre><span></span><code><span class="n">javascript</span>
<span class="k">var</span><span class="w"> </span><span class="n">express</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">require</span><span class="p">(</span><span class="s2">"express"</span><span class="p">);</span>
<span class="k">var</span><span class="w"> </span><span class="n">hawk</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">require</span><span class="p">(</span><span class="s2">"express-hawkauth"</span><span class="p">);</span>
<span class="n">app</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">express</span><span class="p">();</span>
<span class="k">var</span><span class="w"> </span><span class="n">hawkMiddleware</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">hawk</span><span class="o">.</span><span class="n">getMiddleware</span><span class="p">({</span>
<span class="w"> </span><span class="n">hawkOptions</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span>
<span class="w"> </span><span class="n">getSession</span><span class="p">:</span><span class="w"> </span><span class="n">function</span><span class="p">(</span><span class="n">tokenId</span><span class="p">,</span><span class="w"> </span><span class="n">cb</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="o">//</span><span class="w"> </span><span class="n">A</span><span class="w"> </span><span class="n">function</span><span class="w"> </span><span class="n">which</span><span class="w"> </span><span class="k">pass</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">cb</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">key</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="n">algorithm</span><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="n">the</span>
<span class="w"> </span><span class="o">//</span><span class="w"> </span><span class="n">given</span><span class="w"> </span><span class="n">token</span><span class="w"> </span><span class="n">id</span><span class="o">.</span><span class="w"> </span><span class="n">First</span><span class="w"> </span><span class="n">argument</span><span class="w"> </span><span class="n">of</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">callback</span><span class="w"> </span><span class="k">is</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="n">potential</span>
<span class="w"> </span><span class="o">//</span><span class="w"> </span><span class="n">error</span><span class="o">.</span>
<span class="w"> </span><span class="n">cb</span><span class="p">(</span><span class="nb nb-Type">null</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="n">key</span><span class="p">:</span><span class="w"> </span><span class="s2">"key"</span><span class="p">,</span><span class="w"> </span><span class="n">algorithm</span><span class="p">:</span><span class="w"> </span><span class="s2">"sha256"</span><span class="p">});</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="n">createSession</span><span class="p">:</span><span class="w"> </span><span class="n">function</span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="n">cb</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="o">//</span><span class="w"> </span><span class="n">A</span><span class="w"> </span><span class="n">function</span><span class="w"> </span><span class="n">which</span><span class="w"> </span><span class="n">stores</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="n">session</span><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">given</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="n">key</span><span class="o">.</span>
<span class="w"> </span><span class="o">//</span><span class="w"> </span><span class="n">Argument</span><span class="w"> </span><span class="n">returned</span><span class="w"> </span><span class="k">is</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="n">potential</span><span class="w"> </span><span class="n">error</span><span class="o">.</span>
<span class="w"> </span><span class="n">cb</span><span class="p">(</span><span class="nb nb-Type">null</span><span class="p">);</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="n">setUser</span><span class="p">:</span><span class="w"> </span><span class="n">function</span><span class="p">(</span><span class="n">req</span><span class="p">,</span><span class="w"> </span><span class="n">res</span><span class="p">,</span><span class="w"> </span><span class="n">tokenId</span><span class="p">,</span><span class="w"> </span><span class="n">cb</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="o">//</span><span class="w"> </span><span class="n">A</span><span class="w"> </span><span class="n">function</span><span class="w"> </span><span class="n">that</span><span class="w"> </span><span class="n">uses</span><span class="w"> </span><span class="n">req</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="n">res</span><span class="p">,</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">hawkId</span><span class="w"> </span><span class="n">when</span><span class="w"> </span><span class="n">they</span><span class="s1">'re known so</span>
<span class="w"> </span><span class="o">//</span><span class="w"> </span><span class="n">that</span><span class="w"> </span><span class="n">it</span><span class="w"> </span><span class="n">can</span><span class="w"> </span><span class="n">tweak</span><span class="w"> </span><span class="n">it</span><span class="o">.</span><span class="w"> </span><span class="n">For</span><span class="w"> </span><span class="n">instance</span><span class="p">,</span><span class="w"> </span><span class="n">you</span><span class="w"> </span><span class="n">can</span><span class="w"> </span><span class="n">store</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">tokenId</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">the</span>
<span class="w"> </span><span class="o">//</span><span class="w"> </span><span class="n">user</span><span class="o">.</span>
<span class="w"> </span><span class="n">req</span><span class="o">.</span><span class="n">user</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">tokenId</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">});</span>
<span class="n">app</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"/hawk-enabled-endpoint"</span><span class="p">,</span><span class="w"> </span><span class="n">hawkMiddleware</span><span class="p">);</span>
</code></pre></div>
<p>If you pass the createSession parameter, all non-authenticated requests
will create a new hawk session and return it with the response, in the
Hawk-Session-Token header.</p>
<p>If you want to only check a valid hawk session exists (without creating
a new one), just create a middleware which doesn’t have any
createSession parameter defined.</p>
<h2 id="some-reference-implementations">Some reference implementations</h2>
<p>As a reference, here is how we’re using the libraries I’m talking about,
in case that helps you to integrate with your projects.</p>
<ul>
<li>The Mozilla Loop server <a href="https://github.com/mozilla-services/loop-server/blob/master/loop/index.js#L70-L133">uses hawk as authentication once you’re
logged in with a valid BrowserID
assertion</a>;
request, to keep a session between client and server;</li>
<li><a href="https://github.com/spiral-project/daybed/commit/f178b4e43015fa077430798dcd3d0886c7611caf">I recently added hawk support on the Daybed
project</a>
(that’s a pyramid / cornice) app.</li>
<li>It’s also interesting to note that Kumar put together <a href="http://hawkrest.readthedocs.org/en/latest/">hawkrest, for
the django rest framework</a></li>
</ul>Implementing CORS in Cornice2013-02-04T00:00:00+01:002013-02-04T00:00:00+01:00tag:blog.notmyidea.org,2013-02-04:/implementing-cors-in-cornice.html
<div class="note">
<div class="admonition-title">
Note
</div>
I’m cross-posting [on the mozilla services
weblog](https://blog.mozilla.org/services/). Since this is the first
time we’re doing that, I though it could be useful to point you there.
Check it out and expect more technical articles there in the future.
</div>
<p>For security reasons, it …</p>
<div class="note">
<div class="admonition-title">
Note
</div>
I’m cross-posting [on the mozilla services
weblog](https://blog.mozilla.org/services/). Since this is the first
time we’re doing that, I though it could be useful to point you there.
Check it out and expect more technical articles there in the future.
</div>
<p>For security reasons, it’s not possible to do cross-domain requests. In
other words, if you have a page served from the domain lolnet.org, it
will not be possible for it to get data from notmyidea.org.</p>
<p>Well, it’s possible, using tricks and techniques like
<a href="http://en.wikipedia.org/wiki/JSONP"><span class="caps">JSONP</span></a>, but that doesn’t work all
the time (see <a href="#how-this-is-different-from-jsonp">the section below</a>). I
remember myself doing some simple proxies on my domain server to be able
to query other’s <span class="caps">API</span>.</p>
<p>Thankfully, there is a nicer way to do this, namely, “Cross Origin
Resource-Sharing”, or <a href="http://www.w3.org/TR/cors/"><span class="caps">CORS</span></a>.</p>
<h2 id="you-want-an-icecream-go-ask-your-dad-first">You want an icecream? Go ask your dad first.</h2>
<p>If you want to use <span class="caps">CORS</span>, you need the <span class="caps">API</span> you’re querying to support it;
on the server side.</p>
<p>The <span class="caps">HTTP</span> server need to answer to the <span class="caps">OPTIONS</span> verb, and with the
appropriate response headers.</p>
<p><span class="caps">OPTIONS</span> is sent as what the authors of the spec call a “preflight
request”; just before doing a request to the <span class="caps">API</span>, the <em>User-Agent</em> (the
browser most of the time) asks the permission to the resource, with an
<span class="caps">OPTIONS</span> call.</p>
<p>The server answers, and tell what is available and what isn’t:</p>
<p><img alt="The CORS flow (from the HTML5 CORS tutorial)" src="/images/cors_flow.png"></p>
<ul>
<li>
<p>1a. The User-Agent, rather than doing the call directly, asks the
server, the <span class="caps">API</span>, the permission to do the request. It does so with
the following headers:</p>
<ul>
<li><strong>Access-Control-Request-Headers</strong>, contains the headers the
User-Agent want to access.</li>
<li><strong>Access-Control-Request-Method</strong> contains the method the
User-Agent want to access.</li>
</ul>
</li>
<li>
<p>1b. The <span class="caps">API</span> answers what is authorized:</p>
<ul>
<li><strong>Access-Control-Allow-Origin</strong> the origin that’s accepted. Can
be * or the domain name.</li>
<li><strong>Access-Control-Allow-Methods</strong> a <em>list</em> of allowed methods.
This can be cached. Note than the request asks permission for
one method and the server should return a list of accepted methods.</li>
<li><strong>Access-Allow-Headers</strong> a list of allowed headers, for all of
the methods, since this can be cached as well.</li>
</ul>
</li>
<li>
<ol>
<li>The User-Agent can do the “normal” request.</li>
</ol>
</li>
</ul>
<p>So, if you want to access the /icecream resource, and do a <span class="caps">PUT</span> there,
you’ll have the following flow:</p>
<div class="highlight"><pre><span></span><code>> OPTIONS /icecream
> Access-Control-Request-Methods = PUT
> Origin: notmyidea.org
< Access-Control-Allow-Origin = notmyidea.org
< Access-Control-Allow-Methods = PUT,GET,DELETE
200 OK
</code></pre></div>
<p>You can see that we have an Origin Header in the request, as well as a
Access-Control-Request-Methods. We’re here asking if we have the right,
as notmyidea.org, to do a <span class="caps">PUT</span> request on /icecream.</p>
<p>And the server tells us that we can do that, as well as <span class="caps">GET</span> and <span class="caps">DELETE</span>.</p>
<p>I’ll not cover all the details of the <span class="caps">CORS</span> specification here, but bear
in mind than with <span class="caps">CORS</span>, you can control what are the authorized methods,
headers, origins, and if the client is allowed to send authentication
information or not.</p>
<h2 id="a-word-about-security">A word about security</h2>
<p><span class="caps">CORS</span> is not an answer for every cross-domain call you want to do,
because you need to control the service you want to call. For instance,
if you want to build a feed reader and access the feeds on different
domains, you can be pretty much sure that the servers will not implement
<span class="caps">CORS</span>, so you’ll need to write a proxy yourself, to provide this.</p>
<p>Secondly, if misunderstood, <span class="caps">CORS</span> can be insecure, and cause problems.
Because the rules apply when a client wants to do a request to a server,
you need to be extra careful about who you’re authorizing.</p>
<p>An incorrectly secured <span class="caps">CORS</span> server can be accessed by a malicious client
very easily, bypassing network security. For instance, if you host a
server on an intranet that is only available from behind a <span class="caps">VPN</span> but
accepts every cross-origin call. A bad guy can inject javascript into
the browser of a user who has access to your protected server and make
calls to your service, which is probably not what you want.</p>
<h2 id="how-this-is-different-from-jsonp">How this is different from <span class="caps">JSONP</span>?</h2>
<p>You may know the <a href="http://en.wikipedia.org/wiki/JSONP"><span class="caps">JSONP</span></a> protocol.
<span class="caps">JSONP</span> allows cross origin, but for a particular use case, and does have
some drawbacks (for instance, it’s not possible to do DELETEs or PUTs
with <span class="caps">JSONP</span>).</p>
<p><span class="caps">JSONP</span> exploits the fact that it is possible to get information from
another domain when you are asking for javascript code, using the
\<script> element.</p>
<blockquote>
<p>Exploiting the open policy for \<script> elements, some pages use
them to retrieve JavaScript code that operates on dynamically
generated <span class="caps">JSON</span>-formatted data from other origins. This usage pattern
is known as <span class="caps">JSONP</span>. Requests for <span class="caps">JSONP</span> retrieve not <span class="caps">JSON</span>, but arbitrary
JavaScript code. They are evaluated by the JavaScript interpreter, not
parsed by a <span class="caps">JSON</span> parser.</p>
</blockquote>
<h2 id="using-cors-in-cornice">Using <span class="caps">CORS</span> in Cornice</h2>
<p>Okay, things are hopefully clearer about <span class="caps">CORS</span>, let’s see how we
implemented it on the server-side.</p>
<p>Cornice is a toolkit that lets you define resources in python and takes
care of the heavy lifting for you, so I wanted it to take care of the
<span class="caps">CORS</span> support as well.</p>
<p>In Cornice, you define a service like this:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">cornice</span> <span class="kn">import</span> <span class="n">Service</span>
<span class="n">foobar</span> <span class="o">=</span> <span class="n">Service</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">"foobar"</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="s2">"/foobar"</span><span class="p">)</span>
<span class="c1"># and then you do something with it</span>
<span class="nd">@foobar</span><span class="o">.</span><span class="n">get</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get_foobar</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="c1"># do something with the request.</span>
</code></pre></div>
<p>To add <span class="caps">CORS</span> support to this resource, you can go this way, with the
cors_origins parameter:</p>
<div class="highlight"><pre><span></span><code><span class="n">foobar</span> <span class="o">=</span> <span class="n">Service</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">'foobar'</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="s1">'/foobar'</span><span class="p">,</span> <span class="n">cors_origins</span><span class="o">=</span><span class="p">(</span><span class="s1">'*'</span><span class="p">,))</span>
</code></pre></div>
<p>Ta-da! You have enabled <span class="caps">CORS</span> for your service. <strong>Be aware that you’re
authorizing anyone to query your server, that may not be what you want.</strong></p>
<p>Of course, you can specify a list of origins you trust, and you don’t
need to stick with *, which means “authorize everyone”.</p>
<h3 id="headers">Headers</h3>
<p>You can define the headers you want to expose for the service:</p>
<div class="highlight"><pre><span></span><code><span class="n">foobar</span> <span class="o">=</span> <span class="n">Service</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">'foobar'</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="s1">'/foobar'</span><span class="p">,</span> <span class="n">cors_origins</span><span class="o">=</span><span class="p">(</span><span class="s1">'*'</span><span class="p">,))</span>
<span class="nd">@foobar</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">cors_headers</span><span class="o">=</span><span class="p">(</span><span class="s1">'X-My-Header'</span><span class="p">,</span> <span class="s1">'Content-Type'</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_foobars_please</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="k">return</span> <span class="s2">"some foobar for you"</span>
</code></pre></div>
<p>I’ve done some testing and it wasn’t working on Chrome because I wasn’t
handling the headers the right way (The missing one was Content-Type,
that Chrome was asking for). With my first version of the
implementation, I needed the service implementers to explicitely list
all the headers that should be exposed. While this improves security, it
can be frustrating while developing.</p>
<p>So I introduced an expose_all_headers flag, which is set to True by
default, if the service supports <span class="caps">CORS</span>.</p>
<h3 id="cookies-credentials">Cookies / Credentials</h3>
<p>By default, the requests you do to your <span class="caps">API</span> endpoint don’t include the
credential information for security reasons. If you really want to do
that, you need to enable it using the cors_credentials parameter. You
can activate this one on a per-service basis or on a per-method basis.</p>
<h3 id="caching">Caching</h3>
<p>When you do a preflight request, the information returned by the server
can be cached by the User-Agent so that it’s not redone before each
actual call.</p>
<p>The caching period is defined by the server, using the
Access-Control-Max-Age header. You can configure this timing using the
cors_max_age parameter.</p>
<h3 id="simplifying-the-api">Simplifying the <span class="caps">API</span></h3>
<p>We have cors_headers, cors_enabled, cors_origins, cors_credentials,
cors_max_age, cors_expose_all_headers … a fair number of
parameters. If you want to have a specific <span class="caps">CORS</span>-policy for your
services, that can be a bit tedious to pass these to your services all
the time.</p>
<p>I introduced another way to pass the <span class="caps">CORS</span> policy, so you can do
something like that:</p>
<div class="highlight"><pre><span></span><code><span class="n">policy</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="n">enabled</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">headers</span><span class="o">=</span><span class="p">(</span><span class="s1">'X-My-Header'</span><span class="p">,</span> <span class="s1">'Content-Type'</span><span class="p">),</span>
<span class="n">origins</span><span class="o">=</span><span class="p">(</span><span class="s1">'*.notmyidea.org'</span><span class="p">),</span>
<span class="n">credentials</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">max_age</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
<span class="n">foobar</span> <span class="o">=</span> <span class="n">Service</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s1">'foobar'</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="s1">'/foobar'</span><span class="p">,</span> <span class="n">cors_policy</span><span class="o">=</span><span class="n">policy</span><span class="p">)</span>
</code></pre></div>
<h2 id="comparison-with-other-implementations">Comparison with other implementations</h2>
<p>I was curious to have a look at other implementations of <span class="caps">CORS</span>, in django
for instance, and I found <a href="https://gist.github.com/426829.js">a gist about
it</a>.</p>
<p>Basically, this adds a middleware that adds the “rights” headers to the
answer, depending on the request.</p>
<p>While this approach works, it’s not implementing the specification
completely. You need to add support for all the resources at once.</p>
<p>We can think about a nice way to implement this specifying a definition
of what’s supposed to be exposed via <span class="caps">CORS</span> and what shouldn’t directly in
your settings. In my opinion, <span class="caps">CORS</span> support should be handled at the
service definition level, except for the list of authorized hosts.
Otherwise, you don’t know exactly what’s going on when you look at the
definition of the service.</p>
<h2 id="resources">Resources</h2>
<p>There are a number of good resources that can be useful to you if you
want to either understand how <span class="caps">CORS</span> works, or if you want to implement it yourself.</p>
<ul>
<li><a href="http://enable-cors.org/">http://enable-cors.org/</a> is useful to get started when you don’t
know anything about <span class="caps">CORS</span>.</li>
<li>There is a <span class="caps">W3C</span> wiki page containing information that may be useful
about clients, common pitfalls etc:
<a href="http://www.w3.org/wiki/CORS_Enabled">http://www.w3.org/wiki/CORS_Enabled</a></li>
<li><em><span class="caps">HTML5</span> rocks</em> has a tutorial explaining how to implement <span class="caps">CORS</span>, with
<a href="http://www.html5rocks.com/en/tutorials/cors/#toc-adding-cors-support-to-the-server">a nice section about the
server-side</a>.</li>
<li>Be sure to have a look at the <a href="http://caniuse.com/#search=cors">clients support-matrix for this
feature</a>.</li>
<li>About security, <a href="https://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity">check out this page</a></li>
<li>If you want to have a look at the implementation code, check <a href="https://github.com/mozilla-services/cornice/pull/98/files">on github</a></li>
</ul>
<p>Of course, the <span class="caps">W3C</span> specification is the best resource to rely on. This
specification isn’t hard to read, so you may want to go through it.
Especially the <a href="http://www.w3.org/TR/cors/#resource-processing-model">“resource processing model” section</a></p>Status board2012-12-29T00:00:00+01:002012-12-29T00:00:00+01:00tag:blog.notmyidea.org,2012-12-29:/status-board.html
<p>À force de démarrer des services web pour un oui et pour un non, de
proposer à des copains d’héberger leurs sites, de faire pareil pour
quelques assos etc, je me suis retrouvé avec, comme dirait l’autre, <em>une
bonne platrée</em> de sites et de services à gérer sur …</p>
<p>À force de démarrer des services web pour un oui et pour un non, de
proposer à des copains d’héberger leurs sites, de faire pareil pour
quelques assos etc, je me suis retrouvé avec, comme dirait l’autre, <em>une
bonne platrée</em> de sites et de services à gérer sur lolnet.org, mon serveur.</p>
<p>Jusqu’à très récemment, rien de tout ça n’était sauvegardé, et non plus
monitoré. Après quelques recherches, je suis tombé sur
<a href="http://www.stashboard.org/">stashboard</a>, un “status board” qu’il est
bien fait. Le seul problème, c’est écrit pour se lancer sur <span class="caps">GAE</span>, <em>Google
App Engine</em>. Heureusement, c’est open-source, et ça a été forké pour
donner naissance à
<a href="https://github.com/bfirsh/whiskerboard">whiskerboard</a> (la planche
moustachue, pour les non anglophones).</p>
<p><img alt="Capture d'écran du site." src="/images/status_board.png"></p>
<h2 id="verifier-le-statut-des-services">Vérifier le statut des services</h2>
<p>Donc, c’est chouette, c’est facile à installer, tout ça, mais… mais ça ne fait en fait pas ce que je veux: ça ne fait que m’afficher le statut des services, mais ça ne vérifie pas que tout est bien “up”.</p>
<p>Bon, un peu embêtant pour moi, parce que c’est vraiment ça que je voulais. Pas grave, je sais un peu coder, autant que ça serve. J’ai ajouté quelques fonctionnalités au soft, qui sont disponibles sur mon fork, sur github:: <a href="https://github.com/almet/whiskerboard">https://github.com/almet/whiskerboard</a> .</p>
<p>Entres autres, il est désormais possible de lancer
<a href="http://celeryproject.org/">celery</a> en tache de fond et de vérifier périodiquement que les services sont toujours bien vivants, en utilisant une tache spécifique.</p>
<p>C’était un bonheur de développer ça (on a fait ça à deux, avec guillaume, avec un mumble + tmux en pair prog, en une petite soirée, ça dépote).</p>
<p>Les modifications sont assez simples, vous pouvez aller jeter un œil aux changements ici:
<a href="https://github.com/almet/whiskerboard/compare/b539337416...master">https://github.com/almet/whiskerboard/compare/b539337416…master</a></p>
<p>En gros:</p>
<ul>
<li>ajout d’une connection_string aux services (de la forme protocol://host:port)</li>
<li>ajout d’une commande check_status qui s’occupe d’itérer sur les
services et de lancer des taches celery qui vont bien, en fonction
du protocole</li>
<li>ajout des taches en question</li>
</ul>
<h2 id="deploiement">Déploiement</h2>
<p>Le plus long a été de le déployer en fin de compte, parce que je ne
voulais pas déployer mon service de supervision sur mon serveur, forcément.</p>
<p>Après un essai (plutôt rapide en fait) sur <a href="http://heroku.com">heroku</a>,
je me suis rendu compte qu’il me fallait payer pas loin de 35$ par mois
pour avoir un process celeryd qui tourne, donc j’ai un peu cherché
ailleurs, pour finalement déployer la chose chez
<a href="https://www.alwaysdata.com/">alwaysdata</a></p>
<p>Après quelques péripéties, j’ai réussi à faire tourner le tout, ça à été
un peu la bataille au départ pour installer virtualenv (j’ai du faire
des changements dans mon <span class="caps">PATH</span> pour que ça puisse marcher), voici mon `.bash_profile`:</p>
<div class="highlight"><pre><span></span><code><span class="k">export</span><span class="w"> </span><span class="n">PYTHONPATH</span><span class="o">=~/</span><span class="n">modules</span><span class="o">/</span>
<span class="k">export</span><span class="w"> </span><span class="n">PATH</span><span class="o">=$</span><span class="n">HOME</span><span class="o">/</span><span class="n">modules</span><span class="o">/</span><span class="n">bin</span><span class="p">:</span><span class="o">$</span><span class="n">HOME</span><span class="o">/</span><span class="n">modules</span><span class="o">/</span><span class="p">:</span><span class="o">$</span><span class="n">PATH</span>
</code></pre></div>
<p>Et après y’a plus qu’à installer avec `easy_install`:</p>
<div class="highlight"><pre><span></span><code>easy_install --install-dir ~/modules -U pip
easy_install --install-dir ~/modules -U virtualenv
</code></pre></div>
<p>Et à créer le virtualenv:</p>
<div class="highlight"><pre><span></span><code>virtualenv venv
venv/bin/pip install -r requirements.txt
</code></pre></div>
<p>Dernière étape, la création d’un fichier application.wsgi qui s’occupe
de rendre l’application disponible, avec le bon venv:</p>
<h2 id="ssl-et-requests"><span class="caps">SSL</span> et Requests</h2>
<p>Quelques tours de manivelle plus loin, j’ai un celeryd qui tourne et qui
consomme les taches qui lui sont envoyées (pour des questions de
simplicité, j’ai utilisé le backend django de celery, donc pas besoin
d’<span class="caps">AMQP</span>, par exemple).</p>
<p>Problème, les ressources que je vérifie en <span class="caps">SSL</span> (<span class="caps">HTTPS</span>) me jettent. Je
sais pas exactement pourquoi à l’heure qu’il est, mais il semble que
lorsque je fais une requête avec
<a href="http://docs.python-requests.org/en/latest/">Requests</a> je me récupère
des <em>Connection Refused</em>. Peut être une sombre histoire de proxy ? En
attendant, les appels avec <span class="caps">CURL</span> fonctionnent, donc j’ai fait <a href="https://github.com/ametaireau/whiskerboard/blob/master/board/tasks.py#L17">un
fallback vers <span class="caps">CURL</span> lorsque les autres méthodes
échouent</a>.
Pas super propre, mais ça fonctionne.</p>
<p><strong><span class="caps">EDIT</span></strong> Finalement, il se trouve que mon serveur était mal configuré.
J’utilisais haproxy + stunnel, et la négiciation <span class="caps">SSL</span> se passait mal. Une
fois <span class="caps">SSL</span> et <span class="caps">TLS</span> activés, et SSLv2 désactivé, tout fonctionne mieux.</p>
<h2 id="et-voila">Et voilà</h2>
<p>Finalement, j’ai mon joli status-board qui tourne à merveille sur
<a href="http://status.lolnet.org">http://status.lolnet.org</a> :-)</p>Astuces SSH2012-12-27T00:00:00+01:002012-12-27T00:00:00+01:00tag:blog.notmyidea.org,2012-12-27:/astuces-ssh.html
<h2 id="tunelling">Tunelling</h2>
<p>Parce que je m’en rapelle jamais (tête de linote):</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>ssh<span class="w"> </span>-f<span class="w"> </span>hote<span class="w"> </span>-L<span class="w"> </span>local:lolnet.org:destination<span class="w"> </span>-N
</code></pre></div>
<h2 id="sshconfig">.ssh/config</h2>
<p>(merci <a href="http://majerti.fr">gaston</a> !)</p>
<p>La directive suivante dans .ssh/config permet de sauter d’hôte en hôte
séparés par des “+” :</p>
<div class="highlight"><pre><span></span><code>Host <span class="gs">*+*</span>
ProxyCommand ssh $(echo %h | sed
's/+[^+]*$//;s/\([^+%%]*\)%%\([^+]*\)$/\2 -l …</code></pre></div>
<h2 id="tunelling">Tunelling</h2>
<p>Parce que je m’en rapelle jamais (tête de linote):</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>ssh<span class="w"> </span>-f<span class="w"> </span>hote<span class="w"> </span>-L<span class="w"> </span>local:lolnet.org:destination<span class="w"> </span>-N
</code></pre></div>
<h2 id="sshconfig">.ssh/config</h2>
<p>(merci <a href="http://majerti.fr">gaston</a> !)</p>
<p>La directive suivante dans .ssh/config permet de sauter d’hôte en hôte
séparés par des “+” :</p>
<div class="highlight"><pre><span></span><code>Host <span class="gs">*+*</span>
ProxyCommand ssh $(echo %h | sed
's/+[^+]*$//;s/\([^+%%]*\)%%\([^+]*\)$/\2 -l \1/;s/:/ -p /')
PATH=.:\$PATH nc -w1 $(echo %h | sed 's/^.*+//;/:/!s/$/ %p/;s/:/ /')
</code></pre></div>
<p>On peut donc spécifier des “sauts” ssh du style:</p>
<div class="highlight"><pre><span></span><code><span class="n">ssh</span><span class="w"> </span><span class="n">root</span><span class="mf">@91.25.25.25</span><span class="o">+</span><span class="mf">192.168.1.1</span>
</code></pre></div>
<p>Ensuite on peut essayer de rajouter:</p>
<div class="highlight"><pre><span></span><code>Host <label_pour_mon_serveur_privé>
user <monuser(root)>
IdentityFile <chemin vers ma clé ssh pour le serveur publique>
hostname ip_serveur_publique+ip_serveur_privé
</code></pre></div>Gnome 3, extensions2012-12-27T00:00:00+01:002012-12-27T00:00:00+01:00tag:blog.notmyidea.org,2012-12-27:/gnome-3-extensions.html
<p>Après avoir tenté pendant un bout de temps unity, le bureau par defaut
de ubuntu, j’ai eu envie de changements, et j’ai donc essayé un peu de
regarder du coté de gnome 3, à nouveau.</p>
<p>Et finalement, j’ai trouvé quelques extensions qui sont vraiment utiles,
que je …</p>
<p>Après avoir tenté pendant un bout de temps unity, le bureau par defaut
de ubuntu, j’ai eu envie de changements, et j’ai donc essayé un peu de
regarder du coté de gnome 3, à nouveau.</p>
<p>Et finalement, j’ai trouvé quelques extensions qui sont vraiment utiles,
que je liste ici.</p>
<ul>
<li><a href="https://extensions.gnome.org/extension/547/antisocial-menu/">Antisocial
Menu</a>
vire les boutons et textes en rapport avec le web social. J’en avais
pas besoin puisque je suis connecté à mon instant messenger dans un
terminal, en utilisant weechat.</li>
<li><a href="https://extensions.gnome.org/extension/97/coverflow-alt-tab/">Coverflow
Alt-Tab</a>
change le switcher d’applications par defaut. Je le trouve bien plus
pratique que celui par defaut puisqu’il me permet de voir “en grand”
quelle est la fenêtre que je vais afficher.</li>
<li><a href="https://extensions.gnome.org/extension/55/media-player-indicator/">Media player
indicator</a>
me permet de voir en temps réel ce qui se passe dans mon lecteur
audio. Ça semble ne pas être grand chose, mais ça me manquait. Ça
s’intègre niquel avec Spotify, et ça c’est chouette.</li>
<li><a href="https://extensions.gnome.org/extension/149/search-firefox-bookmarks-provider/">Rechercher dans les bookmarks
firefox</a>
permet de… à votre avis ?</li>
</ul>
<p>Un peu moins utile mais sait on jamais:</p>
<ul>
<li>“<a href="https://extensions.gnome.org/extension/130/advanced-settings-in-usermenu/">Advanced Settings in
UserMenu</a>”
permet d’avoir un raccourci vers les paramètres avancés dans le menu
utilisateur (en haut à droite)</li>
<li>Une <a href="https://extensions.gnome.org/extension/409/gtg-integration/">intégration à Getting things
Gnome</a>
(un truc de <span class="caps">GTD</span>). Je suis en train d’expérimenter avec cet outil,
donc je ne sais pas encore si ça va rester, mais pourquoi pas.</li>
</ul>
<p>Vous pouvez aller faire un tour sur <a href="https://extensions.gnome.org/">https://extensions.gnome.org/</a> pour
en trouver d’autres à votre gout.</p>Cheese & code - Wrap-up2012-10-22T00:00:00+02:002012-10-22T00:00:00+02:00tag:blog.notmyidea.org,2012-10-22:/cheese-code-wrap-up.html
<p>This week-end I hosted a <em>cheese <span class="amp">&</span> code</em> session in the country-side of
Angers, France.</p>
<p>We were a bunch of python hackers and it rained a lot, wich forced us to
stay inside and to code. Bad.</p>
<p>We were not enough to get rid of all the cheese and the awesome …</p>
<p>This week-end I hosted a <em>cheese <span class="amp">&</span> code</em> session in the country-side of
Angers, France.</p>
<p>We were a bunch of python hackers and it rained a lot, wich forced us to
stay inside and to code. Bad.</p>
<p>We were not enough to get rid of all the cheese and the awesome meals,
but well, we finally managed it pretty well.</p>
<p>Here is a summary of what we worked on:</p>
<h2 id="daybed">Daybed</h2>
<p>Daybed started some time ago, and intend to be a replacement to google
forms, in term of features, but backed as a <span class="caps">REST</span> web service, in python,
and open source.</p>
<p>In case you wonder, daybed is effectively the name of a couch. We chose
this name because of the similarities (in the sound) with <strong>db</strong>, and
because we’re using <strong>CouchDB</strong> as a backend.</p>
<p><img alt="Daybed is a big couch!" src="/images/daybed.jpg"></p>
<p>We mainly hacked on daybed and are pretty close to the release of the
first version, meaning that we have something working.</p>
<p><a href="http://github.com/spiral-project/daybed">The code</a> is available on
github, and we also wrote <a href="http://daybed.rtfd.org">a small
documentation</a> for it.</p>
<p>Mainly, we did a lot of cleanup, rewrote a bunch of tests so that it
would be easier to continue to work on the project, and implemented some
minor features. I’m pretty confidend that we now have really good basis
for this project.</p>
<p>Also, we will have a nice todolist application, with the backend <strong>and</strong>
the frontend, in javascript / html / css, you’ll know more when it’ll be
ready :-)</p>
<p>Once we have something good enough, we’ll release the first version and
I’ll host it somewhere so that people can play with it.</p>
<h2 id="cornice">Cornice</h2>
<p>Daybed is built on top of <a href="http://cornice.rtfd.org">Cornice</a>, a
framework to ease the creation of web-services.</p>
<p>At Pycon France, we had the opportunity to attend a good presentation
about <a href="https://github.com/SPORE/specifications"><span class="caps">SPORE</span></a>. <span class="caps">SPORE</span> is a way
to describe your <span class="caps">REST</span> web services, as <span class="caps">WSDL</span> is for <span class="caps">WS</span>-* services. This
allows to ease the creation of generic <span class="caps">SPORE</span> clients, which are able to
consume any <span class="caps">REST</span> <span class="caps">API</span> with a <span class="caps">SPORE</span> endpoint.</p>
<p>Here is how you can let cornice describe your web service for you</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">cornice.ext.spore</span> <span class="kn">import</span> <span class="n">generate_spore_description</span>
<span class="kn">from</span> <span class="nn">cornice.service</span> <span class="kn">import</span> <span class="n">Service</span><span class="p">,</span> <span class="n">get_services</span>
<span class="n">spore</span> <span class="o">=</span> <span class="n">Service</span><span class="p">(</span><span class="s1">'spore'</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="s1">'/spore'</span><span class="p">,</span> <span class="n">renderer</span><span class="o">=</span><span class="s1">'jsonp'</span><span class="p">)</span>
<span class="nd">@spore</span><span class="o">.</span><span class="n">get</span>
<span class="k">def</span> <span class="nf">get_spore</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="n">services</span> <span class="o">=</span> <span class="n">get_services</span><span class="p">()</span>
<span class="k">return</span> <span class="n">generate_spore_description</span><span class="p">(</span><span class="n">services</span><span class="p">,</span> <span class="s1">'Service name'</span><span class="p">,</span>
<span class="n">request</span><span class="o">.</span><span class="n">application_url</span><span class="p">,</span> <span class="s1">'1.0'</span><span class="p">)</span>
</code></pre></div>
<p>And you’ll get a definition of your service, in <span class="caps">SPORE</span>, available at /spore.</p>
<p>Of course, you can use it to do other things, like generating the file
locally and exporting it wherever it makes sense to you, etc.</p>
<p>I released today <a href="http://crate.io/packages/cornice/">Cornice 0.11</a>,
which adds into other things the support for <span class="caps">SPORE</span>, plus some other
fixes we found on our way.</p>
<h2 id="respire">Respire</h2>
<p>Once you have the description of the service, you can do generic clients
consuming them!</p>
<p>We first wanted to contribute to <a href="https://github.com/bl0b/spyre">spyre</a>
but it was written in a way that wasn’t supporting to <span class="caps">POST</span> data, and
they were using their own stack to handle <span class="caps">HTTP</span>. A lot of code that
already exists in other libraries.</p>
<p>While waiting the train with <a href="http://natim.ionyse.com/">Rémy</a>, we hacked
something together, named “Respire”, a thin layer on top of the awesome
<a href="http://python-requests.org">Requests</a> library.</p>
<p>We have a first version, feel free to have a look at it and provide
enhancements if you feel like it. We’re still hacking on it so it may
break (for the better), but that had been working pretty well for us so far.</p>
<p>You can <a href="http://github.com/spiral-project/respire">find the project on
github</a>, but here is how to
use it, really quickly (these examples are how to interact with daybed)</p>
<div class="highlight"><pre><span></span><code><span class="o">>>></span> <span class="kn">from</span> <span class="nn">respire</span> <span class="kn">import</span> <span class="n">client_from_url</span>
<span class="o">>>></span> <span class="c1"># create the client from the SPORE definition</span>
<span class="o">>>></span> <span class="n">cl</span> <span class="o">=</span> <span class="n">client_from_url</span><span class="p">(</span><span class="s1">'http://localhost:8000/spore'</span><span class="p">)</span>
<span class="o">>>></span> <span class="c1"># in daybed, create a new definition</span>
<span class="o">>>></span> <span class="n">todo_def</span> <span class="o">=</span> <span class="p">{</span>
<span class="o">...</span> <span class="s2">"title"</span><span class="p">:</span> <span class="s2">"todo"</span><span class="p">,</span>
<span class="o">...</span> <span class="s2">"description"</span><span class="p">:</span> <span class="s2">"A list of my stuff to do"</span><span class="p">,</span>
<span class="o">...</span> <span class="s2">"fields"</span><span class="p">:</span> <span class="p">[</span>
<span class="o">...</span> <span class="p">{</span>
<span class="o">...</span> <span class="s2">"name"</span><span class="p">:</span> <span class="s2">"item"</span><span class="p">,</span>
<span class="o">...</span> <span class="s2">"type"</span><span class="p">:</span> <span class="s2">"string"</span><span class="p">,</span>
<span class="o">...</span> <span class="s2">"description"</span><span class="p">:</span> <span class="s2">"The item"</span>
<span class="o">...</span> <span class="p">},</span>
<span class="o">...</span> <span class="p">{</span>
<span class="o">...</span> <span class="s2">"name"</span><span class="p">:</span> <span class="s2">"status"</span><span class="p">,</span>
<span class="o">...</span> <span class="s2">"type"</span><span class="p">:</span> <span class="s2">"enum"</span><span class="p">,</span>
<span class="o">...</span> <span class="s2">"choices"</span><span class="p">:</span> <span class="p">[</span>
<span class="o">...</span> <span class="s2">"done"</span><span class="p">,</span>
<span class="o">...</span> <span class="s2">"todo"</span>
<span class="o">...</span> <span class="p">],</span>
<span class="o">...</span> <span class="s2">"description"</span><span class="p">:</span> <span class="s2">"is it done or not"</span>
<span class="o">...</span> <span class="p">}</span>
<span class="o">...</span> <span class="p">]}</span>
<span class="o">>>></span> <span class="n">cl</span><span class="o">.</span><span class="n">put_definition</span><span class="p">(</span><span class="n">model_name</span><span class="o">=</span><span class="s1">'todo'</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">todo_def</span><span class="p">)</span>
<span class="o">>>></span> <span class="n">cl</span><span class="o">.</span><span class="n">post_data</span><span class="p">(</span><span class="n">model_name</span><span class="o">=</span><span class="s1">'todo'</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="n">item</span><span class="o">=</span><span class="s1">'make it work'</span><span class="p">,</span> <span class="n">status</span><span class="o">=</span><span class="s1">'todo'</span><span class="p">))</span>
<span class="p">{</span><span class="sa">u</span><span class="s1">'id'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'9f2c90c0529a442cfdc03c191b022cf7'</span><span class="p">}</span>
<span class="o">>>></span> <span class="n">cl</span><span class="o">.</span><span class="n">get_data</span><span class="p">(</span><span class="n">model_name</span><span class="o">=</span><span class="s1">'todo'</span><span class="p">)</span>
</code></pre></div>
<p>Finally, we were out of cheese so everyone headed back to their
respective houses and cities.</p>
<p>Until next time?</p>Circus sprint at PyconFR2012-09-17T00:00:00+02:002012-09-17T00:00:00+02:00tag:blog.notmyidea.org,2012-09-17:/circus-sprint-at-pyconfr.html
<p>Last Thursday to Sunday, <a href="http://pycon.fr">Pycon France</a> took place, in
Paris. It was the opportunity to meet a lot of people and to talk about
python awesomness in general.</p>
<p>We had three tracks this year, plus sprints the two first days. We
sprinted on <a href="http://circus.io">Circus</a>, the process and socket manager
we …</p>
<p>Last Thursday to Sunday, <a href="http://pycon.fr">Pycon France</a> took place, in
Paris. It was the opportunity to meet a lot of people and to talk about
python awesomness in general.</p>
<p>We had three tracks this year, plus sprints the two first days. We
sprinted on <a href="http://circus.io">Circus</a>, the process and socket manager
we’re using at Mozilla for some of our setups.</p>
<p>The project gathered some interest, and we ended up with 5 persons
working on it. Of course, we spent some time explaining what is Circus,
how it had been built, a lot of time talking about use-cases and
possible improvements, but we also managed to add new features.</p>
<p>Having people wanting to sprint on our projects is exciting because
that’s when making things in the open unleashes its full potential. You
can’t imagine how happy I was to have some friends come and work on this
with us :)</p>
<p>Here is a wrap-up of the sprint:</p>
<h2 id="autocompletion-on-the-command-line">Autocompletion on the command-line</h2>
<p><a href="http://natim.ionyse.com">Remy Hubscher</a> worked on the command-line
autocompletion. Now we have a fancy command-line interface which is able
to aucomplete if you’re using bash. It seems that not that much work is
needed to make it happen on zsh as well :)</p>
<p><a href="https://github.com/mozilla-services/circus/blob/master/extras/circusctl_bash_completion">Have a look at the feature</a></p>
<p>On the same topic, we now have a cool shell for Circus. If you start the
circusctl command without any option, you’ll end-up with a cool shell.
Thanks <a href="https://github.com/jojax">Jonathan Dorival</a> for the work on
this! You can have a look at <a href="https://github.com/mozilla-services/circus/pull/268">the pull
request</a>.</p>
<h2 id="future-changes-to-the-web-ui">Future changes to the web ui</h2>
<p><a href="https://twitter.com/rachbelaid">Rachid Belaid</a> had a deep look at the
source code and is much more familiarized to it now than before. We
discussed the possibility to change the implementation of the web ui,
and I’m glad of this. Currently, it’s done with bottle.py and we want to
switch to pyramid.</p>
<p>He fixed some issues that were in the tracker, so we now can have the
age of watchers in the webui, for instance.</p>
<h2 id="bug-and-doc-fixing">Bug and doc fixing</h2>
<p>While reading the source code, we found some inconsistencies and fixed
them, with <a href="http://mathieu.agopian.info/">Mathieu Agopian</a>. We also
tried to improve the documentation at different levels.</p>
<p>Documentation still needs a lot of love, and I’m planning to spend some
time on this shortly. I’ve gathered a bunch of feedback on this</p>
<h2 id="circus-clustering-capabilities">Circus clustering capabilities</h2>
<p>One feature I wanted to work on during this sprint was the clustering
abilities of Circus. Nick Pellegrino made an internship on this topic at
Mozilla so we spent some time to review his pull requests.</p>
<p>A lot of code was written for this so we discussed a bunch of things
regarding all of this. It took us more time than expected (and I still
need to spend more time on this to provide appropriate feedback), but it
allowed us to have a starting-point about what this clustering thing
could be.</p>
<p>Remy wrote <a href="http://tech.novapost.fr/circus-clustering-management-en.html">a good summary about our
brainstorming</a>
so I’ll not do it again here, but feel free to contact us if you have
ideas on this, they’re very welcome!</p>
<h2 id="project-management">Project management</h2>
<p>We’ve had some inquiries telling us that’s not as easy as it should to
get started with the Circus project. Some of the reasons are that we
don’t have any release schedule, and that the documentation is hairy
enough to lost people, at some point :)</p>
<p>That’s something we’ll try to fix soon :)</p>
<p>PyconFR was a very enjoyable event. I’m looking forward to meet the
community again and discuss how Circus can evolve in ways that are
interesting to everyone.</p>
<p>Tarek and me are going to <a href="http://python.ie/pycon/2012/">Pycon ireland</a>,
feel free to reach us if you’re going there, we’ll be happy to meet and
enjoy beers!</p>Refactoring Cornice2012-05-01T00:00:00+02:002012-05-01T00:00:00+02:00tag:blog.notmyidea.org,2012-05-01:/refactoring-cornice.html
<p>After working for a while with <a href="http://cornice.readthedocs.com">Cornice</a>
to define our APIs at <a href="http://docs.services.mozilla.com">Services</a>, it
turned out that the current implementation wasn’t flexible enough to
allow us to do what we wanted to do.</p>
<p>Cornice started as a toolkit on top of the
<a href="http://docs.pylonsproject.org/en/latest/docs/pyramid.html">pyramid</a>
routing system, allowing to register services …</p>
<p>After working for a while with <a href="http://cornice.readthedocs.com">Cornice</a>
to define our APIs at <a href="http://docs.services.mozilla.com">Services</a>, it
turned out that the current implementation wasn’t flexible enough to
allow us to do what we wanted to do.</p>
<p>Cornice started as a toolkit on top of the
<a href="http://docs.pylonsproject.org/en/latest/docs/pyramid.html">pyramid</a>
routing system, allowing to register services in a simpler way. Then we
added some niceties such as the ability to automatically generate the
services documentation or returning the correct <span class="caps">HTTP</span> headers <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html">as defined
by the <span class="caps">HTTP</span>
specification</a>
without the need from the developer to deal with them nor to know them.</p>
<p>If you’re not familiar with Cornice, here is how you define a simple
service with it:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">cornice.service</span> <span class="kn">import</span> <span class="n">Service</span>
<span class="n">bar</span> <span class="o">=</span> <span class="n">Service</span><span class="p">(</span><span class="n">path</span><span class="o">=</span><span class="s2">"/bar"</span><span class="p">)</span>
<span class="nd">@bar</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">validators</span><span class="o">=</span><span class="n">validators</span><span class="p">,</span> <span class="n">accept</span><span class="o">=</span><span class="s1">'application/json'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_drink</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="c1"># do something with the request (with moderation).</span>
</code></pre></div>
<p>This external <span class="caps">API</span> is quite cool, as it allows to do a bunch of things
quite easily. For instance, we’ve written our
<a href="https://github.com/mozilla-services/tokenserver">token-server</a> code on
top of this in a blast.</p>
<h2 id="the-burden">The burden</h2>
<p>The problem with this was that we were mixing internally the service
description logic with the route registration one. The way we were doing
this was via an extensive use of decorators internally.</p>
<p>The <span class="caps">API</span> of the cornice.service.Service class was as following
(simplified so you can get the gist of it).</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">Service</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">service_kwargs</span><span class="p">):</span>
<span class="c1"># some information, such as the colander schemas (for validation),</span>
<span class="c1"># the defined methods that had been registered for this service and</span>
<span class="c1"># some other things were registered as instance variables.</span>
<span class="bp">self</span><span class="o">.</span><span class="n">schemas</span> <span class="o">=</span> <span class="n">service_kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">schema</span><span class="s1">', None)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">defined_methods</span> <span class="o">=</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">definitions</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">def</span> <span class="nf">api</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">view_kwargs</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""This method is a decorator that is being used by some alias</span>
<span class="sd"> methods.</span>
<span class="sd"> """</span>
<span class="k">def</span> <span class="nf">wrapper</span><span class="p">(</span><span class="n">view</span><span class="p">):</span>
<span class="c1"># all the logic goes here. And when I mean all the logic, I</span>
<span class="c1"># mean it.</span>
<span class="c1"># 1. we are registering a callback to the pyramid routing</span>
<span class="c1"># system so it gets called whenever the module using the</span>
<span class="c1"># decorator is used.</span>
<span class="c1"># 2. we are transforming the passed arguments so they conform</span>
<span class="c1"># to what is expected by the pyramid routing system.</span>
<span class="c1"># 3. We are storing some of the passed arguments into the</span>
<span class="c1"># object so we can retrieve them later on.</span>
<span class="c1"># 4. Also, we are transforming the passed view before</span>
<span class="c1"># registering it in the pyramid routing system so that it</span>
<span class="c1"># can do what Cornice wants it to do (checking some rules,</span>
<span class="c1"># applying validators and filters etc.</span>
<span class="k">return</span> <span class="n">wrapper</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""A shortcut of the api decorator"""</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">api</span><span class="p">(</span><span class="n">request_method</span><span class="o">=</span><span class="s2">"GET"</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</code></pre></div>
<p>I encourage you to go read <a href="https://github.com/mozilla-services/cornice/blob/4e0392a2ae137b6a11690459bcafd7325e86fa9e/cornice/service.py#L44">the entire
file</a>.
on github so you can get a better opinion on how all of this was done.</p>
<p>A bunch of things are wrong:</p>
<ul>
<li>first, we are not separating the description logic from the
registration one. This causes problems when we need to access the
parameters passed to the service, because the parameters you get are
not exactly the ones you passed but the ones that the pyramid
routing system is expecting. For instance, if you want to get the
view get_drink, you will instead get a decorator which contains
this view.</li>
<li>second, we are using decorators as APIs we expose. Even if
decorators are good as shortcuts, they shouldn’t be the default way
to deal with an <span class="caps">API</span>. A good example of this is <a href="https://github.com/mozilla-services/cornice/blob/4e0392a2ae137b6a11690459bcafd7325e86fa9e/cornice/resource.py#L56">how the resource
module consumes this
<span class="caps">API</span></a>.
This is quite hard to follow.</li>
<li>Third, in the api method, a bunch of things are done regarding
inheritance of parameters that are passed to the service or to its
decorator methods. This leaves you with a really hard to follow path
when it comes to add new parameters to your <span class="caps">API</span>.</li>
</ul>
<h2 id="how-do-we-improve-this">How do we improve this?</h2>
<p>Python is great because it allows you to refactor things in an easy way.
What I did isn’t breaking our APIs, but make things way simpler to
hack-on. One example is that it allowed me to add features that we
wanted to bring to Cornice really quickly (a matter of minutes), without
touching the <span class="caps">API</span> that much.</p>
<p>Here is the gist of the new architecture:</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">Service</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="c1"># we define class-level variables that will be the default values for</span>
<span class="c1"># this service. This makes things more extensible than it was before.</span>
<span class="n">renderer</span> <span class="o">=</span> <span class="s1">'simplejson'</span>
<span class="n">default_validators</span> <span class="o">=</span> <span class="n">DEFAULT_VALIDATORS</span>
<span class="n">default_filters</span> <span class="o">=</span> <span class="n">DEFAULT_FILTERS</span>
<span class="c1"># we also have some class-level parameters that are useful to know</span>
<span class="c1"># which parameters are supposed to be lists (and so converted as such)</span>
<span class="c1"># or which are mandatory.</span>
<span class="n">mandatory_arguments</span> <span class="o">=</span> <span class="p">(</span><span class="s1">'renderer'</span><span class="p">,)</span>
<span class="n">list_arguments</span> <span class="o">=</span> <span class="p">(</span><span class="s1">'validators'</span><span class="p">,</span> <span class="s1">'filters'</span><span class="p">)</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="o">**</span><span class="n">kw</span><span class="p">):</span>
<span class="c1"># setup name, path and description as instance variables</span>
<span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">name</span>
<span class="bp">self</span><span class="o">.</span><span class="n">path</span> <span class="o">=</span> <span class="n">path</span>
<span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="o">=</span> <span class="n">description</span>
<span class="c1"># convert the arguments passed to something we want to store</span>
<span class="c1"># and then store them as attributes of the instance (because they</span>
<span class="c1"># were passed to the constructor</span>
<span class="bp">self</span><span class="o">.</span><span class="n">arguments</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_arguments</span><span class="p">(</span><span class="n">kw</span><span class="p">)</span>
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">arguments</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="nb">setattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
<span class="c1"># we keep having the defined_methods tuple and the list of</span>
<span class="c1"># definitions that are done for this service</span>
<span class="bp">self</span><span class="o">.</span><span class="n">defined_methods</span> <span class="o">=</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">definitions</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">def</span> <span class="nf">get_arguments</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">conf</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""Returns a dict of arguments. It does all the conversions for</span>
<span class="sd"> you, and uses the information that were defined at the instance</span>
<span class="sd"> level as fallbacks.</span>
<span class="sd"> """</span>
<span class="k">def</span> <span class="nf">add_view</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">method</span><span class="p">,</span> <span class="n">view</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""Add a view to this service."""</span>
<span class="c1"># this is really simple and looks a lot like this</span>
<span class="n">method</span> <span class="o">=</span> <span class="n">method</span><span class="o">.</span><span class="n">upper</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">definitions</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">method</span><span class="p">,</span> <span class="n">view</span><span class="p">,</span> <span class="n">args</span><span class="p">))</span>
<span class="k">if</span> <span class="n">method</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">defined_methods</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">defined_methods</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">method</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">decorator</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">method</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""This is only another interface to the add_view method, exposing a</span>
<span class="sd"> decorator interface"""</span>
<span class="k">def</span> <span class="nf">wrapper</span><span class="p">(</span><span class="n">view</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">add_view</span><span class="p">(</span><span class="n">method</span><span class="p">,</span> <span class="n">view</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">return</span> <span class="n">view</span>
<span class="k">return</span> <span class="n">wrapper</span>
</code></pre></div>
<p>So, the service is now only storing the information that’s passed to it
and nothing more. No more route registration logic goes here. Instead, I
added this as another feature, even in a different module. The function
is named register_service_views and has the following signature:</p>
<div class="highlight"><pre><span></span><code><span class="n">register_service_views</span><span class="p">(</span><span class="n">config</span><span class="p">,</span> <span class="n">service</span><span class="p">)</span>
</code></pre></div>
<p>To sum up, here are the changes I made:</p>
<ol>
<li>Service description is now separated from the route registration.</li>
<li>cornice.service.Service now provides a hook_view method, which is
not a decorator. decorators are still present but they are optional
(you don’t need to use them if you don’t want to).</li>
<li>Everything has been decoupled as much as possible, meaning that you
really can use the Service class as a container of information about
the services you are describing. This is especially useful when
generating documentation.</li>
</ol>
<p>As a result, it is now possible to use Cornice with other frameworks. It
means that you can stick with the service description but plug any other
framework on top of it. cornice.services.Service is now only a
description tool. To register routes, one would need to read the
information contained into this service and inject the right parameters
into their preferred routing system.</p>
<p>However, no integration with other frameworks is done at the moment even
if the design allows it.</p>
<p>The same way, the sphinx description layer is now only a consumer of
this service description tool: it looks at what’s described and build-up
the documentation from it.</p>
<p>The resulting branch is not merged yet. Still, you can <a href="https://github.com/mozilla-services/cornice/tree/refactor-the-world">have a look at
it</a>.</p>
<p>Any suggestions are of course welcome :-)</p>Djangocong 20122012-04-16T00:00:00+02:002012-04-16T00:00:00+02:00tag:blog.notmyidea.org,2012-04-16:/djangocong-2012.html
<p>Ce week-end, c’était <a href="http://rencontres.django-fr.org">djangocong</a>, une
conférence autour de <a href="http://djangoproject.org">django</a>, de
<a href="http://python.org">python</a> et du web, qui avait lieu dans le sud, à
Carnon-plage, à quelques kilomètres de Montpellier la belle.</p>
<p>J’ai vraiment apprécié les trois jours passés avec cette bande de geeks.
Je m’attendais à des <em>nerds</em>, j …</p>
<p>Ce week-end, c’était <a href="http://rencontres.django-fr.org">djangocong</a>, une
conférence autour de <a href="http://djangoproject.org">django</a>, de
<a href="http://python.org">python</a> et du web, qui avait lieu dans le sud, à
Carnon-plage, à quelques kilomètres de Montpellier la belle.</p>
<p>J’ai vraiment apprécié les trois jours passés avec cette bande de geeks.
Je m’attendais à des <em>nerds</em>, j’y ai trouvé une qualité d’écoute, des
personnes qui partagent des valeurs qui leur sont chères, mais qui ne
limitent pas leurs discussions à du technique. Eeeh ouais, encore un
préjugé qui tombe, tiens :)</p>
<p>En tant que <em>hackers</em>, on a le moyen de créer des outils qui sont utiles
à tous, et qui peuvent être utiles pour favoriser la collaboration et la
mise en commun des données. J’ai eu l’occasion de discuter de projets
tournant autour de l’entraide, que ça soit pour mettre en lien des
associations d’économie sociale et solidaire (<span class="caps">ESS</span>) ou simplement pour
que les populations <em>non tech</em> <a href="http://blog.notmyidea.org/quels-usages-pour-linformatique-fr.html">puissent utiliser toute la puissance de
l’outil qu’est le
web</a>.</p>
<p>Au niveau du format des conférences, je ne savais pas trop à quoi
m’attendre, au vu des échos de l’an dernier, mais c’était adapté: des
mini-confs de 12mn le samedi matin + début d’aprem, en mode no-wifi pour
récupérer une qualité d’écoute. Et contrairement à mes attentes, ce
n’est pas trop court. Pas mal de retours d’expérience pour le coup, et
une matinée pas vraiment techniques, mais ça pose le décor et permet de
savoir qui fait quoi.</p>
<p>Parmi l’ensemble des conférences du matin, je retiens principalement
celle de Mathieu Leplatre, “des cartes d’un autre monde”, qui m’a
réellement bluffée quand à la facilité de créer des cartes avec
<a href="http://mapbox.com/tilemill/">TileMill</a>, et qui me pousse à reconsidérer
le fait que “la carto, c’est compliqué”. <a href="https://www.youtube.com/watch?v=7NPQo54NbJ8">La vidéo est (déja !)
disponible en ligne</a>, je
vous invite à la regarder (c’est une 15aine de minutes) pour vous faire
un avis ;)</p>
<p>Une fois les conf passées, ça reste très intéressant, voire plus: il
reste un jour et demi pour discuter avec les autres présents. On a pu se
retrouver avec Mathieu pour discuter de “notre” projet <a href="http://blog.notmyidea.org/carto-forms-fr.html">“carto
forms”</a>, qui à finalement
pu se redéfinir un peu plus et donner naissance à un
<a href="https://github.com/spiral-project/daybed/blob/master/README.rst"><span class="caps">README</span></a>.
On en à profité pour lui choisir un nouveau nom: “daybed”, en référence
à couchdb.</p>
<p>Ça devrait se transformer en code d’ici peu. La curiosité aidant, on a
pu discuter du projet avec d’autres personnes et affiner les attentes de
chacun pour finalement arriver à quelque chose d’assez sympathique.</p>
<p>J’ai aussi pu me rendre compte que pas mal de monde utilise
<a href="http://pelican.notmyidea.org">pelican</a>, le bout de code que j’ai codé
pour générer ce blog, et avoir des retours utiles ! Probablement des
réflexions à venir sur comment éviter qu’un projet open-source ne
devienne chronophage, et sur comment réussir à garder une qualité dans
le code source tout en ne froissant pas les contributeurs.</p>
<p>Bien évidemment, c’était aussi l’occaz de rencontrer des gens qu’on ne
voir que sur les inter-nets, et de discuter un brin de tout ce qui fait
que notre monde est chouette et moins chouette.</p>
<p>Entres autres faits notoires, JMad a perdu au baby-foot face à Exirel,
même en m’ayant à ses cotés pour le déconcentrer (et je suis un joueur
d’un autre monde - en d’autres termes, je suis nul), David`bgk ne s’est
pas levé pour aller courir le dimanche matin (il avait dit 5 heures!),
Les suisses ont essayé de me convertir à coup d’abricotine, j’ai perdu
au skulls-n-roses en quelques tours et on a allumé un feu chez Stéphane
le dimanche soir (oui oui, à montpellier, mi avril, je vous le dis
qu’ils mentent avec leur soit disant soleil).</p>
<p>Et c’est sans parler de <a href="http://jehaisleprintemps.net/blog/fr/2012/04/15/j-ecris-ton-nom/">la
brasucade</a> …</p>
<p>Bref, vivement la prochaine (et allez, cette fois ci je ferais une
présentation !)</p>Génération de formulaires, geolocalisés ?2012-04-02T00:00:00+02:002012-04-02T00:00:00+02:00tag:blog.notmyidea.org,2012-04-02:/generation-de-formulaires-geolocalises.html
<p>On a un plan. Un “truc de ouf”.</p>
<p>À plusieurs reprises, des amis m’ont demandé de leur coder la même
chose, à quelques détails près: une page web avec un formulaire qui
permettrait de soumettre des informations géographiques, lié à une carte
et des manières de filtrer l’information …</p>
<p>On a un plan. Un “truc de ouf”.</p>
<p>À plusieurs reprises, des amis m’ont demandé de leur coder la même
chose, à quelques détails près: une page web avec un formulaire qui
permettrait de soumettre des informations géographiques, lié à une carte
et des manières de filtrer l’information.</p>
<p>L’idée fait son bout de chemin, et je commence à penser qu’on peut même
avoir quelque chose de vraiment flexible et utile. J’ai nommé le projet
<em>carto-forms</em> pour l’instant (mais c’est uniquement un nom de code).</p>
<p>Pour résumer: et si on avait un moyen de construire des formulaires, un
peu comme Google forms, mais avec des informations géographiques en plus?</p>
<p>Si vous ne connaissez pas Google forms, il s’agit d’une interface simple
d’utilisation pour générer des formulaires et récupérer des informations
depuis ces derniers.</p>
<p>Google forms est un super outil mais à mon avis manque deux choses
importantes: premièrement, il s’agit d’un outil propriétaire (oui, on
peut aussi dire privateur) et il n’est donc pas possible de le hacker un
peu pour le faire devenir ce qu’on souhaite, ni l’installer sur notre
propre serveur. Deuxièmement, il ne sait pas vraiment fonctionner avec
des informations géographiques, et il n’y à pas d’autre moyen de filtrer
les informations que l’utilisation de leur système de feuilles de calcul.</p>
<p>Après avoir réfléchi un petit peu à ça, j’ai contacté
<a href="http://blog.mathieu-leplatre.info/">Mathieu</a> et les anciens collègues
de chez <a href="http://makina-corpus.com">Makina Corpus</a>, puisque les projets
libres à base de carto sont à même de les intéresser.</p>
<p>Imaginez le cas suivant:</p>
<ol>
<li>Dans une “mapping party”, on choisit un sujet particulier à
cartographier et on design un formulaire (liste des champs (tags) a
remplir + description + le type d’information) ;</li>
<li>Sur place, les utilisateurs remplissent les champs du formulaire
avec ce qu’ils voient. Les champs géolocalisés peuvent être remplis
automatiquement avec la géolocalisation du téléphone ;</li>
<li>À la fin de la journée, il est possible de voir une carte des
contributions, avec le formulaire choisi ;</li>
<li>Un script peut importer les résultats et les publier vers OpenStreetMap.</li>
</ol>
<h2 id="quelques-cas-dutilisation">Quelques cas d’utilisation</h2>
<p>J’arrive à imaginer différents cas d’utilisation pour cet outil. Le
premier est celui que j’ai approximativement décrit plus haut: la
génération de cartes de manière collaborative, avec des filtres à
facettes. Voici un flux d’utilisation général:</p>
<ul>
<li>
<p>Un “administrateur” se rend sur le site web et crée un nouveau
formulaire pour l’ensemble des évènements alternatifs. Il crée les
champs suivants:</p>
<ul>
<li>Nom: le champ qui contient le nom de l’évènement.</li>
<li>Catégorie: la catégorie de l’évènement (marche, concert,
manifestation…). Il peut s’agir d’un champ à multiples occurrences.</li>
<li>Le lieu de l’évènement. Celui-ci peut être donné soit par une
adresse soit en sélectionnant un point sur une carte.</li>
<li>Date: la date de l’évènement (un “date picker” peut permettre
cela facilement)</li>
</ul>
<p>Chaque champ dans le formulaire a des informations sémantiques
associées (oui/non, multiple sélection, date, heure, champ géocodé,
sélection carto, etc.)</p>
</li>
<li>
<p>Une fois terminé, le formulaire est généré et une <span class="caps">URL</span> permet d’y
accéder. (par exemple <a href="http://forms.notmyidea.org/alternatives">http://forms.notmyidea.org/alternatives</a>).</p>
</li>
<li>
<p>Une <span class="caps">API</span> <span class="caps">REST</span> permet à d’autres applications d’accéder aux
informations et d’en ajouter / modifier de nouvelles.</p>
</li>
<li>
<p>Il est maintenant possible de donner l’<span class="caps">URL</span> à qui voudra en faire bon
usage. N’importe qui peut ajouter des informations. On peut
également imaginer une manière de modérer les modifications si
besoin est.</p>
</li>
<li>
<p>Bien sur, la dernière phase est la plus intéressante: il est
possible de filtrer les informations par lieu, catégorie ou date, le
tout soit via une <span class="caps">API</span> <span class="caps">REST</span>, soit via une jolie carte et quelques
contrôles bien placés, dans le navigateur.</p>
</li>
</ul>
<p>Vous avez dû remarquer que le processus de création d’un formulaire est
volontairement très simple. L’idée est que n’importe qui puisse créer
des cartes facilement, en quelques clics. Si une <span class="caps">API</span> bien pensée suit,
on peut imaginer faire de la validation coté serveur et même faire des
applications pour téléphone assez simplement.</p>
<p>Pour aller un peu plus loin, si on arrive à penser un format de
description pour le formulaire, il sera possible de construire les
formulaires de manière automatisée sur différentes plateformes et
également sur des clients génériques.</p>
<p>On imagine pas mal d’exemples pour ce projet: des points de recyclage,
les endroits accessibles (pour fauteuils roulants etc.), identification
des arbres, bons coins à champignons, recensement des espèces en voie de
disparition (l’aigle de Bonelli est actuellement suivi en utilisant une
feuille de calcul partagée !), suivi des espèces dangereuses (le frelon
asiatique par exemple), cartographier les points d’affichage
publicitaires, participation citoyenne (graffitis, nids de poule, voir
<a href="http://fixmystreet.ca">http://fixmystreet.ca</a>), geocaching, trajectoires (randonnées,
coureurs, cyclistes)…</p>
<p>Voici quelques exemples où ce projet pourrait être utile (la liste n’est
pas exhaustive):</p>
<h3 id="un-backend-sig-simple-a-utiliser">Un backend <span class="caps">SIG</span> simple à utiliser</h3>
<p>Disons que vous êtes développeur mobile. Vous ne voulez pas vous
encombrer avec PostGIS ou écrire du code spécifique pour récupérer et
insérer des données <span class="caps">SIG</span>! Vous avez besoin de <em>Carto-Forms</em>! Une <span class="caps">API</span>
simple vous aide à penser vos modèles et vos formulaires, et cette même
<span class="caps">API</span> vous permet d’insérer et de récupérer des données. Vous pouvez vous
concentrer sur votre application et non pas sur la manière dont les
données géographiques sont stockées et gérées.</p>
<p>En d’autres termes, vous faites une distinction entre le stockage des
informations et leur affichage.</p>
<p>Si vous êtes un développeur django, plomino, drupal etc. vous pouvez
développer un module pour “plugger” vos modèles et votre interface
utilisateur avec celle de <em>Carto-Forms</em>. De cette manière, il est
possible d’exposer les formulaires aux utilisateurs de vos backoffices.
De la même manière, il est possible d’écrire des widgets qui consomment
des données et les affichent (en utilisant par exemple une bibliothèque
javascript de webmapping).</p>
<h3 id="un-outil-de-visualisation">Un outil de visualisation</h3>
<p>Puisque les données peuvent être proposées de manière automatisée en
utilisant l’<span class="caps">API</span>, vous pouvez utiliser la page de résultat de Carto-forms
comme un outil de visualisation.</p>
<p>Il est possible d’explorer mon jeu de données en utilisant des filtres
sur chacun des champs. La recherche à facettes peut être une idée pour
faciliter ce filtrage. Une carte affiche le résultat. Vous avez
l’impressoin d’être en face d’un système d’aide à la décision !</p>
<p>Évidemment, il est possible de télécharger les données brutes (geojson,
xml). Idéalement, le mieux serait d’obtenir ces données filtrées
directement depuis une <span class="caps">API</span> Web, et un lien permet de partager la page
avec l’état des filtres et le niveau de zoom / la localisation de la carte.</p>
<h3 id="un-service-generique-pour-gerer-les-formulaires">Un service générique pour gérer les formulaires</h3>
<p>Si vous souhaitez générer un fichier de configuration (ou ce que vous
voulez, messages emails, …) vous aurez besoin d’un formulaire et d’un
template pour injecter les données proposées par les utilisateurs et
récupérer un résultat.</p>
<p>Un service de gestion des formulaires pourrait être utile pour créer des
formulaires de manière automatique et récupérer les données “nettoyées”
et “validées”.</p>
<p>On peut imaginer par exemple l’utilisation d’un système de templates
externe reposant sur <em>carto-forms</em>. Celui-ci “parserait” le contenu des
templates et pourrait le lier aux informations ajoutées par les
utilisateurs via un formulaire.</p>
<p>Pour ce cas particulier, il n’y a pas besoin d’informations
géographiques (<span class="caps">SIG</span>). Il s’agit quasiment du service proposé
actuellement par Google forms.</p>
<h2 id="ca-nexiste-pas-deja-tout-ca">Ça n’existe pas déjà tout ça ?</h2>
<p>Bien sur, il y a Google forms, qui vous permet de faire ce genre de
choses, mais comme je l’ai précisé plus haut, il ne s’agit pas
exactement de la même chose.</p>
<p>Nous avons découvert <a href="https://webform.com">https://webform.com</a> qui permet de créer des
formulaires avec un système de drag’n’drop. J’adorerais reproduire
quelque chose de similaire pour l’interface utilisateur. Par contre ce
projet ne gère pas les appels via <span class="caps">API</span> et les informations de
géolocalisation …</p>
<p>L’idée de <a href="http://thoth.io">http://thoth.io</a> est également assez sympathique: une api
très simple pour stocker et récupérer des données. En plus de ça,
<em>carto-forms</em> proposerait de la validation de données et proposerait un
support des points <span class="caps">SIG</span> (point, ligne, polygone).</p>
<p><a href="http://mapbox.com">http://mapbox.com</a> fait également un superbe travail autour de la
cartographie, mais ne prends pas en compte le coté auto-génération de formulaires…</p>
<h2 id="on-est-parti">On est parti ?!</h2>
<p>Comme vous avez pu vous en rendre compte, il ne s’agit pas d’un problème
outrageusement complexe. On a pas mal discuté avec Mathieu, à propos de
ce qu’on souhaite faire et du comment. Il se trouve qu’on peut sûrement
s’en sortir avec une solution élégante sans trop de problèmes. Mathieu
est habitué à travailler autour des projets de <span class="caps">SIG</span> (ce qui est parfait
parce que ce n’est pas mon cas) et connaît son sujet. Une bonne
opportunité d’apprendre!</p>
<p>On sera tous les deux à <a href="http://rencontres.django-fr.org">Djangocong</a> le
14 et 15 Avril, et on prévoit une session de <em>tempête de cerveau</em> et un
sprint sur ce projet. Si vous êtes dans le coin et que vous souhaitez
discuter ou nous filer un coup de patte, n’hésitez pas!</p>
<p>On ne sait pas encore si on utilisera django ou quelque chose d’autre.
On a pensé un peu à CouchDB, son système de couchapps et geocouch, mais
rien n’est encore gravé dans le marbre ! N’hésitez pas à proposer vos
solutions ou suggestions.</p>
<p>Voici le document etherpad sur lequel on a travaillé jusqu’à maintenant:
<a href="http://framapad.org/carto-forms">http://framapad.org/carto-forms</a>. N’hésitez pas à l’éditer et à ajouter
vos commentaires, c’est son objectif!</p>
<p>Merci à <a href="http://sneakernet.fr/">Arnaud</a> pour la relecture et la
correction de quelques typos dans le texte :)</p>Thoughts about a form generation service, GIS enabled2012-04-02T00:00:00+02:002012-04-02T00:00:00+02:00tag:blog.notmyidea.org,2012-04-02:/thoughts-about-a-form-generation-service-gis-enabled.html
<p><em>Written by Alexis Métaireau <span class="amp">&</span> Mathieu Leplatre</em></p>
<p>We have a plan. A “fucking good” one.</p>
<p>A bunch of friends asked me twice for quite the same thing: a webpage
with a form, tied to a map generation with some information filtering.
They didn’t explicitly ask that but that’s the …</p>
<p><em>Written by Alexis Métaireau <span class="amp">&</span> Mathieu Leplatre</em></p>
<p>We have a plan. A “fucking good” one.</p>
<p>A bunch of friends asked me twice for quite the same thing: a webpage
with a form, tied to a map generation with some information filtering.
They didn’t explicitly ask that but that’s the gist of it.</p>
<p>This idea has been stuck in my head since then and I even think that we
can come out with something a little bit more flexible and useful. I’ve
named it <em>carto-forms</em> for now, but that’s only the “codename”.</p>
<p>To put it shortly: what if we had a way to build forms, ala Google
forms, but with geographic information in them?</p>
<p>If you don’t know Google forms, it means having an user-friendly way to
build forms and to use them to gather information from different users.</p>
<p>In my opinion, Google forms is missing two important things: first, it’s
not open-source, so it’s not possible to hack it or even to run it on
your own server. Second, it doesn’t really know how to deal with
geographic data, and there is no way to filter the information more than
in a spreadsheet.</p>
<p>I knew that <a href="http://blog.mathieu-leplatre.info/">Mathieu</a> and some folks
at <a href="http://makina-corpus.com">Makina Corpus</a> would be interested in
this, so I started a discussion with him on <span class="caps">IRC</span> and we refined the
details of the project and its objectives.</p>
<p>Imagine the following:</p>
<ol>
<li>For a mapping party, we choose a specific topic to map and design
the form (list of fields (i.e. tags) to be filled + description +
type of the information) ;</li>
<li>In situ, users fill the form fields with what they see. Geo fields
can be pre-populated using device geolocation ;</li>
<li>At the end of the day, we can see a map with all user contributions
seized through this particular form ;</li>
<li>If relevant, a script could eventually import the resulting dataset
and publish/merge with OpenStreetMap.</li>
</ol>
<h2 id="some-use-cases">Some use cases</h2>
<p>I can see some use cases for this. The first one is a collaborative map,
with facet filtering. Let’s draw a potential user flow:</p>
<ul>
<li>
<p>An “administrator” goes to the website and creates a form to list
all the alternative-related events. He creates the following fields:</p>
<ul>
<li>Name: a plain text field containing the name of the event.</li>
<li>Category: the category of the event. Can be a finite list.</li>
<li>Location: The location of the event. It could be provided by
selecting a point on a map or by typing an address.</li>
<li>Date: the date of the event (a datepicker could do the trick)</li>
</ul>
<p>Each field in the form has semantic information associated with it
(yes/no, multiple selection, date-time, geocoding carto, carto
selection etc)</p>
</li>
<li>
<p>Once finished, the form is generated and the user gets an url (say
<a href="http://forms.notmyidea.org/alternatives">http://forms.notmyidea.org/alternatives</a>) for it.</p>
</li>
<li>
<p><span class="caps">REST</span> APIs allow third parties to get the form description and to
push/edit/get information from there.</p>
</li>
<li>
<p>He can communicate the address in any way he wants to his community
so they can go to the page and add information to it.</p>
</li>
<li>
<p>Then, it is possible to filter the results per location / date or
category. This can be done via <span class="caps">API</span> calls (useful for third parties)
or via a nice interface in the browser.</p>
</li>
</ul>
<p>So, as you may have noticed, this would allow us to create interactive
maps really easily. It’s almost just a matter of some clicks to the
users. If we also come up with a nice Web <span class="caps">API</span> for this, we could do
server-side validation and build even phone applications easily.</p>
<p>To push the cursor a bit further, if we can come with a cool description
format for the forms, we could even build the forms dynamically on
different platforms, with generic clients.</p>
<p>As mentioned before, the idea of a simple tool to support collaborative
mapping fullfils a recurring necessity !</p>
<p>We envision a lot of example uses for this : recycling spots, accessible
spots (wheelchairs, etc.), trees identification, mushrooms picking
areas, tracking of endangered species (e.g. Bonelli’s Eagle is currently
tracked by sharing a spreadsheet), spotting of dangerous species (e.g.
asian predatory wasps), map advertisement boards (most cities do not
track them!), citizen reporting (e.g. graffiti, potholes, garbage,
lightning like <a href="http://fixmystreet.ca">http://fixmystreet.ca</a>), geocaching, trajectories (e.g
hiking, runners, cyclists)…</p>
<p>Here are some other examples of where <em>carto-forms</em> could be useful:</p>
<h3 id="simple-gis-storage-backend">Simple <span class="caps">GIS</span> storage backend</h3>
<p>Let’s say you are a mobile developer, you don’t want to bother with
PostGIS nor write a custom and insecure code to insert and retrieve your
<span class="caps">GIS</span> data! You need carto-forms! A simple <span class="caps">API</span> helps you design your
models/forms and the same <span class="caps">API</span> allows you to <span class="caps">CRUD</span> and query your data.
Thus, you only need to focus on your application, not on how <span class="caps">GIS</span> data
will be handled.</p>
<p>We make a distinction between storage and widgets.</p>
<p>Besides, if you are a django / drupal / plomino… maintainer : you can
develop a module to “plug” your models (content types) and <span class="caps">UI</span> to
carto-forms! Carto forms are then exposed to your backoffice users (ex:
drupal admin <span class="caps">UI</span>, django adminsite), and likewise you can write your own
<span class="caps">HTML</span> widgets that consume datasets in frontend views (facets in
<span class="caps">JSON</span>/<span class="caps">XML</span>, and map data in GeoJSON).</p>
<h3 id="visualization-tool">Visualization tool</h3>
<p>Since data submission can be done programmatically using the <span class="caps">API</span>, you
could use Carto-forms results page as a visualization tool.</p>
<p>You can explore your dataset content using filters related to each form
field. Facets filtering is a great advantage, and a map shows the
resulting features set. You feel like you’re in front of a decision
support system!</p>
<p>Of course, filtered raw data can be downloaded (GeoJSON, <span class="caps">XML</span>) and a
permalink allows to share the page with the state of the filters and the
zoom/location of the map.</p>
<h3 id="generic-forms-service">Generic forms service</h3>
<p>If you want to generate a configuration file (or whatever, email
messages, …), you will need a form and a template to inlay user
submitted values and get the result.</p>
<p>A form service would be really useful to create forms programmatically
and retrieve cleaned and validated input values.</p>
<p>You could run a dedicated template service based on <em>carto-forms</em>!
Parsing a template content, this external service could create a form
dynamically and bind them together. The output of the form service
(fields => values) would be bound to the input of a template engine
(variables => final result).</p>
<p>Note that for this use-case, there is no specific need of <span class="caps">GIS</span> data nor
storage of records for further retrieval.</p>
<h2 id="whats-out-in-the-wild-already">What’s out in the wild already?</h2>
<p>Of course, there is Google forms, which allows you to do these kind of
things, but it’s closed and not exactly what we are describing here.</p>
<p>We’ve discovered the interesting <a href="https://webform.com/">https://webform.com/</a> which allows one
to create forms with a nice drag-n-drop flow. I would love to reproduce
something similar for the user experience. However, the project doesn’t
handle APIs and geolocation information.</p>
<p>The idea of <a href="http://thoth.io">http://thoth.io</a> is very attractive : an extremely simple
web <span class="caps">API</span> to store and retrieve data. In addition, <em>carto-forms</em> would do
datatype validation and have basic <span class="caps">GIS</span> fields (point, line, polygon).</p>
<p><a href="http://mapbox.com">http://mapbox.com</a> also did an awesome work on cartography, but didn’t
take into account the form aspect we’re leveraging here.</p>
<h2 id="so-lets-get-it-real">So… Let’s get it real!</h2>
<p>As you may have understood, this isn’t a really complicated problem. We
have been sometimes chatting about that with Mathieu about what we would
need and how we could achieve this.</p>
<p>We can probably come with an elegant solution without too much pain.
Mathieu is used to work with <span class="caps">GIS</span> systems (which is really cool because
I’m not at all) and knows his subject, so that’s an opportunity to learn ;-)</p>
<p>We will be at <a href="http://rencontres.django-fr.org">Djangocong</a> on April 14
and 15 and will probably have a brainstorming session and a sprint on
this, so if you are around and want to help us, or just to discuss, feel
free to join!</p>
<p>We don’t know yet if we will be using django for this or something else.
We have been thinking about couchdb, couchapps and geocouch but nothing
is written in stone yet. Comments and proposals are welcome!</p>
<p>Here is the etherpad document we worked on so far:
<a href="http://framapad.org/carto-forms">http://framapad.org/carto-forms</a>. Don’t hesitate to add your thoughts
and edit it, that’s what it’s made for!</p>
<p>Thanks to <a href="http://sneakernet.fr/">Arnaud</a> and
<a href="http://qwerty.fuzz.me.uk/">Fuzzmz</a> for proof-reading and typo fixing.</p>Introducing Cornice2011-12-07T00:00:00+01:002011-12-07T00:00:00+01:00tag:blog.notmyidea.org,2011-12-07:/introducing-cornice.html
<p>Wow, already my third working day at Mozilla. Since Monday, I’ve been
working with <a href="http://ziade.org">Tarek Ziadé</a>, on a pyramid <span class="caps">REST</span>-ish
toolkit named <a href="https://github.com/mozilla-services/cornice">Cornice</a>.</p>
<p>Its goal is to take care for you of what you’re usually missing so you
can focus on what’s important. Cornice provides you …</p>
<p>Wow, already my third working day at Mozilla. Since Monday, I’ve been
working with <a href="http://ziade.org">Tarek Ziadé</a>, on a pyramid <span class="caps">REST</span>-ish
toolkit named <a href="https://github.com/mozilla-services/cornice">Cornice</a>.</p>
<p>Its goal is to take care for you of what you’re usually missing so you
can focus on what’s important. Cornice provides you facilities for
validation of any kind.</p>
<p>The goal is to simplify your work, but we don’t want to reinvent the
wheel, so it is easily pluggable with validations frameworks, such as
<a href="http://docs.pylonsproject.org/projects/colander/en/latest/">Colander</a>.</p>
<h2 id="handling-errors-and-validation">Handling errors and validation</h2>
<p>Here is how it works:</p>
<div class="highlight"><pre><span></span><code><span class="n">service</span> <span class="o">=</span> <span class="n">Service</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">"service"</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="s2">"/service"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">is_awesome</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="s1">'awesome'</span> <span class="ow">in</span> <span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="p">:</span>
<span class="n">request</span><span class="o">.</span><span class="n">errors</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="s1">'query'</span><span class="p">,</span> <span class="s1">'awesome'</span><span class="p">,</span>
<span class="s1">'the awesome parameter is required'</span><span class="p">)</span>
<span class="nd">@service</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">validator</span><span class="o">=</span><span class="n">is_awesome</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get1</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="k">return</span> <span class="p">{</span><span class="s2">"test"</span><span class="p">:</span> <span class="s2">"yay!"</span><span class="p">}</span>
</code></pre></div>
<p>All the errors collected during the validation process, or after, are
collected before returning the request. If any, a error 400 is fired up,
with the list of problems encountered returned as a nice json list
response (we plan to support multiple formats in the future)</p>
<p>As you might have seen, request.errors.add takes three parameters:
<strong>location</strong>, <strong>name</strong> and <strong>description</strong>.</p>
<p><strong>location</strong> is where the error is located in the request. It can either
be “body”, “query”, “headers” or “path”. <strong>name</strong> is the name of the
variable causing problem, if any, and <strong>description</strong> contains a more
detailed message.</p>
<p>Let’s run this simple service and send some queries to it:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>curl<span class="w"> </span>-v<span class="w"> </span>http://127.0.0.1:5000/service
><span class="w"> </span>GET<span class="w"> </span>/service<span class="w"> </span>HTTP/1.1
><span class="w"> </span>Host:<span class="w"> </span><span class="m">127</span>.0.0.1:5000
><span class="w"> </span>Accept:<span class="w"> </span>*/*
>
*<span class="w"> </span>HTTP<span class="w"> </span><span class="m">1</span>.0,<span class="w"> </span>assume<span class="w"> </span>close<span class="w"> </span>after<span class="w"> </span>body
<<span class="w"> </span>HTTP/1.0<span class="w"> </span><span class="m">400</span><span class="w"> </span>Bad<span class="w"> </span>Request
<<span class="w"> </span>Content-Type:<span class="w"> </span>application/json<span class="p">;</span><span class="w"> </span><span class="nv">charset</span><span class="o">=</span>UTF-8
<span class="o">[{</span><span class="s2">"location"</span>:<span class="w"> </span><span class="s2">"query"</span>,<span class="w"> </span><span class="s2">"name"</span>:<span class="w"> </span><span class="s2">"awesome"</span>,<span class="w"> </span><span class="s2">"description"</span>:<span class="w"> </span><span class="s2">"You lack awesomeness!"</span><span class="o">}</span>
</code></pre></div>
<p>I’ve removed the extra clutter from the curl’s output, but you got the
general idea.</p>
<p>The content returned is in <span class="caps">JSON</span>, and I know exactly what I have to do:
add an “awesome” parameter in my query. Let’s do it again:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>curl<span class="w"> </span>http://127.0.0.1:5000/service?awesome<span class="o">=</span>yeah
<span class="o">{</span><span class="s2">"test"</span>:<span class="w"> </span><span class="s2">"yay!"</span><span class="o">}</span>
</code></pre></div>
<p>Validators can also convert parts of the request and store the converted
value in request.validated. It is a standard dict automatically attached
to the requests.</p>
<p>For instance, in our validator, we can chose to validate the parameter
passed and use it in the body of the webservice:</p>
<div class="highlight"><pre><span></span><code><span class="n">service</span> <span class="o">=</span> <span class="n">Service</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">"service"</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="s2">"/service"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">is_awesome</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="s1">'awesome'</span> <span class="ow">in</span> <span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="p">:</span>
<span class="n">request</span><span class="o">.</span><span class="n">errors</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="s1">'query'</span><span class="p">,</span> <span class="s1">'awesome'</span><span class="p">,</span>
<span class="s1">'the awesome parameter is required'</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">request</span><span class="o">.</span><span class="n">validated</span><span class="p">[</span><span class="s1">'awesome'</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'awesome '</span> <span class="o">+</span> <span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="p">[</span><span class="s1">'awesome'</span><span class="p">]</span>
<span class="nd">@service</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">validator</span><span class="o">=</span><span class="n">is_awesome</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get1</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="k">return</span> <span class="p">{</span><span class="s2">"test"</span><span class="p">:</span> <span class="n">request</span><span class="o">.</span><span class="n">validated</span><span class="p">[</span><span class="s1">'awesome'</span><span class="p">]}</span>
</code></pre></div>
<p>The output would look like this:</p>
<div class="highlight"><pre><span></span><code>curl http://127.0.0.1:5000/service?awesome=yeah
{"test": "awesome yeah"}
</code></pre></div>
<h2 id="dealing-with-accept-headers">Dealing with “Accept” headers</h2>
<p>The <span class="caps">HTTP</span> spec defines a <strong>Accept</strong> header the client can send so the
response is encoded the right way. A resource, available at an <span class="caps">URL</span>, can
be available in different formats. This is especially true for web services.</p>
<p>Cornice can help you dealing with this. The services you define can tell
which Content-Type values they can deal with and this will be checked
against the <strong>Accept</strong> headers sent by the client.</p>
<p>Let’s refine a bit our previous example, by specifying which
content-types are supported, using the accept parameter:</p>
<div class="highlight"><pre><span></span><code><span class="nd">@service</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">validator</span><span class="o">=</span><span class="n">is_awesome</span><span class="p">,</span> <span class="n">accept</span><span class="o">=</span><span class="p">(</span><span class="s2">"application/json"</span><span class="p">,</span> <span class="s2">"text/json"</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get1</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="k">return</span> <span class="p">{</span><span class="s2">"test"</span><span class="p">:</span> <span class="s2">"yay!"</span><span class="p">}</span>
</code></pre></div>
<p>Now, if you specifically ask for <span class="caps">XML</span>, Cornice will throw a 406 with the
list of accepted Content-Type values:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>curl<span class="w"> </span>-vH<span class="w"> </span><span class="s2">"Accept: application/xml"</span><span class="w"> </span>http://127.0.0.1:5000/service
><span class="w"> </span>GET<span class="w"> </span>/service<span class="w"> </span>HTTP/1.1
><span class="w"> </span>Host:<span class="w"> </span><span class="m">127</span>.0.0.1:5000
><span class="w"> </span>Accept:<span class="w"> </span>application/xml
>
<<span class="w"> </span>HTTP/1.0<span class="w"> </span><span class="m">406</span><span class="w"> </span>Not<span class="w"> </span>Acceptable
<<span class="w"> </span>Content-Type:<span class="w"> </span>application/json<span class="p">;</span><span class="w"> </span><span class="nv">charset</span><span class="o">=</span>UTF-8
<<span class="w"> </span>Content-Length:<span class="w"> </span><span class="m">33</span>
<
<span class="o">[</span><span class="s2">"application/json"</span>,<span class="w"> </span><span class="s2">"text/json"</span><span class="o">]</span>
</code></pre></div>
<h2 id="building-your-documentation-automatically">Building your documentation automatically</h2>
<p>writing documentation for web services can be painful, especially when
your services evolve. Cornice provides a sphinx directive to
automatically document your <span class="caps">API</span> in your docs.</p>
<div class="highlight"><pre><span></span><code><span class="nx">rst</span>
<span class="p">..</span><span class="w"> </span><span class="nx">services</span><span class="o">::</span>
<span class="w"> </span><span class="p">:</span><span class="kn">package</span><span class="p">:</span><span class="w"> </span><span class="nx">coolapp</span>
<span class="w"> </span><span class="p">:</span><span class="nx">service</span><span class="p">:</span><span class="w"> </span><span class="nx">quote</span>
</code></pre></div>
<p>Here is an example of what a generated page looks like:
<a href="http://packages.python.org/cornice/exampledoc.html">http://packages.python.org/cornice/exampledoc.html</a></p>
<h2 id="yay-how-can-i-get-it">Yay! How can I get it?</h2>
<p>We just cut a 0.4 release, so it’s available at
<a href="http://pypi.python.org/pypi/cornice">http://pypi.python.org/pypi/cornice</a> You can install it easily using
pip, for instance:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>cornice
</code></pre></div>
<p>You can also have a look at the documentation at
<a href="http://packages.python.org/cornice/">http://packages.python.org/cornice/</a></p>
<h2 id="whats-next">What’s next?</h2>
<p>We try to make our best to find how Cornice can help you build better
web services. Cool features we want for the future include the automatic
publication of a static definition of the services, so it can be used by
clients to discover services in a nice way.</p>
<p>Of course, we are open to all your ideas and patches! If you feel
haskish and want to see the sources, <a href="https://github.com/mozilla-services/cornice">go grab them on
github</a> , commit and send
us a pull request!</p>How are you handling your shared expenses?2011-10-15T00:00:00+02:002011-10-15T00:00:00+02:00tag:blog.notmyidea.org,2011-10-15:/how-are-you-handling-your-shared-expenses.html
<p><strong><span class="caps">TL</span>;<span class="caps">DR</span>:</strong> We’re kick-starting a new application to manage your shared
expenses. Have a look at <a href="http://ihatemoney.notmyidea.org">http://ihatemoney.notmyidea.org</a></p>
<p>As a student, I lived in a lot of different locations, and the majority
of them had something in common: I lived with others. It usually was a
great …</p>
<p><strong><span class="caps">TL</span>;<span class="caps">DR</span>:</strong> We’re kick-starting a new application to manage your shared
expenses. Have a look at <a href="http://ihatemoney.notmyidea.org">http://ihatemoney.notmyidea.org</a></p>
<p>As a student, I lived in a lot of different locations, and the majority
of them had something in common: I lived with others. It usually was a
great experience (and I think I will continue to live with others). Most
of the time, we had to spend some time each month to compute who had to
pay what to the others.</p>
<p>I wanted to create a pet project using flask, so I wrote a little (\~150
lines) flask application to handle this. It worked out pretty well for
my housemates and me, and as we had to move into different locations,
one of them asked me if he could continue to use it for the year to come.</p>
<p>I said yes and gave it some more thoughts: We probably aren’t the only
ones interested by such kind of software. I decided to extend a bit more
the software to have a concept of projects and persons (the list of
persons was hard-coded in the first time, boooh!).</p>
<p>I then discussed with a friend of mine, who was excited about it and
wanted to learn python. Great! That’s a really nice way to get started.
Some more friends were also interested in it and contributed some
features and provided feedback (thanks
<a href="http://www.sneakernet.fr/">Arnaud</a> and Quentin!)</p>
<p>Since that, the project now support multiple languages and provides a
<span class="caps">REST</span> <span class="caps">API</span> (android and iphone apps in the tubes!), into other things.
There is no need to register for an account or whatnot, just enter a
project name, a secret code and a contact email, invite friends and
that’s it (this was inspired by doodle)!</p>
<p><img alt="Capture d'écran du site." src="/images/ihatemoney.png"></p>
<p>You can try the project at <a href="http://ihatemoney.notmyidea.org">http://ihatemoney.notmyidea.org</a> for now,
and the code lives at <a href="https://github.com/spiral-project/ihatemoney/">https://github.com/spiral-project/ihatemoney/</a>.</p>
<h2 id="features">Features</h2>
<p>In the wild, currently, there already are some implementations of this
shared budget manager thing. The fact is that most of them are either
hard to use, with a too much fancy design or simply trying to do too
much things at once.</p>
<p>No, I don’t want my budget manager to make my shopping list, or to run a
blog for me, thanks. I want it to let me focus on something else. Keep
out of my way.</p>
<h3 id="no-user-registration">No user registration</h3>
<p>You don’t need to register an account on the website to start using it.
You just have to create a project, set a secret code for it, and give
both the url and the code to the people you want to share it with (or
the website can poke them for you).</p>
<h3 id="keeping-things-simple">Keeping things simple</h3>
<p><span class="dquo">“</span>Keep It Simple, Stupid” really matches our philosophy here: you want to
add a bill? Okay. Just do it. You just have to enter who paid, for who,
how much, and a description, like you would have done when you’re back
from the farmer’s market on raw paper.</p>
<h3 id="no-categories">No categories</h3>
<p>Some people like to organise their stuff into different “categories”:
leisure, work, eating, etc. That’s not something I want (at least to
begin with).</p>
<p>I want things to be simple. Got that? Great. Just add your bills!</p>
<h3 id="balance">Balance</h3>
<p>One of the most useful thing is to know what’s your “balance” compared
to others. In other words, if you’re negative, you owe money, if you’re
positive, you have to receive money. This allows you to dispatch who has
to pay for the next thing, in order to re-equilibrate the balance.</p>
<p>Additionally, the system is able to compute for you who has to give how
much to who, in order to reduce the number of transactions needed to
restore the balance.</p>
<h3 id="api"><span class="caps">API</span></h3>
<p>All of what’s possible to do with the standard web interface is also
available through a <span class="caps">REST</span> <span class="caps">API</span>. I developed a simple <span class="caps">REST</span> toolkit for
flask for this (and I should release it!).</p>
<h2 id="interested">Interested?</h2>
<p>This project is open source. All of us like to share what we are doing
and would be happy to work with new people and implement new ideas. If
you have a nice idea about this, if you want to tweak it or to fill
bugs. Don’t hesitate a second! The project lives at
<a href="http://github.com/spiral-project/ihatemoney/">http://github.com/spiral-project/ihatemoney/</a></p>Using dbpedia to get languages influences2011-08-16T00:00:00+02:002011-08-16T00:00:00+02:00tag:blog.notmyidea.org,2011-08-16:/using-dbpedia-to-get-languages-influences.html
<p>While browsing the Python’s wikipedia page, I found information about
the languages influenced by python, and the languages that influenced
python itself.</p>
<p>Well, that’s kind of interesting to know which languages influenced
others, it could even be more interesting to have an overview of the
connexion between them …</p>
<p>While browsing the Python’s wikipedia page, I found information about
the languages influenced by python, and the languages that influenced
python itself.</p>
<p>Well, that’s kind of interesting to know which languages influenced
others, it could even be more interesting to have an overview of the
connexion between them, keeping python as the main focus.</p>
<p>This information is available on the wikipedia page, but not in a really
exploitable format. Hopefully, this information is provided into the
information box present on the majority of wikipedia pages. And… guess
what? there is project with the goal to scrap and index all this
information in a more queriable way, using the semantic web technologies.</p>
<p>Well, you may have guessed it, the project in question in dbpedia, and
exposes information in the form of <span class="caps">RDF</span> triples, which are way more easy
to work with than simple <span class="caps">HTML</span>.</p>
<p>For instance, let’s take the page about python:
<a href="http://dbpedia.org/page/Python_%28programming_language%29">http://dbpedia.org/page/Python_%28programming_language%29</a></p>
<p>The interesting properties here are “Influenced” and “InfluencedBy”,
which allows us to get a list of languages. Unfortunately, they are not
really using all the power of the Semantic Web here, and the list is
actually a string with coma separated values in it.</p>
<p>Anyway, we can use a simple rule: All wikipedia pages of programming
languages are either named after the name of the language itself, or
suffixed with “( programming language)”, which is the case for python.</p>
<p>So I’ve built <a href="https://github.com/ametaireau/experiments/blob/master/influences/get_influences.py">a tiny script to extract the information from
dbpedia</a>
and transform them into a shiny graph using graphviz.</p>
<p>After a nice:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>python<span class="w"> </span>get_influences.py<span class="w"> </span>python<span class="w"> </span>dot<span class="w"> </span><span class="p">|</span><span class="w"> </span>dot<span class="w"> </span>-Tpng<span class="w"> </span>><span class="w"> </span>influences.png
</code></pre></div>
<p>The result is the following graph (<a href="http://files.lolnet.org/alexis/influences.png">see it directly
here</a>)</p>
<p><img alt="Graph des influances des langages les uns sur les
autres." src="http://files.lolnet.org/alexis/influences.png"></p>
<p>While reading this diagram, keep in mind that it is a) not listing all
the languages and b) keeping a python perspective.</p>
<p>This means that you can trust the scheme by following the arrows from
python to something and from something to python, it is not trying to
get the matching between all the languages at the same time to keep
stuff readable.</p>
<p>It would certainly be possible to have all the connections between all
languages (and the resulting script would be easier) to do so, but the
resulting graph would probably be way less readable.</p>
<p>You can find the script <a href="https://github.com/ametaireau/experiments">on my github
account</a>. Feel free to adapt
it for whatever you want if you feel hackish.</p>Pelican, 9 months later2011-07-25T00:00:00+02:002011-07-25T00:00:00+02:00tag:blog.notmyidea.org,2011-07-25:/pelican-9-months-later.html
<p>Back in October, I released
<a href="http://docs.notmyidea.org/alexis/pelican">pelican</a>, a little piece of
code I wrote to power this weblog. I had simple needs: I wanted to be
able to use my text editor of choice (vim), a vcs (mercurial) and
restructured text. I started to write a really simple blog engine in …</p>
<p>Back in October, I released
<a href="http://docs.notmyidea.org/alexis/pelican">pelican</a>, a little piece of
code I wrote to power this weblog. I had simple needs: I wanted to be
able to use my text editor of choice (vim), a vcs (mercurial) and
restructured text. I started to write a really simple blog engine in
something like a hundred python lines and released it on github.</p>
<p>And people started contributing. I wasn’t at all expecting to see people
interested in such a little piece of code, but it turned out that they
were. I refactored the code to make it evolve a bit more by two times
and eventually, in 9 months, got 49 forks, 139 issues and 73 pull requests.</p>
<p><strong>Which is clearly awesome.</strong></p>
<p>I pulled features such as translations, tag clouds, integration with
different services such as twitter or piwik, import from dotclear and
rss, fixed a number of mistakes and improved a lot the codebase. This
was a proof that there is a bunch of people that are willing to make
better softwares just for the sake of fun.</p>
<p>Thank you, guys, you’re why I like open source so much.</p>Using JPype to bridge python and Java2011-06-11T00:00:00+02:002011-06-11T00:00:00+02:00tag:blog.notmyidea.org,2011-06-11:/using-jpype-to-bridge-python-and-java.html
<p>Java provides some interesting libraries that have no exact equivalent
in python. In my case, the awesome boilerpipe library allows me to
remove uninteresting parts of <span class="caps">HTML</span> pages, like menus, footers and other
“boilerplate” contents.</p>
<p>Boilerpipe is written in Java. Two solutions then: using java from
python or reimplement boilerpipe …</p>
<p>Java provides some interesting libraries that have no exact equivalent
in python. In my case, the awesome boilerpipe library allows me to
remove uninteresting parts of <span class="caps">HTML</span> pages, like menus, footers and other
“boilerplate” contents.</p>
<p>Boilerpipe is written in Java. Two solutions then: using java from
python or reimplement boilerpipe in python. I will let you guess which
one I chosen, meh.</p>
<p>JPype allows to bridge python project with java libraries. It takes
another point of view than Jython: rather than reimplementing python in
Java, both languages are interfacing at the <span class="caps">VM</span> level. This means you
need to start a <span class="caps">VM</span> from your python script, but it does the job and stay
fully compatible with Cpython and its C extensions.</p>
<h2 id="first-steps-with-jpype">First steps with JPype</h2>
<p>Once JPype installed (you’ll have to hack a bit some files to integrate
seamlessly with your system) you can access java classes by doing
something like that:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">jpype</span>
<span class="n">jpype</span><span class="o">.</span><span class="n">startJVM</span><span class="p">(</span><span class="n">jpype</span><span class="o">.</span><span class="n">getDefaultJVMPath</span><span class="p">())</span>
<span class="c1"># you can then access to the basic java functions</span>
<span class="n">jpype</span><span class="o">.</span><span class="n">java</span><span class="o">.</span><span class="n">lang</span><span class="o">.</span><span class="n">System</span><span class="o">.</span><span class="n">out</span><span class="o">.</span><span class="n">println</span><span class="p">(</span><span class="s2">"hello world"</span><span class="p">)</span>
<span class="c1"># and you have to shutdown the VM at the end</span>
<span class="n">jpype</span><span class="o">.</span><span class="n">shutdownJVM</span><span class="p">()</span>
</code></pre></div>
<p>Okay, now we have a hello world, but what we want seems somehow more
complex. We want to interact with java classes, so we will have to load them.</p>
<h2 id="interfacing-with-boilerpipe">Interfacing with Boilerpipe</h2>
<p>To install boilerpipe, you just have to run an ant script:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span><span class="nb">cd</span><span class="w"> </span>boilerpipe
$<span class="w"> </span>ant
</code></pre></div>
<p>Here is a simple example of how to use boilerpipe in Java, from their sources</p>
<div class="highlight"><pre><span></span><code><span class="kn">package</span><span class="w"> </span><span class="nn">de.l3s.boilerpipe.demo</span><span class="p">;</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">java.net.URL</span><span class="p">;</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">de.l3s.boilerpipe.extractors.ArticleExtractor</span><span class="p">;</span>
<span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">Oneliner</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">public</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">main</span><span class="p">(</span><span class="kd">final</span><span class="w"> </span><span class="n">String</span><span class="o">[]</span><span class="w"> </span><span class="n">args</span><span class="p">)</span><span class="w"> </span><span class="kd">throws</span><span class="w"> </span><span class="n">Exception</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="n">URL</span><span class="w"> </span><span class="n">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">URL</span><span class="p">(</span><span class="s">"http://notmyidea.org"</span><span class="p">);</span>
<span class="w"> </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="n">ArticleExtractor</span><span class="p">.</span><span class="na">INSTANCE</span><span class="p">.</span><span class="na">getText</span><span class="p">(</span><span class="n">url</span><span class="p">));</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>To run it:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>javac<span class="w"> </span>-cp<span class="w"> </span>dist/boilerpipe-1.1-dev.jar:lib/nekohtml-1.9.13.jar:lib/xerces-2.9.1.jar<span class="w"> </span>src/demo/de/l3s/boilerpipe/demo/Oneliner.java
$<span class="w"> </span>java<span class="w"> </span>-cp<span class="w"> </span>src/demo:dist/boilerpipe-1.1-dev.jar:lib/nekohtml-1.9.13.jar:lib/xerces-2.9.1.jar<span class="w"> </span>de.l3s.boilerpipe.demo.Oneliner
</code></pre></div>
<p>Yes, this is kind of ugly, sorry for your eyes. Let’s try something
similar, but from python</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">jpype</span>
<span class="c1"># start the JVM with the good classpaths</span>
<span class="n">classpath</span> <span class="o">=</span> <span class="s2">"dist/boilerpipe-1.1-dev.jar:lib/nekohtml-1.9.13.jar:lib/xerces-2.9.1.jar"</span>
<span class="n">jpype</span><span class="o">.</span><span class="n">startJVM</span><span class="p">(</span><span class="n">jpype</span><span class="o">.</span><span class="n">getDefaultJVMPath</span><span class="p">(),</span> <span class="s2">"-Djava.class.path=</span><span class="si">%s</span><span class="s2">"</span> <span class="o">%</span> <span class="n">classpath</span><span class="p">)</span>
<span class="c1"># get the Java classes we want to use</span>
<span class="n">DefaultExtractor</span> <span class="o">=</span> <span class="n">jpype</span><span class="o">.</span><span class="n">JPackage</span><span class="p">(</span><span class="s2">"de"</span><span class="p">)</span><span class="o">.</span><span class="n">l3s</span><span class="o">.</span><span class="n">boilerpipe</span><span class="o">.</span><span class="n">extractors</span><span class="o">.</span><span class="n">DefaultExtractor</span>
<span class="c1"># call them !</span>
<span class="nb">print</span> <span class="n">DefaultExtractor</span><span class="o">.</span><span class="n">INSTANCE</span><span class="o">.</span><span class="n">getText</span><span class="p">(</span><span class="n">jpype</span><span class="o">.</span><span class="n">java</span><span class="o">.</span><span class="n">net</span><span class="o">.</span><span class="n">URL</span><span class="p">(</span><span class="s2">"http://blog.notmyidea.org"</span><span class="p">))</span>
</code></pre></div>
<p>And you get what you want.</p>
<p>I must say I didn’t thought it could work so easily. This will allow me
to extract text content from URLs and remove the <em>boilerplate</em> text
easily for infuse (my master thesis project), without having to write
java code, nice!</p>Un coup de main pour mon mémoire !2011-05-25T00:00:00+02:002011-05-25T00:00:00+02:00tag:blog.notmyidea.org,2011-05-25:/un-coup-de-main-pour-mon-memoire.html
<p>Ça y est, bientôt la fin. <span class="caps">LA</span> <span class="caps">FIN</span>. La fin des études, et le début du
reste. En attendant je bosse sur mon mémoire de fin d’études et j’aurais
besoin d’un petit coup de main.</p>
<p>Mon mémoire porte sur les systèmes de recommandation. Pour ceux qui
connaissent …</p>
<p>Ça y est, bientôt la fin. <span class="caps">LA</span> <span class="caps">FIN</span>. La fin des études, et le début du
reste. En attendant je bosse sur mon mémoire de fin d’études et j’aurais
besoin d’un petit coup de main.</p>
<p>Mon mémoire porte sur les systèmes de recommandation. Pour ceux qui
connaissent last.fm, je fais quelque chose de similaire mais pour les
sites internet: en me basant sur ce que vous visitez quotidiennement et
comment vous le visitez (quelles horaires, quelle emplacement
géographique, etc.) je souhaites proposer des liens qui vous
intéresseront potentiellement, en me basant sur l’avis des personnes
qui ont des profils similaires au votre.</p>
<p>Le projet est loin d’être terminé, mais la première étape est de
récupérer des données de navigation, idéalement beaucoup de données de
navigation. Donc si vous pouvez me filer un coup de main je vous en
serais éternellement reconnaissant (pour ceux qui font semblant de pas
comprendre, entendez “tournée générale”).</p>
<p>J’ai créé un petit site web (en anglais) qui résume un peu le concept,
qui vous propose de vous inscrire et de télécharger un plugin firefox
qui m’enverra des information sur les sites que vous visitez (si vous
avez l’habitude d’utiliser chrome vous pouvez considérer de switcher à
firefox4 pour les deux prochains mois pour me filer un coup de main). Il
est possible de désactiver le plugin d’un simple clic si vous souhaitez
garder votre vie privée privée ;-)</p>
<p>Le site est par là: <a href="http://infuse.notmyidea.org">http://infuse.notmyidea.org</a>. Une fois le plugin
téléchargé et le compte créé il faut renseigner vos identifiants dans
le plugin en question, et c’est tout!</p>
<p>A votre bon cœur ! Je récupérerais probablement des données durant les
2 prochains mois pour ensuite les analyser correctement.</p>
<p>Merci pour votre aide !</p>Analyse users’ browsing context to build up a web recommender2011-04-01T00:00:00+02:002011-04-01T00:00:00+02:00tag:blog.notmyidea.org,2011-04-01:/analyse-users-browsing-context-to-build-up-a-web-recommender.html
<p>No, this is not an april’s fool ;)</p>
<p>Wow, it’s been a long time. My year in Oxford is going really well. I
realized few days ago that the end of the year is approaching really
quickly. Exams are coming in one month or such and then I’ll …</p>
<p>No, this is not an april’s fool ;)</p>
<p>Wow, it’s been a long time. My year in Oxford is going really well. I
realized few days ago that the end of the year is approaching really
quickly. Exams are coming in one month or such and then I’ll be working
full time on my dissertation topic.</p>
<p>When I learned we’ll have about 6 month to work on something, I first
thought about doing a packaging related stuff, but finally decided to
start something new. After all, that’s the good time to learn.</p>
<p>Since a long time, I’m being impressed by the <a href="http://last.fm">last.fm</a>
recommender system. They’re <em>scrobbling</em> the music I listen to since
something like 5 years now and the recommendations they’re doing are
really nice and accurate (I discovered <strong>a lot</strong> of great artists
listening to the “neighbour radio”.) (by the way, <a href="http://lastfm.com/user/akounet/">here
is</a> my lastfm account)</p>
<p>So I decided to work on recommender systems, to better understand what
is it about.</p>
<p>Recommender systems are usually used to increase the sales of products
(like Amazon.com does) which is not really what I’m looking for (The one
who know me a bit know I’m kind of sick about all this consumerism going on).</p>
<p>Actually, the most simple thing I thought of was the web: I’m browsing
it quite every day and each time new content appears. I’ve stopped to
follow <a href="https://bitbucket.org/bruno/aspirator/">my feed reader</a> because
of the information overload, and reduced drastically the number of
people I follow <a href="http://twitter.com/ametaireau/">on twitter</a>.</p>
<p>Too much information kills the information.</p>
<p>You shall got what will be my dissertation topic: a recommender system
for the web. Well, such recommender systems already exists, so I will
try to add contextual information to them: you’re probably not
interested by the same topics at different times of the day, or
depending on the computer you’re using. We can also probably make good
use of the way you browse to create groups into the content you’re
browsing (or even use the great firefox4 tab group feature).</p>
<p>There is a large part of concerns to have about user’s privacy as well.</p>
<p>Here is my proposal (copy/pasted from the one I had to do for my master)</p>
<h2 id="introduction-and-rationale">Introduction and rationale</h2>
<p>Nowadays, people surf the web more and more often. New web pages are
created each day so the amount of information to retrieve is more
important as the time passes. These users uses the web in different
contexts, from finding cooking recipes to technical articles.</p>
<p>A lot of people share the same interest to various topics, and the
quantity of information is such than it’s really hard to triage them
efficiently without spending hours doing it. Firstly because of the huge
quantity of information but also because the triage is something
relative to each person. Although, this triage can be facilitated by
fetching the browsing information of all particular individuals and put
the in perspective.</p>
<p>Machine learning is a branch of Artificial Intelligence (<span class="caps">AI</span>) which deals
with how a program can learn from data. Recommendation systems are a
particular application area of machine learning which is able to
recommend things (links in our case) to the users, given a particular
database containing the previous choices users have made.</p>
<p>This browsing information is currently available in browsers. Even if it
is not in a very usable format, it is possible to transform it to
something useful. This information gold mine just wait to be used.
Although, it is not as simple as it can seems at the first approach: It
is important to take care of the context the user is in while browsing
links. For instance, It’s more likely that during the day, a computer
scientist will browse computing related links, and that during the
evening, he browse cooking recipes or something else.</p>
<p>Page contents are also interesting to analyse, because that’s what
people browse and what actually contain the most interesting part of the
information. The raw data extracted from the browsing can then be
translated into something more useful (namely tags, type of resource,
visit frequency, navigation context etc.)</p>
<p>The goal of this dissertation is to create a recommender system for web
links, including this context information.</p>
<p>At the end of the dissertation, different pieces of software will be
provided, from raw data collection from the browser to a recommendation system.</p>
<h2 id="background-review">Background Review</h2>
<p>This dissertation is mainly about data extraction, analysis and
recommendation systems. Two different research area can be isolated:
Data preprocessing and Information filtering.</p>
<p>The first step in order to make recommendations is to gather some data.
The more data we have available, the better it is (T. Segaran, 2007).
This data can be retrieved in various ways, one of them is to get it
directly from user’s browsers.</p>
<h3 id="data-preparation-and-extraction">Data preparation and extraction</h3>
<p>The data gathered from browsers is basically URLs and additional
information about the context of the navigation. There is clearly a need
to extract more information about the meaning of the data the user is
browsing, starting by the content of the web pages.</p>
<p>Because the information provided on the current Web is not meant to be
read by machines (T. Berners Lee, 2001) there is a need of tools to
extract meaning from web pages. The information needs to be preprocessed
before stored in a machine readable format, allowing to make
recommendations (Choochart et Al, 2004).</p>
<p>Data preparation is composed of two steps: cleaning and structuring (
Castellano et Al, 2007). Because raw data can contain a lot of un-needed
text (such as menus, headers etc.) and need to be cleaned prior to be
stored. Multiple techniques can be used here and belongs to boilerplate
removal and full text extraction (Kohlschütter et Al, 2010).</p>
<p>Then, structuring the information: category, type of content (news,
blog, wiki) can be extracted from raw data. This kind of information is
not clearly defined by <span class="caps">HTML</span> pages so there is a need of tools to
recognise them.</p>
<p>Some context-related information can also be inferred from each
resource. It can go from the visit frequency to the navigation group the
user was in while browsing. It is also possible to determine if the user
“liked” a resource, and determine a mark for it, which can be used by
information filtering a later step (T. Segaran, 2007).</p>
<p>At this stage, structuring the data is required. Storing this kind of
information in <span class="caps">RDBMS</span> can be a bit tedious and require complex queries to
get back the data in an usable format. Graph databases can play a major
role in the simplification of information storage and querying.</p>
<h3 id="information-filtering">Information filtering</h3>
<p>To filter the information, three techniques can be used (Balabanovic et
Al, 1997):</p>
<ul>
<li>The content-based approach states that if an user have liked
something in the past, he is more likely to like similar things in
the future. So it’s about establishing a profile for the user and
compare new items against it.</li>
<li>The collaborative approach will rather recommend items that other
similar users have liked. This approach consider only the
relationship between users, and not the profile of the user we are
making recommendations to.</li>
<li>the hybrid approach, which appeared recently combine both of the
previous approaches, giving recommendations when items score high
regarding user’s profile, or if a similar user already liked it.</li>
</ul>
<p>Grouping is also something to consider at this stage (G. Myatt, 2007).
Because we are dealing with huge amount of data, it can be useful to
detect group of data that can fit together. Data clustering is able to
find such groups (T. Segaran, 2007).</p>
<p>References:</p>
<ul>
<li>Balabanović, M., <span class="amp">&</span> Shoham, Y. (1997). Fab: content-based,
collaborative recommendation. Communications of the <span class="caps">ACM</span>, 40(3),
66–72. <span class="caps">ACM</span>. Retrieved March 1, 2011, from
<a href="http://portal.acm.org/citation.cfm?id=245108.245124&">http://portal.acm.org/citation.cfm?id=245108.245124&</a>;.</li>
<li>Berners-Lee, T., Hendler, J., <span class="amp">&</span> Lassila, O. (2001). The semantic
web: Scientific american. Scientific American, 284(5), 34–43.
Retrieved November 21, 2010, from
<a href="http://www.citeulike.org/group/222/article/1176986">http://www.citeulike.org/group/222/article/1176986</a>.</li>
<li>Castellano, G., Fanelli, A., <span class="amp">&</span> Torsello, M. (2007). <span class="caps">LODAP</span>: a LOg
DAta Preprocessor for mining Web browsing patterns. Proceedings of
the 6th Conference on 6th <span class="caps">WSEAS</span> Int. Conf. on Artificial
Intelligence, Knowledge Engineering and Data Bases-Volume 6 (p.
12–17). World Scientific and Engineering Academy and Society
(<span class="caps">WSEAS</span>). Retrieved March 8, 2011, from
<a href="http://portal.acm.org/citation.cfm?id=1348485.1348488">http://portal.acm.org/citation.cfm?id=1348485.1348488</a>.</li>
<li>Kohlschutter, C., Fankhauser, P., <span class="amp">&</span> Nejdl, W. (2010). Boilerplate
detection using shallow text features. Proceedings of the third <span class="caps">ACM</span>
international conference on Web search and data mining (p. 441–450).
<span class="caps">ACM</span>. Retrieved March 8, 2011, from
<a href="http://portal.acm.org/citation.cfm?id=1718542">http://portal.acm.org/citation.cfm?id=1718542</a>.</li>
<li>Myatt, <span class="caps">G. J.</span>(2007). Making Sense of Data: A Practical Guide to
Exploratory Data Analysis and Data Mining.</li>
<li>Segaran, T. (2007). Collective Intelligence.</li>
</ul>
<h2 id="privacy">Privacy</h2>
<p>The first thing that’s come to people minds when it comes to process
their browsing data is privacy. People don’t want to be stalked. That’s
perfectly right, and I don’t either.</p>
<p>But such a system don’t have to deal with people identities. It’s
completely possible to process completely anonymous data, and that’s
probably what I’m gonna do.</p>
<p>By the way, if you have interesting thoughts about that, if you do know
projects that do seems related, fire the comments !</p>
<h2 id="whats-the-plan">What’s the plan ?</h2>
<p>There is a lot of different things to explore, especially because I’m a
complete novice in that field.</p>
<ul>
<li>I want to develop a firefox plugin, to extract the browsing
informations ( still, I need to know exactly which kind of
informations to retrieve). The idea is to provide some <em>raw</em>
browsing data, and then to transform it and to store it in the
better possible way.</li>
<li>Analyse how to store the informations in a graph database. What can
be the different methods to store this data and to visualize the
relationship between different pieces of data? How can I define the
different contexts, and add those informations in the db?</li>
<li>Process the data using well known recommendation algorithms. Compare
the results and criticize their value.</li>
</ul>
<p>There is plenty of stuff I want to try during this experimentation:</p>
<ul>
<li>I want to try using Geshi to visualize the connexion between the
links, and the contexts</li>
<li>Try using graph databases such as Neo4j</li>
<li>Having a deeper look at tools such as scikit.learn (a machine
learning toolkit in python)</li>
<li>Analyse web pages in order to categorize them. Processing their
contents as well, to do some keyword based classification will be done.</li>
</ul>
<p>Lot of work on its way, yay !</p>Working directly on your server? How to backup and sync your dev environment with unison2011-03-16T00:00:00+01:002011-03-16T00:00:00+01:00tag:blog.notmyidea.org,2011-03-16:/working-directly-on-your-server-how-to-backup-and-sync-your-dev-environment-with-unison.html
<p>I have a server running freebsd since some time now, and was wondering
about the possibility to directly have a development environment ready
to use when I get a internet connexion, even if I’m not on my computer.</p>
<p>Since I use vim to code, and spend most of my …</p>
<p>I have a server running freebsd since some time now, and was wondering
about the possibility to directly have a development environment ready
to use when I get a internet connexion, even if I’m not on my computer.</p>
<p>Since I use vim to code, and spend most of my time in a console while
developing, it’s possible to work via ssh, from everywhere.</p>
<p>The only problem is the synchronisation of the source code, config files
etc. from my machine to the server.</p>
<p>Unison provides an interesting way to synchronise two folders, even over
a network. So let’s do it !</p>
<h2 id="creating-the-jail">Creating the jail</h2>
<p>In case you don’t use FreeBSD, you can skip this section.</p>
<div class="highlight"><pre><span></span><code># I have a flavour jail named default
$ ezjail-admin -f default workspace.notmyidea.org 172.19.1.6
$ ezjail-admin start workspace.notmyidea.org
</code></pre></div>
<p>In my case, because the “default” flavour contains already a lot of
interesting things, my jail come already setup with ssh, bash and vim
for instance, but maybe you’ll need it in your case.</p>
<p>I want to be redirected to the ssh of the jail when I connect to the
host with the 20006 port. Add lines in <code>/etc/pf.conf</code>:</p>
<div class="highlight"><pre><span></span><code><span class="w"> </span><span class="nv">workspace_jail</span><span class="o">=</span><span class="s2">"172.19.1.6"</span>
<span class="w"> </span>rdr<span class="w"> </span>on<span class="w"> </span><span class="nv">$ext_if</span><span class="w"> </span>proto<span class="w"> </span>tcp<span class="w"> </span>from<span class="w"> </span>any<span class="w"> </span>to<span class="w"> </span><span class="nv">$ext_ip</span><span class="w"> </span>port<span class="w"> </span><span class="m">20006</span><span class="w"> </span>-><span class="w"> </span><span class="nv">$workspace_jail</span><span class="w"> </span>port<span class="w"> </span><span class="m">22</span>
</code></pre></div>
<p>Reload packet filter rules</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>/etc/rc.d/pf<span class="w"> </span>reload
</code></pre></div>
<h2 id="working-with-unison">Working with unison</h2>
<p>Now that we’ve set up the jail. Set up unison on the server and on your
client. Unison is available on the freebsd ports so just install it</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>ssh<span class="w"> </span>notmyidea.org<span class="w"> </span>-p<span class="w"> </span><span class="m">20006</span>
$<span class="w"> </span>make<span class="w"> </span>-C<span class="w"> </span>/usr/ports/net/unison-nox11<span class="w"> </span>config-recursive
$<span class="w"> </span>make<span class="w"> </span>-C<span class="w"> </span>/usr/ports/net/unison-nox11<span class="w"> </span>package-recursive
</code></pre></div>
<p>Install as well unison on your local machine. Double check to install
the same version on the client and on the server. Ubuntu contains the
2.27.57 as well as the 2.32.52.</p>
<p>Check that unison is installed and reachable via ssh from your machine</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>ssh<span class="w"> </span>notmyidea.org<span class="w"> </span>-p<span class="w"> </span><span class="m">20006</span><span class="w"> </span>unison<span class="w"> </span>-version
unison<span class="w"> </span>version<span class="w"> </span><span class="m">2</span>.27.157
$<span class="w"> </span>unison<span class="w"> </span>-version
unison<span class="w"> </span>version<span class="w"> </span><span class="m">2</span>.27.57
</code></pre></div>
<h2 id="let-sync-our-folders">Let sync our folders</h2>
<p>The first thing I want to sync is my vim configuration. Well, it’s
already <a href="http://github.com/ametaireau/dotfiles/">in a git repository</a>
but let’s try to use unison for it right now.</p>
<p>I have two machines then: workspace, the jail, and ecureuil my laptop.</p>
<div class="highlight"><pre><span></span><code>unison<span class="w"> </span>.vim<span class="w"> </span>ssh://notmyidea.org:20006/.vim
unison<span class="w"> </span>.vimrc<span class="w"> </span>ssh://notmyidea.org:20006/.vimrc
</code></pre></div>
<p>It is also possible to put all the informations in a config file, and
then to only run unison. (fire up vim \~/.unison/default.prf.</p>
<p>Here is my config:</p>
<div class="highlight"><pre><span></span><code><span class="w"> </span><span class="na">root</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">/home/alexis</span>
<span class="w"> </span><span class="na">root</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">ssh://notmyidea.org:20006</span>
<span class="w"> </span><span class="na">path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">.vimrc</span>
<span class="w"> </span><span class="na">path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">dotfiles</span>
<span class="w"> </span><span class="na">path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">dev</span>
<span class="w"> </span><span class="na">follow</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">Name *</span>
</code></pre></div>
<p>My vimrc is in fact a symbolic link on my laptop, but I don’t want to
specify each of the links to unison. That’s why the follow = Name * is for.</p>
<p>The folders you want to synchronize are maybe a bit large. If so,
considering others options such as rsync for the first import may be a
good idea (I enjoyed my university huge upload bandwith to upload <span class="caps">2GB</span> in
20mn ;)</p>
<h2 id="run-the-script-frequently">Run the script frequently</h2>
<p>Once that done, you just need to run the unison command line some times
when you want to sync your two machines. I’ve wrote a tiny script to get
some feedback from the sync:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
<span class="n">DEFAULT_LOGFILE</span> <span class="o">=</span> <span class="s2">"~/unison.log"</span>
<span class="n">PROGRAM_NAME</span> <span class="o">=</span> <span class="s2">"Unison syncer"</span>
<span class="k">def</span> <span class="nf">sync</span><span class="p">(</span><span class="n">logfile</span><span class="o">=</span><span class="n">DEFAULT_LOGFILE</span><span class="p">,</span> <span class="n">program_name</span><span class="o">=</span><span class="n">PROGRAM_NAME</span><span class="p">):</span>
<span class="c1"># init</span>
<span class="n">display_message</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">error</span> <span class="o">=</span> <span class="kc">False</span>
<span class="n">before</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span>
<span class="c1"># call unison to make the sync</span>
<span class="n">os</span><span class="o">.</span><span class="n">system</span><span class="p">(</span><span class="s1">'unison -batch > </span><span class="si">{0}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">logfile</span><span class="p">))</span>
<span class="c1"># get the duration of the operation</span>
<span class="n">td</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span> <span class="o">-</span> <span class="n">before</span>
<span class="n">delta</span> <span class="o">=</span> <span class="p">(</span><span class="n">td</span><span class="o">.</span><span class="n">microseconds</span> <span class="o">+</span> <span class="p">(</span><span class="n">td</span><span class="o">.</span><span class="n">seconds</span> <span class="o">+</span> <span class="n">td</span><span class="o">.</span><span class="n">days</span> <span class="o">*</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">3600</span><span class="p">)</span> <span class="o">*</span> <span class="mi">10</span><span class="o">**</span><span class="mi">6</span><span class="p">)</span> <span class="o">/</span> <span class="mi">10</span><span class="o">**</span><span class="mi">6</span>
<span class="c1"># check what was the last entry in the log</span>
<span class="n">log</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">expanduser</span><span class="p">(</span><span class="n">logfile</span><span class="p">))</span>
<span class="n">lines</span> <span class="o">=</span> <span class="n">log</span><span class="o">.</span><span class="n">readlines</span><span class="p">()</span>
<span class="k">if</span> <span class="s1">'No updates to propagate'</span> <span class="ow">in</span> <span class="n">lines</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]:</span>
<span class="n">display_message</span> <span class="o">=</span> <span class="kc">False</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">output</span> <span class="o">=</span> <span class="p">[</span><span class="n">l</span> <span class="k">for</span> <span class="n">l</span> <span class="ow">in</span> <span class="n">lines</span> <span class="k">if</span> <span class="s2">"Synchronization"</span> <span class="ow">in</span> <span class="n">l</span><span class="p">]</span>
<span class="n">message</span> <span class="o">=</span> <span class="n">output</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
<span class="n">message</span> <span class="o">+=</span> <span class="s2">" It took </span><span class="si">{0}</span><span class="s2">s."</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">delta</span><span class="p">)</span>
<span class="k">if</span> <span class="n">display_message</span><span class="p">:</span>
<span class="n">os</span><span class="o">.</span><span class="n">system</span><span class="p">(</span><span class="s1">'notify-send -i </span><span class="si">{2}</span><span class="s1"> "</span><span class="si">{0}</span><span class="s1">" "</span><span class="si">{1}</span><span class="s1">"'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">program_name</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span>
<span class="s1">'error'</span> <span class="k">if</span> <span class="n">error</span> <span class="k">else</span> <span class="s1">'info'</span><span class="p">))</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">"__main__"</span><span class="p">:</span>
<span class="n">sync</span><span class="p">()</span>
</code></pre></div>
<p>This is probably perfectible, but that does the job.</p>
<p>Last step is to tell you machine to run that frequently. That’s what
crontab is made for, so let’s <code>crontab -e</code>:</p>
<div class="highlight"><pre><span></span><code> $ <span class="gs">* *</span>/3 <span class="gs">* *</span> * . ~/.Xdbus; /usr/bin/python /home/alexis/dev/python/unison-syncer/sync.py
</code></pre></div>
<p>The \~/.Xdbus allows cron to communicate with your X11 session. Here is
its content.</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/bin/bash</span>
<span class="c1"># Get the pid of nautilus</span>
<span class="nv">nautilus_pid</span><span class="o">=</span><span class="k">$(</span>pgrep<span class="w"> </span>-u<span class="w"> </span><span class="nv">$LOGNAME</span><span class="w"> </span>-n<span class="w"> </span>nautilus<span class="k">)</span>
<span class="c1"># If nautilus isn't running, just exit silently</span>
<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">"</span><span class="nv">$nautilus_pid</span><span class="s2">"</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="nb">exit</span><span class="w"> </span><span class="m">0</span>
<span class="k">fi</span>
<span class="c1"># Grab the DBUS_SESSION_BUS_ADDRESS variable from nautilus's environment</span>
<span class="nb">eval</span><span class="w"> </span><span class="k">$(</span>tr<span class="w"> </span><span class="s1">'\0'</span><span class="w"> </span><span class="s1">'\n'</span><span class="w"> </span><<span class="w"> </span>/proc/<span class="nv">$nautilus_pid</span>/environ<span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span><span class="s1">'^DBUS_SESSION_BUS_ADDRESS='</span><span class="k">)</span>
<span class="c1"># Check that we actually found it</span>
<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">"</span><span class="nv">$DBUS_SESSION_BUS_ADDRESS</span><span class="s2">"</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">"Failed to find bus address"</span><span class="w"> </span>><span class="p">&</span><span class="m">2</span>
<span class="nb">exit</span><span class="w"> </span><span class="m">1</span>
<span class="k">fi</span>
<span class="c1"># export it so that child processes will inherit it</span>
<span class="nb">export</span><span class="w"> </span>DBUS_SESSION_BUS_ADDRESS
</code></pre></div>
<p>And it comes from
<a href="http://ubuntuforums.org/showthread.php?p=10148738#post10148738">here</a>.</p>
<p>A sync takes about 20s + the upload time on my machine, which stay
acceptable for all of my developments.</p>Wrap up of the distutils2 paris’ sprint2011-02-08T00:00:00+01:002011-02-08T00:00:00+01:00tag:blog.notmyidea.org,2011-02-08:/wrap-up-of-the-distutils2-paris-sprint.html
<p>Finally, thanks to a bunch of people that helped me to pay my train and
bus tickets, I’ve made it to paris for the distutils2 sprint.</p>
<p>They have been a bit more than 10 people to come during the sprint, and
it was very productive. Here’s a taste …</p>
<p>Finally, thanks to a bunch of people that helped me to pay my train and
bus tickets, I’ve made it to paris for the distutils2 sprint.</p>
<p>They have been a bit more than 10 people to come during the sprint, and
it was very productive. Here’s a taste of what we’ve been working on:</p>
<ul>
<li>the datafiles, a way to specify and to handle the installation of
files which are not python-related (pictures, manpages and so on).</li>
<li>mkgcfg, a tool to help you to create a setup.cfg in minutes (and
with funny examples)</li>
<li>converters from setup.py scripts. We do now have a piece of code
which reads your current setup.py file and fill in some fields in
the setup.cfg for you.</li>
<li>a compatibility layer for distutils1, so it can read the setup.cfg
you will wrote for distutils2 :-)</li>
<li>the uninstaller, so it’s now possible to uninstall what have been
installed by distutils2 (see <span class="caps">PEP</span> 376)</li>
<li>the installer, and the setuptools compatibility layer, which will
allow you to rely on setuptools’ based distributions (and there are
plenty of them!)</li>
<li>The compilers, so they are more flexible than they were. Since
that’s an obscure part of the code for distutils2 commiters (it
comes directly from the distutils1 ages), having some guys who
understood the problematics here was a must.</li>
</ul>
<p>Some people have also tried to port their packaging from distutils1 to
distutils2. They have spotted a number of bugs and made some
improvements to the code, to make it more friendly to use.</p>
<p>I’m really pleased to see how newcomers went trough the code, and
started hacking so fast. I must say it wasn’t the case when we started
to work on distutils1 so that’s a very good point: people now can hack
the code quicker than they could before.</p>
<p>Some of the features here are not <em>completely</em> finished yet, but are on
the tubes, and will be ready for a release (hopefully) at the end of the week.</p>
<p>Big thanks to logilab for hosting (and sponsoring my train ticket) and
providing us food, and to bearstech for providing some money for
breakfast and bears^Wbeers.</p>
<p>Again, a big thanks to all the people who gave me money to pay the
transport, I really wasn’t expecting such thing to happen :-)</p>PyPI on CouchDB2011-01-20T00:00:00+01:002011-01-20T00:00:00+01:00tag:blog.notmyidea.org,2011-01-20:/pypi-on-couchdb.html
<p>By now, there are two ways to retrieve data from PyPI (the Python
Package Index). You can both rely on xml/rpc or on the “simple” <span class="caps">API</span>. The
simple <span class="caps">API</span> is not so simple to use as the name suggest, and have several
existing drawbacks.</p>
<p>Basically, if you want to …</p>
<p>By now, there are two ways to retrieve data from PyPI (the Python
Package Index). You can both rely on xml/rpc or on the “simple” <span class="caps">API</span>. The
simple <span class="caps">API</span> is not so simple to use as the name suggest, and have several
existing drawbacks.</p>
<p>Basically, if you want to use informations coming from the simple <span class="caps">API</span>,
you will have to parse web pages manually, to extract informations using
some black vodoo magic. Badly, magic have a price, and it’s sometimes
impossible to get exactly the informations you want to get from this
index. That’s the technique currently being used by distutils2,
setuptools and pip.</p>
<p>On the other side, while <span class="caps">XML</span>/<span class="caps">RPC</span> is working fine, it’s requiring extra
work to the python servers each time you request something, which can
lead to some outages from time to time. Also, it’s important to point
out that, even if PyPI have a mirroring infrastructure, it’s only for
the so-called <em>simple</em> <span class="caps">API</span>, and not for the <span class="caps">XML</span>/<span class="caps">RPC</span>.</p>
<h2 id="couchdb">CouchDB</h2>
<p>Here comes CouchDB. CouchDB is a document oriented database, that knows
how to speak <span class="caps">REST</span> and <span class="caps">JSON</span>. It’s easy to use, and provides out of the
box a replication mechanism.</p>
<h2 id="so-what">So, what ?</h2>
<p>Hmm, I’m sure you got it. I’ve wrote a piece of software to link
informations from PyPI to a CouchDB instance. Then you can replicate all
the PyPI index with only one <span class="caps">HTTP</span> request on the CouchDB server. You can
also access the informations from the index directly using a <span class="caps">REST</span> <span class="caps">API</span>,
speaking json. Handy.</p>
<p>So PyPIonCouch is using the PyPI <span class="caps">XML</span>/<span class="caps">RPC</span> <span class="caps">API</span> to get data from PyPI, and
generate records in the CouchDB instance.</p>
<p>The final goal is to avoid to rely on this “simple” <span class="caps">API</span>, and rely on a
<span class="caps">REST</span> insterface instead. I have set up a couchdb server on my server,
which is available at
<a href="http://couchdb.notmyidea.org/_utils/database.html?pypi">http://couchdb.notmyidea.org/_utils/database.html?pypi</a>.</p>
<p>There is not a lot to see there for now, but I’ve done the first import
from PyPI yesterday and all went fine: it’s possible to access the
metadata of all PyPI projects via a <span class="caps">REST</span> interface. Next step is to
write a client for this <span class="caps">REST</span> interface in distutils2.</p>
<h2 id="example">Example</h2>
<p>For now, you can use pypioncouch via the command line, or via the python
<span class="caps">API</span>.</p>
<h3 id="using-the-command-line">Using the command line</h3>
<p>You can do something like that for a full import. This <strong>will</strong> take
long, because it’s fetching all the projects at pypi and importing their metadata:</p>
<div class="highlight"><pre><span></span><code><span class="err">$</span> <span class="n">pypioncouch</span> <span class="o">--</span><span class="n">fullimport</span> <span class="n">http</span><span class="p">:</span><span class="o">//</span><span class="n">your</span><span class="o">.</span><span class="n">couchdb</span><span class="o">.</span><span class="n">instance</span><span class="o">/</span>
</code></pre></div>
<p>If you already have the data on your couchdb instance, you can just
update it with the last informations from pypi. <strong>However, I recommend
to just replicate the principal node, hosted at
<a href="http://couchdb.notmyidea.org/pypi/">http://couchdb.notmyidea.org/pypi/</a></strong>, to avoid the duplication of nodes:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>pypioncouch<span class="w"> </span>--update<span class="w"> </span>http://your.couchdb.instance/
</code></pre></div>
<p>The principal node is updated once a day by now, I’ll try to see if it’s
enough, and ajust with the time.</p>
<h3 id="using-the-python-api">Using the python <span class="caps">API</span></h3>
<p>You can also use the python <span class="caps">API</span> to interact with pypioncouch:</p>
<div class="highlight"><pre><span></span><code><span class="o">>>></span> <span class="kn">from</span> <span class="nn">pypioncouch</span> <span class="kn">import</span> <span class="n">XmlRpcImporter</span><span class="p">,</span> <span class="n">import_all</span><span class="p">,</span> <span class="n">update</span>
<span class="o">>>></span> <span class="n">full_import</span><span class="p">()</span>
<span class="o">>>></span> <span class="n">update</span><span class="p">()</span>
</code></pre></div>
<h2 id="whats-next">What’s next ?</h2>
<p>I want to make a couchapp, in order to navigate PyPI easily. Here are
some of the features I want to propose:</p>
<ul>
<li>List all the available projects</li>
<li>List all the projects, filtered by specifiers</li>
<li>List all the projects by author/maintainer</li>
<li>List all the projects by keywords</li>
<li>Page for each project.</li>
<li>Provide a PyPI “Simple” <span class="caps">API</span> equivalent, even if I want to replace
it, I do think it will be really easy to setup mirrors that way,
with the out of the box couchdb replication</li>
</ul>
<p>I also still need to polish the import mechanism, so I can directly
store in couchdb:</p>
<ul>
<li>The <span class="caps">OPML</span> files for each project</li>
<li>The upload_time as couchdb friendly format (list of int)</li>
<li>The tags as lists (currently it’s only a string separated by spaces</li>
</ul>
<p>The work I’ve done by now is available on
<a href="https://bitbucket.org/ametaireau/pypioncouch/">https://bitbucket.org/ametaireau/pypioncouch/</a>. Keep in mind that it’s
still a work in progress, and everything can break at any time. However,
any feedback will be appreciated !</p>Help me to go to the distutils2 paris’ sprint2011-01-15T00:00:00+01:002011-01-15T00:00:00+01:00tag:blog.notmyidea.org,2011-01-15:/help-me-to-go-to-the-distutils2-paris-sprint.html
<p><strong>Edit: Thanks to logilab and some amazing people, I can make it to
paris for the sprint. Many thanks to them for the support!</strong></p>
<p>There will be a distutils2 sprint from the 27th to the 30th of january,
thanks to logilab which will host the event.</p>
<p>You can find more …</p>
<p><strong>Edit: Thanks to logilab and some amazing people, I can make it to
paris for the sprint. Many thanks to them for the support!</strong></p>
<p>There will be a distutils2 sprint from the 27th to the 30th of january,
thanks to logilab which will host the event.</p>
<p>You can find more informations about the sprint on the wiki page of the
event (<a href="http://wiki.python.org/moin/Distutils/SprintParis">http://wiki.python.org/moin/Distutils/SprintParis</a>).</p>
<p>I really want to go there but I’m unfortunately blocked in <span class="caps">UK</span> for money
reasons. The cheapest two ways I’ve found is about £80, which I can’t
afford. Following some advices on #distutils, I’ve set up a ChipIn
account for that, so if some people want to help me making it to go
there, they can give me some money that way.</p>
<p>I’ll probably work on the installer (to support old distutils and
setuptools distributions) and on the uninstaller (depending on the first
task). If I can’t make it to paris, I’ll hang around on <span class="caps">IRC</span> to give some
help while needed.</p>
<p>If you want to contribute some money to help me go there, feel free to
use this chipin page:
<a href="http://ametaireau.chipin.com/distutils2-sprint-in-paris">http://ametaireau.chipin.com/distutils2-sprint-in-paris</a></p>
<p>Thanks for your support !</p>How to reboot your bebox using the CLI2010-10-21T00:00:00+02:002010-10-21T00:00:00+02:00tag:blog.notmyidea.org,2010-10-21:/how-to-reboot-your-bebox-using-the-cli.html
<p>I’ve an internet connection which, for some obscure reasons, tend to be
very slow from time to time. After rebooting the box (yes, that’s a hard
solution), all the things seems to go fine again.</p>
<h2 id="edit-using-grep"><span class="caps">EDIT</span> : Using grep</h2>
<p>After a bit of reflexion, that’s also really easy …</p>
<p>I’ve an internet connection which, for some obscure reasons, tend to be
very slow from time to time. After rebooting the box (yes, that’s a hard
solution), all the things seems to go fine again.</p>
<h2 id="edit-using-grep"><span class="caps">EDIT</span> : Using grep</h2>
<p>After a bit of reflexion, that’s also really easy to do using directly
the command line tools curl, grep and tail (but really harder to read).</p>
<div class="highlight"><pre><span></span><code>curl<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-u<span class="w"> </span>joel:joel<span class="w"> </span>http://bebox.config/cgi/b/info/restart/<span class="se">\?</span>be<span class="se">\=</span><span class="m">0</span><span class="se">\&</span>l0<span class="se">\=</span><span class="m">1</span><span class="se">\&</span>l1<span class="se">\=</span><span class="m">0</span><span class="se">\&</span>tid<span class="se">\=</span>RESTART<span class="w"> </span>-d<span class="w"> </span><span class="s2">"0=17&2=`curl -u joel:joel http://bebox.config/cgi/b/info/restart/\?be\=0\&l0\=1\&l1\=0\&tid\=RESTART | grep -o "</span><span class="nv">name</span><span class="o">=</span><span class="s1">'2'</span><span class="w"> </span><span class="nv">value</span><span class="o">=</span><span class="err">'</span><span class="o">[</span><span class="m">0</span>-9<span class="o">]</span><span class="se">\+</span><span class="s2">" | grep -o "</span><span class="o">[</span><span class="m">0</span>-9<span class="o">]</span><span class="se">\+</span><span class="s2">" | tail -n 1`&1"</span>
</code></pre></div>
<h2 id="the-python-version">The Python version</h2>
<p>Well, that’s not the optimal solution, that’s a bit “gruik”, but it works.</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">urllib2</span>
<span class="kn">import</span> <span class="nn">urlparse</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">argparse</span>
<span class="n">REBOOT_URL</span> <span class="o">=</span> <span class="s1">'/b/info/restart/?be=0&l0=1&l1=0&tid=RESTART'</span>
<span class="n">BOX_URL</span> <span class="o">=</span> <span class="s1">'http://bebox.config/cgi'</span>
<span class="k">def</span> <span class="nf">open_url</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">):</span>
<span class="n">passman</span> <span class="o">=</span> <span class="n">urllib2</span><span class="o">.</span><span class="n">HTTPPasswordMgrWithDefaultRealm</span><span class="p">()</span>
<span class="n">passman</span><span class="o">.</span><span class="n">add_password</span><span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
<span class="n">authhandler</span> <span class="o">=</span> <span class="n">urllib2</span><span class="o">.</span><span class="n">HTTPBasicAuthHandler</span><span class="p">(</span><span class="n">passman</span><span class="p">)</span>
<span class="n">opener</span> <span class="o">=</span> <span class="n">urllib2</span><span class="o">.</span><span class="n">build_opener</span><span class="p">(</span><span class="n">authhandler</span><span class="p">)</span>
<span class="n">urllib2</span><span class="o">.</span><span class="n">install_opener</span><span class="p">(</span><span class="n">opener</span><span class="p">)</span>
<span class="k">return</span> <span class="n">urllib2</span><span class="o">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">url</span><span class="p">)</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">reboot</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">):</span>
<span class="n">data</span> <span class="o">=</span> <span class="n">open_url</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
<span class="n">token</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">findall</span><span class="p">(</span><span class="s2">"name\=</span><span class="se">\\</span><span class="s2">'2</span><span class="se">\\</span><span class="s2">' value=</span><span class="se">\\</span><span class="s2">'([0-9]+)</span><span class="se">\\</span><span class="s2">'"</span><span class="p">,</span> <span class="n">data</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
<span class="n">urllib2</span><span class="o">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">urllib2</span><span class="o">.</span><span class="n">Request</span><span class="p">(</span><span class="n">url</span><span class="o">=</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="s1">'0=17&2=</span><span class="si">%s</span><span class="s1">&1'</span> <span class="o">%</span> <span class="n">token</span><span class="p">))</span>
<span class="k">if</span> <span class="vm">__file__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s2">"""Reboot your bebox !"""</span><span class="p">)</span>
<span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="n">dest</span><span class="o">=</span><span class="s1">'user'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'username'</span><span class="p">)</span>
<span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="n">dest</span><span class="o">=</span><span class="s1">'password'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'password'</span><span class="p">)</span>
<span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="n">boxurl</span><span class="o">=</span><span class="s1">'boxurl'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">BOX_URL</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'Base box url. Default is </span><span class="si">%s</span><span class="s1">'</span> <span class="o">%</span> <span class="n">BOX_URL</span><span class="p">)</span>
<span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">url</span> <span class="o">=</span> <span class="n">urlparse</span><span class="o">.</span><span class="n">urljoin</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">boxurl</span><span class="p">,</span> <span class="n">REBOOT_URL</span><span class="p">)</span>
<span class="n">reboot</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">username</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">password</span><span class="p">)</span>
</code></pre></div>Dynamically change your gnome desktop wallpaper2010-10-11T00:00:00+02:002010-10-11T00:00:00+02:00tag:blog.notmyidea.org,2010-10-11:/dynamically-change-your-gnome-desktop-wallpaper.html
<p>In gnome, you can can use a <span class="caps">XML</span> file to have a dynamic wallpaper. It’s
not so easy, and you can’t just tell: use the pictures in this folder to
do so.</p>
<p>You can have a look to the git repository if you want:
<a href="http://github.com/ametaireau/gnome-background-generator">http://github.com/ametaireau …</a></p>
<p>In gnome, you can can use a <span class="caps">XML</span> file to have a dynamic wallpaper. It’s
not so easy, and you can’t just tell: use the pictures in this folder to
do so.</p>
<p>You can have a look to the git repository if you want:
<a href="http://github.com/ametaireau/gnome-background-generator">http://github.com/ametaireau/gnome-background-generator</a></p>
<p>Some time ago, I’ve made a little python script to ease that, and you
can now use it too. It’s named “gnome-background-generator”, and you can
install it via pip for instance.</p>
<div class="highlight"><pre><span></span><code>shell
$ pip install gnome-background-generator
</code></pre></div>
<p>Then, you have just to use it this way:</p>
<div class="highlight"><pre><span></span><code><span class="n">shell</span>
<span class="n">$ gnome-background-generator -p ~/Images/walls -s</span>
<span class="n">/home/alexis/Images/walls/dynamic-wallpaper.xml generated</span>
</code></pre></div>
<p>Here is a extract of the `—help`:</p>
<div class="highlight"><pre><span></span><code><span class="n">shell</span>
<span class="o">$</span><span class="w"> </span><span class="n">gnome</span><span class="o">-</span><span class="n">background</span><span class="o">-</span><span class="n">generator</span><span class="w"> </span><span class="o">--</span><span class="n">help</span>
<span class="n">usage</span><span class="p">:</span><span class="w"> </span><span class="n">gnome</span><span class="o">-</span><span class="n">background</span><span class="o">-</span><span class="n">generator</span><span class="w"> </span><span class="p">[</span><span class="o">-</span><span class="n">h</span><span class="p">]</span><span class="w"> </span><span class="p">[</span><span class="o">-</span><span class="n">p</span><span class="w"> </span><span class="n">PATH</span><span class="p">]</span><span class="w"> </span><span class="p">[</span><span class="o">-</span><span class="n">o</span><span class="w"> </span><span class="n">OUTPUT</span><span class="p">]</span>
<span class="w"> </span><span class="p">[</span><span class="o">-</span><span class="n">t</span><span class="w"> </span><span class="n">TRANSITION_TIME</span><span class="p">]</span><span class="w"> </span><span class="p">[</span><span class="o">-</span><span class="n">d</span><span class="w"> </span><span class="n">DISPLAY_TIME</span><span class="p">]</span><span class="w"> </span><span class="p">[</span><span class="o">-</span><span class="n">s</span><span class="p">]</span>
<span class="w"> </span><span class="p">[</span><span class="o">-</span><span class="n">b</span><span class="p">]</span>
<span class="n">A</span><span class="w"> </span><span class="n">simple</span><span class="w"> </span><span class="n">command</span><span class="w"> </span><span class="n">line</span><span class="w"> </span><span class="k">tool</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="n">generate</span><span class="w"> </span><span class="n">an</span><span class="w"> </span><span class="n">XML</span><span class="w"> </span><span class="n">file</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="n">use</span><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="n">gnome</span>
<span class="n">wallpapers</span><span class="p">,</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="n">have</span><span class="w"> </span><span class="n">dynamic</span><span class="w"> </span><span class="n">walls</span>
<span class="n">optional</span><span class="w"> </span><span class="n">arguments</span><span class="p">:</span>
<span class="w"> </span><span class="o">-</span><span class="n">h</span><span class="p">,</span><span class="w"> </span><span class="o">--</span><span class="n">help</span><span class="w"> </span><span class="n">show</span><span class="w"> </span><span class="n">this</span><span class="w"> </span><span class="n">help</span><span class="w"> </span><span class="n">message</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="n">exit</span>
<span class="w"> </span><span class="o">-</span><span class="n">p</span><span class="w"> </span><span class="n">PATH</span><span class="p">,</span><span class="w"> </span><span class="o">--</span><span class="n">path</span><span class="w"> </span><span class="n">PATH</span><span class="w"> </span><span class="n">Path</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="n">look</span><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">pictures</span><span class="o">.</span><span class="w"> </span><span class="n">If</span><span class="w"> </span><span class="n">no</span><span class="w"> </span><span class="n">output</span><span class="w"> </span><span class="k">is</span>
<span class="w"> </span><span class="n">specified</span><span class="p">,</span><span class="w"> </span><span class="n">will</span><span class="w"> </span><span class="n">be</span><span class="w"> </span><span class="n">used</span><span class="w"> </span><span class="n">too</span><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="n">outputing</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">dynamic</span><span class="o">-</span>
<span class="w"> </span><span class="n">wallpaper</span><span class="o">.</span><span class="n">xml</span><span class="w"> </span><span class="n">file</span><span class="o">.</span><span class="w"> </span><span class="n">Default</span><span class="w"> </span><span class="n">value</span><span class="w"> </span><span class="k">is</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">current</span>
<span class="w"> </span><span class="n">directory</span><span class="w"> </span><span class="p">(</span><span class="o">.</span><span class="p">)</span>
<span class="w"> </span><span class="o">-</span><span class="n">o</span><span class="w"> </span><span class="n">OUTPUT</span><span class="p">,</span><span class="w"> </span><span class="o">--</span><span class="n">output</span><span class="w"> </span><span class="n">OUTPUT</span>
<span class="w"> </span><span class="n">Output</span><span class="w"> </span><span class="n">filename</span><span class="o">.</span><span class="w"> </span><span class="n">If</span><span class="w"> </span><span class="n">no</span><span class="w"> </span><span class="n">filename</span><span class="w"> </span><span class="k">is</span><span class="w"> </span><span class="n">specified</span><span class="p">,</span><span class="w"> </span><span class="n">a</span>
<span class="w"> </span><span class="n">dynamic</span><span class="o">-</span><span class="n">wallpaper</span><span class="o">.</span><span class="n">xml</span><span class="w"> </span><span class="n">file</span><span class="w"> </span><span class="n">will</span><span class="w"> </span><span class="n">be</span><span class="w"> </span><span class="n">generated</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">the</span>
<span class="w"> </span><span class="n">path</span><span class="w"> </span><span class="n">containing</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">pictures</span><span class="o">.</span><span class="w"> </span><span class="n">You</span><span class="w"> </span><span class="n">can</span><span class="w"> </span><span class="n">also</span><span class="w"> </span><span class="n">use</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="n">to</span>
<span class="w"> </span><span class="n">display</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">xml</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">the</span><span class="w"> </span><span class="n">stdout</span><span class="o">.</span>
<span class="w"> </span><span class="o">-</span><span class="n">t</span><span class="w"> </span><span class="n">TRANSITION_TIME</span><span class="p">,</span><span class="w"> </span><span class="o">--</span><span class="n">transition</span><span class="o">-</span><span class="n">time</span><span class="w"> </span><span class="n">TRANSITION_TIME</span>
<span class="w"> </span><span class="n">Time</span><span class="w"> </span><span class="p">(</span><span class="ow">in</span><span class="w"> </span><span class="n">seconds</span><span class="p">)</span><span class="w"> </span><span class="n">transitions</span><span class="w"> </span><span class="n">must</span><span class="w"> </span><span class="n">last</span><span class="w"> </span><span class="p">(</span><span class="n">default</span><span class="w"> </span><span class="n">value</span>
<span class="w"> </span><span class="k">is</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="n">seconds</span><span class="p">)</span>
<span class="w"> </span><span class="o">-</span><span class="n">d</span><span class="w"> </span><span class="n">DISPLAY_TIME</span><span class="p">,</span><span class="w"> </span><span class="o">--</span><span class="n">display</span><span class="o">-</span><span class="n">time</span><span class="w"> </span><span class="n">DISPLAY_TIME</span>
<span class="w"> </span><span class="n">Time</span><span class="w"> </span><span class="p">(</span><span class="ow">in</span><span class="w"> </span><span class="n">seconds</span><span class="p">)</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="n">picture</span><span class="w"> </span><span class="n">must</span><span class="w"> </span><span class="n">be</span><span class="w"> </span><span class="n">displayed</span><span class="o">.</span><span class="w"> </span><span class="n">Default</span>
<span class="w"> </span><span class="n">value</span><span class="w"> </span><span class="k">is</span><span class="w"> </span><span class="mi">900</span><span class="w"> </span><span class="p">(</span><span class="mi">15</span><span class="n">mn</span><span class="p">)</span>
<span class="w"> </span><span class="o">-</span><span class="n">s</span><span class="p">,</span><span class="w"> </span><span class="o">--</span><span class="n">set</span><span class="o">-</span><span class="n">background</span><span class="w"> </span><span class="s1">'''try to set the background using gnome-appearance-</span>
<span class="s1"> properties</span>
<span class="s1"> -b, --debug</span>
</code></pre></div>How to install NGINX + PHP 5.3 on FreeBSD.2010-10-10T00:00:00+02:002010-10-10T00:00:00+02:00tag:blog.notmyidea.org,2010-10-10:/how-to-install-nginx-php-53-on-freebsd.html
<ul>
<li>
<p>date<br> 2010-10-10</p>
</li>
<li>
<p>category<br> tech</p>
</li>
</ul>
<p>I’ve not managed so far to get completely rid of php, so here’s a simple
reminder about how to install php on <span class="caps">NGINX</span>, for FreeBSD. Nothing hard,
but that’s worse to have the piece of configuration somewhere !</p>
<div class="highlight"><pre><span></span><code><span class="err">#</span><span class="w"> </span><span class="nx">update</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">ports</span>
<span class="err">$</span><span class="w"> </span><span class="nx">portsnap</span><span class="w"> </span><span class="nx">fetch</span><span class="w"> </span><span class="nx">update …</span></code></pre></div>
<ul>
<li>
<p>date<br> 2010-10-10</p>
</li>
<li>
<p>category<br> tech</p>
</li>
</ul>
<p>I’ve not managed so far to get completely rid of php, so here’s a simple
reminder about how to install php on <span class="caps">NGINX</span>, for FreeBSD. Nothing hard,
but that’s worse to have the piece of configuration somewhere !</p>
<div class="highlight"><pre><span></span><code><span class="err">#</span><span class="w"> </span><span class="nx">update</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">ports</span>
<span class="err">$</span><span class="w"> </span><span class="nx">portsnap</span><span class="w"> </span><span class="nx">fetch</span><span class="w"> </span><span class="nx">update</span>
<span class="err">#</span><span class="w"> </span><span class="nx">install</span><span class="w"> </span><span class="nx">php5</span><span class="w"> </span><span class="nx">port</span>
<span class="err">$</span><span class="w"> </span><span class="nx">make</span><span class="w"> </span><span class="nx">config</span><span class="o">-</span><span class="nx">recursive</span><span class="w"> </span><span class="o">-</span><span class="nx">C</span><span class="w"> </span><span class="o">/</span><span class="nx">usr</span><span class="o">/</span><span class="nx">ports</span><span class="o">/</span><span class="nx">lang</span><span class="o">/</span><span class="nx">php5</span><span class="o">-</span><span class="nx">extensions</span>
<span class="err">$</span><span class="w"> </span><span class="nx">make</span><span class="w"> </span><span class="kn">package</span><span class="o">-</span><span class="nx">recursive</span><span class="w"> </span><span class="o">-</span><span class="nx">C</span><span class="w"> </span><span class="o">/</span><span class="nx">usr</span><span class="o">/</span><span class="nx">ports</span><span class="o">/</span><span class="nx">lang</span><span class="o">/</span><span class="nx">php5</span><span class="o">-</span><span class="nx">extensions</span>
<span class="err">#</span><span class="w"> </span><span class="nx">install</span><span class="w"> </span><span class="nx">nginx</span>
<span class="err">$</span><span class="w"> </span><span class="nx">make</span><span class="w"> </span><span class="nx">config</span><span class="o">-</span><span class="nx">recursive</span><span class="w"> </span><span class="o">-</span><span class="nx">C</span><span class="w"> </span><span class="o">/</span><span class="nx">usr</span><span class="o">/</span><span class="nx">ports</span><span class="o">/</span><span class="nx">www</span><span class="o">/</span><span class="nx">nginx</span><span class="o">-</span><span class="nx">devel</span>
<span class="err">$</span><span class="w"> </span><span class="nx">make</span><span class="w"> </span><span class="kn">package</span><span class="o">-</span><span class="nx">recursive</span><span class="w"> </span><span class="o">-</span><span class="nx">C</span><span class="w"> </span><span class="o">/</span><span class="nx">usr</span><span class="o">/</span><span class="nx">ports</span><span class="o">/</span><span class="nx">www</span><span class="o">/</span><span class="nx">nginx</span><span class="o">-</span><span class="nx">devel</span>
</code></pre></div>
<p>Now we have all the dependencies installed, we need to configure a bit
the server.</p>
<p>That’s a simple thing in fact, but it could be good to have something
that will work without effort over time.</p>
<p>Here’s a sample of my configuration:</p>
<div class="highlight"><pre><span></span><code><span class="nt">server</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="err">server_name</span><span class="w"> </span><span class="err">ndd</span><span class="p">;</span>
<span class="w"> </span><span class="err">set</span><span class="w"> </span><span class="err">$path</span><span class="w"> </span><span class="err">/path/to/your/files</span><span class="p">;</span>
<span class="w"> </span><span class="err">root</span><span class="w"> </span><span class="err">$path</span><span class="p">;</span>
<span class="w"> </span><span class="err">location</span><span class="w"> </span><span class="err">/</span><span class="w"> </span><span class="err">{</span>
<span class="w"> </span><span class="err">index</span><span class="w"> </span><span class="err">index.php</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nt">location</span><span class="w"> </span><span class="o">~*</span><span class="w"> </span><span class="o">^.+.(</span><span class="nt">jpg</span><span class="o">|</span><span class="nt">jpeg</span><span class="o">|</span><span class="nt">gif</span><span class="o">|</span><span class="nt">css</span><span class="o">|</span><span class="nt">png</span><span class="o">|</span><span class="nt">js</span><span class="o">|</span><span class="nt">ico</span><span class="o">|</span><span class="nt">xml</span><span class="o">)$</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="err">access_log</span><span class="w"> </span><span class="err">off</span><span class="p">;</span>
<span class="w"> </span><span class="err">expires</span><span class="w"> </span><span class="err">30d</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nt">location</span><span class="w"> </span><span class="o">~</span><span class="w"> </span><span class="p">.</span><span class="nc">php</span><span class="o">$</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="err">fastcgi_param</span><span class="w"> </span><span class="err">SCRIPT_FILENAME</span><span class="w"> </span><span class="err">$path$fastcgi_script_name</span><span class="p">;</span>
<span class="w"> </span><span class="err">fastcgi_pass</span><span class="w"> </span><span class="err">backend</span><span class="p">;</span>
<span class="w"> </span><span class="err">include</span><span class="w"> </span><span class="err">fastcgi_params</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="err">}</span>
<span class="nt">upstream</span><span class="w"> </span><span class="nt">backend</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="err">server</span><span class="w"> </span><span class="err">127.0.0.1:9000</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>And that’s it !</p>Pelican, a simple static blog generator in python2010-10-06T00:00:00+02:002010-10-06T00:00:00+02:00tag:blog.notmyidea.org,2010-10-06:/pelican-a-simple-static-blog-generator-in-python.html
<p>Those days, I’ve wrote a little python application to fit my blogging
needs. I’m an occasional blogger, a vim lover, I like restructured text
and DVCSes, so I’ve made a little tool that makes good use of all that.</p>
<p><a href="http://docs.getpelican.com">Pelican</a> (for calepin) is just a simple
tool …</p>
<p>Those days, I’ve wrote a little python application to fit my blogging
needs. I’m an occasional blogger, a vim lover, I like restructured text
and DVCSes, so I’ve made a little tool that makes good use of all that.</p>
<p><a href="http://docs.getpelican.com">Pelican</a> (for calepin) is just a simple
tool to generate your blog as static files, letting you using your
editor of choice (vim!). It’s easy to extend, and has a template
support (via jinja2).</p>
<p>I’ve made it to fit <em>my</em> needs. I hope it will fit yours, but maybe it
wont, and it have not be designed to feet everyone’s needs.</p>
<p>Need an example ? You’re looking at it ! This weblog is using pelican
to be generated, also for the atom feeds.</p>
<p>I’ve released it under <span class="caps">AGPL</span>, since I want all the modifications to be
profitable to all the users.</p>
<p>You can find a repository to fork at
<a href="https://github.com/getpelican/pelican/">https://github.com/getpelican/pelican/</a>. feel free to hack it !</p>
<p>If you just want to get started, use your installer of choice (pip,
easy_install, …) And then have a look to the help (pelican —help)</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>pelican
</code></pre></div>
<h2 id="usage">Usage</h2>
<p>Here’s a sample usage of pelican</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>pelican<span class="w"> </span>.
writing<span class="w"> </span>/home/alexis/projets/notmyidea.org/output/index.html
writing<span class="w"> </span>/home/alexis/projets/notmyidea.org/output/tags.html
writing<span class="w"> </span>/home/alexis/projets/notmyidea.org/output/categories.html
writing<span class="w"> </span>/home/alexis/projets/notmyidea.org/output/archives.html
writing<span class="w"> </span>/home/alexis/projets/notmyidea.org/output/category/python.html
writing
/home/alexis/projets/notmyidea.org/output/pelican-a-simple-static-blog-generator-in-python.html
Done<span class="w"> </span>!
</code></pre></div>
<p>You also can use the —help option for the command line to get more informations</p>
<div class="highlight"><pre><span></span><code><span class="nv">$pelican</span><span class="w"> </span>--help
usage:<span class="w"> </span>pelican<span class="w"> </span><span class="o">[</span>-h<span class="o">]</span><span class="w"> </span><span class="o">[</span>-t<span class="w"> </span>TEMPLATES<span class="o">]</span><span class="w"> </span><span class="o">[</span>-o<span class="w"> </span>OUTPUT<span class="o">]</span><span class="w"> </span><span class="o">[</span>-m<span class="w"> </span>MARKUP<span class="o">]</span><span class="w"> </span><span class="o">[</span>-s<span class="w"> </span>SETTINGS<span class="o">]</span><span class="w"> </span><span class="o">[</span>-b<span class="o">]</span>
<span class="w"> </span>path
A<span class="w"> </span>tool<span class="w"> </span>to<span class="w"> </span>generate<span class="w"> </span>a<span class="w"> </span>static<span class="w"> </span>blog,<span class="w"> </span>with<span class="w"> </span>restructured<span class="w"> </span>text<span class="w"> </span>input<span class="w"> </span>files.
positional<span class="w"> </span>arguments:
<span class="w"> </span>path<span class="w"> </span>Path<span class="w"> </span>where<span class="w"> </span>to<span class="w"> </span>find<span class="w"> </span>the<span class="w"> </span>content<span class="w"> </span>files<span class="w"> </span><span class="o">(</span>default<span class="w"> </span>is
<span class="w"> </span><span class="s2">"content"</span><span class="o">)</span>.
optional<span class="w"> </span>arguments:
<span class="w"> </span>-h,<span class="w"> </span>--help<span class="w"> </span>show<span class="w"> </span>this<span class="w"> </span><span class="nb">help</span><span class="w"> </span>message<span class="w"> </span>and<span class="w"> </span><span class="nb">exit</span>
<span class="w"> </span>-t<span class="w"> </span>TEMPLATES,<span class="w"> </span>--templates-path<span class="w"> </span>TEMPLATES
<span class="w"> </span>Path<span class="w"> </span>where<span class="w"> </span>to<span class="w"> </span>find<span class="w"> </span>the<span class="w"> </span>templates.<span class="w"> </span>If<span class="w"> </span>not<span class="w"> </span>specified,
<span class="w"> </span>will<span class="w"> </span>uses<span class="w"> </span>the<span class="w"> </span>ones<span class="w"> </span>included<span class="w"> </span>with<span class="w"> </span>pelican.
<span class="w"> </span>-o<span class="w"> </span>OUTPUT,<span class="w"> </span>--output<span class="w"> </span>OUTPUT
<span class="w"> </span>Where<span class="w"> </span>to<span class="w"> </span>output<span class="w"> </span>the<span class="w"> </span>generated<span class="w"> </span>files.<span class="w"> </span>If<span class="w"> </span>not<span class="w"> </span>specified,
<span class="w"> </span>a<span class="w"> </span>directory<span class="w"> </span>will<span class="w"> </span>be<span class="w"> </span>created,<span class="w"> </span>named<span class="w"> </span><span class="s2">"output"</span><span class="w"> </span><span class="k">in</span><span class="w"> </span>the
<span class="w"> </span>current<span class="w"> </span>path.
<span class="w"> </span>-m<span class="w"> </span>MARKUP,<span class="w"> </span>--markup<span class="w"> </span>MARKUP
<span class="w"> </span>the<span class="w"> </span>markup<span class="w"> </span>language<span class="w"> </span>to<span class="w"> </span>use.<span class="w"> </span>Currently<span class="w"> </span>only
<span class="w"> </span>ReSTreucturedtext<span class="w"> </span>is<span class="w"> </span>available.
<span class="w"> </span>-s<span class="w"> </span>SETTINGS,<span class="w"> </span>--settings<span class="w"> </span>SETTINGS
<span class="w"> </span>the<span class="w"> </span>settings<span class="w"> </span>of<span class="w"> </span>the<span class="w"> </span>application.<span class="w"> </span>Default<span class="w"> </span>to<span class="w"> </span>None.
<span class="w"> </span>-b,<span class="w"> </span>--debug
</code></pre></div>
<p>Enjoy :)</p>An amazing summer of code working on distutils22010-08-16T00:00:00+02:002010-08-16T00:00:00+02:00tag:blog.notmyidea.org,2010-08-16:/an-amazing-summer-of-code-working-on-distutils2.html
<p>The <a href="http://code.google.com/soc/">Google Summer of Code</a> I’ve spent
working on <a href="http://hg.python.org/distutils2/">distutils2</a> is over. It
was a really amazing experience, for many reasons.</p>
<p>First of all, we had a very good team, we were 5 students working on
distutils2: <a href="http://zubin71.wordpress.com">Zubin</a>,
<a href="http://wokslog.wordpress.com/">Éric</a>,
<a href="http://gsoc.djolonga.com/">Josip</a>,
<a href="http://konryd.blogspot.com/">Konrad</a> and me. In addition,
<a href="http://mouadino.blogspot.com/">Mouad</a> have worked on …</p>
<p>The <a href="http://code.google.com/soc/">Google Summer of Code</a> I’ve spent
working on <a href="http://hg.python.org/distutils2/">distutils2</a> is over. It
was a really amazing experience, for many reasons.</p>
<p>First of all, we had a very good team, we were 5 students working on
distutils2: <a href="http://zubin71.wordpress.com">Zubin</a>,
<a href="http://wokslog.wordpress.com/">Éric</a>,
<a href="http://gsoc.djolonga.com/">Josip</a>,
<a href="http://konryd.blogspot.com/">Konrad</a> and me. In addition,
<a href="http://mouadino.blogspot.com/">Mouad</a> have worked on the PyPI testing
infrastructure. You could find what each person have done on <a href="http://bitbucket.org/tarek/distutils2/wiki/GSoC_2010_teams">the wiki
page of
distutils2</a>.</p>
<p>We were in contact with each others really often, helping us when
possible (in #distutils), and were continuously aware of the state of
the work of each participant. This, in my opinion, have bring us in a
good shape.</p>
<p>Then, I’ve learned a lot. Python packaging was completely new to me at
the time of the GSoC start, and I was pretty unfamiliar with python good
practices too, as I’ve been introducing myself to python in the late 2009.</p>
<p>I’ve recently looked at some python code I wrote just three months ago,
and I was amazed to think about many improvements to made on it. I guess
this is a good indicator of the path I’ve traveled since I wrote it.</p>
<p>This summer was awesome because I’ve learned about python good
practices, now having some strong
<a href="http://mercurial.selenic.com/">mercurial</a> knowledge, and I’ve seen a
little how the python community works.</p>
<p>Then, I would like to say a big thanks to all the mentors that have
hanged around while needed, on <span class="caps">IRC</span> or via mail, and especially my mentor
for this summer, <a href="http://tarek.ziade.org">Tarek Ziadé</a>.</p>
<p>Thanks a lot for your motivation, your leadership and your cheerfulness,
even with a new-born and a new work!</p>
<h2 id="why">Why ?</h2>
<p>I wanted to work on python packaging because, as the time pass, we were
having a sort of complex tools in this field. Each one wanted to add
features to distutils, but not in a standard way.</p>
<p>Now, we have PEPs that describes some format we agreed on (see <span class="caps">PEP</span> 345),
and we wanted to have a tool on which users can base their code on,
that’s <a href="http://hg.python.org/distutils2/">distutils2</a>.</p>
<h2 id="my-job">My job</h2>
<p>I had to provide a way to crawl the PyPI indexes in a simple way, and do
some installation / uninstallation scripts.</p>
<p>All the work done is available in <a href="http://bitbucket.org/ametaireau/distutils2/">my bitbucket
repository</a>.</p>
<h3 id="crawling-the-pypi-indexes">Crawling the PyPI indexes</h3>
<p>There are two ways of requesting informations from the indexes: using
the “simple” index, that is a kind of <span class="caps">REST</span> index, and using <span class="caps">XML</span>-<span class="caps">RPC</span>.</p>
<p>I’ve done the two implementations, and a high level <span class="caps">API</span> to query those
twos. Basically, this supports the mirroring infrastructure defined in
<span class="caps">PEP</span> 381. So far, the work I’ve done is gonna be used in pip (they’ve
basically copy/paste the code, but this will change as soon as we get
something completely stable for distutils2), and that’s a good news, as
it was the main reason for what I’ve done that.</p>
<p>I’ve tried to have an unified <span class="caps">API</span> for the clients, to switch from one to
another implementation easily. I’m already thinking of adding others
crawlers to this stuff, and it was made to be extensible.</p>
<p>If you want to get more informations about the crawlers/PyPI clients,
please refer to the distutils2 documentation, especially <a href="http://distutils2.notmyidea.org/library/distutils2.index.html">the pages
about
indexes</a>.</p>
<p>You can find the changes I made about this in the
<a href="http://hg.python.org/distutils2/">distutils2</a> source code .</p>
<h3 id="installation-uninstallation-scripts">Installation / Uninstallation scripts</h3>
<p>Next step was to think about an installation script, and an uninstaller.
I’ve not done the uninstaller part, and it’s a smart part, as it’s
basically removing some files from the system, so I’ll probably do it in
a near future.</p>
<p><a href="http://hg.python.org/distutils2/">distutils2</a> provides a way to install
distributions, and to handle dependencies between releases. For now,
this support is only about the last version of the <span class="caps">METADATA</span> (1.2) (See,
the <span class="caps">PEP</span> 345), but I’m working on a compatibility layer for the old
metadata, and for the informations provided via <span class="caps">PIP</span> requires.txt, for instance.</p>
<h3 id="extra-work">Extra work</h3>
<p>Also, I’ve done some extra work. this includes:</p>
<ul>
<li>working on the <span class="caps">PEP</span> 345, and having some discussion about it (about
the names of some fields).</li>
<li>writing a PyPI server mock, useful for tests. you can find more
information about it on the
<a href="http://distutils.notmyidea.org">documentation</a>.</li>
</ul>
<h2 id="futures-plans">Futures plans</h2>
<p>As I said, I’ve enjoyed working on distutils2, and the people I’ve met
here are really pleasant to work with. So I <em>want</em> to continue
contributing on python, and especially on python packaging, because
there is still a lot of things to do in this scope, to get something
really usable.</p>
<p>I’m not plainly satisfied by the work I’ve done, so I’ll probably tweak
it a bit: the installer part is not yet completely finished, and I want
to add support for a real
<a href="http://en.wikipedia.org/wiki/Representational_State_Transfer"><span class="caps">REST</span></a>
index in the future.</p>
<p>We’ll talk again of this in the next months, probably, but we definitely
need a real
<a href="http://en.wikipedia.org/wiki/Representational_State_Transfer"><span class="caps">REST</span></a> <span class="caps">API</span>
for <a href="http://pypi.python.org">PyPI</a>, as the “simple” index <em>is</em> an ugly
hack, in my opinion. I’ll work on a serious proposition about this,
maybe involving <a href="http://couchdb.org">CouchDB</a>, as it seems to be a good
option for what we want here.</p>
<h2 id="issues">Issues</h2>
<p>I’ve encountered some issues during this summer. The main one is that’s
hard to work remotely, especially being in the same room that we live,
with others. I like to just think about a project with other people, a
paper and a pencil, no computers. This have been not so possible at the
start of the project, as I needed to read a lot of code to understand
the codebase, and then to read/write emails.</p>
<p>I’ve finally managed to work in an office, so good point for home/office separation.</p>
<p>I’d not planned there will be so a high number of emails to read, in
order to follow what’s up in the python world, and be a part of the
community seems to takes some times to read/write emails, especially for
those (like me) that arent so confortable with english (but this had
brought me some english fu !).</p>
<h2 id="thanks">Thanks !</h2>
<p>A big thanks to <a href="http://www.graine-libre.fr/">Graine Libre</a> and <a href="http://www.makina-corpus.com/">Makina
Corpus</a>, which has offered me to come
into their offices from time to time, to share they cheerfulness ! Many
thanks too to the Google Summer of Code program for setting up such an
initiative. If you’re a student, if you’re interested about <span class="caps">FOSS</span>, dont
hesitate any second, it’s a really good opportunity to work on
interesting projects!</p>Sprinting on distutils2 in Tours2010-07-10T00:00:00+02:002010-07-10T00:00:00+02:00tag:blog.notmyidea.org,2010-07-10:/sprinting-on-distutils2-in-tours.html
<ul>
<li>
<p>date<br> 2010-07-06</p>
</li>
<li>
<p>category<br> tech</p>
</li>
</ul>
<p>Yesterday, as I was traveling to Tours, I’ve took some time to visit
Éric, another student who’s working on distutils2 this summer, as a
part of the GSoC. Basically, it was to take a drink, discuss a bit about
distutils2, our respective tasks and …</p>
<ul>
<li>
<p>date<br> 2010-07-06</p>
</li>
<li>
<p>category<br> tech</p>
</li>
</ul>
<p>Yesterday, as I was traveling to Tours, I’ve took some time to visit
Éric, another student who’s working on distutils2 this summer, as a
part of the GSoC. Basically, it was to take a drink, discuss a bit about
distutils2, our respective tasks and general feelings, and to put a face
on a pseudonym. I’d really enjoyed this time, because Éric knows a lot
of things about mercurial and python good practices, and I’m eager to
learn about those. So, we have discussed about things, have not wrote so
much code, but have some things to propose so far, about documentation,
and I also provides here some bribes of conversations we had.</p>
<h2 id="documentation">Documentation</h2>
<p>While writing the PyPI simple index crawler documentation, I realized
that we miss some structure, or how-to about the documentation. Yep, you
read well. We lack documentation on how to make documentation. Heh.
We’re missing some rules to follow, and this lead to a not-so-structured
final documentation. We probably target three type of publics, and we
can split the documentation regarding those:</p>
<ul>
<li><strong>Packagers</strong> who want to distribute their softwares.</li>
<li><strong>End users</strong> who need to understand how to use end user commands,
like the installer/uninstaller</li>
<li><strong>packaging coders</strong> who <em>use</em> distutils2, as a base for building a
package manager.</li>
</ul>
<p>We also need to discuss about a pattern to follow while writing
documentation. How many parts do we need ? Where to put the <span class="caps">API</span>
description ? etc. That’s maybe seems to be not so important, but I
guess the readers would appreciate to have the same structure all along
distutils2 documentation.</p>
<h2 id="mercurial">Mercurial</h2>
<p>I’m really <em>not</em> a mercurial power user. I use it on daily basis, but I
lack of basic knowledge about it. Big thanks Éric for sharing yours with
me, you’re of a great help. We have talked about some mercurial
extensions that seems to make the life simpler, while used the right
way. I’ve not used them so far, so consider this as a personal note.</p>
<ul>
<li>hg histedit, to edit the history</li>
<li>hg crecord, to select the changes to commit</li>
</ul>
<p>We have spent some time to review a merge I made sunday, to re-merge it,
and commit the changes as a new changeset. Awesome. These things make me
say I <strong>need</strong> to read <a href="http://hgbook.red-bean.com/read/">the hg book</a>,
and will do as soon as I got some spare time: mercurial seems to be
simply great. So … Great. I’m a powerful merger now !</p>
<h2 id="on-using-tools">On using tools</h2>
<p>Because we <em>also</em> are <em>hackers</em>, we have shared a bit our ways to code,
the tools we use, etc. Both of us were using vim, and I’ve discovered
vimdiff and hgtk, which will completely change the way I navigate into
the mercurial history. We aren’t “power users”, so we have learned from
each other about vim tips. You can find <a href="http://github.com/ametaireau/dotfiles">my dotfiles on
github</a>, if it could help.
They’re not perfect, and not intended to be, because changing all the
time, as I learn. Don’t hesitate to have a look, and to propose
enhancements if you have !</p>
<h2 id="on-being-pythonic">On being pythonic</h2>
<p>My background as an old Java user disserves me so far, as the paradigms
are not the same while coding in python. Hard to find the more pythonic
way to do, and sometimes hard to unlearn my way to think about software
engineering. Well, it seems that the only solution is to read code, and
to re-read import this from times to times ! <a href="http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html">Coding like a
pythonista</a>
seems to be a must-read, so, I know what to do.</p>
<h2 id="conclusion">Conclusion</h2>
<p>It was really great. Next time, we’ll need to focus a bit more on
distutils2, and to have a bullet list of things to do, but days like
this one are opportunities to catch ! We’ll probably do another sprint
in a few weeks, stay tuned !</p>Introducing the distutils2 index crawlers2010-07-06T00:00:00+02:002010-07-06T00:00:00+02:00tag:blog.notmyidea.org,2010-07-06:/introducing-the-distutils2-index-crawlers.html
<p>I’m working for about a month for distutils2, even if I was being a bit
busy (as I had some class courses and exams to work on)</p>
<p>I’ll try do sum-up my general feelings here, and the work I’ve made so
far. You can also find, if …</p>
<p>I’m working for about a month for distutils2, even if I was being a bit
busy (as I had some class courses and exams to work on)</p>
<p>I’ll try do sum-up my general feelings here, and the work I’ve made so
far. You can also find, if you’re interested, my weekly summaries in <a href="http://wiki.notmyidea.org/distutils2_schedule">a
dedicated wiki page</a>.</p>
<h2 id="general-feelings">General feelings</h2>
<p>First, and it’s a really important point, the GSoC is going very well,
for me as for other students, at least from my perspective. It’s a
pleasure to work with such enthusiast people, as this make the global
atmosphere very pleasant to live.</p>
<p>First of all, I’ve spent time to read the existing codebase, and to
understand what we’re going to do, and what’s the rationale to do so.</p>
<p>It’s really clear for me now: what we’re building is the foundations of
a packaging infrastructure in python. The fact is that many projects
co-exists, and comes all with their good concepts. Distutils2 tries to
take the interesting parts of all, and to provide it in the python
standard libs, respecting the recently written <span class="caps">PEP</span> about packaging.</p>
<p>With distutils2, it will be simpler to make “things” compatible. So if
you think about a new way to deal with distributions and packaging in
python, you can use the Distutils2 APIs to do so.</p>
<h2 id="tasks">Tasks</h2>
<p>My main task while working on distutils2 is to provide an installation
and an un-installation command, as described in <span class="caps">PEP</span> 376. For this, I
first need to get informations about the existing distributions (what’s
their version, name, metadata, dependencies, etc.)</p>
<p>The main index, you probably know and use, is PyPI. You can access it at
<a href="http://pypi.python.org">http://pypi.python.org</a>.</p>
<h2 id="pypi-index-crawling">PyPI index crawling</h2>
<p>There is two ways to get these informations from PyPI: using the simple
<span class="caps">API</span>, or via xml-rpc calls.</p>
<p>A goal was to use the version specifiers defined
in<a href="http://www.python.org/dev/peps/pep-0345/"><span class="caps">PEP</span> 345</a> and to provides a
way to sort the grabbed distributions depending our needs, to pick the
version we want/need.</p>
<h3 id="using-the-simple-api">Using the simple <span class="caps">API</span></h3>
<p>The simple <span class="caps">API</span> is composed of <span class="caps">HTML</span> pages you can access at
<a href="http://pypi.python.org/simple/">http://pypi.python.org/simple/</a>.</p>
<p>Distribute and Setuptools already provides a crawler for that, but it
deals with their internal mechanisms, and I found that the code was not
so clear as I want, that’s why I’ve preferred to pick up the good ideas,
and some implementation details, plus re-thinking the global architecture.</p>
<p>The rules are simple: each project have a dedicated page, which allows
us to get informations about:</p>
<ul>
<li>the distribution download locations (for some versions)</li>
<li>homepage links</li>
<li>some other useful informations, as the bugtracker address, for instance.</li>
</ul>
<p>If you want to find all the distributions of the “EggsAndSpam” project,
you could do the following (do not take so attention to the names here,
as the <span class="caps">API</span> will probably change a bit):</p>
<div class="highlight"><pre><span></span><code><span class="o">>>></span> <span class="n">index</span> <span class="o">=</span> <span class="n">SimpleIndex</span><span class="p">()</span>
<span class="o">>>></span> <span class="n">index</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">"EggsAndSpam"</span><span class="p">)</span>
<span class="p">[</span><span class="n">EggsAndSpam</span> <span class="mf">1.1</span><span class="p">,</span> <span class="n">EggsAndSpam</span> <span class="mf">1.2</span><span class="p">,</span> <span class="n">EggsAndSpam</span> <span class="mf">1.3</span><span class="p">]</span>
</code></pre></div>
<p>We also could use version specifiers:</p>
<div class="highlight"><pre><span></span><code><span class="o">>>></span> <span class="n">index</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">"EggsAndSpam (< =1.2)"</span><span class="p">)</span>
<span class="p">[</span><span class="n">EggsAndSpam</span> <span class="mf">1.1</span><span class="p">,</span> <span class="n">EggsAndSpam</span> <span class="mf">1.2</span><span class="p">]</span>
</code></pre></div>
<p>Internally, what’s done here is the following:</p>
<ul>
<li>it process the <a href="http://pypi.python.org/simple/FooBar/">http://pypi.python.org/simple/FooBar/</a> page,
searching for download URLs.</li>
<li>for each found distribution download <span class="caps">URL</span>, it creates an object,
containing informations about the project name, the version and the
<span class="caps">URL</span> where the archive remains.</li>
<li>it sort the found distributions, using version numbers. The default
behavior here is to prefer source distributions (over binary ones),
and to rely on the last “final” distribution (rather than beta,
alpha etc. ones)</li>
</ul>
<p>So, nothing hard or difficult here.</p>
<p>We provides a bunch of other features, like relying on the new PyPI
mirroring infrastructure or filter the found distributions by some
criterias. If you’re curious, please browse the <a href="http://distutils2.notmyidea.org/">distutils2
documentation</a>.</p>
<h3 id="using-xml-rpc">Using xml-rpc</h3>
<p>We also can make some xmlrpc calls to retreive informations from PyPI.
It’s a really more reliable way to get informations from from the index
(as it’s just the index that provides the informations), but cost
processes on the PyPI distant server.</p>
<p>For now, this way of querying the xmlrpc client is not available on
Distutils2, as I’m working on it. The main pieces are already present
(I’ll reuse some work I’ve made from the SimpleIndex querying, and <a href="http://github.com/ametaireau/pypiclient">some
code already set up</a>), what I
need to do is to provide a xml-rpc PyPI mock server, and that’s on what
I’m actually working on.</p>
<h2 id="processes">Processes</h2>
<p>For now, I’m trying to follow the “documentation, then test, then code”
path, and that seems to be really needed while working with a community.
Code is hard to read/understand, compared to documentation, and it’s
easier to change.</p>
<p>While writing the simple index crawling work, I must have done this to
avoid some changes on the <span class="caps">API</span>, and some loss of time.</p>
<p>Also, I’ve set up <a href="http://wiki.notmyidea.org/distutils2_schedule">a
schedule</a>, and the goal
is to be sure everything will be ready in time, for the end of the
summer. (And now, I need to learn to follow schedules …)</p>Use Restructured Text (ReST) to power your presentations2010-06-25T00:00:00+02:002010-06-25T00:00:00+02:00tag:blog.notmyidea.org,2010-06-25:/use-restructured-text-rest-to-power-your-presentations.html
<ul>
<li>
<p>date<br> 2010-06-25</p>
</li>
<li>
<p>category<br> tech</p>
</li>
</ul>
<p>Wednesday, we give a presentation, with some friends, about the CouchDB
Database, to <a href="http://www.toulibre.org">the Toulouse local <span class="caps">LUG</span></a>. Thanks a
lot to all the presents for being there, it was a pleasure to talk about
this topic with you. Too bad the season is over now an …</p>
<ul>
<li>
<p>date<br> 2010-06-25</p>
</li>
<li>
<p>category<br> tech</p>
</li>
</ul>
<p>Wednesday, we give a presentation, with some friends, about the CouchDB
Database, to <a href="http://www.toulibre.org">the Toulouse local <span class="caps">LUG</span></a>. Thanks a
lot to all the presents for being there, it was a pleasure to talk about
this topic with you. Too bad the season is over now an I quit Toulouse
next year.</p>
<p>During our brainstorming about the topic, we used some paper, and we
wanted to make a presentation the simpler way. First thing that come to
my mind was using <a href="http://docutils.sourceforge.net/rst.html">restructured
text</a>, so I’ve wrote a simple
file containing our different bullet points. In fact, there is quite
nothing to do then, to have a working presentation.</p>
<p>So far, I’ve used <a href="http://code.google.com/p/rst2pdf/">the rst2pdf
program</a>, and a simple template, to
generate output. It’s probably simple to have similar results using
latex + beamer, I’ll try this next time, but as I’m not familiar with
latex syntax, restructured text was a great option.</p>
<p>Here are <a href="http://files.lolnet.org/alexis/rst-presentations/couchdb/couchdb.pdf">the final <span class="caps">PDF</span>
output</a>,
<a href="http://files.lolnet.org/alexis/rst-presentations/couchdb/couchdb.rst">Rhe ReST
source</a>,
<a href="http://files.lolnet.org/alexis/rst-presentations/slides.style">the theme
used</a>,
and the command line to generate the <span class="caps">PDF</span>:</p>
<div class="highlight"><pre><span></span><code>rst2pdf couchdb.rst -b1 -s ../slides.style
</code></pre></div>first week working on distutils22010-06-04T00:00:00+02:002010-06-04T00:00:00+02:00tag:blog.notmyidea.org,2010-06-04:/first-week-working-on-distutils2.html
<p>As I’ve been working on <a href="http://hg.python.org/distutils2/">Distutils2</a>
during the past week, taking part of the
<a href="http://code.google.com/intl/fr/soc/"><span class="caps">GSOC</span></a> program, here is a short
summary of what I’ve done so far.</p>
<p>As my courses are not over yet, I’ve not worked as much as I wanted, and
this will continues until …</p>
<p>As I’ve been working on <a href="http://hg.python.org/distutils2/">Distutils2</a>
during the past week, taking part of the
<a href="http://code.google.com/intl/fr/soc/"><span class="caps">GSOC</span></a> program, here is a short
summary of what I’ve done so far.</p>
<p>As my courses are not over yet, I’ve not worked as much as I wanted, and
this will continues until the end of June. My main tasks are about
making installation and uninstallation commands, to have a simple way to
install distributions via
<a href="http://hg.python.org/distutils2/">Distutils2</a>.</p>
<p>To do this, we need to rely on informations provided by the Python
Package Index (<a href="http://pypi.python.org/">PyPI</a>), and there is at least
two ways to retreive informations from here: <span class="caps">XML</span>-<span class="caps">RPC</span> and the “simple”
<span class="caps">API</span>.</p>
<p>So, I’ve been working on porting some
<a href="http://bitbucket.org/tarek/distribute/">Distribute</a> related stuff to
<a href="http://hg.python.org/distutils2/">Distutils2</a>, cutting off all non
distutils’ things, as we do not want to depend from Distribute’s
internals. My main work has been about reading the whole code, writing
tests about this and making those tests possible.</p>
<p>In fact, there was a need of a pypi mocked server, and, after reading
and introducing myself to the distutils behaviors and code, I’ve taken
some time to improve the work <a href="http://bitbucket.org/konrad">Konrad</a>
makes about this mock.</p>
<h2 id="a-pypi-server-mock">A PyPI Server mock</h2>
<p>The mock is embeded in a thread, to make it available during the tests,
in a non blocking way. We first used <a href="http://wsgi.org"><span class="caps">WSGI</span></a> and
<a href="http://docs.python.org/library/wsgiref.html">wsgiref</a> in order control
what to serve, and to log the requests made to the server, but finally
realised that <a href="http://docs.python.org/library/wsgiref.html">wsgiref</a> is
not python 2.4 compatible (and we <em>need</em> to be python 2.4 compatible in Distutils2).</p>
<p>So, we switched to
<a href="http://docs.python.org/library/basehttpserver.html">BaseHTTPServer</a> and
<a href="http://docs.python.org/library/simplehttpserver.html">SimpleHTTPServer</a>,
and updated our tests accordingly. It’s been an opportunity to realize
that <a href="http://wsgi.org"><span class="caps">WSGI</span></a> has been a great step forward for making
<span class="caps">HTTP</span> servers, and expose a really simplest way to discuss with <span class="caps">HTTP</span> !</p>
<p>You can find <a href="http://bitbucket.org/ametaireau/distutils2/changesets">the modifications I
made</a>, and the
<a href="http://bitbucket.org/ametaireau/distutils2/src/tip/docs/source/test_framework.rst">related
docs</a>
about this on <a href="http://bitbucket.org/ametaireau/distutils2/">my bitbucket distutils2
clone</a>.</p>
<h2 id="the-pypi-simple-api">The PyPI Simple <span class="caps">API</span></h2>
<p>So, back to the main problematic: make a python library to access and
request information stored on PyPI, via the simple <span class="caps">API</span>. As I said, I’ve
just grabbed the work made from
<a href="http://bitbucket.org/tarek/distribute/">Distribute</a>, and played a bit
with, in order to view what are the different use cases, and started to
write the related tests.</p>
<h2 id="the-work-to-come">The work to come</h2>
<p>So, once all use cases covered with tests, I’ll rewrite a bit the
grabbed code, and do some software design work (to not expose all things
as privates methods, have a clear <span class="caps">API</span>, and other things like this), then
update the tests accordingly and write a documentation to make this clear.</p>
<p>Next step is to a little client, as I’ve <a href="http://github.com/ametaireau/pypiclient">already started
here</a> I’ll take you updated !</p>A Distutils2 GSoC2010-05-01T00:00:00+02:002010-05-01T00:00:00+02:00tag:blog.notmyidea.org,2010-05-01:/a-distutils2-gsoc.html
<p><span class="caps">WOW</span>. I’ve been accepted to be a part of the <a href="http://code.google.com/intl/fr/soc/">Google Summer Of
Code</a> program, and will work on
<a href="http://python.org/">python</a> <a href="http://hg.python.org/distutils2/">distutils2</a>, with <a href="http://pygsoc.wordpress.com/">a</a> <a href="http://konryd.blogspot.com/">lot</a> <a href="http://ziade.org/">of</a> (intersting !) <a href="http://zubin71.wordpress.com/">people</a>.</p>
<blockquote>
<p>So, it’s about building the successor of Distutils2, ie. “the python
package manager”. Today, there is too many ways to package …</p></blockquote>
<p><span class="caps">WOW</span>. I’ve been accepted to be a part of the <a href="http://code.google.com/intl/fr/soc/">Google Summer Of
Code</a> program, and will work on
<a href="http://python.org/">python</a> <a href="http://hg.python.org/distutils2/">distutils2</a>, with <a href="http://pygsoc.wordpress.com/">a</a> <a href="http://konryd.blogspot.com/">lot</a> <a href="http://ziade.org/">of</a> (intersting !) <a href="http://zubin71.wordpress.com/">people</a>.</p>
<blockquote>
<p>So, it’s about building the successor of Distutils2, ie. “the python
package manager”. Today, there is too many ways to package a python
application (pip, setuptools, distribute, distutils, etc.) so there is
a huge effort to make in order to make all this packaging stuff
interoperable, as pointed out by
the <a href="http://www.python.org/dev/peps/pep-0376/"><span class="caps">PEP</span> 376</a>.</p>
</blockquote>
<p>In more details, I’m going to work on the Installer / Uninstaller features of Distutils2, and on a PyPI <span class="caps">XML</span>-<span class="caps">RPC</span> client for distutils2. Here are the already defined tasks:</p>
<ul>
<li>Implement Distutils2 APIs described in <span class="caps">PEP</span> 376.</li>
<li>Add the uninstall command.</li>
<li>think about a basic installer / uninstaller script. (with deps) —
similar to pip/easy_install</li>
<li>in a pypi subpackage;</li>
<li>Integrate a module similar to setuptools’ package_index’</li>
<li>PyPI <span class="caps">XML</span>-<span class="caps">RPC</span> client for distutils 2:
<a href="http://bugs.python.org/issue8190">http://bugs.python.org/issue8190</a></li>
</ul>
<p>As I’m relatively new to python, I’ll need some extra work in order to apply all good practice, among other things that can make a developper-life joyful. I’ll post here, each week, my advancement, and my tought about python and especialy python packaging world.</p>Python ? go !2009-12-17T00:00:00+01:002009-12-17T00:00:00+01:00tag:blog.notmyidea.org,2009-12-17:/python-go.html
<p>Cela fait maintenant un peu plus d’un mois que je travaille sur un
projet en <a href="http://www.djangoproject.org">django</a>, et que,
nécessairement, je me forme à <a href="http://python.org/">Python</a>. Je prends
un plaisir non dissimulé à découvrir ce langage (et à l’utiliser), qui
ne cesse de me surprendre. Les premiers mots qui me …</p>
<p>Cela fait maintenant un peu plus d’un mois que je travaille sur un
projet en <a href="http://www.djangoproject.org">django</a>, et que,
nécessairement, je me forme à <a href="http://python.org/">Python</a>. Je prends
un plaisir non dissimulé à découvrir ce langage (et à l’utiliser), qui
ne cesse de me surprendre. Les premiers mots qui me viennent à l’esprit
à propos de Python, sont “logique” et “simple”. Et pourtant puissant
pour autant. Je ne manque d’ailleurs pas une occasion pour faire un peu
d’<em>évangélisation</em> auprès des quelques personnes qui veulent bien m’écouter.</p>
<h2 id="the-zen-of-python">The Zen of Python</h2>
<p>Avant toute autre chose, je pense utile de citer Tim Peters, et <a href="http://www.python.org/dev/peps/pep-0020/">le
<span class="caps">PEP20</span></a>, qui constituent une
très bonne introduction au langage, qui prends la forme d’un <em>easter
egg</em> présent dans python</p>
<div class="highlight"><pre><span></span><code>>>><span class="w"> </span>import<span class="w"> </span>this
The<span class="w"> </span>Zen<span class="w"> </span>of<span class="w"> </span>Python,<span class="w"> </span>by<span class="w"> </span>Tim<span class="w"> </span>Peters
Beautiful<span class="w"> </span>is<span class="w"> </span>better<span class="w"> </span>than<span class="w"> </span>ugly.
Explicit<span class="w"> </span>is<span class="w"> </span>better<span class="w"> </span>than<span class="w"> </span>implicit.
Simple<span class="w"> </span>is<span class="w"> </span>better<span class="w"> </span>than<span class="w"> </span>complex.
Complex<span class="w"> </span>is<span class="w"> </span>better<span class="w"> </span>than<span class="w"> </span>complicated.
Flat<span class="w"> </span>is<span class="w"> </span>better<span class="w"> </span>than<span class="w"> </span>nested.
Sparse<span class="w"> </span>is<span class="w"> </span>better<span class="w"> </span>than<span class="w"> </span>dense.
Readability<span class="w"> </span>counts.
Special<span class="w"> </span>cases<span class="w"> </span>aren<span class="s1">'t special enough to break the rules.</span>
<span class="s1">Although practicality beats purity.</span>
<span class="s1">Errors should never pass silently.</span>
<span class="s1">Unless explicitly silenced.</span>
<span class="s1">In the face of ambiguity, refuse the temptation to guess.</span>
<span class="s1">There should be one-- and preferably only one --obvious way to do it.</span>
<span class="s1">Although that way may not be obvious at first unless you'</span>re<span class="w"> </span>Dutch.
Now<span class="w"> </span>is<span class="w"> </span>better<span class="w"> </span>than<span class="w"> </span>never.
Although<span class="w"> </span>never<span class="w"> </span>is<span class="w"> </span>often<span class="w"> </span>better<span class="w"> </span>than<span class="w"> </span>*right*<span class="w"> </span>now.
If<span class="w"> </span>the<span class="w"> </span>implementation<span class="w"> </span>is<span class="w"> </span>hard<span class="w"> </span>to<span class="w"> </span>explain,<span class="w"> </span>it<span class="s1">'s a bad idea.</span>
<span class="s1">If the implementation is easy to explain, it may be a good idea.</span>
<span class="s1">Namespaces are one honking great idea -- let'</span>s<span class="w"> </span><span class="k">do</span><span class="w"> </span>more<span class="w"> </span>of<span class="w"> </span>those!
</code></pre></div>
<p>J’ai la vague impression que c’est ce que j’ai toujours cherché à faire
en <span class="caps">PHP</span>, et particulièrement dans <a href="http://www.spiral-project.org">le framework
Spiral</a>, mais en ajoutant ces concepts
dans une sur-couche au langage. Ici, c’est directement de <em>l’esprit</em> de
python qu’il s’agit, ce qui signifie que la plupart des bibliothèques
python suivent ces concepts. Elle est pas belle la vie ?</p>
<h2 id="comment-commencer-et-par-ou">Comment commencer, et par ou ?</h2>
<p>Pour ma part, j’ai commencé par la lecture de quelques livres et
articles intéressants, qui constituent une bonne entrée en matière sur
le sujet (La liste n’est bien évidemment pas exhaustive et vos
commentaires sont les bienvenus) :</p>
<ul>
<li><a href="http://diveintopython.adrahon.org/">Dive into python</a></li>
<li><a href="http://www.swaroopch.com/notes/Python_fr:Table_des_Matières">A byte of python</a></li>
<li><a href="http://www.amazon.fr/Python-Petit-guide-lusage-développeur/dp/2100508830">Python: petit guide à l’usage du développeur
agile</a>
de <a href="http://tarekziade.wordpress.com/">Tarek Ziadé</a></li>
<li><a href="http://docs.python.org/index.html">La documentation officielle
python</a>, bien sûr !</li>
<li><a href="http://video.pycon.fr/videos/pycon-fr-2009/">Les vidéos du
pyconfr 2009</a>!</li>
<li>Un peu de temps, et une console python ouverte :)</li>
</ul>
<p>J’essaye par ailleurs de partager au maximum les ressources que je
trouve de temps à autres, que ce soit <a href="http://www.twitter.com/ametaireau">via
twitter</a> ou <a href="http://delicious.com/ametaireau">via mon compte
delicious</a>. Allez jeter un œil <a href="http://delicious.com/ametaireau/python">au tag
python</a> sur mon profil, peut
être que vous trouverez des choses intéressantes, qui sait!</p>
<h2 id="un-python-sexy">Un python sexy</h2>
<p>Quelques fonctionnalités qui devraient vous mettre l’eau à la bouche:</p>
<ul>
<li><a href="http://docs.python.org/library/stdtypes.html#comparisons">Le chaînage des opérateurs de
comparaison</a>
est possible (a\<b \<c dans une condition)</li>
<li>Assignation de valeurs multiples (il est possible de faire a,b,c =
1,2,3 par exemple)</li>
<li><a href="http://docs.python.org/tutorial/datastructures.html">Les listes</a>
sont simples à manipuler !</li>
<li>Les <a href="http://docs.python.org/tutorial/datastructures.html#list-comprehensions">list
comprehension</a>,
ou comment faire des opérations complexes sur les listes, de manière simple.</li>
<li>Les
<a href="http://docs.python.org/library/doctest.html?highlight=doctest">doctests</a>:
ou comment faire des tests directement dans la documentation de vos
classes, tout en la documentant avec de vrais exemples.</li>
<li>Les
<a href="http://www.python.org/doc/essays/metaclasses/meta-vladimir.txt">métaclasses</a>,
ou comment contrôler la manière dont les classes se construisent</li>
<li>Python est <a href="http://wiki.python.org/moin/Why%20is%20Python%20a%20dynamic%20language%20and%20also%20a%20strongly%20typed%20language">un langage à typage fort
dynamique</a>:
c’est ce qui m’agaçait avec <span class="caps">PHP</span> qui est un langage à typage faible dynamique.</li>
</ul>
<p>Cous pouvez également aller regarder <a href="http://video.pycon.fr/videos/free/53/">l’atelier donné par Victor Stinner
durant le Pyconfr 09</a>. Have fun !</p>