wiktionary-api 0.1.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.
- wikt/__init__.py +11 -0
- wikt/client.py +754 -0
- wikt/py.typed +0 -0
- wikt/util.py +52 -0
- wiktionary_api-0.1.0.dist-info/METADATA +109 -0
- wiktionary_api-0.1.0.dist-info/RECORD +8 -0
- wiktionary_api-0.1.0.dist-info/WHEEL +4 -0
- wiktionary_api-0.1.0.dist-info/licenses/LICENSE +21 -0
wikt/__init__.py
ADDED
wikt/client.py
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
"""Python Wiktionary API.
|
|
2
|
+
|
|
3
|
+
Synchronous client for the English Wiktionary Wikimedia REST API
|
|
4
|
+
(``rest_v1``) plus Commons Core helpers for downloading media bytes.
|
|
5
|
+
|
|
6
|
+
OpenAPI spec: https://en.wiktionary.org/api/rest_v1/?spec
|
|
7
|
+
Interactive docs: https://en.wiktionary.org/api/rest_v1/#/
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# pylint: disable=too-many-lines,too-many-public-methods,too-many-arguments,too-many-positional-arguments
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from pprint import pprint
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
from . import util
|
|
19
|
+
|
|
20
|
+
_DEFAULT_USER_AGENT = (
|
|
21
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
22
|
+
"(KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_MATH_TYPES = frozenset({"tex", "inline-tex", "chem"})
|
|
26
|
+
_MATH_RENDER_FORMATS = frozenset({"svg", "mml", "png"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _pdf_url(base: str, title: str, pdf_format: str | None, pdf_type: str | None) -> str:
|
|
30
|
+
"""Build ``/page/pdf/{title}/{format}/{type}`` with validated options."""
|
|
31
|
+
if pdf_format is None:
|
|
32
|
+
format_ = "a4"
|
|
33
|
+
elif pdf_format in ["a4", "letter", "legal"]:
|
|
34
|
+
format_ = pdf_format
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError("Invalid format. Valid formats are 'a4', 'letter', 'legal'.")
|
|
37
|
+
|
|
38
|
+
if pdf_type is None:
|
|
39
|
+
type_ = "desktop"
|
|
40
|
+
elif pdf_type in ["desktop", "mobile"]:
|
|
41
|
+
type_ = pdf_type
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError("Invalid type. Valid types are 'desktop', 'mobile'.")
|
|
44
|
+
|
|
45
|
+
return f"{base}/page/pdf/{title}/{format_}/{type_}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _parse_media_types_filter(media_types_filter: str | list[str] | None) -> list[str] | None:
|
|
49
|
+
"""Normalize and validate a media-type filter (``image``/``audio``/``video``)."""
|
|
50
|
+
if not media_types_filter:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
if isinstance(media_types_filter, str):
|
|
54
|
+
valid_media_types = [media_types_filter.lower()]
|
|
55
|
+
else:
|
|
56
|
+
valid_media_types = [media_type.lower() for media_type in media_types_filter]
|
|
57
|
+
|
|
58
|
+
for media_type in valid_media_types:
|
|
59
|
+
if media_type not in ["image", "audio", "video"]:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Invalid media type '{media_type}'. Valid media types are "
|
|
62
|
+
"'image', 'audio', 'video'."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return valid_media_types
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _media_titles_to_fetch(media_file_list: list, valid_media_types: list[str] | None) -> list[str]:
|
|
69
|
+
"""Return media titles from a media-list payload, optionally filtered by type."""
|
|
70
|
+
titles = []
|
|
71
|
+
for item in media_file_list:
|
|
72
|
+
if valid_media_types and str(item["type"]).lower() not in valid_media_types:
|
|
73
|
+
continue
|
|
74
|
+
titles.append(item["title"])
|
|
75
|
+
return titles
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _optional_params(**kwargs) -> dict:
|
|
79
|
+
"""Drop ``None`` values so optional query params are omitted."""
|
|
80
|
+
return {key: value for key, value in kwargs.items() if value is not None}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _page_resource_url(base: str, resource: str, title: str, revision: int | None = None) -> str:
|
|
84
|
+
"""Build ``/page/{resource}/{title}[/revision]``."""
|
|
85
|
+
if revision is not None:
|
|
86
|
+
return f"{base}/page/{resource}/{title}/{revision}"
|
|
87
|
+
return f"{base}/page/{resource}/{title}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _transform_url(
|
|
91
|
+
base: str, source: str, target: str, title: str | None = None, revision: int | None = None
|
|
92
|
+
) -> str:
|
|
93
|
+
"""Build ``/transform/{source}/to/{target}[/title[/revision]]``."""
|
|
94
|
+
path = f"/transform/{source}/to/{target}"
|
|
95
|
+
if title is not None:
|
|
96
|
+
path += f"/{title}"
|
|
97
|
+
if revision is not None:
|
|
98
|
+
path += f"/{revision}"
|
|
99
|
+
return f"{base}{path}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _request_headers(
|
|
103
|
+
accept_language: str | None = None, if_match: str | None = None, output_mode: str | None = None
|
|
104
|
+
) -> dict:
|
|
105
|
+
"""Build optional request headers used by several REST routes."""
|
|
106
|
+
headers = {}
|
|
107
|
+
if accept_language is not None:
|
|
108
|
+
headers["Accept-Language"] = accept_language
|
|
109
|
+
if if_match is not None:
|
|
110
|
+
headers["If-Match"] = if_match
|
|
111
|
+
if output_mode is not None:
|
|
112
|
+
headers["output-mode"] = output_mode
|
|
113
|
+
return headers
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _transform_form(
|
|
117
|
+
wikitext: str | None = None,
|
|
118
|
+
html: str | None = None,
|
|
119
|
+
body_only: bool | None = None,
|
|
120
|
+
stash: bool | None = None,
|
|
121
|
+
) -> dict:
|
|
122
|
+
"""Build multipart/form fields for transform endpoints."""
|
|
123
|
+
data = {}
|
|
124
|
+
if wikitext is not None:
|
|
125
|
+
data["wikitext"] = wikitext
|
|
126
|
+
if html is not None:
|
|
127
|
+
data["html"] = html
|
|
128
|
+
if body_only is not None:
|
|
129
|
+
data["body_only"] = body_only
|
|
130
|
+
if stash is not None:
|
|
131
|
+
data["stash"] = stash
|
|
132
|
+
return data
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Wiktionary:
|
|
136
|
+
"""
|
|
137
|
+
Synchronous Python wrapper for the Wiktionary REST API.
|
|
138
|
+
|
|
139
|
+
Covers every path in the live OpenAPI spec at
|
|
140
|
+
``https://en.wiktionary.org/api/rest_v1/?spec``, plus PDF rendering
|
|
141
|
+
(available on the service but omitted from the published spec) and
|
|
142
|
+
Commons Core helpers for downloading media bytes.
|
|
143
|
+
|
|
144
|
+
Wikimedia requires a descriptive User-Agent with your application name
|
|
145
|
+
and a contact email or URL.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, app_name: str, contact_info: str, timeout: int = 30):
|
|
149
|
+
self.base = "https://en.wiktionary.org/api/rest_v1"
|
|
150
|
+
self.media_base = "https://api.wikimedia.org/core/v1/commons"
|
|
151
|
+
self.app_name = app_name
|
|
152
|
+
self.contact_info = contact_info
|
|
153
|
+
self.timeout = timeout
|
|
154
|
+
self.session = requests.Session()
|
|
155
|
+
self.session.headers.update({"User-Agent": _DEFAULT_USER_AGENT})
|
|
156
|
+
self.set_user_agent(self.app_name, self.contact_info)
|
|
157
|
+
|
|
158
|
+
def set_user_agent(self, app_name: str, contact_info: str) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Set the User-Agent header (required by Wikimedia API policy).
|
|
161
|
+
|
|
162
|
+
Parameters:
|
|
163
|
+
app_name: Application name.
|
|
164
|
+
contact_info: Email address or contact URL.
|
|
165
|
+
"""
|
|
166
|
+
self.session.headers.update({"User-Agent": f"{app_name} ({contact_info})"})
|
|
167
|
+
|
|
168
|
+
# ------------------------------------------------------------------
|
|
169
|
+
# Page content
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def page(self) -> list:
|
|
173
|
+
"""
|
|
174
|
+
List page-related API entry points.
|
|
175
|
+
|
|
176
|
+
Endpoint: ``GET /page/``
|
|
177
|
+
Stability: Stable
|
|
178
|
+
"""
|
|
179
|
+
response = self.session.get(f"{self.base}/page/", timeout=self.timeout)
|
|
180
|
+
return response.json()["items"]
|
|
181
|
+
|
|
182
|
+
def get_definition(self, term: str, redirect: bool | None = None) -> list:
|
|
183
|
+
"""
|
|
184
|
+
Get term definitions extracted from Wiktionary content.
|
|
185
|
+
|
|
186
|
+
Parameters:
|
|
187
|
+
term: Term to define.
|
|
188
|
+
redirect: If ``False``, redirect pages return HTTP 200 instead of 302.
|
|
189
|
+
|
|
190
|
+
Endpoint: ``GET /page/definition/{term}``
|
|
191
|
+
Stability: Stable
|
|
192
|
+
"""
|
|
193
|
+
url = f"{self.base}/page/definition/{term}"
|
|
194
|
+
response = self.session.get(
|
|
195
|
+
url,
|
|
196
|
+
params=_optional_params(redirect=redirect),
|
|
197
|
+
timeout=self.timeout,
|
|
198
|
+
)
|
|
199
|
+
return response.json()
|
|
200
|
+
|
|
201
|
+
def get_revision(self, title: str, revision: int | None = None) -> dict:
|
|
202
|
+
"""
|
|
203
|
+
Fetch revision metadata for a title.
|
|
204
|
+
|
|
205
|
+
Parameters:
|
|
206
|
+
title: Page title (underscores; percent-encode if needed).
|
|
207
|
+
revision: Optional specific revision ID. Latest if omitted.
|
|
208
|
+
|
|
209
|
+
Endpoint: ``GET /page/title/{title}/{revision}``
|
|
210
|
+
Stability: Stable
|
|
211
|
+
"""
|
|
212
|
+
if revision is not None:
|
|
213
|
+
url = f"{self.base}/page/title/{title}/{revision}"
|
|
214
|
+
else:
|
|
215
|
+
url = f"{self.base}/page/title/{title}"
|
|
216
|
+
|
|
217
|
+
items = self.session.get(url, timeout=self.timeout).json()["items"]
|
|
218
|
+
if items:
|
|
219
|
+
return items[0]
|
|
220
|
+
return {}
|
|
221
|
+
|
|
222
|
+
def get_html(
|
|
223
|
+
self,
|
|
224
|
+
title: str,
|
|
225
|
+
revision: int | None = None,
|
|
226
|
+
redirect: bool | None = None,
|
|
227
|
+
stash: bool | None = None,
|
|
228
|
+
accept_language: str | None = None,
|
|
229
|
+
) -> str:
|
|
230
|
+
"""
|
|
231
|
+
Get HTML for a title (optionally a specific revision).
|
|
232
|
+
|
|
233
|
+
Endpoint: ``GET /page/html/{title}/{revision}``
|
|
234
|
+
Stability: Stable
|
|
235
|
+
"""
|
|
236
|
+
url = _page_resource_url(self.base, "html", title, revision)
|
|
237
|
+
headers = _request_headers(accept_language=accept_language)
|
|
238
|
+
response = self.session.get(
|
|
239
|
+
url,
|
|
240
|
+
params=_optional_params(redirect=redirect, stash=stash),
|
|
241
|
+
headers=headers or None,
|
|
242
|
+
timeout=self.timeout,
|
|
243
|
+
)
|
|
244
|
+
return response.text
|
|
245
|
+
|
|
246
|
+
def get_lint(self, title: str, revision: int | None = None) -> dict | list:
|
|
247
|
+
"""
|
|
248
|
+
Get linter errors for a page.
|
|
249
|
+
|
|
250
|
+
Endpoint: ``GET /page/lint/{title}/{revision}``
|
|
251
|
+
Stability: Stable
|
|
252
|
+
"""
|
|
253
|
+
url = _page_resource_url(self.base, "lint", title, revision)
|
|
254
|
+
return self.session.get(url, timeout=self.timeout).json()
|
|
255
|
+
|
|
256
|
+
def get_mobile_html(
|
|
257
|
+
self, title: str, revision: int | None = None, redirect: bool | None = None
|
|
258
|
+
) -> str:
|
|
259
|
+
"""
|
|
260
|
+
Get page HTML optimized for mobile consumption.
|
|
261
|
+
|
|
262
|
+
Endpoint: ``GET /page/mobile-html/{title}/{revision}``
|
|
263
|
+
Stability: Stable
|
|
264
|
+
"""
|
|
265
|
+
url = _page_resource_url(self.base, "mobile-html", title, revision)
|
|
266
|
+
response = self.session.get(
|
|
267
|
+
url,
|
|
268
|
+
params=_optional_params(redirect=redirect),
|
|
269
|
+
timeout=self.timeout,
|
|
270
|
+
)
|
|
271
|
+
return response.text
|
|
272
|
+
|
|
273
|
+
def get_mobile_html_offline_resources(self, title: str, revision: int | None = None) -> dict:
|
|
274
|
+
"""
|
|
275
|
+
Get styles and scripts for offline mobile-html pages.
|
|
276
|
+
|
|
277
|
+
Endpoint: ``GET /page/mobile-html-offline-resources/{title}/{revision}``
|
|
278
|
+
Stability: Stable
|
|
279
|
+
"""
|
|
280
|
+
url = _page_resource_url(self.base, "mobile-html-offline-resources", title, revision)
|
|
281
|
+
return self.session.get(url, timeout=self.timeout).json()
|
|
282
|
+
|
|
283
|
+
def get_media_file_list(self, title: str, revision: int | None = None) -> list:
|
|
284
|
+
"""
|
|
285
|
+
List media items used on a wiki page (images, audio, video).
|
|
286
|
+
|
|
287
|
+
Endpoint: ``GET /page/media-list/{title}/{revision}``
|
|
288
|
+
Stability: Unstable
|
|
289
|
+
"""
|
|
290
|
+
url = _page_resource_url(self.base, "media-list", title, revision)
|
|
291
|
+
return self.session.get(url, timeout=self.timeout).json()["items"]
|
|
292
|
+
|
|
293
|
+
def get_pdf(
|
|
294
|
+
self, title: str, pdf_format: str | None = None, pdf_type: str | None = None
|
|
295
|
+
) -> bytes:
|
|
296
|
+
"""
|
|
297
|
+
Render a page as PDF bytes.
|
|
298
|
+
|
|
299
|
+
Parameters:
|
|
300
|
+
title: Page title.
|
|
301
|
+
pdf_format: ``a4`` (default), ``letter``, or ``legal``.
|
|
302
|
+
pdf_type: ``desktop`` (default) or ``mobile``.
|
|
303
|
+
|
|
304
|
+
Endpoint: ``GET /page/pdf/{title}/{format}/{type}``
|
|
305
|
+
Stability: Experimental (available on the service; not in OpenAPI ``?spec``)
|
|
306
|
+
"""
|
|
307
|
+
url = _pdf_url(self.base, title, pdf_format, pdf_type)
|
|
308
|
+
return self.session.get(url, timeout=self.timeout).content
|
|
309
|
+
|
|
310
|
+
def download_pdf(
|
|
311
|
+
self,
|
|
312
|
+
title: str,
|
|
313
|
+
pdf_format: str | None = None,
|
|
314
|
+
pdf_type: str | None = None,
|
|
315
|
+
output_name: str | None = None,
|
|
316
|
+
) -> bool:
|
|
317
|
+
"""
|
|
318
|
+
Download a PDF for ``title`` and save it to disk.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
``True`` on success, ``False`` if the download or save fails.
|
|
322
|
+
"""
|
|
323
|
+
output_name_ = output_name if output_name else f"{title}.pdf"
|
|
324
|
+
try:
|
|
325
|
+
pdf_bytes = self.get_pdf(title, pdf_format=pdf_format, pdf_type=pdf_type)
|
|
326
|
+
util.pdf_from_bytes(pdf_bytes, True, output_name_)
|
|
327
|
+
return True
|
|
328
|
+
except Exception as exc: # pylint: disable=broad-except
|
|
329
|
+
print(exc)
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
def get_media_files(
|
|
333
|
+
self,
|
|
334
|
+
title: str,
|
|
335
|
+
revision: int | None = None,
|
|
336
|
+
media_types_filter: str | list[str] | None = None,
|
|
337
|
+
) -> dict[str, bytes]:
|
|
338
|
+
"""
|
|
339
|
+
Download media file bytes used on a page via the Commons Core API.
|
|
340
|
+
|
|
341
|
+
Parameters:
|
|
342
|
+
title: Page title.
|
|
343
|
+
revision: Optional page revision.
|
|
344
|
+
media_types_filter: ``image``, ``audio``, ``video``, or a list of those.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Mapping of media file title → file bytes.
|
|
348
|
+
"""
|
|
349
|
+
media_file_list = self.get_media_file_list(title, revision)
|
|
350
|
+
valid_media_types = _parse_media_types_filter(media_types_filter)
|
|
351
|
+
titles = _media_titles_to_fetch(media_file_list, valid_media_types)
|
|
352
|
+
|
|
353
|
+
media_files: dict[str, bytes] = {}
|
|
354
|
+
for file_name in titles:
|
|
355
|
+
file_site = f"{self.media_base}/file/{file_name}"
|
|
356
|
+
file_url = self.session.get(file_site, timeout=self.timeout).json()["original"]["url"]
|
|
357
|
+
media_files[file_name] = self.session.get(file_url, timeout=self.timeout).content
|
|
358
|
+
return media_files
|
|
359
|
+
|
|
360
|
+
def download_media_files(
|
|
361
|
+
self,
|
|
362
|
+
title: str,
|
|
363
|
+
revision: int | None = None,
|
|
364
|
+
media_types_filter: str | list[str] | None = None,
|
|
365
|
+
download_path: str | Path | None = None,
|
|
366
|
+
) -> bool:
|
|
367
|
+
"""
|
|
368
|
+
Download media files for a page and write them to ``download_path``.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
``True`` on success, ``False`` on filesystem errors.
|
|
372
|
+
"""
|
|
373
|
+
media_files = self.get_media_files(title, revision, media_types_filter)
|
|
374
|
+
dest_dir = Path(download_path) if download_path is not None else Path()
|
|
375
|
+
try:
|
|
376
|
+
for file_name, file_content in media_files.items():
|
|
377
|
+
output_path = dest_dir / file_name.removeprefix("File:")
|
|
378
|
+
with open(output_path, "wb") as handle:
|
|
379
|
+
handle.write(file_content)
|
|
380
|
+
return True
|
|
381
|
+
except Exception as exc: # pylint: disable=broad-except
|
|
382
|
+
print(exc)
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
# ------------------------------------------------------------------
|
|
386
|
+
# Data / citation / mobile assets / reading lists
|
|
387
|
+
# ------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
def get_citation(self, format_: str, query: str, accept_language: str | None = None) -> dict:
|
|
390
|
+
"""
|
|
391
|
+
Get citation data for an article identifier.
|
|
392
|
+
|
|
393
|
+
Endpoint: ``GET /data/citation/{format}/{query}``
|
|
394
|
+
Stability: Stable
|
|
395
|
+
"""
|
|
396
|
+
url = f"{self.base}/data/citation/{format_}/{query}"
|
|
397
|
+
headers = _request_headers(accept_language=accept_language)
|
|
398
|
+
response = self.session.get(url, headers=headers or None, timeout=self.timeout)
|
|
399
|
+
return response.json()
|
|
400
|
+
|
|
401
|
+
def get_mobile_css(self, type_: str) -> str:
|
|
402
|
+
"""
|
|
403
|
+
Get CSS for mobile apps.
|
|
404
|
+
|
|
405
|
+
Endpoint: ``GET /data/css/mobile/{type}``
|
|
406
|
+
"""
|
|
407
|
+
response = self.session.get(f"{self.base}/data/css/mobile/{type_}", timeout=self.timeout)
|
|
408
|
+
return response.text
|
|
409
|
+
|
|
410
|
+
def get_mobile_javascript(self, type_: str) -> str:
|
|
411
|
+
"""
|
|
412
|
+
Get JavaScript for mobile apps.
|
|
413
|
+
|
|
414
|
+
Endpoint: ``GET /data/javascript/mobile/{type}``
|
|
415
|
+
"""
|
|
416
|
+
response = self.session.get(
|
|
417
|
+
f"{self.base}/data/javascript/mobile/{type_}", timeout=self.timeout
|
|
418
|
+
)
|
|
419
|
+
return response.text
|
|
420
|
+
|
|
421
|
+
def get_i18n(self, type_: str) -> dict:
|
|
422
|
+
"""
|
|
423
|
+
Get internationalization info.
|
|
424
|
+
|
|
425
|
+
Endpoint: ``GET /data/i18n/{type}``
|
|
426
|
+
"""
|
|
427
|
+
response = self.session.get(f"{self.base}/data/i18n/{type_}", timeout=self.timeout)
|
|
428
|
+
return response.json()
|
|
429
|
+
|
|
430
|
+
def get_lists(self, next_: str | None = None, sort: str | None = None) -> dict:
|
|
431
|
+
"""
|
|
432
|
+
Get all reading lists for the authenticated user.
|
|
433
|
+
|
|
434
|
+
Endpoint: ``GET /data/lists/``
|
|
435
|
+
"""
|
|
436
|
+
response = self.session.get(
|
|
437
|
+
f"{self.base}/data/lists/",
|
|
438
|
+
params=_optional_params(next=next_, sort=sort),
|
|
439
|
+
timeout=self.timeout,
|
|
440
|
+
)
|
|
441
|
+
return response.json()
|
|
442
|
+
|
|
443
|
+
def create_list(self, data: dict, csrf_token: str) -> dict:
|
|
444
|
+
"""
|
|
445
|
+
Create a reading list.
|
|
446
|
+
|
|
447
|
+
Endpoint: ``POST /data/lists/``
|
|
448
|
+
"""
|
|
449
|
+
response = self.session.post(
|
|
450
|
+
f"{self.base}/data/lists/",
|
|
451
|
+
params={"csrf_token": csrf_token},
|
|
452
|
+
json=data,
|
|
453
|
+
timeout=self.timeout,
|
|
454
|
+
)
|
|
455
|
+
return response.json()
|
|
456
|
+
|
|
457
|
+
def create_lists_batch(self, batch: list, csrf_token: str) -> dict:
|
|
458
|
+
"""
|
|
459
|
+
Create multiple reading lists.
|
|
460
|
+
|
|
461
|
+
Endpoint: ``POST /data/lists/batch``
|
|
462
|
+
"""
|
|
463
|
+
response = self.session.post(
|
|
464
|
+
f"{self.base}/data/lists/batch",
|
|
465
|
+
params={"csrf_token": csrf_token},
|
|
466
|
+
json={"batch": batch},
|
|
467
|
+
timeout=self.timeout,
|
|
468
|
+
)
|
|
469
|
+
return response.json()
|
|
470
|
+
|
|
471
|
+
def get_list_changes_since(self, date: str, next_: str | None = None) -> dict:
|
|
472
|
+
"""
|
|
473
|
+
Get recent changes to reading lists since ``date``.
|
|
474
|
+
|
|
475
|
+
Endpoint: ``GET /data/lists/changes/since/{date}``
|
|
476
|
+
"""
|
|
477
|
+
response = self.session.get(
|
|
478
|
+
f"{self.base}/data/lists/changes/since/{date}",
|
|
479
|
+
params=_optional_params(next=next_),
|
|
480
|
+
timeout=self.timeout,
|
|
481
|
+
)
|
|
482
|
+
return response.json()
|
|
483
|
+
|
|
484
|
+
def get_lists_for_page(self, project: str, title: str, next_: str | None = None) -> dict:
|
|
485
|
+
"""
|
|
486
|
+
Get lists that contain a given page.
|
|
487
|
+
|
|
488
|
+
Endpoint: ``GET /data/lists/pages/{project}/{title}``
|
|
489
|
+
"""
|
|
490
|
+
response = self.session.get(
|
|
491
|
+
f"{self.base}/data/lists/pages/{project}/{title}",
|
|
492
|
+
params=_optional_params(next=next_),
|
|
493
|
+
timeout=self.timeout,
|
|
494
|
+
)
|
|
495
|
+
return response.json()
|
|
496
|
+
|
|
497
|
+
def setup_lists(self, csrf_token: str) -> dict:
|
|
498
|
+
"""
|
|
499
|
+
Opt in to reading lists.
|
|
500
|
+
|
|
501
|
+
Endpoint: ``POST /data/lists/setup``
|
|
502
|
+
"""
|
|
503
|
+
response = self.session.post(
|
|
504
|
+
f"{self.base}/data/lists/setup",
|
|
505
|
+
params={"csrf_token": csrf_token},
|
|
506
|
+
timeout=self.timeout,
|
|
507
|
+
)
|
|
508
|
+
return response.json()
|
|
509
|
+
|
|
510
|
+
def teardown_lists(self, csrf_token: str) -> dict:
|
|
511
|
+
"""
|
|
512
|
+
Opt out of reading lists.
|
|
513
|
+
|
|
514
|
+
Endpoint: ``POST /data/lists/teardown``
|
|
515
|
+
"""
|
|
516
|
+
response = self.session.post(
|
|
517
|
+
f"{self.base}/data/lists/teardown",
|
|
518
|
+
params={"csrf_token": csrf_token},
|
|
519
|
+
timeout=self.timeout,
|
|
520
|
+
)
|
|
521
|
+
return response.json()
|
|
522
|
+
|
|
523
|
+
def update_list(self, list_id: int, data: dict, csrf_token: str) -> dict:
|
|
524
|
+
"""
|
|
525
|
+
Update a reading list.
|
|
526
|
+
|
|
527
|
+
Endpoint: ``PUT /data/lists/{id}``
|
|
528
|
+
"""
|
|
529
|
+
response = self.session.put(
|
|
530
|
+
f"{self.base}/data/lists/{list_id}",
|
|
531
|
+
params={"csrf_token": csrf_token},
|
|
532
|
+
json=data,
|
|
533
|
+
timeout=self.timeout,
|
|
534
|
+
)
|
|
535
|
+
return response.json()
|
|
536
|
+
|
|
537
|
+
def delete_list(self, list_id: int) -> dict:
|
|
538
|
+
"""
|
|
539
|
+
Delete a reading list.
|
|
540
|
+
|
|
541
|
+
Endpoint: ``DELETE /data/lists/{id}``
|
|
542
|
+
"""
|
|
543
|
+
response = self.session.delete(f"{self.base}/data/lists/{list_id}", timeout=self.timeout)
|
|
544
|
+
return response.json()
|
|
545
|
+
|
|
546
|
+
def get_list_entries(
|
|
547
|
+
self, list_id: int, next_: str | None = None, sort: str | None = None
|
|
548
|
+
) -> dict:
|
|
549
|
+
"""
|
|
550
|
+
Get all entries of a reading list.
|
|
551
|
+
|
|
552
|
+
Endpoint: ``GET /data/lists/{id}/entries/``
|
|
553
|
+
"""
|
|
554
|
+
response = self.session.get(
|
|
555
|
+
f"{self.base}/data/lists/{list_id}/entries/",
|
|
556
|
+
params=_optional_params(next=next_, sort=sort),
|
|
557
|
+
timeout=self.timeout,
|
|
558
|
+
)
|
|
559
|
+
return response.json()
|
|
560
|
+
|
|
561
|
+
def create_list_entry(self, list_id: int, data: dict, csrf_token: str) -> dict:
|
|
562
|
+
"""
|
|
563
|
+
Create a list entry.
|
|
564
|
+
|
|
565
|
+
Endpoint: ``POST /data/lists/{id}/entries/``
|
|
566
|
+
"""
|
|
567
|
+
response = self.session.post(
|
|
568
|
+
f"{self.base}/data/lists/{list_id}/entries/",
|
|
569
|
+
params={"csrf_token": csrf_token},
|
|
570
|
+
json=data,
|
|
571
|
+
timeout=self.timeout,
|
|
572
|
+
)
|
|
573
|
+
return response.json()
|
|
574
|
+
|
|
575
|
+
def create_list_entries_batch(self, list_id: int, batch: list, csrf_token: str) -> dict:
|
|
576
|
+
"""
|
|
577
|
+
Create multiple list entries.
|
|
578
|
+
|
|
579
|
+
Endpoint: ``POST /data/lists/{id}/entries/batch``
|
|
580
|
+
"""
|
|
581
|
+
response = self.session.post(
|
|
582
|
+
f"{self.base}/data/lists/{list_id}/entries/batch",
|
|
583
|
+
params={"csrf_token": csrf_token},
|
|
584
|
+
json={"batch": batch},
|
|
585
|
+
timeout=self.timeout,
|
|
586
|
+
)
|
|
587
|
+
return response.json()
|
|
588
|
+
|
|
589
|
+
def delete_list_entry(self, list_id: int, entry_id: int) -> dict:
|
|
590
|
+
"""
|
|
591
|
+
Delete a list entry.
|
|
592
|
+
|
|
593
|
+
Endpoint: ``DELETE /data/lists/{id}/entries/{entry_id}``
|
|
594
|
+
"""
|
|
595
|
+
response = self.session.delete(
|
|
596
|
+
f"{self.base}/data/lists/{list_id}/entries/{entry_id}",
|
|
597
|
+
timeout=self.timeout,
|
|
598
|
+
)
|
|
599
|
+
return response.json()
|
|
600
|
+
|
|
601
|
+
# ------------------------------------------------------------------
|
|
602
|
+
# Transforms
|
|
603
|
+
# ------------------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
def transform_wikitext_to_html(
|
|
606
|
+
self,
|
|
607
|
+
wikitext: str,
|
|
608
|
+
title: str | None = None,
|
|
609
|
+
revision: int | None = None,
|
|
610
|
+
body_only: bool | None = None,
|
|
611
|
+
stash: bool | None = None,
|
|
612
|
+
) -> str:
|
|
613
|
+
"""
|
|
614
|
+
Transform Wikitext to HTML.
|
|
615
|
+
|
|
616
|
+
Endpoint: ``POST /transform/wikitext/to/html[/{title}[/{revision}]]``
|
|
617
|
+
Stability: Stable
|
|
618
|
+
"""
|
|
619
|
+
url = _transform_url(self.base, "wikitext", "html", title, revision)
|
|
620
|
+
response = self.session.post(
|
|
621
|
+
url,
|
|
622
|
+
data=_transform_form(wikitext=wikitext, body_only=body_only, stash=stash),
|
|
623
|
+
timeout=self.timeout,
|
|
624
|
+
)
|
|
625
|
+
return response.text
|
|
626
|
+
|
|
627
|
+
def transform_html_to_wikitext(
|
|
628
|
+
self,
|
|
629
|
+
html: str,
|
|
630
|
+
title: str | None = None,
|
|
631
|
+
revision: int | None = None,
|
|
632
|
+
if_match: str | None = None,
|
|
633
|
+
) -> str:
|
|
634
|
+
"""
|
|
635
|
+
Transform HTML to Wikitext.
|
|
636
|
+
|
|
637
|
+
Endpoint: ``POST /transform/html/to/wikitext[/{title}[/{revision}]]``
|
|
638
|
+
Stability: Stable
|
|
639
|
+
"""
|
|
640
|
+
url = _transform_url(self.base, "html", "wikitext", title, revision)
|
|
641
|
+
headers = _request_headers(if_match=if_match)
|
|
642
|
+
response = self.session.post(
|
|
643
|
+
url,
|
|
644
|
+
data=_transform_form(html=html),
|
|
645
|
+
headers=headers or None,
|
|
646
|
+
timeout=self.timeout,
|
|
647
|
+
)
|
|
648
|
+
return response.text
|
|
649
|
+
|
|
650
|
+
def transform_wikitext_to_lint(
|
|
651
|
+
self, wikitext: str, title: str | None = None, revision: int | None = None
|
|
652
|
+
) -> dict | list:
|
|
653
|
+
"""
|
|
654
|
+
Check Wikitext for lint errors.
|
|
655
|
+
|
|
656
|
+
Endpoint: ``POST /transform/wikitext/to/lint[/{title}[/{revision}]]``
|
|
657
|
+
Stability: Stable
|
|
658
|
+
"""
|
|
659
|
+
url = _transform_url(self.base, "wikitext", "lint", title, revision)
|
|
660
|
+
response = self.session.post(
|
|
661
|
+
url,
|
|
662
|
+
data=_transform_form(wikitext=wikitext),
|
|
663
|
+
timeout=self.timeout,
|
|
664
|
+
)
|
|
665
|
+
return response.json()
|
|
666
|
+
|
|
667
|
+
def transform_wikitext_to_mobile_html(
|
|
668
|
+
self,
|
|
669
|
+
wikitext: str,
|
|
670
|
+
title: str,
|
|
671
|
+
accept_language: str | None = None,
|
|
672
|
+
output_mode: str | None = None,
|
|
673
|
+
) -> str:
|
|
674
|
+
"""
|
|
675
|
+
Transform Wikitext to mobile HTML.
|
|
676
|
+
|
|
677
|
+
Endpoint: ``POST /transform/wikitext/to/mobile-html/{title}``
|
|
678
|
+
Stability: Stable
|
|
679
|
+
"""
|
|
680
|
+
url = _transform_url(self.base, "wikitext", "mobile-html", title)
|
|
681
|
+
headers = _request_headers(accept_language=accept_language, output_mode=output_mode)
|
|
682
|
+
response = self.session.post(
|
|
683
|
+
url,
|
|
684
|
+
data=_transform_form(wikitext=wikitext),
|
|
685
|
+
headers=headers or None,
|
|
686
|
+
timeout=self.timeout,
|
|
687
|
+
)
|
|
688
|
+
return response.text
|
|
689
|
+
|
|
690
|
+
# ------------------------------------------------------------------
|
|
691
|
+
# Math
|
|
692
|
+
# ------------------------------------------------------------------
|
|
693
|
+
|
|
694
|
+
def check_math(self, type_: str, formula: str) -> dict:
|
|
695
|
+
"""
|
|
696
|
+
Check and normalize a TeX (or chem) formula.
|
|
697
|
+
|
|
698
|
+
The returned dict includes ``resource_location`` from the
|
|
699
|
+
``x-resource-location`` response header (hash for render/formula).
|
|
700
|
+
|
|
701
|
+
Endpoint: ``POST /media/math/check/{type}``
|
|
702
|
+
Stability: Deprecated on this host (prefer Math API on wikimedia.org)
|
|
703
|
+
"""
|
|
704
|
+
if type_ not in _MATH_TYPES:
|
|
705
|
+
raise ValueError(f"Invalid math type '{type_}'. Valid types are {sorted(_MATH_TYPES)}.")
|
|
706
|
+
|
|
707
|
+
response = self.session.post(
|
|
708
|
+
f"{self.base}/media/math/check/{type_}",
|
|
709
|
+
data={"q": formula},
|
|
710
|
+
timeout=self.timeout,
|
|
711
|
+
)
|
|
712
|
+
result = response.json()
|
|
713
|
+
result["resource_location"] = response.headers.get("x-resource-location")
|
|
714
|
+
return result
|
|
715
|
+
|
|
716
|
+
def get_math_formula(self, hash_: str) -> dict:
|
|
717
|
+
"""
|
|
718
|
+
Get a previously stored formula by hash.
|
|
719
|
+
|
|
720
|
+
Endpoint: ``GET /media/math/formula/{hash}``
|
|
721
|
+
Stability: Deprecated on this host
|
|
722
|
+
"""
|
|
723
|
+
response = self.session.get(f"{self.base}/media/math/formula/{hash_}", timeout=self.timeout)
|
|
724
|
+
return response.json()
|
|
725
|
+
|
|
726
|
+
def render_math(self, format_: str, hash_: str) -> bytes:
|
|
727
|
+
"""
|
|
728
|
+
Render a previously checked formula (``svg``, ``mml``, or ``png``).
|
|
729
|
+
|
|
730
|
+
Endpoint: ``GET /media/math/render/{format}/{hash}``
|
|
731
|
+
Stability: Deprecated on this host
|
|
732
|
+
"""
|
|
733
|
+
if format_ not in _MATH_RENDER_FORMATS:
|
|
734
|
+
raise ValueError(
|
|
735
|
+
f"Invalid render format '{format_}'. Valid formats are "
|
|
736
|
+
f"{sorted(_MATH_RENDER_FORMATS)}."
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
response = self.session.get(
|
|
740
|
+
f"{self.base}/media/math/render/{format_}/{hash_}",
|
|
741
|
+
timeout=self.timeout,
|
|
742
|
+
)
|
|
743
|
+
return response.content
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def main() -> None:
|
|
747
|
+
"""Demo entry point."""
|
|
748
|
+
wiki = Wiktionary("WiktionaryPy", "example@example.com")
|
|
749
|
+
pprint(wiki.get_definition("hello"))
|
|
750
|
+
pprint(dict(wiki.session.headers))
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
if __name__ == "__main__":
|
|
754
|
+
main()
|
wikt/py.typed
ADDED
|
File without changes
|
wikt/util.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Util functions for the Wiktionary API"""
|
|
2
|
+
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
|
|
5
|
+
from PyPDF2 import PdfReader, PdfWriter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def pdf_from_bytes(pdf_bytes: bytes, save: bool = False, output_file_path: str | None = None):
|
|
9
|
+
"""
|
|
10
|
+
Parse PDF bytes into a ``PdfWriter``, optionally saving to disk.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
pdf_bytes: Raw PDF payload (e.g. from ``Wiktionary.get_pdf``).
|
|
14
|
+
save: When ``True``, write the PDF to ``output_file_path``.
|
|
15
|
+
output_file_path: Destination path ending in ``.pdf`` (required if ``save``).
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
A ``PdfWriter`` with the copied pages, or ``None`` if parsing fails.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: If ``save`` is set without a valid ``.pdf`` output path.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if save and not output_file_path:
|
|
25
|
+
raise ValueError("save is true but output_file_path is not provided/empty")
|
|
26
|
+
|
|
27
|
+
if save and not output_file_path.endswith(".pdf"):
|
|
28
|
+
raise ValueError("output_file does not end with '.pdf'")
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
# Create an in-memory stream from the bytes
|
|
32
|
+
bytes_stream = BytesIO(pdf_bytes)
|
|
33
|
+
|
|
34
|
+
# Read the PDF from the stream
|
|
35
|
+
pdf_reader = PdfReader(bytes_stream)
|
|
36
|
+
|
|
37
|
+
# Create a new PDF writer
|
|
38
|
+
pdf_writer = PdfWriter()
|
|
39
|
+
|
|
40
|
+
# Add all pages from the reader to the writer
|
|
41
|
+
for page in pdf_reader.pages:
|
|
42
|
+
pdf_writer.add_page(page)
|
|
43
|
+
|
|
44
|
+
if save:
|
|
45
|
+
with open(output_file_path, "wb") as output_file:
|
|
46
|
+
pdf_writer.write(output_file)
|
|
47
|
+
|
|
48
|
+
return pdf_writer
|
|
49
|
+
|
|
50
|
+
except Exception as e: # pylint: disable=broad-except
|
|
51
|
+
print(f"Error creating PDF from bytes: {e}")
|
|
52
|
+
return None
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: wiktionary-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Synchronous Python client for the Wikimedia Wiktionary REST API
|
|
5
|
+
Project-URL: Homepage, https://github.com/NebilI/WiktionaryPythonAPI
|
|
6
|
+
Project-URL: Repository, https://github.com/NebilI/WiktionaryPythonAPI
|
|
7
|
+
Project-URL: Issues, https://github.com/NebilI/WiktionaryPythonAPI/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/NebilI/WiktionaryPythonAPI#readme
|
|
9
|
+
Author: Nebil Ibrahim
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: dictionary,requests,rest-api,wikimedia,wiktionary
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: pypdf2<4,>=3.0.1
|
|
26
|
+
Requires-Dist: requests<3,>=2.32
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# WiktionaryPythonAPI
|
|
34
|
+
|
|
35
|
+
Synchronous Python client for the [Wiktionary REST API](https://en.wiktionary.org/api/rest_v1/#/) and related [Commons Core](https://api.wikimedia.org/wiki/Core_REST_API) media downloads.
|
|
36
|
+
|
|
37
|
+
**PyPI:** `wiktionary-api` · **Import:** `wikt`
|
|
38
|
+
|
|
39
|
+
Covers **all 41** path patterns in the live OpenAPI spec (`?spec`), plus PDF rendering and Commons helpers. See [docs/ENDPOINTS.md](docs/ENDPOINTS.md) for the full path → method map.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
Works with any PEP 517 installer:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install wiktionary-api
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv add wiktionary-api
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
poetry add wiktionary-api
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Requires **Python 3.10+**.
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
Wikimedia requires a descriptive [User-Agent](https://foundation.wikimedia.org/wiki/Policy:Wikimedia_Foundation_User-Agent_Policy) with your application name and a contact email or URL.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from wikt import Wiktionary
|
|
65
|
+
|
|
66
|
+
wiki = Wiktionary("MyApp", "you@example.com")
|
|
67
|
+
|
|
68
|
+
# Definitions (Wiktionary-specific)
|
|
69
|
+
print(wiki.get_definition("hello"))
|
|
70
|
+
|
|
71
|
+
# HTML / revision metadata
|
|
72
|
+
print(wiki.get_html("hello")[:200])
|
|
73
|
+
print(wiki.get_revision("hello"))
|
|
74
|
+
|
|
75
|
+
# Math (check → render)
|
|
76
|
+
checked = wiki.check_math("tex", "E=mc^2")
|
|
77
|
+
svg = wiki.render_math("svg", checked["resource_location"])
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API surface (summary)
|
|
81
|
+
|
|
82
|
+
| Area | Examples |
|
|
83
|
+
|------|----------|
|
|
84
|
+
| Page content | `page`, `get_definition`, `get_html`, `get_lint`, `get_mobile_html`, `get_revision`, `get_media_file_list`, `get_pdf` |
|
|
85
|
+
| Data / mobile | `get_citation`, `get_mobile_css`, `get_mobile_javascript`, `get_i18n` |
|
|
86
|
+
| Reading lists | `get_lists`, `create_list`, `update_list`, `delete_list`, entry CRUD, setup/teardown |
|
|
87
|
+
| Transforms | `transform_wikitext_to_html`, `transform_html_to_wikitext`, `transform_wikitext_to_lint`, `transform_wikitext_to_mobile_html` |
|
|
88
|
+
| Math | `check_math`, `get_math_formula`, `render_math` |
|
|
89
|
+
| Commons helpers | `get_media_files`, `download_media_files` |
|
|
90
|
+
|
|
91
|
+
Method docstrings document parameters, return types, and REST paths. Official sandbox: https://en.wiktionary.org/api/rest_v1/#/
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install -e ".[dev]"
|
|
97
|
+
# or: uv sync --all-extras
|
|
98
|
+
ruff check src tests
|
|
99
|
+
ruff format src tests
|
|
100
|
+
pytest tests/ -v
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Publishing
|
|
104
|
+
|
|
105
|
+
Releases publish to PyPI on `v*` tags via trusted publishing. See [.github/PYPI_PUBLISHING.md](.github/PYPI_PUBLISHING.md).
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
wikt/__init__.py,sha256=J-_pjAD4mCWIAeIYc0yCNwREUyyjvPGPViQG1wg-V2g,202
|
|
2
|
+
wikt/client.py,sha256=ItO1HA5GDzaKdHIorKeU-N5gHnExhmgUX_tPLRNbrsU,25509
|
|
3
|
+
wikt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
wikt/util.py,sha256=s9Id49WZb7-PhdjnkbIB6e-vguQAPVTn50opUJtbesc,1622
|
|
5
|
+
wiktionary_api-0.1.0.dist-info/METADATA,sha256=xxEad86OFwhTApEipjy0NP5OA10SuTIjqYjkaQWBb-Q,3722
|
|
6
|
+
wiktionary_api-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
wiktionary_api-0.1.0.dist-info/licenses/LICENSE,sha256=2ZUv95E4ju6IepiCtjKlOhODaXbdqgf-W30wkXh4FmE,1070
|
|
8
|
+
wiktionary_api-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nebil Ibrahim
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|