Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae88367fac | |||
| a305d00c75 | |||
| 57fcfc7693 | |||
| a07b1e42a8 | |||
| 7c6f4c9f1d |
@@ -49,6 +49,7 @@ jobs:
|
|||||||
subprocess.run([str(venv/'bin/paper-atlas'),'--help'],cwd=outside,check=True)
|
subprocess.run([str(venv/'bin/paper-atlas'),'--help'],cwd=outside,check=True)
|
||||||
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','cvf','--help'],cwd=outside,check=True)
|
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','cvf','--help'],cwd=outside,check=True)
|
||||||
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','www-archive','--help'],cwd=outside,check=True)
|
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','www-archive','--help'],cwd=outside,check=True)
|
||||||
|
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','aamas-proceedings','--help'],cwd=outside,check=True)
|
||||||
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','postgres','--help'],cwd=outside,check=True)
|
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','postgres','--help'],cwd=outside,check=True)
|
||||||
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','edition-tracks','--help'],cwd=outside,check=True)
|
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','edition-tracks','--help'],cwd=outside,check=True)
|
||||||
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','joint-editions','--help'],cwd=outside,check=True)
|
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','joint-editions','--help'],cwd=outside,check=True)
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ paper-atlas collect colm --cache /data/paper-index/raw/http-cache \
|
|||||||
--out /data/paper-index/collected
|
--out /data/paper-index/collected
|
||||||
```
|
```
|
||||||
|
|
||||||
|
早期主系列的Workshop名称按[ICML/IROS精确届次证据](docs/coverage/historical-primary-series-2026-11-09.md)识别,保留原名与范围证据;代码支持不代表已补齐生产数据。
|
||||||
|
|
||||||
|
AAMAS 的[官方论文集采集器](docs/aamas-proceedings.md)目前核验了 2012 版式,可从缓存或已校验快照采集标题、作者及页面提供的摘要;发表形式单独保存,全文/扩展摘要中未明确的投稿 track 保持待明确,不根据会话主题推定主会轨道。
|
||||||
|
|
||||||
`paper-atlas collect --help` 查看来源;`paper-atlas import --help` 查看导入方式。完整批次步骤见[使用指南](docs/usage.md)。
|
`paper-atlas collect --help` 查看来源;`paper-atlas import --help` 查看导入方式。完整批次步骤见[使用指南](docs/usage.md)。
|
||||||
|
|
||||||
共享实例名为 `foundry-postgres`,数据位于 `foundry_data` 库的 `research` schema;其他项目可按权限复用同一组表。库名通过 libpq service 配置、schema 通过 search_path 或 `--pg-schema` 指定,不写死在查询中。PostgreSQL 凭据放在服务器权限为 `0600` 的 libpq service 文件中;命令行只使用服务名称。`paper-atlas maintain postgres --help` 提供迁移入口:先完整核验 21 张业务表,再发布目标 schema。SQLite FTS5 内部表由 PostgreSQL GIN 索引替代。切换后 SQLite 为只读备份;`import records`、`import arxiv` 和 `maintain backfill` 使用 `--pg-service foundry-research` 写入主库,整个写入会话只允许一个协调写入者,会议文件失败时整体回滚并保留失败批次记录。
|
共享实例名为 `foundry-postgres`,数据位于 `foundry_data` 库的 `research` schema;其他项目可按权限复用同一组表。库名通过 libpq service 配置、schema 通过 search_path 或 `--pg-schema` 指定,不写死在查询中。PostgreSQL 凭据放在服务器权限为 `0600` 的 libpq service 文件中;命令行只使用服务名称。`paper-atlas maintain postgres --help` 提供迁移入口:先完整核验 21 张业务表,再发布目标 schema。SQLite FTS5 内部表由 PostgreSQL GIN 索引替代。切换后 SQLite 为只读备份;`import records`、`import arxiv` 和 `maintain backfill` 使用 `--pg-service foundry-research` 写入主库,整个写入会话只允许一个协调写入者,会议文件失败时整体回滚并保留失败批次记录。
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# AAMAS 官方论文集元数据
|
||||||
|
|
||||||
|
`paper_atlas.collectors.aamas` 是按需运行的采集适配器,通过统一入口 `collect aamas-proceedings` 使用。目前只支持已独立核验的 **AAMAS 2012** HTML 版式;其他年份需先核对官方目录和逐篇结构,再增加适配,不能仅替换年份。
|
||||||
|
|
||||||
|
## 来源与范围
|
||||||
|
|
||||||
|
[官方论文集介绍](https://www.ifaamas.org/Proceedings/aamas2012/)及其 Full Papers、Extended Abstracts、Demos、Doctorial Consortium Abstracts 四个栏目分别列出 136、146、28、24 条,共 334 条。旧 DBLP 候选卷的 310 条不能替代这四类官方目录,也不能全部标记为主会全文。栏目数量来自 2026-11-09 冻结的原始页面;页面结构或数量发生变化时,采集器停止,留待重新核验。
|
||||||
|
|
||||||
|
采集标题、逐篇作者、页面实际提供的摘要、官方条目 ID、目录锚点和 PDF 地址。**只保存 PDF 链接,不下载 PDF。** 全文页面的摘要藏在标题链接内部的 `abstract` 元素,作者链接里的 “has N papers” 是界面徽标,二者不会混入标题或姓名。未提供的摘要保存为 `null`,不会用标题、说明文字或其他论文填充。
|
||||||
|
|
||||||
|
官方介绍说明了 general、robotics、virtual agents、innovative applications 等投稿轨道,但逐篇目录的主题会话不能独立证明某篇论文的投稿轨道。因此全文和扩展摘要均使用 `proceedings_unspecified`;演示、博士栏目使用 `demo`、`doctoral`。发表形式保留在 `metadata.publication_format` 和 `aamas_proceedings:publication_format` 标签中,会话原文单独保存。它们不是研究类型标注,也不会写入自定义 annotation。
|
||||||
|
|
||||||
|
目录条目仅提供发表录用证据,不是原始决定信。`observed_at` 是页面采集时间,不能当作 2012 年的决定日期;官方介绍中的投稿、撤稿、录用汇总数不能识别逐篇拒稿或撤稿,采集器不会由总量相减生成这些决定。
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
|
||||||
|
在已确认的远程运行环境中,使用 HTTP 缓存采集:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
paper-atlas collect aamas-proceedings --year 2012 \
|
||||||
|
--cache /data/paper-index/cache/aamas \
|
||||||
|
--out /data/paper-index/collected/aamas-2012
|
||||||
|
```
|
||||||
|
|
||||||
|
也可使用已冻结的官方页面:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
paper-atlas collect aamas-proceedings --year 2012 \
|
||||||
|
--snapshots /data/paper-index/collected/historical-next-source-evidence-v1 \
|
||||||
|
--out /data/paper-index/collected/aamas-2012
|
||||||
|
```
|
||||||
|
|
||||||
|
快照目录需含 `receipts.json`,其中每个响应包含原始 `url`、HTTP `status`、相对 `file`、`bytes`、`sha256` 和 `finished_at`。要求五个准确官方 URL 均唯一匹配且返回 200;验证全部字节数和哈希后才解析,不接受目录穿越或绝对文件路径。快照中其他来源的响应不会被当成 AAMAS 页面。
|
||||||
|
|
||||||
|
输出为按数据库 track 分组的三个 JSONL 文件和对应 manifest、五份原始 HTML 证据,以及最后写出的 `aamas-proceedings-2012.json` 采集报告。锁文件避免同一输出目录并发运行。manifest 始终为 `partial`,记录发表形式数量和缺失摘要数;**采集命令不连接数据库,也不导入。**
|
||||||
|
|
||||||
|
## 复核与恢复
|
||||||
|
|
||||||
|
每条记录携带对应官方页面的 URL、采集时间、字节数、SHA-256 与输出内证据路径。重新运行前先检查已有采集报告和文件哈希;采集中断不等于主库已导入,报告缺失或哈希不一致的目录不能直接作为导入队列。输出中的原始证据应随文件一起保存。
|
||||||
|
|
||||||
|
生产导入前仍需单独与主库核验稳定标识、标题、作者及已有投稿,按证据补充 DOI/arXiv 等标识;目录页上的作者计数徽标和会话 ID 不能作为这些标识。需要另建经过核验的增量清单、保护旧历史,完成独立验收后才能计入覆盖。跨来源同一论文不得仅因同题就新增或合并投稿。
|
||||||
|
|
||||||
|
本功能的代码、测试或采集成功均不代表 AAMAS 2012 已导入生产,也不证明拒稿全集完整。全库最终去重、摘要和来源覆盖验收属于独立后续阶段。
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# ICML 与 IROS 早期主系列的来源范围
|
||||||
|
|
||||||
|
本轮修正采集阶段的届次识别,不修改任何现有生产投稿轨道或历史。正式主系列曾以Workshop命名,不等于附属workshop;也不能反过来把所有旧workshop升为主会。
|
||||||
|
|
||||||
|
## 已核实的精确范围
|
||||||
|
|
||||||
|
| 会议 | 论文集键 | 依据与行为 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ICML | `conf/icml/1988`、`1989`、`1990`、`1991`、`1992` | [官方历史](https://icml.cc/Conferences/2008/past_icmls.shtml.html)将其列为第5–9届,年份与举办地对应固定目录中的完整标题。1989/1991/1992的Workshop名称不再导致主系列被默认排除;1988/1990保留main并增加来源范围证据 |
|
||||||
|
| IROS | `conf/iros/1988`、`1989`、`1990` | [IEEE RAS官方历届目录](https://www.ieee-ras.org/conferences-workshops/financially-co-sponsored/iros/iros-past-and-future-venues/)列出三届与论文集链接,地点及日期对应固定目录标题;这些是IROS主系列 |
|
||||||
|
|
||||||
|
规则同时要求family、booktitle基础别名(ML或IROS)、空轨道后缀、完整精确论文集键、匹配的事件年份及固定完整标题。仍执行原有书目年份核查:与事件年不同且标题无明确事件年时拒绝推断。ML91原始标题没有四位年份,使用精确1991卷键、完整标题、逐篇书目年和官方第8届记录,不从两位标题代码猜世纪。
|
||||||
|
|
||||||
|
`1991w`/`1988w`等卷键、booktitle中的Workshops后缀、不同届次/地点/完整标题、未列入的年份均不会获得上述主系列豁免。AISTATS/COLT既有精确规则保留;真正的附属workshop仍受默认不采集workshop的边界约束。
|
||||||
|
|
||||||
|
## 原始字段、决定与后续导入
|
||||||
|
|
||||||
|
采集器仍保存原始proceedings标题(包括Workshop)、书目年、原始作者名、完整来源书目字段及稳定标识;新增 `track_scope_note` 与官方 `track_scope_evidence_url`。接受状态仍只源自该论文的inproceedings书目证据;不从历史目录生成占位论文或拒稿,也不重新导入已经存在的记录。
|
||||||
|
|
||||||
|
新规则的输出必须重新做原始来源/标题/作者/稳定标识与现有库冲突预检,再按增量导入。此前被隔离的ICML1988/1990记录不能仅凭代码规则而直接解除全部冲突。当前正在运行的历史批次使用原固定版本和文件,不能在运行中替换其源码或输入。
|
||||||
|
|
||||||
|
## 验证与证据
|
||||||
|
|
||||||
|
新增回归覆盖:ICML三届Workshop名称、IROS最初三届、ICML原Conference名称的两届、错误卷键/后缀/标题/未知年份,以及归一化记录经过原生文件导入后保留轨道证据。构建与测试仅在已授权的ces-control远程runner执行,本机仅做语法和文本/JSON一致性检查;结果以相应commit的CI回执为准。
|
||||||
|
|
||||||
|
官方HTML在工作证据目录 `paper-index/historical-primary-series-evidence-v1/`,附响应状态、字节数、时间与SHA256;仅取元数据网页,没有下载论文全文。
|
||||||
|
|
||||||
|
| 来源 | UTC采样时间 | HTTP | 字节数 | SHA256 |
|
||||||
|
| --- | --- | ---: | ---: | --- |
|
||||||
|
| [icml-history](https://icml.cc/Conferences/2008/past_icmls.shtml.html) | 2026-11-08T18:36:43.043135+00:00 | 200 | 11993 | `80cce886fe985551339a52b13c4077acf827e8fcc01555f854dc24077bdebdf0` |
|
||||||
|
| [iros-history](https://www.ieee-ras.org/conferences-workshops/financially-co-sponsored/iros/iros-past-and-future-venues/) | 2026-11-08T18:36:45.687171+00:00 | 200 | 466059 | `fdb4594621e68567e1099f5c88aae53de835b7b8146025148c3b1968df990697` |
|
||||||
|
|
||||||
|
本次不关闭历史年度缺口,也不替代全部导入后的全库去重、来源/年度/track覆盖、摘要和实际查询验收。
|
||||||
@@ -23,6 +23,7 @@ COMMANDS = {
|
|||||||
('collect','mlsys'): ('paper_atlas.collectors.proceedings', {'source':'mlsys'}, 'MLSys 官方历年标题与摘要'),
|
('collect','mlsys'): ('paper_atlas.collectors.proceedings', {'source':'mlsys'}, 'MLSys 官方历年标题与摘要'),
|
||||||
('collect','cvf'): ('paper_atlas.collectors.vision', {'source':'cvf'}, 'CVPR / ICCV 官方开放目录'),
|
('collect','cvf'): ('paper_atlas.collectors.vision', {'source':'cvf'}, 'CVPR / ICCV 官方开放目录'),
|
||||||
('collect','ecva'): ('paper_atlas.collectors.vision', {'source':'ecva'}, 'ECCV 官方开放目录'),
|
('collect','ecva'): ('paper_atlas.collectors.vision', {'source':'ecva'}, 'ECCV 官方开放目录'),
|
||||||
|
('collect','aamas-proceedings'): ('paper_atlas.collectors.aamas', {}, '按官方目录区分 AAMAS 发表形式和摘要'),
|
||||||
('collect','www-archive'): ('paper_atlas.collectors.www_archive', {}, '核验早期WWW官方档案并分开待核查条目'),
|
('collect','www-archive'): ('paper_atlas.collectors.www_archive', {}, '核验早期WWW官方档案并分开待核查条目'),
|
||||||
('collect','dblp'): ('paper_atlas.collectors.dblp_records', {}, '核对并整理 DBLP 历史发表记录'),
|
('collect','dblp'): ('paper_atlas.collectors.dblp_records', {}, '核对并整理 DBLP 历史发表记录'),
|
||||||
('catalogue','dblp'): ('paper_atlas.collectors.dblp', {}, '盘点 DBLP 历史会议目录'),
|
('catalogue','dblp'): ('paper_atlas.collectors.dblp', {}, '盘点 DBLP 历史会议目录'),
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
"""Official AAMAS proceedings metadata, with publication format separate from track."""
|
||||||
|
import argparse
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
import fcntl
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
from urllib.parse import urljoin, urlsplit
|
||||||
|
|
||||||
|
from paper_atlas.db.submissions import timestamp
|
||||||
|
from paper_atlas.io.html import Document, Node, clean
|
||||||
|
from paper_atlas.io.http import fetch
|
||||||
|
|
||||||
|
SOURCE = 'aamas_proceedings'
|
||||||
|
ROOT = 'https://www.ifaamas.org/Proceedings/aamas2012/'
|
||||||
|
# Counts independently checked against the frozen publisher sections. This
|
||||||
|
# adapter deliberately refuses other editions/layouts until they are reviewed.
|
||||||
|
SECTIONS = {
|
||||||
|
'fullpapers': ('Full Papers', 'full_paper', 'proceedings_unspecified', 136),
|
||||||
|
'extabstracts': ('Extended Abstracts', 'extended_abstract', 'proceedings_unspecified', 146),
|
||||||
|
'demos': ('Demos', 'demo', 'demo', 28),
|
||||||
|
'doctorals': ('Doctorial Consortium Abstracts', 'doctoral_abstract', 'doctoral', 24),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def classes(node):
|
||||||
|
return set(node.attrs.get('class', '').split())
|
||||||
|
|
||||||
|
|
||||||
|
def text_without(node, omitted):
|
||||||
|
if node.tag in ('script', 'style') or classes(node) & omitted:
|
||||||
|
return ''
|
||||||
|
parts = [text_without(child, omitted) if isinstance(child, Node) else child
|
||||||
|
for child in node.children]
|
||||||
|
value = ''.join(parts)
|
||||||
|
return ' '+value+' ' if node.tag in ('p', 'div', 'section', 'li', 'br') else value
|
||||||
|
|
||||||
|
|
||||||
|
def one(nodes, description):
|
||||||
|
if len(nodes) != 1:
|
||||||
|
raise ValueError('Expected one '+description)
|
||||||
|
return nodes[0]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_section(html, section, observed_at, evidence):
|
||||||
|
heading, publication_format, track, _ = SECTIONS[section]
|
||||||
|
document = Document(html)
|
||||||
|
titles = [clean(n.text()) for n in document.root.walk() if n.tag == 'title']
|
||||||
|
if titles != [heading+' - AAMAS 2012']:
|
||||||
|
raise ValueError('Unexpected proceedings edition or section title')
|
||||||
|
headings = [clean(n.text()) for n in document.root.walk() if n.tag == 'h1']
|
||||||
|
if headings != [heading]:
|
||||||
|
raise ValueError('Unexpected proceedings section heading')
|
||||||
|
url = ROOT+'resources/'+section+'.html'
|
||||||
|
records = []
|
||||||
|
seen = set()
|
||||||
|
session = None
|
||||||
|
for node in document.root.walk():
|
||||||
|
if node.tag == 'h2' and 'section' in classes(node):
|
||||||
|
session = {'id': node.attrs.get('id'), 'title': clean(node.text())}
|
||||||
|
if node.tag != 'div' or 'paper' not in classes(node):
|
||||||
|
continue
|
||||||
|
paper_id = node.attrs.get('id', '')
|
||||||
|
if not re.fullmatch(r'[A-Za-z0-9]+_[0-9]+', paper_id) or paper_id in seen:
|
||||||
|
raise ValueError('Missing or repeated publisher paper ID')
|
||||||
|
seen.add(paper_id)
|
||||||
|
title_node = one([n for n in node.walk() if n.tag == 'span' and 'title' in classes(n)], 'paper title')
|
||||||
|
link = one([n for n in title_node.children if isinstance(n, Node) and n.tag == 'a'], 'paper link')
|
||||||
|
paper_url = urljoin(url, link.attrs.get('href', ''))
|
||||||
|
if paper_url != ROOT+'papers/'+paper_id+'.pdf':
|
||||||
|
raise ValueError('Paper link differs from publisher ID or official host')
|
||||||
|
title = clean(text_without(link, {'abstract'}))
|
||||||
|
abstract_nodes = [n for n in link.walk() if n.tag == 'span' and 'abstract' in classes(n)]
|
||||||
|
if len(abstract_nodes) > 1:
|
||||||
|
raise ValueError('Multiple abstracts for one paper')
|
||||||
|
abstract = (clean(abstract_nodes[0].text()) or None) if abstract_nodes else None
|
||||||
|
author_node = one([n for n in node.walk() if n.tag == 'span' and 'authors' in classes(n)], 'author list')
|
||||||
|
author_display = clean(text_without(author_node, {'amount'}))
|
||||||
|
authors = [clean(name) for name in author_display.split(',')]
|
||||||
|
if not title or not author_display or any(not name for name in authors):
|
||||||
|
raise ValueError('Empty paper title or author')
|
||||||
|
tags = [{'namespace': 'aamas_proceedings:publication_format', 'value': publication_format}]
|
||||||
|
if session:
|
||||||
|
tags.append({'namespace': 'aamas_proceedings:session', 'value': session['title']})
|
||||||
|
records.append({
|
||||||
|
'source': SOURCE, 'external_id': '2012/'+paper_id,
|
||||||
|
'url': url+'#'+paper_id, 'title': title, 'abstract': abstract,
|
||||||
|
'authors': authors, 'identifiers': {}, 'venue': 'AAMAS', 'venue_aliases': [],
|
||||||
|
'year': 2012, 'track': track, 'status': 'accepted',
|
||||||
|
'observed_at': observed_at,
|
||||||
|
'raw_decision': 'listed in official AAMAS 2012 proceedings '+heading,
|
||||||
|
'metadata': {'publisher_paper_id': paper_id, 'paper_url': paper_url,
|
||||||
|
'section_url': url, 'section_heading': heading,
|
||||||
|
'publication_format': publication_format, 'session': session,
|
||||||
|
'authors_display': author_display, 'source_evidence': evidence,
|
||||||
|
'publication_evidence': 'official published proceedings entry; not an original decision note',
|
||||||
|
'track_scope_note': ('Full papers and extended abstracts include general and special submission tracks; '
|
||||||
|
'session topics do not establish an individual submission track.'
|
||||||
|
if track == 'proceedings_unspecified' else 'Explicit publisher section.'),
|
||||||
|
'decision_date_precision': 'not_supplied'},
|
||||||
|
'tags': tags})
|
||||||
|
if not records:
|
||||||
|
raise ValueError('No publisher paper entries found')
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
def read_pages(cache=None, snapshots=None):
|
||||||
|
if bool(cache) == bool(snapshots):
|
||||||
|
raise ValueError('Choose exactly one HTTP cache or frozen snapshot directory')
|
||||||
|
urls = {'introduction': ROOT, **{key: ROOT+'resources/'+key+'.html' for key in SECTIONS}}
|
||||||
|
pages = {}
|
||||||
|
receipts = []
|
||||||
|
frozen = json.loads((Path(snapshots)/'receipts.json').read_text())['receipts'] if snapshots else []
|
||||||
|
for key, url in urls.items():
|
||||||
|
if snapshots:
|
||||||
|
receipt = one([r for r in frozen if r['url'] == url], 'frozen response receipt')
|
||||||
|
filename = Path(receipt['file'])
|
||||||
|
if filename.is_absolute() or '..' in filename.parts or receipt.get('status') != 200:
|
||||||
|
raise ValueError('Unsafe or unsuccessful frozen response')
|
||||||
|
data = (Path(snapshots)/filename).read_bytes()
|
||||||
|
if len(data) > 8*1024**2 or hashlib.sha256(data).hexdigest() != receipt['sha256'] or len(data) != receipt['bytes']:
|
||||||
|
raise ValueError('Frozen publisher response checksum or size mismatch')
|
||||||
|
observed = receipt['finished_at']
|
||||||
|
else:
|
||||||
|
data = fetch(url, cache, 8*1024**2, delay=1)
|
||||||
|
receipt = json.loads((Path(cache)/hashlib.sha256(url.encode()).hexdigest()).with_suffix('.json').read_text())
|
||||||
|
observed = receipt['retrieved_at']
|
||||||
|
evidence = {'url': url, 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data),
|
||||||
|
'file': 'evidence/'+hashlib.sha256(data).hexdigest()+'.html', 'observed_at': observed,
|
||||||
|
'http_status': 200, 'response_receipt': receipt}
|
||||||
|
pages[key] = (data, evidence)
|
||||||
|
receipts.append(evidence)
|
||||||
|
return pages, receipts
|
||||||
|
|
||||||
|
|
||||||
|
def _collect(year, out, *, cache=None, snapshots=None):
|
||||||
|
if year != 2012:
|
||||||
|
raise ValueError('Only the independently reviewed AAMAS 2012 layout is supported')
|
||||||
|
pages, receipts = read_pages(cache, snapshots)
|
||||||
|
intro = Document(pages['introduction'][0].decode('utf-8'))
|
||||||
|
if [clean(n.text()) for n in intro.root.walk() if n.tag == 'title'] != ['AAMAS 2012']:
|
||||||
|
raise ValueError('Unexpected proceedings introduction')
|
||||||
|
records = []
|
||||||
|
for section, (_, _, _, expected) in SECTIONS.items():
|
||||||
|
data, evidence = pages[section]
|
||||||
|
group = parse_section(data.decode('utf-8'), section, evidence['observed_at'], evidence)
|
||||||
|
if len(group) != expected:
|
||||||
|
raise ValueError('Publisher section count differs from reviewed inventory: '+section)
|
||||||
|
records.extend(group)
|
||||||
|
if len({r['external_id'] for r in records}) != len(records):
|
||||||
|
raise ValueError('Paper IDs collide across publisher sections')
|
||||||
|
grouped = defaultdict(list)
|
||||||
|
for record in records:
|
||||||
|
grouped[record['track']].append(record)
|
||||||
|
out = Path(out)
|
||||||
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
|
files = []
|
||||||
|
with tempfile.TemporaryDirectory(prefix='.aamas-', dir=out) as temporary:
|
||||||
|
stage = Path(temporary)
|
||||||
|
(stage/'evidence').mkdir()
|
||||||
|
for data, evidence in pages.values():
|
||||||
|
(stage/evidence['file']).write_bytes(data)
|
||||||
|
for track, group in sorted(grouped.items()):
|
||||||
|
path = stage/f'aamas-proceedings-2012-{track}.jsonl'
|
||||||
|
path.write_text(''.join(json.dumps(r, ensure_ascii=False)+'\n' for r in group))
|
||||||
|
manifest = {'source': SOURCE, 'parser_version': 1, 'venue': 'AAMAS', 'year': 2012, 'track': track,
|
||||||
|
'count': len(group), 'file': str(out/path.name), 'file_sha256': hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||||
|
'list_url': ROOT, 'status': 'partial', 'missing_abstract_count': sum(not r['abstract'] for r in group),
|
||||||
|
'publication_format_counts': dict(Counter(r['metadata']['publication_format'] for r in group)),
|
||||||
|
'note': 'Publisher sections verified; individual full/extended submission track, decision history and independent identities still need review.'}
|
||||||
|
path.with_suffix('.manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2)+'\n')
|
||||||
|
files.append(manifest)
|
||||||
|
result = {'source': SOURCE, 'year': year, 'finished_at': timestamp(), 'status': 'collected_pending_identity_review',
|
||||||
|
'records': len(records), 'files': files, 'source_receipts': receipts,
|
||||||
|
'publication_format_counts': dict(Counter(r['metadata']['publication_format'] for r in records)),
|
||||||
|
'database_written': False, 'pdfs_downloaded': False,
|
||||||
|
'note': 'Aggregate submission/withdrawal totals do not identify individual rejected or withdrawn papers.'}
|
||||||
|
(stage/'aamas-proceedings-2012.json').write_text(json.dumps(result, ensure_ascii=False, indent=2)+'\n')
|
||||||
|
# Validated files first; write the completed collection report last.
|
||||||
|
(out/'evidence').mkdir(exist_ok=True)
|
||||||
|
for path in (stage/'evidence').iterdir():
|
||||||
|
path.replace(out/'evidence'/path.name)
|
||||||
|
for path in stage.iterdir():
|
||||||
|
if path.is_file() and path.name != 'aamas-proceedings-2012.json':
|
||||||
|
path.replace(out/path.name)
|
||||||
|
(stage/'aamas-proceedings-2012.json').replace(out/'aamas-proceedings-2012.json')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def collect(year, out, *, cache=None, snapshots=None):
|
||||||
|
if year != 2012:
|
||||||
|
raise ValueError('Only the independently reviewed AAMAS 2012 layout is supported')
|
||||||
|
out = Path(out)
|
||||||
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
|
with (out/'aamas.collect.lock').open('a') as lock:
|
||||||
|
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
return _collect(year, out, cache=cache, snapshots=snapshots)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None, *, prog=None):
|
||||||
|
parser = argparse.ArgumentParser(prog=prog)
|
||||||
|
parser.add_argument('--year', type=int, choices=[2012], required=True)
|
||||||
|
parser.add_argument('--out', required=True)
|
||||||
|
source = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
source.add_argument('--cache')
|
||||||
|
source.add_argument('--snapshots')
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
print(json.dumps(collect(args.year, args.out, cache=args.cache, snapshots=args.snapshots), ensure_ascii=False))
|
||||||
@@ -54,7 +54,34 @@ HISTORICAL_WORKSHOP_TITLES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Exact pinned volume titles corroborated by official series histories. The
|
||||||
|
# original title (including Workshop) remains in source metadata. In particular
|
||||||
|
# ML91 has no four-digit year in its title; its exact key/title and bibliographic
|
||||||
|
# year are checked independently, rather than guessing a title year.
|
||||||
|
HISTORICAL_PRIMARY_EVIDENCE = {
|
||||||
|
'icml': ('ML', 'https://icml.cc/Conferences/2008/past_icmls.shtml.html'),
|
||||||
|
'iros': ('IROS', 'https://www.ieee-ras.org/conferences-workshops/financially-co-sponsored/iros/iros-past-and-future-venues/'),
|
||||||
|
}
|
||||||
|
HISTORICAL_PRIMARY_TITLES = {
|
||||||
|
('icml', 1988): 'Machine Learning, Proceedings of the Fifth International Conference on Machine Learning, Ann Arbor, Michigan, USA, June 12-14, 1988',
|
||||||
|
('icml', 1989): 'Proceedings of the Sixth International Workshop on Machine Learning (ML 1989), Cornell University, Ithaca, New York, USA, June 26-27, 1989',
|
||||||
|
('icml', 1990): 'Machine Learning, Proceedings of the Seventh International Conference on Machine Learning, Austin, Texas, USA, June 21-23, 1990',
|
||||||
|
('icml', 1991): 'Proceedings of the Eighth International Workshop (ML91), Northwestern University, Evanston, Illinois, USA',
|
||||||
|
('icml', 1992): 'Proceedings of the Ninth International Workshop on Machine Learning (ML 1992), Aberdeen, Scotland, UK, July 1-3, 1992',
|
||||||
|
('iros', 1988): "IEEE International Workshop on Intelligent Robots and Systems '88, Proceedings. IROS 1988, Tokyo, Japan, October 31 - November 2, 1988",
|
||||||
|
('iros', 1989): "IEEE International Workshop on Intelligent Robots and Systems '89, Proceedings. IROS 1989, September 4-6, 1989, Tsukuba, Japan",
|
||||||
|
('iros', 1990): "IEEE International Workshop on Intelligent Robots and Systems '90, Towards a New Frontier of Applications, Proceedings. IROS 1990, July 3-6, 1990, Ibaraki, Japan",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def historical_track(family, base, suffix, volume_key, year, title):
|
def historical_track(family, base, suffix, volume_key, year, title):
|
||||||
|
primary = HISTORICAL_PRIMARY_EVIDENCE.get(family)
|
||||||
|
if (primary and not suffix and base.casefold() == primary[0].casefold()
|
||||||
|
and volume_key == f'conf/{family}/{year}'
|
||||||
|
and title == HISTORICAL_PRIMARY_TITLES.get((family, year))):
|
||||||
|
return ('main',
|
||||||
|
'Historical primary series; exact proceedings key, complete title and bibliographic/event year corroborated by official series history. Workshop in this original title is not an affiliated track.',
|
||||||
|
primary[1])
|
||||||
if suffix or base.casefold() != family or volume_key != f'conf/{family}/{year}':
|
if suffix or base.casefold() != family or volume_key != f'conf/{family}/{year}':
|
||||||
return None
|
return None
|
||||||
prefix = HISTORICAL_WORKSHOP_TITLES.get((family, year))
|
prefix = HISTORICAL_WORKSHOP_TITLES.get((family, year))
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
"""Publisher evidence, section boundaries and native-ingestion regression checks."""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
import sqlite3
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from paper_atlas.collectors import aamas
|
||||||
|
from paper_atlas.ingestion.records import ingest_file
|
||||||
|
|
||||||
|
|
||||||
|
def paper(pid, abstract=True):
|
||||||
|
summary = '<span class="abstract">Research with <i>evidence</i>.<br>More detail.</span>' if abstract else ''
|
||||||
|
return f'''<div class="paper" id="{pid}"><span class="id">{pid}</span>
|
||||||
|
<p class="paper"><span class="title"><a href="../papers/{pid}.pdf">{summary}
|
||||||
|
A structured & auditable paper {pid}</a></span><span class="authors">
|
||||||
|
<a href="authors.html#Jane">Jane Doe<span class="amount">has 7 papers</span></a>, Bob Smith
|
||||||
|
</span></p></div>'''
|
||||||
|
|
||||||
|
|
||||||
|
def page(section, pid, abstract=True):
|
||||||
|
heading = aamas.SECTIONS[section][0]
|
||||||
|
return (f'<html><head><title>{heading} - AAMAS 2012</title></head><body>'
|
||||||
|
f'<h1>{heading}</h1><h2 class="section" id="s-1A">Session 1A – Robotics I</h2>'
|
||||||
|
+paper(pid, abstract)+'</body></html>')
|
||||||
|
|
||||||
|
|
||||||
|
class AamasProceedingsTests(unittest.TestCase):
|
||||||
|
def snapshots(self, root):
|
||||||
|
root.mkdir()
|
||||||
|
receipts = []
|
||||||
|
items = {'introduction': ('<title>AAMAS 2012</title><p>Aggregate withdrawals do not identify papers.</p>', aamas.ROOT)}
|
||||||
|
for index, section in enumerate(aamas.SECTIONS):
|
||||||
|
items[section] = (page(section, f'P{index}_1', section != 'extabstracts'), aamas.ROOT+'resources/'+section+'.html')
|
||||||
|
for key, (html, url) in items.items():
|
||||||
|
data = html.encode()
|
||||||
|
(root/(key+'.html')).write_bytes(data)
|
||||||
|
receipts.append({'url': url, 'status': 200, 'file': key+'.html', 'bytes': len(data),
|
||||||
|
'sha256': hashlib.sha256(data).hexdigest(), 'finished_at': '2026-11-09T00:00:00+00:00'})
|
||||||
|
(root/'receipts.json').write_text(json.dumps({'receipts': receipts}))
|
||||||
|
return root
|
||||||
|
|
||||||
|
def test_hidden_abstract_and_author_badges_are_not_title_or_authors(self):
|
||||||
|
rows = aamas.parse_section(page('fullpapers', '1A_1'), 'fullpapers', '2026-11-09T00:00:00Z', {'sha256': 'proof'})
|
||||||
|
row, = rows
|
||||||
|
self.assertEqual(row['title'], 'A structured & auditable paper 1A_1')
|
||||||
|
self.assertEqual(row['abstract'], 'Research with evidence. More detail.')
|
||||||
|
self.assertEqual(row['authors'], ['Jane Doe', 'Bob Smith'])
|
||||||
|
self.assertEqual(row['metadata']['authors_display'], 'Jane Doe, Bob Smith')
|
||||||
|
self.assertEqual(row['url'], aamas.ROOT+'resources/fullpapers.html#1A_1')
|
||||||
|
self.assertEqual(row['metadata']['paper_url'], aamas.ROOT+'papers/1A_1.pdf')
|
||||||
|
self.assertEqual(row['metadata']['source_evidence'], {'sha256': 'proof'})
|
||||||
|
|
||||||
|
def test_publication_formats_and_session_topics_do_not_invent_submission_tracks(self):
|
||||||
|
for section, (_, fmt, track, _) in aamas.SECTIONS.items():
|
||||||
|
row, = aamas.parse_section(page(section, 'P_1', False), section, '2026-11-09T00:00:00Z', {})
|
||||||
|
self.assertEqual((row['track'], row['metadata']['publication_format']), (track, fmt))
|
||||||
|
self.assertIsNone(row['abstract'])
|
||||||
|
self.assertEqual(row['status'], 'accepted')
|
||||||
|
self.assertEqual(row['metadata']['decision_date_precision'], 'not_supplied')
|
||||||
|
self.assertEqual(row['metadata']['session']['title'], 'Session 1A – Robotics I')
|
||||||
|
self.assertIn({'namespace': 'aamas_proceedings:publication_format', 'value': fmt}, row['tags'])
|
||||||
|
self.assertEqual(aamas.SECTIONS['fullpapers'][2], 'proceedings_unspecified')
|
||||||
|
self.assertEqual(aamas.SECTIONS['extabstracts'][2], 'proceedings_unspecified')
|
||||||
|
|
||||||
|
def test_changed_edition_section_duplicate_id_or_pdf_link_is_rejected(self):
|
||||||
|
html = page('fullpapers', '1A_1')
|
||||||
|
for changed in [html.replace('AAMAS 2012', 'AAMAS 2025'),
|
||||||
|
html.replace('<h1>Full Papers</h1>', '<h1>Rejected Papers</h1>'),
|
||||||
|
html.replace('</body>', paper('1A_1')+'</body>'),
|
||||||
|
html.replace('../papers/1A_1.pdf', 'https://example.org/1A_1.pdf'),
|
||||||
|
html.replace('../papers/1A_1.pdf', '../papers/1A_2.pdf')]:
|
||||||
|
with self.subTest(changed=changed):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
aamas.parse_section(changed, 'fullpapers', '2026-11-09T00:00:00Z', {})
|
||||||
|
|
||||||
|
def test_frozen_responses_require_checksums_success_and_safe_paths(self):
|
||||||
|
for failure in ['checksum', 'status', 'path']:
|
||||||
|
with self.subTest(failure=failure), tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = self.snapshots(Path(temporary)/'source')
|
||||||
|
path = root/'receipts.json'
|
||||||
|
receipts = json.loads(path.read_text())
|
||||||
|
item = receipts['receipts'][0]
|
||||||
|
if failure == 'checksum': item['sha256'] = '0'*64
|
||||||
|
elif failure == 'status': item['status'] = 404
|
||||||
|
else: item['file'] = '../outside.html'
|
||||||
|
path.write_text(json.dumps(receipts))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
aamas.read_pages(snapshots=root)
|
||||||
|
|
||||||
|
def test_reviewed_section_counts_and_year_are_required(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
source = self.snapshots(root/'source')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
aamas.collect(2012, root/'out', snapshots=source)
|
||||||
|
self.assertFalse((root/'out/aamas-proceedings-2012.json').exists())
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
aamas.collect(2025, root/'future', snapshots=source)
|
||||||
|
self.assertFalse((root/'future').exists())
|
||||||
|
|
||||||
|
def test_ids_colliding_across_sections_do_not_produce_a_completed_collection(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
source = self.snapshots(root/'source')
|
||||||
|
receipts_path = source/'receipts.json'
|
||||||
|
receipts = json.loads(receipts_path.read_text())
|
||||||
|
for item in receipts['receipts'][1:]:
|
||||||
|
path = source/item['file']
|
||||||
|
data = re.sub(r'P[0-9]_1', 'SAME_1', path.read_text()).encode()
|
||||||
|
path.write_bytes(data)
|
||||||
|
item.update(bytes=len(data), sha256=hashlib.sha256(data).hexdigest())
|
||||||
|
receipts_path.write_text(json.dumps(receipts))
|
||||||
|
small = {key: (*value[:3], 1) for key, value in aamas.SECTIONS.items()}
|
||||||
|
with patch.dict(aamas.SECTIONS, small, clear=True), self.assertRaises(ValueError):
|
||||||
|
aamas.collect(2012, root/'out', snapshots=source)
|
||||||
|
self.assertFalse((root/'out/aamas-proceedings-2012.json').exists())
|
||||||
|
|
||||||
|
def test_collection_and_native_ingestion_preserve_formats_evidence_and_missing_abstracts(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
source = self.snapshots(root/'source')
|
||||||
|
small = {key: (*value[:3], 1) for key, value in aamas.SECTIONS.items()}
|
||||||
|
with patch.dict(aamas.SECTIONS, small, clear=True):
|
||||||
|
result = aamas.collect(2012, root/'out', snapshots=source)
|
||||||
|
self.assertEqual(result['records'], 4)
|
||||||
|
self.assertEqual(len(result['files']), 3)
|
||||||
|
self.assertFalse(result['database_written'])
|
||||||
|
self.assertFalse(result['pdfs_downloaded'])
|
||||||
|
db = root/'sample.sqlite'
|
||||||
|
for manifest in result['files']:
|
||||||
|
path = Path(manifest['file'])
|
||||||
|
self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), manifest['file_sha256'])
|
||||||
|
for raw in path.read_text().splitlines():
|
||||||
|
record = json.loads(raw)
|
||||||
|
evidence = record['metadata']['source_evidence']
|
||||||
|
self.assertEqual(hashlib.sha256((root/'out'/evidence['file']).read_bytes()).hexdigest(), evidence['sha256'])
|
||||||
|
ingest_file(db, path)
|
||||||
|
with sqlite3.connect(db) as connection:
|
||||||
|
self.assertEqual(connection.execute('SELECT count(*) FROM papers').fetchone()[0], 4)
|
||||||
|
self.assertEqual(connection.execute('SELECT count(*) FROM submissions').fetchone()[0], 4)
|
||||||
|
self.assertEqual(connection.execute("SELECT count(*) FROM decisions WHERE status='accepted'").fetchone()[0], 4)
|
||||||
|
self.assertEqual(connection.execute('SELECT count(*) FROM papers WHERE abstract IS NULL').fetchone()[0], 1)
|
||||||
|
metadata = [json.loads(r[0]) for r in connection.execute('SELECT metadata_json FROM source_records')]
|
||||||
|
self.assertEqual({r['publication_format'] for r in metadata}, {'full_paper','extended_abstract','demo','doctoral_abstract'})
|
||||||
|
namespaces = {r[0] for r in connection.execute('SELECT DISTINCT namespace FROM tags')}
|
||||||
|
self.assertEqual(namespaces, {'aamas_proceedings:publication_format', 'aamas_proceedings:session'})
|
||||||
|
self.assertEqual(connection.execute('PRAGMA foreign_key_check').fetchall(), [])
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""Regression coverage for primary series once named Workshop, with strict scope boundaries."""
|
||||||
|
from contextlib import closing
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sqlite3
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from paper_atlas.collectors.dblp_records import normalize_row
|
||||||
|
from paper_atlas.collectors.dblp_scope import scope
|
||||||
|
from paper_atlas.ingestion.records import ingest_file
|
||||||
|
|
||||||
|
ICML_HISTORY = 'https://icml.cc/Conferences/2008/past_icmls.shtml.html'
|
||||||
|
IROS_HISTORY = 'https://www.ieee-ras.org/conferences-workshops/financially-co-sponsored/iros/iros-past-and-future-venues/'
|
||||||
|
|
||||||
|
|
||||||
|
class HistoricalPrimarySeriesTests(unittest.TestCase):
|
||||||
|
def example(self, family, year, title, book=None, key=None):
|
||||||
|
key = key or f'conf/{family}/{year}'
|
||||||
|
book = book or ('ML' if family == 'icml' else 'IROS')
|
||||||
|
row = {'record_type':'inproceedings', 'key':f'conf/{family}/Paper{year}', 'publtype':None,
|
||||||
|
'fields':{'title':['An independently identified historical research paper'],
|
||||||
|
'author':['Jane Doe'], 'booktitle':[book], 'year':[str(year)],
|
||||||
|
'crossref':[key], 'ee':[f'https://doi.org/10.1234/{family}.{year}']}}
|
||||||
|
volume = {'record_type':'proceedings', 'key':key,
|
||||||
|
'fields':{'title':[title], 'year':[str(year)]}}
|
||||||
|
return row, volume
|
||||||
|
|
||||||
|
def test_icml_workshop_primary_editions_are_not_silently_filtered(self):
|
||||||
|
examples = {
|
||||||
|
1989:'Proceedings of the Sixth International Workshop on Machine Learning (ML 1989), Cornell University, Ithaca, New York, USA, June 26-27, 1989',
|
||||||
|
1991:'Proceedings of the Eighth International Workshop (ML91), Northwestern University, Evanston, Illinois, USA',
|
||||||
|
1992:'Proceedings of the Ninth International Workshop on Machine Learning (ML 1992), Aberdeen, Scotland, UK, July 1-3, 1992',
|
||||||
|
}
|
||||||
|
for year, title in examples.items():
|
||||||
|
with self.subTest(year=year):
|
||||||
|
row, volume = self.example('icml', year, title)
|
||||||
|
record, reason = normalize_row(row, {volume['key']:volume}, {'ICML'})
|
||||||
|
self.assertIsNone(reason)
|
||||||
|
self.assertEqual((record['venue'],record['year'],record['track'],record['status']), ('ICML',year,'main','accepted'))
|
||||||
|
self.assertEqual(record['metadata']['proceedings_title'], title)
|
||||||
|
self.assertEqual(record['metadata']['track_scope_evidence_url'], ICML_HISTORY)
|
||||||
|
self.assertEqual(record['metadata']['bibliographic_year'], year)
|
||||||
|
|
||||||
|
def test_iros_first_three_editions_are_primary_series(self):
|
||||||
|
examples = {
|
||||||
|
1988:"IEEE International Workshop on Intelligent Robots and Systems '88, Proceedings. IROS 1988, Tokyo, Japan, October 31 - November 2, 1988",
|
||||||
|
1989:"IEEE International Workshop on Intelligent Robots and Systems '89, Proceedings. IROS 1989, September 4-6, 1989, Tsukuba, Japan",
|
||||||
|
1990:"IEEE International Workshop on Intelligent Robots and Systems '90, Towards a New Frontier of Applications, Proceedings. IROS 1990, July 3-6, 1990, Ibaraki, Japan",
|
||||||
|
}
|
||||||
|
for year, title in examples.items():
|
||||||
|
with self.subTest(year=year):
|
||||||
|
row, volume = self.example('iros', year, title)
|
||||||
|
record, reason = normalize_row(row, {volume['key']:volume}, {'IROS'})
|
||||||
|
self.assertIsNone(reason)
|
||||||
|
self.assertEqual((record['venue'],record['year'],record['track']), ('IROS',year,'main'))
|
||||||
|
self.assertEqual(record['metadata']['proceedings_title'], title)
|
||||||
|
self.assertEqual(record['metadata']['track_scope_evidence_url'], IROS_HISTORY)
|
||||||
|
|
||||||
|
def test_main_named_icml_predecessors_also_carry_primary_scope_provenance(self):
|
||||||
|
examples = {
|
||||||
|
1988:'Machine Learning, Proceedings of the Fifth International Conference on Machine Learning, Ann Arbor, Michigan, USA, June 12-14, 1988',
|
||||||
|
1990:'Machine Learning, Proceedings of the Seventh International Conference on Machine Learning, Austin, Texas, USA, June 21-23, 1990',
|
||||||
|
}
|
||||||
|
for year, title in examples.items():
|
||||||
|
row, volume = self.example('icml', year, title)
|
||||||
|
edition, reason = scope(row, volume)
|
||||||
|
self.assertIsNone(reason)
|
||||||
|
self.assertEqual(edition['track'], 'main')
|
||||||
|
self.assertEqual(edition['track_scope_evidence_url'], ICML_HISTORY)
|
||||||
|
|
||||||
|
def test_wrong_keys_titles_suffixes_and_unknown_years_keep_workshop_boundary(self):
|
||||||
|
title = 'Proceedings of the Eighth International Workshop (ML91), Northwestern University, Evanston, Illinois, USA'
|
||||||
|
for year, changed, book, key in [
|
||||||
|
(1991,title,'ML','conf/icml/1991w'),
|
||||||
|
(1991,title,'ML (Workshops)','conf/icml/1991'),
|
||||||
|
(1991,title.replace('Eighth','Ninth'),'ML','conf/icml/1991'),
|
||||||
|
(1991,title + ', Affiliated Workshop','ML','conf/icml/1991'),
|
||||||
|
(1987,'Machine Learning Workshop 1987, Irvine','ML','conf/icml/1987'),
|
||||||
|
]:
|
||||||
|
with self.subTest(year=year, book=book, key=key, title=changed):
|
||||||
|
row, volume = self.example('icml',year,changed,book,key)
|
||||||
|
record, reason = normalize_row(row,{volume['key']:volume},{'ICML'})
|
||||||
|
self.assertIsNone(record)
|
||||||
|
self.assertEqual(reason,'separate_workshop')
|
||||||
|
self.assertNotIn('track_scope_evidence_url',scope(row,volume)[0])
|
||||||
|
title = "IEEE International Workshop on Intelligent Robots and Systems '88, Proceedings. IROS 1988, Tokyo, Japan, October 31 - November 2, 1988"
|
||||||
|
for changed, book, key in [
|
||||||
|
(title,'IROS','conf/iros/1988w'),
|
||||||
|
(title,'IROS (Workshops)','conf/iros/1988'),
|
||||||
|
(title.replace('Tokyo','Another city'),'IROS','conf/iros/1988'),
|
||||||
|
]:
|
||||||
|
row, volume = self.example('iros',1988,changed,book,key)
|
||||||
|
self.assertEqual(normalize_row(row,{volume['key']:volume},{'IROS'})[1],'separate_workshop')
|
||||||
|
|
||||||
|
def test_primary_scope_proof_survives_native_file_ingestion(self):
|
||||||
|
title = 'Proceedings of the Eighth International Workshop (ML91), Northwestern University, Evanston, Illinois, USA'
|
||||||
|
row, volume = self.example('icml',1991,title)
|
||||||
|
record, reason = normalize_row(row,{volume['key']:volume},{'ICML'})
|
||||||
|
self.assertIsNone(reason)
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root=Path(temporary);path=root/'records.jsonl';path.write_text(json.dumps(record)+'\n')
|
||||||
|
path.with_suffix('.manifest.json').write_text(json.dumps({'source':'dblp','count':1,'list_url':ICML_HISTORY,'file_sha256':hashlib.sha256(path.read_bytes()).hexdigest()}))
|
||||||
|
ingest_file(root/'test.sqlite',path)
|
||||||
|
with closing(sqlite3.connect(root/'test.sqlite')) as connection:
|
||||||
|
self.assertEqual(connection.execute('SELECT track FROM editions').fetchone()[0],'main')
|
||||||
|
self.assertEqual(connection.execute('SELECT status FROM decisions').fetchone()[0],'accepted')
|
||||||
|
metadata=json.loads(connection.execute('SELECT metadata_json FROM source_records').fetchone()[0])
|
||||||
|
self.assertEqual(metadata['proceedings_title'],title)
|
||||||
|
self.assertEqual(metadata['track_scope_evidence_url'],ICML_HISTORY)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user