diff --git a/.gitignore b/.gitignore index f35cfb5..534bfa5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ /docs/_build/ /src/*.egg-info/ __pycache__/ +*.xls # Pycharm .idea/ diff --git a/README.rst b/README.rst index 8877a77..87a0caf 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,7 @@ Instale na sua máquina o Python 3.8.0 ou superior (versão 3.10 recomendada) pa Usuários do Windows devem baixar a versão `Windows x86-64 executable installer` e na tela de instalação marcar a opção `Add Python 3.8 to PATH`: -.. image:: docs/_images/winpath.png +.. image:: docs/images/winpath.png :width: 400 :alt: Checkbox PATH na instalação Windows diff --git a/docs/images/winpath.png b/docs/images/winpath.png new file mode 100644 index 0000000..c4abeb8 Binary files /dev/null and b/docs/images/winpath.png differ diff --git a/noxfile.py b/noxfile.py index 944ce1e..20b8e13 100644 --- a/noxfile.py +++ b/noxfile.py @@ -151,7 +151,7 @@ def coverage(session: Session) -> None: session.run("coverage", *args) -@session(python=python_versions) +@session(python=python_versions[0]) def typeguard(session: Session) -> None: """Runtime type checking using Typeguard.""" session.install(".") @@ -174,7 +174,7 @@ def xdoctest(session: Session) -> None: session.run("python", "-m", "xdoctest", *args) -@session(name="docs-build", python="3.10") +@session(name="docs-build", python=python_versions[0]) def docs_build(session: Session) -> None: """Build the documentation.""" args = session.posargs or ["docs", "docs/_build"] @@ -191,7 +191,7 @@ def docs_build(session: Session) -> None: session.run("sphinx-build", *args) -@session(python="3.10") +@session(python=python_versions[0]) def docs(session: Session) -> None: """Build and serve the documentation with live reloading on file changes.""" args = session.posargs or ["--open-browser", "docs", "docs/_build"] diff --git a/poetry.lock b/poetry.lock index c8c0efc..0da9219 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,14 +6,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "ansicon" -version = "1.89.0" -description = "Python wrapper for loading Jason Hood's ANSICON" -category = "main" -optional = false -python-versions = "*" - [[package]] name = "atomicwrites" version = "1.4.0" @@ -102,19 +94,6 @@ jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] python2 = ["typed-ast (>=1.4.3)"] uvloop = ["uvloop (>=0.15.2)"] -[[package]] -name = "blessed" -version = "1.19.0" -description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." -category = "main" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""} -six = ">=1.9.0" -wcwidth = ">=0.1.4" - [[package]] name = "certifi" version = "2021.10.8" @@ -380,19 +359,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "inquirer" -version = "2.9.1" -description = "Collection of common interactive command line user interfaces, based on Inquirer.js" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -blessed = ">=1.19.0" -python-editor = ">=1.0.4" -readchar = ">=2.0.1" - [[package]] name = "isort" version = "5.10.1" @@ -421,17 +387,6 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] -[[package]] -name = "jinxed" -version = "1.1.0" -description = "Jinxed Terminal Library" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -ansicon = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "livereload" version = "2.6.3" @@ -613,6 +568,17 @@ python-versions = ">=3.6.1" "ruamel.yaml" = ">=0.15" toml = "*" +[[package]] +name = "prompt-toolkit" +version = "3.0.24" +description = "Library for building powerful interactive command lines in Python" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +wcwidth = "*" + [[package]] name = "py" version = "1.11.0" @@ -724,14 +690,6 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] six = ">=1.5" -[[package]] -name = "python-editor" -version = "1.0.4" -description = "Programmatically open an editor, capture the result." -category = "main" -optional = false -python-versions = "*" - [[package]] name = "pytz" version = "2021.3" @@ -759,14 +717,6 @@ category = "dev" optional = false python-versions = ">=3.6" -[[package]] -name = "readchar" -version = "3.0.4" -description = "Utilities to read single characters and key-strokes" -category = "main" -optional = false -python-versions = "*" - [[package]] name = "requests" version = "2.26.0" @@ -1134,17 +1084,13 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "1.1" python-versions = ">=3.8,<4.0" -content-hash = "6230bcf867a2b208159cf6a1f48c9dc4f5b7514c638b61e3135c38a92fb28904" +content-hash = "86d692077a6d1c687c1ffbc73189843f33a686218ca8eadddc6d37cb647b7f9d" [metadata.files] alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] -ansicon = [ - {file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"}, - {file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"}, -] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -1169,10 +1115,6 @@ black = [ {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, ] -blessed = [ - {file = "blessed-1.19.0-py2.py3-none-any.whl", hash = "sha256:1f2d462631b2b6d2d4c3c65b54ef79ad87a6ca2dd55255df2f8d739fcc8a1ddb"}, - {file = "blessed-1.19.0.tar.gz", hash = "sha256:4db0f94e5761aea330b528e84a250027ffe996b5a94bf03e502600c9a5ad7a61"}, -] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, @@ -1313,10 +1255,6 @@ iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] -inquirer = [ - {file = "inquirer-2.9.1-py3-none-any.whl", hash = "sha256:f50876f5073c8c5fc482b44b8ef4e9720061498abeb352f62b5c94cacf0e43e2"}, - {file = "inquirer-2.9.1.tar.gz", hash = "sha256:65f0a8eaa8bcabd7ec65771c9f872bb2700753c23bbe8b8ff12efd264a9dbf5d"}, -] isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, @@ -1325,10 +1263,6 @@ jinja2 = [ {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, ] -jinxed = [ - {file = "jinxed-1.1.0-py2.py3-none-any.whl", hash = "sha256:6a61ccf963c16aa885304f27e6e5693783676897cea0c7f223270c8b8e78baf8"}, - {file = "jinxed-1.1.0.tar.gz", hash = "sha256:d8f1731f134e9e6b04d95095845ae6c10eb15cb223a5f0cabdea87d4a279c305"}, -] livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, ] @@ -1505,6 +1439,10 @@ pre-commit-hooks = [ {file = "pre_commit_hooks-4.1.0-py2.py3-none-any.whl", hash = "sha256:ba95316b79038e56ce998cdacb1ce922831ac0e41744c77bcc2b9677bf183206"}, {file = "pre_commit_hooks-4.1.0.tar.gz", hash = "sha256:b6361865d1877c5da5ac3a944aab19ce6bd749a534d2ede28e683d07194a57e1"}, ] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.24-py3-none-any.whl", hash = "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506"}, + {file = "prompt_toolkit-3.0.24.tar.gz", hash = "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6"}, +] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1545,13 +1483,6 @@ python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] -python-editor = [ - {file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"}, - {file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"}, - {file = "python_editor-1.0.4-py2.7.egg", hash = "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"}, - {file = "python_editor-1.0.4-py3-none-any.whl", hash = "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d"}, - {file = "python_editor-1.0.4-py3.5.egg", hash = "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77"}, -] pytz = [ {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, @@ -1595,10 +1526,6 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] -readchar = [ - {file = "readchar-3.0.4-py3-none-any.whl", hash = "sha256:ebe7b51edae808101ea54b63468e8bea87e0c246301a0100f7777219eb2eb844"}, - {file = "readchar-3.0.4.tar.gz", hash = "sha256:3ce642ade5b61efee273b3c4bf55c77a84398178842627c74fcb796e1666ab13"}, -] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, diff --git a/pyproject.toml b/pyproject.toml index af57838..f032623 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "irpf-investidor" -version = "2022.1" +version = "2021.1" description = "IRPF Investidor" authors = ["staticdev "] license = "MIT" @@ -19,8 +19,8 @@ Changelog = "https://github.com/staticdev/irpf-investidor/releases" python = ">=3.8,<4.0" click = ">=8.0.1" pandas = ">=1.3.5" +prompt-toolkit = ">=3.0.24" xlrd = ">=2.0.1" -inquirer = ">=2.9.1" [tool.poetry.dev-dependencies] Pygments = ">=2.10.0" @@ -73,7 +73,7 @@ show_error_codes = true show_error_context = true [[tool.mypy.overrides]] -module = ["inquirer", "pandas", "pytest_mock", "xlrd"] +module = ["pandas", "pytest_mock", "xlrd"] ignore_missing_imports = true [build-system] diff --git a/src/irpf_investidor/__main__.py b/src/irpf_investidor/__main__.py index a9e5349..2a63f71 100644 --- a/src/irpf_investidor/__main__.py +++ b/src/irpf_investidor/__main__.py @@ -29,15 +29,6 @@ def main() -> None: source_df = irpf_investidor.report_reader.clean_table_cols(source_df) source_df = irpf_investidor.report_reader.group_trades(source_df) trades = irpf_investidor.report_reader.get_trades(source_df) - - click.secho( - ( - "Para o cálculo dos emolumentos é necessário informar operações" - "realizadas em horário de leilão. Essa informação é obtida com " - "a sua corretora através de relatórios de ordem de compra." - ), - fg="green", - ) auction_trades = prompt.select_trades(trades) tax_df = irpf_investidor.report_reader.calculate_taxes(source_df, auction_trades) irpf_investidor.report_reader.output_taxes(tax_df) diff --git a/src/irpf_investidor/prompt.py b/src/irpf_investidor/prompt.py index 76cf578..7d29ed9 100644 --- a/src/irpf_investidor/prompt.py +++ b/src/irpf_investidor/prompt.py @@ -1,44 +1,36 @@ """Prompt module.""" from __future__ import annotations -from typing import Any +import prompt_toolkit.shortcuts as shortcuts -import inquirer +TITLE = "IRPF Investidor" -def select_trades(trades: list[tuple[str, int]]) -> Any: + +def select_trades(trades: list[tuple[int, str]]) -> list[int]: """Checkbox selection of auction trades. Args: - trades (list[tuple[str, int]]): list of all trades and indexes. + trades: list of all trades and indexes. Returns: - Any: list of indexes of selected auction trades. + list of string indexes of selected auction trades. """ + text = ( + "Informe as operações realizadas em horário de leilão para cálculo dos " + "emolumentos.\nEssa informação é obtida através de sua corretora." + ) while True: - selection = inquirer.prompt( - [ - inquirer.Checkbox( - "trades", - message=( - "Quais operações foram realizadas em horário de leilão? " - "(Selecione apertando espaço e ao terminar aperte enter)" - ), - choices=trades, - ) - ] - )["trades"] - if len(selection) == 0: - answer = inquirer.prompt( - [ - inquirer.List( - "", - message="Nenhuma operação selecionada.\nIsso está correto?", - choices=["Sim", "Não"], - ) - ] - )[""] - if answer == "Sim": + operations: list[int] = shortcuts.checkboxlist_dialog( + title=TITLE, + text=text, + values=trades, # type: ignore + ).run() + if not operations or len(operations) == 0: + confirmed = shortcuts.yes_no_dialog( + title=TITLE, text="Nenhuma operação selecionada.\nIsso está correto?" + ).run() + if confirmed: return [] else: - return selection + return operations diff --git a/src/irpf_investidor/report_reader.py b/src/irpf_investidor/report_reader.py index 638a95c..c67a13d 100644 --- a/src/irpf_investidor/report_reader.py +++ b/src/irpf_investidor/report_reader.py @@ -132,7 +132,7 @@ def clean_table_cols(source_df: pd.DataFrame) -> pd.DataFrame: return source_df.dropna(axis="columns", how="all") -def get_trades(df: pd.DataFrame) -> list[tuple[str, int]]: +def get_trades(df: pd.DataFrame) -> list[tuple[int, str]]: """Return trades representations. Args: @@ -147,7 +147,7 @@ def get_trades(df: pd.DataFrame) -> list[tuple[str, int]]: df = df.drop(columns=["Valor Total (R$)"]) list_of_list = df.astype(str).values.tolist() df = df.drop(columns=["total_cost_rs"]) - return [(" ".join(x), i) for i, x in enumerate(list_of_list)] + return [(i, " ".join(x)) for i, x in enumerate(list_of_list)] def group_trades(df: pd.DataFrame) -> pd.DataFrame: diff --git a/tests/test_prompt.py b/tests/test_prompt.py index b497a13..a2ba041 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -1,21 +1,43 @@ """Test cases for prompt module.""" +import pytest from pytest_mock import MockerFixture import irpf_investidor.prompt as prompt -def test_select_trades_empty(mocker: MockerFixture) -> None: +TRADES = [(0, "trade 1"), (0, "trade 2")] + + +@pytest.fixture +def mock_checkboxlist_dialog(mocker: MockerFixture) -> MockerFixture: + """Fixture for mocking shortcuts.checkboxlist_dialog.""" + return mocker.patch("prompt_toolkit.shortcuts.checkboxlist_dialog") + + +@pytest.fixture +def mock_yes_no_dialog(mocker: MockerFixture) -> MockerFixture: + """Fixture for mocking shortcuts.yes_no_dialog.""" + return mocker.patch("prompt_toolkit.shortcuts.yes_no_dialog") + + +def test_select_trades_empty( + mock_checkboxlist_dialog: MockerFixture, mock_yes_no_dialog: MockerFixture +) -> None: """It returns empty list.""" - mocker.patch( - "inquirer.prompt", - side_effect=[{"trades": []}, {"": "Não"}, {"trades": []}, {"": "Sim"}], - ) - trades = [("trade 1", 0), ("trade 2", 1)] - assert prompt.select_trades(trades) == [] + mock_checkboxlist_dialog.return_value.run.side_effect = [[], None] + mock_yes_no_dialog.return_value.run.side_effect = [False, True] + + result = prompt.select_trades(TRADES) + + assert mock_checkboxlist_dialog.call_count == 2 + assert mock_yes_no_dialog.call_count == 2 + assert result == [] -def test_select_trades_some_selected(mocker: MockerFixture) -> None: +def test_select_trades_some_selected(mock_checkboxlist_dialog: MockerFixture) -> None: """It returns list with id 1.""" - mocker.patch("inquirer.prompt", return_value={"trades": [1]}) - trades = [("trade 1", 0), ("trade 2", 1)] - assert prompt.select_trades(trades) == [1] + mock_checkboxlist_dialog.return_value.run.return_value = [1] + + result = prompt.select_trades(TRADES) + + assert result == [1] diff --git a/tests/test_report_reader.py b/tests/test_report_reader.py index 93aed4d..f58193f 100644 --- a/tests/test_report_reader.py +++ b/tests/test_report_reader.py @@ -160,8 +160,8 @@ def test_get_trades() -> None: } ) expected_result = [ - ("10/10/2019 B 10 R$ 102,00", 0), - ("12/11/2019 S 100 R$ 3050,00", 1), + (0, "10/10/2019 B 10 R$ 102,00"), + (1, "12/11/2019 S 100 R$ 3050,00"), ] result = report_reader.get_trades(df) assert expected_result == result