Coverage for oc_meta / run / count / meta_entities.py: 71%
156 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-07-25 10:39 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-07-25 10:39 +0000
1#!/usr/bin/python
3# Copyright 2025 Arcangelo Massari <arcangelo.massari@unibo.it>
4# SPDX-FileCopyrightText: 2026 Arcangelo Massari <arcangelo.massari@unibo.it>
5#
6# SPDX-License-Identifier: ISC
8from __future__ import annotations
10import argparse
11import os
12import sys
13import multiprocessing
14from concurrent.futures import ProcessPoolExecutor, as_completed
15from typing import Dict, Set
17from rich_argparse import RichHelpFormatter
18from oc_meta.lib.sparql import execute_sparql
20from oc_meta.lib.console import create_progress
21from oc_meta.lib.file_manager import get_csv_data
22from oc_meta.lib.master_of_regex import split_name_and_ids
25def _count_venues_in_file(filepath: str) -> Set[str]:
26 csv_data = get_csv_data(filepath)
27 venues = set()
28 for row in csv_data:
29 if not row["venue"]:
30 continue
31 venue_name, venue_ids_str = split_name_and_ids(row["venue"])
32 if not venue_ids_str:
33 continue
34 venue_ids = set(venue_ids_str.split())
35 venue_metaid = next(
36 identifier
37 for identifier in venue_ids
38 if identifier.split(":", maxsplit=1)[0] == "omid"
39 )
40 if not venue_ids.difference({venue_metaid}):
41 venues.add(venue_name.lower())
42 else:
43 venues.add(venue_metaid)
44 return venues
47class OCMetaStatistics:
48 def __init__(
49 self,
50 sparql_endpoint: str,
51 csv_dump_path: str | None = None,
52 max_retries: int = 3,
53 retry_delay: int = 5,
54 ):
55 self.sparql_endpoint = sparql_endpoint
56 self.csv_dump_path = csv_dump_path
57 self.max_retries = max_retries
58 self.retry_delay = retry_delay
60 def _execute_sparql_query(self, query: str) -> Dict:
61 try:
62 return execute_sparql(
63 self.sparql_endpoint,
64 query,
65 max_retries=self.max_retries,
66 backoff_factor=self.retry_delay,
67 )
68 except Exception as e:
69 print(f"Query failed after {self.max_retries} retries.", file=sys.stderr)
70 raise Exception("SPARQL query failed after multiple retries.") from e
72 def __enter__(self):
73 return self
75 def __exit__(self, exc_type, exc_val, exc_tb):
76 return False
78 def count_expressions(self) -> int:
79 query = """
80 PREFIX fabio: <http://purl.org/spar/fabio/>
82 SELECT (COUNT(DISTINCT ?expression) AS ?count)
83 WHERE {
84 ?expression a fabio:Expression .
85 }
86 """
87 results = self._execute_sparql_query(query)
88 return int(results["results"]["bindings"][0]["count"]["value"])
90 def count_role_entities(self) -> Dict[str, int]:
91 query = """
92 PREFIX pro: <http://purl.org/spar/pro/>
94 SELECT ?role (COUNT(DISTINCT ?roleInTime) AS ?count)
95 WHERE {
96 ?roleInTime pro:withRole ?role .
97 FILTER(?role IN (pro:author, pro:publisher, pro:editor))
98 }
99 GROUP BY ?role
100 """
101 results = self._execute_sparql_query(query)
103 role_counts = {"pro:author": 0, "pro:publisher": 0, "pro:editor": 0}
105 for binding in results["results"]["bindings"]:
106 role_uri = binding["role"]["value"]
107 count = int(binding["count"]["value"])
109 if role_uri == "http://purl.org/spar/pro/author":
110 role_counts["pro:author"] = count
111 elif role_uri == "http://purl.org/spar/pro/publisher":
112 role_counts["pro:publisher"] = count
113 elif role_uri == "http://purl.org/spar/pro/editor":
114 role_counts["pro:editor"] = count
116 return role_counts
118 def count_venues_from_csv(self) -> int:
119 if not self.csv_dump_path:
120 raise ValueError("CSV dump path is required to count venues")
122 filenames = sorted(os.listdir(self.csv_dump_path))
123 filepaths = [
124 os.path.join(self.csv_dump_path, f) for f in filenames if f.endswith(".csv")
125 ]
127 all_venues: Set[str] = set()
129 with create_progress() as progress:
130 task = progress.add_task(
131 "Counting venues from CSV files...", total=len(filepaths)
132 )
134 # Use forkserver to avoid deadlocks when forking in a multi-threaded environment
135 with ProcessPoolExecutor(
136 mp_context=multiprocessing.get_context("forkserver")
137 ) as executor:
138 futures = {
139 executor.submit(_count_venues_in_file, fp): fp for fp in filepaths
140 }
141 for future in as_completed(futures):
142 venues = future.result()
143 all_venues.update(venues)
144 progress.update(task, advance=1)
146 return len(all_venues)
148 def run_selected_analyses(
149 self, analyze_br: bool, analyze_ar: bool, analyze_venues: bool
150 ) -> Dict:
151 print("Starting dataset statistics...")
152 print(f"Connected to endpoint: {self.sparql_endpoint}")
153 if self.csv_dump_path:
154 print(f"CSV dump path: {self.csv_dump_path}")
155 print()
157 results = {}
159 if analyze_br:
160 print("1. Counting fabio:Expression entities...")
161 try:
162 expressions_count = self.count_expressions()
163 results["fabio_expressions"] = expressions_count
164 print(f" Found {expressions_count:,} fabio:Expression entities")
165 except Exception as e:
166 print(f" Error: {e}")
167 results["fabio_expressions"] = None
168 print()
170 if analyze_ar:
171 print("2. Counting pro:author, pro:publisher and pro:editor roles...")
172 try:
173 role_counts = self.count_role_entities()
174 results["roles"] = role_counts
175 print(f" Found {role_counts['pro:author']:,} pro:author roles")
176 print(f" Found {role_counts['pro:publisher']:,} pro:publisher roles")
177 print(f" Found {role_counts['pro:editor']:,} pro:editor roles")
178 except Exception as e:
179 print(f" Error: {e}")
180 results["roles"] = None
181 print()
183 if analyze_venues:
184 print("3. Counting venues from CSV dump...")
185 if not self.csv_dump_path:
186 print(" Error: CSV dump path is required for venue counting")
187 results["venues"] = None
188 else:
189 try:
190 venues_count = self.count_venues_from_csv()
191 results["venues"] = venues_count
192 print(f" Found {venues_count:,} distinct venues")
193 except Exception as e:
194 print(f" Error: {e}")
195 results["venues"] = None
196 print()
198 print("Statistics completed!")
199 return results
201 def run_all_analyses(self) -> Dict:
202 return self.run_selected_analyses(
203 analyze_br=True, analyze_ar=True, analyze_venues=True
204 )
207def main():
208 parser = argparse.ArgumentParser(
209 description="Compute OpenCitations Meta dataset statistics",
210 formatter_class=RichHelpFormatter,
211 epilog="""
212Examples:
213 # Run all statistics
214 python -m oc_meta.run.count.meta_entities http://localhost:8890/sparql --csv /path/to/csv/dump
216 # Count only bibliographic resources
217 python -m oc_meta.run.count.meta_entities http://localhost:8890/sparql --br
219 # Count only roles
220 python -m oc_meta.run.count.meta_entities http://localhost:8890/sparql --ar
222 # Count only venues (requires CSV dump)
223 python -m oc_meta.run.count.meta_entities http://localhost:8890/sparql --venues --csv /path/to/csv/dump
225Statistics computed:
226 --br: Count fabio:Expression entities (via SPARQL)
227 --ar: Count pro:author, pro:publisher and pro:editor roles (via SPARQL)
228 --venues: Count distinct venues with disambiguation (via CSV dump)
230If no specific options are provided, all statistics will be computed.
231 """,
232 )
234 parser.add_argument("sparql_endpoint", help="SPARQL endpoint URL")
236 parser.add_argument(
237 "--csv",
238 dest="csv_dump_path",
239 help="Path to CSV dump directory (required for venue counting)",
240 )
242 parser.add_argument(
243 "--br",
244 action="store_true",
245 help="Count bibliographic resources (fabio:Expression entities)",
246 )
248 parser.add_argument(
249 "--ar",
250 action="store_true",
251 help="Count roles (pro:author, pro:publisher, pro:editor)",
252 )
254 parser.add_argument(
255 "--venues", action="store_true", help="Count distinct venues (requires --csv)"
256 )
258 args = parser.parse_args()
260 analyze_br = args.br or not (args.br or args.ar or args.venues)
261 analyze_ar = args.ar or not (args.br or args.ar or args.venues)
262 analyze_venues = args.venues or not (args.br or args.ar or args.venues)
264 if analyze_venues and not args.csv_dump_path:
265 print("Error: --csv is required for venue counting", file=sys.stderr)
266 sys.exit(1)
268 try:
269 with OCMetaStatistics(args.sparql_endpoint, args.csv_dump_path) as stats:
270 results = stats.run_selected_analyses(
271 analyze_br, analyze_ar, analyze_venues
272 )
274 print("\n" + "=" * 50)
275 print("SUMMARY")
276 print("=" * 50)
278 if results.get("fabio_expressions") is not None:
279 print(f"fabio:Expression entities: {results['fabio_expressions']:,}")
281 if results.get("roles"):
282 print(f"pro:author roles: {results['roles']['pro:author']:,}")
283 print(f"pro:publisher roles: {results['roles']['pro:publisher']:,}")
284 print(f"pro:editor roles: {results['roles']['pro:editor']:,}")
286 if results.get("venues") is not None:
287 print(f"Distinct venues: {results['venues']:,}")
289 return results
291 except Exception as e:
292 print(f"Statistics failed: {e}", file=sys.stderr)
293 sys.exit(1)
296if __name__ == "__main__":
297 main()