Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


Iniciando com o ORM Pony no Python II - Banco de Dados com Docker

Continuando a jornada com o ORM Pony, agora com outros Banco de Dados, em Docker

Logo do projeto Pony

https://ponyorm.org

No primeiro texto desta série, demos os primeiros passos com o ORM Pony, usando o SQLite como banco de dados.

Nesse texto vamos declarar uma tabela e usá-la em três bancos distintos: MySQL, Postgres e o bom e velho SQLite, usasndo docker.

Preparando o terreno

No Debian, distribuição que uso, instale os pacotes oficiais:


sudo apt install docker.io docker-compose

Caso sua distribuição não tenha algo similar, acesse https://www.docker.com/get-started para instruções sobre como fazer uma instalação com pacotes não específicos a uma distribuição.

Outro passo é criar um diretório para trabalharmos:


$ sudo mkdir -p /opt/pony

Darmos permissões para nós mesmos, já que está fora de nosso home:


$ sudo chown ${USER}: /opt/pony

E criar subdiretórios que armazenarão os dados de nossos bancos e código:


$ mkdir -p /opt/pony/data/{mysql,postgresql}
$ mkdir /opt/pony/src

Verificar é sempre legal:


$ tree /opt/pony
/opt/pony
├── data
│   ├── mysql
│   └── postgresql
└── src

4 directories, 0 files

Salve esse texto como /opt/pony/requirements.txt:


ipython
pony
PyMySQL

Salve esse conteúdo como /opt/pony/Dockerfile


FROM alpine
WORKDIR /code
RUN apk add --no-cache python3 py3-pip py3-psycopg2 bash sqlite postgresql-client mysql-client
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
RUN mkdir src

Esse arquivo define um container com uma imagem Python, para que trabalhemos sem instalar alterar os pacotes de nossa máquina. Optei por usar pacotes pré-compilados de Python para os drivers de banco de dados, para facilitar a vida e não ter que instalar compilador e afins.

Compartilhando os fontes:

E salve o seguinte conteúdo como /opt/pony/docker-compose.yml:


version: "3"
services:

    python:
      build: .
      volumes:
        - /opt/pony/src/:/code/src

    mysql:
      image: mysql
      command: --default-authentication-plugin=mysql_native_password
      restart: always
      environment:
        MYSQL_ROOT_PASSWORD: top123
      volumes:
        - /opt/data/mysql:/var/lib/mysql/

    postgres:
      image: postgres
      environment:
        POSTGRES_PASSWORD: top123
        PGDATA: /var/lib/postgresql/data/pgdata
      volumes:
        - /opt/data/postgres:/var/lib/postgresql/data/pgdata/

Salve esse arquivo como /opt/pony/src/models.py:


from pony.orm import Database, db_session, PrimaryKey, Required


db = Database()


class Github(db.Entity):
    _table_ = "github"

    key = Required(str)
    value = Required(str)

E esse como /opt/pony/src/bombril.py:


import csv
import json
import urllib.request


def get_data():
    with urllib.request.urlopen('https://api.github.com/') as f:
        response = f.read()

    return json.loads(response)


def save_csv(data, filenename='github.csv'):
    with open(filename, 'w', newline='') as csvfile:
        fieldnames = ['id', 'key', 'value']

        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()

        id_value = 0
        for key, value in data.items():
            id_value += 1
            writer.writerow({'id': id_value, 'key': key, 'value': value})


def read_csv(filename='githb.csv'):
    with open('github.csv', newline='') as csvfile:
        reader = csv.DictReader(csvfile)
        return list(reader)

Feito isso, estando no diterório /opt/pony, podemos subir nosso laboratório de testes, executando o comando a seguir, que em sua primeira executação nos exigirá paciência e banda de Internet, para baixar tudo que é necessário:


$ docker-composer up

Seu terminal será inundado com informações, aproveite para hidratar-se e caminhar um pouco. Ao final de tudo, ele estará "truncado", com algo parecido a isso:


postgres_1| 2021-05-08 20:30:50.406 UTC [1] LOG:  database system is ready to accept connections

... muitas linhas entre essas mensagens

mysql_1 | 2021-05-08T20:32:03.868866Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for  connections. Version: '8.0.24'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.

Aconselho deixar quieto esse terminal para ver os logs, e abrir outro para trabalhar (pode-se iniciar com docker-compose up -d para manter o terminal livre e o processo em background).

Pode-se ver o que temos rodando:


$ docker-compose ps
     Name                    Command               State           Ports       
-------------------------------------------------------------------------------
pony_mysql_1      docker-entrypoint.sh --def ...   Up       3306/tcp, 33060/tcp
pony_postgres_1   docker-entrypoint.sh postgres    Up       5432/tcp           
pony_python_1     /bin/sh                          Exit 0                

Para criar os databases, nos conectaremos aos bancos de dados através do container Python, no qual instalamos os clientes para acesso a eles:


$ docker-compose run python bash
bash-5.1# psql -Upostgres -hpostgres
Password for user postgres: 
psql (13.2)
Type "help" for help.

postgres=# CREATE DATABASE github;
CREATE DATABASE
postgres=# \q

bash-5.1# mysql -uroot -p -hmysql
Enter password: 
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.24 MySQL Community Server - GPL

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> CREATE DATASE github;
Query OK, 1 row affected (0.301 sec)

MySQL [(none)]> quit
Bye
bash-5.1#

Por fim, vamos pegar alguns dados para inserir nos bancos. Vamos pegar algumas informações do Github, salvar em um arquivo CSV, e sempre que precisarmos, carregamos em memória num dicionário para usar. Ainda no container Python (se estiber fora, basta um docker-compose run python bash):


$ docker-compose run python bash
bash-5.1# cd /code/src
bash-5.1# ipython
Python 3.8.10 (default, May  6 2021, 00:05:59) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.23.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import bombril

In [2]: data = bombril.get_data()

In [3]: bombril.save_csv(data)

In [4]:                                                                                                                                                                   
Do you really want to exit ([y]/n)? y
bash-5.1# head github.csv 
id,key,value
1,current_user_url,https://api.github.com/user
2,current_user_authorizations_html_url,https://github.com/settings/connections/applications{/client_id}
3,authorizations_url,https://api.github.com/authorizations
4,code_search_url,"https://api.github.com/search/code?q={query}{&page,per_page,sort,order}"
5,commit_search_url,"https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}"
6,emails_url,https://api.github.com/user/emails
7,emojis_url,https://api.github.com/emojis
8,events_url,https://api.github.com/events
9,feeds_url,https://api.github.com/feeds

Como queríamos. Temos nosso laboratório preparado, enfim!

Conectando aos bancos com Pony

Agora sim, vamos usar nossos dados, criar uma descrição deles com Pony, e inserí-los em cada banco.

Começando com o sqlite (importnte notar o so do db_session para garantir o commit dos dados):


bash-5.1# ipython
Python 3.8.10 (default, May  6 2021, 00:05:59)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.23.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import bombril

In [2]: import models

In [3]: from pony.orm import db_session

In [4]: data = bombril.read_csv()

In [5]: db = models.db

In [6]: db.bind(provider='sqlite', filename='/code/src/github.db', create_db=True)

In [7]: db.generate_mapping(create_tables=True)

In [8]: with db_session:
   ...:     for reg in data:
   ...:         models.Github(**reg)
   ...:

In [9]:                                                                                                                                                                   
Do you really want to exit ([y]/n)?
bash-5.1# sqlite3 github.db
SQLite version 3.34.1 2021-01-20 14:10:07
Enter ".help" for usage hints.
sqlite> SELECT * FROM github LIMIT 2;
1|current_user_url|https://api.github.com/user
2|current_user_authorizations_html_url|https://github.com/settings/connections/applications{/client_id}

Partamos para os bancos de dados menos eficientes :)


bash-5.1# cd /code/src
bash-5.1# ipython
Python 3.8.10 (default, May  6 2021, 00:05:59)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.23.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import bombril

In [2]: import models

In [3]: from pony.orm import db_session

In [4]: data = bombril.read_csv()

In [5]: db = models.db

In [6]: db.bind(provider='mysql', host='mysql', user='root', passwd='top123', db='github')

In [7]: db.generate_mapping(create_tables=True)

In [8]: with db_session:
   ...:     for reg in data:
   ...:         models.Github(**reg)
   ...:

In [9]: models.Github[10].to_dict()
Out[9]:
{'id': 10,
 'key': 'followers_url',
 'value': 'https://api.github.com/user/followers'}

In [10]: import iportlib
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
<ipython-input-10-4e5d0833eb20> in <module>
----> 1 import iportlib

ModuleNotFoundError: No module named 'iportlib'

In [11]: import importlib

In [12]: importlib.reload(models)
Out[12]: <module 'models' from '/code/src/models.py'>

In [13]: db = models.db

In [14]: db.bind(provider='postgres', user='postgres', password='top123', host='postgres', database='github')

In [15]: db.generate_mapping(create_tables=True)

In [16]: with db_session:
    ...:     for reg in data:
    ...:         models.Github(**reg)
    ...:

In [17]: models.Github[10].to_dict()
Out[17]:
{'id': 10,
 'key': 'followers_url',
 'value': 'https://api.github.com/user/followers'}

In [18]: quit

Vamos abrir outro terminal e verificar as coisas:


bash-5.1# psql -Upostgres -hpostgres
Password for user postgres:
psql (13.2)
Type "help" for help.

postgres=# \c github
You are now connected to database "github" as user "postgres".
github=# SELECT COUNT(*) FROM github;
 count 
-------
    32
(1 row)

github=# SELECT * FROM github LIMIT 2;
 id |                 key                  |                              value                               
----+--------------------------------------+------------------------------------------------------------------
  1 | current_user_url                     | https://api.github.com/user
  2 | current_user_authorizations_html_url | https://github.com/settings/connections/applications{/client_id}
(2 rows)

github=# ^D\q
bash-5.1# mysql -uroot -p -hmysql
Enter password: 
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 8.0.25 MySQL Community Server - GPL

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> USE github;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MySQL [github]> SELECT COUNT(*) FROM github;
+----------+
| COUNT(*) |
+----------+
|       32 |
+----------+
1 row in set (0.001 sec)

MySQL [github]> SELECT * FROM github LIMIT 2;
+----+--------------------------------------+------------------------------------------------------------------+
| id | key                                  | value                                                            |
+----+--------------------------------------+------------------------------------------------------------------+
|  1 | current_user_url                     | https://api.github.com/user                                      |
|  2 | current_user_authorizations_html_url | https://github.com/settings/connections/applications{/client_id} |
+----+--------------------------------------+------------------------------------------------------------------+
2 rows in set (0.001 sec)

MySQL [github]> 

Como se pode perceber, com um mesmo código, fizemos a mesma coisa: inserir dados no banco, para bancos diferentes. Nems sempre isso é possível, nem sempre é desejável e confiável delegar ao ORM a decisão de como realizar essa tarefa, mas isso pode ser interessante em alguns casos, como, por exemplo, sistemas não tão complexos em que ter consultas SQLs espalhadas pelo código seja uma opção.

Materiais consultados

Consultei muito material para resolver umasérie de problemas que fui enfrentando no decorrer do desenvolvimento dessas ideias aqui apresentadas. E pra quem, como eu, estiver aprendendo, aconselhor fortemente consultar com calma as seguintes páginas:

Para as imagens docker
Para os scripts Python

Off-topic

E num movimento que eu mesmo não esperava, comecei a testar o editor micro: leve, fácil e bonitão, no lugar do vim. Recomendo:

https://micro-editor.github.io/