domingo, 13 de diciembre de 2015

Erlang - Sintaxis en funciones

Que sucede con el if?
El condicional if  actúa como las guardas y comparte con ellas la sintaxis pero fuera de la clausula cabecera  de la funcion. Las clausulas if son llamada Patrones de Guardas. Los IFs en Erlang son diferentes de los ifs  que encuentras en la mayoria de los lenguajes. Cuando entrás al pais de Erlang deberías dejar en la puerta todo lo que sabes acerca de los ifs

Para ver que tan similar a las guardas es la expresión if, mira lo siguiente.

-module(what_the_if).
-export([heh_fine/0]).

heh_fine()->
    if 1 =:= 1 ->
        works
    end,
    if 1=:= 2; 1 =:= 1->
        works
    end,
    if 1 =:= 2, 1=:= 1 ->
        fails
    end.


guarda esto como what_the_if.erl y probemos.

1> c(what_the_if).
./what_the_if.erl:12: Warning: no clause will ever match
./what_the_if.erl:12: Warning: the guard for this clause evaluates to 'false' {ok,what_the_if}
2> what_the_if:heh_fine().
** exception error: no true branch found when evaluating an if expression
        in function what_the_if:heh_fine/0


Bueno, el compilador nos informa que hay un error en la linea 12 que informa que esa linea siempre se evalua como falso. Recuerda, que en Erlang, todo debe retornar algo y la expresión if no es una excepción a la regla. De esta manera cuando Erlang no puede encontrar una manera de tener una guarda exitosa, este se rompe: no puede no devolver algo. Así que necesitamos agregar una rama que capture todo haciendo que siempre sea exitoso de cualquier manera.

En cualquier lenguaje esto es llamado un "else". En Erlang usamos "true" (esto explica por que la VM lanza el error "no hay rama verdadera")

oh_god(N) ->
    if N =:= 2 -> might_succeed;
        true -> always_does
    end.

Y ahora si nosotros testeamos esta función (la parte anterior seguirá lanzando esos warnings)

3> c(what_the_if).
./what_the_if.erl:12: Warning: no clause will ever match
./what_the_if.erl:12: Warning: the guard for this clause evaluates to 'false' {ok,what_the_if}
4> what_the_if:oh_god(2).
might_succeed
5> what_the_if:oh_god(3).
always_does


Seguramente serás alguno de los programadores que se sorprende por el uso de atomo true en vez de else para el control de flujo, despues de todo es más familiar. Como dice Richard O'Keefe básicamente usar el true es capturar todas las otras posibilidades y estos ejemplos que dá él mismo deberián solucionar nuestra confusión.

                          by
 if X > Y -> a()  if X > Y  -> a()
  ; true  -> b()   ; X =< Y -> b()
 end        end

 if X > Y -> a()  if X > Y -> a()
  ; X < Y -> b()   ; X < Y -> b()
  ; true  -> c()   ; X ==Y -> c()
 end   end 
 

Las ramas "else" o "true" deberián ser evitadas ambas. Los ifs son usualmente más fácil de leer cuando cubres todas los extremos lógicos que cuando confias eso a una clausula "captura-todo".

En caso ... de

Si la expresión if es como una guarda una expresión case ... of  es como la cabecera de la función completa: puede tener una compleja coincidencia de patrones para cada argumento, y puedes tener guardas encima de él.

Probablemente estás familiarizado con la sintaxis, no necesitaremos demasiados ejemplos. Para esto, escribiremos una función para agregar elementos a un conjunto (una colección de valores únicos) que podremos representar como una lista desordenada. Esta posiblemente sea la peor implementación en términos de eficiencia. Aquí la sintaxis.

insert(X, []) -&gt
    [X];
insert(X,Set) -&gt
    case lists:member(X,Set) of
        true -&gt Set;
        false -&gt [X|Set]
    end.

Si enviamos una lista vacia y un término x para ser agregado, entonces nos retornará una lista solo conteniendo el valor x. De otra manera la función lists:member/2 verifica si un elemento es parte de un lista y retorna true si existe o false si no lo está. En caso de que ya exista el elemento en el conjunto entonces no lo modificamos. Sino, agregamos x al conjunto como el primer elemento de la lista.

En este caso la coincidencia de patrones, fue realmente simple. Aquí una más compleja,

beach(Temperature) -&gt
    case Temperature of
        {celsius, N} when &gt= 20, N =&lt 45 -&gt
            'favorable';
        {kelvin, N} when N &gt= 293, N =&lt 318 -&gt
            'scientifically favorable';
        {fahrenheit, N} when N &gt= 68, N =&lt 113 -&gt
            'favorable in the US';
        _ -&gt
            'avoid beach'
    end.

Aquí la respuesta a "es el momento adecuado para ir a la playa?", dadas 3 temperaturas diferentes: grados Celsius, Kelvin, Fahrenheit. La coincidencia de patrones y guardas son combinadas en orden para devolver una respuesta satisfactoria a todos los usos. Como apuntamos anteriormente, las expresiones "case ... of" son la misma cosa que varias cabeceras de funciones con guardas. De hecho podriamos reescribir nuestro código de la siguiente manera.

beachf({celsius, N}) when N &gt= 20, N =&lt 45 -&gt
    'favorable';
...
beachf(_) -&gt
    'avoid beach'.

Esto genera la siguiente pregunta. Cuando deberiamos usar "if", "case ... of" o funciones para hacer expresiones condicionales.

Que utilizar?

Que utilizar es una pregunta dificil. La diferencia entre llamadas a funciones y 'case ... of' son minimas: de hecho son representadas de la misma manera a bajo nivel. y usar una o la otra tiene el mismo costo en términos de rendimiento. Una diferencia entre ambas es cuando más de un argumento necesita ser evaluado "function(A,B) -&gt ... end." puede tener guardas y valores que coincidan con A y B pero una expresión case necesitaría ser formulada de la siguiente manera:

case {A,B} of
    Pattern Guards -> ...
end.

Esta forma es raramente vista, y puede sorprender un poco al lector. En situaciones similares llamar a una función debería ser más apropiado. De igual manera la función insert/2 previamente escrita, es podría decirse la manera más limpia en lugar de tener una llamada a función para continua con la manejar la simple clausula true o false.

La otra pregunta es para que usar if, dado que cases y functions son los suficientemente flexibles para incluso abarcar if a través de guardas? Lo racional detrás del if es simple. Este fue agregado para que tener en el lenguaje una manera corta de tener guardas sin necesidad de escribir toda la coincidencia de patrones cuando no es necesaria.

[0] http://learnyousomeerlang.com/syntax-in-functions

sábado, 21 de febrero de 2015

Algoritmos y Programación - Python [0]

Problemas no computables [1]
Son aquellos problemas que nunca podrán ser resueltos por una computadora por más poderosa que sea.

Problemas intratables [2]
Son aquellos problemas que pueden ser resueltos pero que requieren de un enorme poder de computo y memoria.

Algoritmo
es cualquier metodo para obtener un resultado. [3]

Construcción de Programas

1. Analizar el problema
    Entender profundamente cual es el problema y dejarlo por escrito.

2. Especificar la solución
    Describir qué debe hacer el programa, especificar datos de entrada, de salida y la relación entre ellos.

3. Diseñar la solución
    Cómo vamos a resolver el problema, cuales son los algortimos y las estructuras de datos que usaremos.

4. Implementar el diseño
    Traducir en un lenguaje de programación el diseño.

5. Probar el programa.
    Diseñar un conjunto de pruebas para probar cada una de sus partes.

6. Mantener el programa
    Realizar los cambios en respuesta a nuevas demandas.

Todos estos pasos deben ser documentados.

[4] Guia para diseño

  • ¿Han visto este problema antes, aunque sea de manera ligeramente diferente?
  • ¿Conocen un problema relacionado? ¿Conocen un programa que puede ser útil?
  • Fijense en la especificación. Traten de encontrar un problema que les resulte familiar y que tenga la misma especificación o una parecido.
  • Acá hay un problema relacionado con el que ustedes tienen y que ya fue resuelto. ¿Lo pueden usar? ¿Puede usar sus resultados? ¿Pueden usar sus métodos? ¿Pueden agregarle alguna parte auxiliar a ese programa del que ya disponen?
  • Si no pueden resolver el propuesto, traten de resolver uno relacionado. ¿Pueden imaginarse uno relacionado que sea más fácil de resolver? ¿Uno más general? ¿Uno más especifico? ¿Un problema analogo?
  • ¿Pueden resolver una parte del problema? ¿Pueden sacar algo útil de los datos de entrada? ¿Pueden pensar que információn es útil para calcular las salidas? ¿De qué manera se pueden manipular las entradas y las salidas de modo tal que estén "más cerca" unas de las otras?
  • ¿Usaron todos los datos de entrada? ¿Usaron las condiciones especiales sobre los datos de entrada que aparecen en el enunciado? ¿Han tenido en cuenta todos los requisitos que se enuncian en la especificación?
Las funciones

Una función es un conjunto de instrucciones que llevan a cabo la solución de una parte particular del problema. Las funciones llevan ninguno, uno o más argumentos que son la parte variable que se debe definir en cada ejecución de la función. Es recomentable documentar las funciones ya que con el crecimiento del programa crece su complejidad y tener las funciones documentadas ayudar a la mantenibilidad.

Las variables y parametros que se declaran dentro de una función no existen fuera de ella. Por consiguiente en lenguajes como python de utiliza "return" para decirle a una función que el valor debe ser retornado al hilo principal para que el programa pueda utilzar esa salida para hacer otras tareas.

[0] Algoritmos y Programación - Python
[1][2][3] Algoritmos y Programación - Python  Pagina 9
[4] Algoritmos y Programación - Python  Pagina 28
[5] http://www.cs.kent.ac.uk/people/staff/sjt/Haskell_craft/HowToProgIt.html
[6] Algoritmos y Programación - Python  Pagina 30

lunes, 15 de diciembre de 2014

WebSocket Server Escalable Software

Pareciera que los servicios de WebSocket en la nube están de moda. Muchos proyectos proveen una abstracción sobre los websockets con un modelo de pago por transacción que además parece ser bastante caro es masomenos entendible por que diseñar un servicio de WebSocket o de Publish/Subscribe parecería bastente dificil en principio pero no es más dificil que armar una API Rest sin framework hoy en día. Algunos datos del software que creo necesario para lograr hacer un WebSocket server escalable en Python obviamente ya que es el lenguaje que nos gusta en este Blog.

Tornado[0], es un Framework para hacer aplicaciones Web principalmente pero también es muy conocido por tener una clase para manejo de WebSockets y hay benchmarks que lo comparan en velocidad de respuesta con nodejs un muy conocido lenguaje de programación basado en el motor v8 sobre el que corre javascript en los browsers. [1]

SockJS[2], es principalmente una libreria de javascript que emula el funcionamiento de WebSockets intentando dar la mayor compatibilidad con los browsers a la vez que intenta mantener una interfaz muy similar al WebSocket puro. El único que inconveniente en principio es que para implementar sockjs y aprovechar todas sus caracteristicas se necesita una implementación del lado del servidor.

sockjs-tornado[3], es una libreria que implementar sockjs sobre un servidor tornado, esto se hace cambiando la clase de la que se hereda el WebSocketHandler.

redis [4], es un base de datos en memoria que utiliza como estructura principal key-value, donde las peticiones son hechas a traves de una clave y devuelven un valor, muy util para utilizar como cache. Pero en el caso de un WebSocket server vamos a usar una de sus caracteristicas como Publish/Subscribe [5].

Antes de terminar con las tecnologias a usar voy contarles por que necesitamos un server con la modalidad de Publish/Subscribe. Una aplicación web masomenos bien diseñada puede escalar horizontalmente [6][7], gracias a que hay un tercero donde comparten la información, y esa es la base de datos comunmente relacional del tipo SQL. En cuanto al WebSocket cada conexión hecha contra un servidor puede (en su implementación normal) solamente comunicarse con otros websockets dentro del mismo servidor por eso la única manera para hablar entre diferentes servidores es agregando a un tercero que maneje algo parecido a canales de comunicación y a eso viene la funcionalidad de Publish/Subscribe, que nos ofrece tener canales donde dos servidores o más  pueden publicar y subscribirse y al subscribirse recibiran todos los mensajes que vayan a ese canal. De esa manera podemos levantar n servers sabiendo que todos los websockets aunque estén distribuidos si se subscriben al canal adecuado van a recibir los mensajes que les corresponden.

tornado-redis[8] es una implementación de redis para tornado, lo que permite generar un solo cliente por instancia de tornado y manejar las subscripciones desde ahí, además, tiene una clase de abstracción para manejar subscripciones basadas en sockjs-tornado[9]

Y eso creo que es todo ya tenemos todo un hermoso Stack de tecnologias para empezar a desarrollar nuestra aplicación web basada en Sockets ahora es cuestión de juntar todo y ponerse a escribir código.

[0] http://tornadoweb.org/
[1] http://swizec.com/blog/benchmarking-node-tornado-and-django-for-concurrency/swizec/1616
[2] http://sockjs.org/
[3] https://github.com/MrJoes/sockjs-tornado
[4] http://redis.io/
[5] http://redis.io/topics/pubsub
[6] http://blog.deploshark.com.ar/2014/01/otros-atributos-de-calidad-capitulo-12.html
[7] http://es.wikipedia.org/wiki/Escalabilidad#Escalabilidad_horizontal
[8] https://github.com/leporo/tornado-redis/
[9] https://github.com/leporo/tornado-redis#pubsub

domingo, 14 de septiembre de 2014

Aprendiendo Erlang parte 6 Modulos III

Más acerca de módulos

Antes de movernos a profundizar nuestro conocimiento en relación a como escribir funciones y algunos fragmentos de código, pero antes tenemos un poco más de información que te será útil en el futuro.

Una de las primeras son los metadatos de los módulos. Los atributos de los módulos son metadatos que describen el módulo en si mismo. Donde podemos encontrar estos metadatos cuando no tenemos acceso al código fuente? Bueno, el compilador nos ayuda con esto: cuando compilamos un módulo, este toma la mayoría de los atributos y los almacena en una función llamada module_info/0 . Así pueden ver los metadatos de un módulo.

9> useless:module_info().
[{exports,[{add,2},
           {hello,0},
           {greet_and_add_two,1},
           {module_info,0},
           {module_info,1}]},
 {imports,[]},
 {attributes,[{vsn,[174839656007867314473085021121413256129]}]},
 {compile,[{options,[]},
           {version,"4.6.2"},
           {time,{2009,9,9,22,15,50}},
           {source,"/home/ferd/learn-you-some-erlang/useless.erl"}]}]
10> useless:module_info(attributes).
[{vsn,[174839656007867314473085021121413256129]}]

El snippet anterior además muestra la función module_info/1 que permite solicitar una pieza especifica de información.

Sintaxis en funciones

Coincidencia de patrones

Ahora tenemos la habilidad de almacenar y compilar código, podemos empezar a escribir funciones más avanzadas. La primera función que vamos a escribir necesita saludar de manera diferente según el genero.  En la mayoría de los lenguajes podrías escribir algo así.

function saludar(Genero, Nombre)
    if Genero == masculino then
        printf("Hola, Sr. %s!", Nombre)
    else if Genero == femenino then
        printf("Hola Sra. %s!", Nombre)
    else
        printf("Hola, %s!", Nombre)

Con coincidencia de patrones (pattern matching), erlang te ayuda a no escribir tanto código similar. La misma función en erlang se vería como esto.

saludar(masculino, Nombre) ->
    io:format("Hola, Sr. ~s!", [Nombre]);
saludar(femenino, Nombre) ->
    io:format("Hola, Sra. ~s!", [Nombre]);
saludar(_, Nombre) ->
    io:format("Hola, ~s!", [Nombre]).

Hay que adminit que la función de impresión por pantalla es un poco más fea en erlang que entro lenguaje pero ese no es el punto. La diferencia principal aquí es que nosotros usamos la coincidencia de patrones para definir las dos partes de una función se debe utilizar y ligas los valores al mismo tiempo. Aquí no se necesita primero ligar los valores y entonces compararlos.
En lugar de:

function(Args)
    if X then
        Expression
    else if Y then
        Expression
    else
        Expression

Nosotros escribimos:

function(X) ->
    Expression;
function(Y) ->
    Expression;
function(_) ->
    Expression.

De esta manera se obtiene los mismo resultados pero con un estilo más declarativo. Cada una de estas declaraciones de funciones es llamada clausula de función. Las clausulas de función deben ser separadas con punto y coma ";"

La coincidencia de patrones en las funciones puede ser más complejo y poderoso que eso. Tal vez recuerdas de capitulos anteriores donde usabamos coincidencia de patrones patrones para encontrar la cabeza y cola de una lista. Vamos a hacer esto.  Creamos un modulo llamado funciones.

-module(functions).
-compile(export_all).

La primera función que vamos a escribir es head/1, que actua exactamente como erlang:hd/1 que toma una lista como argumento y retorna su primer elemento. Los haremos con la ayuda del signo "|".

head([H|_])  -> H.

Si vos escribis functions:head([1,2,3,4]) en la terminal (una vez que el modulo sea compilado), puedes esperar que te retorne el valor 1. Consecuentemente para obtener el segundo elemento de la lista, puedes crear la siguiente función.

second([_,X|_]) -> X.

La lista será deconstruida por erlang en orden a hacer coincidir los patrones. Intentalo en la terminal.

1> c(functions).
{ok, functions}
2> functions.head([1,2,3,4]).
1
3> functions.second([1,2,3,4]).
2

esto podría ser repetido en la lista tanto como quitas, pero es impráctico para cientos de valores. Esto se puede resolver escribiendo funciones recursivas, aunque veremos como más adelante. Por ahora concentremonos más en la coincidencia de patrones. El concepto de variables libres y llenas nosotros los discutimos anteriormente, esto es así también para las funciones, podemos entonces comparar y conocer si dos parametros pasados a una función son lo mismo o no. Para esto, crearemos una función llamada same/2 que toma dos argumentos y dice si son identicos.

same(X,X) ->
    true;
save(_,_) ->
    false.

Y es así de simple.

Guardas, guardas.

Las guardas son clausulas adicionales que pueden ir en la cabecera de una función para hacer la coincidencia de patrones más expresiva. Como mencionamos antes la coincidencia de patrones está de alguna manera limitada ya que no puede expresar cosas como rangos de valores o cierto tipo de datos. Unos conceptos que no podemos representar serían los siguientes. Es este jugador de 12 años demasiado petizo para jugar con los profesionales? Es esta distincia demasiado larga para caminar sobre tus manos? Eres demasiado viejo o demasiado joven para manejar un auto?. No puedes responder esto simplemente con coincidencia de patrones. Se puede representar la pregunta sobre el manejo de un auto de la siguiente manera.

old_enough(0) -> false;
old_enough(1) -> false;
old_enough(2) -> false;
...
old_enough(14) -> false;
old_enough(15) -> false;
old_enough(_) -> true.

Pero esto es increiblemente impráctico. Puedes hacerlo si lo quieres, pero trabajarás solo en tu código por siempre. Si quieres eventualmente hacer amigos, entonces debes usar el modulo de guardas  así podremos escribir la pregunta sobre el manejo de la siguiente manera.

old_enough(X) when X >= 16 -> true;
old_enough(_) -> false.

y listo. Como puedes ver es mucho más limpio y corto. Notarás que la regla básica de una guarda es que debe retornar true cuando es correcta, la guarda puede fallar si retorna false o si lanza una excepción. Supongamos ahora que no queremos tener en cuenta a las personas que son mayores de 104 años. Entonces deberiamos cuidarnos de eso, pero como?, simplemente agregando una segunda guarda.

right_age(X) when X >= 16, X <= 104 ->
    true;
right_age(_) ->
    false.

La "," funciona como un "y tambien", y el punto y coma ";"  funciona como un "o sino"


http://learnyousomeerlang.com/modules#mode-about-modules

viernes, 5 de septiembre de 2014

WeUseShort - Un acortador de Urls con tu propio dominio

Con la intención de probar AngularJS en algunas cuestiones y hacer algo sencillo en Flask y Redis para probar esas tecnologías fue que me dispuse a hacer el pequeño ejemplo de un Acortador de Urls que cualquier puede usar para acortar las urls que envia en sus redes sociales. Si bien twitter lo hace por default a veces es bonito tener tu propio subdominio acortado con tu propia redirección. Por eso está es una aplicación muy sencilla que cumple su objetivo y que está abierta a que la mejores y propongas cambios.


Caracteristicas:
Acorta urls usando tu propio dominio.
Redis como db.
Cuenta los hits por cada url.
Login sencillo.
Ultrarápido

Mejoras pensadas para un futuro:

Mapa de Geolocalización por IP origen. (GeoIP)
Redirección por tipo de dispositivo (user-agent?)

Si alguien quiere colaborar

https://github.com/gonzafirewall/WeUseShort

Están más que invitados.

viernes, 15 de agosto de 2014

Poderosa Edición Nro8 de #TheOriginalHacker

Esta edición [0] es poderosa, con el estilo simple y profundo que la caracteriza, Eugenia nos redobla la apuesta con distintos análisis de alto nivel en lo profesional y de bajo nivel en lo computacional.










PHP y el manejo de datos en memoria
Los lenguajes de programación de alto nivel generalmente administran la memoria por nosotros y los más desinteresados crean y usan variables sin preocuparse demasiado pero si vos sos de aquellos a los que SI les importa ese tipo de manejos. Bueno este artiulo es para vos.

PHP: Hacking, debugging, o simplemente diversión
La diversión, como la belleza en la programación pueden parecer (para algunos) conceptos mal usados cuando se trata de un proceso que en gran proporción es racional, pero, la creatividad y la humanidad que cada uno le imprime a su manera de escribir código, nos hace retomar esos conceptos en su forma más pura. Divertirse debuggeando o hackeando eso es lo que Eugenia nos propone en este articulo.
«Belleza es la API de la librería requests de Kenneth Reitz para Python[1]»

Software Libre y la diferencia con el software privativo desde la óptica intrínsecamente profesional y no política
Huff, un articulo profundo, simplemente tremendo.

La orientación a objetos y sus enfoques como estilo y paradigma.
Amo este articulo (no es por que los ejemplos estén escritos en Python bueno un poco sí) por que nuevamente se puede ver como con palabras sencillas se puede llegar profundo en cosas que normalmente se explican en libros enormes. (que igualmente no habría que dejar de leer).

En síntesis:

Una edición tremenda donde encontrarás como dije al principio un alto nivel profesional y un bajo nivel computacional.

Y nuevamente me gustaría decirle a Eugenia como le dije por Twitter [2].
Gracias por decidir ser Libre.

[0] http://originalhacker.org/
[1] http://docs.python-requests.org/en/latest/
[2] https://twitter.com/GonzaMartinez/status/500384932536262656

sábado, 9 de agosto de 2014

Autenticación en Apache2 contra DB SQL

Hace un par de días estaba limpiando modulos que estaban instalados innecesariamente en un WebServer y obviamente para deshabilitarlos sin romper demasiado me fuí leyendo para que funcionaba cada directiva. Acá les dejo como configurar una que me pareció cuando menos interesante.

mod_authn_dbd es una directiva para poder usar como backend de autenticación una base de datos de manera masomenos simple pero directamente desde el WebServer en este caso apache2.

Acá les dejo un ejemplo sacado de la doc oficial y resolviendo algunas cosas que en esa misma doc no se aclaran sobre como hacerlo en debian-based OS.

Instalamos los siguientes paquetes ya que es un servidor limpio recien instalado.

# Instalamos Apache y MySQL hasta acá nada raro es lo minimo
apt-get install apache2 mysql-server
#  Esta es la libreria que hacer la magia
apt-get install libaprutil1-dbd-mysql

No hace falta obviamente tener el servidor mysql en el mismo que el apache yo lo hago así para simplificar la configuración

# Se debe habilitar ese modulo
a2enmod authn_dbd

/etc/init.d/apache2 restart

luego en el conf correspondiente limitamos el Directory con la configuración como continua.

DBDriver mysql
DBDParams "host=localhost, dbname=midb,user=user_db,pass=pass_db,port=3306"

DBDMin 4
DBD Keep 8
DBDMax 20
DBDExptime 300

<Directory /var/www/privado>
    AuthType Basic
    AuthName "Mi Servidor"
    AuthBasicProvider dbd
    Require valid-user
    AuthDBDUserPWQuery \
        "SELECT password from auth_table where user = %s"
</Directory>

A mi no me resulto tan obvio pero la password del usuario tiene que ser encriptada en la DB con la función ENCRYPT de mysql.

Otra directiva interesante permite hacer login y logout pero esa los voy a dejar que se peleen ustedes con los problemas que pueda causa esa configuración.

Espero que les haya parecido cuando menos raro esto de autenticar desde el WebServer contra la DB directamente creo que yo ya le tengo una utilidad.

http://httpd.apache.org/docs/2.2/mod/mod_authn_dbd.html
http://httpd.apache.org/docs/current/mod/mod_authz_dbd.html