diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..ac17062 Binary files /dev/null and b/.coverage differ diff --git a/README.md b/README.md deleted file mode 100644 index 4fa56d4..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# Dev-ACME \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1316de8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "acmecli" +version = "0.0.1" +requires-python = ">=3.10" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/python b/python new file mode 100644 index 0000000..e69de29 diff --git a/run b/run new file mode 100644 index 0000000..6cacf51 --- /dev/null +++ b/run @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +import sys, subprocess, re, os + +def do_install(): + subprocess.check_call([sys.executable, "-m", "pip", "install", "-e", "."]) + return 0 + +def do_test(): + import re, subprocess, sys, os, pathlib + + # Always run from repo root + os.chdir(pathlib.Path(__file__).parent.resolve()) + + cmd = [sys.executable, "-m", "pytest", "tests", + "--maxfail=1", "--disable-warnings", + "--cov=acmecli", "--cov-report=term-missing"] + r = subprocess.run(cmd, text=True, capture_output=True) + out = (r.stdout or "") + (r.stderr or "") + + # Extract counts + collected = re.search(r"collected\s+(\d+)", out) + passed = re.search(r"(\d+)\s+passed", out) + cov = re.search(r"TOTAL\s+.*?(\d+)%", out) + + x = int(passed.group(1)) if passed else 0 + y = int(collected.group(1)) if collected else 0 + z = int(cov.group(1)) if cov else 0 + + print(f"{x}/{y} test cases passed. {z}% line coverage achieved.") + return 0 if r.returncode == 0 else 1 + + +def main(): + if len(sys.argv) < 2: + print("Usage: run install|test|score ") + sys.exit(1) + cmd = sys.argv[1] + if cmd == "install": sys.exit(do_install()) + if cmd == "test": sys.exit(do_test()) + if cmd == "score": + if len(sys.argv) < 3: + print("Usage: run score ") + sys.exit(1) + sys.exit(do_score(sys.argv[2])) + print("Unknown command.") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/run.py b/run.py new file mode 100644 index 0000000..3dc2192 --- /dev/null +++ b/run.py @@ -0,0 +1 @@ +import run diff --git a/src/acmecli.egg-info/PKG-INFO b/src/acmecli.egg-info/PKG-INFO new file mode 100644 index 0000000..f2ba886 --- /dev/null +++ b/src/acmecli.egg-info/PKG-INFO @@ -0,0 +1,4 @@ +Metadata-Version: 2.4 +Name: acmecli +Version: 0.0.1 +Requires-Python: >=3.10 diff --git a/src/acmecli.egg-info/SOURCES.txt b/src/acmecli.egg-info/SOURCES.txt new file mode 100644 index 0000000..7ab9ca9 --- /dev/null +++ b/src/acmecli.egg-info/SOURCES.txt @@ -0,0 +1,15 @@ +pyproject.toml +src/acmecli/__init__.py +src/acmecli/cli.py +src/acmecli/reporter.py +src/acmecli/scoring.py +src/acmecli/types.py +src/acmecli.egg-info/PKG-INFO +src/acmecli.egg-info/SOURCES.txt +src/acmecli.egg-info/dependency_links.txt +src/acmecli.egg-info/top_level.txt +src/acmecli/metrics/__init__.py +src/acmecli/metrics/base.py +src/acmecli/metrics/license_metric.py +tests/test_metrics_contract.py +tests/test_reporter_schema.py \ No newline at end of file diff --git a/src/acmecli.egg-info/dependency_links.txt b/src/acmecli.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/acmecli.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/acmecli.egg-info/top_level.txt b/src/acmecli.egg-info/top_level.txt new file mode 100644 index 0000000..b96d3b9 --- /dev/null +++ b/src/acmecli.egg-info/top_level.txt @@ -0,0 +1 @@ +acmecli diff --git a/src/acmecli/__init__.py b/src/acmecli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/acmecli/__pycache__/__init__.cpython-313.pyc b/src/acmecli/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..bc5983a Binary files /dev/null and b/src/acmecli/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/acmecli/__pycache__/types.cpython-313.pyc b/src/acmecli/__pycache__/types.cpython-313.pyc new file mode 100644 index 0000000..9e236bb Binary files /dev/null and b/src/acmecli/__pycache__/types.cpython-313.pyc differ diff --git a/src/acmecli/cli.py b/src/acmecli/cli.py new file mode 100644 index 0000000..6c5d6d3 --- /dev/null +++ b/src/acmecli/cli.py @@ -0,0 +1,42 @@ +import sys +from pathlib import Path +from .types import ReportRow +from .reporter import write_ndjson + +def _classify(url: str) -> str: + u = url.strip().lower() + if "huggingface.co/datasets/" in u: + return "DATASET" + if "github.com/" in u: + return "CODE" + if "huggingface.co/" in u: + return "MODEL" + return "CODE" + +def _stub_row(name: str) -> ReportRow: + zero = 0.0 + return ReportRow( + name=name, category="MODEL", + net_score=zero, net_score_latency=0, + ramp_up_time=zero, ramp_up_time_latency=0, + bus_factor=zero, bus_factor_latency=0, + performance_claims=zero, performance_claims_latency=0, + license=zero, license_latency=0, + size_score={"raspberry_pi":0.0, "jetson_nano":0.0, "desktop_pc":0.0, "aws_server":0.0}, + size_score_latency=0, + dataset_and_code_score=zero, dataset_and_code_score_latency=0, + dataset_quality=zero, dataset_quality_latency=0, + code_quality=zero, code_quality_latency=0 + ) + +def main(argv: list[str]) -> int: + # argv pattern: ["score", "/abs/path/URL_FILE"] + _, url_file = argv + lines = Path(url_file).read_text(encoding="utf-8").splitlines() + for raw in lines: + url = raw.strip() + if not url: + continue + if _classify(url) == "MODEL": + write_ndjson(_stub_row(url)) + return 0 diff --git a/src/acmecli/metrics/__init__.py b/src/acmecli/metrics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/acmecli/metrics/__pycache__/__init__.cpython-313.pyc b/src/acmecli/metrics/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..63ad76f Binary files /dev/null and b/src/acmecli/metrics/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/acmecli/metrics/__pycache__/base.cpython-313.pyc b/src/acmecli/metrics/__pycache__/base.cpython-313.pyc new file mode 100644 index 0000000..2574346 Binary files /dev/null and b/src/acmecli/metrics/__pycache__/base.cpython-313.pyc differ diff --git a/src/acmecli/metrics/__pycache__/license_metric.cpython-313.pyc b/src/acmecli/metrics/__pycache__/license_metric.cpython-313.pyc new file mode 100644 index 0000000..c89cfa2 Binary files /dev/null and b/src/acmecli/metrics/__pycache__/license_metric.cpython-313.pyc differ diff --git a/src/acmecli/metrics/base.py b/src/acmecli/metrics/base.py new file mode 100644 index 0000000..1745002 --- /dev/null +++ b/src/acmecli/metrics/base.py @@ -0,0 +1,7 @@ +from typing import List +from ..types import Metric, MetricValue, TargetSpec, SourceHandler, Cache + +REGISTRY: List[Metric] = [] + +def register(metric: Metric) -> None: + REGISTRY.append(metric) diff --git a/src/acmecli/metrics/license_metric.py b/src/acmecli/metrics/license_metric.py new file mode 100644 index 0000000..838708a --- /dev/null +++ b/src/acmecli/metrics/license_metric.py @@ -0,0 +1,18 @@ +import time +from ..types import Metric, Signals, TargetSpec, SourceHandler, Cache, MetricValue +from .base import register + +class LicenseMetric: + name = "license" + + def collect(self, spec: TargetSpec, handler: SourceHandler, cache: Cache) -> Signals: + # Week 1 stub: no network; just return empty signals + return {} + + def score(self, signals: Signals) -> MetricValue: + t0 = time.perf_counter() + value = 0.0 # TODO: map license presence/compatibility → [0,1] + latency_ms = int((time.perf_counter() - t0) * 1000) + return MetricValue(self.name, value, latency_ms) + +register(LicenseMetric()) diff --git a/src/acmecli/reporter.py b/src/acmecli/reporter.py new file mode 100644 index 0000000..a881795 --- /dev/null +++ b/src/acmecli/reporter.py @@ -0,0 +1,6 @@ +import json +from dataclasses import asdict +from .types import ReportRow + +def write_ndjson(row: ReportRow) -> None: + print(json.dumps(asdict(row), ensure_ascii=False)) diff --git a/src/acmecli/scoring.py b/src/acmecli/scoring.py new file mode 100644 index 0000000..9fcc384 --- /dev/null +++ b/src/acmecli/scoring.py @@ -0,0 +1,26 @@ +from typing import Dict, List +from .types import MetricValue + +# Initial weights (sum ≈ 1.0). Tweak later. +WEIGHTS: Dict[str, float] = { + "ramp_up_time": 0.15, + "bus_factor": 0.15, + "performance_claims": 0.10, + "license": 0.20, + "size": 0.10, # average of size_score dict + "dataset_and_code_score": 0.10, + "dataset_quality": 0.10, + "code_quality": 0.10, +} + +class ScoringEngine: + def compute(self, values: List[MetricValue], size_avg: float = 0.0) -> tuple[float, int]: + lookup = {mv.name: mv for mv in values} + total_latency = sum(mv.latency_ms for mv in values) + net = 0.0 + for k, w in WEIGHTS.items(): + if k == "size": + net += w * float(size_avg) + elif k in lookup: + net += w * float(lookup[k].value) + return (max(0.0, min(1.0, net)), total_latency) diff --git a/src/acmecli/types.py b/src/acmecli/types.py new file mode 100644 index 0000000..32ec282 --- /dev/null +++ b/src/acmecli/types.py @@ -0,0 +1,64 @@ +from dataclasses import dataclass, asdict +from typing import Protocol, Iterable, TypedDict, Literal, Optional, Dict + +Category = Literal["MODEL", "DATASET", "CODE"] +Source = Literal["GITHUB", "HUGGINGFACE", "LOCAL"] + +@dataclass(frozen=True) +class TargetSpec: + url: str + source: Source + name: str + category: Category + revision: Optional[str] = None # commit/tag if known + +class Signals(TypedDict, total=False): + readme_text: str + license_name: str + contributors: Dict[str, int] + stars: int + downloads: int + +@dataclass(frozen=True) +class MetricValue: + name: str # e.g. "ramp_up_time" + value: float # [0,1] + latency_ms: int + +@dataclass(frozen=True) +class ReportRow: + # Required NDJSON fields + per-metric latencies (values are stubs for now) + name: str + category: Category + net_score: float + net_score_latency: int + ramp_up_time: float + ramp_up_time_latency: int + bus_factor: float + bus_factor_latency: int + performance_claims: float + performance_claims_latency: int + license: float + license_latency: int + size_score: Dict[str, float] # {raspberry_pi, jetson_nano, desktop_pc, aws_server} + size_score_latency: int + dataset_and_code_score: float + dataset_and_code_score_latency: int + dataset_quality: float + dataset_quality_latency: int + code_quality: float + code_quality_latency: int + +class SourceHandler(Protocol): + def resolve_revision(self, url: str) -> str: ... + def fetch_meta(self, spec: TargetSpec) -> dict: ... + def stream_files(self, spec: TargetSpec, patterns: list[str]) -> Iterable[tuple[str, bytes]]: ... + +class Cache(Protocol): + def get(self, key: str) -> bytes | None: ... + def set(self, key: str, data: bytes, etag: str | None = None) -> None: ... + +class Metric(Protocol): + name: str + def collect(self, spec: TargetSpec, handler: SourceHandler, cache: Cache) -> Signals: ... + def score(self, signals: Signals) -> MetricValue: ... diff --git a/tests/__pycache__/test_metrics_contract.cpython-313-pytest-8.4.2.pyc b/tests/__pycache__/test_metrics_contract.cpython-313-pytest-8.4.2.pyc new file mode 100644 index 0000000..ba1c99c Binary files /dev/null and b/tests/__pycache__/test_metrics_contract.cpython-313-pytest-8.4.2.pyc differ diff --git a/tests/__pycache__/test_reporter_schema.cpython-313-pytest-8.4.2.pyc b/tests/__pycache__/test_reporter_schema.cpython-313-pytest-8.4.2.pyc new file mode 100644 index 0000000..f2d1c77 Binary files /dev/null and b/tests/__pycache__/test_reporter_schema.cpython-313-pytest-8.4.2.pyc differ diff --git a/tests/test_metrics_contract.py b/tests/test_metrics_contract.py new file mode 100644 index 0000000..51199a7 --- /dev/null +++ b/tests/test_metrics_contract.py @@ -0,0 +1,11 @@ +from acmecli.metrics.base import REGISTRY +from acmecli.metrics.license_metric import LicenseMetric + +def test_registry_has_license_metric(): + assert any(m.name == "license" for m in REGISTRY) + +def test_metric_value_range(): + m = LicenseMetric() + mv = m.score({}) + assert 0.0 <= mv.value <= 1.0 + assert mv.latency_ms >= 0 diff --git a/tests/test_reporter_schema.py b/tests/test_reporter_schema.py new file mode 100644 index 0000000..ba0dbcd --- /dev/null +++ b/tests/test_reporter_schema.py @@ -0,0 +1,16 @@ +from acmecli.types import ReportRow + +def test_reportrow_has_required_fields(): + row = ReportRow( + name="demo", category="MODEL", + net_score=0.0, net_score_latency=0, + ramp_up_time=0.0, ramp_up_time_latency=0, + bus_factor=0.0, bus_factor_latency=0, + performance_claims=0.0, performance_claims_latency=0, + license=0.0, license_latency=0, + size_score={}, size_score_latency=0, + dataset_and_code_score=0.0, dataset_and_code_score_latency=0, + dataset_quality=0.0, dataset_quality_latency=0, + code_quality=0.0, code_quality_latency=0 + ) + assert row.category == "MODEL"