179 lines
5.8 KiB
Python
179 lines
5.8 KiB
Python
"""
|
|
Report library — scans the on-disk archive of PDF market reports and exposes
|
|
a small summary (sources -> categories -> counts/latest) plus a per-category
|
|
file listing and inline PDF streaming.
|
|
|
|
Folder layout (one level deep):
|
|
<REPORTS_DIR>/<Source> - <Category>/<file>.pdf
|
|
e.g. "FerroAlloyNet - Iron Ore/iron ore daily report (07022025).pdf"
|
|
|
|
Dates live inside the filenames (mtime is just the copy date and is useless for
|
|
sorting), so we parse them out. Two formats are present in the wild:
|
|
DDMMYYYY -> "(07022025)" = 2025-02-07
|
|
YYYYMMDD -> "(20240523)" = 2024-05-23
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import time
|
|
from datetime import datetime
|
|
|
|
REPORTS_DIR = os.environ.get(
|
|
"REPORTS_DIR", r"C:\Users\DATIS STAR\Desktop\asianmetal_reports"
|
|
)
|
|
KNOWN_SOURCES = ["Fastmarkets", "FerroAlloyNet", "Delphica", "Platts", "Baiinfo"]
|
|
|
|
_CACHE: dict = {"ts": 0.0, "index": None}
|
|
_TTL = 600 # re-scan disk at most every 10 minutes
|
|
_DATE_RE = re.compile(r"(\d{8})")
|
|
|
|
|
|
def _split_source(folder: str):
|
|
"""'FerroAlloyNet - Iron Ore' -> ('FerroAlloyNet', 'Iron Ore'); 'Baiinfo China' -> ('Baiinfo', 'China')."""
|
|
if " - " in folder:
|
|
s, c = folder.split(" - ", 1)
|
|
return s.strip(), c.strip()
|
|
for s in KNOWN_SOURCES:
|
|
if folder.startswith(s):
|
|
rest = folder[len(s):].strip()
|
|
return s, (rest or s)
|
|
parts = folder.split(" ", 1)
|
|
return parts[0], (parts[1] if len(parts) > 1 else parts[0])
|
|
|
|
|
|
def _parse_date(name: str):
|
|
"""Best-effort report date from a filename. Returns 'YYYY-MM-DD' or None."""
|
|
m = _DATE_RE.search(name)
|
|
if not m:
|
|
return None
|
|
s = m.group(1)
|
|
candidates = [
|
|
(int(s[0:4]), int(s[4:6]), int(s[6:8])), # YYYYMMDD
|
|
(int(s[4:8]), int(s[2:4]), int(s[0:2])), # DDMMYYYY
|
|
(int(s[4:8]), int(s[0:2]), int(s[2:4])), # MMDDYYYY
|
|
]
|
|
for y, mo, d in candidates:
|
|
if 2000 <= y <= 2099 and 1 <= mo <= 12 and 1 <= d <= 31:
|
|
try:
|
|
return datetime(y, mo, d).strftime("%Y-%m-%d")
|
|
except ValueError:
|
|
continue
|
|
return None
|
|
|
|
|
|
def _kind(name: str) -> str:
|
|
n = name.lower()
|
|
if "annual" in n:
|
|
return "annual"
|
|
if "weekly" in n:
|
|
return "weekly"
|
|
if "monthly" in n:
|
|
return "monthly"
|
|
if "daily" in n:
|
|
return "daily"
|
|
return "report"
|
|
|
|
|
|
def _file_record(entry, source: str, category: str, folder: str) -> dict:
|
|
try:
|
|
size = entry.stat().st_size
|
|
except OSError:
|
|
size = 0
|
|
return {
|
|
"name": entry.name,
|
|
"date": _parse_date(entry.name),
|
|
"size": size,
|
|
"kind": _kind(entry.name),
|
|
"source": source,
|
|
"category": category,
|
|
"folder": folder,
|
|
}
|
|
|
|
|
|
def _scan() -> dict:
|
|
if not os.path.isdir(REPORTS_DIR):
|
|
return {"sources": [], "latest": [], "total": 0, "dir": REPORTS_DIR, "exists": False}
|
|
|
|
sources: dict = {}
|
|
flat: list = []
|
|
|
|
for entry in os.scandir(REPORTS_DIR):
|
|
if not entry.is_dir():
|
|
continue
|
|
source, category = _split_source(entry.name)
|
|
files = []
|
|
try:
|
|
for f in os.scandir(entry.path):
|
|
if f.is_file() and f.name.lower().endswith(".pdf"):
|
|
rec = _file_record(f, source, category, entry.name)
|
|
files.append(rec)
|
|
flat.append(rec)
|
|
except OSError:
|
|
continue
|
|
files.sort(key=lambda r: (r["date"] or "0000-00-00"), reverse=True)
|
|
sources.setdefault(source, {})[category] = {
|
|
"folder": entry.name,
|
|
"category": category,
|
|
"count": len(files),
|
|
"latest": files[0]["date"] if files else None,
|
|
"preview": files[:3],
|
|
}
|
|
|
|
src_list = []
|
|
for source, cats in sources.items():
|
|
cat_list = sorted(cats.values(), key=lambda c: c["category"])
|
|
dates = [c["latest"] for c in cat_list if c["latest"]]
|
|
src_list.append({
|
|
"source": source,
|
|
"categories": cat_list,
|
|
"count": sum(c["count"] for c in cat_list),
|
|
"category_count": len(cat_list),
|
|
"latest": max(dates) if dates else None,
|
|
})
|
|
src_list.sort(key=lambda s: s["count"], reverse=True)
|
|
flat.sort(key=lambda r: (r["date"] or "0000-00-00"), reverse=True)
|
|
|
|
return {
|
|
"sources": src_list,
|
|
"latest": flat[:30],
|
|
"total": len(flat),
|
|
"dir": REPORTS_DIR,
|
|
"exists": True,
|
|
}
|
|
|
|
|
|
def get_index(force: bool = False) -> dict:
|
|
now = time.time()
|
|
if force or _CACHE["index"] is None or now - _CACHE["ts"] > _TTL:
|
|
_CACHE["index"] = _scan()
|
|
_CACHE["ts"] = now
|
|
return _CACHE["index"]
|
|
|
|
|
|
def list_files(folder: str) -> list:
|
|
"""Full PDF listing for a single category folder (newest first)."""
|
|
base = os.path.realpath(REPORTS_DIR)
|
|
path = os.path.realpath(os.path.join(REPORTS_DIR, folder))
|
|
if path != base and not path.startswith(base + os.sep):
|
|
return []
|
|
if not os.path.isdir(path):
|
|
return []
|
|
source, category = _split_source(folder)
|
|
out = []
|
|
for f in os.scandir(path):
|
|
if f.is_file() and f.name.lower().endswith(".pdf"):
|
|
out.append(_file_record(f, source, category, folder))
|
|
out.sort(key=lambda r: (r["date"] or "0000-00-00"), reverse=True)
|
|
return out
|
|
|
|
|
|
def file_path(folder: str, name: str):
|
|
"""Resolve a safe absolute path to a report PDF, or None if out of bounds/missing."""
|
|
base = os.path.realpath(REPORTS_DIR)
|
|
p = os.path.realpath(os.path.join(REPORTS_DIR, folder, name))
|
|
if not p.startswith(base + os.sep):
|
|
return None
|
|
if not os.path.isfile(p) or not p.lower().endswith(".pdf"):
|
|
return None
|
|
return p
|