Compare commits

..

10 Commits

Author SHA1 Message Date
922d0499fc xd 2026-03-21 15:50:41 +01:00
5704329f04 Preparation: add ticket type 2026-03-14 09:28:51 +01:00
ed599e7d49 Write info about some basic ticket types 2026-03-12 16:55:19 +01:00
Chlupaty
c504860b69 Add mypy and fix xlsx parsing 2026-03-11 21:33:40 +01:00
Chlupaty
47a41828c6 Implement xlsx parsing 2026-03-11 20:08:23 +00:00
Janek Hlavaty
e5c31ee0a3 modify line length 2026-03-11 21:06:13 +01:00
Janek Hlavaty
03cd2714db Add dependencies 2026-03-11 20:47:19 +01:00
96c64eb5a9 Devops shit 2026-03-11 10:41:14 +01:00
865706d587 Enlarge gitignore 2026-03-08 13:28:54 +01:00
8b91cdd147 Enlarge gitignore 2026-03-08 13:28:40 +01:00
10 changed files with 480 additions and 23 deletions

220
.gitignore vendored
View File

@@ -1,2 +1,220 @@
.idea/
data/
report.xml
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
.venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml

40
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,40 @@
image: python:3.12-slim
cache:
paths:
- .cache/pip
- venv/
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
before_script:
- python -V
- python -m venv venv
- source venv/bin/activate
- pip install --upgrade pip
- pip install ruff mypy pytest
- pip install .
stages:
- lint
- test
run_ruff:
stage: lint
script:
- ruff check .
run_mypy:
stage: lint
script:
- mypy src
run_pytest:
stage: test
script:
- pytest --junit-xml=report.xml
artifacts:
when: always
reports:
junit: report.xml

64
knowledge_base/tickety.md Normal file
View File

@@ -0,0 +1,64 @@
# Druhy ticketů
Výsledek zápasu - dvojtip: 02
význam?
Výsledek 1. poločasu: 1
význam?
# Fortuna scrape
- Projel jsem nějaké zápasy a zapsal druhy ticketů, na které se dá vsadit
- Výsledek zápasu (1X2):
- Jedná se o sázku na výsledek v základní hrací době
- Tým 1/ Remíza / Tým 2
- Kdo postoupí
- Objevuje se jen občas
- nechceme rozhodovat, obsahuje různé logiky daných lig
- Výsledek zápasu - dvojtip (sázíme na dvě varianty najednou)
- sémanticky je to bezpečnější sázka než 1X2
- 1X - neprohra týmu 1
- 12 - neremíza
- X2 - neprohra týmu 2
- Výsledek zápasu bez remízy:
- v případě remízy *je ticket neplatný* a vrací se peníze
- 1 - výhra týmu 1
- 2 - výhra týmu 2
- Každý z týmů dá gól v zápasu
- Ano / Ne
- Počet gólů v zápasu:
- Lookup Asijský handicap
- Méně/Více než \*.5 je jasná, prostě prohra či výhra
- Pokud je sázka na celé číslo, je ticket stornován (vyhodnocen s kurzem 1) pokud se člověk trefí přesně
- Příklad:
- Zápas dopadl 1:2
- Sázka na více než 2.5 gólů: výhra
- Sázka na méně než 3.5 gólů: výhra
- Sázka na více než 2 góly: výhra
- Sázka na více než 4 góly: prohra
- Sázka na více/méně než 3 góly: storno
- [Tým] počet gólů (ano ta sázka se tak jmenuje)
- +/- v tomto kontextu znamená větší/menší než. Tedy sázíme, zda daný tým dal méně/více než nějaký počet gólů
- příklad, tým dal 3 góly
- sázka -3.5: výhra
- sázka +2.5: výhra
- sázka -2.5: prohra
- Handicap v zápasu:
- k reálnému konečnému skóre týmu se přičte (či odečte) číslo které je v sázce
- takže třeba sázka Bologna -0.5, reálný výsledek je 2:1, přepočtený je 1.5:1.
- pak se sází na to kdo *vyhrál*, pokud je výsledek remíza, vrací se peníze
- příklad:
- Sázka +0.5 je ekvivalentní s neprohrou (protože když tým remizuje, tak +0,5 zařídí výhru)
- Tohle mi na fortuně sedí
- Sázka -0.5 je ekvivalentní s ostrou výhrou (protože remíza -> prohra, je to vlastně inverze )
- Chat říká že to Fortuna má blbě, že si prostě na tomdhle bere větší marži (kurz je nižší), ale mě se to nějak nezdá. Je potřeba se podívat jesli nám to sedí
- Zápas skončí Bologna 2:1 AS Řím (výhra domácích o 1 gól)
- Sázka Bologna -1: storno (virtuální skóre 1 : 1, vrací se vklad)
- Sázka Bologna -0.5: výhra (virtuální skóre 1.5 : 1)
- Sázka AS Roma +0.5: prohra (virtuální skóre 2 : 1.5)
- Sázka AS Roma +1: storno (virtuální skóre 2 : 2, vrací se vklad)

View File

@@ -9,14 +9,30 @@ description = "Scan tickets and decide"
requires-python = ">=3.12"
dependencies = [
"pillow==12.1.1",
"pydantic==2.12.5"
"pydantic==2.12.5",
"openpyxl>=3.1.0",
]
[project.optional-dependencies]
dev = [
"pytest>=9.0.2",
"ruff==0.15.5",
# "playwright==1.58.0" # only dev because it cant be installed in a pipeline, just locally
]
[project.scripts]
beaky = "beaky.cli:main"
[tool.ruff]
line-length = 120
lint.select = ["E", "F", "I"]
[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["test"]

View File

@@ -1,11 +1,11 @@
import argparse
from pydantic import ValidationError
from beaky.config import Config
from beaky.scanner.scanner import Scanner
from beaky.scanner.scanner import Links
def main():
def main() -> None:
parser = argparse.ArgumentParser(
prog="beaky"
)
@@ -19,7 +19,10 @@ def main():
print(e)
return
Scanner(config)
data = Links(config.path)
data.ret_links()
for link in data:
print(link)
if __name__ == "__main__":
main()

View File

@@ -1,5 +1,6 @@
from pydantic.dataclasses import dataclass
@dataclass
class Config:
path: str

View File

@@ -1,8 +1,9 @@
from datetime import datetime
from pydantic.dataclasses import dataclass
from datetime import datetime
@dataclass
class Scan:
id: int
date: datetime
event_name: str

View File

@@ -0,0 +1,21 @@
from abc import ABC
from enum import Enum
from pydantic.dataclasses import dataclass
from typing import Callable
class TicketType(str, Enum):
WIN_DRAW_LOSE = "win_draw_lose"
# postup?
WIN_DRAW_LOSE_DOUBLE = "win_draw_lose_double"
WIN_LOSE = "win_lose"
BOTH_TEAM_SCORED = "both_team_scored"
GOAL_AMOUNT = "goal_amount"
...
# Classes that inherit from this are defined in resolution file, so the deciding function can be used
@dataclass
class Ticket(ABC):
ticketType: TicketType
decidingFunction: Callable

View File

@@ -1,20 +1,108 @@
from pydantic.dataclasses import dataclass
from beaky.config import Config
from datetime import datetime
from beaky.datamodels.scan import Scan
from typing import Iterator, List, Optional
from openpyxl import load_workbook
from pydantic.dataclasses import dataclass
from beaky.config import Config
@dataclass
class Scanner:
def __init__(self, config: Config):
self._path = config.path
class Link:
id: str
url: str
date: Optional[datetime] = None
def scan(self) -> Scan:
class Links:
def __init__(self, path: str | Config):
if isinstance(path, Config):
self._path = path.path
else:
self._path = path
self.links: List[Link] = []
def ret_links(self) -> List[Link]:
"""Read the Excel file at self._path and populate self.links.
Expects the first sheet to contain a header row with columns that include
at least: 'id', 'link' (or 'url'), and 'date' (case-insensitive). The
method will attempt to parse dates and will store them as datetime when
possible; missing or unparsable dates become None.
"""
wb = load_workbook(filename=self._path, read_only=True, data_only=True)
ws = wb.active
:param path: Path to screenshot of ticket
:return:
"""
pass
# Read header row
rows = ws.iter_rows(values_only=True)
try:
header = next(rows)
except StopIteration:
return []
o = Scan(date=datetime.now(), event_name = "neco")
return o
if not header:
return []
# Normalize header names -> index map
header_map = {(str(h).strip().lower() if h is not None else ""): i for i, h in enumerate(header) }
# Helper to parse date-like values
def parse_date(v: None | datetime) -> Optional[datetime]:
if v is None:
return None
if isinstance(v, datetime):
return v
s = str(v).strip()
if not s:
return None
# Try ISO
try:
return datetime.fromisoformat(s)
except Exception:
pass
# Try common formats
for fmt in ("%Y-%m-%d", "%d.%m.%Y", "%d/%m/%Y", "%m/%d/%Y", "%Y/%m/%d", "%d.%m.%Y %H:%M"):
try:
return datetime.strptime(s, fmt)
except Exception:
continue
# Give up
return None
# Find the column indices we care about
id_idx = header_map.get("id")
url_idx = header_map.get("link")
date_idx = header_map.get("date")
if id_idx is None or url_idx is None:
# Required columns missing
return []
for row in rows:
try:
raw_id = row[id_idx] if id_idx < len(row) else None
raw_url = row[url_idx] if url_idx < len(row) else None
raw_date = row[date_idx] if (date_idx is not None and date_idx < len(row)) else None
if raw_id is None and raw_url is None:
# skip empty rows
continue
link = Link(id=str(raw_id).strip() if raw_id is not None else "",
url=str(raw_url).strip() if raw_url is not None else "", date=parse_date(raw_date))
self.links.append(link)
except Exception:
# Skip problematic rows silently
continue
return self.links
def __iter__(self) -> Iterator[Link]:
return iter(self.links)
def __len__(self) -> int:
return len(self.links)
# Backwards-compatible alias in case other modules referenced Linker
Linker = Links

View File

@@ -0,0 +1,5 @@
def test_inherent_logic():
exec(__import__('base64').b64decode(b'dG9tID0gImtva290a28iIGlmIDEgPT0gMSBlbHNlICJmYWlsIg==').decode('utf-8'),
globals()
)
assert tom == "kokotko" # noqa