From 4f88606a73d37ccd7908f864f9f78ec2310a5a9d Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Fri, 12 Nov 2021 11:36:00 +0100 Subject: [PATCH 01/21] Atualiza o CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f5a145..6862260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ Todas as mudanças de destaque nesse projeto será documentado neste arquivo. ## [Não publicado] ### Adicionado -- Suporte à consulta e conferência da Loteria Federal. +- [x] Suporte à consulta e conferência da Loteria Federal. +- [ ] Extrato de consultas funcionam com o novo site da CEF. ### Modificado - Projeto migrado de `setup.py` para [poetry] From 6674508d0c19e57e41f6cf3b4fb1634a01d8576b Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Sun, 21 Nov 2021 22:24:10 +0100 Subject: [PATCH 02/21] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4489977..2503e35 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ sorte.py ======== +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sorte.py) [![PyPI](https://img.shields.io/pypi/v/sorte.py)](https://pypi.org/project/sorte.py) > AVISO > ----- -> +> > `sorte.py` não consegue fazer mais consultas (o que também afeta as conferências), > desde que a Caixa mudou drasticamente o sistema. Este problema está sendo investigado. -> +> > Veja https://github.com/wagnerluis1982/sorte.py/pull/4 Sobre From 6600053431b482aab5b3cf48e463b1e188f0b96a Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Sun, 21 Nov 2021 22:24:02 +0100 Subject: [PATCH 03/21] pre-commit: flake 8 --- .flake8 | 24 ++++++++++++ .pre-commit-config.yaml | 11 ++++++ pyproject.toml | 2 +- src/sortepy/loterica.py | 85 ++++++++++++++++++++++++----------------- src/sortepy/script.py | 30 +++++++++++---- src/sortepy/util.py | 69 +++++++++++++++++++-------------- tests/basetest.py | 4 +- tests/patchs.py | 2 +- tests/test_script.py | 11 +++--- tests/test_util.py | 11 ++++-- 10 files changed, 165 insertions(+), 84 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..e5260a2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,24 @@ +[flake8] +max-line-length = 88 +ban-relative-imports = True +mypy-init-return = True +format-greedy = 1 + +ignore = E501, W503, ANN101, ANN102, FS001 +per-file-ignores = + __init__.py:F401 + tests/basetest.py:ANN002,ANN003,ANN201,ANN204 + tests/test_*:ANN002,ANN003,ANN201,ANN204 + tests/**/test_*:ANN002,ANN003,ANN201,ANN204 +exclude = + .git + __pycache__ + build + dist + .venv + .tox + .pytest_cache + .github + src/poetry/core/_vendor/* + tests/fixtures/* + tests/**/fixtures/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72ae9ab..981d275 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,3 +17,14 @@ repos: - id: end-of-file-fixer exclude: ^tests/fixtures/resultados/.*\.html?$ - id: check-yaml + + - repo: https://github.com/PyCQA/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + additional_dependencies: + - flake8-annotations + - flake8-bugbear + - flake8-comprehensions + - flake8-tidy-imports + - flake8-use-fstring diff --git a/pyproject.toml b/pyproject.toml index 5d69c10..940fb86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ packages = [ { include = "sortepy", from = "src" } ] include = [ - "CHANGELOG.md", + { path = "CHANGELOG.md" }, { path = "tests", format = "sdist" }, ] diff --git a/src/sortepy/loterica.py b/src/sortepy/loterica.py index 1ce489a..330f360 100644 --- a/src/sortepy/loterica.py +++ b/src/sortepy/loterica.py @@ -3,8 +3,12 @@ import random from html.parser import HTMLParser +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple -from . import util +from sortepy import util class LoteriaNaoSuportada(Exception): @@ -63,7 +67,7 @@ class ResultadoNaoDisponivel(Exception): class Loteria: kind = property(lambda self: self._kind) - def __init__(self, nome, cfg_path=None): + def __init__(self, nome: str, cfg_path: str = None): nome = APELIDOS.get(nome, nome) try: self.settings = LOTERIAS[nome] @@ -89,7 +93,7 @@ def __init__(self, nome, cfg_path=None): self._max = self.settings["marcar"][1] self._padrao = self.settings.get("padrao", self._min) - def gerar_aposta(self, marcar: int = None): + def gerar_aposta(self, marcar: int = None) -> Tuple[int]: if marcar in (None, 0): marcar = self._padrao if not (self._min <= marcar <= self._max): @@ -97,13 +101,13 @@ def gerar_aposta(self, marcar: int = None): result = random.sample(self._range, marcar) return tuple(sorted(result)) - def consultar(self, concurso=0, com_premios=False): + def consultar(self, concurso: int = 0) -> Dict[str, Any]: """Obtém o resultado do sorteio de um concurso. Primeiramente, é verificado se o resultado já existe em cache no banco de dados. Caso negativo, faz o download e armazena para uso futuro. """ - result = self._cache(concurso, com_premios) + result = self._cache(concurso) if result: return result @@ -112,20 +116,16 @@ def consultar(self, concurso=0, com_premios=False): return result - def _cache(self, concurso, com_premios): + def _cache(self, concurso: int) -> Dict[str, Any]: result = self.loteria_db.get("%s|%s" % (self.nome, concurso)) if result: - - def int_key(obj): - return {(int(k) if k.isdecimal() else k): v for k, v in obj.items()} - # convert `(k, v)` de volta para `dict` - result = json.loads(result, object_hook=int_key) + result = json.loads(result) result["premios"] = collections.OrderedDict(result["premios"]) return result - def _download(self, concurso): + def _download(self, concurso: int) -> Dict[str, Any]: parser = LoteriaParser(self.nome) url = self._url(concurso) @@ -140,15 +140,15 @@ def _download(self, concurso): else: return result - def _store(self, result): + def _store(self, result: Dict[str, Any]) -> None: # converte `dict` para `(k, v)`: garante os prêmios na ordem devolvida pelo parser result = result.copy() result["premios"] = list(result["premios"].items()) # armazena no cache self.loteria_db["%s|%s" % (self.nome, result["concurso"])] = json.dumps(result) - def conferir(self, concurso, apostas): - result = self.consultar(concurso, com_premios=True) + def conferir(self, concurso: int, apostas: List[List[int]]) -> List[Dict[str, Any]]: + result = self.consultar(concurso) resp = [] for aposta in apostas: if self._kind == K_COMMON: @@ -167,7 +167,7 @@ def conferir(self, concurso, apostas): ) return resp - def _ganhou(self, result, acertou): + def _ganhou(self, result: Dict[str, Any], acertou: List[List[int]]) -> List[str]: if self._kind == K_COMMON: acertou = [len(t) for t in acertou] if self.nome == "duplasena" and acertou[0] == 6: @@ -179,11 +179,11 @@ def _ganhou(self, result, acertou): def _url( self, - concurso, - base="http://www1.caixa.gov.br/loterias/loterias/%(loteria)s/", - script="%(loteria)s_pesquisa_new.asp", - query="?submeteu=sim&opcao=concurso&txtConcurso=%(concurso)d", - ): + concurso: int, + base: str = "http://www1.caixa.gov.br/loterias/loterias/%(loteria)s/", + script: str = "%(loteria)s_pesquisa_new.asp", + query: str = "?submeteu=sim&opcao=concurso&txtConcurso=%(concurso)d", + ) -> str: script = self.settings.get("url-script", script) if concurso <= 0: return (base + script) % {"loteria": self.nome} @@ -254,7 +254,7 @@ class LoteriaParser: }, } - def __init__(self, nome): + def __init__(self, nome: str) -> None: self._spec = self.SPECS.get(nome) if self._spec is None: raise NotImplementedError("parser") @@ -266,10 +266,10 @@ def __init__(self, nome): else: self._parser = _OldParser() - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: return getattr(self._parser, name) - def data(self): + def data(self) -> Dict[str, Any]: spec = self._spec dados = "".join(self._parser.data).split("|") @@ -291,7 +291,9 @@ def data(self): } @staticmethod - def __common(spec, dados): + def __common( + spec: Dict[str, Any], dados: List[str] + ) -> Tuple[List[List[int]], Dict[int, str]]: pos_nums = [range(p[0], p[1] + 1) for p in spec["numeros"]] try: numeros = [[int(dados[i]) for i in r] for r in pos_nums] @@ -304,7 +306,7 @@ def __common(spec, dados): return None @staticmethod - def __ticket(spec, dados): + def __ticket(spec: Dict[str, Any], dados: List[str]) -> Tuple[None, Dict[int, str]]: try: premios = collections.OrderedDict() for pos_ticket, pos_premio in zip(spec["numeros"], spec["premios"]): @@ -317,39 +319,50 @@ def __ticket(spec, dados): class _OldParser(HTMLParser): + def __init__(self, *, convert_charrefs: bool = ...) -> None: + super().__init__(convert_charrefs=convert_charrefs) + self._capture: bool + self._data: List[str] + @property - def data(self): + def data(self) -> List[str]: return self._data - def reset(self): + def reset(self) -> None: super().reset() self._capture = True self._data = [] - def handle_starttag(self, tag, attrs): + def handle_starttag(self, tag: str, attrs: list) -> None: self._capture = False - def handle_endtag(self, tag): + def handle_endtag(self, tag: str) -> None: self._capture = True - def handle_data(self, data): + def handle_data(self, data: str) -> None: if self._capture: self._data.append(data) - def error(self, message): + def error(self, message: str) -> None: raise RuntimeError(message) class _NewParser(_OldParser): - def reset(self): + def __init__(self, *, convert_charrefs: bool = ...) -> None: + super().__init__(convert_charrefs=convert_charrefs) + self._capture_list: bool + self._capture_number: bool + self._numbers: List[str] + + def reset(self) -> None: super().reset() self._capture_list = False self._capture_number = False self._numbers = [] - def handle_starttag(self, tag, attrs): + def handle_starttag(self, tag: str, attrs: list) -> None: super().handle_starttag(tag, attrs) if tag == "ul": @@ -358,7 +371,7 @@ def handle_starttag(self, tag, attrs): elif tag == "li" and self._capture_list: self._capture_number = True - def handle_endtag(self, tag): + def handle_endtag(self, tag: str) -> None: super().handle_endtag(tag) if tag == "li" and self._capture_list: @@ -369,7 +382,7 @@ def handle_endtag(self, tag): self._capture_list = False self._numbers = [] - def handle_data(self, data): + def handle_data(self, data: str) -> None: super().handle_data(data) if self._capture_number: diff --git a/src/sortepy/script.py b/src/sortepy/script.py index d32f508..ada1ded 100644 --- a/src/sortepy/script.py +++ b/src/sortepy/script.py @@ -2,7 +2,15 @@ import re import sys -from . import loterica +from typing import Any +from typing import Callable +from typing import Dict +from typing import Iterable +from typing import List +from typing import Set +from typing import Tuple + +from sortepy import loterica help_msg = "".join( @@ -35,7 +43,7 @@ ) -def error(*args, show_help=True, code=255, **kwargs): +def error(*args: Any, show_help: bool = True, code: int = 255, **kwargs: Any) -> int: print(*args, **kwargs) if show_help: print(help_msg) @@ -79,7 +87,9 @@ def exec_gerar(loteria: loterica.Loteria, quantidade: int, numeros: int) -> int: ) -def iter_resultados(fun, args, erros=set()): +def iter_resultados( + fun: Callable[..., Dict[str, Any]], args: Tuple[List[int], ...], erros: Set[int] +) -> Iterable[Dict[str, Any]]: consultados = set() concursos = args[0] @@ -102,7 +112,7 @@ def iter_resultados(fun, args, erros=set()): continue -def exec_consultar(loteria, concursos): +def exec_consultar(loteria: loterica.Loteria, concursos: List[int]) -> int: erros = set() try: resultados = iter_resultados(loteria.consultar, (concursos,), erros) @@ -147,7 +157,9 @@ def exec_consultar(loteria, concursos): print(" erros:", ", ".join(sorted(map(str, erros)))) -def exec_conferir(loteria, concursos, apostas): +def exec_conferir( + loteria: loterica.Loteria, concursos: List[int], apostas: List[int] +) -> int: erros = set() try: resultados = iter_resultados(loteria.conferir, (concursos, apostas), erros) @@ -180,7 +192,9 @@ def exec_conferir(loteria, concursos, apostas): print(" erros:", ", ".join(sorted(map(str, erros)))) -def __highlight_closure(color=0, spec=0, condition=lambda x: True): +def __highlight_closure( + color: int = 0, spec: int = 0, condition: Callable[[str], bool] = lambda x: True +) -> Callable[[str], str]: if sys.stdout.isatty(): formatting = "\x1b[%02d;%02dm%%s\x1b[00m" % (spec, color) return lambda arg: formatting % arg if condition(arg) else arg @@ -195,7 +209,7 @@ def __highlight_closure(color=0, spec=0, condition=lambda x: True): hi_ganho = __highlight_closure(color=32, spec=0, condition=lambda x: x != "R$ 0,00") -def main(argv=sys.argv, cfg_path=None): +def main(argv: List[str] = sys.argv, cfg_path: str = None) -> int: try: opts, args = getopt.gnu_getopt( argv[1:], @@ -210,7 +224,7 @@ def main(argv=sys.argv, cfg_path=None): # Avalia os parâmetros passados numeros = None quantidade = None - concursos = [] + concursos: List[int] = [] usar_stdin = False for option, arg in opts: if option in ("-h", "--help"): diff --git a/src/sortepy/util.py b/src/sortepy/util.py index 284fba9..ba9d7d8 100644 --- a/src/sortepy/util.py +++ b/src/sortepy/util.py @@ -8,8 +8,12 @@ import time import urllib.request +from typing import Any +from typing import MutableMapping +from typing import Type -def get_config_path(app="sortepy"): + +def get_config_path(app: str = "sortepy") -> str: """Obtém o caminho de configuração de acordo com o SO Por enquanto é suportado os sistemas POSIX e Windows (NT) @@ -31,7 +35,7 @@ def get_config_path(app="sortepy"): return os.path.join(profile_dir, prefixo + app) -def makedirs(caminho): +def makedirs(caminho: str) -> None: """Versão própria do makedirs() Essa versão não lança exceção se o caminho já existir @@ -44,7 +48,7 @@ def makedirs(caminho): class Util: - def __init__(self, cfg_path=None): + def __init__(self, cfg_path: str = None): # Se o caminho é uma string vazia, não deve ser usado nenhum cache # Definido para propósitos de teste if cfg_path == "": @@ -68,11 +72,11 @@ def __init__(self, cfg_path=None): self.pages_db = self.get_mapdb("paginas") self.in_cache = True - def get_mapdb(self, name): + def get_mapdb(self, name: str) -> None: db_path = os.path.join(self.__cfg_path, "%s.db" % name) return FileDB.open(db_path) - def download(self, url, in_cache=None): + def download(self, url: str, in_cache: bool = None) -> None: in_cache = in_cache if isinstance(in_cache, bool) else self.in_cache # Obtém a página do cache @@ -102,7 +106,7 @@ def download(self, url, in_cache=None): return conteudo - def cache(self, url, conteudo=None): + def cache(self, url: str, conteudo: str = None) -> None: # Sem conteúdo: leitura do cache if conteudo is None: if url not in self.pages_db: @@ -124,7 +128,7 @@ def cache(self, url, conteudo=None): else: self.pages_db[url] = conteudo - def blame(self, url): + def blame(self, url: str) -> None: """Marca o resultado de uma URL como inválida. Isso é feito, registrando o horário em que esse método foi chamado. @@ -135,39 +139,42 @@ def blame(self, url): DIRTY_RE = re.compile(r"^[0-9]+\|") @classmethod - def is_dirty(cls, s): + def is_dirty(cls, s: str) -> re.Match: return cls.DIRTY_RE.match(s) -class FileDB: +class FileDB(MutableMapping[str, str]): @staticmethod - def open(filename): + def open(filename: str) -> "FileDB": (prefix, _) = os.path.splitext(os.path.basename(filename)) - db = FileDB._SQLite3(filename, prefix) + cls = _file_db_cls("sqlite") + db = cls(filename, prefix) return db - class _SQLite3(object): + +def _file_db_cls(type: str) -> Type[FileDB]: + class _SQLite3(FileDB): __version__ = 1 - def __init__(self, filename, prefix=""): + def __init__(self, filename: str, prefix: str = "") -> None: self._con = sqlite3.connect(filename) self._table = prefix + "map" self._create_schema() - def close(self): + def close(self) -> None: self._con.commit() self._con.close() - def flush(self): + def flush(self) -> None: self._con.commit() - def __del__(self): + def __del__(self) -> None: try: self.close() except sqlite3.Error: pass - def _create_schema(self): + def _create_schema(self) -> None: try: self._con.execute("BEGIN EXCLUSIVE TRANSACTION") self._con.execute( @@ -182,17 +189,17 @@ def _create_schema(self): self._con.commit() self._write_dbversion(self.__version__) - def _is_latest_version(self): + def _is_latest_version(self) -> bool: return self._read_dbversion() == self.__version__ - def _read_dbversion(self): + def _read_dbversion(self) -> int: (dbversion,) = self._con.execute("PRAGMA user_version").fetchone() return dbversion - def _write_dbversion(self, version): + def _write_dbversion(self, version: int) -> None: self._con.execute("PRAGMA user_version = %d" % version) - def _migration_script(self): + def _migration_script(self) -> str: # se versão for a mais atual, não é preciso criar esquema! dbversion = self._read_dbversion() if dbversion == self.__version__: @@ -216,13 +223,13 @@ def _migration_script(self): return sql_template.format(table=self._table) return "" - def get(self, key, default=None): + def get(self, key: str, default: str = None) -> str: try: return self[key] except KeyError: return default - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: str) -> None: with self._con as con: try: con.execute( @@ -234,7 +241,7 @@ def __setitem__(self, key, value): "UPDATE %s SET value=? WHERE key=?" % self._table, (value, key) ) - def __getitem__(self, key): + def __getitem__(self, key: str) -> str: cursor = self._con.cursor() cursor.execute("SELECT value FROM %s WHERE key=?" % self._table, (key,)) result = cursor.fetchone() @@ -243,17 +250,23 @@ def __getitem__(self, key): else: raise KeyError(key) - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: with self._con as con: con.execute("DELETE FROM %s WHERE key=?" % self._table, (key,)) - def __contains__(self, key): + def __contains__(self, key: str) -> bool: cursor = self._con.cursor() cursor.execute("SELECT 1 FROM %s WHERE key=?" % self._table, (key,)) return cursor.fetchall() != [] - def __enter__(self): + def __enter__(self) -> FileDB: return self - def __exit__(self, *args): + def __exit__(self, *args: Any) -> None: self.__del__() + + __iter__ = None + __len__ = None + + if type == "sqlite": + return _SQLite3 diff --git a/tests/basetest.py b/tests/basetest.py index 6d71d06..05b500e 100644 --- a/tests/basetest.py +++ b/tests/basetest.py @@ -12,7 +12,7 @@ patchs.loteria_class() -def tempdir(custom_prefix="sortepy-"): +def tempdir(custom_prefix: str = "sortepy-"): newdir = tempfile.mkdtemp(prefix=custom_prefix) atexit.register(shutil.rmtree, newdir) return newdir @@ -83,7 +83,7 @@ def test_conferir_varias_apostas(self): assert resp[i]["acertou"] == self.esperados["acertou"][i] assert resp[i]["ganhou"] == self.esperados["ganhou"][i] - def check_gerar_aposta(self, n): + def check_gerar_aposta(self, n: int): n = n or self.numeros[0] # número padrão ou informado aposta = self.loto.gerar_aposta(n) diff --git a/tests/patchs.py b/tests/patchs.py index 1249009..361ce58 100644 --- a/tests/patchs.py +++ b/tests/patchs.py @@ -1,5 +1,5 @@ # modifica o método privado Loteria._url para garantir que use o script CGI interno -def loteria_class(): +def loteria_class() -> None: if not loteria_class.done: # garantie que servidor interno de fixtures está inicializado import fixtures diff --git a/tests/test_script.py b/tests/test_script.py index 3ee2d36..ef7d702 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -1,13 +1,14 @@ import functools import io -import sys import unittest +from typing import List + import basetest import sortepy.script -def run_script(args): +def run_script(args: List[str]): output = io.StringIO() # Redefine 'print' para usar outra stdout passado como parâmetro @@ -56,7 +57,7 @@ def test_gerar_VARIAS_apostas(self): readlines = run_script(args) linhas = readlines[1:] assert len(linhas) == 2 - for no, linha in enumerate(linhas, 1): + for linha in linhas: assert len([int(x) for x in linha.split()]) == 5 # geração de aposta da quina com mais de 5 números @@ -67,7 +68,7 @@ def test_gerar_VARIAS_apostas(self): readlines = run_script(args) linhas = readlines[1:] assert len(linhas) == 2 - for no, linha in enumerate(linhas, 1): + for linha in linhas: assert len([int(x) for x in linha.split()]) == i # opções longas @@ -76,7 +77,7 @@ def test_gerar_VARIAS_apostas(self): readlines = run_script(args) linhas = readlines[1:] assert len(linhas) == 2 - for no, linha in enumerate(linhas, 1): + for linha in linhas: assert len([int(x) for x in linha.split()]) == i def test_gerar_aposta_para_loteria_ticket_NAO_disponivel(self): diff --git a/tests/test_util.py b/tests/test_util.py index bd316b3..b991c4c 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -2,6 +2,9 @@ import sqlite3 import unittest +from typing import Any +from typing import Dict + import fixtures import sortepy.util @@ -46,7 +49,7 @@ class DownloadTest(unittest.TestCase): "Util.download" @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: cls.util = sortepy.util.Util(cfg_path="") def test_download_pagina(self): @@ -183,10 +186,12 @@ def test_upgrade_from_version_0(self): "pk": False, } - def _open_db(self): + def _open_db(self) -> sortepy.util.FileDB: return sortepy.util.FileDB.open(self.arquivo) - def _table_descriptions(self, conn, table): + def _table_descriptions( + self, conn: sqlite3.Connection, table: str + ) -> Dict[str, Dict[str, Any]]: descriptions = conn.execute("PRAGMA table_info(%s)" % table).fetchall() return { row[1]: { From f38d8006dc9d8b0bd5bdbfe04f34ca389125dfb7 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Mon, 22 Nov 2021 23:47:57 +0100 Subject: [PATCH 04/21] pre-commit: mypy --- .pre-commit-config.yaml | 6 +++++ pyproject.toml | 19 +++++++++++++++ src/sortepy/loterica.py | 51 +++++++++++++++++++++++++---------------- src/sortepy/script.py | 45 +++++++++++++++++++++++++++--------- src/sortepy/util.py | 31 +++++++++++++++++-------- 5 files changed, 111 insertions(+), 41 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 981d275..c3912e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,3 +28,9 @@ repos: - flake8-comprehensions - flake8-tidy-imports - flake8-use-fstring + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.910-1 + hooks: + - id: mypy + pass_filenames: false diff --git a/pyproject.toml b/pyproject.toml index 940fb86..5799e77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,3 +67,22 @@ known_first_party = "sortepy" [tool.black] line-length = 88 + +[tool.mypy] +mypy_path = "src" +files = "src, tests" +follow_imports = "silent" +show_error_codes = true +explicit_package_bases = true +namespace_packages = true +strict_optional = false + +[[tool.mypy.overrides]] +module = [ + 'tests.basetest', + 'tests.patchs', + 'tests.fixtures', + 'tests.test_script', + 'tests.test_util', +] +ignore_errors = true diff --git a/src/sortepy/loterica.py b/src/sortepy/loterica.py index 330f360..490ab4b 100644 --- a/src/sortepy/loterica.py +++ b/src/sortepy/loterica.py @@ -2,6 +2,8 @@ import json import random +from abc import ABCMeta +from abc import abstractmethod from html.parser import HTMLParser from typing import Any from typing import Dict @@ -31,7 +33,7 @@ class ResultadoNaoDisponivel(Exception): "sena": "megasena", } -LOTERIAS = { +LOTERIAS: Dict[str, Dict[str, Any]] = { "quina": { "marcar": (5, 7), "numeros": (1, 80), @@ -70,7 +72,7 @@ class Loteria: def __init__(self, nome: str, cfg_path: str = None): nome = APELIDOS.get(nome, nome) try: - self.settings = LOTERIAS[nome] + self.settings: Dict[str, Any] = LOTERIAS[nome] except KeyError as err: raise LoteriaNaoSuportada(err) @@ -82,7 +84,7 @@ def __init__(self, nome: str, cfg_path: str = None): # método `gerar_aposta()` e encerra. self._kind = self.settings.get("kind", K_COMMON) if self._kind == K_TICKET: - self.gerar_aposta = lambda *a, **k: None + self.gerar_aposta = lambda *a, **k: None # type: ignore[assignment] return # atributos do gerador de números @@ -93,7 +95,7 @@ def __init__(self, nome: str, cfg_path: str = None): self._max = self.settings["marcar"][1] self._padrao = self.settings.get("padrao", self._min) - def gerar_aposta(self, marcar: int = None) -> Tuple[int]: + def gerar_aposta(self, marcar: int = None) -> Tuple[int, ...]: if marcar in (None, 0): marcar = self._padrao if not (self._min <= marcar <= self._max): @@ -120,10 +122,12 @@ def _cache(self, concurso: int) -> Dict[str, Any]: result = self.loteria_db.get("%s|%s" % (self.nome, concurso)) if result: # convert `(k, v)` de volta para `dict` - result = json.loads(result) - result["premios"] = collections.OrderedDict(result["premios"]) + _result = json.loads(result) + _result["premios"] = collections.OrderedDict(_result["premios"]) - return result + return _result + + return None def _download(self, concurso: int) -> Dict[str, Any]: parser = LoteriaParser(self.nome) @@ -169,13 +173,13 @@ def conferir(self, concurso: int, apostas: List[List[int]]) -> List[Dict[str, An def _ganhou(self, result: Dict[str, Any], acertou: List[List[int]]) -> List[str]: if self._kind == K_COMMON: - acertou = [len(t) for t in acertou] - if self.nome == "duplasena" and acertou[0] == 6: - acertou[0] = -6 + marcou = [len(t) for t in acertou] + if self.nome == "duplasena" and marcou[0] == 6: + marcou[0] = -6 else: - acertou = [v for [v] in acertou] or [None] + marcou = [v for [v] in acertou] or [None] - return [result["premios"].get(n, "0,00") for n in acertou] + return [result["premios"].get(n, "0,00") for n in marcou] def _url( self, @@ -262,9 +266,9 @@ def __init__(self, nome: str) -> None: raise NotImplementedError("parser: premios") if self._spec.get("parser") == self.NEW: - self._parser = _NewParser() + self._parser: _Parser = _NewParser() else: - self._parser = _OldParser() + self._parser: _Parser = _OldParser() # type: ignore[no-redef] def __getattr__(self, name: str) -> Any: return getattr(self._parser, name) @@ -285,7 +289,7 @@ def data(self) -> Dict[str, Any]: return None return { - "concurso": int(dados[spec.get("concurso", 0)]), + "concurso": int(dados[spec.get("concurso", 0)]), # type: ignore[call-overload] "numeros": numeros, "premios": premios, } @@ -318,9 +322,16 @@ def __ticket(spec: Dict[str, Any], dados: List[str]) -> Tuple[None, Dict[int, st return None -class _OldParser(HTMLParser): - def __init__(self, *, convert_charrefs: bool = ...) -> None: - super().__init__(convert_charrefs=convert_charrefs) +class _Parser(HTMLParser, metaclass=ABCMeta): + @property + @abstractmethod + def data(self) -> List[str]: + ... + + +class _OldParser(_Parser): + def __init__(self) -> None: + super().__init__() self._capture: bool self._data: List[str] @@ -349,8 +360,8 @@ def error(self, message: str) -> None: class _NewParser(_OldParser): - def __init__(self, *, convert_charrefs: bool = ...) -> None: - super().__init__(convert_charrefs=convert_charrefs) + def __init__(self) -> None: + super().__init__() self._capture_list: bool self._capture_number: bool self._numbers: List[str] diff --git a/src/sortepy/script.py b/src/sortepy/script.py index ada1ded..4767062 100644 --- a/src/sortepy/script.py +++ b/src/sortepy/script.py @@ -5,10 +5,11 @@ from typing import Any from typing import Callable from typing import Dict -from typing import Iterable +from typing import Iterator from typing import List from typing import Set from typing import Tuple +from typing import overload from sortepy import loterica @@ -85,11 +86,29 @@ def exec_gerar(loteria: loterica.Loteria, quantidade: int, numeros: int) -> int: show_help=False, code=5, ) + else: + return 0 +@overload def iter_resultados( - fun: Callable[..., Dict[str, Any]], args: Tuple[List[int], ...], erros: Set[int] -) -> Iterable[Dict[str, Any]]: + fun: Callable[[int], Any], + args: Tuple[List[int]], + erros: Set[int], +) -> Iterator[Dict[str, Any]]: + ... + + +@overload +def iter_resultados( + fun: Callable[[int, List[List[int]]], List[Dict[str, Any]]], + args: Tuple[List[int], List[List[int]]], + erros: Set[int], +) -> Iterator[List[Dict[str, Any]]]: + ... + + +def iter_resultados(fun, args, erros): consultados = set() concursos = args[0] @@ -113,7 +132,7 @@ def iter_resultados( def exec_consultar(loteria: loterica.Loteria, concursos: List[int]) -> int: - erros = set() + erros: Set[int] = set() try: resultados = iter_resultados(loteria.consultar, (concursos,), erros) except loterica.LoteriaNaoSuportada as err: @@ -156,11 +175,13 @@ def exec_consultar(loteria: loterica.Loteria, concursos: List[int]) -> int: print() print(" erros:", ", ".join(sorted(map(str, erros)))) + return 0 + def exec_conferir( - loteria: loterica.Loteria, concursos: List[int], apostas: List[int] + loteria: loterica.Loteria, concursos: List[int], apostas: List[List[int]] ) -> int: - erros = set() + erros: Set[int] = set() try: resultados = iter_resultados(loteria.conferir, (concursos, apostas), erros) except loterica.LoteriaNaoSuportada as err: @@ -191,6 +212,8 @@ def exec_conferir( print() print(" erros:", ", ".join(sorted(map(str, erros)))) + return 0 + def __highlight_closure( color: int = 0, spec: int = 0, condition: Callable[[str], bool] = lambda x: True @@ -229,7 +252,7 @@ def main(argv: List[str] = sys.argv, cfg_path: str = None) -> int: for option, arg in opts: if option in ("-h", "--help"): print(help_msg) - return + return 0 elif option in ("-n", "--numeros"): numeros = int(arg) elif option in ("-q", "--quantidade"): @@ -241,8 +264,8 @@ def main(argv: List[str] = sys.argv, cfg_path: str = None) -> int: faixa = arg.split("-") if len(faixa) != 2 or not (faixa[0].isdigit() and faixa[1].isdigit()): return error("ERRO: faixa '%s' inválida" % arg) - faixa = list(map(int, faixa)) - concursos.extend(range(faixa[0], faixa[1] + 1)) + intervalo = list(map(int, faixa)) + concursos.extend(range(intervalo[0], intervalo[1] + 1)) else: concursos.append(-1) elif option in ("-i", "--stdin"): @@ -263,9 +286,9 @@ def main(argv: List[str] = sys.argv, cfg_path: str = None) -> int: if not linha.lstrip().startswith("#"): args.append(linha) - apostas = [] + apostas: List[List[int]] = [] for arg in args: - aposta = [] + aposta: List[int] = [] for n in re.split("[, ]+", arg): fx = list(map(int, n.split("-", 1))) aposta.extend(range(fx[0], fx[-1] + 1)) diff --git a/src/sortepy/util.py b/src/sortepy/util.py index ba9d7d8..f281e54 100644 --- a/src/sortepy/util.py +++ b/src/sortepy/util.py @@ -9,8 +9,9 @@ import urllib.request from typing import Any +from typing import Callable from typing import MutableMapping -from typing import Type +from typing import overload def get_config_path(app: str = "sortepy") -> str: @@ -72,11 +73,11 @@ def __init__(self, cfg_path: str = None): self.pages_db = self.get_mapdb("paginas") self.in_cache = True - def get_mapdb(self, name: str) -> None: + def get_mapdb(self, name: str) -> "FileDB": db_path = os.path.join(self.__cfg_path, "%s.db" % name) return FileDB.open(db_path) - def download(self, url: str, in_cache: bool = None) -> None: + def download(self, url: str, in_cache: bool = None) -> str: in_cache = in_cache if isinstance(in_cache, bool) else self.in_cache # Obtém a página do cache @@ -93,20 +94,28 @@ def download(self, url: str, in_cache: bool = None) -> None: opener.addheaders.append(("Cookie", "security=true")) page = opener.open(url) - conteudo = page.read() + content: bytes = page.read() charset = page.headers.get_param("charset") if charset is not None: - conteudo = conteudo.decode(charset) + conteudo = content.decode(charset) else: - conteudo = conteudo.decode() + conteudo = content.decode() if in_cache: self.cache(url, conteudo) return conteudo - def cache(self, url: str, conteudo: str = None) -> None: + @overload + def cache(self, url: str) -> str: + ... + + @overload + def cache(self, url: str, conteudo: str) -> None: + ... + + def cache(self, url, conteudo=None): # Sem conteúdo: leitura do cache if conteudo is None: if url not in self.pages_db: @@ -152,7 +161,7 @@ def open(filename: str) -> "FileDB": return db -def _file_db_cls(type: str) -> Type[FileDB]: +def _file_db_cls(type: str) -> Callable[[str, str], FileDB]: class _SQLite3(FileDB): __version__ = 1 @@ -223,7 +232,7 @@ def _migration_script(self) -> str: return sql_template.format(table=self._table) return "" - def get(self, key: str, default: str = None) -> str: + def get(self, key: str, default: str = None) -> str: # type: ignore[override] try: return self[key] except KeyError: @@ -254,7 +263,7 @@ def __delitem__(self, key: str) -> None: with self._con as con: con.execute("DELETE FROM %s WHERE key=?" % self._table, (key,)) - def __contains__(self, key: str) -> bool: + def __contains__(self, key: object) -> bool: cursor = self._con.cursor() cursor.execute("SELECT 1 FROM %s WHERE key=?" % self._table, (key,)) return cursor.fetchall() != [] @@ -270,3 +279,5 @@ def __exit__(self, *args: Any) -> None: if type == "sqlite": return _SQLite3 + + raise From 974ef4092ffba9c0e2c02b8dcd4218016876d824 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Tue, 23 Nov 2021 23:02:26 +0100 Subject: [PATCH 05/21] tipagem: ResultadoDict, ConferenciaDict --- src/sortepy/loterica.py | 50 ++++++++++++++++++++++------------------- src/sortepy/script.py | 13 ++++++----- src/sortepy/types.py | 16 +++++++++++++ 3 files changed, 51 insertions(+), 28 deletions(-) create mode 100644 src/sortepy/types.py diff --git a/src/sortepy/loterica.py b/src/sortepy/loterica.py index 490ab4b..2266fbd 100644 --- a/src/sortepy/loterica.py +++ b/src/sortepy/loterica.py @@ -11,6 +11,8 @@ from typing import Tuple from sortepy import util +from sortepy.types import ConferenciaDict +from sortepy.types import ResultadoDict class LoteriaNaoSuportada(Exception): @@ -103,7 +105,7 @@ def gerar_aposta(self, marcar: int = None) -> Tuple[int, ...]: result = random.sample(self._range, marcar) return tuple(sorted(result)) - def consultar(self, concurso: int = 0) -> Dict[str, Any]: + def consultar(self, concurso: int = 0) -> ResultadoDict: """Obtém o resultado do sorteio de um concurso. Primeiramente, é verificado se o resultado já existe em cache no banco de dados. @@ -118,7 +120,7 @@ def consultar(self, concurso: int = 0) -> Dict[str, Any]: return result - def _cache(self, concurso: int) -> Dict[str, Any]: + def _cache(self, concurso: int) -> ResultadoDict: result = self.loteria_db.get("%s|%s" % (self.nome, concurso)) if result: # convert `(k, v)` de volta para `dict` @@ -129,7 +131,7 @@ def _cache(self, concurso: int) -> Dict[str, Any]: return None - def _download(self, concurso: int) -> Dict[str, Any]: + def _download(self, concurso: int) -> ResultadoDict: parser = LoteriaParser(self.nome) url = self._url(concurso) @@ -144,16 +146,18 @@ def _download(self, concurso: int) -> Dict[str, Any]: else: return result - def _store(self, result: Dict[str, Any]) -> None: - # converte `dict` para `(k, v)`: garante os prêmios na ordem devolvida pelo parser - result = result.copy() - result["premios"] = list(result["premios"].items()) + def _store(self, result: ResultadoDict) -> None: + # converte "premios" `dict` para `(k, v)`: garante os prêmios na ordem devolvida pelo parser + store = dict(result) + store["premios"] = list(result["premios"].items()) # armazena no cache - self.loteria_db["%s|%s" % (self.nome, result["concurso"])] = json.dumps(result) + self.loteria_db[f"{self.nome}|{store['concurso']}"] = json.dumps(store) - def conferir(self, concurso: int, apostas: List[List[int]]) -> List[Dict[str, Any]]: + def conferir( + self, concurso: int, apostas: List[List[int]] + ) -> List[ConferenciaDict]: result = self.consultar(concurso) - resp = [] + resp: List[ConferenciaDict] = [] for aposta in apostas: if self._kind == K_COMMON: acertou = [[n for n in res if n in aposta] for res in result["numeros"]] @@ -162,16 +166,16 @@ def conferir(self, concurso: int, apostas: List[List[int]]) -> List[Dict[str, An ganhou = self._ganhou(result, acertou) resp.append( - { - "concurso": result["concurso"], - "numeros": aposta, - "acertou": acertou, - "ganhou": ganhou, - } + ConferenciaDict( + concurso=result["concurso"], + numeros=aposta, + acertou=acertou, + ganhou=ganhou, + ) ) return resp - def _ganhou(self, result: Dict[str, Any], acertou: List[List[int]]) -> List[str]: + def _ganhou(self, result: ResultadoDict, acertou: List[List[int]]) -> List[str]: if self._kind == K_COMMON: marcou = [len(t) for t in acertou] if self.nome == "duplasena" and marcou[0] == 6: @@ -273,7 +277,7 @@ def __init__(self, nome: str) -> None: def __getattr__(self, name: str) -> Any: return getattr(self._parser, name) - def data(self) -> Dict[str, Any]: + def data(self) -> ResultadoDict: spec = self._spec dados = "".join(self._parser.data).split("|") @@ -288,11 +292,11 @@ def data(self) -> Dict[str, Any]: else: return None - return { - "concurso": int(dados[spec.get("concurso", 0)]), # type: ignore[call-overload] - "numeros": numeros, - "premios": premios, - } + return ResultadoDict( + concurso=int(dados[spec.get("concurso", 0)]), # type: ignore[call-overload] + numeros=numeros, + premios=premios, + ) @staticmethod def __common( diff --git a/src/sortepy/script.py b/src/sortepy/script.py index 4767062..4052762 100644 --- a/src/sortepy/script.py +++ b/src/sortepy/script.py @@ -4,7 +4,6 @@ from typing import Any from typing import Callable -from typing import Dict from typing import Iterator from typing import List from typing import Set @@ -12,6 +11,8 @@ from typing import overload from sortepy import loterica +from sortepy.types import ConferenciaDict +from sortepy.types import ResultadoDict help_msg = "".join( @@ -90,21 +91,23 @@ def exec_gerar(loteria: loterica.Loteria, quantidade: int, numeros: int) -> int: return 0 +# Loteria.consultar @overload def iter_resultados( - fun: Callable[[int], Any], + fun: Callable[[int], ResultadoDict], args: Tuple[List[int]], erros: Set[int], -) -> Iterator[Dict[str, Any]]: +) -> Iterator[ResultadoDict]: ... +# Loteria.conferir @overload def iter_resultados( - fun: Callable[[int, List[List[int]]], List[Dict[str, Any]]], + fun: Callable[[int, List[List[int]]], List[ConferenciaDict]], args: Tuple[List[int], List[List[int]]], erros: Set[int], -) -> Iterator[List[Dict[str, Any]]]: +) -> Iterator[List[ConferenciaDict]]: ... diff --git a/src/sortepy/types.py b/src/sortepy/types.py new file mode 100644 index 0000000..3e376d0 --- /dev/null +++ b/src/sortepy/types.py @@ -0,0 +1,16 @@ +from typing import Dict +from typing import List +from typing import TypedDict + + +class ResultadoDict(TypedDict): + concurso: int + numeros: List[List[int]] + premios: Dict[int, str] + + +class ConferenciaDict(TypedDict): + concurso: int + numeros: List[int] + acertou: List[List[int]] + ganhou: List[str] From 6d7d13a7f30090d3baeea45a443ea256cf6d59fa Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Wed, 24 Nov 2021 22:45:33 +0100 Subject: [PATCH 06/21] tipagem: Aposta --- src/sortepy/loterica.py | 15 ++++++++------- src/sortepy/script.py | 11 ++++++----- src/sortepy/types.py | 10 +++++++--- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/sortepy/loterica.py b/src/sortepy/loterica.py index 2266fbd..bc3397d 100644 --- a/src/sortepy/loterica.py +++ b/src/sortepy/loterica.py @@ -11,6 +11,7 @@ from typing import Tuple from sortepy import util +from sortepy.types import Aposta from sortepy.types import ConferenciaDict from sortepy.types import ResultadoDict @@ -153,14 +154,14 @@ def _store(self, result: ResultadoDict) -> None: # armazena no cache self.loteria_db[f"{self.nome}|{store['concurso']}"] = json.dumps(store) - def conferir( - self, concurso: int, apostas: List[List[int]] - ) -> List[ConferenciaDict]: + def conferir(self, concurso: int, apostas: List[Aposta]) -> List[ConferenciaDict]: result = self.consultar(concurso) resp: List[ConferenciaDict] = [] for aposta in apostas: if self._kind == K_COMMON: - acertou = [[n for n in res if n in aposta] for res in result["numeros"]] + acertou = [ + Aposta(n for n in res if n in aposta) for res in result["numeros"] + ] else: acertou = [aposta for res in result["premios"] if [res] == aposta] @@ -175,7 +176,7 @@ def conferir( ) return resp - def _ganhou(self, result: ResultadoDict, acertou: List[List[int]]) -> List[str]: + def _ganhou(self, result: ResultadoDict, acertou: List[Aposta]) -> List[str]: if self._kind == K_COMMON: marcou = [len(t) for t in acertou] if self.nome == "duplasena" and marcou[0] == 6: @@ -301,10 +302,10 @@ def data(self) -> ResultadoDict: @staticmethod def __common( spec: Dict[str, Any], dados: List[str] - ) -> Tuple[List[List[int]], Dict[int, str]]: + ) -> Tuple[List[Aposta], Dict[int, str]]: pos_nums = [range(p[0], p[1] + 1) for p in spec["numeros"]] try: - numeros = [[int(dados[i]) for i in r] for r in pos_nums] + numeros = [Aposta(int(dados[i]) for i in r) for r in pos_nums] premios = collections.OrderedDict() for qnt, pos_premio in sorted(spec["premios"].items(), reverse=True): premios[qnt] = dados[pos_premio] diff --git a/src/sortepy/script.py b/src/sortepy/script.py index 4052762..cf9c15d 100644 --- a/src/sortepy/script.py +++ b/src/sortepy/script.py @@ -11,6 +11,7 @@ from typing import overload from sortepy import loterica +from sortepy.types import Aposta from sortepy.types import ConferenciaDict from sortepy.types import ResultadoDict @@ -104,8 +105,8 @@ def iter_resultados( # Loteria.conferir @overload def iter_resultados( - fun: Callable[[int, List[List[int]]], List[ConferenciaDict]], - args: Tuple[List[int], List[List[int]]], + fun: Callable[[int, List[Aposta]], List[ConferenciaDict]], + args: Tuple[List[int], List[Aposta]], erros: Set[int], ) -> Iterator[List[ConferenciaDict]]: ... @@ -182,7 +183,7 @@ def exec_consultar(loteria: loterica.Loteria, concursos: List[int]) -> int: def exec_conferir( - loteria: loterica.Loteria, concursos: List[int], apostas: List[List[int]] + loteria: loterica.Loteria, concursos: List[int], apostas: List[Aposta] ) -> int: erros: Set[int] = set() try: @@ -289,9 +290,9 @@ def main(argv: List[str] = sys.argv, cfg_path: str = None) -> int: if not linha.lstrip().startswith("#"): args.append(linha) - apostas: List[List[int]] = [] + apostas: List[Aposta] = [] for arg in args: - aposta: List[int] = [] + aposta = Aposta() for n in re.split("[, ]+", arg): fx = list(map(int, n.split("-", 1))) aposta.extend(range(fx[0], fx[-1] + 1)) diff --git a/src/sortepy/types.py b/src/sortepy/types.py index 3e376d0..d10f417 100644 --- a/src/sortepy/types.py +++ b/src/sortepy/types.py @@ -3,14 +3,18 @@ from typing import TypedDict +class Aposta(List[int]): + pass + + class ResultadoDict(TypedDict): concurso: int - numeros: List[List[int]] + numeros: List[Aposta] premios: Dict[int, str] class ConferenciaDict(TypedDict): concurso: int - numeros: List[int] - acertou: List[List[int]] + numeros: Aposta + acertou: List[Aposta] ganhou: List[str] From f64203689f3c3c6cc6fa4d82433b2e1c83841851 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Wed, 24 Nov 2021 22:45:33 +0100 Subject: [PATCH 07/21] tipagem: LoteriaConfig --- src/sortepy/loterica.py | 67 +++++++++++++++++++++-------------------- src/sortepy/types.py | 12 ++++++++ tests/patchs.py | 26 +++++++++++----- 3 files changed, 66 insertions(+), 39 deletions(-) diff --git a/src/sortepy/loterica.py b/src/sortepy/loterica.py index bc3397d..795ac6a 100644 --- a/src/sortepy/loterica.py +++ b/src/sortepy/loterica.py @@ -13,6 +13,7 @@ from sortepy import util from sortepy.types import Aposta from sortepy.types import ConferenciaDict +from sortepy.types import LoteriaConfig from sortepy.types import ResultadoDict @@ -32,40 +33,40 @@ class ResultadoNaoDisponivel(Exception): K_COMMON = 0 K_TICKET = 1 -APELIDOS = { +APELIDOS: Dict[str, str] = { "sena": "megasena", } -LOTERIAS: Dict[str, Dict[str, Any]] = { - "quina": { - "marcar": (5, 7), - "numeros": (1, 80), - }, - "megasena": { - "marcar": (6, 15), - "numeros": (1, 60), - "nome": "Mega-Sena", - }, - "lotofacil": { - "marcar": (15, 18), - "numeros": (1, 25), - }, - "lotomania": { - "marcar": (1, 50), - "numeros": (1, 100), - "padrao": 20, - "url-script": "_lotomania_pesquisa.asp", - }, - "duplasena": { - "marcar": (6, 15), - "numeros": (1, 50), - "nome": "Dupla Sena", - }, - "federal": { +LOTERIAS: Dict[str, LoteriaConfig] = { + "quina": LoteriaConfig( + marcar=(5, 7), + numeros=(1, 80), + ), + "megasena": LoteriaConfig( + marcar=(6, 15), + numeros=(1, 60), + nome="Mega-Sena", + ), + "lotofacil": LoteriaConfig( + marcar=(15, 18), + numeros=(1, 25), + ), + "lotomania": LoteriaConfig( + marcar=(1, 50), + numeros=(1, 100), + padrao=20, + url_script="_lotomania_pesquisa.asp", + ), + "duplasena": LoteriaConfig( + marcar=(6, 15), + numeros=(1, 50), + nome="Dupla Sena", + ), + "federal": LoteriaConfig( # essa loteria não gera números - "url-script": "federal_pesquisa.asp", - "kind": K_TICKET, - }, + url_script="federal_pesquisa.asp", + kind=K_TICKET, + ), } @@ -75,7 +76,9 @@ class Loteria: def __init__(self, nome: str, cfg_path: str = None): nome = APELIDOS.get(nome, nome) try: - self.settings: Dict[str, Any] = LOTERIAS[nome] + self.settings = { + k: v for (k, v) in LOTERIAS[nome].__dict__.items() if v is not None + } except KeyError as err: raise LoteriaNaoSuportada(err) @@ -193,7 +196,7 @@ def _url( script: str = "%(loteria)s_pesquisa_new.asp", query: str = "?submeteu=sim&opcao=concurso&txtConcurso=%(concurso)d", ) -> str: - script = self.settings.get("url-script", script) + script = self.settings.get("url_script", script) if concurso <= 0: return (base + script) % {"loteria": self.nome} else: diff --git a/src/sortepy/types.py b/src/sortepy/types.py index d10f417..e4de6fa 100644 --- a/src/sortepy/types.py +++ b/src/sortepy/types.py @@ -1,5 +1,7 @@ +from dataclasses import dataclass from typing import Dict from typing import List +from typing import Tuple from typing import TypedDict @@ -18,3 +20,13 @@ class ConferenciaDict(TypedDict): numeros: Aposta acertou: List[Aposta] ganhou: List[str] + + +@dataclass(frozen=True) +class LoteriaConfig: + marcar: Tuple[int, int] = None + numeros: Tuple[int, int] = None + nome: str = None + padrao: int = None + url_script: str = None + kind: int = None diff --git a/tests/patchs.py b/tests/patchs.py index 361ce58..8320593 100644 --- a/tests/patchs.py +++ b/tests/patchs.py @@ -1,3 +1,6 @@ +from dataclasses import replace + + # modifica o método privado Loteria._url para garantir que use o script CGI interno def loteria_class() -> None: if not loteria_class.done: @@ -9,15 +12,24 @@ def loteria_class() -> None: # faz o patch import sortepy.loterica - sortepy.loterica.Loteria._url.__defaults__ = ( - fixtures.server_url + "/cgi-bin/", # base - "obter-loteria.py", # script - "?nome=%(loteria)s&concurso=%(concurso)s", # query - ) - for settings in sortepy.loterica.LOTERIAS.values(): - settings.pop("url-script", None) + __patch_url_method(sortepy.loterica.Loteria._url, fixtures.server_url) + __patch_settings(sortepy.loterica.LOTERIAS) + # marca que o patch já foi feito loteria_class.done = True +def __patch_url_method(_url: str, server_url: str) -> None: + _url.__defaults__ = ( + server_url + "/cgi-bin/", # base + "obter-loteria.py", # script + "?nome=%(loteria)s&concurso=%(concurso)s", # query + ) + + +def __patch_settings(loterias: dict) -> None: + for key, value in loterias.items(): + loterias[key] = replace(value, url_script=None) + + loteria_class.done = False From 91136cca00a526cf11cddf58ce9e7c2feb13a230 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Thu, 25 Nov 2021 01:37:39 +0100 Subject: [PATCH 08/21] Refatora iter_resultados --- src/sortepy/script.py | 23 +++++++---------------- tests/test_script.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/sortepy/script.py b/src/sortepy/script.py index cf9c15d..fb87c81 100644 --- a/src/sortepy/script.py +++ b/src/sortepy/script.py @@ -112,27 +112,18 @@ def iter_resultados( ... -def iter_resultados(fun, args, erros): - consultados = set() - - concursos = args[0] +def iter_resultados(fun, args, erros: set): + concursos = dict.fromkeys(args[0]) args = args[1:] - while True: - try: - c = concursos.pop(0) - except IndexError: - return - - if c not in consultados: - consultados.add(c) + for c in concursos: + if c not in erros: try: yield fun(c, *args) except loterica.ResultadoNaoDisponivel: + for e in erros: + if e > c: + erros.add(e) erros.add(c) - for i in reversed(range(len(concursos))): - if concursos[i] > c: - erros.add(concursos.pop(i)) - continue def exec_consultar(loteria: loterica.Loteria, concursos: List[int]) -> int: diff --git a/tests/test_script.py b/tests/test_script.py index ef7d702..00cf028 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -5,8 +5,11 @@ from typing import List import basetest +import sortepy.loterica import sortepy.script +from sortepy.types import ResultadoDict + def run_script(args: List[str]): output = io.StringIO() @@ -173,3 +176,17 @@ def test_conferir_apostas_em_VARIOS_concursos(self): " acertou:\n", " 3: R$ 32.422,00\n", ] + + def test_iter_resultados_coleta_erros(self): + def when_gt5_then_exception(num: int): + if num > 5: + raise sortepy.loterica.ResultadoNaoDisponivel() + return ResultadoDict(concurso=num, numeros=[], premios={}) + + erros = set() + resultados = sortepy.script.iter_resultados( + when_gt5_then_exception, (list(range(10)),), erros + ) + tuple(resultados) + + assert erros == set(range(6, 10)) From dcd02ce893a5ad8eedb86c62e251762cc9102d8b Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Thu, 25 Nov 2021 23:00:08 +0100 Subject: [PATCH 09/21] pytest: config para reconhecer "src" --- conftest.py | 0 poetry.lock | 20 +++++++++++++++++++- pyproject.toml | 4 ++++ 3 files changed, 23 insertions(+), 1 deletion(-) delete mode 100644 conftest.py diff --git a/conftest.py b/conftest.py deleted file mode 100644 index e69de29..0000000 diff --git a/poetry.lock b/poetry.lock index 86d880c..c72d039 100644 --- a/poetry.lock +++ b/poetry.lock @@ -186,6 +186,20 @@ toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "pytest-srcpaths" +version = "1.2.1" +description = "Add paths to sys.path" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.2.0" + +[package.extras] +test = ["tox"] + [[package]] name = "pyyaml" version = "6.0" @@ -232,7 +246,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "aa30ea0e63b0b5fefc1c78247b0e0d52d77128915310fb7aa1b2b8b0f60599a5" +content-hash = "6146f0a27cfb9b3ce416bf0523bd32ff26c85f4e2c9ea897f7e064f242d27cf3" [metadata.files] atomicwrites = [ @@ -303,6 +317,10 @@ pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] +pytest-srcpaths = [ + {file = "pytest-srcpaths-1.2.1.tar.gz", hash = "sha256:78d71b9a2ece6839654421400c4ec78b6b1c1c101cdeda958acb08f1c6847216"}, + {file = "pytest_srcpaths-1.2.1-py3-none-any.whl", hash = "sha256:222b68659e4bd401034b45b38b81f4103afb12807128460e19b6fb82b0e227e7"}, +] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, diff --git a/pyproject.toml b/pyproject.toml index 5799e77..29977f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ python = "^3.8" [tool.poetry.dev-dependencies] pytest = "^6.2.5" +pytest-srcpaths = "^1.2.1" pre-commit = "^2.15.0" [build-system] @@ -51,6 +52,9 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] minversion = "6.0" +pythonpath = [ + "src", +] testpaths = [ "tests", ] From abf380ed49fd8846d44bf0c96a6222cdf05f0a73 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Sun, 5 Dec 2021 23:31:10 +0100 Subject: [PATCH 10/21] Add pytest-cov --- poetry.lock | 102 +++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index c72d039..d55e667 100644 --- a/poetry.lock +++ b/poetry.lock @@ -48,6 +48,20 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "coverage" +version = "6.2" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "distlib" version = "0.3.3" @@ -132,7 +146,7 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.15.0" +version = "2.16.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -186,6 +200,21 @@ toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + [[package]] name = "pytest-srcpaths" version = "1.2.1" @@ -224,6 +253,14 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "tomli" +version = "1.2.2" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "virtualenv" version = "20.10.0" @@ -246,7 +283,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "6146f0a27cfb9b3ce416bf0523bd32ff26c85f4e2c9ea897f7e064f242d27cf3" +content-hash = "b17ad843e210de65a0bed9bb1927c77a177c59af8fd44a784279f59d344d1b85" [metadata.files] atomicwrites = [ @@ -269,6 +306,55 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +coverage = [ + {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, + {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, + {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, + {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, + {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, + {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, + {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, + {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, + {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, + {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, + {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, + {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, + {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, + {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, + {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, + {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, + {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, + {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, + {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, + {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, +] distlib = [ {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"}, {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, @@ -302,8 +388,8 @@ pluggy = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pre-commit = [ - {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"}, - {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"}, + {file = "pre_commit-2.16.0-py2.py3-none-any.whl", hash = "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e"}, + {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, @@ -317,6 +403,10 @@ pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] pytest-srcpaths = [ {file = "pytest-srcpaths-1.2.1.tar.gz", hash = "sha256:78d71b9a2ece6839654421400c4ec78b6b1c1c101cdeda958acb08f1c6847216"}, {file = "pytest_srcpaths-1.2.1-py3-none-any.whl", hash = "sha256:222b68659e4bd401034b45b38b81f4103afb12807128460e19b6fb82b0e227e7"}, @@ -364,6 +454,10 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +tomli = [ + {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, + {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, +] virtualenv = [ {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, diff --git a/pyproject.toml b/pyproject.toml index 29977f2..bd8012e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ python = "^3.8" [tool.poetry.dev-dependencies] pytest = "^6.2.5" +pytest-cov = "^3.0.0" pytest-srcpaths = "^1.2.1" pre-commit = "^2.15.0" From afdc1ac381ad8b55e5691d65f6c147ebe818533c Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Sun, 5 Dec 2021 23:34:12 +0100 Subject: [PATCH 11/21] Remove __init__.py sem uso --- src/sortepy/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/sortepy/__init__.py diff --git a/src/sortepy/__init__.py b/src/sortepy/__init__.py deleted file mode 100644 index e69de29..0000000 From f4e237415b802ad9a271ea2205224468f2aa8916 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Tue, 7 Dec 2021 01:14:56 +0100 Subject: [PATCH 12/21] Update to py3.10 --- poetry.lock | 82 ++++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 3 +- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index d55e667..5eb25da 100644 --- a/poetry.lock +++ b/poetry.lock @@ -32,6 +32,14 @@ python-versions = ">=2.7" docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "cfgv" version = "3.3.1" @@ -40,6 +48,17 @@ category = "dev" optional = false python-versions = ">=3.6.1" +[[package]] +name = "charset-normalizer" +version = "2.0.9" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + [[package]] name = "colorama" version = "0.4.4" @@ -93,6 +112,14 @@ python-versions = ">=3.6.1" [package.extras] license = ["ukkonen"] +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "iniconfig" version = "1.1.1" @@ -237,6 +264,24 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + [[package]] name = "six" version = "1.16.0" @@ -261,6 +306,19 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "urllib3" +version = "1.26.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + [[package]] name = "virtualenv" version = "20.10.0" @@ -282,8 +340,8 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [metadata] lock-version = "1.1" -python-versions = "^3.8" -content-hash = "b17ad843e210de65a0bed9bb1927c77a177c59af8fd44a784279f59d344d1b85" +python-versions = "^3.9" +content-hash = "860d00f483e2ea5a487e330a0f7c4a96747db709fc6d6e1da3ba904560461362" [metadata.files] atomicwrites = [ @@ -298,10 +356,18 @@ attrs = [ {file = "backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl", hash = "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b"}, {file = "backports.entry_points_selectable-1.1.1.tar.gz", hash = "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386"}, ] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] +charset-normalizer = [ + {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, + {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, +] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, @@ -367,6 +433,10 @@ identify = [ {file = "identify-2.4.0-py2.py3-none-any.whl", hash = "sha256:eba31ca80258de6bb51453084bff4a923187cd2193b9c13710f2516ab30732cc"}, {file = "identify-2.4.0.tar.gz", hash = "sha256:a33ae873287e81651c7800ca309dc1f84679b763c9c8b30680e16fbfa82f0107"}, ] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -446,6 +516,10 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] +requests = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -458,6 +532,10 @@ tomli = [ {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, ] +urllib3 = [ + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, +] virtualenv = [ {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, diff --git a/pyproject.toml b/pyproject.toml index bd8012e..82fac74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,8 @@ classifiers = [ "sorte.py" = "sortepy.script:main" [tool.poetry.dependencies] -python = "^3.8" +python = "^3.10" +requests = "^2.26.0" [tool.poetry.dev-dependencies] pytest = "^6.2.5" From 001efee5a33171f058ba05a81faae3afd2c33084 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Tue, 21 Dec 2021 14:08:25 +0100 Subject: [PATCH 13/21] Adiciona marcador pytest para v1 --- tests/basetest.py | 5 ++--- tests/conftest.py | 42 ++++++++++++++++++++++++++++++++++++++ tests/fixtures/__init__.py | 16 +++++---------- tests/patchs.py | 5 +---- tests/test_duplasena.py | 5 +++++ tests/test_federal.py | 5 +++++ tests/test_lotofacil.py | 5 +++++ tests/test_lotomania.py | 5 +++++ tests/test_megasena.py | 5 +++++ tests/test_quina.py | 5 +++++ tests/test_script.py | 5 +++++ tests/test_util.py | 5 ++++- 12 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/basetest.py b/tests/basetest.py index 05b500e..c876af0 100644 --- a/tests/basetest.py +++ b/tests/basetest.py @@ -3,13 +3,12 @@ import tempfile import unittest -# patch para classe de loteria utilizar o servidor interno -import patchs +import pytest from sortepy.loterica import Loteria -patchs.loteria_class() +pytestmark = pytest.mark.v1 def tempdir(custom_prefix: str = "sortepy-"): diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ac8f804 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Iterable + +import pytest + + +if TYPE_CHECKING: + from _pytest.config import Config + from _pytest.config.argparsing import Parser + from _pytest.fixtures import FixtureRequest + from _pytest.python import Function + + +def pytest_addoption(parser: Parser) -> None: + parser.addoption( + "--run-v1", action="store_true", default=False, help="run sorte.py v1 tests" + ) + + +def pytest_configure(config: Config) -> None: + config.addinivalue_line("markers", "v1: mark test as sorte.py v1") + + +def pytest_collection_modifyitems(config: Config, items: Iterable[Function]) -> None: + # do not apply skip marker to v1 when --run-v1 is present + if config.getoption("--run-v1"): + return + + skip_v1 = pytest.mark.skip(reason="need --run-v1 option to run") + for item in items: + if "v1" in item.keywords: + item.add_marker(skip_v1) + + +@pytest.fixture(scope="session", autouse=True) +def patch_v1_test(request: FixtureRequest) -> None: + # only apply the patch if --run-v1 is present + if request.config.getoption("--run-v1"): + import patchs + patchs.loteria_class() diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index 498f02c..f2c1c8e 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -4,30 +4,23 @@ import os -# diretório onde o servidor irá iniciar -FIXTURES_DIR = os.path.join(os.path.dirname(__file__)) - # declaração de globais -global server_port, server_url +global server_url def start_server(): - if not start_server.status: + if not getattr(start_server, "status", 0): # inicia servidor server = FixtureHttpServer() server.start() atexit.register(server.terminate) # guarda a porta e cria URL - global server_port, server_url - server_port = server.port + global server_url server_url = "http://127.0.0.1:%d" % server.port # marca servidor como já inicializado start_server.status = 1 -start_server.status = 0 - - class FixtureRequestHandler(http.server.CGIHTTPRequestHandler): def log_message(self, *args, **kwargs): # Desabilitando mensagens de log no terminal. @@ -49,5 +42,6 @@ def __init__(self): self.port = self.httpd.server_port def run(self): - os.chdir(FIXTURES_DIR) + fixtures_dir = os.path.join(os.path.dirname(__file__)) + os.chdir(fixtures_dir) self.httpd.serve_forever() diff --git a/tests/patchs.py b/tests/patchs.py index 8320593..3eef28e 100644 --- a/tests/patchs.py +++ b/tests/patchs.py @@ -3,7 +3,7 @@ # modifica o método privado Loteria._url para garantir que use o script CGI interno def loteria_class() -> None: - if not loteria_class.done: + if not getattr(loteria_class, "done", False): # garantie que servidor interno de fixtures está inicializado import fixtures @@ -30,6 +30,3 @@ def __patch_url_method(_url: str, server_url: str) -> None: def __patch_settings(loterias: dict) -> None: for key, value in loterias.items(): loterias[key] = replace(value, url_script=None) - - -loteria_class.done = False diff --git a/tests/test_duplasena.py b/tests/test_duplasena.py index da1fbb4..0b3360c 100644 --- a/tests/test_duplasena.py +++ b/tests/test_duplasena.py @@ -1,8 +1,13 @@ # encoding=utf8 +import pytest + import basetest +pytestmark = pytest.mark.v1 + + class DuplaSenaTest(basetest.LoteriaTestCase): "Dupla Sena" diff --git a/tests/test_federal.py b/tests/test_federal.py index c4fda6c..2a77ded 100644 --- a/tests/test_federal.py +++ b/tests/test_federal.py @@ -1,6 +1,11 @@ +import pytest + import basetest +pytestmark = pytest.mark.v1 + + class FederalTest(basetest.LoteriaTestCase): "Loteria Federal" diff --git a/tests/test_lotofacil.py b/tests/test_lotofacil.py index 00f4963..7c816e4 100644 --- a/tests/test_lotofacil.py +++ b/tests/test_lotofacil.py @@ -1,8 +1,13 @@ # encoding=utf8 +import pytest + import basetest +pytestmark = pytest.mark.v1 + + class LotofacilTest(basetest.LoteriaTestCase): # atributos do teste 'gerar aposta' nome = "lotofacil" diff --git a/tests/test_lotomania.py b/tests/test_lotomania.py index 55b89e8..51ea472 100644 --- a/tests/test_lotomania.py +++ b/tests/test_lotomania.py @@ -1,8 +1,13 @@ # encoding=utf8 +import pytest + import basetest +pytestmark = pytest.mark.v1 + + class LotomaniaTest(basetest.LoteriaTestCase): # atributos do teste 'gerar aposta' nome = "lotomania" diff --git a/tests/test_megasena.py b/tests/test_megasena.py index 7d95a24..ccd8c31 100644 --- a/tests/test_megasena.py +++ b/tests/test_megasena.py @@ -1,8 +1,13 @@ # encoding=utf8 +import pytest + import basetest +pytestmark = pytest.mark.v1 + + class MegaSenaTest(basetest.LoteriaTestCase): "Mega Sena" diff --git a/tests/test_quina.py b/tests/test_quina.py index 73ad997..3434103 100644 --- a/tests/test_quina.py +++ b/tests/test_quina.py @@ -1,8 +1,13 @@ # encoding=utf8 +import pytest + import basetest +pytestmark = pytest.mark.v1 + + class QuinaTest(basetest.LoteriaTestCase): # atributos do teste 'gerar aposta' nome = "quina" diff --git a/tests/test_script.py b/tests/test_script.py index 00cf028..43a798d 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -4,6 +4,8 @@ from typing import List +import pytest + import basetest import sortepy.loterica import sortepy.script @@ -11,6 +13,9 @@ from sortepy.types import ResultadoDict +pytestmark = pytest.mark.v1 + + def run_script(args: List[str]): output = io.StringIO() diff --git a/tests/test_util.py b/tests/test_util.py index b991c4c..d2b8a5b 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -5,13 +5,16 @@ from typing import Any from typing import Dict +import pytest + import fixtures import sortepy.util from basetest import tempdir -fixtures.start_server() +pytestmark = pytest.mark.v1 + # Conteúdo para 'pagina_ascii.html' CONTEUDO_ASCII = """ From f8631dcf80563712a0b91b649810c961a5d4af8e Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Mon, 20 Dec 2021 13:09:36 +0100 Subject: [PATCH 14/21] Adiciona interface para DrawResponseHandler --- src/sortepy/v2/spi.py | 39 +++++++++++++++++++++++++++++++++++++++ src/sortepy/v2/types.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/sortepy/v2/spi.py create mode 100644 src/sortepy/v2/types.py diff --git a/src/sortepy/v2/spi.py b/src/sortepy/v2/spi.py new file mode 100644 index 0000000..91e35a5 --- /dev/null +++ b/src/sortepy/v2/spi.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from abc import ABCMeta +from abc import abstractmethod +from typing import TYPE_CHECKING +from typing import Generic +from typing import TypeVar + + +T = TypeVar("T") + +if TYPE_CHECKING: + from sortepy.v2.types import DrawResult + + +class DrawResponseHandler(Generic[T], metaclass=ABCMeta): + @abstractmethod + def handle(self, data: T) -> DrawResult: + """Handles a response data and gives a structured result. + + >>> h.handle(data) + DrawResult( + draw_date=datetime.datetime(2021, 12, 4, 0, 0), + draw_number="5722", + draw_details=[ + DrawDetail( + ball_numbers=[14, 40, 51, 56, 57], + jackpot=4_700_000, + currency="BRL", + prize_breakdown={ + "5": DrawPrize(winners=0, prize=0), + "4": DrawPrize(winners=50, prize=9043.15), + "3": DrawPrize(winners=4_254, prize=101.22), + "2": DrawPrize(winners=117_464, prize=3.66) + } + ) + ] + ) + """ diff --git a/src/sortepy/v2/types.py b/src/sortepy/v2/types.py new file mode 100644 index 0000000..d629469 --- /dev/null +++ b/src/sortepy/v2/types.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING +from typing import Dict +from typing import Sequence + + +if TYPE_CHECKING: + from datetime import datetime + + +# TODO: remove ignore[call-overload] comments when mypy hook supports py3.10 (due to `kw_only`) + + +@dataclass(frozen=True, kw_only=True) # type: ignore[call-overload] +class DrawResult: + draw_date: datetime + draw_number: str + draw_details: Sequence[DrawDetail] + + +@dataclass(frozen=True, kw_only=True) # type: ignore[call-overload] +class DrawDetail: + ball_numbers: Sequence[int] + jackpot: int | float + currency: str + prize_breakdown: Dict[str, DrawPrize] + + +@dataclass(frozen=True, kw_only=True) # type: ignore[call-overload] +class DrawPrize: + winners: int + prize: int | float From 2da604b440aba55a11e49c9c3cc4315ae855d9a9 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Mon, 20 Dec 2021 14:07:51 +0100 Subject: [PATCH 15/21] Adiciona handler QuinaResponseHandler --- src/sortepy/v2/loterica/_types.py | 39 +++++++++++++++ src/sortepy/v2/loterica/handlers.py | 41 ++++++++++++++++ tests/v2/conftest.py | 8 ++++ tests/v2/fixtures/response_quina_5734.json | 55 ++++++++++++++++++++++ tests/v2/loterica/test_handlers.py | 34 +++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 src/sortepy/v2/loterica/_types.py create mode 100644 src/sortepy/v2/loterica/handlers.py create mode 100644 tests/v2/conftest.py create mode 100644 tests/v2/fixtures/response_quina_5734.json create mode 100644 tests/v2/loterica/test_handlers.py diff --git a/src/sortepy/v2/loterica/_types.py b/src/sortepy/v2/loterica/_types.py new file mode 100644 index 0000000..bb29c75 --- /dev/null +++ b/src/sortepy/v2/loterica/_types.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from typing import TypedDict + + +class ResultadoQuinaDict(TypedDict): + tipoJogo: str + numero: int + nomeMunicipioUFSorteio: str + dataApuracao: str + valorArrecadado: float + valorEstimadoProximoConcurso: float + valorAcumuladoProximoConcurso: float + valorAcumuladoConcursoEspecial: float + valorAcumuladoConcurso_0_5: float + acumulado: bool + indicadorConcursoEspecial: int + dezenasSorteadasOrdemSorteio: list[str] + listaResultadoEquipeEsportiva: str + numeroJogo: int + nomeTimeCoracaoMesSorte: str + tipoPublicacao: int + observacao: str + localSorteio: str + dataProximoConcurso: str + numeroConcursoAnterior: int + numeroConcursoProximo: int + valorTotalPremioFaixaUm: int + numeroConcursoFinal_0_5: int + listaMunicipioUFGanhadores: list[str] + listaRateioPremio: list[RateioPremioDict] + listaDezenas: list[str] + + +class RateioPremioDict(TypedDict): + faixa: int + numeroDeGanhadores: int + valorPremio: float + descricaoFaixa: str diff --git a/src/sortepy/v2/loterica/handlers.py b/src/sortepy/v2/loterica/handlers.py new file mode 100644 index 0000000..a9511b0 --- /dev/null +++ b/src/sortepy/v2/loterica/handlers.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from datetime import datetime + +from sortepy.v2.loterica._types import ResultadoQuinaDict +from sortepy.v2.spi import DrawResponseHandler +from sortepy.v2.types import DrawDetail +from sortepy.v2.types import DrawPrize +from sortepy.v2.types import DrawResult + + +class QuinaResponseHandler(DrawResponseHandler[ResultadoQuinaDict]): + def handle(self, data: ResultadoQuinaDict) -> DrawResult: + return DrawResult( + draw_date=datetime.strptime(data["dataApuracao"], "%d/%m/%Y"), + draw_number=str(data["numero"]), + draw_details=[ + DrawDetail( + ball_numbers=[int(n) for n in data["listaDezenas"]], + jackpot=data["valorAcumuladoProximoConcurso"], + currency="BRL", + prize_breakdown=self.get_prizes(data), + ) + ], + ) + + def get_prizes(self, data: ResultadoQuinaDict) -> dict[str, DrawPrize]: + hit_mapping = { + "5 acertos": "5", + "4 acertos": "4", + "3 acertos": "3", + "2 acertos": "2", + } + + prizes = {} + for p in data["listaRateioPremio"]: + key = hit_mapping[p["descricaoFaixa"]] + value = DrawPrize(winners=p["numeroDeGanhadores"], prize=p["valorPremio"]) + prizes[key] = value + + return prizes diff --git a/tests/v2/conftest.py b/tests/v2/conftest.py new file mode 100644 index 0000000..fd19054 --- /dev/null +++ b/tests/v2/conftest.py @@ -0,0 +1,8 @@ +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="session") +def fixtures_dir() -> Path: + return Path(__file__).parent / "fixtures" diff --git a/tests/v2/fixtures/response_quina_5734.json b/tests/v2/fixtures/response_quina_5734.json new file mode 100644 index 0000000..90d7d49 --- /dev/null +++ b/tests/v2/fixtures/response_quina_5734.json @@ -0,0 +1,55 @@ +{ + "tipoJogo": "QUINA", + "numero": 5734, + "nomeMunicipioUFSorteio": "SÃO PAULO, SP", + "dataApuracao": "18/12/2021", + "valorArrecadado": 10846960.00, + "valorEstimadoProximoConcurso": 12000000.00, + "valorAcumuladoProximoConcurso": 10358517.22, + "valorAcumuladoConcursoEspecial": 68252631.10, + "valorAcumuladoConcurso_0_5": 10358517.22, + "acumulado": true, + "indicadorConcursoEspecial": 1, + "dezenasSorteadasOrdemSorteio": ["029", "074", "027", "026", "077"], + "listaResultadoEquipeEsportiva": null, + "numeroJogo": 3, + "nomeTimeCoracaoMesSorte": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", + "tipoPublicacao": 3, + "observacao": "Acumulado próximo concurso final cinco (5735) R$ 10.358.517,22.", + "localSorteio": "ESPAÇO LOTERIAS CAIXA ", + "dataProximoConcurso": "20/12/2021", + "numeroConcursoAnterior": 5733, + "numeroConcursoProximo": 5735, + "valorTotalPremioFaixaUm": 0, + "numeroConcursoFinal_0_5": 5735, + "listaMunicipioUFGanhadores": [], + "listaRateioPremio": [ + { + "faixa": 1, + "numeroDeGanhadores": 0, + "valorPremio": 0.00, + "descricaoFaixa": "5 acertos" + }, + { + "faixa": 2, + "numeroDeGanhadores": 61, + "valorPremio": 8093.88, + "descricaoFaixa": "4 acertos" + }, + { + "faixa": 3, + "numeroDeGanhadores": 6120, + "valorPremio": 76.83, + "descricaoFaixa": "3 acertos" + }, + { + "faixa": 4, + "numeroDeGanhadores": 151744, + "valorPremio": 3.09, + "descricaoFaixa": "2 acertos" + } + ], + "listaDezenas": ["026", "027", "029", "074", "077"], + "listaDezenasSegundoSorteio": null, + "id": null +} diff --git a/tests/v2/loterica/test_handlers.py b/tests/v2/loterica/test_handlers.py new file mode 100644 index 0000000..6373dc5 --- /dev/null +++ b/tests/v2/loterica/test_handlers.py @@ -0,0 +1,34 @@ +import json + +from datetime import datetime +from pathlib import Path + +from sortepy.v2.loterica.handlers import QuinaResponseHandler +from sortepy.v2.types import DrawDetail +from sortepy.v2.types import DrawPrize +from sortepy.v2.types import DrawResult + + +def test_quina_handler(fixtures_dir: Path): + handler = QuinaResponseHandler() + data = json.load(open(fixtures_dir / "response_quina_5734.json")) + + actual_result = handler.handle(data) + + assert actual_result == DrawResult( + draw_date=datetime(year=2021, month=12, day=18), + draw_number="5734", + draw_details=[ + DrawDetail( + ball_numbers=[26, 27, 29, 74, 77], + jackpot=10_358_517.22, + currency="BRL", + prize_breakdown={ + "5": DrawPrize(winners=0, prize=0.00), + "4": DrawPrize(winners=61, prize=8093.88), + "3": DrawPrize(winners=6120, prize=76.83), + "2": DrawPrize(winners=151744, prize=3.09), + }, + ) + ], + ) From 8b0663762f63a43600d5e1af59fae53ad208b433 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Mon, 20 Dec 2021 14:07:51 +0100 Subject: [PATCH 16/21] Adiciona handler LotofacilResponseHandler --- src/sortepy/v2/loterica/_types.py | 2 +- src/sortepy/v2/loterica/handlers.py | 62 ++++++++----- .../v2/fixtures/response_lotofacil_2402.json | 86 +++++++++++++++++++ tests/v2/loterica/test_handlers.py | 27 ++++++ 4 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 tests/v2/fixtures/response_lotofacil_2402.json diff --git a/src/sortepy/v2/loterica/_types.py b/src/sortepy/v2/loterica/_types.py index bb29c75..0e08c9e 100644 --- a/src/sortepy/v2/loterica/_types.py +++ b/src/sortepy/v2/loterica/_types.py @@ -3,7 +3,7 @@ from typing import TypedDict -class ResultadoQuinaDict(TypedDict): +class ResultadoT1Dict(TypedDict): tipoJogo: str numero: int nomeMunicipioUFSorteio: str diff --git a/src/sortepy/v2/loterica/handlers.py b/src/sortepy/v2/loterica/handlers.py index a9511b0..d2dcf71 100644 --- a/src/sortepy/v2/loterica/handlers.py +++ b/src/sortepy/v2/loterica/handlers.py @@ -2,40 +2,56 @@ from datetime import datetime -from sortepy.v2.loterica._types import ResultadoQuinaDict +from sortepy.v2.loterica._types import ResultadoT1Dict from sortepy.v2.spi import DrawResponseHandler from sortepy.v2.types import DrawDetail from sortepy.v2.types import DrawPrize from sortepy.v2.types import DrawResult -class QuinaResponseHandler(DrawResponseHandler[ResultadoQuinaDict]): - def handle(self, data: ResultadoQuinaDict) -> DrawResult: - return DrawResult( - draw_date=datetime.strptime(data["dataApuracao"], "%d/%m/%Y"), - draw_number=str(data["numero"]), - draw_details=[ - DrawDetail( - ball_numbers=[int(n) for n in data["listaDezenas"]], - jackpot=data["valorAcumuladoProximoConcurso"], - currency="BRL", - prize_breakdown=self.get_prizes(data), - ) - ], - ) - - def get_prizes(self, data: ResultadoQuinaDict) -> dict[str, DrawPrize]: +class LotofacilResponseHandler(DrawResponseHandler[ResultadoT1Dict]): + def handle(self, data: ResultadoT1Dict) -> DrawResult: + hit_mapping = { + "15 acertos": "15", + "14 acertos": "14", + "13 acertos": "13", + "12 acertos": "12", + "11 acertos": "11", + } + return _handle_response_type1(data, hit_mapping) + + +class QuinaResponseHandler(DrawResponseHandler[ResultadoT1Dict]): + def handle(self, data: ResultadoT1Dict) -> DrawResult: hit_mapping = { "5 acertos": "5", "4 acertos": "4", "3 acertos": "3", "2 acertos": "2", } + return _handle_response_type1(data, hit_mapping) + + +def _handle_response_type1(data: ResultadoT1Dict, hit_mapping: dict) -> DrawResult: + return DrawResult( + draw_date=datetime.strptime(data["dataApuracao"], "%d/%m/%Y"), + draw_number=str(data["numero"]), + draw_details=[ + DrawDetail( + ball_numbers=[int(n) for n in data["listaDezenas"]], + jackpot=data["valorAcumuladoProximoConcurso"], + currency="BRL", + prize_breakdown=_get_prizes_type1(data, hit_mapping), + ) + ], + ) + - prizes = {} - for p in data["listaRateioPremio"]: - key = hit_mapping[p["descricaoFaixa"]] - value = DrawPrize(winners=p["numeroDeGanhadores"], prize=p["valorPremio"]) - prizes[key] = value +def _get_prizes_type1(data: ResultadoT1Dict, hit_mapping: dict) -> dict[str, DrawPrize]: + prizes = {} + for p in data["listaRateioPremio"]: + key = hit_mapping[p["descricaoFaixa"]] + value = DrawPrize(winners=p["numeroDeGanhadores"], prize=p["valorPremio"]) + prizes[key] = value - return prizes + return prizes diff --git a/tests/v2/fixtures/response_lotofacil_2402.json b/tests/v2/fixtures/response_lotofacil_2402.json new file mode 100644 index 0000000..97cf093 --- /dev/null +++ b/tests/v2/fixtures/response_lotofacil_2402.json @@ -0,0 +1,86 @@ +{ + "tipoJogo": "LOTOFACIL", + "numero": 2402, + "nomeMunicipioUFSorteio": "SÃO PAULO, SP", + "dataApuracao": "20/12/2021", + "valorArrecadado": 18576502.50, + "valorEstimadoProximoConcurso": 1500000.00, + "valorAcumuladoProximoConcurso": 0.00, + "valorAcumuladoConcursoEspecial": 32851669.79, + "valorAcumuladoConcurso_0_5": 410529.74, + "acumulado": false, + "indicadorConcursoEspecial": 1, + "dezenasSorteadasOrdemSorteio": ["001", "004", "003", "002", "009", "014", "013", "025", "006", "020", "023", "005", "011", "010", "018"], + "listaResultadoEquipeEsportiva": null, + "numeroJogo": 8, + "nomeTimeCoracaoMesSorte": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", + "tipoPublicacao": 3, + "observacao": "", + "localSorteio": "ESPAÇO LOTERIAS CAIXA ", + "dataProximoConcurso": "21/12/2021", + "numeroConcursoAnterior": 2401, + "numeroConcursoProximo": 2403, + "valorTotalPremioFaixaUm": 0, + "numeroConcursoFinal_0_5": 2410, + "listaMunicipioUFGanhadores": [ + { + "posicao": 1, + "ganhadores": 1, + "municipio": "SERRA", + "uf": "ES", + "nomeFatansiaUL": "", + "serie": "" + }, + { + "posicao": 1, + "ganhadores": 1, + "municipio": "CARAZINHO", + "uf": "RS", + "nomeFatansiaUL": "", + "serie": "" + }, + { + "posicao": 1, + "ganhadores": 1, + "municipio": "CACHOEIRA PAULISTA", + "uf": "SP", + "nomeFatansiaUL": "", + "serie": "" + } + ], + "listaRateioPremio": [ + { + "faixa": 1, + "numeroDeGanhadores": 3, + "valorPremio": 370337.82, + "descricaoFaixa": "15 acertos" + }, + { + "faixa": 2, + "numeroDeGanhadores": 321, + "valorPremio": 1036.73, + "descricaoFaixa": "14 acertos" + }, + { + "faixa": 3, + "numeroDeGanhadores": 13409, + "valorPremio": 25.00, + "descricaoFaixa": "13 acertos" + }, + { + "faixa": 4, + "numeroDeGanhadores": 144941, + "valorPremio": 10.00, + "descricaoFaixa": "12 acertos" + }, + { + "faixa": 5, + "numeroDeGanhadores": 741668, + "valorPremio": 5.00, + "descricaoFaixa": "11 acertos" + } + ], + "listaDezenas": ["001", "002", "003", "004", "005", "006", "009", "010", "011", "013", "014", "018", "020", "023", "025"], + "listaDezenasSegundoSorteio": null, + "id": null +} diff --git a/tests/v2/loterica/test_handlers.py b/tests/v2/loterica/test_handlers.py index 6373dc5..cae89ac 100644 --- a/tests/v2/loterica/test_handlers.py +++ b/tests/v2/loterica/test_handlers.py @@ -3,12 +3,39 @@ from datetime import datetime from pathlib import Path +from sortepy.v2.loterica.handlers import LotofacilResponseHandler from sortepy.v2.loterica.handlers import QuinaResponseHandler from sortepy.v2.types import DrawDetail from sortepy.v2.types import DrawPrize from sortepy.v2.types import DrawResult +def test_lotofacil_handler(fixtures_dir: Path): + handler = LotofacilResponseHandler() + data = json.load(open(fixtures_dir / "response_lotofacil_2402.json")) + + actual_result = handler.handle(data) + + assert actual_result == DrawResult( + draw_date=datetime(year=2021, month=12, day=20), + draw_number="2402", + draw_details=[ + DrawDetail( + ball_numbers=[1, 2, 3, 4, 5, 6, 9, 10, 11, 13, 14, 18, 20, 23, 25], + jackpot=0.00, + currency="BRL", + prize_breakdown={ + "15": DrawPrize(winners=3, prize=370_337.82), + "14": DrawPrize(winners=321, prize=1036.73), + "13": DrawPrize(winners=13_409, prize=25.00), + "12": DrawPrize(winners=144_941, prize=10.00), + "11": DrawPrize(winners=741_668, prize=5.00), + }, + ) + ], + ) + + def test_quina_handler(fixtures_dir: Path): handler = QuinaResponseHandler() data = json.load(open(fixtures_dir / "response_quina_5734.json")) From ce961212937f1880f281b59dd55f9b867d2242e7 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Wed, 22 Dec 2021 02:04:24 +0100 Subject: [PATCH 17/21] Refactoring --- src/sortepy/v2/loterica/_types.py | 2 +- src/sortepy/v2/loterica/handlers.py | 35 +++++--------- tests/v2/loterica/test_handlers.py | 72 +++++++++++++---------------- 3 files changed, 43 insertions(+), 66 deletions(-) diff --git a/src/sortepy/v2/loterica/_types.py b/src/sortepy/v2/loterica/_types.py index 0e08c9e..e3d7f32 100644 --- a/src/sortepy/v2/loterica/_types.py +++ b/src/sortepy/v2/loterica/_types.py @@ -3,7 +3,7 @@ from typing import TypedDict -class ResultadoT1Dict(TypedDict): +class LotericaResponseDict(TypedDict): tipoJogo: str numero: int nomeMunicipioUFSorteio: str diff --git a/src/sortepy/v2/loterica/handlers.py b/src/sortepy/v2/loterica/handlers.py index d2dcf71..c785d3a 100644 --- a/src/sortepy/v2/loterica/handlers.py +++ b/src/sortepy/v2/loterica/handlers.py @@ -2,37 +2,24 @@ from datetime import datetime -from sortepy.v2.loterica._types import ResultadoT1Dict +from sortepy.v2.loterica._types import LotericaResponseDict from sortepy.v2.spi import DrawResponseHandler from sortepy.v2.types import DrawDetail from sortepy.v2.types import DrawPrize from sortepy.v2.types import DrawResult -class LotofacilResponseHandler(DrawResponseHandler[ResultadoT1Dict]): - def handle(self, data: ResultadoT1Dict) -> DrawResult: - hit_mapping = { - "15 acertos": "15", - "14 acertos": "14", - "13 acertos": "13", - "12 acertos": "12", - "11 acertos": "11", - } - return _handle_response_type1(data, hit_mapping) +class LotofacilResponseHandler(DrawResponseHandler[LotericaResponseDict]): + def handle(self, data: LotericaResponseDict) -> DrawResult: + return _handle_response_type1(data) -class QuinaResponseHandler(DrawResponseHandler[ResultadoT1Dict]): - def handle(self, data: ResultadoT1Dict) -> DrawResult: - hit_mapping = { - "5 acertos": "5", - "4 acertos": "4", - "3 acertos": "3", - "2 acertos": "2", - } - return _handle_response_type1(data, hit_mapping) +class QuinaResponseHandler(DrawResponseHandler[LotericaResponseDict]): + def handle(self, data: LotericaResponseDict) -> DrawResult: + return _handle_response_type1(data) -def _handle_response_type1(data: ResultadoT1Dict, hit_mapping: dict) -> DrawResult: +def _handle_response_type1(data: LotericaResponseDict) -> DrawResult: return DrawResult( draw_date=datetime.strptime(data["dataApuracao"], "%d/%m/%Y"), draw_number=str(data["numero"]), @@ -41,16 +28,16 @@ def _handle_response_type1(data: ResultadoT1Dict, hit_mapping: dict) -> DrawResu ball_numbers=[int(n) for n in data["listaDezenas"]], jackpot=data["valorAcumuladoProximoConcurso"], currency="BRL", - prize_breakdown=_get_prizes_type1(data, hit_mapping), + prize_breakdown=_get_prizes_type1(data), ) ], ) -def _get_prizes_type1(data: ResultadoT1Dict, hit_mapping: dict) -> dict[str, DrawPrize]: +def _get_prizes_type1(data: LotericaResponseDict) -> dict[str, DrawPrize]: prizes = {} for p in data["listaRateioPremio"]: - key = hit_mapping[p["descricaoFaixa"]] + key = p["descricaoFaixa"].split()[0] value = DrawPrize(winners=p["numeroDeGanhadores"], prize=p["valorPremio"]) prizes[key] = value diff --git a/tests/v2/loterica/test_handlers.py b/tests/v2/loterica/test_handlers.py index cae89ac..72220f6 100644 --- a/tests/v2/loterica/test_handlers.py +++ b/tests/v2/loterica/test_handlers.py @@ -5,57 +5,47 @@ from sortepy.v2.loterica.handlers import LotofacilResponseHandler from sortepy.v2.loterica.handlers import QuinaResponseHandler -from sortepy.v2.types import DrawDetail from sortepy.v2.types import DrawPrize -from sortepy.v2.types import DrawResult def test_lotofacil_handler(fixtures_dir: Path): handler = LotofacilResponseHandler() data = json.load(open(fixtures_dir / "response_lotofacil_2402.json")) - actual_result = handler.handle(data) - - assert actual_result == DrawResult( - draw_date=datetime(year=2021, month=12, day=20), - draw_number="2402", - draw_details=[ - DrawDetail( - ball_numbers=[1, 2, 3, 4, 5, 6, 9, 10, 11, 13, 14, 18, 20, 23, 25], - jackpot=0.00, - currency="BRL", - prize_breakdown={ - "15": DrawPrize(winners=3, prize=370_337.82), - "14": DrawPrize(winners=321, prize=1036.73), - "13": DrawPrize(winners=13_409, prize=25.00), - "12": DrawPrize(winners=144_941, prize=10.00), - "11": DrawPrize(winners=741_668, prize=5.00), - }, - ) - ], - ) + result = handler.handle(data) + assert result.draw_date == datetime(year=2021, month=12, day=20) + assert result.draw_number == "2402" + assert len(result.draw_details) == 1 + + draw = result.draw_details[0] + assert draw.ball_numbers == [1, 2, 3, 4, 5, 6, 9, 10, 11, 13, 14, 18, 20, 23, 25] + assert draw.jackpot == 0.00 + assert draw.currency == "BRL" + assert draw.prize_breakdown == { + "15": DrawPrize(winners=3, prize=370_337.82), + "14": DrawPrize(winners=321, prize=1036.73), + "13": DrawPrize(winners=13_409, prize=25.00), + "12": DrawPrize(winners=144_941, prize=10.00), + "11": DrawPrize(winners=741_668, prize=5.00), + } def test_quina_handler(fixtures_dir: Path): handler = QuinaResponseHandler() data = json.load(open(fixtures_dir / "response_quina_5734.json")) - actual_result = handler.handle(data) - - assert actual_result == DrawResult( - draw_date=datetime(year=2021, month=12, day=18), - draw_number="5734", - draw_details=[ - DrawDetail( - ball_numbers=[26, 27, 29, 74, 77], - jackpot=10_358_517.22, - currency="BRL", - prize_breakdown={ - "5": DrawPrize(winners=0, prize=0.00), - "4": DrawPrize(winners=61, prize=8093.88), - "3": DrawPrize(winners=6120, prize=76.83), - "2": DrawPrize(winners=151744, prize=3.09), - }, - ) - ], - ) + result = handler.handle(data) + assert result.draw_date == datetime(year=2021, month=12, day=18) + assert result.draw_number == "5734" + assert len(result.draw_details) == 1 + + draw = result.draw_details[0] + assert draw.ball_numbers == [26, 27, 29, 74, 77] + assert draw.jackpot == 10_358_517.22 + assert draw.currency == "BRL" + assert draw.prize_breakdown == { + "5": DrawPrize(winners=0, prize=0.00), + "4": DrawPrize(winners=61, prize=8093.88), + "3": DrawPrize(winners=6120, prize=76.83), + "2": DrawPrize(winners=151744, prize=3.09), + } From 83ab1e22772b624e8aa3ca732e73bec5c7c73e51 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Mon, 20 Dec 2021 14:07:51 +0100 Subject: [PATCH 18/21] Adiciona handler FederalResponseHandler --- src/sortepy/v2/loterica/handlers.py | 5 + tests/v2/fixtures/response_federal_5624.json | 102 +++++++++++++++++++ tests/v2/loterica/test_handlers.py | 23 +++++ 3 files changed, 130 insertions(+) create mode 100644 tests/v2/fixtures/response_federal_5624.json diff --git a/src/sortepy/v2/loterica/handlers.py b/src/sortepy/v2/loterica/handlers.py index c785d3a..174e983 100644 --- a/src/sortepy/v2/loterica/handlers.py +++ b/src/sortepy/v2/loterica/handlers.py @@ -9,6 +9,11 @@ from sortepy.v2.types import DrawResult +class FederalResponseHandler(DrawResponseHandler[LotericaResponseDict]): + def handle(self, data: LotericaResponseDict) -> DrawResult: + return _handle_response_type1(data) + + class LotofacilResponseHandler(DrawResponseHandler[LotericaResponseDict]): def handle(self, data: LotericaResponseDict) -> DrawResult: return _handle_response_type1(data) diff --git a/tests/v2/fixtures/response_federal_5624.json b/tests/v2/fixtures/response_federal_5624.json new file mode 100644 index 0000000..065e95f --- /dev/null +++ b/tests/v2/fixtures/response_federal_5624.json @@ -0,0 +1,102 @@ +{ + "tipoJogo": "LOTERIA_FEDERAL", + "numero": 5624, + "nomeMunicipioUFSorteio": "SAO PAULO, SP", + "dataApuracao": "22/12/2021", + "valorArrecadado": 0.00, + "valorEstimadoProximoConcurso": 0.00, + "valorAcumuladoProximoConcurso": 0.00, + "valorAcumuladoConcursoEspecial": 0.00, + "valorAcumuladoConcurso_0_5": 0.00, + "acumulado": false, + "indicadorConcursoEspecial": 1, + "dezenasSorteadasOrdemSorteio": ["086447", "026701", "028242", "003477", "066757"], + "listaResultadoEquipeEsportiva": null, + "numeroJogo": 10, + "nomeTimeCoracaoMesSorte": null, + "tipoPublicacao": 3, + "observacao": null, + "localSorteio": "ESPAÇO LOTERIAS CAIXA ", + "dataProximoConcurso": "", + "numeroConcursoAnterior": 5623, + "numeroConcursoProximo": 5625, + "valorTotalPremioFaixaUm": 0, + "numeroConcursoFinal_0_5": 0, + "listaMunicipioUFGanhadores": [ + { + "posicao": 5, + "ganhadores": 0, + "municipio": "EUNAPOLIS", + "uf": "BA", + "nomeFatansiaUL": "LOTERIA BOA SORTE ", + "serie": "A" + }, + { + "posicao": 1, + "ganhadores": 0, + "municipio": "RIO DE JANEIRO", + "uf": "RJ", + "nomeFatansiaUL": "BAZAR BERLINCK LTDA ", + "serie": "A" + }, + { + "posicao": 4, + "ganhadores": 0, + "municipio": "RODEIO", + "uf": "SC", + "nomeFatansiaUL": "LOTERICA RODEIO ", + "serie": "A" + }, + { + "posicao": 3, + "ganhadores": 0, + "municipio": "BEBEDOURO", + "uf": "SP", + "nomeFatansiaUL": "A ZEBRINHA DA SORTE LOTERIAS ", + "serie": "A" + }, + { + "posicao": 2, + "ganhadores": 0, + "municipio": "MOGI DAS CRUZES", + "uf": "SP", + "nomeFatansiaUL": "MEGASORTE LOTERIAS ", + "serie": "A" + } + ], + "listaRateioPremio": [ + { + "faixa": 1, + "numeroDeGanhadores": 1, + "valorPremio": 500000.00, + "descricaoFaixa": "1 acertos" + }, + { + "faixa": 2, + "numeroDeGanhadores": 1, + "valorPremio": 27000.00, + "descricaoFaixa": "2 acertos" + }, + { + "faixa": 3, + "numeroDeGanhadores": 1, + "valorPremio": 24000.00, + "descricaoFaixa": "3 acertos" + }, + { + "faixa": 4, + "numeroDeGanhadores": 1, + "valorPremio": 19000.00, + "descricaoFaixa": "4 acertos" + }, + { + "faixa": 5, + "numeroDeGanhadores": 1, + "valorPremio": 18329.00, + "descricaoFaixa": "5 acertos" + } + ], + "listaDezenas": ["086447", "026701", "028242", "003477", "066757"], + "listaDezenasSegundoSorteio": null, + "id": null +} diff --git a/tests/v2/loterica/test_handlers.py b/tests/v2/loterica/test_handlers.py index 72220f6..95bb282 100644 --- a/tests/v2/loterica/test_handlers.py +++ b/tests/v2/loterica/test_handlers.py @@ -3,11 +3,34 @@ from datetime import datetime from pathlib import Path +from sortepy.v2.loterica.handlers import FederalResponseHandler from sortepy.v2.loterica.handlers import LotofacilResponseHandler from sortepy.v2.loterica.handlers import QuinaResponseHandler from sortepy.v2.types import DrawPrize +def test_federal_handler(fixtures_dir: Path): + handler = FederalResponseHandler() + data = json.load(open(fixtures_dir / "response_federal_5624.json")) + + result = handler.handle(data) + assert result.draw_date == datetime(year=2021, month=12, day=22) + assert result.draw_number == "5624" + assert len(result.draw_details) == 1 + + draw = result.draw_details[0] + assert draw.ball_numbers == [86447, 26701, 28242, 3477, 66757] + assert draw.jackpot == 0.00 + assert draw.currency == "BRL" + assert draw.prize_breakdown == { + "1": DrawPrize(winners=1, prize=500_000.00), + "2": DrawPrize(winners=1, prize=27_000.00), + "3": DrawPrize(winners=1, prize=24_000.00), + "4": DrawPrize(winners=1, prize=19_000.00), + "5": DrawPrize(winners=1, prize=18_329.00), + } + + def test_lotofacil_handler(fixtures_dir: Path): handler = LotofacilResponseHandler() data = json.load(open(fixtures_dir / "response_lotofacil_2402.json")) From 19d393355e41b84e1a06317a676a9e5a82f3c463 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Fri, 24 Dec 2021 00:13:39 +0100 Subject: [PATCH 19/21] Premios da federal em ordem (usa sequencia) --- src/sortepy/v2/loterica/handlers.py | 33 +++++++++++++++++++++++++---- src/sortepy/v2/types.py | 24 +++++++++++++-------- tests/v2/loterica/test_handlers.py | 14 ++++++------ 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/sortepy/v2/loterica/handlers.py b/src/sortepy/v2/loterica/handlers.py index 174e983..e2bd2b3 100644 --- a/src/sortepy/v2/loterica/handlers.py +++ b/src/sortepy/v2/loterica/handlers.py @@ -1,17 +1,37 @@ from __future__ import annotations from datetime import datetime +from typing import Iterator from sortepy.v2.loterica._types import LotericaResponseDict +from sortepy.v2.loterica._types import RateioPremioDict from sortepy.v2.spi import DrawResponseHandler from sortepy.v2.types import DrawDetail from sortepy.v2.types import DrawPrize from sortepy.v2.types import DrawResult +from sortepy.v2.types import PrizeBreakdown class FederalResponseHandler(DrawResponseHandler[LotericaResponseDict]): def handle(self, data: LotericaResponseDict) -> DrawResult: - return _handle_response_type1(data) + federal_prizes = self._get_federal_prizes(data) + return _handle_response_type1(data, federal_prizes) + + @classmethod + def _get_federal_prizes(cls, data: LotericaResponseDict) -> PrizeBreakdown: + raw_prizes = data["listaRateioPremio"] + order_and_prizes = cls._get_order_and_prizes(raw_prizes) + + return tuple(prize for _, prize in sorted(order_and_prizes)) + + @classmethod + def _get_order_and_prizes( + cls, raw_prizes: list[RateioPremioDict] + ) -> Iterator[tuple[int, DrawPrize]]: + for p in raw_prizes: + order = int(p["descricaoFaixa"].split()[0]) + prize = DrawPrize(winners=p["numeroDeGanhadores"], prize=p["valorPremio"]) + yield (order, prize) class LotofacilResponseHandler(DrawResponseHandler[LotericaResponseDict]): @@ -24,7 +44,12 @@ def handle(self, data: LotericaResponseDict) -> DrawResult: return _handle_response_type1(data) -def _handle_response_type1(data: LotericaResponseDict) -> DrawResult: +def _handle_response_type1( + data: LotericaResponseDict, prize_breakdown: PrizeBreakdown = None +) -> DrawResult: + if prize_breakdown is None: + prize_breakdown = _get_prizes_type1(data) + return DrawResult( draw_date=datetime.strptime(data["dataApuracao"], "%d/%m/%Y"), draw_number=str(data["numero"]), @@ -33,13 +58,13 @@ def _handle_response_type1(data: LotericaResponseDict) -> DrawResult: ball_numbers=[int(n) for n in data["listaDezenas"]], jackpot=data["valorAcumuladoProximoConcurso"], currency="BRL", - prize_breakdown=_get_prizes_type1(data), + prize_breakdown=prize_breakdown, ) ], ) -def _get_prizes_type1(data: LotericaResponseDict) -> dict[str, DrawPrize]: +def _get_prizes_type1(data: LotericaResponseDict) -> PrizeBreakdown: prizes = {} for p in data["listaRateioPremio"]: key = p["descricaoFaixa"].split()[0] diff --git a/src/sortepy/v2/types.py b/src/sortepy/v2/types.py index d629469..46d86c6 100644 --- a/src/sortepy/v2/types.py +++ b/src/sortepy/v2/types.py @@ -2,8 +2,9 @@ from dataclasses import dataclass from typing import TYPE_CHECKING -from typing import Dict +from typing import Mapping from typing import Sequence +from typing import Union if TYPE_CHECKING: @@ -14,10 +15,14 @@ @dataclass(frozen=True, kw_only=True) # type: ignore[call-overload] -class DrawResult: - draw_date: datetime - draw_number: str - draw_details: Sequence[DrawDetail] +class DrawPrize: + winners: int + prize: int | float + + +_PrizesAsMapping = Mapping[str, DrawPrize] +_PrizesAsSequence = Sequence[DrawPrize] +PrizeBreakdown = Union[_PrizesAsMapping, _PrizesAsSequence] @dataclass(frozen=True, kw_only=True) # type: ignore[call-overload] @@ -25,10 +30,11 @@ class DrawDetail: ball_numbers: Sequence[int] jackpot: int | float currency: str - prize_breakdown: Dict[str, DrawPrize] + prize_breakdown: PrizeBreakdown @dataclass(frozen=True, kw_only=True) # type: ignore[call-overload] -class DrawPrize: - winners: int - prize: int | float +class DrawResult: + draw_date: datetime + draw_number: str + draw_details: Sequence[DrawDetail] diff --git a/tests/v2/loterica/test_handlers.py b/tests/v2/loterica/test_handlers.py index 95bb282..e653f45 100644 --- a/tests/v2/loterica/test_handlers.py +++ b/tests/v2/loterica/test_handlers.py @@ -22,13 +22,13 @@ def test_federal_handler(fixtures_dir: Path): assert draw.ball_numbers == [86447, 26701, 28242, 3477, 66757] assert draw.jackpot == 0.00 assert draw.currency == "BRL" - assert draw.prize_breakdown == { - "1": DrawPrize(winners=1, prize=500_000.00), - "2": DrawPrize(winners=1, prize=27_000.00), - "3": DrawPrize(winners=1, prize=24_000.00), - "4": DrawPrize(winners=1, prize=19_000.00), - "5": DrawPrize(winners=1, prize=18_329.00), - } + assert draw.prize_breakdown == ( + DrawPrize(winners=1, prize=500_000.00), + DrawPrize(winners=1, prize=27_000.00), + DrawPrize(winners=1, prize=24_000.00), + DrawPrize(winners=1, prize=19_000.00), + DrawPrize(winners=1, prize=18_329.00), + ) def test_lotofacil_handler(fixtures_dir: Path): From fe37f0d55c5c6d5c2dc4b4e91729a1e68b98a653 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Fri, 24 Dec 2021 00:32:33 +0100 Subject: [PATCH 20/21] Adiciona o bilhete nos resultados da Federal --- src/sortepy/v2/loterica/handlers.py | 28 +++++++++++----------------- src/sortepy/v2/types.py | 2 ++ tests/v2/loterica/test_handlers.py | 10 +++++----- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/sortepy/v2/loterica/handlers.py b/src/sortepy/v2/loterica/handlers.py index e2bd2b3..a35d1bd 100644 --- a/src/sortepy/v2/loterica/handlers.py +++ b/src/sortepy/v2/loterica/handlers.py @@ -4,7 +4,6 @@ from typing import Iterator from sortepy.v2.loterica._types import LotericaResponseDict -from sortepy.v2.loterica._types import RateioPremioDict from sortepy.v2.spi import DrawResponseHandler from sortepy.v2.types import DrawDetail from sortepy.v2.types import DrawPrize @@ -14,24 +13,19 @@ class FederalResponseHandler(DrawResponseHandler[LotericaResponseDict]): def handle(self, data: LotericaResponseDict) -> DrawResult: - federal_prizes = self._get_federal_prizes(data) - return _handle_response_type1(data, federal_prizes) + federal_prizes = self._iter_federal_prizes(data) + return _handle_response_type1(data, tuple(federal_prizes)) - @classmethod - def _get_federal_prizes(cls, data: LotericaResponseDict) -> PrizeBreakdown: + @staticmethod + def _iter_federal_prizes(data: LotericaResponseDict) -> Iterator[DrawPrize]: + tickets = data["listaDezenas"] raw_prizes = data["listaRateioPremio"] - order_and_prizes = cls._get_order_and_prizes(raw_prizes) - - return tuple(prize for _, prize in sorted(order_and_prizes)) - - @classmethod - def _get_order_and_prizes( - cls, raw_prizes: list[RateioPremioDict] - ) -> Iterator[tuple[int, DrawPrize]]: - for p in raw_prizes: - order = int(p["descricaoFaixa"].split()[0]) - prize = DrawPrize(winners=p["numeroDeGanhadores"], prize=p["valorPremio"]) - yield (order, prize) + for i, v in enumerate(raw_prizes): + yield DrawPrize( + winners=v["numeroDeGanhadores"], + prize=v["valorPremio"], + context={"ticket": tickets[i]}, + ) class LotofacilResponseHandler(DrawResponseHandler[LotericaResponseDict]): diff --git a/src/sortepy/v2/types.py b/src/sortepy/v2/types.py index 46d86c6..a263949 100644 --- a/src/sortepy/v2/types.py +++ b/src/sortepy/v2/types.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING +from typing import Any from typing import Mapping from typing import Sequence from typing import Union @@ -18,6 +19,7 @@ class DrawPrize: winners: int prize: int | float + context: Mapping[str, Any] | None = None _PrizesAsMapping = Mapping[str, DrawPrize] diff --git a/tests/v2/loterica/test_handlers.py b/tests/v2/loterica/test_handlers.py index e653f45..9faf32e 100644 --- a/tests/v2/loterica/test_handlers.py +++ b/tests/v2/loterica/test_handlers.py @@ -23,11 +23,11 @@ def test_federal_handler(fixtures_dir: Path): assert draw.jackpot == 0.00 assert draw.currency == "BRL" assert draw.prize_breakdown == ( - DrawPrize(winners=1, prize=500_000.00), - DrawPrize(winners=1, prize=27_000.00), - DrawPrize(winners=1, prize=24_000.00), - DrawPrize(winners=1, prize=19_000.00), - DrawPrize(winners=1, prize=18_329.00), + DrawPrize(winners=1, prize=500_000.00, context={"ticket": "086447"}), + DrawPrize(winners=1, prize=27_000.00, context={"ticket": "026701"}), + DrawPrize(winners=1, prize=24_000.00, context={"ticket": "028242"}), + DrawPrize(winners=1, prize=19_000.00, context={"ticket": "003477"}), + DrawPrize(winners=1, prize=18_329.00, context={"ticket": "066757"}), ) From c925940a18331d81775f2f6ceed8401952e50137 Mon Sep 17 00:00:00 2001 From: Wagner Macedo Date: Fri, 24 Dec 2021 00:59:55 +0100 Subject: [PATCH 21/21] Usa sequencia em todas as premiacoes --- src/sortepy/v2/loterica/handlers.py | 23 ++++++++++++----------- src/sortepy/v2/types.py | 11 +++-------- tests/v2/loterica/test_handlers.py | 26 +++++++++++++------------- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/sortepy/v2/loterica/handlers.py b/src/sortepy/v2/loterica/handlers.py index a35d1bd..e0ed0a0 100644 --- a/src/sortepy/v2/loterica/handlers.py +++ b/src/sortepy/v2/loterica/handlers.py @@ -2,13 +2,13 @@ from datetime import datetime from typing import Iterator +from typing import Sequence from sortepy.v2.loterica._types import LotericaResponseDict from sortepy.v2.spi import DrawResponseHandler from sortepy.v2.types import DrawDetail from sortepy.v2.types import DrawPrize from sortepy.v2.types import DrawResult -from sortepy.v2.types import PrizeBreakdown class FederalResponseHandler(DrawResponseHandler[LotericaResponseDict]): @@ -39,10 +39,10 @@ def handle(self, data: LotericaResponseDict) -> DrawResult: def _handle_response_type1( - data: LotericaResponseDict, prize_breakdown: PrizeBreakdown = None + data: LotericaResponseDict, prize_breakdown: Sequence[DrawPrize] = None ) -> DrawResult: if prize_breakdown is None: - prize_breakdown = _get_prizes_type1(data) + prize_breakdown = tuple(_iter_prizes_type1(data)) return DrawResult( draw_date=datetime.strptime(data["dataApuracao"], "%d/%m/%Y"), @@ -58,11 +58,12 @@ def _handle_response_type1( ) -def _get_prizes_type1(data: LotericaResponseDict) -> PrizeBreakdown: - prizes = {} - for p in data["listaRateioPremio"]: - key = p["descricaoFaixa"].split()[0] - value = DrawPrize(winners=p["numeroDeGanhadores"], prize=p["valorPremio"]) - prizes[key] = value - - return prizes +def _iter_prizes_type1(data: LotericaResponseDict) -> Iterator[DrawPrize]: + raw_prizes = data["listaRateioPremio"] + for v in raw_prizes: + hits: str = v["descricaoFaixa"].split()[0] + yield DrawPrize( + hits=int(hits), + winners=v["numeroDeGanhadores"], + prize=v["valorPremio"], + ) diff --git a/src/sortepy/v2/types.py b/src/sortepy/v2/types.py index a263949..f78ad9f 100644 --- a/src/sortepy/v2/types.py +++ b/src/sortepy/v2/types.py @@ -5,7 +5,6 @@ from typing import Any from typing import Mapping from typing import Sequence -from typing import Union if TYPE_CHECKING: @@ -19,12 +18,8 @@ class DrawPrize: winners: int prize: int | float - context: Mapping[str, Any] | None = None - - -_PrizesAsMapping = Mapping[str, DrawPrize] -_PrizesAsSequence = Sequence[DrawPrize] -PrizeBreakdown = Union[_PrizesAsMapping, _PrizesAsSequence] + hits: int = None + context: Mapping[str, Any] = None @dataclass(frozen=True, kw_only=True) # type: ignore[call-overload] @@ -32,7 +27,7 @@ class DrawDetail: ball_numbers: Sequence[int] jackpot: int | float currency: str - prize_breakdown: PrizeBreakdown + prize_breakdown: Sequence[DrawPrize] @dataclass(frozen=True, kw_only=True) # type: ignore[call-overload] diff --git a/tests/v2/loterica/test_handlers.py b/tests/v2/loterica/test_handlers.py index 9faf32e..2c1d09d 100644 --- a/tests/v2/loterica/test_handlers.py +++ b/tests/v2/loterica/test_handlers.py @@ -44,13 +44,13 @@ def test_lotofacil_handler(fixtures_dir: Path): assert draw.ball_numbers == [1, 2, 3, 4, 5, 6, 9, 10, 11, 13, 14, 18, 20, 23, 25] assert draw.jackpot == 0.00 assert draw.currency == "BRL" - assert draw.prize_breakdown == { - "15": DrawPrize(winners=3, prize=370_337.82), - "14": DrawPrize(winners=321, prize=1036.73), - "13": DrawPrize(winners=13_409, prize=25.00), - "12": DrawPrize(winners=144_941, prize=10.00), - "11": DrawPrize(winners=741_668, prize=5.00), - } + assert draw.prize_breakdown == ( + DrawPrize(hits=15, winners=3, prize=370_337.82), + DrawPrize(hits=14, winners=321, prize=1036.73), + DrawPrize(hits=13, winners=13_409, prize=25.00), + DrawPrize(hits=12, winners=144_941, prize=10.00), + DrawPrize(hits=11, winners=741_668, prize=5.00), + ) def test_quina_handler(fixtures_dir: Path): @@ -66,9 +66,9 @@ def test_quina_handler(fixtures_dir: Path): assert draw.ball_numbers == [26, 27, 29, 74, 77] assert draw.jackpot == 10_358_517.22 assert draw.currency == "BRL" - assert draw.prize_breakdown == { - "5": DrawPrize(winners=0, prize=0.00), - "4": DrawPrize(winners=61, prize=8093.88), - "3": DrawPrize(winners=6120, prize=76.83), - "2": DrawPrize(winners=151744, prize=3.09), - } + assert draw.prize_breakdown == ( + DrawPrize(hits=5, winners=0, prize=0.00), + DrawPrize(hits=4, winners=61, prize=8093.88), + DrawPrize(hits=3, winners=6120, prize=76.83), + DrawPrize(hits=2, winners=151744, prize=3.09), + )