first commit
This commit is contained in:
@@ -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()
|
||||
@@ -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"]}
|
||||
@@ -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()))
|
||||
Reference in New Issue
Block a user