diff --git a/.pyup.yml b/.pyup.yml new file mode 100644 index 000000000..551ae40dd --- /dev/null +++ b/.pyup.yml @@ -0,0 +1,3 @@ +requirements: + - Pipfile + - Pipfile.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0944b0b06..6694375b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,9 @@ language: python python: - - 3.6 + - 3.7 install: - - pip install -q -r requirements-dev.txt codecov + - pip install -q pipenv codecov + - pipenv sync --dev script: - flake8 - pytest libpythonpro --cov=libpythonpro diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..af5c7333d --- /dev/null +++ b/Pipfile @@ -0,0 +1,17 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +flake8 = "*" +pytest = "*" +coverage = "*" +pytest-cov = "*" +pytest-mock = "*" + +[packages] +requests = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..8a647ca6f --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,233 @@ +{ + "_meta": { + "hash": { + "sha256": "36b4d15ab997722625c5463b1a4e8164dfc3dd1788d6e7e90b62a7d7b5ed6a50" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", + "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" + ], + "version": "==2019.6.16" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "index": "pypi", + "version": "==2.22.0" + }, + "urllib3": { + "hashes": [ + "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", + "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" + ], + "version": "==1.25.3" + } + }, + "develop": { + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "coverage": { + "hashes": [ + "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", + "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", + "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", + "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", + "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", + "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", + "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", + "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", + "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", + "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", + "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", + "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", + "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", + "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", + "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", + "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", + "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", + "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", + "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", + "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", + "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", + "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", + "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", + "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", + "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", + "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", + "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", + "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", + "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", + "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", + "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" + ], + "index": "pypi", + "version": "==4.5.3" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "flake8": { + "hashes": [ + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" + ], + "index": "pypi", + "version": "==3.7.7" + }, + "importlib-metadata": { + "hashes": [ + "sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7", + "sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db" + ], + "version": "==0.18" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", + "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a" + ], + "markers": "python_version > '2.7'", + "version": "==7.0.0" + }, + "packaging": { + "hashes": [ + "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", + "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" + ], + "version": "==19.0" + }, + "pluggy": { + "hashes": [ + "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", + "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c" + ], + "version": "==0.12.0" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "pyparsing": { + "hashes": [ + "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", + "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" + ], + "version": "==2.4.0" + }, + "pytest": { + "hashes": [ + "sha256:4a784f1d4f2ef198fe9b7aef793e9fa1a3b2f84e822d9b3a64a181293a572d45", + "sha256:926855726d8ae8371803f7b2e6ec0a69953d9c6311fa7c3b6c1b929ff92d27da" + ], + "index": "pypi", + "version": "==4.6.3" + }, + "pytest-cov": { + "hashes": [ + "sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", + "sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a" + ], + "index": "pypi", + "version": "==2.7.1" + }, + "pytest-mock": { + "hashes": [ + "sha256:43ce4e9dd5074993e7c021bb1c22cbb5363e612a2b5a76bc6d956775b10758b7", + "sha256:5bf5771b1db93beac965a7347dc81c675ec4090cb841e49d9d34637a25c30568" + ], + "index": "pypi", + "version": "==1.10.4" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "zipp": { + "hashes": [ + "sha256:8c1019c6aad13642199fbe458275ad6a84907634cc9f0989877ccc4a2840139d", + "sha256:ca943a7e809cc12257001ccfb99e3563da9af99d52f261725e96dfe0f9275bc3" + ], + "version": "==0.5.1" + } + } +} diff --git a/README.md b/README.md index cf235dd67..676c14d7f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # libpythonpro + Módulo para exemplificar construção de projetos Python no curso PyTools Nesse curso é ensinado como contribuir com projetos de código aberto @@ -15,19 +16,20 @@ Suportada versão 3 de Python Para instalar: ```console -python3 -m venv .venv -source .venv/bin/activate -pip install -r requirements-dev.txt +pip install pipenv +pipenv install --dev ``` Para conferir qualidade de código: ```console -flake8 - +pipenv run flake8 ``` Tópicos a serem abordados: 1. Git 2. Virtualenv - 3. Pip \ No newline at end of file + 3. Pip + 4. Mock + 5. Pipenv + \ No newline at end of file diff --git a/libpythonpro/github_api.py b/libpythonpro/github_api.py index 7d29e4e68..8b0fbafb9 100644 --- a/libpythonpro/github_api.py +++ b/libpythonpro/github_api.py @@ -11,7 +11,3 @@ def buscar_avatar(usuario): url = f'https://api.github.com/users/{usuario}' resp = requests.get(url) return resp.json()['avatar_url'] - - -if __name__ == '__main__': - print(buscar_avatar('renzon')) diff --git a/libpythonpro/spam/db.py b/libpythonpro/spam/db.py new file mode 100644 index 000000000..ce90ca60b --- /dev/null +++ b/libpythonpro/spam/db.py @@ -0,0 +1,31 @@ +from time import sleep + + +class Sessao: + contador = 0 + usuarios = [] + + def salvar(self, usuario): + Sessao.contador += 1 + usuario.id = Sessao.contador + self.usuarios.append(usuario) + + def listar(self): + return self.usuarios + + def roll_back(self): + self.usuarios.clear() + + def fechar(self): + pass + + +class Conexao: + def __init__(self): + sleep(1) + + def gerar_sessao(self): + return Sessao() + + def fechar(self): + pass diff --git a/libpythonpro/spam/enviador_de_email.py b/libpythonpro/spam/enviador_de_email.py index 684f833d5..fa0952223 100644 --- a/libpythonpro/spam/enviador_de_email.py +++ b/libpythonpro/spam/enviador_de_email.py @@ -1,3 +1,9 @@ class Enviador: def enviar(self, remetente, destinatario, assunto, corpo): - return 'renzo@python.pro.br' + if '@' not in remetente: + raise EmailInvalido(f'Email de remetente inválido: {remetente}') + return remetente + + +class EmailInvalido(Exception): + pass diff --git a/libpythonpro/spam/main.py b/libpythonpro/spam/main.py new file mode 100644 index 000000000..f81821ae4 --- /dev/null +++ b/libpythonpro/spam/main.py @@ -0,0 +1,13 @@ +class EnviadorDeSpam: + def __init__(self, sessao, enviador): + self.sessao = sessao + self.enviador = enviador + + def enviar_emails(self, remetente, assunto, corpo): + for usuario in self.sessao.listar(): + self.enviador.enviar( + remetente, + usuario.email, + assunto, + corpo + ) diff --git a/libpythonpro/spam/modelos.py b/libpythonpro/spam/modelos.py new file mode 100644 index 000000000..093b35a2a --- /dev/null +++ b/libpythonpro/spam/modelos.py @@ -0,0 +1,5 @@ +class Usuario: + def __init__(self, nome, email): + self.email = email + self.nome = nome + self.id = None diff --git a/libpythonpro/tests/test_spam/conftest.py b/libpythonpro/tests/test_spam/conftest.py new file mode 100644 index 000000000..a16f3970a --- /dev/null +++ b/libpythonpro/tests/test_spam/conftest.py @@ -0,0 +1,20 @@ +import pytest + +from libpythonpro.spam.db import Conexao + + +@pytest.fixture(scope='session') +def conexao(): + # Setup + conexao_obj = Conexao() + yield conexao_obj + # Tear Down + conexao_obj.fechar() + + +@pytest.fixture +def sessao(conexao): + sessao_obj = conexao.gerar_sessao() + yield sessao_obj + sessao_obj.roll_back() + sessao_obj.fechar() diff --git a/libpythonpro/tests/test_spam/test_enviador_de_email.py b/libpythonpro/tests/test_spam/test_enviador_de_email.py index 325b5a393..c72a0bbe9 100644 --- a/libpythonpro/tests/test_spam/test_enviador_de_email.py +++ b/libpythonpro/tests/test_spam/test_enviador_de_email.py @@ -1,4 +1,6 @@ -from libpythonpro.spam.enviador_de_email import Enviador +import pytest + +from libpythonpro.spam.enviador_de_email import EmailInvalido, Enviador def test_criar_enviador_de_email(): @@ -6,12 +8,31 @@ def test_criar_enviador_de_email(): assert enviador is not None -def test_remetente(): +@pytest.mark.parametrize( + 'remetente', + ['foo@bar.com.br', 'renzo@python.pro.br'] +) +def test_remetente(remetente): enviador = Enviador() resultado = enviador.enviar( - 'renzo@python.pro.br', + remetente, 'luciano@python.pro.br', 'Cursos Python Pro', 'Primeira turma Guido Von Rossum aberta.' ) - assert 'renzo@python.pro.br' in resultado + assert remetente in resultado + + +@pytest.mark.parametrize( + 'remetente', + ['', 'renzo'] +) +def test_remetente_invalido(remetente): + enviador = Enviador() + with pytest.raises(EmailInvalido): + enviador.enviar( + remetente, + 'luciano@python.pro.br', + 'Cursos Python Pro', + 'Primeira turma Guido Von Rossum aberta.' + ) diff --git a/libpythonpro/tests/test_spam/test_envio_para_base_de_usuarios.py b/libpythonpro/tests/test_spam/test_envio_para_base_de_usuarios.py new file mode 100644 index 000000000..d2db43893 --- /dev/null +++ b/libpythonpro/tests/test_spam/test_envio_para_base_de_usuarios.py @@ -0,0 +1,49 @@ +from unittest.mock import Mock + +import pytest + +from libpythonpro.spam.main import EnviadorDeSpam +from libpythonpro.spam.modelos import Usuario + + +@pytest.mark.parametrize( + 'usuarios', + [ + [ + Usuario(nome='Renzo', email='renzo@python.pro.br'), + Usuario(nome='Luciano', email='renzo@python.pro.br') + ], + [ + Usuario(nome='Renzo', email='renzo@python.pro.br') + ] + ] +) +def test_qde_de_spam(sessao, usuarios): + for usuario in usuarios: + sessao.salvar(usuario) + enviador = Mock() + enviador_de_spam = EnviadorDeSpam(sessao, enviador) + enviador_de_spam.enviar_emails( + 'renzo@python.pro.br', + 'Curso Python Pro', + 'Confira os módulos fantásticos' + ) + assert len(usuarios) == enviador.enviar.call_count + + +def test_parametros_de_spam(sessao): + usuario = Usuario(nome='Renzo', email='renzo@python.pro.br') + sessao.salvar(usuario) + enviador = Mock() + enviador_de_spam = EnviadorDeSpam(sessao, enviador) + enviador_de_spam.enviar_emails( + 'luciano@python.pro.br', + 'Curso Python Pro', + 'Confira os módulos fantásticos' + ) + enviador.enviar.assert_called_once_with( + 'luciano@python.pro.br', + 'renzo@python.pro.br', + 'Curso Python Pro', + 'Confira os módulos fantásticos' + ) diff --git a/libpythonpro/tests/test_spam/test_github_api.py b/libpythonpro/tests/test_spam/test_github_api.py new file mode 100644 index 000000000..21ce9c5d0 --- /dev/null +++ b/libpythonpro/tests/test_spam/test_github_api.py @@ -0,0 +1,28 @@ +from unittest.mock import Mock + +import pytest + +from libpythonpro import github_api + + +@pytest.fixture +def avatar_url(mocker): + resp_mock = Mock() + url = 'https://avatars3.githubusercontent.com/u/402714?v=4' + resp_mock.json.return_value = { + 'login': 'renzo', 'id': 402714, + 'avatar_url': url, + } + get_mock = mocker.patch('libpythonpro.github_api.requests.get') + get_mock.return_value = resp_mock + return url + + +def test_buscar_avatar(avatar_url): + url = github_api.buscar_avatar('renzo') + assert avatar_url == url + + +def test_buscar_avatar_integracao(): + url = github_api.buscar_avatar('renzon') + assert 'https://avatars3.githubusercontent.com/u/3457115?v=4' == url diff --git a/libpythonpro/tests/test_spam/test_usuarios.py b/libpythonpro/tests/test_spam/test_usuarios.py new file mode 100644 index 000000000..09b523445 --- /dev/null +++ b/libpythonpro/tests/test_spam/test_usuarios.py @@ -0,0 +1,17 @@ +from libpythonpro.spam.modelos import Usuario + + +def test_salvar_usuario(sessao): + usuario = Usuario(nome='Renzo', email='renzo@python.pro.br') + sessao.salvar(usuario) + assert isinstance(usuario.id, int) + + +def test_listar_usuarios(sessao): + usuarios = [ + Usuario(nome='Renzo', email='renzo@python.pro.br'), + Usuario(nome='Luciano', email='renzo@python.pro.br') + ] + for usuario in usuarios: + sessao.salvar(usuario) + assert usuarios == sessao.listar() diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index d08fc4414..000000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,19 +0,0 @@ -flake8==3.5.0 -mccabe==0.6.1 -pycodestyle==2.3.1 -pyflakes==1.6.0 - -# Deps do pytest -py==1.5.3 -more-itertools==4.1.0 -six==1.11.0 -attrs==18.1.0 -pluggy==0.6.0 -pytest==3.5.1 - - -#Converage -coverage==4.5.1 -pytest-cov==2.5.1 - --r requirements.txt \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 585fa8413..000000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -certifi==2018.4.16 -chardet==3.0.4 -idna==2.6 -requests==2.18.4 -urllib3==1.22 diff --git a/setup.py b/setup.py index c5fa366d4..1fea97a65 100644 --- a/setup.py +++ b/setup.py @@ -70,9 +70,8 @@ def find_package_data( or fn.lower() == pattern.lower()): bad_name = True if show_ignored: - print >> sys.stderr, ( - "Directory %s ignored by pattern %s" - % (fn, pattern)) + print("Directory %s ignored by pattern %s" % + (fn, pattern), file=sys.stderr) break if bad_name: continue @@ -93,9 +92,8 @@ def find_package_data( or fn.lower() == pattern.lower()): bad_name = True if show_ignored: - print >> sys.stderr, ( - "File %s ignored by pattern %s" - % (fn, pattern)) + print("File %s ignored by pattern %s" % + (fn, pattern), file=sys.stderr) break if bad_name: continue @@ -119,7 +117,7 @@ def find_package_data( long_description_content_type='text/markdown', author=AUTHOR, author_email=AUTHOR_EMAIL, - license=read('LICENSE'), + license="GNU AFFERO GENERAL PUBLIC LICENSE", url=URL, packages=find_packages(exclude=["tests.*", "tests"]), package_data=find_package_data(PACKAGE, only_in_packages=False),