Mariano Guerra: Creemos en la Web

Este es el primero de lo que espero sera una serie de artículos sobre como aprender a hacer paginas web para personas sin ningún conocimiento previo de tecnología o programación.

Si sabes de tecnología recomendaselo a la mayor cantidad de personas posible, no sabemos cuantos grandes diseñadores y programadores web se encuentran escondidos por ahí.

Si te recomendaron esto y pensás que no es para vos, la cuestión es que si es para vos, y si algo es confuso no es tu culpa, es miá, así que contactame y decime que parte no esta clara así mejoramos esta guiá para todos.

Este articulo es una introducción a la herramienta que vamos a usar para los siguientes ejemplos, un proyecto llamado Mozilla Thimble, el cual facilita el proceso de crear, compartir y remixar paginas de otros.

Para los que prefieren ver videos, acá hay uno con el mismo contenido que este articulo:

Lo primero que vamos a necesitar hacer es crear una cuenta en Mozilla Thimble, visitando https://thimble.mozilla.org/es/, vamos a ver algo similar a la siguiente imagen:

/galleries/cew/1/01-landing.png

Creando una cuenta

/galleries/cew/1/02-sign-up.png

Click en Crea una cuenta en la parte superior derecha

Llena el formulario para crear una nueva cuenta:

  • Nombre de usuario
  • Tu dirección de correo electrónico
  • Contraseña (al menos una mayúscula, una minúscula y un numero)
/galleries/cew/1/03-sign-up-form.png

Luego de crear la cuenta debería ir directamente a la pagina principal de tu cuenta.

Iniciando sesión

Si en otro momento querés acceder de nuevo desde la pagina principal de Mozilla Thimble:

/galleries/cew/1/04-sign-in.png

Click en Inicia sesión

/galleries/cew/1/05-sign-in-form.png

Llena el formulario con tu usuario y contraseña

Espacio de trabajo principal

Espacio de trabajo vacío una vez que ingresamos a nuestra cuenta, para trabajar en un nuevo proyecto hay que hacer click en el botón verde Crear un nuevo proyecto

/galleries/cew/1/06-workspace.png

El espacio de trabajo de un proyecto tiene 3 paneles, de izquierda a derecha:

  • Explorador de archivos de proyecto
  • Editor de código
  • Vista previa del proyecto
/galleries/cew/1/07-new-project.png

Para hacer visible el proyecto en la web y compartirlo con un enlace, hacemos click en el botón blanco Publicar arriba a la derecha en el entorno de trabajo de nuestro proyecto.

/galleries/cew/1/08-publish-project.png

Ingresamos la descripción de nuestro proyecto y hacemos click en el botón verde Publicar

/galleries/cew/1/09-publish-project.png

Al finalizar el proceso podemos hacer click o copiar el enlace a nuestro proyecto publico en la web.

Si hacemos click en el botón rojo Eliminar la versión publicada* nuestro proyecto ya no sera accesible en la web.

/galleries/cew/1/10-publish-project.png
/galleries/cew/1/11-public-project.png

Nuestro proyecto de prueba en la web.

Si hacemos click en el botón verde Remix de cualquier proyecto publicado con Thimble, vamos a poder acceder a el desde nuestro entorno de trabajo, hacer nuestros cambios y publicar nuestros cambios.

/galleries/cew/1/12-remix-project.png

Podemos buscar proyectos para modificar en la página principal de Thimble

/galleries/cew/1/13-remix-landing.png

Si vamos a la sección Mezcla un proyecto para comenzar... podemos buscar por tema o filtrar por etiqueta, si seleccionamos la etiqueta html podemos ver proyectos que usen principalmente HTML, buscamos el proyecto Keep Calm and Carry On y clickeamos en el botón verde Mezclar.

/galleries/cew/1/14-remix-landing.png

Esto hará una copia del proyecto en nuestro usuario y abrirá el editor.

/galleries/cew/1/15-remix-change.png

Podemos cambiar el texto del poster a

Calma<br>
<span>y</span>
crea<br> en la web
/galleries/cew/1/16-remix-change.png

Y luego republicarlo con nuestros cambios

Mariano Guerra: Creemos en la Web

Este es el primero de lo que espero sera una serie de artículos sobre como aprender a hacer paginas web para personas sin ningún conocimiento previo de tecnología o programación.

Si sabes de tecnología recomendaselo a la mayor cantidad de personas posible, no sabemos cuantos grandes diseñadores y programadores web se encuentran escondidos por ahí.

Si te recomendaron esto y pensás que no es para vos, la cuestión es que si es para vos, y si algo es confuso no es tu culpa, es miá, así que contactame y decime que parte no esta clara así mejoramos esta guiá para todos.

Este articulo es una introducción a la herramienta que vamos a usar para los siguientes ejemplos, un proyecto llamado Mozilla Thimble, el cual facilita el proceso de crear, compartir y remixar paginas de otros.

Para los que prefieren ver videos, acá hay uno con el mismo contenido que este articulo:

Lo primero que vamos a necesitar hacer es crear una cuenta en Mozilla Thimble, visitando https://thimble.mozilla.org/es/, vamos a ver algo similar a la siguiente imagen:

/galleries/cew/1/01-landing.png

Creando una cuenta

/galleries/cew/1/02-sign-up.png

Click en Crea una cuenta en la parte superior derecha

Llena el formulario para crear una nueva cuenta:

  • Nombre de usuario
  • Tu dirección de correo electrónico
  • Contraseña (al menos una mayúscula, una minúscula y un numero)
/galleries/cew/1/03-sign-up-form.png

Luego de crear la cuenta debería ir directamente a la pagina principal de tu cuenta.

Iniciando sesión

Si en otro momento querés acceder de nuevo desde la pagina principal de Mozilla Thimble:

/galleries/cew/1/04-sign-in.png

Click en Inicia sesión

/galleries/cew/1/05-sign-in-form.png

Llena el formulario con tu usuario y contraseña

Espacio de trabajo principal

Espacio de trabajo vacío una vez que ingresamos a nuestra cuenta, para trabajar en un nuevo proyecto hay que hacer click en el botón verde Crear un nuevo proyecto

/galleries/cew/1/06-workspace.png

El espacio de trabajo de un proyecto tiene 3 paneles, de izquierda a derecha:

  • Explorador de archivos de proyecto
  • Editor de código
  • Vista previa del proyecto
/galleries/cew/1/07-new-project.png

Para hacer visible el proyecto en la web y compartirlo con un enlace, hacemos click en el botón blanco Publicar arriba a la derecha en el entorno de trabajo de nuestro proyecto.

/galleries/cew/1/08-publish-project.png

Ingresamos la descripción de nuestro proyecto y hacemos click en el botón verde Publicar

/galleries/cew/1/09-publish-project.png

Al finalizar el proceso podemos hacer click o copiar el enlace a nuestro proyecto publico en la web.

Si hacemos click en el botón rojo Eliminar la versión publicada* nuestro proyecto ya no sera accesible en la web.

/galleries/cew/1/10-publish-project.png
/galleries/cew/1/11-public-project.png

Nuestro proyecto de prueba en la web.

Si hacemos click en el botón verde Remix de cualquier proyecto publicado con Thimble, vamos a poder acceder a el desde nuestro entorno de trabajo, hacer nuestros cambios y publicar nuestros cambios.

/galleries/cew/1/12-remix-project.png

Podemos buscar proyectos para modificar en la página principal de Thimble

/galleries/cew/1/13-remix-landing.png

Si vamos a la sección Mezcla un proyecto para comenzar... podemos buscar por tema o filtrar por etiqueta, si seleccionamos la etiqueta html podemos ver proyectos que usen principalmente HTML, buscamos el proyecto Keep Calm and Carry On y clickeamos en el botón verde Mezclar.

/galleries/cew/1/14-remix-landing.png

Esto hará una copia del proyecto en nuestro usuario y abrirá el editor.

/galleries/cew/1/15-remix-change.png

Podemos cambiar el texto del poster a

Calma<br>
<span>y</span>
crea<br> en la web
/galleries/cew/1/16-remix-change.png

Y luego republicarlo con nuestros cambios

Facundo Batista: De trabajo en Hungría


Estuve una semana en Budapest, trabajando en un sprint con otros compañeros de equipo y de otros equipos, en general.

El viaje largo, pero sin sorpresas... sólo el detalle que me perdieron la valija en el viaje de ida :(. Cuando fui a hacer el reclamo, se fijaron y la ubicaron en Frankfurt (donde era la escala) y me dijeron que llegaba esa noche. Incluso me dieron un pelpa para que el hotel pueda recibir la valija por mí. Obviamente, cuando hice el checkin les comenté la situación. A las diez de la noche golpearon la puerta de la habitación y era alguien del hotel con mi valija \o/.

Así y todo tuve que salir vestido como venía (pantalón finito y zapatillas náuticas) a pasear durante la tarde... y me cagué de frío, aunque tenía polar y campera. Salimos a pegar una vuelta con Naty, Matías y Guillo, y caminamos un par de horas a la tardecita, antes de que caiga el sol, porque luego teníamos el coctel de bienvenida de la empresa. Aunque era "de día", estaba muy nublado, y eso, hacía mucho frío...

El auto tapado de hielo, ese frío hacía

El agua se congelaba a mitad del chorro (?)

En general no paseé demasiado, porque los días eran grises y fríos, y cuando terminábamos el día laboral (entre las 17:30 y las 18) ya era de noche. Excepto el viernes, que terminamos a las 16hs, y encima salió el sol. Y el sábado, claro, que salí a pegar una vuelta durante la mañana y mediodía. A diferencia de los primeros días, ya teníamos como 12°C, estábamos como queríamos (?)

Los primeros días nos poníamos toda la ropa que teníamos

Nerdeando en un bar de cerveza artesanal; indoor las temperaturas eran otras, claro

El sábado hice paseo por la zona del Danubio, subí un montecito donde estaba la Estatua de la Libertad (levantada originalmente en 1947, en recuerdo a la liberación soviética de Hungría durante la Segunda Guerra Mundial, finalizando la ocupación nazi), fui al mercado central de la ciudad, y caminé bastante para un lado y para el otro.

La gente, en general, educada. La mayoría no sabe inglés, incluso en zonas turísticas y en lugares como para comer o comprar cosas "de turistas", así que a veces uno vuelve a la típica charla de señas y sonidos varios. O se termina hablando en italiano, como nos pasó en una heladería :p.

Estatua de la Libertad

Claro, los otros días también estuvimos caminando por acá y por allá, pero en general de noche y con todos los negocios (excepto los relacionados a comer y beber) cerrados... Budapest realmente es una ciudad distinta antes y después de las 18 horas (porque a las seis cierran la mayoría de los negocios, y ya es de noche...).

Pero bueno, eso obviamente no impidió que saliéramos a comer, y yo me dediqué a los gulashs. El gulash, originario de Hungría, justamente, es simplemente un estofado de carne, y de ahí salen muchas variantes... con papa, sin papa, con spätzl chicos, grandes o directamente sin nada de eso, con cebolla o sin, etc... siempre con carne, cocida varias horas, apenas picante (por eso se lo acompaña con alguna salsita para apicantarlo, como hacemos nosotros con el locro), y MUY RICO.

A varios gulashs, le entré

Todas las fotos, acá.

Facundo Batista: Actualización y nuevo proyecto

fades

Después de casi un año, con Nico liberamos una nueva versión de fades.

¿Qué hay de nuevo en esta release?

  • Revisar si todo lo pedido está realmente disponible en PyPI antes de comenzar a instalarlo
  • Ignora dependencias duplicadas
  • Varias mejoras y correcciones en los mensajes que fades muestra en modo verbose
  • Prohibimos el mal uso de fades: instalarlo en legacy Python y ejecutarlo desde adentro de otro virtualenv
  • Un montón de mejoras relacionadas al proyecto en sí (pero no directamente visibles para el usuario final) y algunas pequeñas otras correcciones

Pruébenlo.

Loguito de fades :)

infoauth

infoauth es un un pequeño pero práctico módulo de Python y script para grabar/cargar tokens a/desde disco.

Esto es lo que hace:

  • graba tokens en un archivo en disco, pickleado y zippeado
  • cambia el archivo a sólo lectura, y sólo legible por vos
  • carga los tokens de ese archivo en disco

En qué casos este módulo es útil? Digamos que tenés un script o programa que necesita algunos tokens secretos (autenticación de mail, tokens de Twitter, la info para conectarse a una base de datos, etc...), pero no querés incluir estos tokens en el código, porque el mismo es público, entonces con este módulo harías:

tokens = infoauth.load(os.path.expanduser("~/.my-tokens"))

Fijate que el archivo va a quedar legible sólo por vos y no en el directorio del proyecto (así no tenés el riesgo de compartirlo por accidente).

CUIDADO: infoauth NO protege tus secretos con una clave o algo así, este módulo NO asegura tus secretos de ninguna manera. Sí, los tokens están enmarañados (porque se picklean y comprimen) y otra gente quizás no pueda accederlos fácilmente (legible sólo por vos), pero no hay más protección que esa. Usalo bajo tu propio riesgo.

Entonces, ¿cómo usarlo desde un programa en Python? Es fácil, para cargar la data:

import infoauth
auth = infoauth.load(os.path.expanduser("~/.my-mail-auth"))
# ...
mail.auth(auth['user'], auth['password'])

Para grabarla:

import infoauth
secrets = {'some-stuff': 'foo', 'code': 67}
infoauth.dump(secrets, os.path.expanduser("~/.secrets"))

Fijate que como grabar los tokens es algo que normalmente se hace una sola vez, seguro es más práctico hacerlo desde la linea de comandos, como se muestra a continuación...

Por eso, ¿cómo usarlo desde la linea de comandos? Para mostrar la info:

$ infoauth show ~/.my-mail-auth
password: ...
user: ...

Y para grabar un archivo con los datos:

$ infoauth create ~/.secrets some-stuff=foo code=67

Fijate que al crear el archivo desde la linea de comandos tenemos la limitación de que todos los valores almacenados van a ser cadenas de texto; si querés grabar otros tipos de datos, como enteros, listas, o lo que quieras, tendrías que usar la forma programática que se muestra arriba.

Esta es la página del proyecto, y claro que está en PyPI así que se puede usar sin problema desde fades (guiño, guiño).

Facundo Batista: De trabajo en Hungría

Estuve una semana en Budapest, trabajando en un sprint con otros compañeros de equipo y de otros equipos, en general.

El viaje largo, pero sin sorpresas... sólo el detalle que me perdieron la valija en el viaje de ida :(. Cuando fui a hacer el reclamo, se fijaron y la ubicaron en Frankfurt (donde era la escala) y me dijeron que llegaba esa noche. Incluso me dieron un pelpa para que el hotel pueda recibir la valija por mí. Obviamente, cuando hice el checkin les comenté la situación. A las diez de la noche golpearon la puerta de la habitación y era alguien del hotel con mi valija \o/.

Así y todo tuve que salir vestido como venía (pantalón finito y zapatillas náuticas) a pasear durante la tarde... y me cagué de frío, aunque tenía polar y campera. Salimos a pegar una vuelta con Naty, Matías y Guillo, y caminamos un par de horas a la tardecita, antes de que caiga el sol, porque luego teníamos el coctel de bienvenida de la empresa. Aunque era "de día", estaba muy nublado, y eso, hacía mucho frío...

El auto tapado de hielo, ese frío hacíaEl agua se congelaba a mitad del chorro (?)

En general no paseé demasiado, porque los días eran grises y fríos, y cuando terminábamos el día laboral (entre las 17:30 y las 18) ya era de noche. Excepto el viernes, que terminamos a las 16hs, y encima salió el sol. Y el sábado, claro, que salí a pegar una vuelta durante la mañana y mediodía. A diferencia de los primeros días, ya teníamos como 12°C, estábamos como queríamos (?)

Los primeros días nos poníamos toda la ropa que teníamosNerdeando en un bar de cerveza artesanal; indoor las temperaturas eran otras, claro

El sábado hice paseo por la zona del Danubio, subí un montecito donde estaba la Estatua de la Libertad (levantada originalmente en 1947, en recuerdo a la liberación soviética de Hungría durante la Segunda Guerra Mundial, finalizando la ocupación nazi), fui al mercado central de la ciudad, y caminé bastante para un lado y para el otro.

La gente, en general, educada. La mayoría no sabe inglés, incluso en zonas turísticas y en lugares como para comer o comprar cosas "de turistas", así que a veces uno vuelve a la típica charla de señas y sonidos varios. O se termina hablando en italiano, como nos pasó en una heladería :p.

Estatua de la Libertad

Claro, los otros días también estuvimos caminando por acá y por allá, pero en general de noche y con todos los negocios (excepto los relacionados a comer y beber) cerrados... Budapest realmente es una ciudad distinta antes y después de las 18 horas (porque a las seis cierran la mayoría de los negocios, y ya es de noche...).

Pero bueno, eso obviamente no impidió que saliéramos a comer, y yo me dediqué a los gulashs. El gulash, originario de Hungría, justamente, es simplemente un estofado de carne, y de ahí salen muchas variantes... con papa, sin papa, con spätzl chicos, grandes o directamente sin nada de eso, con cebolla o sin, etc... siempre con carne, cocida varias horas, apenas picante (por eso se lo acompaña con alguna salsita para apicantarlo, como hacemos nosotros con el locro), y MUY RICO.

A varios gulashs, le entré

Todas las fotos, acá.

Mariano Guerra: How to use leveled, a pure erlang leveldb implementation

Yesterday at the riak_core tutorial at CodeBEAMSF I was trying to implement a leveled based backend for the key value store we were building, I was having troubles with leveled crashing when trying to destroy it (stop and remove files in leveled parlance), after fighting for a while I needed a smaller example to see if it was my mistake or not.

I decided to do the smaller example and to share the process here.

First we need some erlang application to hold our leveled dependency and configuration, let's do it by creating an erlang release with rebar3:

rebar3 new release name=lvld
cd lvld

Now that the skeleton is ready, we need to change rebar.config to add the information to use leveled, the resulting rebar.config below, see comments:

{erl_opts, [debug_info]}.

{deps, [
    % add leveled dependency
    {leveled, {git, "https://github.com/martinsumner/leveled.git", {branch, "master"}}}
]}.

{relx, [{release, { lvld, "0.1.0" },
    [lvld,
    % leveled needs crypto
    crypto,
    % make sure to load leveled, don't start it, it's not an app
    {leveled, load},
    % required by leveled
    {lz4, load},
    sasl]},

    {sys_config, "./config/sys.config"},
    {vm_args, "./config/vm.args"},

    {dev_mode, true},
    {include_erts, false},

    {extended_start_script, true}]
}.

{profiles, [{prod, [{relx, [{dev_mode, false},
    {include_erts, true}]}]
            }]
}.

% leveled generates lots of warnings and has warnings_as_errors set, we need
% to override that by copying the erl_opts field without warnings_as_errors
{overrides,
    [{override, leveled,
        [{erl_opts, [{platform_define, "^1[7-8]{1}", old_rand},
            {platform_define, "^R", old_rand},
            {platform_define, "^R", no_sync}]}]}
    ]}.

We will build a wrapper for leveled that exposes a simple kv store in apps/lvld/src/lvld_kv.erl:

-module(lvld_kv).
-export([new/1, get/3, put/4, delete/3, keys/2, close/1, delete/1, is_empty/1, foldl/3]).

-include_lib("leveled/include/leveled.hrl").

-record(state, {bookie, base_path}).

new(Opts=#{path := Path}) ->
    LedgerCacheSize = maps:get(ledger_cache_size, Opts, 2000),
    JournalSize = maps:get(journal_size, Opts, 500000000),
    SyncStrategy = maps:get(sync_strategy, Opts, none),
    {ok, Bookie} = leveled_bookie:book_start(Path, LedgerCacheSize,
                                             JournalSize, SyncStrategy),
    State = #state{bookie=Bookie, base_path=Path},
    {ok, State}.

put(State=#state{bookie=Bookie}, Bucket, Key, Value) ->
    R = leveled_bookie:book_put(Bookie, Bucket, Key, Value, []),
    {R, State}.

get(State=#state{bookie=Bookie}, Bucket, Key) ->
    K = {Bucket, Key},
    Res = case leveled_bookie:book_get(Bookie, Bucket, Key) of
              not_found -> {not_found, K};
              {ok, Value} -> {found, {K, Value}}
          end,
    {Res, State}.

delete(State=#state{bookie=Bookie}, Bucket, Key) ->
    R = leveled_bookie:book_delete(Bookie, Bucket, Key, []),
    {R, State}.

keys(State=#state{bookie=Bookie}, Bucket) ->
    FoldHeadsFun = fun(_B, K, _ProxyV, Acc) -> [K | Acc] end,
    {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie,
                            {foldheads_bybucket,
                                ?STD_TAG,
                                Bucket,
                                all,
                                FoldHeadsFun,
                                true, true, false}),
    Keys = FoldFn(),
    {Keys, State}.

is_empty(State=#state{bookie=Bookie}) ->
    FoldBucketsFun = fun(B, Acc) -> [B | Acc] end,
    {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie,
                                                       {binary_bucketlist,
                                                        ?STD_TAG,
                                                        {FoldBucketsFun, []}}),
    IsEmpty = case FoldFn() of
                  [] -> true;
                  _ -> false
              end,
    {IsEmpty, State}.

close(State=#state{bookie=Bookie}) ->
    R = leveled_bookie:book_close(Bookie),
    {R, State}.

delete(State=#state{base_path=Path}) ->
    R = remove_path(Path),
    {R, State}.


foldl(Fun, Acc0, State=#state{bookie=Bookie}) ->
    FoldObjectsFun = fun(B, K, V, Acc) -> Fun({{B, K}, V}, Acc) end,
    {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie, {foldobjects_allkeys,
                                                                ?STD_TAG,
                                                                {FoldObjectsFun, Acc0},
                                                                true}),
    AccOut = FoldFn(),
    {AccOut, State}.

% private functions

sub_files(From) ->
    {ok, SubFiles} = file:list_dir(From),
    [filename:join(From, SubFile) || SubFile <- SubFiles].

remove_path(Path) ->
    case filelib:is_dir(Path) of
        false ->
            file:delete(Path);
        true ->
            lists:foreach(fun(ChildPath) -> remove_path(ChildPath) end,
                          sub_files(Path)),
            file:del_dir(Path)
    end.

We are ready to build a release and try our kv api on the repl:

rebar3 release
./_build/default/rel/lvld/bin/lvld console

This is the code we will run in the repl, I put it here so it's easy to read and copy and paste:

Nums = lists:seq(1, 10).
Buckets = lists:map(fun (N) -> list_to_binary("bucket-" ++ integer_to_list(N)) end,
Nums).
Keys = lists:map(fun (N) -> list_to_binary("key-" ++ integer_to_list(N)) end, Nums).

GenValue = fun (Bucket, Key) -> <<"v/", Bucket/binary, "/", Key/binary>> end.

{ok, Kv} = lvld_kv:new(#{path => "/tmp/lvld_test"}).
lists:foreach(fun (Bucket) ->
        lists:foreach(fun (Key) ->
                Val = GenValue(Bucket, Key),
                lvld_kv:put(Kv, Bucket, Key, Val)
        end, Keys)
end, Buckets).


B1 = <<"bucket-1">>.
K1 = <<"key-1">>.
V1 = <<"value-1">>.
B2 = <<"bucket-2">>.
K2 = <<"key-2">>.

FoldFn = fun ({{B, K}, V}, AccIn) -> [{B, K, V} | AccIn] end.
lvld_kv:foldl(FoldFn, [], Kv).

lvld_kv:put(Kv, B1, K1, V1).
lvld_kv:get(Kv, B1, K1).
lvld_kv:delete(Kv, B1, K1).
lvld_kv:get(Kv, B1, K1).

lvld_kv:keys(Kv, B1).

lvld_kv:close(Kv).
lvld_kv:delete(Kv).

The results of running it (removing some of the verbose logging):

(lvld@ganesha)1> Nums = lists:seq(1, 10).

[1,2,3,4,5,6,7,8,9,10]


(lvld@ganesha)2> Buckets = lists:map(fun (N) -> list_to_binary("bucket-" ++ integer_to_list(N)) end, Nums).

[<<"bucket-1">>,<<"bucket-2">>,<<"bucket-3">>,
 <<"bucket-4">>,<<"bucket-5">>,<<"bucket-6">>,<<"bucket-7">>,
  <<"bucket-8">>,<<"bucket-9">>,<<"bucket-10">>]


(lvld@ganesha)3> Keys = lists:map(fun (N) -> list_to_binary("key-" ++ integer_to_list(N)) end, Nums).

  [<<"key-1">>,<<"key-2">>,<<"key-3">>,<<"key-4">>,
   <<"key-5">>,<<"key-6">>,<<"key-7">>,<<"key-8">>,<<"key-9">>,
    <<"key-10">>]


(lvld@ganesha)4> GenValue = fun (Bucket, Key) -> <<"v/", Bucket/binary, "/", Key/binary>> end.

#Fun<erl_eval.12.99386804>


(lvld@ganesha)5> {ok, Kv} = lvld_kv:new(#{path => "/tmp/lvld_test"}).

{ok,{state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)6> B1 = <<"bucket-1">>.
<<"bucket-1">>

(lvld@ganesha)7> K1 = <<"key-1">>.
<<"key-1">>

(lvld@ganesha)8> V1 = <<"value-1">>.
<<"value-1">>

(lvld@ganesha)9> B2 = <<"bucket-2">>.
<<"bucket-2">>

(lvld@ganesha)10> K2 = <<"key-2">>.
<<"key-2">>

(lvld@ganesha)11> FoldFn = fun ({{B, K}, V}, AccIn) -> [{B, K, V} | AccIn] end.
#Fun<erl_eval.12.99386804>

(lvld@ganesha)13> lists:foreach(fun (Bucket) ->
(lvld@ganesha)13>         lists:foreach(fun (Key) ->
(lvld@ganesha)13>                 Val = GenValue(Bucket, Key),
(lvld@ganesha)13>                 lvld_kv:put(Kv, Bucket, Key, Val)
(lvld@ganesha)13>         end, Keys)
(lvld@ganesha)13> end, Buckets).

(lvld@ganesha)14> lvld_kv:foldl(FoldFn, [], Kv).

{[{<<"bucket-9">>,<<"key-9">>,<<"v/bucket-9/key-9">>},
  {<<"bucket-9">>,<<"key-8">>,<<"v/bucket-9/key-8">>},
  {<<"bucket-9">>,<<"key-7">>,<<"v/bucket-9/key-7">>},
  {<<"bucket-9">>,<<"key-6">>,<<"v/bucket-9/key-6">>},
  {<<"bucket-9">>,<<"key-5">>,<<"v/bucket-9/key-5">>},
  {<<"bucket-9">>,<<"key-4">>,<<"v/bucket-9/key-4">>},
  {<<"bucket-9">>,<<"key-3">>,<<"v/bucket-9/key-3">>},
  {<<"bucket-9">>,<<"key-2">>,<<"v/bucket-9/key-2">>},
  {<<"bucket-9">>,<<"key-10">>,<<"v/bucket-9/key-10">>},
  {<<"bucket-9">>,<<"key-1">>,<<"v/bucket-9/key-1">>},
  {<<"bucket-8">>,<<"key-9">>,<<"v/bucket-8/key-9">>},
  {<<"bucket-8">>,<<"key-8">>,<<"v/bucket-8/key-8">>},
  {<<"bucket-8">>,<<"key-7">>,<<"v/bucket-8/key-7">>},
  {<<"bucket-8">>,<<"key-6">>,<<"v/bucket-8/key-6">>},
  {<<"bucket-8">>,<<"key-5">>,<<"v/bucket-8/key-5">>},
  {<<"bucket-8">>,<<"key-4">>,<<"v/bucket-8/key-4">>},
  {<<"bucket-8">>,<<"key-3">>,<<"v/bucket-8/key-3">>},
  {<<"bucket-8">>,<<"key-2">>,<<"v/bucket-8/key-2">>},
  {<<"bucket-8">>,<<"key-10">>,<<"v/bucket-8/key-10">>},
  {<<"bucket-8">>,<<"key-1">>,<<"v/bucket-8/key-1">>},
  {<<"bucket-7">>,<<"key-9">>,<<"v/bucket-7/key-9">>},
  {<<"bucket-7">>,<<"key-8">>,<<"v/bucket-7/k"...>>},
  {<<"bucket-7">>,<<"key-7">>,<<"v/bucket"...>>},
  {<<"bucket-7">>,<<"key-6">>,<<"v/bu"...>>},
  {<<"bucket-7">>,<<"key-"...>>,<<...>>},
  {<<"buck"...>>,<<...>>,...},
  {<<...>>,...},
  {...}|...],

 {state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)15> lvld_kv:put(Kv, B1, K1, V1).
{ok,{state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)16> lvld_kv:get(Kv, B1, K1).
{{found,{{<<"bucket-1">>,<<"key-1">>},<<"value-1">>}},
 {state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)17> lvld_kv:delete(Kv, B1, K1).
{ok,{state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)18> lvld_kv:get(Kv, B1, K1).
{{not_found,{<<"bucket-1">>,<<"key-1">>}},
 {state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)19> lvld_kv:keys(Kv, B1).
{<"key-9">>,<<"key-8">>,<<"key-7">>,<<"key-6">>,
  <<"key-5">>,<<"key-4">>,<<"key-3">>,<<"key-2">>,
  <<"key-10">>],
 {state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)20> lvld_kv:close(Kv).
{ok,{state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)21> lvld_kv:delete(Kv).
{ok,{state,<0.264.0>,"/tmp/lvld_test"}}

In case you want to know the case for the crashing, when calling destroy on leveled, it returns destroy as reason for gen_server stop, which doesn't seem to make Erlang happy and it crashes the process and propagates the error.

The solution here is to just close it and remove the files myself (the difference between close and destroy is file removal).

Mariano Guerra: How to use leveled, a pure erlang leveldb implementation

Yesterday at the riak_core tutorial at CodeBEAMSF I was trying to implement a leveled based backend for the key value store we were building, I was having troubles with leveled crashing when trying to destroy it (stop and remove files in leveled parlance), after fighting for a while I needed a smaller example to see if it was my mistake or not.

I decided to do the smaller example and to share the process here.

First we need some erlang application to hold our leveled dependency and configuration, let's do it by creating an erlang release with rebar3:

rebar3 new release name=lvld
cd lvld

Now that the skeleton is ready, we need to change rebar.config to add the information to use leveled, the resulting rebar.config below, see comments:

{erl_opts, [debug_info]}.

{deps, [
    % add leveled dependency
    {leveled, {git, "https://github.com/martinsumner/leveled.git", {branch, "master"}}}
]}.

{relx, [{release, { lvld, "0.1.0" },
    [lvld,
    % leveled needs crypto
    crypto,
    % make sure to load leveled, don't start it, it's not an app
    {leveled, load},
    % required by leveled
    {lz4, load},
    sasl]},

    {sys_config, "./config/sys.config"},
    {vm_args, "./config/vm.args"},

    {dev_mode, true},
    {include_erts, false},

    {extended_start_script, true}]
}.

{profiles, [{prod, [{relx, [{dev_mode, false},
    {include_erts, true}]}]
            }]
}.

% leveled generates lots of warnings and has warnings_as_errors set, we need
% to override that by copying the erl_opts field without warnings_as_errors
{overrides,
    [{override, leveled,
        [{erl_opts, [{platform_define, "^1[7-8]{1}", old_rand},
            {platform_define, "^R", old_rand},
            {platform_define, "^R", no_sync}]}]}
    ]}.

We will build a wrapper for leveled that exposes a simple kv store in apps/lvld/src/lvld_kv.erl:

-module(lvld_kv).
-export([new/1, get/3, put/4, delete/3, keys/2, close/1, delete/1, is_empty/1, foldl/3]).

-include_lib("leveled/include/leveled.hrl").

-record(state, {bookie, base_path}).

new(Opts=#{path := Path}) ->
    LedgerCacheSize = maps:get(ledger_cache_size, Opts, 2000),
    JournalSize = maps:get(journal_size, Opts, 500000000),
    SyncStrategy = maps:get(sync_strategy, Opts, none),
    {ok, Bookie} = leveled_bookie:book_start(Path, LedgerCacheSize,
                                             JournalSize, SyncStrategy),
    State = #state{bookie=Bookie, base_path=Path},
    {ok, State}.

put(State=#state{bookie=Bookie}, Bucket, Key, Value) ->
    R = leveled_bookie:book_put(Bookie, Bucket, Key, Value, []),
    {R, State}.

get(State=#state{bookie=Bookie}, Bucket, Key) ->
    K = {Bucket, Key},
    Res = case leveled_bookie:book_get(Bookie, Bucket, Key) of
              not_found -> {not_found, K};
              {ok, Value} -> {found, {K, Value}}
          end,
    {Res, State}.

delete(State=#state{bookie=Bookie}, Bucket, Key) ->
    R = leveled_bookie:book_delete(Bookie, Bucket, Key, []),
    {R, State}.

keys(State=#state{bookie=Bookie}, Bucket) ->
    FoldHeadsFun = fun(_B, K, _ProxyV, Acc) -> [K | Acc] end,
    {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie,
                            {foldheads_bybucket,
                                ?STD_TAG,
                                Bucket,
                                all,
                                FoldHeadsFun,
                                true, true, false}),
    Keys = FoldFn(),
    {Keys, State}.

is_empty(State=#state{bookie=Bookie}) ->
    FoldBucketsFun = fun(B, Acc) -> [B | Acc] end,
    {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie,
                                                       {binary_bucketlist,
                                                        ?STD_TAG,
                                                        {FoldBucketsFun, []}}),
    IsEmpty = case FoldFn() of
                  [] -> true;
                  _ -> false
              end,
    {IsEmpty, State}.

close(State=#state{bookie=Bookie}) ->
    R = leveled_bookie:book_close(Bookie),
    {R, State}.

delete(State=#state{base_path=Path}) ->
    R = remove_path(Path),
    {R, State}.


foldl(Fun, Acc0, State=#state{bookie=Bookie}) ->
    FoldObjectsFun = fun(B, K, V, Acc) -> Fun({{B, K}, V}, Acc) end,
    {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie, {foldobjects_allkeys,
                                                                ?STD_TAG,
                                                                {FoldObjectsFun, Acc0},
                                                                true}),
    AccOut = FoldFn(),
    {AccOut, State}.

% private functions

sub_files(From) ->
    {ok, SubFiles} = file:list_dir(From),
    [filename:join(From, SubFile) || SubFile <- SubFiles].

remove_path(Path) ->
    case filelib:is_dir(Path) of
        false ->
            file:delete(Path);
        true ->
            lists:foreach(fun(ChildPath) -> remove_path(ChildPath) end,
                          sub_files(Path)),
            file:del_dir(Path)
    end.

We are ready to build a release and try our kv api on the repl:

rebar3 release
./_build/default/rel/lvld/bin/lvld console

This is the code we will run in the repl, I put it here so it's easy to read and copy and paste:

Nums = lists:seq(1, 10).
Buckets = lists:map(fun (N) -> list_to_binary("bucket-" ++ integer_to_list(N)) end,
Nums).
Keys = lists:map(fun (N) -> list_to_binary("key-" ++ integer_to_list(N)) end, Nums).

GenValue = fun (Bucket, Key) -> <<"v/", Bucket/binary, "/", Key/binary>> end.

{ok, Kv} = lvld_kv:new(#{path => "/tmp/lvld_test"}).
lists:foreach(fun (Bucket) ->
        lists:foreach(fun (Key) ->
                Val = GenValue(Bucket, Key),
                lvld_kv:put(Kv, Bucket, Key, Val)
        end, Keys)
end, Buckets).


B1 = <<"bucket-1">>.
K1 = <<"key-1">>.
V1 = <<"value-1">>.
B2 = <<"bucket-2">>.
K2 = <<"key-2">>.

FoldFn = fun ({{B, K}, V}, AccIn) -> [{B, K, V} | AccIn] end.
lvld_kv:foldl(FoldFn, [], Kv).

lvld_kv:put(Kv, B1, K1, V1).
lvld_kv:get(Kv, B1, K1).
lvld_kv:delete(Kv, B1, K1).
lvld_kv:get(Kv, B1, K1).

lvld_kv:keys(Kv, B1).

lvld_kv:close(Kv).
lvld_kv:delete(Kv).

The results of running it (removing some of the verbose logging):

(lvld@ganesha)1> Nums = lists:seq(1, 10).

[1,2,3,4,5,6,7,8,9,10]


(lvld@ganesha)2> Buckets = lists:map(fun (N) -> list_to_binary("bucket-" ++ integer_to_list(N)) end, Nums).

[<<"bucket-1">>,<<"bucket-2">>,<<"bucket-3">>,
 <<"bucket-4">>,<<"bucket-5">>,<<"bucket-6">>,<<"bucket-7">>,
  <<"bucket-8">>,<<"bucket-9">>,<<"bucket-10">>]


(lvld@ganesha)3> Keys = lists:map(fun (N) -> list_to_binary("key-" ++ integer_to_list(N)) end, Nums).

  [<<"key-1">>,<<"key-2">>,<<"key-3">>,<<"key-4">>,
   <<"key-5">>,<<"key-6">>,<<"key-7">>,<<"key-8">>,<<"key-9">>,
    <<"key-10">>]


(lvld@ganesha)4> GenValue = fun (Bucket, Key) -> <<"v/", Bucket/binary, "/", Key/binary>> end.

#Fun<erl_eval.12.99386804>


(lvld@ganesha)5> {ok, Kv} = lvld_kv:new(#{path => "/tmp/lvld_test"}).

{ok,{state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)6> B1 = <<"bucket-1">>.
<<"bucket-1">>

(lvld@ganesha)7> K1 = <<"key-1">>.
<<"key-1">>

(lvld@ganesha)8> V1 = <<"value-1">>.
<<"value-1">>

(lvld@ganesha)9> B2 = <<"bucket-2">>.
<<"bucket-2">>

(lvld@ganesha)10> K2 = <<"key-2">>.
<<"key-2">>

(lvld@ganesha)11> FoldFn = fun ({{B, K}, V}, AccIn) -> [{B, K, V} | AccIn] end.
#Fun<erl_eval.12.99386804>

(lvld@ganesha)13> lists:foreach(fun (Bucket) ->
(lvld@ganesha)13>         lists:foreach(fun (Key) ->
(lvld@ganesha)13>                 Val = GenValue(Bucket, Key),
(lvld@ganesha)13>                 lvld_kv:put(Kv, Bucket, Key, Val)
(lvld@ganesha)13>         end, Keys)
(lvld@ganesha)13> end, Buckets).

(lvld@ganesha)14> lvld_kv:foldl(FoldFn, [], Kv).

{[{<<"bucket-9">>,<<"key-9">>,<<"v/bucket-9/key-9">>},
  {<<"bucket-9">>,<<"key-8">>,<<"v/bucket-9/key-8">>},
  {<<"bucket-9">>,<<"key-7">>,<<"v/bucket-9/key-7">>},
  {<<"bucket-9">>,<<"key-6">>,<<"v/bucket-9/key-6">>},
  {<<"bucket-9">>,<<"key-5">>,<<"v/bucket-9/key-5">>},
  {<<"bucket-9">>,<<"key-4">>,<<"v/bucket-9/key-4">>},
  {<<"bucket-9">>,<<"key-3">>,<<"v/bucket-9/key-3">>},
  {<<"bucket-9">>,<<"key-2">>,<<"v/bucket-9/key-2">>},
  {<<"bucket-9">>,<<"key-10">>,<<"v/bucket-9/key-10">>},
  {<<"bucket-9">>,<<"key-1">>,<<"v/bucket-9/key-1">>},
  {<<"bucket-8">>,<<"key-9">>,<<"v/bucket-8/key-9">>},
  {<<"bucket-8">>,<<"key-8">>,<<"v/bucket-8/key-8">>},
  {<<"bucket-8">>,<<"key-7">>,<<"v/bucket-8/key-7">>},
  {<<"bucket-8">>,<<"key-6">>,<<"v/bucket-8/key-6">>},
  {<<"bucket-8">>,<<"key-5">>,<<"v/bucket-8/key-5">>},
  {<<"bucket-8">>,<<"key-4">>,<<"v/bucket-8/key-4">>},
  {<<"bucket-8">>,<<"key-3">>,<<"v/bucket-8/key-3">>},
  {<<"bucket-8">>,<<"key-2">>,<<"v/bucket-8/key-2">>},
  {<<"bucket-8">>,<<"key-10">>,<<"v/bucket-8/key-10">>},
  {<<"bucket-8">>,<<"key-1">>,<<"v/bucket-8/key-1">>},
  {<<"bucket-7">>,<<"key-9">>,<<"v/bucket-7/key-9">>},
  {<<"bucket-7">>,<<"key-8">>,<<"v/bucket-7/k"...>>},
  {<<"bucket-7">>,<<"key-7">>,<<"v/bucket"...>>},
  {<<"bucket-7">>,<<"key-6">>,<<"v/bu"...>>},
  {<<"bucket-7">>,<<"key-"...>>,<<...>>},
  {<<"buck"...>>,<<...>>,...},
  {<<...>>,...},
  {...}|...],

 {state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)15> lvld_kv:put(Kv, B1, K1, V1).
{ok,{state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)16> lvld_kv:get(Kv, B1, K1).
{{found,{{<<"bucket-1">>,<<"key-1">>},<<"value-1">>}},
 {state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)17> lvld_kv:delete(Kv, B1, K1).
{ok,{state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)18> lvld_kv:get(Kv, B1, K1).
{{not_found,{<<"bucket-1">>,<<"key-1">>}},
 {state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)19> lvld_kv:keys(Kv, B1).
{<"key-9">>,<<"key-8">>,<<"key-7">>,<<"key-6">>,
  <<"key-5">>,<<"key-4">>,<<"key-3">>,<<"key-2">>,
  <<"key-10">>],
 {state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)20> lvld_kv:close(Kv).
{ok,{state,<0.264.0>,"/tmp/lvld_test"}}

(lvld@ganesha)21> lvld_kv:delete(Kv).
{ok,{state,<0.264.0>,"/tmp/lvld_test"}}

In case you want to know the case for the crashing, when calling destroy on leveled, it returns destroy as reason for gen_server stop, which doesn't seem to make Erlang happy and it crashes the process and propagates the error.

The solution here is to just close it and remove the files myself (the difference between close and destroy is file removal).

Manuel Kaufmann (Humitos): Modifying a Django setting could be a nightmare

I've been working at Read the Docs for four months more or less at this time. Although I've been using Django for a decade now, there are many things that I never used, forget or don't know how they work internally.

During the last three or four weeks I worked in a new feature for the corporate site of Read the Docs (https://readthedocs.com/) and I had a lot of different problems: mainly because I didn't know the full codebase, but also because I never used some pieces of technology involved to make this feature.

So, what do you do when you are afraid of breaking everything? You write many test to cover your ass! That's what I did and I felt very comfortable when I started writing the logic of the feature itself --of course, to write the tests I had to ask many questions to my team to be sure the tests were accurate regarding what they were testing.

Finally, the PR was tested for some members of the team, everything worked as expected, the code was merged and deployed. Successful story, right?

One of two days later, I started seeing one of my tests related to that PR failing locally. I did a simple PR to run the test in CircleCI and it also failed there. "WTF? This doesn't make sense" -I thought. I did the manual QA locally and the code worked as expected but the test were failing but... It didn't make sense because the initial tests under CircleCI before merging the PR were passing and now without any change at all this specific test wasn't passing. I was very confused.

I went to the code of my tests to find something strange and after taking a look at this code I didn't find anything that called my attention directly but I found that I was mixing @override_settings and @modify_settings as decorators of the class. "That may be a reason... Really? Why?" -I was confused. I went to the documentation, search at Google to see if the first results could give some clue and after a couple of hours reading, thinking and shouting alone I realized that I was using both options of the @modify_settings decorator: append and remove which, in theory, is not a problem.

Note

What is the difference between @override_settings and @modify_settings? Well, one allows you to replace the setting completely and the other one allows you to append, prepend or remove values from the current value of the setting.

As I wanted to modify the MIDDLEWARE_CLASSES to inject a new one in the middle of the others, I needed to remove the ones that go after the one I wanted to inject and then add mine (the new one) plus the ones that I had removed. Am I right?

I'm sure that at this point I'm wrong and there should be a much better way of doing this, but I'm already writing the post :)

So, my code looks something like:

@modify_settings(MIDDLEWARE_CLASSES={
    'append': [
        'readthedocsinc.middleware.MyOwnMiddleware',
        'externallib.middleware.ExternalMiddleware',
    ],
    'remove': [
        'externallib.middleware.NoNeededMiddleware',
        'externallib.middleware.ExternalMiddleware',
    ],
})
class FooBar(TestCase):
    # ...

In my case, NoNeededMiddleware wasn't needed for the tests and it had to be completely removed. ExternalMiddleware was needed but MyOwnMiddleware should precede it; that's why I needed to first remove it and then append it -in another position in the list, though.

Anything wrong up to here? No. Well, yes. Oh... "No, the test are passing", or in a better way "The tests were passing right before the deploy but now there is one that it's not passing anymore"

At this point, I did what I learnt from my first boss in my professional career: I went to the Django source code of @modify_settings and I found the issue: it removes everything from remove key and appends everything from append key. Makes sense.

Now, which operation is executed first? AHA! Well, it depends since dictionaries don't have order (< Python 3.6), sometimes it could be remove and sometimes it could be append. So, I'd say that all the planets were aligned to pass all the tests locally and in CircleCI before merging, and after merging they got unaligned and the order started behaving in the other way :)

What I did? I just used collections.OrderedDict and put remove as the first element in the dictionary, and then append. That way, I'm 100% sure that first I'm removing the middlewares I don't need and then I'm adding the ones that I'm interested in the proper order.

The final code looks like this:

from collections import OrderedDict
@modify_settings(MIDDLEWARE_CLASSES=OrderedDict([
    (
        'remove',
        [
            'externallib.middleware.NoNeededMiddleware',
            'externallib.middleware.ExternalMiddleware',
        ],
    ).
    (
        'append',
        [
            'readthedocsinc.middleware.MyOwnMiddleware',
            'externallib.middleware.ExternalMiddleware',
        ],
    ),
]))
class FooBar(TestCase):
    # ...

I think this should be clearly detailed in the Django documentation of @modify_settings since the behaviour is way different and can cause a lot of time lost because of this --even worse if you are working with Python 2 which will randomly do one or the other first.

Facundo Batista: Eventos, eventos! Python en el primer semestre

Tenemos algunos eventos de Python Argentina en el primer semestre del año. Vayamos cronológicamente.

El miércoles 4 de Abril, 19hs, hacemos un meetup de Python Argentina en Devecoop. Tendremos un par de charlas técnicas, quizás hagamos un "Consultorio Python", veremos cómo lo armamos. Si no están anotados en el meetup de Python Buenos Aires, regístrense así reciben las noticias de estos meetups y se anotan fácil y etc.

Del 28 de Abril al 1 de Mayo tenemos una nueva edición del PyCamp, nuevamente en Baradero. Ya está abierta la inscripción, considerando incluso descuentos para socios de Python Argentina. Pueden buscar toda la info en la página del evento.

También tenemos dos PyDays, ambos en Mayo. Sí, dos, por ahora... habrán más en el año. El primero en La Plata, todavía pendiente de definir cual sábado, y el segundo en Corrientes, el sábado 19. Ya les traeré más noticias cuando nos acerquemos a las fechas.

Por otro lado, estamos armando la PyCon Argentina de este año en Buenos Aires. Fecha todavía no tenemos (estamos buscando el lugar, todavía) pero será Septiembre, Octubre o Noviembre.

Todo esto es en parte gracias a la Asociación Civil de Python Argentina, que nos permite tener un marco estructural con el cual movernos y que no nos venga la AFIP a meter en cana por mover guita por ahí... así que si querés apoyarnos en general considerá hacerte socia/o vos o ayudanos pasando este folleto en donde trabajes para que esa empresa/cooperativa lo considere también. Gracias!