Theflame47 commited on
Commit
6ee42cd
·
verified ·
1 Parent(s): fb632ec

Create commercial_stats.py

Browse files
Files changed (1) hide show
  1. commercial_stats.py +171 -0
commercial_stats.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # commercial_stats.py
2
+ import json
3
+ import os
4
+ import re
5
+ import subprocess
6
+ import tempfile
7
+ from typing import Any, Dict, List, Tuple, Optional
8
+
9
+ import requests
10
+ from fastapi import APIRouter
11
+ from fastapi.responses import JSONResponse
12
+
13
+ from data import flatten_models, license_label_and_url
14
+
15
+ router = APIRouter()
16
+
17
+ SCANCODE_CMD = os.environ.get("SCANCODE_CMD", "scancode") # allow override if needed
18
+ HTTP_TIMEOUT = 25
19
+
20
+ # Very light categorization strictly from SPDX/CC ids detected by ScanCode.
21
+ # No guesswork/fallback detection—just label grouping for the stats panel.
22
+ def _bucket_spdx(spdx_ids: List[str]) -> Dict[str, bool]:
23
+ s = " ".join(spdx_ids).upper()
24
+ is_noncommercial = ("-NC" in s) or ("NON-COMMERCIAL" in s)
25
+ is_no_derivatives = ("-ND" in s) or ("NO-DERIVATIVE" in s)
26
+ is_copyleft = bool(re.search(r"\b(AGPL|GPL|LGPL|MPL)\b", s))
27
+ # “Commercial-friendly” = not NC, not ND. Copyleft may still allow commercial use;
28
+ # we keep it separate so the UI can show both signals.
29
+ is_commercial_friendly = not is_noncommercial and not is_no_derivatives
30
+ return {
31
+ "commercial_friendly": is_commercial_friendly,
32
+ "noncommercial": is_noncommercial,
33
+ "no_derivatives": is_no_derivatives,
34
+ "copyleft": is_copyleft,
35
+ }
36
+
37
+ def _run_scancode_on_text(text: str) -> Tuple[List[str], List[str]]:
38
+ """
39
+ Run ScanCode CLI on a temp file containing `text`.
40
+ Returns (spdx_ids, keys) where:
41
+ - spdx_ids: list of detected SPDX license identifiers
42
+ - keys: ScanCode 'key' values (their internal identifiers)
43
+ """
44
+ with tempfile.TemporaryDirectory() as td:
45
+ path = os.path.join(td, "LICENSE.txt")
46
+ out_json = os.path.join(td, "out.json")
47
+ with open(path, "w", encoding="utf-8") as f:
48
+ f.write(text)
49
+ # CLI: detect licenses only, output JSON
50
+ cmd = [
51
+ SCANCODE_CMD,
52
+ "--license",
53
+ "--json-pp", out_json,
54
+ path,
55
+ ]
56
+ try:
57
+ proc = subprocess.run(
58
+ cmd, check=True, capture_output=True, text=True, timeout=120
59
+ )
60
+ except subprocess.CalledProcessError as e:
61
+ raise RuntimeError(f"ScanCode failed: {e.stderr or e.stdout}") from e
62
+ except Exception as e:
63
+ raise
64
+
65
+ with open(out_json, "r", encoding="utf-8") as f:
66
+ data = json.load(f)
67
+
68
+ # Extract SPDX ids and ScanCode keys from results
69
+ spdx_ids: List[str] = []
70
+ keys: List[str] = []
71
+ for res in data.get("files", []):
72
+ for lic in res.get("licenses", []):
73
+ spdx = lic.get("spdx_license_key") or ""
74
+ key = lic.get("key") or ""
75
+ if spdx:
76
+ spdx_ids.append(spdx)
77
+ if key:
78
+ keys.append(key)
79
+ return (sorted(set(spdx_ids)), sorted(set(keys)))
80
+
81
+ def _fetch_license_text(url: str) -> Optional[str]:
82
+ try:
83
+ r = requests.get(url, timeout=HTTP_TIMEOUT)
84
+ if r.status_code == 200 and r.text:
85
+ return r.text
86
+ except Exception:
87
+ pass
88
+ return None
89
+
90
+ @router.get("/commercial/stats")
91
+ def commercial_stats():
92
+ models = flatten_models()
93
+ total = len(models)
94
+ analyzed = 0
95
+ unknown = 0
96
+
97
+ # Aggregate by buckets and by SPDX id for transparency
98
+ buckets = {
99
+ "commercial_friendly": 0,
100
+ "noncommercial": 0,
101
+ "no_derivatives": 0,
102
+ "copyleft": 0,
103
+ }
104
+ by_spdx: Dict[str, int] = {}
105
+
106
+ per_model: List[Dict[str, Any]] = []
107
+
108
+ for m in models:
109
+ label, url = license_label_and_url(m)
110
+ if not url:
111
+ unknown += 1
112
+ per_model.append({
113
+ "id": f"{m.get('owner') or m.get('username') or ''}/{m.get('name') or ''}".strip("/"),
114
+ "license_label": label,
115
+ "spdx": [],
116
+ "buckets": {},
117
+ "source": None
118
+ })
119
+ continue
120
+
121
+ text = _fetch_license_text(url)
122
+ if not text:
123
+ unknown += 1
124
+ per_model.append({
125
+ "id": f"{m.get('owner') or m.get('username') or ''}/{m.get('name') or ''}".strip("/"),
126
+ "license_label": label,
127
+ "spdx": [],
128
+ "buckets": {},
129
+ "source": url
130
+ })
131
+ continue
132
+
133
+ try:
134
+ spdx_ids, _keys = _run_scancode_on_text(text)
135
+ except Exception:
136
+ # No fallback detection—just mark unknown
137
+ unknown += 1
138
+ per_model.append({
139
+ "id": f"{m.get('owner') or m.get('username') or ''}/{m.get('name') or ''}".strip("/"),
140
+ "license_label": label,
141
+ "spdx": [],
142
+ "buckets": {},
143
+ "source": url
144
+ })
145
+ continue
146
+
147
+ analyzed += 1
148
+ for sid in spdx_ids:
149
+ by_spdx[sid] = by_spdx.get(sid, 0) + 1
150
+
151
+ b = _bucket_spdx(spdx_ids)
152
+ for k, v in b.items():
153
+ if v:
154
+ buckets[k] += 1
155
+
156
+ per_model.append({
157
+ "id": f"{m.get('owner') or m.get('username') or ''}/{m.get('name') or ''}".strip("/"),
158
+ "license_label": label,
159
+ "spdx": spdx_ids,
160
+ "buckets": b,
161
+ "source": url
162
+ })
163
+
164
+ return JSONResponse({
165
+ "total_models": total,
166
+ "analyzed": analyzed,
167
+ "unknown": unknown,
168
+ "buckets": buckets,
169
+ "by_spdx": dict(sorted(by_spdx.items(), key=lambda kv: kv[1], reverse=True)),
170
+ "per_model": per_model[:200] # cap for payload size
171
+ })