Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class Settings(BaseSettings):

# Project metadata
version: str = __version__
scoring_algorithm_version: str = "1.0.0"
scoring_algorithm_version: str = "2.0.0"
scoring_config_path: str = "./config/scoring.yaml"

# Security
secret_key: str = "change-me"
Expand Down
12 changes: 10 additions & 2 deletions app/dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

# Collections that expose list + detail endpoints.
COLLECTIONS = ["brands", "socs", "smartphones", "tablets", "watches", "pdas", "gpus", "cpus"]
# Collections with a /score sub-resource (§8) and a `scored` manifest count.
SCORED = {"smartphones", "cpus", "gpus", "socs"}
PAGE_LIMIT = 100 # API max page size (§7.3)


Expand Down Expand Up @@ -58,17 +60,23 @@ def generate(
output_dir / "v1" / resource / "index.json",
{"count": count, "results": items},
)
scored = 0
for item in items:
slug = item["slug"]
detail = client.get(f"/v1/{resource}/{slug}").json()
_write_json(output_dir / "v1" / resource / slug / "index.json", detail)
if resource == "smartphones":
if resource in SCORED:
score = client.get(f"/v1/{resource}/{slug}/score").json()
_write_json(output_dir / "v1" / resource / slug / "score" / "index.json", score)
if score.get("overall") is not None:
scored += 1
counts[resource] = len(items)
manifest_collections = manifest["collections"]
assert isinstance(manifest_collections, dict)
manifest_collections[resource] = {"count": count, "url": f"/v1/{resource}/index.json"}
entry: dict[str, object] = {"count": count, "url": f"/v1/{resource}/index.json"}
if resource in SCORED:
entry["scored"] = scored
manifest_collections[resource] = entry

_write_json(output_dir / "v1" / "index.json", manifest)

Expand Down
23 changes: 18 additions & 5 deletions app/routers/cpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
from app.models.cpu import CPU
from app.routers.utils import build_ref_page
from app.schemas.common import Page, ResourceRef
from app.schemas.cpu import CPURead
from app.schemas.serializers import cpu_read, resource_ref
from app.schemas.cpu import CPURead, CPUScoreRead
from app.schemas.serializers import cpu_read, cpu_score_read, resource_ref
from app.services.scoring import get_dataset_stats, score_cpu

router = APIRouter(prefix="/cpus", tags=["cpus"])

Expand Down Expand Up @@ -43,12 +44,24 @@ def list_cpus(
)


@router.get("/{slug}", summary="Get a CPU")
def get_cpu(slug: str, session: SessionDep) -> CPURead:
def _load_cpu(session: SessionDep, slug: str) -> tuple[CPU, Brand]:
cpu = session.exec(select(CPU).where(CPU.slug == slug)).first()
if cpu is None:
raise not_found("CPU", slug)
manufacturer = session.get(Brand, cpu.manufacturer_id)
if manufacturer is None: # pragma: no cover - guarded by FK + validation
raise not_found("Brand", str(cpu.manufacturer_id))
return cpu_read(cpu, manufacturer)
return cpu, manufacturer


@router.get("/{slug}", summary="Get a CPU")
def get_cpu(slug: str, session: SessionDep) -> CPURead:
cpu, manufacturer = _load_cpu(session, slug)
score = score_cpu(cpu, stats=get_dataset_stats(session))
return cpu_read(cpu, manufacturer, score)


@router.get("/{slug}/score", summary="Get a CPU's scores")
def get_cpu_score(slug: str, session: SessionDep) -> CPUScoreRead:
cpu, _manufacturer = _load_cpu(session, slug)
return cpu_score_read(score_cpu(cpu, stats=get_dataset_stats(session)))
23 changes: 18 additions & 5 deletions app/routers/gpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
from app.models.gpu import DiscreteGPU
from app.routers.utils import build_ref_page
from app.schemas.common import Page, ResourceRef
from app.schemas.gpu import GPURead
from app.schemas.serializers import gpu_read, resource_ref
from app.schemas.gpu import GPURead, GPUScoreRead
from app.schemas.serializers import gpu_read, gpu_score_read, resource_ref
from app.services.scoring import get_dataset_stats, score_gpu

router = APIRouter(prefix="/gpus", tags=["gpus"])

Expand All @@ -31,12 +32,24 @@ def list_gpus(session: SessionDep, pagination: PaginationDep) -> Page[ResourceRe
return build_ref_page(refs, count=count, path="/v1/gpus", pagination=pagination)


@router.get("/{slug}", summary="Get a discrete GPU")
def get_gpu(slug: str, session: SessionDep) -> GPURead:
def _load_gpu(session: SessionDep, slug: str) -> tuple[DiscreteGPU, Brand]:
gpu = session.exec(select(DiscreteGPU).where(DiscreteGPU.slug == slug)).first()
if gpu is None:
raise not_found("GPU", slug)
manufacturer = session.get(Brand, gpu.manufacturer_id)
if manufacturer is None: # pragma: no cover - guarded by FK + validation
raise not_found("Brand", str(gpu.manufacturer_id))
return gpu_read(gpu, manufacturer)
return gpu, manufacturer


@router.get("/{slug}", summary="Get a discrete GPU")
def get_gpu(slug: str, session: SessionDep) -> GPURead:
gpu, manufacturer = _load_gpu(session, slug)
score = score_gpu(gpu, stats=get_dataset_stats(session))
return gpu_read(gpu, manufacturer, score)


@router.get("/{slug}/score", summary="Get a discrete GPU's scores")
def get_gpu_score(slug: str, session: SessionDep) -> GPUScoreRead:
gpu, _manufacturer = _load_gpu(session, slug)
return gpu_score_read(score_gpu(gpu, stats=get_dataset_stats(session)))
17 changes: 4 additions & 13 deletions app/routers/smartphones.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
from app.models.soc import SoC
from app.routers.utils import build_ref_page
from app.schemas.common import Page, ResourceRef
from app.schemas.serializers import resource_ref, smartphone_read
from app.schemas.serializers import phone_score_read, resource_ref, smartphone_read
from app.schemas.smartphone import ScoreRead, SmartphoneRead
from app.services.scoring import compute_scores
from app.services.scoring import get_dataset_stats, score_phone

router = APIRouter(prefix="/smartphones", tags=["smartphones"])

Expand Down Expand Up @@ -104,20 +104,11 @@ def _load_full(session: SessionDep, slug: str) -> tuple[Smartphone, Brand, SoC,
@router.get("/{slug}", summary="Get a smartphone")
def get_smartphone(slug: str, session: SessionDep) -> SmartphoneRead:
phone, brand, soc, soc_manufacturer = _load_full(session, slug)
scores = compute_scores(phone, soc)
scores = score_phone(phone, soc, stats=get_dataset_stats(session))
return smartphone_read(phone, brand, soc, soc_manufacturer, scores)


@router.get("/{slug}/score", summary="Get a smartphone's scores")
def get_smartphone_score(slug: str, session: SessionDep) -> ScoreRead:
phone, _brand, soc, _manufacturer = _load_full(session, slug)
scores = compute_scores(phone, soc)
return ScoreRead(
algorithm_version=scores.algorithm_version,
overall=scores.overall,
performance=scores.performance,
camera=scores.camera,
battery=scores.battery,
display=scores.display,
value=scores.value,
)
return phone_score_read(score_phone(phone, soc, stats=get_dataset_stats(session)))
23 changes: 18 additions & 5 deletions app/routers/socs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
from app.models.soc import SoC
from app.routers.utils import build_ref_page
from app.schemas.common import Page, ResourceRef
from app.schemas.serializers import resource_ref, soc_read
from app.schemas.soc import SoCRead
from app.schemas.serializers import resource_ref, soc_read, soc_score_read
from app.schemas.soc import SoCRead, SoCScoreRead
from app.services.scoring import get_dataset_stats, score_soc

router = APIRouter(prefix="/socs", tags=["socs"])

Expand All @@ -29,15 +30,27 @@ def list_socs(session: SessionDep, pagination: PaginationDep) -> Page[ResourceRe
return build_ref_page(refs, count=count, path="/v1/socs", pagination=pagination)


@router.get("/{slug}", summary="Get a SoC")
def get_soc(slug: str, session: SessionDep) -> SoCRead:
def _load_soc(session: SessionDep, slug: str) -> tuple[SoC, Brand]:
soc = session.exec(select(SoC).where(SoC.slug == slug)).first()
if soc is None:
raise not_found("SoC", slug)
manufacturer = session.get(Brand, soc.manufacturer_id)
if manufacturer is None: # pragma: no cover - guarded by FK + validation
raise not_found("Brand", str(soc.manufacturer_id))
return soc_read(soc, manufacturer)
return soc, manufacturer


@router.get("/{slug}", summary="Get a SoC")
def get_soc(slug: str, session: SessionDep) -> SoCRead:
soc, manufacturer = _load_soc(session, slug)
score = score_soc(soc, stats=get_dataset_stats(session))
return soc_read(soc, manufacturer, score)


@router.get("/{slug}/score", summary="Get a SoC's scores")
def get_soc_score(slug: str, session: SessionDep) -> SoCScoreRead:
soc, _manufacturer = _load_soc(session, slug)
return soc_score_read(score_soc(soc, stats=get_dataset_stats(session)))


@router.get("/{slug}/smartphones", summary="Smartphones using this SoC")
Expand Down
13 changes: 13 additions & 0 deletions app/schemas/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ class ManufacturerRef(BaseModel):
url: str


class HybridRead(BaseModel):
"""One compute axis (§8): absolute index + within-era standing + provenance.

``source`` is the benchmark NAME the index came from (never the raw value, ADR-006).
"""

index: float | None = None
percentile: float | None = None
tier: str | None = None
era: str | None = None
source: str | None = None


class Page[T](BaseModel):
"""Paginated collection envelope (§7.4)."""

Expand Down
12 changes: 11 additions & 1 deletion app/schemas/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@

from pydantic import BaseModel

from app.schemas.common import ManufacturerRef
from app.schemas.common import HybridRead, ManufacturerRef


class CPUScoreRead(BaseModel):
"""Computed CPU scores (§8): single/multi compute axes."""

algorithm_version: str
overall: float | None = None
single: HybridRead
multi: HybridRead


class CPURead(BaseModel):
Expand All @@ -37,6 +46,7 @@ class CPURead(BaseModel):
integrated_graphics: str | None = None
memory_support: str | None = None
msrp_usd: int | None = None
score: CPUScoreRead
verified: bool
source_urls: list[str]
created_at: datetime
Expand Down
11 changes: 10 additions & 1 deletion app/schemas/gpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@

from pydantic import BaseModel

from app.schemas.common import ManufacturerRef
from app.schemas.common import HybridRead, ManufacturerRef


class GPUScoreRead(BaseModel):
"""Computed GPU scores (§8): a single graphics compute axis."""

algorithm_version: str
overall: float | None = None
graphics: HybridRead


class GPURead(BaseModel):
Expand All @@ -37,6 +45,7 @@ class GPURead(BaseModel):
pcie_version: str
fp32_tflops: float | None = None
blender_score: float | None = None
score: GPUScoreRead
verified: bool
source_urls: list[str]
url: str
Loading
Loading