Vícios e amores servem para preencher o vazio
Introdução
Com este projeto planejo fazer um updater, ou seja, um utilitário que atualiza o seu programa em Python. Pessoalmente eu não tenho tanta familiaridade com essa linguagem então estarei aprendendo junto vocêsEsse tutorial será dividido em partes que serão lançadas irregularmente.
Funcionamento
Esse projeto funcionará da seguinte maneira:
- Um documento XML online que será acessado para buscar as informações;
- Um arquivo .zip com os arquivos que serão baixados para servir como atualização;
- O nosso projeto em python que acessará o XML, deletará e atualizará (baixando o .zip) os arquivos listados lá.
O XML consiste de um nó principal, que chamarei de ventura (pois este é o nome que escolhi para o projeto, entendedores entenderão) e dentro terá os nós update que contém o atributo com a versão e os nós com o link de download (url) para a atualização e um com a lista de pastas e arquivos a serem deletados (delete). As pastas/arquivos serão separados por uma virgula e serão consideradas pastas nomes com uma barra inversa "/" no final.
Código:
<?xml version="1.0" encoding="UTF-8"?>
<ventura>
<update version="0.1">
<url>https://um.link</url>
<delete>file_to_delete1.txt,folder1\</delete>
</update>
</ventura>
Preparativos
Precisaremos, claro, do Python. No caso eu estou usando o Python 3.6.1 mas você pode clicar aqui para baixar a última versão disponível. Só tenha em mente que o Python muda consideravelmente de uma principal versão para outra
Depois, tenha certeza de criar o seu documento XML e hospeda-lo em um lugar que permita você acessa-lo diretamente (assim), para isso estarei usando o Gist GitHub mas creio que o Pasterbin e genéricos permitam você fazer o mesmo.
Igualmente precisará de acesso direto (link direto) ao .zip que terá que hospedar. Isso pode ser obtido com o Dropbox/Google Drive/Microsoft Drive deixando o arquivo publico, estarei usando o GitHub para isso já que tenho um .zip de outro projeto que armazenado (veja o arquivo / link direto do arquivo) que será o que usarei.
Com isso, nossas preparações estão completas. Let's Code!
1º Passo - Main & Zip
Antes de qualquer coisa digo que tenho a convenção de colocar "h" antes do nome dos arquivos que tenham nome comum pois me já aconteceu de haver colisões entre meus módulos (nome bonito para arquivo) e os do Python.
Começaremos criando a rotina para extrair o arquivo .zip: Crie a pasta de seu projeto e os arquivos main.py (que será o ponto de entrada do programa) e hzip.py (responsável pela rotina de extração).
Em hzip importe zipfile, modulo que manipula arquivos .zip e os, que são diversas interfaces com o sistema operacional.
Código:
import zipfile
import os
Logo em seguida criaremos a única função desse arquivo, extract, que receberá dois argumentos; um com o local do arquivo .zip e outro com o local onde será extraído:
Código:
def extract(file_url, path_to_extract):
# carrega o arquivo .zip especificado
zip_file = zipfile.ZipFile(file_url, 'r')
# extrai o conteudo do .zip
zip_file.extractall(path_to_extract)
# fecha o arquivo (no caminho especificado) e libera os recursos usados
zip_file.close()
# apaga o arquivo
os.remove(file_url)
No main vamos importar o nosso hzip:
Código:
import hzip
Agora você pode fazer um teste para verificar se está funcionando colocando um arquivo .zip na mesma pasta e adicionando o seguinte código:
Código:
# extrai o "arquivo".zip na pasta onde está o arquivo.
hzip.extract('arquivo.zip', '')
Agora vamos adicionar a função main e adicionar isso dentro (note que mudei o nome do arquivo para um que seja mais coerente, será esse o nome com qual salvaremos o arquivo da internet):
Código:
def main():
hzip.extract('update.zip', '')
# Só executa se o arquivo for chamado diretamente ao invés de importado.
if __name__ == '__main__':
main()
2º Passo - Path
Agora criaremos um arquivo para com as rotinas de manipulação de arquivos, o chamarei de hpath.py.
Iniciemos importando pathlib, para operações com diretórios, shutil, para operações com arquivos e a já conhecida os:
Código:
import pathlib
import shutil
import os
Agora criemos a função para criar uma pasta, passando o caminho (+ nome dela) por parâmetro:
Código:
def create_dir(dir):
# dir recebe o nome da pasta, sou seja se for passado "pasta1\pasta2" então ele receberá "pasta2"
dir = os.path.dirname(dir)
# retorna se a pasta não possuir nome
if dir == '':
return
# cria um objeto do tipo path passando o nosso paramêtro (e deixa no formato adequado para o sistema)
path = pathlib.Path(dir)
# cria a pasta, os parâmetros dizem criar as subpastas e para não fazer nada se ela existir (ou caso jogaria uma exceção)
path.mkdir(parents = True, exist_ok = True)
Agora vamos criar a função que verifica se é um arquivo ou uma pasta, recebendo o parâmetro da string a ser verificada:
Nota: o Python tem funções que fazem isso mas elas funcionam somente com o caminho absoluto, mas ela não convém aqui mesmo se não fosse o caso já que nós definimos que queremos que ele considere pasta uma string que contenha uma barra inversa no final, então convém criarmos essa função.
Código:
def is_folder(text):
# retorna verdadeiro se o texto acaba com uma barra inversa e falso se não
return text.endswith("\\")
Agora adicionaremos as duas funções que se encarregarão de apagar pastas e arquivos, passando uma string em cada um representando a pasta/arquivo:
Código:
def remove_file(file):
# verifica se o arquivo existe
if os.path.isfile(file):
# apaga o arquivo
os.remove(file)
def remove_folder(folder):
# verifica se a pasta existe
if os.path.isdir(folder):
# apaga a pasta (mesmo com arquivos dentro)
shutil.rmtree(folder)
E por fim, a função que receberá um array de nomes de arquivos e pasta a serem apagados, darei o nome de delete_files (não confundam com o remove_file que apaga um arquivo):
Código:
def delete_files(files):
# navega em cada item de files usando file para representar o valor atual
for file in files:
# verifica se o valor é uma pasta (função explicada anteriormente)
if is_folder(file):
# chama a função que remove pastas para apagá-lo
remove_folder(file)
else:
# chama a função que remove arquivos para apagá-lo
remove_file(file)
Aftermath
Irei parar por aqui pois o tópico já está ficando grande. Espero que gostem, até a parte dois.
E cá estamos na última parte \o/. Espero que isso tenha ajudado você a entender melhor como o Python funciona, pois me ajudou. Sem mais delongas:
4º Passo - Package
Agora vamos transformar esse projeto em um package para que ele possa ver usado por outros projetos. Primeiro renomeie o arquivo main para o nome que queres para seu projeto (pois não faz sentido alguém importar o main), o eu será ventura. Depois adicione o nome do projeto (o da pasta) antes do nome de cada importação que fizemos de nossos arquivos. Isso será necessário para que eles possam ser importados mesmo que o caminho onde o programa está rodando é outro. Também remova o import do sys pois não usaremos mais argumentos de linha de comando.
Outra coisa que faremos é adicionar "_" no nome de cada arquivo exceto o mais para que o usuário saiba que esses arquivos não foram projetados para serem usados por eles.
Note que a importação do _hxml não tem "ventura." pois nesse caso só um ponto basta.
ventura.py (antigo main.py)
Código:
from ._hxml import VenturaXML
import ventura._hzip as hzip
import ventura._hweb as hweb
import ventura._hpath as hpath
_hpath.py (antigo hpath.py)
Código:
import ventura._hpath as hpath
Agora crie um arquivo chamado __init__.py, este arquivo não precisa conter nada dentro, ele serve para que o diretório seja considerado um package.
Parabéns! Você criou um package, tente adicionar a pasta dele em seu projeto e importa-lo em um arquivo seu (exemplo: import ventura).
5º Passo - Publicação
Agora prepararemos para que este package possa ser publicado [url=https://pypi.org]PyPI, o lugar onde armazena-se packages para que sejam baixados. Mas especificamente publicaremos no Test PyPI, subdomínio para testes. Eu me basearei neste tutorial.
Crie uma subpasta com o nome do projeto e mova todos os arquivos para lá, mas fique na pasta onde está pois nela que criaremos os próximos arquivos que precisamos para publicar o projeto.
Aqueles acostumados com git entenderão bem o que faremos em seguida: crie README.md, LICENSE (arquivo sem extensão) e .gigignore (arquivo sem o nome). No README escreva uma descrição do projeto (aprenda aqui), em LICENSE, a licença (que no meu caso usei a MIT, ela é só um arquivo de texto normal), o gitignore é onde você escreverá todas as pastas e arquivos que serão ignorados na hora de subir o projeto para o repositório (saiba como).
O próximo arquivo que criaremos é o setup.py, este é o responsável por armazenar informações do projeto. Abaixo deixarei um modelo (que não tem todas as informações possíveis mas tem as essenciais).
Código:
from setuptools import setup
# o método que será chamado pelo "empacotador"
setup(
# nome do pacote
name = 'ventura',
# versão do pacote
version = '0.1',
# uma descrição, existe um descrição mais completa que podemos fazer em formato
#markdown usando o texto do README mas não usaremos
description = 'Simple xml-based updater in python 3.6 with no try-catch statement',
# link do projeto, geralmente o repositório do projeto.
url = 'http://github.com/hermespasser/ventura',
# nome do autor
author = 'Hermes Passer',
# email do autor
author_email = 'hermespasser@gmail.com',
# a licença escolhida para o projeto
license = 'MIT',
# os pacotes inseridos, ou coloque o mesmo valor que colocou em name ou você pode
# procurar dinamicamente importando o find_packages() do setuptools
packages = ['ventura']
)
Agora vamos iniciar o processo de criar a distribuição e enviar. Rode python -m pip install --user --upgrade setuptools wheel só para ter certeza que o setuptools e wheel estão instalados e devidamente atualizados.
Agora vamos efetivamente criar a distribuição, para isso devemos está na mesma pasta que o setup.py está instalado. Rode python setup.py sdist bdist_wheel e será criada a pasta dist, que são os arquivos que serão enviados.
Para subir o package para o Test PyPI precisaremos criar uma conta lá, clique aqui para fazer isso. Depois precisaremos baixar e atualizar o twine, um utilitário que simplifica o processo de envio: python -m pip install --user --upgrade twine.
Note: o twine ficará referenciado no PATH do sistema então precisaremos escrever todo o caminho até ele quando fomos usa-lo. Ele fica em c:\users\nome do usuário\appdata\local\programs\python\python36\Scripts.
Rode c:\users\nome do usuário\appdata\local\programs\python\python36\Scripts\twine upload --repository-url https://test.pypi.org/legacy/ dist/* para que o package seja subido para o PyPI, você precisará digitar o seu usuário e senha.
\o/ congrats \o/
Seu package está disponível para download, se você quiser baixa-lo para testar use python -m pip install --index-url https://test.pypi.org/simple/ projeto
Note que que o Test PyPI é para testes e por isso não é incomum que packages e contas sejam deletadas nele, se quer que seu projeto fique permanentemente online então precisará subir para o PyPI normal. Para isso você precisará registrar-se nele e omitir o --repository-url https://test.pypi.org/legacy/ no twine que fez anteriormente para subir o package e --index-url https://test.pypi.org/simple/ no comando pip anteriormente feito para baixar o package.
Aftermath & Exercícios
Parabéns por ter chegado até aqui! Qualquer dúvida é só perguntas nos comentários que tentarei responder.
Deixarei aqui os links para o projeto, no PyPI e GitHub. Você pode baixa-lo usando o comando pip install ventura
Agora deixarei alguns exercícios para você tentar em casa:
- Diminuir os efeitos colaterais do projeto usando blocos try-except (diga: retorne falso no update_if_is_need se alguma função vital não está funcionando, como no caso de faltar internet)
- O usuário pode estar versões atrás da atual, então crie um sistema que execute todas as atualizações na frente da sua.
Bem é isso, até a próxima. Talvez eu faça um vídeo com isso em funcionamento depois.