Current status - seems to be working well enough

This commit is contained in:
Kumi 2022-10-24 14:19:17 +00:00
commit 2ddefe1154
Signed by: kumi
GPG key ID: ECBCC9082395383F
5 changed files with 204 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
venv/
*.pyc
__pycache__/
settings.ini

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
Copyright (c) 2022, Kumi Mitterer <git@kumi.email>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

169
allkeyshop.py Normal file
View file

@ -0,0 +1,169 @@
from prometheus_client import start_http_server, Gauge
from configparser import ConfigParser
from pathlib import Path
from urllib.request import urlopen, Request
from html.parser import HTMLParser
from typing import List, Tuple, Optional
import re
import json
import time
class AllKeyShop:
PLATFORM_PC = "cd-key"
PLATFORM_PS5 = "ps5"
PLATFORM_PS4 = "ps4"
PLATFORM_XB1 = "xbox-one"
PLATFORM_XBSX = "xbox-series-x"
PLATFORM_SWITCH = "nintendo-switch"
class ProductParser(HTMLParser):
def __init__(self):
super().__init__()
self.reset()
self.result: int
def handle_starttag(self, tag: str, attrs: List[Tuple[str, str]]):
for attr in attrs:
if attr[0] == "data-product-id":
try:
self.result = int(attr[1])
except:
pass
class HTTPRequest(Request):
def __init__(self, url: str, *args, **kwargs):
super().__init__(url, *args, **kwargs)
self.headers["user-agent"] = "allkeyshop.com prometheus exporter (https://kumig.it/kumitterer/prometheus-allkeyshop)"
class ProductPageRequest(HTTPRequest):
@staticmethod
def to_slug(string: str) -> str:
# Shamelessly stolen from https://www.30secondsofcode.org/python/s/slugify
# Website, name & logo © 2017-2022 30 seconds of code (https://github.com/30-seconds)
# Individual snippets licensed under CC-BY-4.0 (https://creativecommons.org/licenses/by/4.0/)
string = string.lower().strip()
string = re.sub(r'[^\w\s-]', '', string)
string = re.sub(r'[\s_-]+', '-', string)
string = re.sub(r'^-+|-+$', '', string)
return string
def __init__(self, product: str, platform: str, *args, **kwargs):
slug = self.__class__.to_slug(product)
if platform == "pc":
platform = AllKeyShop.PLATFORM_PC
url = f"https://www.allkeyshop.com/blog/buy-{slug}-{platform}-compare-prices/"
super().__init__(url, *args, **kwargs)
class OffersRequest(HTTPRequest):
def __init__(self, product: int, *args, currency: str, **kwargs):
region: str = kwargs.pop("region", "")
edition: str = kwargs.pop("edition", "")
moreq: str = kwargs.pop("moreq", "")
url = f"https://www.allkeyshop.com/blog/wp-admin/admin-ajax.php?action=get_offers&product={product}&currency={currency}&region={region}&edition={edition}&moreq={moreq}&use_beta_offers_display=1"
super().__init__(url, *args, **kwargs)
def __init__(self, product: int | str, platform: Optional[str] = None, **kwargs):
self.product: int
self.kwargs: dict = kwargs
if isinstance(product, int):
self.product = product
else:
assert platform
self.product = self.__class__.resolve_product(product, platform)
@classmethod
def resolve_product(cls, product: str, platform: str) -> int:
content = urlopen(cls.ProductPageRequest(product, platform))
html = content.read().decode()
parser = cls.ProductParser()
parser.feed(html)
assert parser.result
return parser.result
def get_offers(self) -> dict:
content = urlopen(self.__class__.OffersRequest(
self.product, **self.kwargs))
raw = content.read()
content = json.loads(raw)
assert content["success"]
return content["offers"]
config = ConfigParser()
config.read(Path(__file__).parent / "settings.ini")
gauges: List[Tuple[Gauge, AllKeyShop]] = list()
if config.has_section("DEFAULT"):
defaults = config["DEFAULT"]
else:
defaults = dict()
for section, settings in filter(lambda x: x[0] != "DEFAULT", config.items()):
try:
currency: int
assert (currency := settings.get(
"Currency", fallback=defaults.get("Currency")).lower())
region: str = settings.get(
"Region", fallback=defaults.get("Region", ""))
edition: str = settings.get("Edition", fallback="")
product: str | int
platform: Optional[str]
name: str
try:
product = int(section)
name = settings.get("Name", fallback=section)
aks: AllKeyShop = AllKeyShop(
product, currency=currency, region=region, edition=edition)
except ValueError:
product = name = section
platform = settings.get(
"Platform", fallback=defaults.get("Platform"))
assert platform
aks: AllKeyShop = AllKeyShop(
product, platform, currency=currency, region=region, edition=edition)
gauge = Gauge(
f"allkeyshop_{AllKeyShop.ProductPageRequest.to_slug(name).replace('-', '_')}_{currency}", "Best price for {name}")
gauges.append((gauge, aks))
except Exception as e:
print(f"Error setting up gauge for section {section}: {e}")
start_http_server(8090)
while True:
for gauge, aks in gauges:
offers: List[dict] = aks.get_offers()
available_offers: List[dict] = filter(
lambda x: x["stock"] == "InStock", offers)
store: Optional[str]
if (store := settings.get("Store", fallback=defaults.get("Store", ""))):
available_offers: List[dict] = filter(
lambda x: x["platform"] == store, available_offers)
best_offer: dict = min(
available_offers, key=lambda x: x["price"][currency]["price"])
gauge.set(best_offer["price"][currency]["price"])
time.sleep(60)

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
prometheus-client

9
settings.dist.ini Normal file
View file

@ -0,0 +1,9 @@
[DEFAULT]
Currency = eur
Platform = pc
Store = steam
[Persona 5 Royal]
[10539]
Name = Cyberpunk 2077