first commit

This commit is contained in:
Mateusz Gruszczyński
2026-07-13 13:19:26 +02:00
commit bd562b3da6
50 changed files with 2221 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
import requests
from flask import current_app
def get_json(url, params=None, timeout_key="HTTP_TIMEOUT_SECONDS"):
timeout = float(current_app.config.get(timeout_key, current_app.config["HTTP_TIMEOUT_SECONDS"]))
response = requests.get(url, params=params, timeout=timeout)
response.raise_for_status()
return response.json()
+70
View File
@@ -0,0 +1,70 @@
from datetime import date, timedelta
from decimal import Decimal
from flask import current_app
from .http import get_json
PRODUCT_IDS = {"PB95": 41, "PB98": 42, "DIESEL": 43}
POLISH_REGIONS = (
"dolnośląskie", "kujawsko-pomorskie", "lubelskie", "lubuskie", "łódzkie",
"małopolskie", "mazowieckie", "opolskie", "podkarpackie", "podlaskie",
"pomorskie", "śląskie", "świętokrzyskie", "warmińsko-mazurskie",
"wielkopolskie", "zachodnio-pomorskie",
)
def _rows(url, params=None):
data = get_json(url, params=params, timeout_key="ORLEN_TIMEOUT_SECONDS")
if not isinstance(data, list):
raise ValueError("Nieprawidłowa odpowiedź API Orlen")
return data
def fetch_orlen_range(fuel_type, date_from, date_to, region="mazowieckie"):
fuel_type = fuel_type.upper()
if fuel_type == "LPG":
result, seen = [], set()
current = date_from
while current <= date_to:
rows = _rows(current_app.config["ORLEN_LPG_BY_DATE_URL"], {"date": current.isoformat()})
for row in rows:
row_region = (row.get("locationName") or "").strip().lower()
if region and str(region).lower() not in ("all", "*") and row_region != str(region).lower():
continue
effective = date.fromisoformat(row["effectiveDate"][:10])
if not (date_from <= effective <= date_to):
continue
key = (effective, row_region)
if key in seen:
continue
seen.add(key)
value = Decimal(str(row["value"]))
result.append({"fuel_type": "LPG", "effective_date": effective, "price_per_liter": value,
"raw_value": value, "region": row_region, "product_name": row.get("productName") or "LPG",
"source": f"Orlen LPG ByDate, {row_region}"})
current += timedelta(days=1)
return result
product_id = PRODUCT_IDS.get(fuel_type)
if not product_id:
raise ValueError("Nieobsługiwany rodzaj paliwa")
rows = _rows(current_app.config["ORLEN_WHOLESALE_URL"], {
"productId": product_id, "from": date_from.isoformat(), "to": date_to.isoformat()
})
result = []
for row in rows:
effective = date.fromisoformat(row["effectiveDate"][:10])
raw = Decimal(str(row["value"]))
result.append({"fuel_type": fuel_type, "effective_date": effective, "price_per_liter": raw / Decimal("1000"),
"raw_value": raw, "region": "", "product_name": row.get("productName") or fuel_type,
"source": f"Orlen {row.get('productName') or fuel_type}, netto/m³ → zł/l"})
return result
def fetch_orlen_price(fuel_type, day=None, region="mazowieckie"):
day = day or date.today()
rows = fetch_orlen_range(fuel_type, day.replace(day=1), day, region)
candidates = [row for row in rows if row["effective_date"] <= day]
if not candidates:
raise ValueError("Brak ceny Orlen dla wybranej daty")
row = max(candidates, key=lambda item: item["effective_date"])
return {"price_per_liter": float(row["price_per_liter"]), "date": row["effective_date"].isoformat(), "source": row["source"]}
+112
View File
@@ -0,0 +1,112 @@
import hashlib
import re
from flask import current_app
from .http import get_json
LEGAL_SUFFIXES = re.compile(r"\b(SPÓŁKA|SPÓŁKA AKCYJNA|SP\. ?Z ?O\. ?O\.|S\. ?A\.|ODDZIAŁ|SPÓŁKA EUROPEJSKA)\b.*$", re.I)
def fetch_ure_stations():
payload = get_json(current_app.config["URE_STATIONS_URL"], timeout_key="URE_TIMEOUT_SECONDS")
if isinstance(payload, list):
return payload
if isinstance(payload, dict):
for key in ("items", "results", "data", "value"):
if isinstance(payload.get(key), list):
return payload[key]
raise ValueError("Nieprawidłowa odpowiedź API URE")
def _clean(value):
return " ".join(str(value or "").strip().split())
def infer_brand(company_name, station_name=""):
combined = f"{_clean(company_name)} {_clean(station_name)}".upper()
aliases = current_app.config.get("STATION_BRAND_ALIASES", {})
# Najpierw szukamy marki w nazwie konkretnej stacji, później w przedsiębiorcy.
for token, label in sorted(aliases.items(), key=lambda item: len(item[0]), reverse=True):
if token.upper() in combined:
return label
normalized = _clean(company_name)
shortened = LEGAL_SUFFIXES.sub("", normalized).strip(" ,-\"")
return shortened or normalized or "Nieznana sieć"
def _address_parts(row):
return [
_clean(row.get("infraUlica")),
_clean(row.get("infraNrLokalu")),
_clean(row.get("infraKod")) or _clean(row.get("kod")),
_clean(row.get("infraPoczta")) or _clean(row.get("miejscowosc")) or _clean(row.get("poczta")),
_clean(row.get("wojewodztwo")).lower(),
]
def _point_key(row, index):
address = "|".join(x.casefold() for x in _address_parts(row) if x)
station_name = _clean(row.get("nazwaStacji")).casefold()
raw = address or f"{station_name}|{index}"
digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16]
dkn = _clean(row.get("dkn"))
storage_key = f"{dkn or 'no-dkn'}:{digest}"
return raw, storage_key
def _merge_point(target, source):
for field in ("station_name", "street", "street_number", "postal_code", "city", "region", "coordinates"):
if not target.get(field) and source.get(field):
target[field] = source[field]
for field in ("has_petrol", "has_diesel", "has_lpg"):
target[field] = bool(target.get(field) or source.get(field))
def aggregate_ure_companies(rows):
grouped = {}
aliases = {label.casefold() for label in current_app.config.get("STATION_BRAND_ALIASES", {}).values()}
for index, row in enumerate(rows, start=1):
nip, regon = _clean(row.get("nip")), _clean(row.get("regon"))
company_name = _clean(row.get("nazwa")) or "Nieznana firma"
station_name = _clean(row.get("nazwaStacji"))
brand = infer_brand(company_name, station_name)
# Rozpoznane sieci agregujemy jako jedną markę, niezależnie od operatora/franczyzobiorcy.
if brand.casefold() in aliases:
key = f"brand:{brand.casefold()}"
else:
key = f"nip:{nip}" if nip else (f"regon:{regon}" if regon else f"name:{company_name.casefold()}")
item = grouped.setdefault(key, {
"ure_key": key,
"brand_name": brand,
"company_name": brand if key.startswith("brand:") else company_name,
"nip": None if key.startswith("brand:") else (nip or None),
"regon": None if key.startswith("brand:") else (regon or None),
"station_names": set(), "regions": set(),
"has_petrol": False, "has_diesel": False, "has_lpg": False,
"points": {},
})
if station_name: item["station_names"].add(station_name)
region = _clean(row.get("wojewodztwo")).lower()
if region: item["regions"].add(region)
item["has_petrol"] |= bool(row.get("benzynySilnikowe"))
item["has_diesel"] |= bool(row.get("olejeNapedowe"))
item["has_lpg"] |= bool(row.get("gazPlynnyLPG"))
point_key, stored_dkn = _point_key(row, index)
point = {
"ure_dkn": stored_dkn,
"station_name": station_name or brand,
"street": _clean(row.get("infraUlica")), "street_number": _clean(row.get("infraNrLokalu")),
"postal_code": _clean(row.get("infraKod")) or _clean(row.get("kod")),
"city": _clean(row.get("infraPoczta")) or _clean(row.get("miejscowosc")) or _clean(row.get("poczta")),
"region": region, "coordinates": _clean(row.get("wspolrzedne")),
"has_petrol": bool(row.get("benzynySilnikowe")), "has_diesel": bool(row.get("olejeNapedowe")), "has_lpg": bool(row.get("gazPlynnyLPG")),
}
existing = item["points"].get(point_key)
_merge_point(existing, point) if existing else item["points"].__setitem__(point_key, point)
result = []
for item in grouped.values():
item["points"] = list(item["points"].values())
item["station_count"] = len(item["points"])
item["station_names"] = "|".join(sorted(item["station_names"], key=str.casefold))
item["regions"] = "|".join(sorted(item["regions"], key=str.casefold))
result.append(item)
return sorted(result, key=lambda x: (x["brand_name"].casefold(), x["company_name"].casefold()))