wmlinksfromhell 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ """small Wikimedia-aware link tools for wikitext"""
2
+
3
+ from .cache import MetadataCache
4
+ from .destination import Destination
5
+ from .exceptions import CacheError, ConversionError, MetadataMissingError, ResolutionError, SourceResolutionError, WMLinkFromHellError, WMLinksFromHellError
6
+ from .metadata import InterwikiMap, MetadataStore
7
+ from .models import DestinationType, InterwikiEntry, InterwikiInfo, ResolutionStatus, SyntaxType, WikiInfo
8
+ from .nodes import WMLink
9
+ from .parser import WMCode, parse
10
+ from .pywikibot_support import parse_page
11
+ from .resolver import ResolutionResult, Resolver
12
+
13
+ # short names cover the usual case; longer names stay available too
14
+ Code = WMCode
15
+ Link = WMLink
16
+ Wiki = WikiInfo
17
+ Metadata = MetadataStore
18
+ Result = ResolutionResult
19
+ Error = WMLinksFromHellError
20
+
21
+
22
+ def wiki(dbname: str, metadata: MetadataStore = None) -> WikiInfo:
23
+ result = (metadata or MetadataStore()).wiki_by_dbname(dbname)
24
+ if result is None:
25
+ raise MetadataMissingError(f"no metadata known for dbname {dbname!r}")
26
+ return result
27
+
28
+
29
+ def interwiki(dbname: str, metadata: MetadataStore = None, source=None) -> InterwikiInfo:
30
+ result = (metadata or MetadataStore()).interwiki(dbname, source=source)
31
+ if result is None:
32
+ raise MetadataMissingError(f"no interwiki prefix known for dbname {dbname!r}")
33
+ return result
34
+
35
+
36
+ def url(dbname: str, title: str, metadata: MetadataStore = None, source=None) -> str:
37
+ return wiki(dbname, metadata=metadata).page_url(title, metadata=metadata, source=source)
38
+
39
+
40
+ def link(dbname: str, title: str, label: str = None, metadata: MetadataStore = None, source=None) -> str:
41
+ return wiki(dbname, metadata=metadata).link(title, label=label, metadata=metadata, source=source)
42
+
43
+
44
+ def resolve_url(value: str, source=None, metadata: MetadataStore = None) -> ResolutionResult:
45
+ return Resolver(metadata).resolve_url(value, source=source)
46
+
47
+
48
+ def resolve_interwiki(value: str, source=None, metadata: MetadataStore = None) -> ResolutionResult:
49
+ return Resolver(metadata).resolve_interwiki(value, source=source)
50
+
51
+
52
+ def to_url(destination: Destination, metadata: MetadataStore = None) -> str:
53
+ return Resolver(metadata).to_url(destination)
54
+
55
+
56
+ def to_interwiki(destination: Destination, source=None, metadata: MetadataStore = None) -> str:
57
+ return Resolver(metadata).to_interwiki(destination, source=source)
58
+
59
+
60
+ def to_local(destination: Destination, source=None, metadata: MetadataStore = None) -> str:
61
+ return Resolver(metadata).to_local(destination, source=source)
62
+
63
+ __version__ = "1.0.0"
64
+
65
+ __all__ = [
66
+ "parse", "parse_page", "resolve", "resolve_url", "resolve_interwiki", "to_url", "to_interwiki", "to_local", "wiki", "interwiki", "url", "link", "Code", "Link", "Wiki", "Metadata", "Result", "WMCode", "WMLink", "Resolver", "ResolutionResult", "MetadataStore", "MetadataCache",
67
+ "InterwikiMap", "InterwikiEntry", "InterwikiInfo", "Destination", "DestinationType", "ResolutionStatus", "SyntaxType", "WikiInfo",
68
+ "Error", "WMLinksFromHellError", "WMLinkFromHellError", "ConversionError", "MetadataMissingError", "CacheError", "SourceResolutionError", "ResolutionError",
69
+ ]
70
+
71
+
72
+ def resolve(value: str, source=None, metadata: MetadataStore = None) -> ResolutionResult:
73
+ return Resolver(metadata).resolve(value, source=source)
@@ -0,0 +1,92 @@
1
+ """small atomic JSON cache used for explicit Wikimedia metadata refreshes"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import tempfile
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from .constants import CACHE_SCHEMA_VERSION
13
+ from .exceptions import CacheError
14
+
15
+
16
+ class MetadataCache:
17
+ """persistent cache; a bad cache never destroys a good in-memory bootstrap"""
18
+
19
+ def __init__(self, path: Optional[os.PathLike] = None):
20
+ self.path = Path(path) if path is not None else self.default_path()
21
+
22
+ @staticmethod
23
+ def default_path() -> Path:
24
+ base = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
25
+ return Path(base) / "wmlinksfromhell" / "metadata.json"
26
+
27
+ def load(self) -> dict:
28
+ if not self.path.exists():
29
+ return self.empty()
30
+ try:
31
+ with self.path.open("r", encoding="utf-8") as fh:
32
+ data = json.load(fh)
33
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
34
+ self._quarantine()
35
+ return self.empty()
36
+ if not isinstance(data, dict) or data.get("schema_version") != CACHE_SCHEMA_VERSION:
37
+ self._quarantine()
38
+ return self.empty()
39
+ return data
40
+
41
+ def save(self, data: dict) -> None:
42
+ payload = dict(data)
43
+ payload["schema_version"] = CACHE_SCHEMA_VERSION
44
+ payload["updated_at"] = time.time()
45
+ self.path.parent.mkdir(parents=True, exist_ok=True)
46
+ fd, tmp = tempfile.mkstemp(dir=str(self.path.parent), prefix=".metadata-", suffix=".tmp")
47
+ try:
48
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
49
+ json.dump(payload, fh, ensure_ascii=False, indent=2, sort_keys=True)
50
+ fh.flush()
51
+ os.fsync(fh.fileno())
52
+ os.replace(tmp, self.path)
53
+ except OSError as exc:
54
+ try:
55
+ os.unlink(tmp)
56
+ except OSError:
57
+ pass
58
+ raise CacheError(f"couldn't write metadata cache to {self.path}") from exc
59
+
60
+ def is_expired(self, max_age_seconds: float) -> bool:
61
+ updated_at = self.load().get("updated_at")
62
+ return updated_at is None or (time.time() - updated_at) > max_age_seconds
63
+
64
+ def clear(self) -> None:
65
+ try:
66
+ self.path.unlink(missing_ok=True)
67
+ except OSError:
68
+ pass
69
+
70
+ def empty(self) -> dict:
71
+ return {
72
+ "schema_version": CACHE_SCHEMA_VERSION,
73
+ "updated_at": None,
74
+ "wikis": {},
75
+ "languages": {},
76
+ "interwiki_maps": {},
77
+ "global_interwikis": {},
78
+ "global_interwiki_lists": {},
79
+ "family_language_aliases": {},
80
+ "family_language_prefixes": {},
81
+ "namespaces": {},
82
+ "http": {},
83
+ }
84
+
85
+ def _quarantine(self) -> None:
86
+ try:
87
+ target = self.path.with_suffix(self.path.suffix + ".corrupt")
88
+ if target.exists():
89
+ target = self.path.with_suffix(self.path.suffix + f".{int(time.time())}.corrupt")
90
+ os.replace(self.path, target)
91
+ except OSError:
92
+ pass
@@ -0,0 +1,307 @@
1
+ """bootstrap data; live Wikimedia metadata can extend/override this"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .models import WikiInfo
6
+
7
+ CACHE_SCHEMA_VERSION = 2
8
+
9
+ LANGUAGES: dict[str, str] = {
10
+ "en": "English", "de": "German", "fr": "French", "es": "Spanish", "it": "Italian",
11
+ "pt": "Portuguese", "nl": "Dutch", "pl": "Polish", "ru": "Russian", "ja": "Japanese",
12
+ "zh": "Chinese", "hi": "Hindi", "ar": "Arabic", "sv": "Swedish", "fi": "Finnish",
13
+ "no": "Norwegian", "da": "Danish", "cs": "Czech", "uk": "Ukrainian", "he": "Hebrew",
14
+ "ko": "Korean", "id": "Indonesian", "tr": "Turkish", "os": "Ossetian", "mul": "Multilingual",
15
+ "az": "Azerbaijani", "ba": "Bashkir", "be": "Belarusian", "eo": "Esperanto", "hsb": "Upper Sorbian",
16
+ "lez": "Lezghian", "tg": "Tajik", "udm": "Udmurt", "ms": "Malay", "tk": "Turkmen",
17
+ "vls": "West Flemish", "dv": "Divehi", "ca": "Catalan", "fa": "Persian", "bn": "Bangla",
18
+ "el": "Greek", "hu": "Hungarian", "ro": "Romanian", "sk": "Slovak", "sl": "Slovenian",
19
+ "hr": "Croatian", "sr": "Serbian", "bg": "Bulgarian", "et": "Estonian", "lv": "Latvian",
20
+ "lt": "Lithuanian", "is": "Icelandic", "ga": "Irish", "cy": "Welsh", "af": "Afrikaans", "simple": "Simple English",
21
+ }
22
+
23
+ MULTILINGUAL_FAMILIES: dict[str, dict[str, str]] = {
24
+ "wikipedia": {"project": "wikipedia", "site_code": "wiki", "suffix": "wiki"},
25
+ "wiktionary": {"project": "wiktionary", "site_code": "wiktionary", "suffix": "wiktionary"},
26
+ "wikinews": {"project": "wikinews", "site_code": "wikinews", "suffix": "wikinews"},
27
+ "wikibooks": {"project": "wikibooks", "site_code": "wikibooks", "suffix": "wikibooks"},
28
+ "wikiquote": {"project": "wikiquote", "site_code": "wikiquote", "suffix": "wikiquote"},
29
+ "wikisource": {"project": "wikisource", "site_code": "wikisource", "suffix": "wikisource"},
30
+ "wikiversity": {"project": "wikiversity", "site_code": "wikiversity", "suffix": "wikiversity"},
31
+ "wikivoyage": {"project": "wikivoyage", "site_code": "wikivoyage", "suffix": "wikivoyage"},
32
+ }
33
+
34
+ BOOTSTRAP_LANGS_BY_FAMILY: dict[str, tuple[str, ...]] = {
35
+ "wikipedia": tuple(LANGUAGES) + ("simple",),
36
+ "wiktionary": ("en", "de", "fr", "it", "fa", "simple"),
37
+ "wikinews": ("en", "de"),
38
+ "wikibooks": ("en", "de", "nl", "it", "simple"),
39
+ "wikiquote": ("en", "de", "fr"),
40
+ "wikisource": ("en", "de", "mul", "az"),
41
+ "wikiversity": ("en", "de"),
42
+ "wikivoyage": ("en", "de", "it"),
43
+ }
44
+
45
+ SINGLE_WIKIS: dict[str, dict] = {
46
+ "commonswiki": dict(family="commons", project="commons", language="mul", hostname="commons.wikimedia.org", sitename="Wikimedia Commons"),
47
+ "mulwikisource": dict(family="wikisource", project="wikisource", language="mul", hostname="wikisource.org", sitename="Wikisource multilingual"),
48
+ "wikidatawiki": dict(family="wikidata", project="wikidata", language="mul", hostname="www.wikidata.org", sitename="Wikidata"),
49
+ "metawiki": dict(family="meta", project="meta", language="en", hostname="meta.wikimedia.org", sitename="Meta-Wiki"),
50
+ "mediawikiwiki": dict(family="mediawiki", project="mediawiki", language="en", hostname="www.mediawiki.org", sitename="MediaWiki.org"),
51
+ "specieswiki": dict(family="wikispecies", project="wikispecies", language="mul", hostname="species.wikimedia.org", sitename="Wikispecies"),
52
+ "incubatorwiki": dict(family="incubator", project="incubator", language="mul", hostname="incubator.wikimedia.org", sitename="Wikimedia Incubator"),
53
+ "foundationwiki": dict(family="foundation", project="foundation", language="en", hostname="foundation.wikimedia.org", sitename="Wikimedia Foundation Governance Wiki"),
54
+ "wikifunctionswiki": dict(family="wikifunctions", project="wikifunctions", language="mul", hostname="www.wikifunctions.org", sitename="Wikifunctions"),
55
+ "wikitechwiki": dict(family="wikitech", project="wikitech", language="en", hostname="wikitech.wikimedia.org", sitename="Wikitech"),
56
+ "strategywiki": dict(family="strategy", project="strategy", language="en", hostname="strategy.wikimedia.org", sitename="Wikimedia Strategic Planning"),
57
+ "testwiki": dict(family="test", project="test", language="en", hostname="test.wikipedia.org", sitename="Test Wikipedia"),
58
+ "betawikiversity": dict(family="wikiversity", project="wikiversity", language="en", hostname="beta.wikiversity.org", sitename="Beta Wikiversity"),
59
+ "arbcom_cswiki": dict(family="special", project="arbcom_cswiki", language="cs", hostname="arbcom-cs.wikipedia.org", sitename="Czech Wikipedia Arbitration Committee wiki"),
60
+ "arbcom_dewiki": dict(family="special", project="arbcom_dewiki", language="de", hostname="arbcom-de.wikipedia.org", sitename="German Wikipedia Arbitration Committee wiki"),
61
+ "arbcom_enwiki": dict(family="special", project="arbcom_enwiki", language="en", hostname="arbcom-en.wikipedia.org", sitename="English Wikipedia Arbitration Committee wiki"),
62
+ "arbcom_fiwiki": dict(family="special", project="arbcom_fiwiki", language="fi", hostname="arbcom-fi.wikipedia.org", sitename="Finnish Wikipedia Arbitration Committee wiki"),
63
+ "arbcom_itwiki": dict(family="special", project="arbcom_itwiki", language="it", hostname="wikipedia-it-arbcom.wikimedia.org", sitename="Italian Wikipedia Arbitration Committee wiki"),
64
+ "arbcom_nlwiki": dict(family="special", project="arbcom_nlwiki", language="nl", hostname="arbcom-nl.wikipedia.org", sitename="Dutch Wikipedia Arbitration Committee wiki"),
65
+ "arbcom_plwiki": dict(family="special", project="arbcom_plwiki", language="pl", hostname="wikipedia-pl-arbcom.wikimedia.org", sitename="Polish Wikipedia Arbitration Committee wiki"),
66
+ "arbcom_ruwiki": dict(family="special", project="arbcom_ruwiki", language="ru", hostname="arbcom-ru.wikipedia.org", sitename="Russian Wikipedia Arbitration Committee wiki"),
67
+ "arbcom_zhwiki": dict(family="special", project="arbcom_zhwiki", language="zh", hostname="wikipedia-zh-arbcom.wikimedia.org", sitename="Chinese Wikipedia Arbitration Committee wiki"),
68
+ "boardwiki": dict(family="special", project="boardwiki", language="en", hostname="board.wikimedia.org", sitename="Wikimedia Board wiki"),
69
+ "checkuserwiki": dict(family="special", project="checkuserwiki", language="en", hostname="checkuser.wikimedia.org", sitename="CheckUser wiki"),
70
+ "stewardwiki": dict(family="special", project="stewardwiki", language="en", hostname="steward.wikimedia.org", sitename="Steward wiki"),
71
+ "wikimaniateamwiki": dict(family="special", project="wikimaniateamwiki", language="en", hostname="wikimaniateam.wikimedia.org", sitename="Wikimania Team wiki"),
72
+ "arbcom_ukwiki": dict(family="special", project="arbcom_ukwiki", language="uk", hostname="wikipedia-uk-arbcom.wikimedia.org", sitename="Ukrainian Wikipedia Arbitration Committee wiki"),
73
+ }
74
+
75
+ COMMON_WIKIMEDIA_HOST_SUFFIXES = (
76
+ ".wikipedia.org", ".wiktionary.org", ".wikinews.org", ".wikibooks.org", ".wikiquote.org",
77
+ ".wikisource.org", ".wikiversity.org", ".wikivoyage.org", ".wikimedia.org", ".mediawiki.org",
78
+ ".wikifunctions.org", ".wikitech.org", ".toolforge.org",
79
+ )
80
+
81
+ # Short family prefixes are context-sensitive: they can carry the source
82
+ # language when switching to another Wikimedia project family.
83
+ FAMILY_PREFIXES = {
84
+ "w": "wikipedia",
85
+ "wikt": "wiktionary",
86
+ "n": "wikinews",
87
+ "b": "wikibooks",
88
+ "q": "wikiquote",
89
+ "s": "wikisource",
90
+ "v": "wikiversity",
91
+ "voy": "wikivoyage",
92
+ }
93
+
94
+ # Long project names are actual interwiki entries and normally point at the
95
+ # English project. Keep them separate from context-sensitive short prefixes.
96
+ SINGLE_WIKI_PREFIXES = {
97
+ "wikipedia": "enwiki",
98
+ "wiktionary": "enwiktionary",
99
+ "wikinews": "enwikinews",
100
+ "wikibooks": "enwikibooks",
101
+ "wikiquote": "enwikiquote",
102
+ "wikisource": "enwikisource",
103
+ "wikiversity": "enwikiversity",
104
+ "wikivoyage": "enwikivoyage",
105
+ "oldwikisource": "mulwikisource",
106
+ "c": "commonswiki",
107
+ "commons": "commonswiki",
108
+ "d": "wikidatawiki",
109
+ "wikidata": "wikidatawiki",
110
+ "m": "metawiki",
111
+ "meta": "metawiki",
112
+ "metawiki": "metawiki",
113
+ "metawikimedia": "metawiki",
114
+ "metawikipedia": "metawiki",
115
+ "mw": "mediawikiwiki",
116
+ "mediawikiwiki": "mediawikiwiki",
117
+ "species": "specieswiki",
118
+ "wikispecies": "specieswiki",
119
+ "incubator": "incubatorwiki",
120
+ "betawikiversity": "betawikiversity",
121
+ "wmf": "foundationwiki",
122
+ "foundation": "foundationwiki",
123
+ "wikimedia": "foundationwiki",
124
+ "f": "wikifunctionswiki",
125
+ "wikifunctions": "wikifunctionswiki",
126
+ "wikitech": "wikitechwiki",
127
+ "strategy": "strategywiki",
128
+ "testwiki": "testwiki",
129
+ }
130
+
131
+
132
+ FAMILY_LANGUAGE_ALIASES = {
133
+ ("wikiversity", "mul"): "betawikiversity",
134
+ }
135
+
136
+
137
+ # small bootstrap of Wikimedia global interwiki prefixes whose targets are
138
+ # wikimedia wikis. this is fallback prefix metadata only; explicit live
139
+ # interwiki metadata loaded by MetadataStore replaces/extends it.
140
+ WIKIMEDIA_GLOBAL_WIKI_PREFIXES = {
141
+ "arbcom-zh": ("https://wikipedia-zh-arbcom.wikimedia.org/wiki/$1", "arbcom_zhwiki"),
142
+ "abstract": ("https://abstract.wikipedia.org/wiki/$1", "abstractwiki"),
143
+ "advisory": ("https://advisory.wikimedia.org/wiki/$1", "advisorywiki"),
144
+ "betawikiversity": ("https://beta.wikiversity.org/wiki/$1", "betawikiversity"),
145
+ "nostalgia": ("https://nostalgia.wikipedia.org/wiki/$1", "nostalgiawiki"),
146
+ "tenwiki": ("https://ten.wikipedia.org/wiki/$1", "tenwiki"),
147
+ "test2wiki": ("https://test2.wikipedia.org/wiki/$1", "test2wiki"),
148
+ "testcommons": ("https://test-commons.wikimedia.org/wiki/$1", "testcommonswiki"),
149
+ "testwikidata": ("https://test.wikidata.org/wiki/$1", "testwikidatawiki"),
150
+ "wm2005": ("https://wikimania2005.wikimedia.org/wiki/$1", "wikimania2005wiki"),
151
+ "wm2006": ("https://wikimania2006.wikimedia.org/wiki/$1", "wikimania2006wiki"),
152
+ "wm2007": ("https://wikimania2007.wikimedia.org/wiki/$1", "wikimania2007wiki"),
153
+ "wm2008": ("https://wikimania2008.wikimedia.org/wiki/$1", "wikimania2008wiki"),
154
+ "wm2009": ("https://wikimania2009.wikimedia.org/wiki/$1", "wikimania2009wiki"),
155
+ "wm2010": ("https://wikimania2010.wikimedia.org/wiki/$1", "wikimania2010wiki"),
156
+ "wm2011": ("https://wikimania2011.wikimedia.org/wiki/$1", "wikimania2011wiki"),
157
+ "wm2012": ("https://wikimania2012.wikimedia.org/wiki/$1", "wikimania2012wiki"),
158
+ "wm2013": ("https://wikimania2013.wikimedia.org/wiki/$1", "wikimania2013wiki"),
159
+ "wm2014": ("https://wikimania2014.wikimedia.org/wiki/$1", "wikimania2014wiki"),
160
+ "wm2015": ("https://wikimania2015.wikimedia.org/wiki/$1", "wikimania2015wiki"),
161
+ "wm2016": ("https://wikimania2016.wikimedia.org/wiki/$1", "wikimania2016wiki"),
162
+ "wm2017": ("https://wikimania2017.wikimedia.org/wiki/$1", "wikimania2017wiki"),
163
+ "wm2018": ("https://wikimania2018.wikimedia.org/wiki/$1", "wikimania2018wiki"),
164
+ "wmteam": ("https://wikimaniateam.wikimedia.org/wiki/$1", "wikimaniateamwiki"),
165
+ "wmania": ("https://wikimania.wikimedia.org/wiki/$1", "wikimaniawiki"),
166
+ }
167
+
168
+ SERVICE_PREFIXES = {
169
+ "phab": dict(hostname="phabricator.wikimedia.org", name="Phabricator", destination_type="service", url="https://phabricator.wikimedia.org/$1"),
170
+ "phabricator": dict(hostname="phabricator.wikimedia.org", name="Phabricator", destination_type="service", url="https://phabricator.wikimedia.org/$1"),
171
+ "bugzilla": dict(hostname="bugzilla.wikimedia.org", name="Bugzilla", destination_type="service", url="https://bugzilla.wikimedia.org/show_bug.cgi?id=$1"),
172
+ "mediazilla": dict(hostname="bugzilla.wikimedia.org", name="Bugzilla", destination_type="service", url="https://bugzilla.wikimedia.org/show_bug.cgi?id=$1"),
173
+ "gerrit": dict(hostname="gerrit.wikimedia.org", name="Gerrit", destination_type="service", url="https://gerrit.wikimedia.org/$1"),
174
+ "tools": dict(hostname="toolforge.org", name="Toolforge", destination_type="tool", url="https://toolforge.org/$1"),
175
+ "toolforge": dict(hostname="toolforge.org", name="Toolforge", destination_type="tool", url="https://toolforge.org/$1"),
176
+ "tswiki": dict(hostname="wikitech.wikimedia.org", name="Toolforge wiki", destination_type="service", url="https://wikitech.wikimedia.org/wiki/$1"),
177
+ "sulutil": dict(hostname="meta.wikimedia.org", name="SUL utility", destination_type="service", url="https://meta.wikimedia.org/wiki/Special:CentralAuth/$1"),
178
+ "mail": dict(hostname="lists.wikimedia.org", name="Wikimedia mailing lists", destination_type="service", url="https://lists.wikimedia.org/$1"),
179
+ "mailarchive": dict(hostname="lists.wikimedia.org", name="Wikimedia mailing list archives", destination_type="service", url="https://lists.wikimedia.org/pipermail/$1"),
180
+ "outreach": dict(hostname="outreach.wikimedia.org", name="Outreach wiki", destination_type="service", url="https://outreach.wikimedia.org/wiki/$1"),
181
+ "otrs": dict(hostname="ticket.wikimedia.org", name="Volunteer Response Team", destination_type="service", url="https://ticket.wikimedia.org/otrs/index.pl?Action=AgentTicketZoom&TicketNumber=$1"),
182
+ "otrswiki": dict(hostname="vrt-wiki.wikimedia.org", name="OTRS wiki", destination_type="service", url="https://vrt-wiki.wikimedia.org/wiki/$1"),
183
+ "quality": dict(hostname="quality.wikimedia.org", name="Wikimedia quality initiative", destination_type="service", url="https://quality.wikimedia.org/wiki/$1"),
184
+ "spcom": dict(hostname="spcom.wikimedia.org", name="Wikimedia Ombuds/spam commission", destination_type="service", url="https://spcom.wikimedia.org/wiki/$1"),
185
+ "ticket": dict(hostname="ticket.wikimedia.org", name="Wikimedia ticketing system", destination_type="service", url="https://ticket.wikimedia.org/otrs/index.pl?Action=AgentTicketZoom&TicketNumber=$1"),
186
+ "svn": dict(hostname="svn.wikimedia.org", name="Wikimedia Subversion", destination_type="service", url="https://svn.wikimedia.org/$1"),
187
+ "rev": dict(hostname="www.mediawiki.org", name="MediaWiki revision service", destination_type="service", url="https://www.mediawiki.org/wiki/Special:Code/MediaWiki/$1"),
188
+ "wmania": dict(hostname="wikimania.wikimedia.org", name="Wikimania", destination_type="service", url="https://wikimania.wikimedia.org/wiki/$1"),
189
+ "wm2016": dict(hostname="wikimania2016.wikimedia.org", name="Wikimania 2016", destination_type="service", url="https://wikimania2016.wikimedia.org/wiki/$1"),
190
+ "wm2017": dict(hostname="wikimania2017.wikimedia.org", name="Wikimania 2017", destination_type="service", url="https://wikimania2017.wikimedia.org/wiki/$1"),
191
+ "download": dict(hostname="releases.wikimedia.org", name="Wikimedia releases", destination_type="service", url="https://releases.wikimedia.org/$1"),
192
+ "dbdump": dict(hostname="dumps.wikimedia.org", name="Wikimedia database dumps", destination_type="service", url="https://dumps.wikimedia.org/$1"),
193
+ }
194
+
195
+ CHAPTER_PREFIXES = {
196
+ "wmar": "Wikimedia Argentina", "wmau": "Wikimedia Australia", "wmat": "Wikimedia Austria", "wmbd": "Wikimedia Bangladesh",
197
+ "wmbe": "Wikimedia Belgium", "wmbr": "Wikimedia Brasil", "wmca": "Wikimedia Canada", "wmcz": "Wikimedia Czech Republic",
198
+ "wmdk": "Wikimedia Danmark", "wmde": "Wikimedia Deutschland", "wmfi": "Wikimedia Suomi", "wmhk": "Wikimedia Hong Kong",
199
+ "wmhu": "Wikimedia Hungary", "wmin": "Wikimedia India", "wmke": "Wikimedia Kenya", "wmid": "Wikimedia Indonesia", "wmil": "Wikimedia Israel",
200
+ "wmit": "Wikimedia Italia", "wmnl": "Wikimedia Nederland", "wmmk": "Wikimedia Macedonia", "wmno": "Wikimedia Norge",
201
+ "wmpl": "Wikimedia Polska", "wmru": "Wikimedia Russia", "wmrs": "Wikimedia Srbije", "wmes": "Wikimedia Espana",
202
+ "wmse": "Wikimedia Sverige", "wmch": "Wikimedia CH", "wmph": "Wiki Society of the Philippines", "wmtw": "Wikimedia Taiwan", "wmua": "Wikimedia Ukraine", "wmuk": "Wikimedia UK", "wmza": "Wikimedia South Africa",
203
+ }
204
+
205
+ CHAPTER_FIXED_URLS = {
206
+ "wmhk": "https://meta.wikimedia.org/wiki/Wikimedia_Hong_Kong",
207
+ "wmin": "https://meta.wikimedia.org/wiki/Wikimedia_India",
208
+ "wmke": "https://meta.wikimedia.org/wiki/Wikimedia_Kenya",
209
+ "wmph": "https://meta.wikimedia.org/wiki/Wiki_Society_of_the_Philippines",
210
+ "wmtw": "https://meta.wikimedia.org/wiki/Wikimedia_Taiwan",
211
+ "wmza": "https://meta.wikimedia.org/wiki/Wikimedia_South_Africa",
212
+ }
213
+
214
+ CHAPTER_HOSTS = {
215
+ "wmar": "www.wikimedia.org.ar", "wmau": "wikimedia.org.au", "wmat": "mitglieder.wikimedia.at", "wmbd": "bd.wikimedia.org",
216
+ "wmbe": "be.wikimedia.org", "wmbr": "br.wikimedia.org", "wmca": "ca.wikimedia.org", "wmcz": "www.wikimedia.cz",
217
+ "wmdk": "dk.wikimedia.org", "wmde": "wikimedia.de", "wmfi": "fi.wikimedia.org", "wmke": "meta.wikimedia.org", "wmhk": "meta.wikimedia.org",
218
+ "wmhu": "wikimedia.hu", "wmin": "meta.wikimedia.org", "wmid": "id.wikimedia.org", "wmil": "www.wikimedia.org.il",
219
+ "wmit": "wiki.wikimedia.it", "wmnl": "nl.wikimedia.org", "wmmk": "mk.wikimedia.org", "wmno": "no.wikimedia.org",
220
+ "wmpl": "pl.wikimedia.org", "wmru": "ru.wikimedia.org", "wmrs": "rs.wikimedia.org", "wmes": "www.wikimedia.es",
221
+ "wmse": "se.wikimedia.org", "wmch": "www.wikimedia.ch", "wmph": "meta.wikimedia.org", "wmtw": "meta.wikimedia.org", "wmua": "ua.wikimedia.org", "wmuk": "wikimedia.org.uk", "wmza": "meta.wikimedia.org",
222
+ }
223
+
224
+ COMMON_NAMESPACE_NAMES = frozenset({
225
+ "media", "special", "talk", "user", "user talk", "project", "project talk", "file", "image", "file talk", "image talk",
226
+ "mediawiki", "mediawiki talk", "template", "template talk", "help", "help talk", "category", "category talk", "module", "module talk",
227
+ "portal", "portal talk", "draft", "draft talk", "timedtext", "timedtext talk", "gadget", "gadget talk", "widget", "widget talk",
228
+ })
229
+
230
+ NAMESPACE_ALIASES = {"image": "file", "image talk": "file talk"}
231
+
232
+ PREFERRED_FAMILY_PREFIX = {
233
+ "wikipedia": "w", "wiktionary": "wikt", "wikinews": "n", "wikibooks": "b", "wikiquote": "q", "wikisource": "s", "wikiversity": "v", "wikivoyage": "voy",
234
+ }
235
+
236
+ SINGLE_WIKI_HOST_ALIASES = {
237
+ "wikidatawiki": ("wikidata.org",),
238
+ "metawiki": ("meta.wikipedia.org",),
239
+ "mediawikiwiki": ("mediawiki.org",),
240
+ "wikifunctionswiki": ("wikifunctions.org",),
241
+ }
242
+
243
+ WIKIMEDIA_PORTAL_HOSTS = {
244
+ "wikipedia.org",
245
+ "www.wikipedia.org",
246
+ }
247
+
248
+ PREFERRED_SERVICE_PREFIX = {
249
+ "Phabricator": "phab",
250
+ "Bugzilla": "bugzilla",
251
+ "Gerrit": "gerrit",
252
+ "Toolforge": "toolforge",
253
+ "Toolforge wiki": "tswiki",
254
+ "SUL utility": "sulutil",
255
+ "Wikimedia mailing lists": "mail",
256
+ "Wikimedia mailing list archives": "mailarchive",
257
+ "Outreach wiki": "outreach",
258
+ "Volunteer Response Team": "otrs",
259
+ "OTRS wiki": "otrswiki",
260
+ "Wikimedia quality initiative": "quality",
261
+ "Wikimedia Subversion": "svn",
262
+ "MediaWiki revision service": "rev",
263
+ "Wikimania": "wmania",
264
+ "Wikimedia releases": "download",
265
+ "Wikimedia database dumps": "dbdump",
266
+ }
267
+
268
+ PREFERRED_SINGLE_WIKI_PREFIX = {
269
+ "commonswiki": "c", "wikidatawiki": "d", "metawiki": "m", "mediawikiwiki": "mw", "specieswiki": "species", "incubatorwiki": "incubator",
270
+ "foundationwiki": "wmf", "wikifunctionswiki": "f", "wikitechwiki": "wikitech", "strategywiki": "strategy", "testwiki": "testwiki",
271
+ }
272
+
273
+
274
+ def _build_wikis() -> dict[str, WikiInfo]:
275
+ wikis: dict[str, WikiInfo] = {}
276
+ for family, langs in BOOTSTRAP_LANGS_BY_FAMILY.items():
277
+ data = MULTILINGUAL_FAMILIES[family]
278
+ for lang in langs:
279
+ dbname = f"{lang}{data['suffix']}"
280
+ wikis[dbname] = WikiInfo(
281
+ dbname=dbname,
282
+ family=family,
283
+ project=data["project"],
284
+ language=lang,
285
+ language_name=LANGUAGES.get(lang, lang),
286
+ hostname=f"{lang}.{family}.org",
287
+ site_code=data["site_code"],
288
+ is_multilingual=False,
289
+ is_standard_project=True,
290
+ )
291
+ for dbname, data in SINGLE_WIKIS.items():
292
+ wikis[dbname] = WikiInfo(
293
+ dbname=dbname,
294
+ family=data["family"],
295
+ project=data["project"],
296
+ language=data["language"],
297
+ language_name=LANGUAGES.get(data["language"], data["language"]),
298
+ hostname=data["hostname"],
299
+ sitename=data.get("sitename"),
300
+ is_multilingual=data["language"] == "mul",
301
+ is_standard_project=False,
302
+ )
303
+ return wikis
304
+
305
+
306
+ WIKIS = _build_wikis()
307
+ MULTILINGUAL_FAMILY_NAMES = frozenset(MULTILINGUAL_FAMILIES)
@@ -0,0 +1,125 @@
1
+ """semantic destinations independent of link syntax"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import json
7
+ import re
8
+ from typing import Optional
9
+
10
+ from .models import DestinationType
11
+
12
+ _WHITESPACE_RE = re.compile(r"[ \t_]+")
13
+
14
+
15
+ def normalize_title(title: str) -> str:
16
+ return _WHITESPACE_RE.sub(" ", title).strip()
17
+
18
+
19
+ def split_namespace(title: str, known_namespaces: frozenset[str]) -> tuple[Optional[str], str]:
20
+ if ":" not in title:
21
+ return None, title
22
+ head, rest = title.split(":", 1)
23
+ candidate = normalize_title(head).casefold()
24
+ if candidate not in known_namespaces:
25
+ return None, title
26
+ return head.strip(), rest.strip()
27
+
28
+
29
+ @dataclasses.dataclass(frozen=True, eq=False)
30
+ class Destination:
31
+ """a syntax-independent Wikimedia destination"""
32
+
33
+ destination_type: DestinationType
34
+ family: Optional[str] = None
35
+ project: Optional[str] = None
36
+ language: Optional[str] = None
37
+ dbname: Optional[str] = None
38
+ wikiid: Optional[str] = None
39
+ hostname: Optional[str] = None
40
+ title: Optional[str] = None
41
+ namespace: Optional[str] = None
42
+ fragment: Optional[str] = None
43
+ revision: Optional[int] = None
44
+ diff: Optional[object] = None
45
+ action: Optional[str] = None
46
+ special_page: Optional[str] = None
47
+ organization_name: Optional[str] = None
48
+ service_name: Optional[str] = None
49
+ query_parameters: tuple[tuple[str, str], ...] = ()
50
+ page_id: Optional[int] = None
51
+ canonical_url: Optional[str] = None
52
+ metadata_source: Optional[str] = None
53
+ is_multilingual: Optional[bool] = None
54
+ is_standard_project: Optional[bool] = None
55
+ is_wikimedia_project: Optional[bool] = None
56
+
57
+ def __eq__(self, other: object) -> bool:
58
+ if not isinstance(other, Destination):
59
+ return NotImplemented
60
+ return self.identity_key() == other.identity_key()
61
+
62
+ def __hash__(self) -> int:
63
+ return hash(self.identity_key())
64
+
65
+ def as_dict(self) -> dict:
66
+ data = dataclasses.asdict(self)
67
+ data["destination_type"] = self.destination_type.value
68
+ data["query_parameters"] = list(self.query_parameters)
69
+ return data
70
+
71
+ def as_json(self) -> str:
72
+ return json.dumps(self.as_dict(), sort_keys=True, ensure_ascii=False)
73
+
74
+ @property
75
+ def full_title(self) -> Optional[str]:
76
+ if self.title is None:
77
+ return None
78
+ return f"{self.namespace}:{self.title}" if self.namespace else self.title
79
+
80
+ def page_identity_key(self) -> tuple:
81
+ if self.destination_type in {DestinationType.EXTERNAL, DestinationType.UNKNOWN}:
82
+ return (self.destination_type, self.canonical_url or self.hostname or self.full_title)
83
+ base = (
84
+ self.destination_type,
85
+ self.dbname,
86
+ self.hostname if self.dbname is None else None,
87
+ self.organization_name,
88
+ self.service_name,
89
+ )
90
+ # a diff is its own operation target, even when it includes an oldid.
91
+ # don't collapse it to the permanent-link identity of that oldid.
92
+ if self.diff is not None:
93
+ return base + ("diff", self.revision, self.diff)
94
+ if self.revision is not None:
95
+ return base + ("revision", self.revision)
96
+ if self.page_id is not None:
97
+ return base + ("page_id", self.page_id)
98
+ return base + (normalize_title(self.full_title) if self.full_title else None,)
99
+
100
+ def identity_key(self) -> tuple:
101
+ """complete semantic identity, including fragment and operations."""
102
+ return self.page_identity_key() + (
103
+ self.fragment,
104
+ self.revision,
105
+ self.diff,
106
+ self.action,
107
+ self.query_parameters,
108
+ )
109
+
110
+ def same_page_as(self, other: "Destination") -> bool:
111
+ if not isinstance(other, Destination):
112
+ return NotImplemented
113
+ return self.page_identity_key() == other.page_identity_key()
114
+
115
+ @property
116
+ def is_revision(self) -> bool:
117
+ return self.revision is not None
118
+
119
+ @property
120
+ def is_diff(self) -> bool:
121
+ return self.diff is not None
122
+
123
+ @property
124
+ def is_action(self) -> bool:
125
+ return self.action is not None
@@ -0,0 +1,29 @@
1
+ """exceptions used when a requested operation cannot be completed"""
2
+
3
+
4
+ class WMLinksFromHellError(Exception):
5
+ """base exception for the package"""
6
+
7
+
8
+ class MetadataMissingError(WMLinksFromHellError):
9
+ """required Wikimedia metadata is not available"""
10
+
11
+
12
+ class ConversionError(WMLinksFromHellError):
13
+ """a destination cannot be safely rendered in the requested form"""
14
+
15
+
16
+ class CacheError(WMLinksFromHellError):
17
+ """the metadata cache could not be written or managed"""
18
+
19
+
20
+ class SourceResolutionError(WMLinksFromHellError):
21
+ """the supplied source wiki could not be identified"""
22
+
23
+
24
+ class ResolutionError(WMLinksFromHellError):
25
+ """strict resolution was requested but the result was not resolvable"""
26
+
27
+
28
+ # deprecated spelling kept for compatibility; use WMLinksFromHellError
29
+ WMLinkFromHellError = WMLinksFromHellError