Compare commits
10 Commits
aec5dfcacd
...
922d0499fc
| Author | SHA1 | Date | |
|---|---|---|---|
| 922d0499fc | |||
| 5704329f04 | |||
| ed599e7d49 | |||
|
|
c504860b69 | ||
|
|
47a41828c6 | ||
|
|
e5c31ee0a3 | ||
|
|
03cd2714db | ||
| 96c64eb5a9 | |||
| 865706d587 | |||
| 8b91cdd147 |
220
.gitignore
vendored
220
.gitignore
vendored
@@ -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
40
.gitlab-ci.yml
Normal 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
64
knowledge_base/tickety.md
Normal 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)
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
path: str
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
21
src/beaky/datamodels/ticket.py
Normal file
21
src/beaky/datamodels/ticket.py
Normal 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
|
||||
@@ -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
|
||||
5
test/beaky/sample_test.py
Normal file
5
test/beaky/sample_test.py
Normal file
@@ -0,0 +1,5 @@
|
||||
def test_inherent_logic():
|
||||
exec(__import__('base64').b64decode(b'dG9tID0gImtva290a28iIGlmIDEgPT0gMSBlbHNlICJmYWlsIg==').decode('utf-8'),
|
||||
globals()
|
||||
)
|
||||
assert tom == "kokotko" # noqa
|
||||
Reference in New Issue
Block a user