Skip to content
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ import-lmfdb:

import-house-of-graphs:
python ./web/manage.py import_house_of_graphs

import-oeis:
python ./web/manage.py import_oeis
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Finally, populate the database.
```bash
make install-scispacy
```
then configure your email `WIKIPEDIA_CONTACT_EMAIL` in [source_wikidata.py](web/slurper/source_wikidata.py)
then configure your email `MATHSWITCH_CONTACT_EMAIL` in [source_wikidata.py](web/slurper/source_wikidata.py)
* This is needed
* Then run the database population (make sure your db is cleared)

Expand Down
2 changes: 1 addition & 1 deletion web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ SECRET_KEY="django-insecure-9wy9w#vf^tde0262doyy_j19=64c()_qub!1)f+fh-b^=7ndw*"

# Wikidata
WIKIDATA_PAGE_SIZE=1000
WIKIPEDIA_CONTACT_EMAIL=my@email.com
MATHSWITCH_CONTACT_EMAIL=my@email.com
IMPORT_MATH_ENTITIES=True
IMPORT_PHYSICS_ENTITIES=False

Expand Down
4 changes: 2 additions & 2 deletions web/categorizer/wikidata_fetch_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import requests
from concepts.models import Item

from web.settings import WIKIPEDIA_CONTACT_EMAIL
from web.settings import MATHSWITCH_CONTACT_EMAIL

WIKIDATA_API_URL = "https://www.wikidata.org/w/api.php"

Expand All @@ -27,7 +27,7 @@ def __init__(self):

def get_headers(self):
return {
"User-Agent": f"MathSwitch/1.0 ({WIKIPEDIA_CONTACT_EMAIL})",
"User-Agent": f"MathSwitch/1.0 ({MATHSWITCH_CONTACT_EMAIL})",
"Accept": "application/json",
}

Expand Down
32 changes: 32 additions & 0 deletions web/concepts/migrations/0024_alter_item_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.2.30 on 2026-09-21 17:36

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("concepts", "0023_alter_item_source"),
]

operations = [
migrations.AlterField(
model_name="item",
name="source",
field=models.CharField(
choices=[
("Wd", "Wikidata"),
("nL", "nLab"),
("MW", "MathWorld"),
("PW", "ProofWiki"),
("EoM", "Encyclopedia of Mathematics"),
("WpEN", "Wikipedia (English)"),
("AUm", "Agda Unimath"),
("LMF", "The L-functions and modular forms database"),
("HoG", "House of Graphs"),
("OEIS", "OEIS"),
],
max_length=4,
),
),
]
2 changes: 2 additions & 0 deletions web/concepts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class Source(models.TextChoices):
AGDA_UNIMATH = "AUm", "Agda Unimath"
LMFDB = "LMF", "The L-functions and modular forms database"
HOUSE_OF_GRAPHS = "HoG", "House of Graphs"
OEIS = "OEIS", "OEIS"

@staticmethod
def key():
Expand All @@ -118,6 +119,7 @@ def key():
Item.Source.AGDA_UNIMATH,
Item.Source.LMFDB,
Item.Source.HOUSE_OF_GRAPHS,
Item.Source.OEIS,
]
return lambda item: SOURCES.index(item.source)

Expand Down
1 change: 1 addition & 0 deletions web/concepts/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ <h2>About</h2>
<li><a href="https://unimath.github.io/agda-unimath/">Agda Unimath</a> ({{ number_of_links.agda_unimath }} entries)</li>
<li><a href="https://www.lmfdb.org/">LMFDB</a> ({{ number_of_links.lmfdb }} entries)</li>
<li><a href="https://houseofgraphs.org/">House of Graphs</a> ({{ number_of_links.house_of_graphs }} entries)</li>
<li><a href="https://oeis.org/">OEIS</a> ({{ number_of_links.oeis }} entries)</li>
</ul>
<p>The entries are organized into a concept network that connects the same concept appearing in different sources.</p>
<p>The team: <a href="https://katja.not.si">Katja Berčič</a> and Slobodan Stanojevikj.</p>
Expand Down
1 change: 1 addition & 0 deletions web/concepts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def home(request):
"agda_unimath": items.filter(source=Item.Source.AGDA_UNIMATH).count(),
"lmfdb": items.filter(source=Item.Source.LMFDB).count(),
"house_of_graphs": items.filter(source=Item.Source.HOUSE_OF_GRAPHS).count(),
"oeis": items.filter(source=Item.Source.OEIS).count(),
},
}
return render(request, "index.html", context)
Expand Down
46 changes: 46 additions & 0 deletions web/slurper/management/commands/clear_oeis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import logging
import sys
from datetime import timedelta

from concepts.models import Item
from django.core.management.base import BaseCommand
from slurper.models import SlurperRun

MIN_INTERVAL = timedelta(days=7)


class Command(BaseCommand):
help = (
"Delete all OEIS items. Guarded by a 7-day throttle; use --force to override."
)

def add_arguments(self, parser):
parser.add_argument(
"--force",
action="store_true",
help="Clear even if the OEIS slurper ran within the last 7 days.",
)

def handle(self, *args, force=False, **options):
source = Item.Source.OEIS
if not force and not SlurperRun.can_run(source, MIN_INTERVAL):
if sys.stdin.isatty():
answer = (
input(
f"OEIS slurper ran within the last {MIN_INTERVAL.days} days. "
f"Clear anyway? [y/N] "
)
.strip()
.lower()
)
if answer not in ("y", "yes"):
logging.info(f"[{source.label}] clear cancelled.")
return
else:
logging.info(
f"[{source.label}] clear skipped: ran less than "
f"{MIN_INTERVAL.days} days ago (use --force to override)."
)
return
deleted, _ = Item.objects.filter(source=source).delete()
logging.info(f"[{source.label}] cleared {deleted} items.")
14 changes: 14 additions & 0 deletions web/slurper/management/commands/import_oeis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django.core.management.base import BaseCommand
from slurper import source_oeis


class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"--force",
action="store_true",
help="Bypass the 7-day throttle and run anyway.",
)

def handle(self, *args, force=False, **options):
source_oeis.OEIS_SLURPER.save_items(force=force)
103 changes: 103 additions & 0 deletions web/slurper/source_oeis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
Slurper for the On-Line Encyclopedia of Integer Sequences (OEIS).

Sequence names are fetched from the OEIS names.gz dump. OEIS content is
licensed under CC-BY-SA-4.0 by The On-Line Encyclopedia of Integer
Sequences, https://oeis.org/.
"""

import gzip
import logging
from datetime import timedelta
from typing import Iterator, Optional, Tuple

import requests
from concepts.models import Item, Link
from slurper.models import SlurperRun

from web.settings import MATHSWITCH_CONTACT_EMAIL


class OeisSlurper:
NAMES_URL = "https://oeis.org/names.gz"
SEQUENCE_URL_PREFIX = "https://oeis.org/"
MIN_INTERVAL = timedelta(days=7)

def __init__(self):
self.source = Item.Source.OEIS

def fetch_names(self) -> bytes:
headers = {"User-Agent": f"MathSwitch/1.0 ({MATHSWITCH_CONTACT_EMAIL})"}
response = requests.get(self.NAMES_URL, headers=headers)
response.raise_for_status()
return gzip.decompress(response.content)

def parse_names(self, raw: bytes) -> Iterator[Tuple[str, str]]:
for line in raw.decode("utf-8", errors="replace").splitlines():
if not line or line.startswith("#"):
continue
identifier, _, description = line.partition(" ")
description = description.strip()
if not identifier or not description:
continue
yield identifier, description

def line_to_item(self, identifier: str, description: str) -> Item:
return Item(
source=self.source,
identifier=identifier,
url=self.SEQUENCE_URL_PREFIX + identifier,
name=identifier,
description=description,
)

def extract_candidate_name(self, description: str) -> Optional[str]:
if ":" not in description:
return None
candidate = description.split(":", 1)[0].strip()
return candidate or None

def save_items(self, force: bool = False):
if not force and not SlurperRun.can_run(self.source, self.MIN_INTERVAL):
logging.info(
f"[{self.source.label}] skipped: ran less than "
f"{self.MIN_INTERVAL.days} days ago (use --force to override)."
)
return
total_filled = 0
total_linked = 0
for identifier, description in self.parse_names(self.fetch_names()):
existing = Item.objects.filter(
source=self.source, identifier=identifier
).first()
if existing is not None:
existing.description = description
existing.save(update_fields=["description"])
total_filled += 1
continue

candidate_name = self.extract_candidate_name(description)
if candidate_name is None:
continue
matches = list(
Item.objects.exclude(source=self.source).filter(
name__iexact=candidate_name
)
)
if not matches:
continue

item = self.line_to_item(identifier, description)
item.save()
for match in matches:
Link.save_new(item, match, Link.Label.NAME_EQ)
total_linked += 1

SlurperRun.mark_ran(self.source)
logging.info(
f"[{self.source.label}] save_items finished: "
f"{total_filled} shells filled, {total_linked} NAME_EQ links created."
)


OEIS_SLURPER = OeisSlurper()
10 changes: 5 additions & 5 deletions web/slurper/source_wikidata.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from web.settings import (
IMPORT_MATH_ENTITIES,
IMPORT_PHYSICS_ENTITIES,
MATHSWITCH_CONTACT_EMAIL,
WIKIDATA_PAGE_SIZE,
WIKIPEDIA_CONTACT_EMAIL,
)

# Wikipedia API contact email (required by Wikipedia API guidelines)
Expand Down Expand Up @@ -233,7 +233,7 @@ def fetch_pages(self):

def get_headers(self):
return {
"User-Agent": f"MathSwitch/1.0 ({WIKIPEDIA_CONTACT_EMAIL})",
"User-Agent": f"MathSwitch/1.0 ({MATHSWITCH_CONTACT_EMAIL})",
"Accept": "application/json",
"Accept-Language": "en-US,en;q=0.9",
}
Expand All @@ -242,13 +242,13 @@ def fetch_article(self, json_item, index=None, total=None):
global _missing_email_logged

# Check if contact email is configured
if WIKIPEDIA_CONTACT_EMAIL is None:
if MATHSWITCH_CONTACT_EMAIL is None:
if not _missing_email_logged:
logging.log(
logging.WARNING,
"WIKIPEDIA_CONTACT_EMAIL is not set. "
"MATHSWITCH_CONTACT_EMAIL is not set. "
"Wikipedia article fetching is disabled. "
"Please set WIKIPEDIA_CONTACT_EMAIL at the top of "
"Please set MATHSWITCH_CONTACT_EMAIL at the top of "
"source_wikidata.py to enable article fetching.",
)
_missing_email_logged = True
Expand Down
Loading
Loading