├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ └── main.yaml ├── .gitignore ├── .readthedocs.yaml ├── .rstcheck.cfg ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTING.rst ├── LICENSE ├── README.rst ├── conftest.py ├── docs ├── Makefile ├── _static │ └── .gitkeep ├── changes.rst ├── conf.py ├── contributing.rst ├── index.rst ├── make.bat ├── wikidata.rst └── wikidata │ ├── cache.rst │ ├── client.rst │ ├── commonsmedia.rst │ ├── datavalue.rst │ ├── entity.rst │ ├── globecoordinate.rst │ ├── multilingual.rst │ └── quantity.rst ├── mypy.ini ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── cache_test.py ├── client_test.py ├── commonsmedia_test.py ├── conftest.py ├── datavalue_test.py ├── entity_test.py ├── fixtures │ ├── entities │ │ ├── P2003.json │ │ ├── P434.json │ │ ├── Q1299.json │ │ ├── Q16231742.json │ │ ├── Q20145.json │ │ ├── Q33281.json │ │ ├── Q494290.json │ │ └── Q8646.json │ └── media │ │ ├── 5834c5ab48506a503f290d49e3056427.json │ │ └── d7d79f7a3f939df88ff32571cbbeb9c3.json ├── globecoordinate_test.py ├── mock.py ├── multilingual_test.py └── quantity_test.py ├── tox.ini └── wikidata ├── __init__.py ├── cache.py ├── client.py ├── commonsmedia.py ├── datavalue.py ├── entity.py ├── globecoordinate.py ├── multilingual.py ├── py.typed └── quantity.py /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base:ubuntu 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y software-properties-common && \ 5 | add-apt-repository -y ppa:deadsnakes/ppa && \ 6 | rm -rf /var/lib/apt/lists/* 7 | 8 | RUN apt-get update && \ 9 | apt-get install -y \ 10 | pypy3 \ 11 | python3.8-full \ 12 | python3.9-full \ 13 | python3.10-full \ 14 | python3.11-full \ 15 | tox && \ 16 | rm -rf /var/lib/apt/lists/* -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Multiple Python versions", 3 | 4 | "build": { 5 | "dockerfile": "Dockerfile" 6 | }, 7 | 8 | // Configure tool-specific properties. 9 | "customizations": { 10 | "vscode": { 11 | "extensions": [ 12 | "ms-python.python", 13 | "lextudio.restructuredtext" 14 | ] 15 | } 16 | }, 17 | 18 | // Features to add to the dev container. More info: https://containers.dev/features. 19 | // "features": {}, 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | // "forwardPorts": [], 23 | 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | "postCreateCommand": "pip install -e .[dev]" 26 | 27 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 28 | // "remoteUser": "root" 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | strategy: 7 | matrix: 8 | python-version: 9 | - "3.11" 10 | - "3.10" 11 | - "3.9" 12 | - "3.8" 13 | - pypy3.9 14 | - pypy3.8 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - run: | 22 | python -m pip install -U pip 23 | python -m pip install tox tox-gh-actions 24 | - run: tox 25 | 26 | check: 27 | if: github.event_name == 'pull_request' 28 | runs-on: ubuntu-latest 29 | env: 30 | GITHUB_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} 31 | steps: 32 | - run: | 33 | mkdir -p "$HOME/bin" 34 | curl -L -o "$HOME/bin/checkmate" \ 35 | https://github.com/dahlia/checkmate/releases/download/0.4.0/checkmate-linux-x86_64 36 | chmod +x "$HOME/bin/checkmate" 37 | - run: | 38 | "$HOME/bin/checkmate" \ 39 | --token "$GITHUB_TOKEN" \ 40 | --login "$GITHUB_REPOSITORY_OWNER" \ 41 | --repo "${GITHUB_REPOSITORY#*/}" \ 42 | --pr "$GITHUB_PULL_REQUEST_NUMBER" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | .*.swp 4 | .cache 5 | .mypy_cache 6 | .pytest_cache 7 | .tox 8 | __pycache__ 9 | build 10 | dist 11 | docs/_build 12 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - method: pip 14 | path: . 15 | extra_requirements: 16 | - docs -------------------------------------------------------------------------------- /.rstcheck.cfg: -------------------------------------------------------------------------------- 1 | [rstcheck] 2 | ignore_directives=automodule 3 | ignore_roles=issue,pr 4 | report=error -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "lextudio.restructuredtext" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "files.insertFinalNewline": false, 4 | "files.trimFinalNewlines": true, 5 | "esbonio.sphinx.buildDir": "${workspaceFolder}/docs/_build", 6 | "esbonio.sphinx.confDir": "${workspaceFolder}/docs", 7 | "esbonio.sphinx.srcDir": "${workspaceFolder}/docs", 8 | "restructuredtext.linter.rstcheck.executablePath": "${workspaceFolder}/.tox/docs/bin/rstcheck" 9 | } 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | What branch to work on 5 | ---------------------- 6 | 7 | This project follows `Semantic Versioning`_, and every release is tagged. 8 | There are also branches looking like version numbers except of they have only 9 | one period (instead of two), e.g., *0.6*, *0.7*. These are maintenance 10 | branches. The branch named *main* [#]_ is for preparing next major version. 11 | 12 | If you send a patch to fix a bug your pull request usually should be based on 13 | the latest maintenance branch, not *main*. 14 | 15 | If you send a patch to add a new feature it should be based on 16 | the *main* branch. 17 | 18 | .. [#] We don't use the term *master*. For the rationale, please read 19 | the following discussion and articles: 20 | 21 | - https://lore.kernel.org/git/CAOAHyQwyXC1Z3v7BZAC+Bq6JBaM7FvBenA-1fcqeDV==apdWDg@mail.gmail.com/ 22 | - https://www.bbc.com/news/technology-53050955 23 | - https://sfconservancy.org/news/2020/jun/23/gitbranchname/ 24 | 25 | .. _Semantic Versioning: https://semver.org/ 26 | 27 | 28 | How to run tests 29 | ---------------- 30 | 31 | As this project supports various Python interpreters (CPython and PyPy) and 32 | versions, to ensure it works well with them, we use tox_. You don't need to 33 | create a virtual environment by yourself. ``tox`` automatically creates 34 | virtual environments for various Python versions and run the same test suite 35 | on all of them. 36 | 37 | The easiest to install ``tox`` is to use ``pip`` [#]_:: 38 | 39 | pip install tox 40 | 41 | Once you've installed ``tox``, it's very simple to run the test suite on 42 | all Python versions this project aims to support:: 43 | 44 | tox 45 | 46 | Note that you need to install Python interpreters besides ``tox``. 47 | If you don't want to install all of them use ``--skip-missing-interpreters`` 48 | option:: 49 | 50 | tox --skip-missing-interpreters 51 | 52 | To run tests on multiple interpreters at a time, use ``--parallel`` option:: 53 | 54 | tox --parallel 55 | 56 | .. [#] See also the `tox's official docs`__. 57 | .. _tox: https://tox.readthedocs.io/ 58 | __ https://tox.readthedocs.io/en/latest/install.html 59 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Wikidata_ client library for Python 2 | =================================== 3 | 4 | .. image:: https://badge.fury.io/py/Wikidata.svg 5 | :target: https://pypi.org/project/Wikidata/ 6 | :alt: Latest PyPI version 7 | 8 | .. image:: https://readthedocs.org/projects/wikidata/badge/?version=latest 9 | :target: https://wikidata.readthedocs.io/ 10 | :alt: Documentation Status 11 | 12 | .. image:: https://github.com/dahlia/wikidata/actions/workflows/main.yaml/badge.svg?branch=main 13 | :alt: GitHub Actions 14 | :target: https://github.com/dahlia/wikidata/actions/workflows/main.yaml 15 | 16 | This package provides easy APIs to use Wikidata_ for Python. 17 | 18 | >>> from wikidata.client import Client 19 | >>> client = Client() # doctest: +SKIP 20 | >>> entity = client.get('Q20145', load=True) 21 | >>> entity 22 | 23 | >>> entity.description 24 | m'South Korean singer and actress' 25 | >>> image_prop = client.get('P18') 26 | >>> image = entity[image_prop] 27 | >>> image 28 | 29 | >>> image.image_resolution 30 | (820, 1122) 31 | >>> image.image_url 32 | 'https://upload.wikimedia.org/wikipedia/commons/6/60/KBS_%22The_Producers%22_press_conference%2C_11_May_2015_10.jpg' 33 | 34 | .. _Wikidata: https://www.wikidata.org/ 35 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | """pytest conftest.py for doctests""" 2 | from tests.conftest import * # noqa 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Wikidata.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Wikidata.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Wikidata" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Wikidata" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dahlia/wikidata/0a3e0eccef8f78c395e7ed8a258fcbe13e1752fc/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version 0.9.0 5 | ------------- 6 | 7 | To be released. 8 | 9 | 10 | Version 0.8.0 & 0.8.1 11 | --------------------- 12 | 13 | Released on July 7, 2024. 14 | 15 | - Python 3.4--3.7 are no more supported. The minimum supported Python version 16 | is Python 3.8. Instead, now it's tested with Python 3.8--3.11. 17 | - :class:`~wikidata.entity.Entity` and :class:`~wikidata.client.Client` became 18 | possible to be serialized using :mod:`pickle`. [:issue:`31`] 19 | - Fixed a typing bug that :attr:`Entity.label ` 20 | and :attr:`Entity.description ` properties 21 | were incorrectly typed. 22 | - :class:`wikidata.multilingual.MultilingualText`'s constructor became to take 23 | only :class:`Locale` for parameter ``locale``. 24 | - Added date precision 7 in :class:`wikidata.datavalue.decoder`. 25 | [:pr:`59` by Baptiste Bayche] 26 | - Added date precision 10 in :class:`wikidata.datavalue.decoder`. 27 | [:pr:`60` by David Doukhan] 28 | 29 | 30 | Version 0.7.0 31 | ------------- 32 | 33 | Released on July 31, 2020. 34 | 35 | - Marked the package as supporting type checking by following :pep:`561`. 36 | 37 | - Now non-existent entities became able to be handled. [:pr:`11`] 38 | 39 | - Added :class:`~wikidata.entity.EntityState` enum class. 40 | - Added :attr:`Entity.state ` attribute. 41 | - Fixed a bug that raised :exc:`~urllib.error.HTTPError` when 42 | non-existent :class:`~wikidata.entity.Entity` was requested. 43 | 44 | - Languages (locales) became no more represented as :class:`babel.core.Locale`, 45 | but represented :class:`wikidata.multilingual.Locale` instead. 46 | [:issue:`2`, :issue:`27`, :pr:`30` by Nelson Liu] 47 | 48 | - Removed Babel_ from the dependencies. 49 | 50 | - Added :class:`wikidata.multilingual.Locale` type. 51 | 52 | To replace the :class:`babel.core.Locale` type, 53 | the :class:`wikidata.multilingual.Locale` type has been 54 | aliased to `str`. This is a *breaking change* for all Wikidata public API 55 | functions that formerly returned or ingested :class:`babel.core.Locale` . 56 | 57 | - Added support for ``time`` datatypes with precision 9 (year-only). 58 | [:pr:`26` by Nelson Liu] 59 | 60 | - Added support for globe coordinate datatype. [:pr:`28` by Nelson Liu] 61 | 62 | - Added support for decoding the ``globe-coordinate`` datatype. 63 | - Added :mod:`wikidata.globecoordinate` module. 64 | 65 | - Added support for quantity datatype. [:pr:`29` by Nelson Liu] 66 | 67 | - Added support for decoding the ``quantity`` datatype. 68 | - Added :mod:`wikidata.quantity` module. [:pr:`29`] 69 | 70 | - Fixed :exc:`KeyError` from :meth:`Entity.getlist() 71 | ` if the property is explicitly associated 72 | with "no value". [:issue:`18`] 73 | 74 | - Fixed a bug that raised :exc:`KeyError` when accessing an image more than 75 | once and :class:`~wikidata.cache.MemoryCachePolicy` was enabled. 76 | [:pr:`24` by Héctor Cordobés] 77 | 78 | 79 | Version 0.6.1 80 | ------------- 81 | 82 | Released on September 18, 2017. 83 | 84 | - Fixed :exc:`ImportError` on Python 3.4 due to lack of :mod:`typing` module. 85 | [:issue:`4`] 86 | 87 | 88 | Version 0.6.0 89 | ------------- 90 | 91 | Released on September 12, 2017. 92 | 93 | - Fixed :exc:`KeyError` from :meth:`Client.get() ` 94 | on an entity is redirected to its canonical entity. 95 | 96 | 97 | Version 0.5.4 98 | ------------- 99 | 100 | Released on September 18, 2017. 101 | 102 | - Fixed :exc:`ImportError` on Python 3.4 due to lack of :mod:`typing` module. 103 | [:issue:`4`] 104 | 105 | 106 | Version 0.5.3 107 | ------------- 108 | 109 | Released on June 30, 2017. 110 | 111 | - Fixed :exc:`ValueError` from :attr:`Entity.label 112 | `/:attr:`Entity.description 113 | ` with languages `ISO 639-1`_ 114 | doesn't cover (e.g. ``cbk-zam``). [:issue:`2`] 115 | 116 | Although this fix prevents these properties from raising :exc:`ValueError`, 117 | it doesn't completely fix the problem. :class:`babel.core.Locale` type, 118 | which Wikidata depends on, currently doesn't supprot languages other 119 | than `ISO 639-1`_. In order to completely fix the problem, we need to 120 | patch Babel_ to support them, or make Wikidata independent from Babel_. 121 | 122 | .. _ISO 639-1: https://www.iso.org/standard/22109.html 123 | .. _Babel: http://babel.pocoo.org/ 124 | 125 | 126 | Version 0.5.2 127 | ------------- 128 | 129 | Released on June 28, 2017. 130 | 131 | - Fixed :exc:`AssertionError` from empty 132 | :class:`~wikidata.entity.multilingual_attribute`\ s. 133 | 134 | 135 | Version 0.5.1 136 | ------------- 137 | 138 | Released on June 28, 2017. 139 | 140 | - Fixed :exc:`AssertionError` from :func:`len()` or iterating (:func:`iter()`) 141 | on :class:`~wikidata.entity.Entity` objects with empty claims. 142 | 143 | 144 | Version 0.5.0 145 | ------------- 146 | 147 | Released on June 13, 2017. 148 | 149 | - Wikidata API calls over network became possible to be cached. 150 | 151 | - :class:`~wikidata.client.Client` now has 152 | :attr:`~wikidata.client.Client.cache_policy` attribute and constructor 153 | option. Nothing is cached by default. 154 | 155 | - Added :mod:`wikidata.cache` module and :class:`~wikidata.cache.CachePolicy` 156 | interface in it. Two built-in implementation of the interface were added: 157 | 158 | :class:`~wikidata.cache.NullCachePolicy` 159 | No-op. 160 | 161 | :class:`~wikidata.cache.MemoryCachePolicy` 162 | LRU cache in memory. 163 | 164 | :class:`~wikidata.cache.ProxyCachePolicy` 165 | Proxy/adapter to another proxy object. Useful for utilizing third-party 166 | cache libraries. 167 | 168 | - ``wikidata.client.Client.request`` logger became to record logs about 169 | cache hits as :const:`~logging.DEBUG` level. 170 | 171 | 172 | Version 0.4.4 173 | ------------- 174 | 175 | Released on June 30, 2017. 176 | 177 | - Fixed :exc:`ValueError` from :attr:`Entity.label 178 | `/:attr:`Entity.description 179 | ` with languages `ISO 639-1`_ 180 | doesn't cover (e.g. ``cbk-zam``). [:issue:`2`] 181 | 182 | Although this fix prevents these properties from raising :exc:`ValueError`, 183 | it doesn't completely fix the problem. :class:`babel.core.Locale` type, 184 | which Wikidata depends on, currently doesn't supprot languages other 185 | than `ISO 639-1`_. In order to completely fix the problem, we need to 186 | patch Babel_ to support them, or make Wikidata independent from Babel_. 187 | 188 | 189 | Version 0.4.3 190 | ------------- 191 | 192 | Released on June 28, 2017. 193 | 194 | - Fixed :exc:`AssertionError` from empty 195 | :class:`~wikidata.entity.multilingual_attribute`\ s. 196 | 197 | 198 | Version 0.4.2 199 | ------------- 200 | 201 | Released on June 28, 2017. 202 | 203 | - Fixed :exc:`AssertionError` from :func:`len()` or iterating (:func:`iter()`) 204 | on :class:`~wikidata.entity.Entity` objects with empty claims. 205 | 206 | 207 | Version 0.4.1 208 | ------------- 209 | 210 | Released on April 30, 2017. 211 | 212 | - Fixed :exc:`AssertionError` from :meth:`~wikidata.entity.Entity.getlist()` 213 | on entities with empty claims. 214 | 215 | 216 | Version 0.4.0 217 | ------------- 218 | 219 | Released on April 24, 2017. 220 | 221 | - Monolingual texts became able to be handled. 222 | 223 | - Added :class:`~wikidata.multilingual.MonolingualText` type which is a true 224 | subtype of :class:`str`. 225 | 226 | 227 | Version 0.3.0 228 | ------------- 229 | 230 | Released on February 23, 2017. 231 | 232 | - Now :class:`~wikidata.client.Client` became able to customize how it decodes 233 | datavalues to Python objects. 234 | 235 | - Added :mod:`wikidata.datavalue` module and 236 | :class:`~wikidata.datavalue.Decoder` class inside it. 237 | - Added :attr:`~.wikidata.client.Client.datavalue_decoder` option to 238 | :class:`~wikidata.client.Client`. 239 | 240 | - Now files on Wikimeda Commons became able to be handled. 241 | 242 | - New decoder became able to parse Wikimedia Commons files e.g. images. 243 | - Added :mod:`wikidata.commonsmedia` module and 244 | :class:`~wikidata.commonsmedia.File` class inside it. 245 | 246 | - The meaning of :class:`~wikidata.client.Client` constructor's ``base_url`` 247 | prameter beccame not to contain the trailing path ``wiki/`` from 248 | ``https://www.wikidata.org/wiki/``. As its meaning changed, the value of 249 | :const:`~wikidata.client.WIKIDATA_BASE_URL` constant also changed to not 250 | have the trailing path. 251 | 252 | - Added ``load`` option to :meth:`Client.get() ` 253 | method. 254 | 255 | 256 | Version 0.2.0 257 | ------------- 258 | 259 | Released on February 19, 2017. 260 | 261 | - Made :class:`~wikidata.entity.Entity` multidict. Now it satisfies 262 | :class:`~typing.Mapping`\ [:class:`~wikidata.entity.Entity`, :class:`object`] 263 | protocol. 264 | - Added :attr:`Entity.type ` property and 265 | :class:`~wikidata.entity.EntityType` enum class to represent it. 266 | - Added :attr:`~wikidata.client.Client.entity_type_guess` option and 267 | :meth:`~wikidata.client.Client.guess_entity_type()` method to 268 | :class:`~wikidata.client.Client` class. 269 | - Implemented :class:`~typing.Hashable` protocol and :token:`==`/:token:`!=` 270 | operators to :class:`~wikidata.entity.Entity` for equality test. 271 | 272 | 273 | Version 0.1.0 274 | ------------- 275 | 276 | Initial version. Released on February 15, 2017. 277 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Wikidata documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Feb 19 05:48:40 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os.path 21 | import sys 22 | 23 | sys.path.insert( 24 | 0, 25 | os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') 26 | ) 27 | 28 | from wikidata import __version__ # noqa: E402 29 | 30 | # -- General configuration ------------------------------------------------ 31 | 32 | # If your documentation needs a minimal Sphinx version, state it here. 33 | # 34 | needs_sphinx = '3.0' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinx.ext.autodoc', 41 | 'sphinx.ext.autosummary', 42 | 'sphinx.ext.intersphinx', 43 | 'sphinx.ext.extlinks', 44 | 'sphinx.ext.todo', 45 | 'sphinx.ext.coverage', 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | # 54 | # source_suffix = ['.rst', '.md'] 55 | source_suffix = '.rst' 56 | 57 | # The encoding of source files. 58 | # 59 | # source_encoding = 'utf-8-sig' 60 | 61 | # The master toctree document. 62 | master_doc = 'index' 63 | 64 | # General information about the project. 65 | project = 'Wikidata' 66 | copyright = '2017\N{EN DASH}2020, Hong Minhee' 67 | author = 'Hong Minhee' 68 | 69 | # The version info for the project you're documenting, acts as replacement for 70 | # |version| and |release|, also used in various other places throughout the 71 | # built documents. 72 | # 73 | # The short X.Y version. 74 | version = __version__[:__version__.rindex('.')] 75 | # The full version, including alpha/beta/rc tags. 76 | release = __version__ 77 | 78 | # The language for content autogenerated by Sphinx. Refer to documentation 79 | # for a list of supported languages. 80 | # 81 | # This is also used if you do content translation via gettext catalogs. 82 | # Usually you set "language" from the command line for these cases. 83 | language = 'en' 84 | 85 | # There are two options for replacing |today|: either, you set today to some 86 | # non-false value, then it is used: 87 | # 88 | # today = '' 89 | # 90 | # Else, today_fmt is used as the format for a strftime call. 91 | # 92 | # today_fmt = '%B %d, %Y' 93 | 94 | # List of patterns, relative to source directory, that match files and 95 | # directories to ignore when looking for source files. 96 | # This patterns also effect to html_static_path and html_extra_path 97 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 98 | 99 | # The reST default role (used for this markup: `text`) to use for all 100 | # documents. 101 | # 102 | # default_role = None 103 | 104 | primary_domain = 'py' 105 | highlight_language = 'python3' 106 | 107 | # If true, '()' will be appended to :func: etc. cross-reference text. 108 | # 109 | # add_function_parentheses = True 110 | 111 | # If true, the current module name will be prepended to all description 112 | # unit titles (such as .. function::). 113 | # 114 | # add_module_names = True 115 | 116 | # If true, sectionauthor and moduleauthor directives will be shown in the 117 | # output. They are ignored by default. 118 | # 119 | # show_authors = False 120 | 121 | # The name of the Pygments (syntax highlighting) style to use. 122 | # pygments_style = '' 123 | 124 | # A list of ignored prefixes for module index sorting. 125 | # modindex_common_prefix = [] 126 | 127 | # If true, keep warnings as "system message" paragraphs in the built documents. 128 | # keep_warnings = False 129 | 130 | # If true, `todo` and `todoList` produce output, else they produce nothing. 131 | todo_include_todos = True 132 | 133 | 134 | # -- Options for HTML output ---------------------------------------------- 135 | 136 | # The theme to use for HTML and HTML Help pages. See the documentation for 137 | # a list of builtin themes. 138 | # 139 | html_theme = 'furo' 140 | 141 | # Theme options are theme-specific and customize the look and feel of a theme 142 | # further. For a list of options available for each theme, see the 143 | # documentation. 144 | # 145 | # html_theme_options = {} 146 | 147 | # Add any paths that contain custom themes here, relative to this directory. 148 | # html_theme_path = [] 149 | 150 | # The name for this set of Sphinx documents. 151 | # " v documentation" by default. 152 | # 153 | html_title = f'Wikidata v{release}' 154 | 155 | # A shorter title for the navigation bar. Default is the same as html_title. 156 | # 157 | # html_short_title = None 158 | 159 | # The name of an image file (relative to this directory) to place at the top 160 | # of the sidebar. 161 | # 162 | # html_logo = None 163 | 164 | # The name of an image file (relative to this directory) to use as a favicon of 165 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 166 | # pixels large. 167 | # 168 | # html_favicon = None 169 | 170 | # Add any paths that contain custom static files (such as style sheets) here, 171 | # relative to this directory. They are copied after the builtin static files, 172 | # so a file named "default.css" will overwrite the builtin "default.css". 173 | html_static_path = ['_static'] 174 | 175 | # Add any extra paths that contain custom files (such as robots.txt or 176 | # .htaccess) here, relative to this directory. These files are copied 177 | # directly to the root of the documentation. 178 | # 179 | # html_extra_path = [] 180 | 181 | # If not None, a 'Last updated on:' timestamp is inserted at every page 182 | # bottom, using the given strftime format. 183 | # The empty string is equivalent to '%b %d, %Y'. 184 | # 185 | # html_last_updated_fmt = None 186 | 187 | # If true, SmartyPants will be used to convert quotes and dashes to 188 | # typographically correct entities. 189 | # 190 | # html_use_smartypants = True 191 | 192 | # Custom sidebar templates, maps document names to template names. 193 | # 194 | # html_sidebars = {} 195 | 196 | # Additional templates that should be rendered to pages, maps page names to 197 | # template names. 198 | # 199 | # html_additional_pages = {} 200 | 201 | # If false, no module index is generated. 202 | # 203 | # html_domain_indices = True 204 | 205 | # If false, no index is generated. 206 | # 207 | # html_use_index = True 208 | 209 | # If true, the index is split into individual pages for each letter. 210 | # 211 | # html_split_index = False 212 | 213 | # If true, links to the reST sources are added to the pages. 214 | # 215 | # html_show_sourcelink = True 216 | 217 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 218 | # 219 | # html_show_sphinx = True 220 | 221 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 222 | # 223 | # html_show_copyright = True 224 | 225 | # If true, an OpenSearch description file will be output, and all pages will 226 | # contain a tag referring to it. The value of this option must be the 227 | # base URL from which the finished HTML is served. 228 | # 229 | # html_use_opensearch = '' 230 | 231 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 232 | # html_file_suffix = None 233 | 234 | # Language to be used for generating the HTML full-text search index. 235 | # Sphinx supports the following languages: 236 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 237 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 238 | # 239 | # html_search_language = 'en' 240 | 241 | # A dictionary with options for the search language support, empty by default. 242 | # 'ja' uses this config value. 243 | # 'zh' user can custom change `jieba` dictionary path. 244 | # 245 | # html_search_options = {'type': 'default'} 246 | 247 | # The name of a javascript file (relative to the configuration directory) that 248 | # implements a search results scorer. If empty, the default will be used. 249 | # 250 | # html_search_scorer = 'scorer.js' 251 | 252 | # Output file base name for HTML help builder. 253 | htmlhelp_basename = 'Wikidatadoc' 254 | 255 | # -- Options for LaTeX output --------------------------------------------- 256 | 257 | latex_elements = { 258 | # The paper size ('letterpaper' or 'a4paper'). 259 | # 260 | # 'papersize': 'letterpaper', 261 | 262 | # The font size ('10pt', '11pt' or '12pt'). 263 | # 264 | # 'pointsize': '10pt', 265 | 266 | # Additional stuff for the LaTeX preamble. 267 | # 268 | # 'preamble': '', 269 | 270 | # Latex figure (float) alignment 271 | # 272 | # 'figure_align': 'htbp', 273 | } 274 | 275 | # Grouping the document tree into LaTeX files. List of tuples 276 | # (source start file, target name, title, 277 | # author, documentclass [howto, manual, or own class]). 278 | latex_documents = [ 279 | (master_doc, 'Wikidata.tex', 'Wikidata Documentation', 280 | 'Hong Minhee', 'manual'), 281 | ] 282 | 283 | # The name of an image file (relative to this directory) to place at the top of 284 | # the title page. 285 | # 286 | # latex_logo = None 287 | 288 | # For "manual" documents, if this is true, then toplevel headings are parts, 289 | # not chapters. 290 | # 291 | # latex_use_parts = False 292 | 293 | # If true, show page references after internal links. 294 | # 295 | # latex_show_pagerefs = False 296 | 297 | # If true, show URL addresses after external links. 298 | # 299 | # latex_show_urls = False 300 | 301 | # Documents to append as an appendix to all manuals. 302 | # 303 | # latex_appendices = [] 304 | 305 | # It false, will not define \strong, \code, itleref, \crossref ... but only 306 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 307 | # packages. 308 | # 309 | # latex_keep_old_macro_names = True 310 | 311 | # If false, no module index is generated. 312 | # 313 | # latex_domain_indices = True 314 | 315 | 316 | # -- Options for manual page output --------------------------------------- 317 | 318 | # One entry per manual page. List of tuples 319 | # (source start file, name, description, authors, manual section). 320 | man_pages = [ 321 | (master_doc, 'wikidata', 'Wikidata Documentation', 322 | [author], 1) 323 | ] 324 | 325 | # If true, show URL addresses after external links. 326 | # 327 | # man_show_urls = False 328 | 329 | 330 | # -- Options for Texinfo output ------------------------------------------- 331 | 332 | # Grouping the document tree into Texinfo files. List of tuples 333 | # (source start file, target name, title, author, 334 | # dir menu entry, description, category) 335 | texinfo_documents = [ 336 | (master_doc, 'Wikidata', 'Wikidata Documentation', 337 | author, 'Wikidata', 'One line description of project.', 338 | 'Miscellaneous'), 339 | ] 340 | 341 | # Documents to append as an appendix to all manuals. 342 | # 343 | # texinfo_appendices = [] 344 | 345 | # If false, no module index is generated. 346 | # 347 | # texinfo_domain_indices = True 348 | 349 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 350 | # 351 | # texinfo_show_urls = 'footnote' 352 | 353 | # If true, do not generate a @detailmenu in the "Top" node's menu. 354 | # 355 | # texinfo_no_detailmenu = False 356 | 357 | autosummary_generate = True 358 | 359 | # Example configuration for intersphinx: refer to the Python standard library. 360 | intersphinx_mapping = { 361 | 'python': ('https://docs.python.org/3/', None), 362 | 'werkzeug': ('https://werkzeug.palletsprojects.com/en/1.0.x/', None), 363 | 364 | # Remains for the changelog: 365 | 'babel': ('http://babel.pocoo.org/en/stable/', None), 366 | } 367 | 368 | 369 | extlinks = { 370 | 'issue': ('https://github.com/dahlia/wikidata/issues/%s', '#%s'), 371 | 'pr': ('https://github.com/dahlia/wikidata/pull/%s', '#%s'), 372 | 'branch': ('https://github.com/dahlia/wikidata/compare/%s', '%s'), 373 | 'commit': ('https://github.com/dahlia/wikidata/commit/%s', '%s') 374 | } 375 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | .. toctree:: 4 | :maxdepth: 2 5 | 6 | wikidata 7 | contributing 8 | changes 9 | 10 | 11 | Indices and tables 12 | ================== 13 | 14 | * :ref:`genindex` 15 | * :ref:`modindex` 16 | * :ref:`search` 17 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Wikidata.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Wikidata.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/wikidata.rst: -------------------------------------------------------------------------------- 1 | :mod:`wikidata` --- Wikidata_ client library 2 | ============================================ 3 | 4 | .. _Wikidata: https://www.wikidata.org/ 5 | 6 | .. automodule:: wikidata 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | :glob: 11 | 12 | wikidata/* 13 | -------------------------------------------------------------------------------- /docs/wikidata/cache.rst: -------------------------------------------------------------------------------- 1 | :mod:`wikidata.cache` --- Caching policies 2 | ========================================== 3 | 4 | .. versionadded:: 0.5.0 5 | 6 | .. automodule:: wikidata.cache 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/wikidata/client.rst: -------------------------------------------------------------------------------- 1 | :mod:`wikidata.client` --- Client session 2 | ========================================= 3 | 4 | .. automodule:: wikidata.client 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/wikidata/commonsmedia.rst: -------------------------------------------------------------------------------- 1 | :mod:`wikidata.commonsmedia` --- `Wikimedia Commons`_ 2 | ===================================================== 3 | 4 | .. _Wikimedia Commons: https://commons.wikimedia.org/ 5 | 6 | .. versionadded:: 0.3.0 7 | 8 | .. automodule:: wikidata.commonsmedia 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/wikidata/datavalue.rst: -------------------------------------------------------------------------------- 1 | :mod:`wikidata.datavalue` --- Interpreting datavalues 2 | ===================================================== 3 | 4 | .. versionadded:: 0.3.0 5 | 6 | .. automodule:: wikidata.datavalue 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/wikidata/entity.rst: -------------------------------------------------------------------------------- 1 | :mod:`wikidata.entity` --- Wikidata entities 2 | ============================================ 3 | 4 | .. automodule:: wikidata.entity 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/wikidata/globecoordinate.rst: -------------------------------------------------------------------------------- 1 | :mod:`wikidata.globecoordinate` --- Globe coordinate 2 | ==================================================== 3 | 4 | .. versionadded:: 0.7.0 5 | 6 | .. automodule:: wikidata.globecoordinate 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/wikidata/multilingual.rst: -------------------------------------------------------------------------------- 1 | :mod:`wikidata.multilingual` --- Multilingual texts 2 | =================================================== 3 | 4 | .. automodule:: wikidata.multilingual 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/wikidata/quantity.rst: -------------------------------------------------------------------------------- 1 | :mod:`wikidata.quantity` --- Quantity 2 | ===================================== 3 | 4 | .. versionadded:: 0.7.0 5 | 6 | .. automodule:: wikidata.quantity 7 | :members: 8 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.8 3 | check_untyped_defs = true 4 | follow_imports = silent 5 | strict_optional = true 6 | warn_unused_ignores = true 7 | scripts_are_modules = true 8 | 9 | [mypy-tests.*] 10 | check_untyped_defs = false 11 | warn_unused_ignores = false 12 | 13 | [mypy-pytest.*] -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = Wikidata 3 | version = attr: wikidata.__version__ 4 | description = Wikidata client library 5 | long_description = file: README.rst 6 | long_description_content_type = text/x-rst; charset=UTF-8 7 | url = https://github.com/dahlia/wikidata 8 | project_urls = 9 | Docs = https://wikidata.readthedocs.io/ 10 | author = Hong Minhee 11 | author_email = hong.minhee@gmail.com 12 | license = GPLv3 or later 13 | keywords = wikidata ontology 14 | platforms = any 15 | classifiers = 16 | Development Status :: 4 - Beta 17 | Intended Audience :: Developers 18 | Intended Audience :: Information Technology 19 | License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) 20 | Operating System :: OS Independent 21 | # CHECK If you're going to change the list of supported Python versions 22 | # update .travis.yml and tox.ini as well. 23 | Programming Language :: Python :: 3 24 | Programming Language :: Python :: 3.8 25 | Programming Language :: Python :: 3.9 26 | Programming Language :: Python :: 3.10 27 | Programming Language :: Python :: 3.11 28 | Programming Language :: Python :: 3 :: Only 29 | Programming Language :: Python :: Implementation :: CPython 30 | Programming Language :: Python :: Implementation :: PyPy 31 | Topic :: Database 32 | Topic :: Internet :: WWW/HTTP 33 | Topic :: Scientific/Engineering 34 | Topic :: Software Development :: Libraries :: Python Modules 35 | 36 | [options] 37 | packages = find: 38 | python_requires = >=3.8.0 39 | install_requires = 40 | typing; python_version<"3.5" 41 | 42 | [options.packages.find] 43 | exclude = 44 | docs 45 | tests 46 | 47 | [options.package_data] 48 | wikidata = 49 | py.typed 50 | 51 | [options.extras_require] 52 | tests = 53 | flake8 >= 6.0.0 54 | flake8-import-order-spoqa 55 | pytest ~= 7.2.1 56 | mypy >= 0.991 57 | docs = 58 | furo 59 | rstcheck 60 | Sphinx ~= 6.1.3 61 | 62 | [tool:pytest] 63 | addopts = --ff --doctest-glob=*.rst --doctest-modules 64 | testpaths = 65 | tests 66 | wikidata 67 | README.rst 68 | doctest_optionflags = 69 | NORMALIZE_WHITESPACE 70 | IGNORE_EXCEPTION_DETAIL 71 | ELLIPSIS 72 | 73 | [flake8] 74 | exclude = .tox,build,dist,docs,typeshed 75 | import-order-style = spoqa 76 | application-import-names = wikidata, tests 77 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup # type: ignore 2 | 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dahlia/wikidata/0a3e0eccef8f78c395e7ed8a258fcbe13e1752fc/tests/__init__.py -------------------------------------------------------------------------------- /tests/cache_test.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import typing 3 | 4 | from wikidata.cache import MemoryCachePolicy, ProxyCachePolicy 5 | 6 | 7 | def test_memory_cache_policy(): 8 | m = MemoryCachePolicy(max_size=5) 9 | assert m.get('a') is None 10 | m.set('a', 1) 11 | assert m.get('a') == 1 12 | m.set('b', 2) 13 | m.set('c', 3) 14 | m.set('d', 4) 15 | m.set('e', 5) 16 | m.set('f', 6) 17 | assert m.get('a') is None 18 | m.get('b') 19 | m.set('g', 7) 20 | assert m.get('b') == 2 21 | assert m.get('c') is None 22 | 23 | 24 | class MockCache: 25 | 26 | def __init__(self) -> None: 27 | self.records = [] # type: typing.List[typing.Tuple[str, typing.List]] 28 | 29 | def get(self, key: str) -> typing.Optional[bytes]: 30 | self.records.append(('get', [key])) 31 | if key == 'wd/37b51d194a7513e45b56f6524f2d51f2': 32 | return pickle.dumps('cached value') 33 | return None 34 | 35 | def set(self, key: str, value: bytes, timeout: int = 0) -> None: 36 | self.records.append(('set', [key, value, timeout])) 37 | 38 | def delete(self, key: str) -> None: 39 | self.records.append(('delete', [key])) 40 | 41 | 42 | def test_proxy_cache_policy(): 43 | mock = MockCache() 44 | proxy = ProxyCachePolicy(mock, 123, 456, 'wd/') 45 | assert proxy.get('foo') is None 46 | assert len(mock.records) == 1 47 | assert mock.records[0] == ('get', ['wd/acbd18db4cc2f85cedef654fccc4a4d8']) 48 | assert proxy.get('bar') == 'cached value' 49 | assert len(mock.records) == 2 50 | assert mock.records[1] == ('get', ['wd/37b51d194a7513e45b56f6524f2d51f2']) 51 | proxy.set('baz', 'asdf') 52 | assert len(mock.records) == 3 53 | assert mock.records[2][0] == 'set' 54 | assert mock.records[2][1][0] == 'wd/73feffa4b7f6bb68e44cf984c85f6e88' 55 | assert pickle.loads(mock.records[2][1][1]) == 'asdf' 56 | assert mock.records[2][1][2] == 123 57 | proxy.set('qux', None) 58 | assert len(mock.records) == 4 59 | assert (mock.records[3] == 60 | ('delete', ['wd/d85b1213473c2fd7c2045020a6b9c62b'])) 61 | proxy.set('https://www.wikidata.org/wiki/Special:EntityData/P18.json', 62 | 'foo') 63 | assert len(mock.records) == 5 64 | assert mock.records[4][0] == 'set' 65 | assert mock.records[4][1][0] == 'wd/a071db2de830f9369edfcb773750ccc9' 66 | assert pickle.loads(mock.records[4][1][1]) == 'foo' 67 | assert mock.records[4][1][2] == 456 68 | -------------------------------------------------------------------------------- /tests/client_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | from typing import TYPE_CHECKING, Optional 4 | import urllib.request 5 | 6 | from .mock import FixtureOpener 7 | from wikidata.cache import CacheKey, CachePolicy, CacheValue 8 | from wikidata.client import Client 9 | from wikidata.entity import Entity, EntityId, EntityState, EntityType 10 | from wikidata.multilingual import Locale 11 | 12 | if TYPE_CHECKING: 13 | from typing import Dict, Union # noqa: F401 14 | 15 | 16 | def test_client_get(fx_client: Client): 17 | entity = fx_client.get(EntityId('Q1299')) 18 | assert isinstance(entity, Entity) 19 | assert entity.data is None 20 | assert entity.id == EntityId('Q1299') 21 | entity2 = fx_client.get(EntityId('Q1299'), load=True) 22 | assert entity2.data is not None 23 | assert entity2 is entity 24 | entity3 = fx_client.get(EntityId('1299'), load=True) # http 400 error 25 | assert entity3.state is EntityState.non_existent 26 | 27 | 28 | def test_client_guess_entity_type( 29 | fx_client_opener: urllib.request.OpenerDirector 30 | ): 31 | guess_client = Client(opener=fx_client_opener, entity_type_guess=True) 32 | assert guess_client.guess_entity_type(EntityId('Q1299')) is EntityType.item 33 | assert (guess_client.guess_entity_type(EntityId('P434')) is 34 | EntityType.property) 35 | assert guess_client.guess_entity_type(EntityId('NotApplicable')) is None 36 | noguess_client = Client(opener=fx_client_opener, entity_type_guess=False) 37 | assert noguess_client.guess_entity_type(EntityId('Q1299')) is None 38 | assert noguess_client.guess_entity_type(EntityId('P434')) is None 39 | assert noguess_client.guess_entity_type(EntityId('NotApplicable')) is None 40 | 41 | 42 | def test_client_request(fx_client: Client): 43 | data = fx_client.request('./wiki/Special:EntityData/Q1299.json') 44 | assert isinstance(data, dict) 45 | assert set(data) == {'entities'} 46 | entities = data['entities'] 47 | assert isinstance(entities, dict) 48 | assert set(entities) == {'Q1299'} 49 | entity = entities['Q1299'] 50 | assert isinstance(entity, dict) 51 | assert set(entity) >= { 52 | 'pageid', 'ns', 'title', 'lastrevid', 'modified', 'type', 'id', 53 | 'labels', 'descriptions', 'aliases', 'claims', 'sitelinks' 54 | } 55 | assert entity['title'] == 'Q1299' 56 | assert entity['type'] == 'item' 57 | assert entity['labels']['en'] == {'language': 'en', 'value': 'The Beatles'} 58 | 59 | 60 | class MockCachePolicy(CachePolicy): 61 | 62 | def __init__(self) -> None: 63 | self.store = { 64 | } # type: Dict[Union[CacheKey, str], Optional[CacheValue]] 65 | 66 | def get(self, key: CacheKey) -> Optional[CacheValue]: 67 | return self.store.get(key) 68 | 69 | def set(self, key: CacheKey, value: Optional[CacheValue]) -> None: 70 | self.store[key] = value 71 | 72 | 73 | def test_client_cache_policy(fx_client_opener: FixtureOpener): 74 | mock = MockCachePolicy() 75 | client1 = Client(opener=fx_client_opener, cache_policy=mock) 76 | e1 = client1.get(EntityId('Q1299'), load=True) 77 | assert len(fx_client_opener.records) == 1 78 | url = 'https://www.wikidata.org/wiki/Special:EntityData/Q1299.json' 79 | url_open = fx_client_opener.open 80 | assert frozenset(mock.store) == {url} 81 | assert mock.store[url] == json.loads(url_open(url).read().decode('utf-8')) 82 | assert len(fx_client_opener.records) == 2 83 | client2 = Client(opener=fx_client_opener, cache_policy=mock) 84 | e2 = client2.get(EntityId('Q1299'), load=True) 85 | assert e1.attributes == e2.attributes 86 | assert len(fx_client_opener.records) == 2 87 | 88 | 89 | def test_client_pickle(fx_client: Client): 90 | dumped = pickle.dumps(fx_client) 91 | c = pickle.loads(dumped) 92 | entity = c.get(EntityId('Q1299'), load=True) 93 | assert isinstance(entity, Entity) 94 | assert entity.label[Locale('en')] == 'The Beatles' 95 | 96 | 97 | def test_client_repr(): 98 | assert repr(Client(repr_string='repr_string test')) == 'repr_string test' 99 | assert repr(Client()) == \ 100 | "wikidata.client.Client('https://www.wikidata.org/')" 101 | -------------------------------------------------------------------------------- /tests/commonsmedia_test.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | import pathlib 4 | 5 | from pytest import fixture 6 | 7 | from .mock import MEDIA_FIXTURES_PATH 8 | from wikidata.commonsmedia import File 9 | 10 | 11 | def test_file_page_url(fx_file: File): 12 | assert (fx_file.page_url == 13 | 'https://www.wikidata.org/wiki/File:Gandhara_Buddha_(tnm).jpeg') 14 | 15 | 16 | def test_file_image_url(fx_file: File): 17 | assert fx_file.image_url == \ 18 | 'https://upload.wikimedia.org/wikipedia/commons/b/b8/Gandhara_Buddha_%28tnm%29.jpeg' # noqa: E501 19 | 20 | 21 | def test_file_image_mimetype(fx_file: File): 22 | assert fx_file.image_mimetype == 'image/jpeg' 23 | 24 | 25 | def test_file_image_resolution(fx_file: File): 26 | assert fx_file.image_resolution == (1746, 2894) 27 | 28 | 29 | def test_file_image_size(fx_file: File): 30 | assert fx_file.image_size == 823440 31 | 32 | 33 | @fixture 34 | def fx_file_mock_path(fx_file: File) -> pathlib.Path: 35 | title_id = hashlib.md5(fx_file.title.encode('utf-8')).hexdigest().lower() 36 | return MEDIA_FIXTURES_PATH / '{}.json'.format(title_id) 37 | 38 | 39 | def test_file_attributes(fx_file: File, fx_file_mock_path: pathlib.Path): 40 | with fx_file_mock_path.open('r') as f: 41 | assert fx_file.attributes == json.load(f)['query']['pages']['-1'] 42 | 43 | 44 | def test_file_load(fx_file: File, fx_file_mock_path: pathlib.Path): 45 | fx_file.load() 46 | with fx_file_mock_path.open('r') as f: 47 | assert fx_file.data == json.load(f)['query']['pages']['-1'] 48 | 49 | 50 | def test_file_repr(fx_file: File): 51 | assert (repr(fx_file) == 52 | "") 53 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from typing import AbstractSet, FrozenSet, Optional, Sequence, Set, Union, cast 2 | import urllib.request 3 | 4 | from pytest import fixture 5 | 6 | from .mock import FixtureOpener 7 | from wikidata.client import WIKIDATA_BASE_URL, Client 8 | from wikidata.commonsmedia import File 9 | from wikidata.entity import Entity, EntityId 10 | 11 | 12 | def pytest_assertrepr_compare(op: str, left, right) -> Optional[Sequence[str]]: 13 | # set of entities 14 | if op == '==' and isinstance(left, (set, frozenset)) and \ 15 | isinstance(right, (set, frozenset)) and \ 16 | all(isinstance(v, Entity) for v in left) and \ 17 | all(isinstance(v, Entity) for v in right): 18 | def repr_ids(ids: AbstractSet[EntityId]) -> str: 19 | sorted_ids = sorted( 20 | ids, 21 | key=lambda i: ( 22 | i[0], 23 | # Since EntityIds usually consist of one letter followed 24 | # by digits, order them numerically. If it's not in 25 | # that format they should be sorted in the other bucket. 26 | (0, int(i[1:])) if i[1:].isdigit() else (1, i[1:]) 27 | ) 28 | ) 29 | return '{' + ', '.join(sorted_ids) + '}' 30 | left = cast(Union[Set[Entity], FrozenSet[Entity]], left) 31 | right = cast(Union[Set[Entity], FrozenSet[Entity]], right) 32 | left_ids = {e.id for e in left} 33 | right_ids = {e.id for e in right} 34 | return [ 35 | '{} == {}'.format(repr_ids(left_ids), repr_ids(right_ids)), 36 | 'Extra entities in the left set:', 37 | repr_ids(left_ids - right_ids), 38 | 'Extra entities in the right set:', 39 | repr_ids(right_ids - left_ids), 40 | ] 41 | return None 42 | 43 | 44 | @fixture 45 | def fx_client_opener() -> urllib.request.OpenerDirector: 46 | return FixtureOpener(WIKIDATA_BASE_URL) 47 | 48 | 49 | @fixture 50 | def fx_client(fx_client_opener: urllib.request.OpenerDirector) -> Client: 51 | return Client(opener=fx_client_opener) 52 | 53 | 54 | @fixture(autouse=True) 55 | def add_doctest_namespace(doctest_namespace, fx_client: Client): 56 | doctest_namespace['client'] = fx_client 57 | 58 | 59 | @fixture 60 | def fx_unloaded_entity(fx_client: Client) -> Entity: 61 | return fx_client.get(EntityId('Q1299')) 62 | 63 | 64 | @fixture 65 | def fx_loaded_entity(fx_client: Client) -> Entity: 66 | entity = fx_client.get(EntityId('Q494290')) 67 | entity.load() 68 | return entity 69 | 70 | 71 | @fixture 72 | def fx_redirected_entity(fx_client: Client) -> Entity: 73 | return fx_client.get(EntityId('Q16231742')) 74 | 75 | 76 | @fixture 77 | def fx_item(fx_loaded_entity: Entity) -> Entity: 78 | return fx_loaded_entity 79 | 80 | 81 | @fixture 82 | def fx_property(fx_client: Client) -> Entity: 83 | return fx_client.get(EntityId('P2003')) 84 | 85 | 86 | @fixture 87 | def fx_file(fx_client: Client) -> File: 88 | return File(fx_client, 'File:Gandhara Buddha (tnm).jpeg') 89 | -------------------------------------------------------------------------------- /tests/datavalue_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Dict, cast 3 | 4 | from pytest import mark, raises 5 | 6 | from wikidata.client import Client 7 | from wikidata.commonsmedia import File 8 | from wikidata.datavalue import DatavalueError, Decoder 9 | from wikidata.entity import Entity, EntityId 10 | from wikidata.globecoordinate import GlobeCoordinate 11 | from wikidata.multilingual import Locale, MonolingualText 12 | from wikidata.quantity import Quantity 13 | 14 | 15 | def test_datavalue_error(): 16 | with raises(TypeError): 17 | DatavalueError() 18 | with raises(TypeError): 19 | DatavalueError('message') 20 | assert str(DatavalueError('message', {'type': 'string', 'value': '...'})) \ 21 | == "message: {'type': 'string', 'value': '...'}" 22 | assert str(DatavalueError('msg', {'foo': 'bar'})) == "msg: {'foo': 'bar'}" 23 | 24 | 25 | def test_decoder_missing_type(fx_client: Client): 26 | d = Decoder() 27 | with raises(DatavalueError): 28 | d(fx_client, 'string', {'value': '...'}) 29 | 30 | 31 | def test_decoder_missing_value(fx_client: Client): 32 | d = Decoder() 33 | with raises(DatavalueError): 34 | d(fx_client, 'string', {'type': 'string'}) 35 | 36 | 37 | def test_decoder_unsupported_type(fx_client: Client): 38 | d = Decoder() 39 | with raises(DatavalueError): 40 | d(fx_client, 'unsupportedtype', {'type': 'unsupport', 'value': '...'}) 41 | with raises(DatavalueError): 42 | d(fx_client, 'string', {'type': 'unsupport', 'value': '...'}) 43 | 44 | 45 | @mark.parametrize('datatype', ['string', 'wikibase-item']) 46 | def test_decoder_wikibase_entityid(datatype: str, 47 | fx_client: Client, 48 | fx_loaded_entity: Entity): 49 | d = Decoder() 50 | with raises(DatavalueError): 51 | d( 52 | fx_client, datatype, 53 | {'type': 'wikibase-entityid', 'value': 'not mapping'} 54 | ) 55 | with raises(DatavalueError): 56 | d( 57 | fx_client, datatype, 58 | {'type': 'wikibase-entityid', 'value': {}} # no id 59 | ) 60 | decoded = d( 61 | fx_client, datatype, 62 | {'type': 'wikibase-entityid', 'value': {'id': fx_loaded_entity.id}} 63 | ) 64 | assert decoded is fx_loaded_entity 65 | 66 | 67 | @mark.parametrize('datatype', ['string', 'external-id']) 68 | def test_decoder_string(datatype: str, fx_client: Client): 69 | d = Decoder() 70 | assert d(fx_client, datatype, 71 | {'type': 'string', 'value': 'foobar'}) == 'foobar' 72 | 73 | 74 | @mark.parametrize('datatype', ['time', 'string']) 75 | def test_decoder__time(datatype: str, fx_client: Client): 76 | d = Decoder() 77 | valid_value = { 78 | 'calendarmodel': 'http://www.wikidata.org/entity/Q1985727', 79 | 'time': '+2017-02-22T02:53:12Z', 80 | 'timezone': 0, 'before': 0, 'after': 0, 'precision': 14, 81 | } 82 | valid = {'type': 'time', 'value': valid_value} 83 | 84 | def other_value(**kwargs) -> Dict[str, object]: 85 | value = dict(valid_value, **cast(Dict[str, object], kwargs)) 86 | return dict(valid, value={ 87 | k: v for k, v in value.items() if v is not None 88 | }) 89 | assert (datetime.date(2017, 2, 22) == 90 | d(fx_client, datatype, other_value(precision=11))) 91 | assert 2017 == d(fx_client, datatype, other_value(precision=9)) 92 | utc = datetime.timezone.utc 93 | assert (datetime.datetime(2017, 2, 22, 2, 53, 12, tzinfo=utc) == 94 | d(fx_client, datatype, valid)) 95 | with raises(DatavalueError): 96 | d(fx_client, datatype, dict(valid, value='not mapping')) 97 | with raises(DatavalueError): 98 | d(fx_client, datatype, other_value(calendarmodel=None)) 99 | # no calendarmodel field 100 | with raises(DatavalueError): 101 | d(fx_client, datatype, other_value(time=None)) 102 | # no time field 103 | with raises(DatavalueError): 104 | d( 105 | fx_client, datatype, 106 | other_value(calendarmodel='unspported calendar model') 107 | ) 108 | with raises(DatavalueError): 109 | d(fx_client, datatype, other_value(time='-2017-02-22T02:53:12Z')) 110 | # only AD (CE) time is supported 111 | with raises(DatavalueError): 112 | d(fx_client, datatype, other_value(timezone=None)) 113 | # timezone field is missing 114 | with raises(DatavalueError): 115 | d(fx_client, datatype, other_value(timezone=60)) 116 | # timezone field should be 0 117 | with raises(DatavalueError): 118 | d(fx_client, datatype, other_value(after=None)) 119 | # after field is missing 120 | with raises(DatavalueError): 121 | d(fx_client, datatype, other_value(before=None)) 122 | # before field is missing 123 | with raises(DatavalueError): 124 | d(fx_client, datatype, other_value(after=60)) 125 | # after field (other than 0) is unsupported 126 | with raises(DatavalueError): 127 | d(fx_client, datatype, other_value(before=60)) 128 | # before field (other than 0) is unsupported 129 | with raises(DatavalueError): 130 | d(fx_client, datatype, other_value(precision=None)) 131 | # precision field is missing 132 | for p in range(1, 15): 133 | if p in (7, 9, 10, 11, 14): 134 | continue 135 | with raises(DatavalueError): 136 | d(fx_client, datatype, other_value(precision=p)) 137 | # precision (other than 7, 9, 10, 11 or 14) is unsupported 138 | 139 | 140 | def test_decoder_monolingualtext(fx_client: Client): 141 | d = Decoder() 142 | assert d(fx_client, 'monolingualtext', { 143 | 'type': 'monolingualtext', 144 | 'value': { 145 | 'language': 'ko', 146 | 'text': '윤동주', 147 | }, 148 | }) == MonolingualText('윤동주', Locale('ko')) 149 | 150 | 151 | def test_decoder_commonsMedia__string(fx_client: Client): 152 | d = Decoder() 153 | f = d(fx_client, 'commonsMedia', 154 | {'value': 'The Fabs.JPG', 'type': 'string'}) 155 | assert isinstance(f, File) 156 | assert f.title == 'File:The Fabs.JPG' 157 | 158 | 159 | def test_decoder_quantity_with_unit(fx_client: Client): 160 | d = Decoder() 161 | decoded = d(fx_client, 'quantity', { 162 | 'value': { 163 | "amount": "+610.13", 164 | "lower_bound": "+610.12", 165 | "upper_bound": "+610.14", 166 | "unit": "http://www.wikidata.org/entity/Q828224" 167 | }, 168 | 'type': 'quantity' 169 | }) 170 | gold = Quantity( 171 | amount=610.13, 172 | lower_bound=610.12, 173 | upper_bound=610.14, 174 | unit=fx_client.get(EntityId("Q828224"))) 175 | assert decoded == gold 176 | 177 | 178 | def test_decoder_quantity_unitless(fx_client: Client): 179 | d = Decoder() 180 | decoded = d(fx_client, 'quantity', { 181 | 'value': { 182 | "amount": "+12", 183 | "unit": "1" 184 | }, 185 | 'type': 'quantity' 186 | }) 187 | gold = Quantity( 188 | amount=12, 189 | lower_bound=None, 190 | upper_bound=None, 191 | unit=None) 192 | assert decoded == gold 193 | 194 | 195 | def test_decoder_globecoordinate(fx_client: Client): 196 | d = Decoder() 197 | decoded = d(fx_client, 'globe-coordinate', { 198 | 'value': { 199 | "latitude": 70.1525, 200 | "longitude": 70.1525, 201 | "precision": 0.0002777777777777778, 202 | "globe": "http://www.wikidata.org/entity/Q111" 203 | }, 204 | 'type': 'globecoordinate' 205 | }) 206 | gold = GlobeCoordinate(70.1525, 207 | 70.1525, 208 | fx_client.get(EntityId("Q111")), 209 | 0.0002777777777777778,) 210 | assert decoded == gold 211 | -------------------------------------------------------------------------------- /tests/entity_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | from typing import Iterable 4 | import urllib.request 5 | 6 | from pytest import raises 7 | 8 | from .mock import ENTITY_FIXTURES_PATH 9 | from wikidata.client import Client 10 | from wikidata.entity import Entity, EntityId, EntityType 11 | from wikidata.multilingual import Locale, MultilingualText 12 | 13 | 14 | def test_entity_equality(fx_client_opener: urllib.request.OpenerDirector, 15 | fx_client: Client, 16 | fx_loaded_entity: Entity): 17 | # When entity id and client are the same 18 | assert fx_loaded_entity is fx_client.get(fx_loaded_entity.id) 19 | assert fx_loaded_entity == fx_client.get(fx_loaded_entity.id) 20 | assert hash(fx_loaded_entity) == hash(fx_client.get(fx_loaded_entity.id)) 21 | # When entity id differs 22 | assert fx_loaded_entity is not fx_client.get(EntityId('Q1299')) 23 | assert fx_loaded_entity != fx_client.get(EntityId('Q1299')) 24 | assert hash(fx_loaded_entity) != hash(fx_client.get(EntityId('Q1299'))) 25 | # When client differs 26 | client = Client(opener=fx_client_opener) 27 | assert fx_loaded_entity is not client.get(fx_loaded_entity.id) 28 | assert fx_loaded_entity != client.get(fx_loaded_entity.id) 29 | assert hash(fx_loaded_entity) != hash(client.get(fx_loaded_entity.id)) 30 | 31 | 32 | def test_entity_label(fx_loaded_entity: Entity, 33 | fx_unloaded_entity: Entity): 34 | assert isinstance(fx_loaded_entity.label, MultilingualText) 35 | assert fx_loaded_entity.label[Locale('ko')] == '신중현' 36 | assert fx_loaded_entity.label[Locale('zh-hant')] == '申重鉉' 37 | assert isinstance(fx_unloaded_entity.label, MultilingualText) 38 | assert fx_unloaded_entity.label[Locale('en')] == 'The Beatles' 39 | assert fx_unloaded_entity.label[Locale('ko')] == '비틀즈' 40 | 41 | 42 | def test_entity_description(fx_loaded_entity: Entity, 43 | fx_unloaded_entity: Entity): 44 | assert isinstance(fx_loaded_entity.description, MultilingualText) 45 | assert fx_loaded_entity.description[Locale('ko')] == \ 46 | '대한민국의 록 음악 싱어송라이터 및 기타리스트' 47 | assert fx_loaded_entity.description[Locale('ja')] == \ 48 | '韓国のロックミュージシャン' 49 | assert isinstance(fx_unloaded_entity.description, MultilingualText) 50 | assert fx_unloaded_entity.description[Locale('en')] == \ 51 | 'English rock band' 52 | assert fx_unloaded_entity.description[Locale('ko')] == \ 53 | '영국의 락 밴드' 54 | 55 | 56 | def test_entity_label_description_three_chars_lang_codes(fx_client: Client): 57 | """ 58 | Ensure that three-character language codes are handled 59 | """ 60 | cbk_zam = fx_client.get(EntityId('Q33281'), load=True) 61 | assert isinstance(cbk_zam.label, MultilingualText) 62 | assert cbk_zam.label[Locale('ko')] == '차바카노어' 63 | assert Locale('cbk-zam') in cbk_zam.label 64 | assert isinstance(cbk_zam.description, MultilingualText) 65 | assert cbk_zam.description[Locale('en')] == \ 66 | 'Spanish-based creole language spoken in the Philippines' 67 | assert Locale('cbk-zam') not in cbk_zam.description 68 | 69 | 70 | def test_entity_type(fx_item: Entity, 71 | fx_property: Entity, 72 | fx_client_opener: urllib.request.OpenerDirector): 73 | assert fx_item.type == EntityType.item 74 | assert fx_property.type == EntityType.property 75 | guess_client = Client(opener=fx_client_opener, entity_type_guess=True) 76 | item = guess_client.get(EntityId('Q494290')) 77 | prop = guess_client.get(EntityId('P434')) 78 | assert item.type == EntityType.item 79 | assert item.data is None # entity data shouldn't be loaded 80 | assert prop.type == EntityType.property 81 | assert prop.data is None # entity data shouldn't be loaded 82 | noguess_client = Client(opener=fx_client_opener, entity_type_guess=False) 83 | item = noguess_client.get(EntityId('Q494290')) 84 | prop = noguess_client.get(EntityId('P434')) 85 | assert item.type == EntityType.item 86 | assert item.data is not None 87 | assert item.data['type'] == 'item' 88 | assert prop.type == EntityType.property 89 | assert prop.data is not None 90 | assert prop.data['type'] == 'property' 91 | 92 | 93 | def test_entity_mapping(fx_client: Client, 94 | fx_loaded_entity: Entity): 95 | occupation = fx_client.get(EntityId('P106')) 96 | musicbrainz_id = fx_client.get(EntityId('P434')) 97 | singer = fx_client.get(EntityId('Q177220')) 98 | instagram_username = fx_client.get(EntityId('P2003')) 99 | assert len(fx_loaded_entity) == 13 100 | expected_ids = { 101 | 'P19', 'P21', 'P27', 'P31', 'P106', 'P136', 'P345', 'P434', 'P569', 102 | 'P646', 'P1303', 'P1728', 'P1953' 103 | } 104 | expected = {fx_client.get(EntityId(pid)) for pid in expected_ids} 105 | assert set(fx_loaded_entity) == expected 106 | assert musicbrainz_id in fx_loaded_entity 107 | assert (fx_loaded_entity[musicbrainz_id] == 108 | fx_loaded_entity.get(musicbrainz_id) == 109 | fx_loaded_entity.get(musicbrainz_id, ...) == 110 | '3eb63662-a02c-4d2d-9544-845cd92fd4e7') 111 | assert (fx_loaded_entity.getlist(musicbrainz_id) == 112 | ['3eb63662-a02c-4d2d-9544-845cd92fd4e7']) 113 | assert occupation in fx_loaded_entity 114 | assert (fx_loaded_entity[occupation] == 115 | fx_loaded_entity.get(occupation) == 116 | fx_loaded_entity.get(occupation, ...) == 117 | singer) 118 | assert (fx_loaded_entity.getlist(occupation) == 119 | [singer, fx_client.get(EntityId('Q753110'))]) 120 | assert instagram_username not in fx_loaded_entity 121 | with raises(KeyError): 122 | fx_loaded_entity[instagram_username] 123 | assert fx_loaded_entity.get(instagram_username) is None 124 | assert fx_loaded_entity.get(instagram_username, ...) is ... 125 | assert fx_loaded_entity.getlist(instagram_username) == [] 126 | assert (dict(fx_loaded_entity.iterlists()) == 127 | dict(fx_loaded_entity.lists()) == 128 | {p: fx_loaded_entity.getlist(p) for p in expected}) 129 | 130 | def sorted_list(v: Iterable) -> list: 131 | return list(sorted(v, key=str)) 132 | assert (sorted_list(fx_loaded_entity.iterlistvalues()) == 133 | sorted_list(fx_loaded_entity.listvalues()) == 134 | sorted_list(fx_loaded_entity.getlist(p) for p in expected)) 135 | 136 | 137 | def test_entity_attributes(fx_unloaded_entity: Entity, 138 | fx_loaded_entity: Entity): 139 | for entity in fx_unloaded_entity, fx_loaded_entity: 140 | filename = '{}.json'.format(entity.id) 141 | with (ENTITY_FIXTURES_PATH / filename).open('r') as f: 142 | assert entity.attributes == json.load(f)['entities'][entity.id] 143 | 144 | 145 | def test_entity_load(fx_unloaded_entity: Entity): 146 | fx_unloaded_entity.load() 147 | with (ENTITY_FIXTURES_PATH / 'Q1299.json').open('r') as f: 148 | assert fx_unloaded_entity.data == json.load(f)['entities']['Q1299'] 149 | 150 | 151 | def test_entity_load_redirected_entity(fx_client: Client, 152 | fx_redirected_entity: Entity): 153 | canonical_id = EntityId('Q3571994') 154 | alternate_id = EntityId('Q16231742') 155 | assert fx_redirected_entity.id == alternate_id 156 | fx_redirected_entity.load() 157 | assert fx_redirected_entity.id == canonical_id 158 | 159 | 160 | def test_entity_pickle(fx_unloaded_entity: Entity, fx_loaded_entity: Entity): 161 | for entity in fx_unloaded_entity, fx_unloaded_entity: 162 | dumped = pickle.dumps(entity) 163 | loaded = pickle.loads(dumped) 164 | assert isinstance(loaded, Entity) 165 | assert loaded.state is entity.state 166 | assert loaded.label[Locale('en')] == entity.label[Locale('en')] 167 | 168 | 169 | def test_entity_repr(fx_unloaded_entity: Entity, 170 | fx_loaded_entity: Entity): 171 | assert repr(fx_unloaded_entity) == '' 172 | assert repr(fx_loaded_entity) == \ 173 | "" 174 | 175 | 176 | def test_entity_getlist_novalue(fx_client: Client): 177 | hong_kong = fx_client.get(EntityId('Q8646')) 178 | locator_map_image = fx_client.get(EntityId('P242')) 179 | # There are 3 snaks for this property, but one has no associated value 180 | assert len(hong_kong.getlist(locator_map_image)) == 2 181 | -------------------------------------------------------------------------------- /tests/fixtures/entities/P2003.json: -------------------------------------------------------------------------------- 1 | {"entities":{"P2003":{"pageid":22470555,"ns":120,"title":"Property:P2003","lastrevid":447356356,"modified":"2017-02-13T00:25:09Z","type":"property","datatype":"external-id","id":"P2003","labels":{"zh":{"language":"zh","value":"Instagram\u8d26\u53f7"},"en":{"language":"en","value":"Instagram username"},"pt":{"language":"pt","value":"nome de utilizador no Instagram"},"nl":{"language":"nl","value":"Instagram-gebruikersnaam"},"ru":{"language":"ru","value":"\u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0432 Instagram"},"es":{"language":"es","value":"nombre de usuario de Instagram"},"uk":{"language":"uk","value":"\u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432 Instagram"},"fr":{"language":"fr","value":"nom d'usager Instagram"},"el":{"language":"el","value":"\u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c3\u03c4\u03bf Instagram"},"it":{"language":"it","value":"nome utente Instagram"},"sv":{"language":"sv","value":"Instagramkonto"},"fi":{"language":"fi","value":"Instagram-k\u00e4ytt\u00e4j\u00e4nimi"},"nb":{"language":"nb","value":"Instagram-konto"},"de":{"language":"de","value":"Instagram-Benutzername"},"af":{"language":"af","value":"Instagram"},"an":{"language":"an","value":"Instagram"},"ast":{"language":"ast","value":"Instagram"},"bar":{"language":"bar","value":"Instagram"},"br":{"language":"br","value":"Instagram"},"ca":{"language":"ca","value":"nom d'usuari d'Instagram"},"co":{"language":"co","value":"Instagram"},"cs":{"language":"cs","value":"jm\u00e9no na Instagramu"},"cy":{"language":"cy","value":"enw defnyddiwr Instagram"},"da":{"language":"da","value":"Instagram-konto"},"de-at":{"language":"de-at","value":"Instagram"},"de-ch":{"language":"de-ch","value":"Instagram"},"en-ca":{"language":"en-ca","value":"Instagram"},"en-gb":{"language":"en-gb","value":"Instagram"},"eo":{"language":"eo","value":"Instagram"},"et":{"language":"et","value":"Instagram"},"eu":{"language":"eu","value":"Instagrameko erabiltzaile izena"},"frp":{"language":"frp","value":"Instagram"},"fur":{"language":"fur","value":"Instagram"},"ga":{"language":"ga","value":"Instagram"},"gd":{"language":"gd","value":"Instagram"},"gl":{"language":"gl","value":"Instagram"},"gsw":{"language":"gsw","value":"Instagram"},"hr":{"language":"hr","value":"Instagram"},"hu":{"language":"hu","value":"Instagram-felhaszn\u00e1l\u00f3in\u00e9v"},"ia":{"language":"ia","value":"Instagram"},"id":{"language":"id","value":"Instagram"},"ie":{"language":"ie","value":"Instagram"},"io":{"language":"io","value":"Instagram"},"is":{"language":"is","value":"Instagram"},"kg":{"language":"kg","value":"Instagram"},"lb":{"language":"lb","value":"Instagram"},"li":{"language":"li","value":"Instagram"},"lij":{"language":"lij","value":"Instagram"},"mg":{"language":"mg","value":"Instagram"},"min":{"language":"min","value":"Instagram"},"ms":{"language":"ms","value":"Instagram"},"nap":{"language":"nap","value":"Instagram"},"nds":{"language":"nds","value":"Instagram"},"nds-nl":{"language":"nds-nl","value":"Instagram"},"nn":{"language":"nn","value":"Instagram-brukarnamn"},"nrm":{"language":"nrm","value":"Instagram"},"oc":{"language":"oc","value":"Instagram"},"pcd":{"language":"pcd","value":"Instagram"},"pl":{"language":"pl","value":"Instagram"},"pms":{"language":"pms","value":"Instagram"},"pt-br":{"language":"pt-br","value":"nome de usu\u00e1rio no Instagram"},"rm":{"language":"rm","value":"Instagram"},"ro":{"language":"ro","value":"Instagram"},"sc":{"language":"sc","value":"Instagram"},"scn":{"language":"scn","value":"Instagram"},"sco":{"language":"sco","value":"Instagram"},"sk":{"language":"sk","value":"Instagram"},"sl":{"language":"sl","value":"Instagram"},"sr-el":{"language":"sr-el","value":"Instagram"},"sw":{"language":"sw","value":"Instagram"},"tr":{"language":"tr","value":"Instagram"},"vec":{"language":"vec","value":"Instagram"},"vi":{"language":"vi","value":"Instagram"},"vls":{"language":"vls","value":"Instagram"},"vo":{"language":"vo","value":"Instagram"},"wa":{"language":"wa","value":"Instagram"},"wo":{"language":"wo","value":"Instagram"},"zu":{"language":"zu","value":"Instagram"},"sr":{"language":"sr","value":"\u0418\u043d\u0441\u0442\u0430\u0433\u0440\u0430\u043c"},"be":{"language":"be","value":"\u0443\u043b\u0456\u043a\u043e\u0432\u044b \u0437\u0430\u043f\u0456\u0441 \u0443 Instagram"},"ja":{"language":"ja","value":"\u30a4\u30f3\u30b9\u30bf\u30b0\u30e9\u30e0\u306e\u30e6\u30fc\u30b6\u30fc\u540d"},"ko":{"language":"ko","value":"\uc778\uc2a4\ud0c0\uadf8\ub7a8 \uacc4\uc815\uba85"},"th":{"language":"th","value":"\u0e0a\u0e37\u0e48\u0e2d\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49\u0e2d\u0e34\u0e19\u0e2a\u0e15\u0e32\u0e41\u0e01\u0e23\u0e21"},"he":{"language":"he","value":"\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05d1\u05d0\u05d9\u05e0\u05e1\u05d8\u05d2\u05e8\u05dd"},"ta":{"language":"ta","value":"\u0b87\u0ba9\u0bcd\u0bb8\u0bcd\u0b9f\u0bcd\u0b9f\u0bbe\u0b95\u0bbf\u0bb0\u0bbe\u0bae\u0bcd \u0baa\u0baf\u0ba9\u0bb0\u0bcd \u0baa\u0bc6\u0baf\u0bb0\u0bcd"},"mk":{"language":"mk","value":"\u043a\u043e\u0440\u0438\u0441\u043d\u0438\u0447\u043a\u043e \u0438\u043c\u0435 \u043d\u0430 \u0418\u043d\u0441\u0442\u0430\u0433\u0440\u0430\u043c"},"bn":{"language":"bn","value":"\u0987\u09a8\u09cd\u09b8\u099f\u09be\u0997\u09cd\u09b0\u09be\u09ae \u09ac\u09cd\u09af\u09ac\u09b9\u09be\u09b0\u0995\u09be\u09b0\u09c0 \u09a8\u09be\u09ae"},"zh-hk":{"language":"zh-hk","value":"Instagram\u5e33\u865f"},"lv":{"language":"lv","value":"Instagram lietot\u0101jv\u0101rds"},"zh-tw":{"language":"zh-tw","value":"Instagram\u5e33\u865f"},"zh-hant":{"language":"zh-hant","value":"Instagram\u5e33\u865f"},"ar":{"language":"ar","value":"\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0641\u064a \u0625\u0646\u0633\u062a\u063a\u0631\u0627\u0645"},"fa":{"language":"fa","value":"\u0646\u0634\u0627\u0646\u06cc \u0627\u06cc\u0646\u0633\u062a\u0627\u06af\u0631\u0627\u0645"},"zh-hans":{"language":"zh-hans","value":"Instagram\u8d26\u53f7"},"zh-mo":{"language":"zh-mo","value":"Instagram\u5e33\u865f"},"zh-cn":{"language":"zh-cn","value":"Instagram\u8d26\u53f7"},"zh-my":{"language":"zh-my","value":"Instagram\u8d26\u53f7"},"zh-sg":{"language":"zh-sg","value":"Instagram\u8d26\u53f7"}},"descriptions":{"en":{"language":"en","value":"item's username on Instagram"},"nl":{"language":"nl","value":"gebruikersnaam van deze entiteit op Instagram"},"es":{"language":"es","value":"nombre de usuario en Instagram"},"fr":{"language":"fr","value":"nom d'usager du site web Instagram"},"it":{"language":"it","value":"nome utente su Instagram"},"sv":{"language":"sv","value":"objektets anv\u00e4ndarnamn p\u00e5 Instagram"},"fi":{"language":"fi","value":"t\u00e4m\u00e4n kohteen k\u00e4ytt\u00e4j\u00e4nimi Instagramissa"},"nb":{"language":"nb","value":"objektets brukernavn p\u00e5 Instagram"},"de":{"language":"de","value":"Name des Instagramkontos eines Projekts / einer Person"},"ko":{"language":"ko","value":"\ud56d\ubaa9 \uc8fc\uc81c\uc758 \uc778\uc2a4\ud0c0\uadf8\ub7a8 \uacc4\uc815\uba85"},"bn":{"language":"bn","value":"\u0987\u09a8\u09cd\u09b8\u099f\u09be\u0997\u09cd\u09b0\u09be\u09ae\u09c7 \u0986\u0987\u099f\u09c7\u09ae\u09c7\u09b0 \u09ac\u09cd\u09af\u09ac\u09b9\u09be\u09b0\u0995\u09be\u09b0\u09c0 \u09a8\u09be\u09ae"},"pt-br":{"language":"pt-br","value":"nome de usu\u00e1rio do item no Instagram"},"pt":{"language":"pt","value":"nome de utilizador do item no Instagram"},"ca":{"language":"ca","value":"nom d'usuari de l'\u00edtem a Instagram"},"zh-hans":{"language":"zh-hans","value":"\u6b64\u9879\u5728Instagram\u4e0a\u7684\u7528\u6237\u540d"},"ja":{"language":"ja","value":"\u30a4\u30f3\u30b9\u30bf\u30b0\u30e9\u30e0\u306e\u30e6\u30fc\u30b6\u30fc\u540d\u3092\u8a18\u3059\u305f\u3081\u306e\u9805\u76ee"},"zh-mo":{"language":"zh-mo","value":"\u6b64\u9805\u5728Instagram\u4e0a\u7684\u7528\u6236\u540d"},"zh":{"language":"zh","value":"\u6b64\u9879\u5728Instagram\u4e0a\u7684\u7528\u6237\u540d"},"zh-cn":{"language":"zh-cn","value":"\u6b64\u9879\u5728Instagram\u4e0a\u7684\u7528\u6237\u540d"},"zh-hant":{"language":"zh-hant","value":"\u6b64\u9805\u5728Instagram\u4e0a\u7684\u7528\u6236\u540d"},"zh-hk":{"language":"zh-hk","value":"\u6b64\u9805\u5728Instagram\u4e0a\u7684\u7528\u6236\u540d"},"zh-my":{"language":"zh-my","value":"\u6b64\u9879\u5728Instagram\u4e0a\u7684\u7528\u6237\u540d"},"zh-sg":{"language":"zh-sg","value":"\u6b64\u9879\u5728Instagram\u4e0a\u7684\u7528\u6237\u540d"},"zh-tw":{"language":"zh-tw","value":"\u6b64\u9805\u5728Instagram\u4e0a\u7684\u7528\u6236\u540d"}},"aliases":{"ru":[{"language":"ru","value":"Instagram"},{"language":"ru","value":"\u0438\u043c\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f \u0432 Instagram"},{"language":"ru","value":"\u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0432 \u0418\u043d\u0441\u0442\u0430\u0433\u0440\u0430\u043c\u0435"},{"language":"ru","value":"\u0418\u043d\u0441\u0442\u0430\u0433\u0440\u0430\u043c"},{"language":"ru","value":"\u0443\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0432 \u0418\u043d\u0441\u0442\u0430\u0433\u0440\u0430\u043c\u0435"},{"language":"ru","value":"\u0430\u043a\u043a\u0430\u0443\u043d\u0442 \u0432 \u0418\u043d\u0441\u0442\u0430\u0433\u0440\u0430\u043c\u0435"},{"language":"ru","value":"\u0430\u043a\u043a\u0430\u0443\u043d\u0442 \u0432 Instagram"}],"en":[{"language":"en","value":"Instagram"}],"es":[{"language":"es","value":"Instagram"},{"language":"es","value":"usuario de Instagram"}],"be":[{"language":"be","value":"Instagram"}],"ja":[{"language":"ja","value":"Instagram\u306e\u30e6\u30fc\u30b6\u30fc\u540d"}],"ko":[{"language":"ko","value":"\uc778\uc2a4\ud0c0\uadf8\ub7a8"},{"language":"ko","value":"\uc778\uc2a4\ud0c0\uadf8\ub7a8 \uc0ac\uc6a9\uc790\uc774\ub984"},{"language":"ko","value":"\uc778\uc2a4\ud0c0\uadf8\ub7a8 \uacc4\uc815\uc774\ub984"},{"language":"ko","value":"\uc778\uc2a4\ud0c0\uadf8\ub7a8 \uc720\uc800\uc774\ub984"},{"language":"ko","value":"\uc778\uc2a4\ud0c0\uadf8\ub7a8 \uc544\uc774\ub514"},{"language":"ko","value":"\uacf5\uc2dd \uc778\uc2a4\ud0c0\uadf8\ub7a8"},{"language":"ko","value":"Instagram \uacc4\uc815\uba85"},{"language":"ko","value":"Instagram ID"},{"language":"ko","value":"\uc778\uc2a4\ud0c0\uadf8\ub7a8 ID"}],"ca":[{"language":"ca","value":"Instagram"}],"it":[{"language":"it","value":"Instagram"}],"cs":[{"language":"cs","value":"Instagram"}],"bn":[{"language":"bn","value":"\u0987\u09a8\u09cd\u09b8\u099f\u09be\u0997\u09cd\u09b0\u09be\u09ae"}],"pt-br":[{"language":"pt-br","value":"conta do Instagram"}],"pt":[{"language":"pt","value":"conta do Instagram"}],"sr":[{"language":"sr","value":"\u043a\u043e\u0440\u0438\u0441\u043d\u0438\u0447\u043a\u0438 \u043d\u0430\u043b\u043e\u0433 \u043d\u0430 \u0418\u043d\u0441\u0442\u0430\u0433\u0440\u0430\u043c\u0443"}],"cy":[{"language":"cy","value":"Instagram"}],"eu":[{"language":"eu","value":"Instagram"}],"fr":[{"language":"fr","value":"Instagram"}],"zh":[{"language":"zh","value":"Instagram\u5e33\u865f"}]},"claims":{"P1629":[{"mainsnak":{"snaktype":"value","property":"P1629","datavalue":{"value":{"entity-type":"item","numeric-id":209330,"id":"Q209330"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"P2003$284aaa70-4e73-05a9-e43a-ee7a13350062","rank":"normal"}],"P1630":[{"mainsnak":{"snaktype":"value","property":"P1630","datavalue":{"value":"https://www.instagram.com/$1/","type":"string"},"datatype":"string"},"type":"statement","id":"P2003$f45c40d1-4430-0f45-8017-5815ba437305","rank":"normal"}],"P1793":[{"mainsnak":{"snaktype":"value","property":"P1793","datavalue":{"value":"[a-z0-9_\\.]{1,30}","type":"string"},"datatype":"string"},"type":"statement","id":"P2003$ea66a66a-4be0-0538-c258-933fb4893160","rank":"normal"}],"P1855":[{"mainsnak":{"snaktype":"value","property":"P1855","datavalue":{"value":{"entity-type":"item","numeric-id":23548,"id":"Q23548"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","qualifiers":{"P2003":[{"snaktype":"value","property":"P2003","hash":"330cfcae58dd65abbf0cce987df5657e6e3de645","datavalue":{"value":"nasa","type":"string"},"datatype":"external-id"}]},"qualifiers-order":["P2003"],"id":"P2003$bc083ef4-4f89-127b-d73d-2f4b0f719591","rank":"normal"}],"P31":[{"mainsnak":{"snaktype":"value","property":"P31","datavalue":{"value":{"entity-type":"item","numeric-id":18608871,"id":"Q18608871"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"P2003$6f8d70b6-434d-d3d8-cd9f-ebe4d219c93a","rank":"normal"},{"mainsnak":{"snaktype":"value","property":"P31","datavalue":{"value":{"entity-type":"item","numeric-id":19847637,"id":"Q19847637"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"P2003$a0e2ce34-46c7-723f-4481-2ab422827738","rank":"normal"}],"P2302":[{"mainsnak":{"snaktype":"value","property":"P2302","datavalue":{"value":{"entity-type":"item","numeric-id":21502410,"id":"Q21502410"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"P2003$0471A7F5-C722-4532-AB30-FC63184D2252","rank":"normal"}],"P2875":[{"mainsnak":{"snaktype":"value","property":"P2875","datavalue":{"value":{"entity-type":"item","numeric-id":28511268,"id":"Q28511268"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"P2003$cdf590e2-4b97-fcc5-a66f-7222f590667b","rank":"normal"}]}}}} -------------------------------------------------------------------------------- /tests/fixtures/entities/P434.json: -------------------------------------------------------------------------------- 1 | {"entities":{"P434":{"pageid":12517786,"ns":120,"title":"Property:P434","lastrevid":425575104,"modified":"2017-01-08T10:28:21Z","type":"property","datatype":"external-id","id":"P434","labels":{"de":{"language":"de","value":"MusicBrainz-K\u00fcnstler-ID"},"en":{"language":"en","value":"MusicBrainz artist ID"},"fa":{"language":"fa","value":"\u0634\u0646\u0627\u0633\u06c0 \u0647\u0646\u0631\u0645\u0646\u062f \u062f\u0631 \u0645\u0648\u0632\u06cc\u06a9\u200c\u0628\u0631\u06cc\u0646\u0632"},"nl":{"language":"nl","value":"MusicBrainz-identificatiecode voor artiest"},"ja":{"language":"ja","value":"MusicBrainz\u30a2\u30fc\u30c6\u30a3\u30b9\u30c8ID"},"it":{"language":"it","value":"identificativo artista MusicBrainz"},"nds":{"language":"nds","value":"MusicBrainz-K\u00fcnstler-ID"},"da":{"language":"da","value":"MusicBrainz-kunstner-ID"},"zh-hant":{"language":"zh-hant","value":"MusicBrainz\u97f3\u6a02\u5bb6\u7de8\u865f"},"zh-tw":{"language":"zh-tw","value":"MusicBrainz\u97f3\u6a02\u5bb6\u7de8\u865f"},"fr":{"language":"fr","value":"identifiant MusicBrainz de l'artiste"},"ca":{"language":"ca","value":"identificador d'artista de MusicBrainz"},"pl":{"language":"pl","value":"identyfikator artysty MusicBrainz"},"es":{"language":"es","value":"identificador MusicBrainz de artista"},"he":{"language":"he","value":"\u05de\u05d6\u05d4\u05d4 \u05d0\u05de\u05df \u05d1\u05beMusicBrainz"},"en-gb":{"language":"en-gb","value":"MusicBrainz artist ID"},"pt":{"language":"pt","value":"identificador MusicBrainz de artista"},"ru":{"language":"ru","value":"\u043a\u043e\u0434 MusicBrainz \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044f"},"nb":{"language":"nb","value":"MusicBrainz-artist-ID"},"zh":{"language":"zh","value":"MusicBrainz\u97f3\u4e50\u5bb6\u7f16\u53f7"},"zh-hans":{"language":"zh-hans","value":"MusicBrainz\u97f3\u4e50\u5bb6\u7f16\u53f7"},"gl":{"language":"gl","value":"identificador MusicBrainz do/a artista"},"cs":{"language":"cs","value":"k\u00f3d MusicBrainz pro um\u011blce"},"el":{"language":"el","value":"\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03ba\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b7 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03c4\u03bf\u03c5 MusicBrainz"},"pt-br":{"language":"pt-br","value":"identificador MusicBrainz de artista"},"hi":{"language":"hi","value":"MusicBrainz \u0915\u0932\u093e\u0915\u093e\u0930 \u0906\u0908\u0921\u0940"},"lv":{"language":"lv","value":"MusicBrainz m\u0101kslinieka identifikators"},"uk":{"language":"uk","value":"\u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 MusicBrainz \u0432\u0438\u043a\u043e\u043d\u0430\u0432\u0446\u044f"},"sr":{"language":"sr","value":"\u041c\u0443\u0437\u0438\u043a\u0431\u0440\u0435\u0458\u043d\u0446 \u0438\u0437\u0432\u043e\u0452\u0430\u0447"},"sr-ec":{"language":"sr-ec","value":"MusicBrainz \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0438\u0437\u0432\u043e\u0452\u0430\u0447\u0430"},"sr-el":{"language":"sr-el","value":"MusicBrainz identifikator izvo\u0111a\u010da"},"oc":{"language":"oc","value":"identificant MusicBrainz de l'artista"},"sv":{"language":"sv","value":"MusicBrainz artist-ID"},"mk":{"language":"mk","value":"\u043d\u0430\u0437\u043d\u0430\u043a\u0430 \u043d\u0430 \u0438\u0437\u0432\u0435\u0434\u0443\u0432\u0430\u0447 \u043d\u0430 MusicBrainz"},"hu":{"language":"hu","value":"MusicBrainz-m\u0171v\u00e9szazonos\u00edt\u00f3"},"ka":{"language":"ka","value":"MusicBrainz-\u10d8\u10e1 \u10e8\u10d4\u10db\u10e1\u10e0\u10e3\u10da\u10d4\u10d1\u10da\u10d8\u10e1 \u10d8\u10d3\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10e2\u10dd\u10e0\u10d8"},"eo":{"language":"eo","value":"MusicBrainz-identigilo de artisto"},"sk":{"language":"sk","value":"identifik\u00e1tor umelca na MusicBrainz"},"ko":{"language":"ko","value":"\ubba4\uc9c1\ube0c\ub808\uc778\uc988 \uc74c\uc545\uac00 ID"},"be":{"language":"be","value":"\u0456\u0434\u044d\u043d\u0442\u044b\u0444\u0456\u043a\u0430\u0442\u0430\u0440 MusicBrainz \u0434\u043b\u044f \u0432\u044b\u043a\u0430\u043d\u0430\u045e\u0446\u044b"},"rm":{"language":"rm","value":"MusicBrainz artist ID"},"fi":{"language":"fi","value":"MusicBrainz-artistitunniste"},"nn":{"language":"nn","value":"MusicBrainz-artist-ID"},"nds-nl":{"language":"nds-nl","value":"MusicBrainz ID"},"ms":{"language":"ms","value":"ID artis MusicBrainz"},"be-tarask":{"language":"be-tarask","value":"\u0456\u0434\u044d\u043d\u0442\u044b\u0444\u0456\u043a\u0430\u0442\u0430\u0440 \u0432\u044b\u043a\u0430\u043d\u0430\u045e\u0446\u044b \u045e MusicBrainz"},"gu":{"language":"gu","value":"\u0aae\u0acd\u0aaf\u0ac1\u0a9d\u0abf\u0a95\u0aac\u0acd\u0ab0\u0ac7\u0a88\u0aa8\u0acd\u0a9d \u0a95\u0ab2\u0abe\u0a95\u0abe\u0ab0 \u0a93\u0ab3\u0a96"},"bs":{"language":"bs","value":"MusicBrainz ID oznaka"},"is":{"language":"is","value":"MusicBrainz h\u00f6fundan\u00famer"},"mzn":{"language":"mzn","value":"\u0647\u0646\u0631\u0645\u0646\u062f \u0634\u0646\u0627\u0633\u0647 \u0645\u0648\u0632\u06cc\u06a9\u200c\u0628\u0631\u06cc\u0646\u0632 \u062f\u0644\u0647"},"vi":{"language":"vi","value":"\u0111\u1ecbnh danh ngh\u1ec7 s\u0129 MusicBrainz"},"en-ca":{"language":"en-ca","value":"MusicBrainz artist ID"},"af":{"language":"af","value":"MusicBrainz-ID"},"ts":{"language":"ts","value":"Xihlawulekisi xa MusicBrainz"},"lb":{"language":"lb","value":"MusicBrainz-K\u00ebnschtler-ID"},"sco":{"language":"sco","value":"MusicBrainz airtist ID"},"zh-hk":{"language":"zh-hk","value":"MusicBrainz\u97f3\u6a02\u5bb6\u7de8\u865f"},"or":{"language":"or","value":"\u0b2e\u0b4d\u0b5f\u0b41\u0b1c\u0b3f\u0b15\u0b2c\u0b4d\u0b30\u0b47\u0b1e\u0b4d\u0b1c \u0b15\u0b33\u0b3e\u0b15\u0b3e\u0b30 \u0b2a\u0b30\u0b3f\u0b1a\u0b5f"},"zh-cn":{"language":"zh-cn","value":"MusicBrainz\u97f3\u4e50\u5bb6\u7f16\u53f7"},"id":{"language":"id","value":"Identifikasi MusicBrainz"},"ilo":{"language":"ilo","value":"ID ti artista ti MusicBrainz"},"sl":{"language":"sl","value":"MusicBrainz"},"ia":{"language":"ia","value":"identificator de artista MusicBrainz"},"ksh":{"language":"ksh","value":"MusicBrainz-K\u00f6nsler-K\u00e4nnong"},"eu":{"language":"eu","value":"MusicBrainz-en identifikatzailea"},"scn":{"language":"scn","value":"c\u00f2dici idintificativu MusicBrainz di l'artista"},"la":{"language":"la","value":"siglum apud MusicBrainz"},"et":{"language":"et","value":"MusicBrainzi esineja ID"},"cy":{"language":"cy","value":"dynodwr MusicBrainz (artist)"},"ar":{"language":"ar","value":"\u0645\u0639\u0631\u0641 \u0645\u064a\u0648\u0632\u0643 \u0628\u0631\u064a\u0646\u0632 \u0644\u0644\u0641\u0646\u0627\u0646\u064a\u0646"},"zh-mo":{"language":"zh-mo","value":"MusicBrainz\u97f3\u6a02\u5bb6\u7de8\u865f"},"zh-my":{"language":"zh-my","value":"MusicBrainz\u97f3\u4e50\u5bb6\u7f16\u53f7"},"zh-sg":{"language":"zh-sg","value":"MusicBrainz\u97f3\u4e50\u5bb6\u7f16\u53f7"}},"descriptions":{"de":{"language":"de","value":"Identifikationsnummer des K\u00fcnstlers (Musiker, Schriftsteller) in MusicBrainz"},"en":{"language":"en","value":"identifier for an artist per the MusicBrainz open music encyclopedia"},"fa":{"language":"fa","value":"\u0634\u0646\u0627\u0633\u06c0 \u0647\u0646\u0631\u0645\u0646\u062f \u0645\u0648\u0633\u06cc\u0642\u06cc \u062f\u0631 \u062f\u0627\u0646\u0634\u0646\u0627\u0645\u06c0 \u0622\u0632\u0627\u062f \u0645\u0648\u0633\u06cc\u0642\u06cc\u0627\u06cc\u06cc \u0645\u0648\u0632\u06cc\u06a9\u200c\u0628\u0631\u06cc\u0646\u0632"},"ja":{"language":"ja","value":"MusicBrainz\u30aa\u30fc\u30d7\u30f3\u97f3\u697d\u767e\u79d1\u4e8b\u5178\u306e\u30a2\u30fc\u30c6\u30a3\u30b9\u30c8ID"},"pl":{"language":"pl","value":"identyfikator artysty w otwartej encyklopedii muzycznej MusicBrainz"},"fr":{"language":"fr","value":"identifiant de l'artiste dans l'encyclop\u00e9die musicale MusicBrainz"},"cs":{"language":"cs","value":"k\u00f3d um\u011blce v hudebn\u00ed encyklopedii MusicBrainz"},"el":{"language":"el","value":"\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03ba\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b7 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03bf\u03b9\u03c7\u03c4\u03ae \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae \u03b5\u03b3\u03ba\u03c5\u03ba\u03bb\u03bf\u03c0\u03b1\u03af\u03b4\u03b5\u03b9\u03b1 MusicBrainz"},"it":{"language":"it","value":"ID dell'artista nell'enciclopedia musicale libera MusicBrainz"},"hu":{"language":"hu","value":"a MusicBrainz ny\u00edlt zenei enciklop\u00e9dia m\u0171v\u00e9szazonos\u00edt\u00f3ja"},"ca":{"language":"ca","value":"identificador de l'artista en l'enciclop\u00e8dia oberta de m\u00fasica MusicBrainz"},"gu":{"language":"gu","value":"\u0aae\u0acd\u0aaf\u0ac1\u0a9d\u0abf\u0a95\u0aac\u0acd\u0ab0\u0ac7\u0a88\u0aa8\u0acd\u0a9d \u0aae\u0ac1\u0a95\u0acd\u0aa4 \u0ab8\u0a82\u0a97\u0ac0\u0aa4 \u0a9c\u0acd\u0a9e\u0abe\u0aa8\u0a95\u0acb\u0ab7 \u0a85\u0aa8\u0ac1\u0ab8\u0abe\u0ab0 \u0a95\u0ab2\u0abe\u0a95\u0abe\u0ab0\u0aa8\u0ac0 \u0a93\u0ab3\u0a96"},"en-ca":{"language":"en-ca","value":"artist identifier per the MusicBrainz open music encyclopedia"},"af":{"language":"af","value":"identifikasie by MusicBrainz"},"da":{"language":"da","value":"kunstnerens ID i MusicBrainz"},"ko":{"language":"ko","value":"\ud56d\ubaa9 \uc8fc\uc81c\uc778 \uc74c\uc545\uac00\uc5d0 \ub300\ud574 \ubba4\uc9c1\ube0c\ub808\uc778\uc988(MusicBrainz)\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc2dd\ubcc4\uc790"},"ilo":{"language":"ilo","value":"panagilasin ti artista iti silulukat nga ensiklopedia ti MusicBrainz"},"ksh":{"language":"ksh","value":"de K\u00e4nnong vun enem Mussiker, Schriever, M\u00f6hler, un esu, bei MusicBrainz"},"scn":{"language":"scn","value":"c\u00f2dici idintificativu assignatu a st'artista nta l'enciclupid\u00eca aperta d\u00e2 musica MusicBrainz"},"ru":{"language":"ru","value":"\u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0438\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044f \u0432 \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u0439 \u043c\u0443\u0437\u044b\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u044d\u043d\u0446\u0438\u043a\u043b\u043e\u043f\u0435\u0434\u0438\u0438"},"es":{"language":"es","value":"n\u00famero de indentificaci\u00f3n de artista en la enciclopedia MusicBrainz"},"nb":{"language":"nb","value":"artist-id hos MusicBrainz"}},"aliases":{"en":[{"language":"en","value":"artist MBID"},{"language":"en","value":"BBC Music artist ID"},{"language":"en","value":"MBID artist"},{"language":"en","value":"artist ID"}],"ru":[{"language":"ru","value":"MusicBrainz ID \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044f"},{"language":"ru","value":"\u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 MusicBrainz \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044f"}],"cs":[{"language":"cs","value":"MBID um\u011blce"},{"language":"cs","value":"MusicBrainz pro um\u011blce"}],"eo":[{"language":"eo","value":"MBID"}],"ro":[{"language":"ro","value":"MBID"}],"yi":[{"language":"yi","value":"MBID"}],"de":[{"language":"de","value":"MusicBrainz-Musiker-ID"},{"language":"de","value":"MBA"}],"fr":[{"language":"fr","value":"MusicBrainz de l'artiste"}],"es":[{"language":"es","value":"artista MBID"},{"language":"es","value":"MusicBrainz del artista"},{"language":"es","value":"identificador artista MusicBrainz"},{"language":"es","value":"identificador de artista MusicBrainz"}],"ko":[{"language":"ko","value":"\ubba4\uc9c1\ube0c\ub808\uc778\uc988 \uc544\ud2f0\uc2a4\ud2b8 ID"},{"language":"ko","value":"\uc544\ud2f0\uc2a4\ud2b8 ID"},{"language":"ko","value":"MusicBrainz \uc544\ud2f0\uc2a4\ud2b8 ID"},{"language":"ko","value":"MusicBrainz \uc608\uc220\uac00 ID"},{"language":"ko","value":"\uc608\uc220\uac00 ID"},{"language":"ko","value":"MusicBrainz \uc74c\uc545\uac00 ID"},{"language":"ko","value":"\ubba4\uc9c1\ube0c\ub808\uc778\uc988 \uc608\uc220\uac00 ID"},{"language":"ko","value":"\uc74c\uc545\uac00 ID"}],"sr":[{"language":"sr","value":"MusicBrainz \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0438\u0437\u0432\u043e\u0452\u0430\u0447\u0430"}]},"claims":{"P31":[{"mainsnak":{"snaktype":"value","property":"P31","datavalue":{"value":{"entity-type":"item","numeric-id":19595382,"id":"Q19595382"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"P434$df482c2b-427d-0664-4e59-e8b1ce471e20","rank":"normal"},{"mainsnak":{"snaktype":"value","property":"P31","datavalue":{"value":{"entity-type":"item","numeric-id":21745557,"id":"Q21745557"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"P434$1f9d9f77-417d-60f7-3ab0-724d3b3bb4cc","rank":"normal"},{"mainsnak":{"snaktype":"value","property":"P31","datavalue":{"value":{"entity-type":"item","numeric-id":24075706,"id":"Q24075706"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"P434$e4cc80e9-44db-4a24-d26d-a704faaf402a","rank":"normal"}],"P1630":[{"mainsnak":{"snaktype":"value","property":"P1630","datavalue":{"value":"https://musicbrainz.org/artist/$1","type":"string"},"datatype":"string"},"type":"statement","qualifiers":{"P137":[{"snaktype":"value","property":"P137","hash":"1f760ebd600cfce2d95ac7fdfe34a327c30c4569","datavalue":{"value":{"entity-type":"item","numeric-id":14005,"id":"Q14005"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"qualifiers-order":["P137"],"id":"P434$2CBB5F81-31E7-45E7-9A87-5DAE8193207F","rank":"preferred","references":[{"hash":"a151d5bf1b7e92fc8f0c34de02f422e09fc2fc28","snaks":{"P854":[{"snaktype":"value","property":"P854","datavalue":{"value":"https://www.wikidata.org/w/index.php?title=MediaWiki:Gadget-AuthorityControl.js&oldid=179329592","type":"string"},"datatype":"url"}]},"snaks-order":["P854"]}]}],"P1793":[{"mainsnak":{"snaktype":"value","property":"P1793","datavalue":{"value":"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}","type":"string"},"datatype":"string"},"type":"statement","qualifiers":{"P2916":[{"snaktype":"value","property":"P2916","hash":"8f016e5d0142bcbe7975a6c7bfbf38a1cd3b4187","datavalue":{"value":{"text":"UUID format string, 36 characters, see [[Q195284]]","language":"en"},"type":"monolingualtext"},"datatype":"monolingualtext"},{"snaktype":"value","property":"P2916","hash":"a7d01ee845c64f1ee23f91785accfe0e1c3ca075","datavalue":{"value":{"text":"cha\u00eene de 36 caract\u00e8res, format UUID, voir [[Q195284]]","language":"fr"},"type":"monolingualtext"},"datatype":"monolingualtext"}]},"qualifiers-order":["P2916"],"id":"P434$587c6402-4895-990f-02ac-1623e3c38d27","rank":"normal"}],"P1629":[{"mainsnak":{"snaktype":"value","property":"P1629","datavalue":{"value":{"entity-type":"item","numeric-id":19832969,"id":"Q19832969"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"P434$7cdfe4c5-44d0-54f0-344f-2b508ac44422","rank":"normal"}],"P1921":[{"mainsnak":{"snaktype":"value","property":"P1921","datavalue":{"value":"http://musicbrainz.org/$1/artist","type":"string"},"datatype":"string"},"type":"statement","id":"P434$e8fc9d91-409e-7288-d35a-8fb62810043b","rank":"normal"}],"P1855":[{"mainsnak":{"snaktype":"value","property":"P1855","datavalue":{"value":{"entity-type":"item","numeric-id":1299,"id":"Q1299"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","qualifiers":{"P434":[{"snaktype":"value","property":"P434","hash":"ba98afec52694b476e34237a56dfc83def96fc7d","datavalue":{"value":"b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d","type":"string"},"datatype":"external-id"}]},"qualifiers-order":["P434"],"id":"P434$31c7f8ec-48b1-df39-a6c2-f0c580861fd1","rank":"normal"},{"mainsnak":{"snaktype":"value","property":"P1855","datavalue":{"value":{"entity-type":"item","numeric-id":2599,"id":"Q2599"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","qualifiers":{"P434":[{"snaktype":"value","property":"P434","hash":"6ea5e9f0a3e3063407d7a6ad872b00da78e9dbf7","datavalue":{"value":"ba550d0e-adac-4864-b88b-407cab5e76af","type":"string"},"datatype":"external-id"}]},"qualifiers-order":["P434"],"id":"P434$63e590c3-4a14-5179-445c-9df21ccac640","rank":"normal"}],"P1896":[{"mainsnak":{"snaktype":"value","property":"P1896","datavalue":{"value":"http://musicbrainz.org/doc/MusicBrainz_Identifier","type":"string"},"datatype":"url"},"type":"statement","id":"P434$acf139a7-49f5-0ad0-b136-ec2dfa16ba6d","rank":"normal"}],"P2378":[{"mainsnak":{"snaktype":"value","property":"P2378","datavalue":{"value":{"entity-type":"item","numeric-id":14005,"id":"Q14005"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"P434$135ecd72-45aa-8ac0-83e3-ce4e0a00446d","rank":"normal"}],"P2302":[{"mainsnak":{"snaktype":"value","property":"P2302","datavalue":{"value":{"entity-type":"item","numeric-id":21502410,"id":"Q21502410"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"P434$764026EB-7164-44FB-AAEC-DDA421AB42AB","rank":"normal"}],"P3303":[{"mainsnak":{"snaktype":"value","property":"P3303","datavalue":{"value":"http://www.bbc.co.uk/music/artists/$1","type":"string"},"datatype":"string"},"type":"statement","qualifiers":{"P137":[{"snaktype":"value","property":"P137","hash":"d860812a6ca544c08da612fe41ab0691445c544a","datavalue":{"value":{"entity-type":"item","numeric-id":4834855,"id":"Q4834855"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"qualifiers-order":["P137"],"id":"P434$506dda4c-49ec-3a0f-8825-4f122472f0dd","rank":"normal"}]}}}} -------------------------------------------------------------------------------- /tests/fixtures/entities/Q16231742.json: -------------------------------------------------------------------------------- 1 | {"entities":{"Q3571994":{"pageid":3400672,"ns":0,"title":"Q3571994","lastrevid":530445734,"modified":"2017-08-02T03:49:17Z","type":"item","id":"Q3571994","labels":{"fr":{"language":"fr","value":"Yasuo Namekawa"},"en":{"language":"en","value":"Kangnam"},"ko":{"language":"ko","value":"\uac15\ub0a8"},"zh-tw":{"language":"zh-tw","value":"\u6ed1\u5ddd\u5eb7\u7537"},"zh":{"language":"zh","value":"\u6ed1\u5ddd\u5eb7\u7537"},"nl":{"language":"nl","value":"Kangnam"},"ja":{"language":"ja","value":"\u6ed1\u5ddd\u5eb7\u7537"},"ru":{"language":"ru","value":"\u041d\u0430\u043c\u044d\u043a\u0430\u0432\u0430, \u042f\u0441\u0443\u043e"},"id":{"language":"id","value":"Kangnam"}},"descriptions":{"en":{"language":"en","value":"Japanese singer"},"nl":{"language":"nl","value":"Japans zanger"},"ca":{"language":"ca","value":"cantant japon\u00e8s"},"he":{"language":"he","value":"\u05d6\u05de\u05e8 \u05d9\u05e4\u05e0\u05d9"},"es":{"language":"es","value":"cantante japon\u00e9s"},"gl":{"language":"gl","value":"cantante xapon\u00e9s"},"ar":{"language":"ar","value":"\u0645\u063a\u0646\u064a \u064a\u0627\u0628\u0627\u0646\u064a"},"fr":{"language":"fr","value":"chanteur japonais"},"bn":{"language":"bn","value":"\u099c\u09be\u09aa\u09be\u09a8\u09bf \u0997\u09be\u09af\u09bc\u0995"},"fa":{"language":"fa","value":"\u062e\u0648\u0627\u0646\u0646\u062f\u0647 \u0698\u0627\u067e\u0646\u06cc"},"zh-tw":{"language":"zh-tw","value":"M.I.B\u6210\u54e1"}},"aliases":{"en":[{"language":"en","value":"Kang-nam"},{"language":"en","value":"Yasuo Namekawa"},{"language":"en","value":"Namekawa Yasuo"}],"fr":[{"language":"fr","value":"Kangnam"}],"zh-tw":[{"language":"zh-tw","value":"\u5eb7\u7537"}],"zh":[{"language":"zh","value":"\u5eb7\u7537"}],"ko":[{"language":"ko","value":"\uac15\u3000\ub0a8"}],"ru":[{"language":"ru","value":"\u042f\u0441\u0443\u043e \u041d\u0430\u043c\u044d\u043a\u0430\u0432\u0430"}]},"claims":{"P569":[{"mainsnak":{"snaktype":"value","property":"P569","datavalue":{"value":{"time":"+1987-03-23T00:00:00Z","timezone":0,"before":0,"after":0,"precision":11,"calendarmodel":"http://www.wikidata.org/entity/Q1985727"},"type":"time"},"datatype":"time"},"type":"statement","id":"Q3571994$901E68D1-9390-4D81-93BA-493B71F613C3","rank":"normal","references":[{"hash":"d4bd87b862b12d99d26e86472d44f26858dee639","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":8447,"id":"Q8447"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P31":[{"mainsnak":{"snaktype":"value","property":"P31","datavalue":{"value":{"entity-type":"item","numeric-id":5,"id":"Q5"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q3571994$ED40E794-CAAB-428D-963B-6468E331F548","rank":"normal"}],"P21":[{"mainsnak":{"snaktype":"value","property":"P21","datavalue":{"value":{"entity-type":"item","numeric-id":6581097,"id":"Q6581097"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q3571994$17C6EDB1-4BCA-4118-B0E2-1B5BC74E2288","rank":"normal"}],"P106":[{"mainsnak":{"snaktype":"value","property":"P106","datavalue":{"value":{"entity-type":"item","numeric-id":177220,"id":"Q177220"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q3571994$305271A3-FE50-4E03-8707-3BAADE662980","rank":"normal"}],"P27":[{"mainsnak":{"snaktype":"value","property":"P27","datavalue":{"value":{"entity-type":"item","numeric-id":17,"id":"Q17"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q3571994$9694E40E-8FA4-4968-835D-3F42F3431F7C","rank":"normal"},{"mainsnak":{"snaktype":"value","property":"P27","datavalue":{"value":{"entity-type":"item","numeric-id":884,"id":"Q884"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q3571994$3056A36D-2FF4-4531-880E-70DEAB567EAD","rank":"normal"}],"P136":[{"mainsnak":{"snaktype":"value","property":"P136","datavalue":{"value":{"entity-type":"item","numeric-id":213665,"id":"Q213665"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q3571994$D14D7425-4553-45E4-9039-45C431159624","rank":"normal","references":[{"hash":"d4bd87b862b12d99d26e86472d44f26858dee639","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":8447,"id":"Q8447"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]},{"mainsnak":{"snaktype":"value","property":"P136","datavalue":{"value":{"entity-type":"item","numeric-id":11401,"id":"Q11401"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q3571994$F938CD82-1C52-47BF-AE3E-B2FF27B5E969","rank":"normal","references":[{"hash":"fa278ebfc458360e5aed63d5058cca83c46134f1","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":328,"id":"Q328"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P18":[{"mainsnak":{"snaktype":"value","property":"P18","datavalue":{"value":"Kangnam (Namekawa Yasuo).jpg","type":"string"},"datatype":"commonsMedia"},"type":"statement","id":"Q3571994$45B826D8-2BAB-4480-B843-7FF86DFA153D","rank":"normal"}],"P2002":[{"mainsnak":{"snaktype":"value","property":"P2002","datavalue":{"value":"Kangnam11","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q3571994$2B95C5FE-6BFA-4DA7-9ACB-D51C5E3D2118","rank":"normal","references":[{"hash":"0ee3b3ba1c958f4c3dcba7ed8091fe4b57311348","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":30239,"id":"Q30239"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P2003":[{"mainsnak":{"snaktype":"value","property":"P2003","datavalue":{"value":"kangkangnam","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q3571994$7DDD5B0D-2C03-4EB5-9B25-509ADCA2C4B3","rank":"normal","references":[{"hash":"0ee3b3ba1c958f4c3dcba7ed8091fe4b57311348","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":30239,"id":"Q30239"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P434":[{"mainsnak":{"snaktype":"value","property":"P434","datavalue":{"value":"afdb00b8-a7b2-4041-a479-e205d90c8e20","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q3571994$259A748B-26BA-4336-B3F7-9D6C926300DD","rank":"normal"}],"P1814":[{"mainsnak":{"snaktype":"value","property":"P1814","datavalue":{"value":"\u306a\u3081\u304b\u308f \u3084\u3059\u304a","type":"string"},"datatype":"string"},"type":"statement","id":"Q3571994$40FCBEAC-3824-4C34-A3E7-A0A1C2FEE2BB","rank":"normal","references":[{"hash":"0ee3b3ba1c958f4c3dcba7ed8091fe4b57311348","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":30239,"id":"Q30239"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P19":[{"mainsnak":{"snaktype":"value","property":"P19","datavalue":{"value":{"entity-type":"item","numeric-id":1490,"id":"Q1490"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q3571994$04B2B2EE-1189-40C9-9048-ED767DE36E01","rank":"normal","references":[{"hash":"fa278ebfc458360e5aed63d5058cca83c46134f1","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":328,"id":"Q328"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P264":[{"mainsnak":{"snaktype":"value","property":"P264","datavalue":{"value":{"entity-type":"item","numeric-id":625631,"id":"Q625631"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q3571994$17FC21F6-1FDF-4CF3-9EEA-BAFD622FA5AF","rank":"normal","references":[{"hash":"fa278ebfc458360e5aed63d5058cca83c46134f1","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":328,"id":"Q328"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P646":[{"mainsnak":{"snaktype":"value","property":"P646","datavalue":{"value":"/m/0vxdzcb","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q3571994$11F6524E-209D-4CF0-921F-8E1E03A4E633","rank":"normal","references":[{"hash":"f9f729b8b3ecf4dcac2cf76f377bca0a17e808f7","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":648625,"id":"Q648625"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P3579":[{"mainsnak":{"snaktype":"value","property":"P3579","datavalue":{"value":"/u/5611386725","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q3571994$9D404844-8B38-49ED-9230-825826AE6EBA","rank":"normal","references":[{"hash":"0ee3b3ba1c958f4c3dcba7ed8091fe4b57311348","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":30239,"id":"Q30239"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P856":[{"mainsnak":{"snaktype":"value","property":"P856","datavalue":{"value":"http://kangnam.jp/","type":"string"},"datatype":"url"},"type":"statement","id":"Q3571994$FB052882-DDBB-4A82-BF91-1FD697F9ACF4","rank":"normal","references":[{"hash":"a29a646602abf65105ed0f39a44231c962ece9ee","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":177837,"id":"Q177837"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}]},"sitelinks":{"enwiki":{"site":"enwiki","title":"Kangnam (singer)","badges":[],"url":"https://en.wikipedia.org/wiki/Kangnam_(singer)"},"frwiki":{"site":"frwiki","title":"Yasuo Namekawa","badges":[],"url":"https://fr.wikipedia.org/wiki/Yasuo_Namekawa"},"idwiki":{"site":"idwiki","title":"Kangnam (penyanyi)","badges":[],"url":"https://id.wikipedia.org/wiki/Kangnam_(penyanyi)"},"jawiki":{"site":"jawiki","title":"KangNam","badges":[],"url":"https://ja.wikipedia.org/wiki/KangNam"},"kowiki":{"site":"kowiki","title":"\uac15\ub0a8 (\uac00\uc218)","badges":[],"url":"https://ko.wikipedia.org/wiki/%EA%B0%95%EB%82%A8_(%EA%B0%80%EC%88%98)"},"zhwiki":{"site":"zhwiki","title":"\u6ed1\u5ddd\u5eb7\u7537","badges":[],"url":"https://zh.wikipedia.org/wiki/%E6%BB%91%E5%B7%9D%E5%BA%B7%E7%94%B7"}}}}} -------------------------------------------------------------------------------- /tests/fixtures/entities/Q33281.json: -------------------------------------------------------------------------------- 1 | {"entities":{"Q33281":{"pageid":36128,"ns":0,"title":"Q33281","lastrevid":493456804,"modified":"2017-06-02T06:19:02Z","type":"item","id":"Q33281","labels":{"zh":{"language":"zh","value":"\u67e5\u74e6\u5361\u8afe\u8a9e"},"pms":{"language":"pms","value":"Lenga chavacano"},"pl":{"language":"pl","value":"J\u0119zyk chavacano"},"eu":{"language":"eu","value":"Chavacano"},"ko":{"language":"ko","value":"\ucc28\ubc14\uce74\ub178\uc5b4"},"fr":{"language":"fr","value":"Chavacano"},"es":{"language":"es","value":"chabacano"},"ta":{"language":"ta","value":"\u0b9a\u0bb5\u0b95\u0bbe\u0ba9 \u0bae\u0bca\u0bb4\u0bbf"},"ceb":{"language":"ceb","value":"Pinulongang Tsabakano"},"hu":{"language":"hu","value":"Chabacano nyelv"},"cbk-zam":{"language":"cbk-zam","value":"Chavacano"},"it":{"language":"it","value":"zamboangue\u00f1o"},"de":{"language":"de","value":"Chabacano"},"ja":{"language":"ja","value":"\u30c1\u30e3\u30d0\u30ab\u30ce\u8a9e"},"br":{"language":"br","value":"Chabacano"},"vi":{"language":"vi","value":"ti\u1ebfng Chavacano"},"ia":{"language":"ia","value":"Lingua chavacan"},"bcl":{"language":"bcl","value":"Tataramon na Zamboangue\u00f1o"},"sv":{"language":"sv","value":"Chavacano"},"pt":{"language":"pt","value":"chavacano"},"eo":{"language":"eo","value":"\u0108abakana lingvo"},"ru":{"language":"ru","value":"\u0427\u0430\u0431\u0430\u043a\u0430\u043d\u043e"},"en":{"language":"en","value":"Chavacano"},"tr":{"language":"tr","value":"Chabacano"},"th":{"language":"th","value":"\u0e20\u0e32\u0e29\u0e32\u0e0a\u0e32\u0e1a\u0e32\u0e01\u0e32\u0e42\u0e19"},"hak":{"language":"hak","value":"Chavacano-ng\u00ee"},"uk":{"language":"uk","value":"\u0427\u0430\u0432\u0430\u043a\u0430\u043d\u043e"},"la":{"language":"la","value":"Chavacano"},"ilo":{"language":"ilo","value":"Pagsasao a Chabacano"},"cs":{"language":"cs","value":"Chavacano"},"hr":{"language":"hr","value":"Chavacano jezik"},"tl":{"language":"tl","value":"Wikang Zamboangue\u00f1o"},"fa":{"language":"fa","value":"\u0632\u0628\u0627\u0646 \u0686\u0627\u0628\u0627\u06a9\u0627\u0646\u0648"},"ca":{"language":"ca","value":"Chavacano"},"nl":{"language":"nl","value":"Chavacano"},"mk":{"language":"mk","value":"\u0447\u0430\u0432\u0430\u043a\u0430\u043d\u0441\u043a\u0438 \u0458\u0430\u0437\u0438\u043a"},"oc":{"language":"oc","value":"Chavacan"},"ar":{"language":"ar","value":"\u0644\u063a\u0629 \u062a\u0634\u0627\u0628\u0627\u0643\u0627\u0646\u0648"},"gl":{"language":"gl","value":"Chavacano"}},"descriptions":{"en":{"language":"en","value":"Spanish-based creole language spoken in the Philippines"},"ilo":{"language":"ilo","value":"Naibatay ti pagsasao nga Espaniol a kreol nga insasao idiay Pilipinas"},"es":{"language":"es","value":"lengua criolla de Filipinas"},"de":{"language":"de","value":"Sprache"},"it":{"language":"it","value":"lingua chabacana"},"nl":{"language":"nl","value":"Creooltaal met Spaanse woorden in de Filipijnen"},"mk":{"language":"mk","value":"\u0458\u0430\u0437\u0438\u043a \u043d\u0430 \u0424\u0438\u043b\u0438\u043f\u0438\u043d\u0438\u0442\u0435"},"he":{"language":"he","value":"\u05e9\u05e4\u05d4"}},"aliases":{"zh":[{"language":"zh","value":"\u4e09\u5bf6\u984f\u67e5\u74e6\u5361\u8afe\u8a9e"}],"eu":[{"language":"eu","value":"Chabacano"}],"fr":[{"language":"fr","value":"Chabacano"},{"language":"fr","value":"Zamboangue\u00f1o"},{"language":"fr","value":"cbk-zam"}],"es":[{"language":"es","value":"Chavacano"},{"language":"es","value":"Idioma chavacano"},{"language":"es","value":"Chavacano de Zamboanga"},{"language":"es","value":"Idioma chabacano"}],"ceb":[{"language":"ceb","value":"Chavacano"},{"language":"ceb","value":"Tsinabakano"},{"language":"ceb","value":"Tsabakano"},{"language":"ceb","value":"Chabacano"}],"hu":[{"language":"hu","value":"Chavacano nyelv"},{"language":"hu","value":"F\u00fcl\u00f6p-szigeteki spanyol kreol nyelv"}],"cbk-zam":[{"language":"cbk-zam","value":"Chabacano"},{"language":"cbk-zam","value":"Chavacano Language"}],"it":[{"language":"it","value":"lingua zamboangue\u00f1a"},{"language":"it","value":"lingua chabacana"},{"language":"it","value":"lingua chavacana"},{"language":"it","value":"chavacano"},{"language":"it","value":"lingua chabakana"},{"language":"it","value":"chavacano di Zamboanga"}],"de":[{"language":"de","value":"Zamboanguenos"},{"language":"de","value":"Zamboangue\u00f1os"},{"language":"de","value":"Chavacano"},{"language":"de","value":"Chavakano"},{"language":"de","value":"cbk-zam"}],"ja":[{"language":"ja","value":"\u30b5\u30f3\u30dc\u30a2\u30f3\u30ac\u30fb\u30c1\u30e3\u30d0\u30ab\u30ce\u8a9e"}],"ia":[{"language":"ia","value":"Lingua chavacano"},{"language":"ia","value":"Chavacano"},{"language":"ia","value":"Chabacano"},{"language":"ia","value":"Zamboangue\u00f1o"}],"bcl":[{"language":"bcl","value":"Zamboangue\u00f1o"}],"sv":[{"language":"sv","value":"Zamboangueno"},{"language":"sv","value":"Chabacano"},{"language":"sv","value":"Zamboangue\u00f1o"}],"pt":[{"language":"pt","value":"Chavacano de Zamboanga"},{"language":"pt","value":"Chabacano"},{"language":"pt","value":"L\u00edngua chavacana"}],"eo":[{"language":"eo","value":"\u0108avaka lingvo"},{"language":"eo","value":"\u0108avakano"}],"ru":[{"language":"ru","value":"\u0427\u0430\u0432\u0430\u043a\u0430\u043d\u043e"},{"language":"ru","value":"\u0427\u0430\u0432\u0430\u043a\u0430\u043d\u043e \u044f\u0437\u044b\u043a"}],"th":[{"language":"th","value":"\u0e20\u0e32\u0e29\u0e32\u0e0a\u0e32\u0e27\u0e32\u0e04\u0e32\u0e42\u0e19"}],"la":[{"language":"la","value":"Chabacani"},{"language":"la","value":"Chabacano"}],"ilo":[{"language":"ilo","value":"Pagsasao a Chavacano"}],"tl":[{"language":"tl","value":"Wikang Tsabakano"},{"language":"tl","value":"Zamboangueno/Chavacano"},{"language":"tl","value":"Chabacano"},{"language":"tl","value":"Wikang Chabacano"},{"language":"tl","value":"Wikang Zamboangueno"},{"language":"tl","value":"Chavacano"},{"language":"tl","value":"Tsabakano"},{"language":"tl","value":"Wikang Chavacano"}],"en":[{"language":"en","value":"cbk-zam"},{"language":"en","value":"Chavacano language"}],"mk":[{"language":"mk","value":"\u0447\u0430\u0432\u0430\u043a\u0430\u043d\u043e"}]},"claims":{"P220":[{"mainsnak":{"snaktype":"value","property":"P220","datavalue":{"value":"cbk","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q33281$C181B018-2884-4560-9873-C17315D97E95","rank":"normal","references":[{"hash":"fa278ebfc458360e5aed63d5058cca83c46134f1","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":328,"id":"Q328"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P31":[{"mainsnak":{"snaktype":"value","property":"P31","datavalue":{"value":{"entity-type":"item","numeric-id":34770,"id":"Q34770"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q33281$FDB02052-3E85-439A-B9E5-C17BD169EC88","rank":"normal"},{"mainsnak":{"snaktype":"value","property":"P31","datavalue":{"value":{"entity-type":"item","numeric-id":33289,"id":"Q33289"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q33281$2de7134a-4e17-88b7-f863-0b15ccf20620","rank":"normal"}],"P646":[{"mainsnak":{"snaktype":"value","property":"P646","datavalue":{"value":"/m/02zjbp","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q33281$AE9AC997-3E1B-4167-822D-4BDAD80B3693","rank":"normal","references":[{"hash":"2b00cb481cddcac7623114367489b5c194901c4a","snaks":{"P248":[{"snaktype":"value","property":"P248","datavalue":{"value":{"entity-type":"item","numeric-id":15241312,"id":"Q15241312"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}],"P577":[{"snaktype":"value","property":"P577","datavalue":{"value":{"time":"+2013-10-28T00:00:00Z","timezone":0,"before":0,"after":0,"precision":11,"calendarmodel":"http://www.wikidata.org/entity/Q1985727"},"type":"time"},"datatype":"time"}]},"snaks-order":["P248","P577"]}]}],"P424":[{"mainsnak":{"snaktype":"value","property":"P424","datavalue":{"value":"cbk-zam","type":"string"},"datatype":"string"},"type":"statement","qualifiers":{"P794":[{"snaktype":"value","property":"P794","hash":"23d291c4efb6e837e48100bfab67b81c93e677a5","datavalue":{"value":{"entity-type":"item","numeric-id":22283033,"id":"Q22283033"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"qualifiers-order":["P794"],"id":"Q33281$EB4039A9-FAE7-4B65-9553-7FAEE4AD82F2","rank":"normal"}],"P305":[{"mainsnak":{"snaktype":"value","property":"P305","datavalue":{"value":"cbk","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q33281$AC634F7B-BC9B-467E-9AD6-BE5A3757B328","rank":"normal","references":[{"hash":"d4bd87b862b12d99d26e86472d44f26858dee639","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":8447,"id":"Q8447"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P282":[{"mainsnak":{"snaktype":"value","property":"P282","datavalue":{"value":{"entity-type":"item","numeric-id":8229,"id":"Q8229"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q33281$51157829-AD19-422B-B2C2-198F2D894784","rank":"normal"}],"P1627":[{"mainsnak":{"snaktype":"value","property":"P1627","datavalue":{"value":"cbk","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q33281$C2B66615-A5AD-4619-9E9C-C41841D5FA66","rank":"normal"}],"P1705":[{"mainsnak":{"snaktype":"value","property":"P1705","datavalue":{"value":{"text":"Chavacano","language":"cbk-zam"},"type":"monolingualtext"},"datatype":"monolingualtext"},"type":"statement","id":"Q33281$154F8DE1-EBB5-4ACD-B5AF-0996A9AB7486","rank":"normal"}],"P1098":[{"mainsnak":{"snaktype":"value","property":"P1098","datavalue":{"value":{"amount":"+300000","unit":"1","upperBound":"+300000","lowerBound":"+300000"},"type":"quantity"},"datatype":"quantity"},"type":"statement","qualifiers":{"P585":[{"snaktype":"value","property":"P585","hash":"e7a1014aabf7385ef5bf93bb24cfe308ca42cdbe","datavalue":{"value":{"time":"+1990-01-01T00:00:00Z","timezone":0,"before":0,"after":0,"precision":9,"calendarmodel":"http://www.wikidata.org/entity/Q1985727"},"type":"time"},"datatype":"time"}]},"qualifiers-order":["P585"],"id":"Q33281$CB2F7901-22AA-4FDD-BEB0-733CAEA8BFBA","rank":"normal"}],"P269":[{"mainsnak":{"snaktype":"value","property":"P269","datavalue":{"value":"135615437","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q33281$9EED6FA5-CBCA-4261-932C-F46EB01EF1BE","rank":"normal","references":[{"hash":"d4bd87b862b12d99d26e86472d44f26858dee639","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":8447,"id":"Q8447"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P279":[{"mainsnak":{"snaktype":"value","property":"P279","datavalue":{"value":{"entity-type":"item","numeric-id":2655227,"id":"Q2655227"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q33281$ee71ff33-4c92-8ee7-afde-611ef002359c","rank":"normal"}],"P1394":[{"mainsnak":{"snaktype":"value","property":"P1394","datavalue":{"value":"chav1241","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q33281$DE202E84-D912-4995-8282-27A27FA23F29","rank":"normal","references":[{"hash":"fa278ebfc458360e5aed63d5058cca83c46134f1","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":328,"id":"Q328"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}]},"sitelinks":{"arwiki":{"site":"arwiki","title":"\u0644\u063a\u0629 \u062a\u0634\u0627\u0628\u0627\u0643\u0627\u0646\u0648","badges":[],"url":"https://ar.wikipedia.org/wiki/%D9%84%D8%BA%D8%A9_%D8%AA%D8%B4%D8%A7%D8%A8%D8%A7%D9%83%D8%A7%D9%86%D9%88"},"bclwiki":{"site":"bclwiki","title":"Tataramon na Zamboangue\u00f1o","badges":[],"url":"https://bcl.wikipedia.org/wiki/Tataramon_na_Zamboangue%C3%B1o"},"brwiki":{"site":"brwiki","title":"Chabacano","badges":[],"url":"https://br.wikipedia.org/wiki/Chabacano"},"cawiki":{"site":"cawiki","title":"Chavacano","badges":[],"url":"https://ca.wikipedia.org/wiki/Chavacano"},"cbk_zamwiki":{"site":"cbk_zamwiki","title":"Chavacano","badges":[],"url":"https://cbk-zam.wikipedia.org/wiki/Chavacano"},"cebwiki":{"site":"cebwiki","title":"Pinulongang Tsabakano","badges":[],"url":"https://ceb.wikipedia.org/wiki/Pinulongang_Tsabakano"},"cswiki":{"site":"cswiki","title":"Chavacano","badges":[],"url":"https://cs.wikipedia.org/wiki/Chavacano"},"dewiki":{"site":"dewiki","title":"Chabacano","badges":[],"url":"https://de.wikipedia.org/wiki/Chabacano"},"enwiki":{"site":"enwiki","title":"Chavacano","badges":[],"url":"https://en.wikipedia.org/wiki/Chavacano"},"eowiki":{"site":"eowiki","title":"\u0108abakana lingvo","badges":[],"url":"https://eo.wikipedia.org/wiki/%C4%88abakana_lingvo"},"eswiki":{"site":"eswiki","title":"Criollo chabacano","badges":[],"url":"https://es.wikipedia.org/wiki/Criollo_chabacano"},"euwiki":{"site":"euwiki","title":"Chavacano","badges":[],"url":"https://eu.wikipedia.org/wiki/Chavacano"},"fawiki":{"site":"fawiki","title":"\u0632\u0628\u0627\u0646 \u0686\u0627\u0628\u0627\u06a9\u0627\u0646\u0648","badges":[],"url":"https://fa.wikipedia.org/wiki/%D8%B2%D8%A8%D8%A7%D9%86_%DA%86%D8%A7%D8%A8%D8%A7%DA%A9%D8%A7%D9%86%D9%88"},"frwiki":{"site":"frwiki","title":"Chavacano","badges":[],"url":"https://fr.wikipedia.org/wiki/Chavacano"},"glwiki":{"site":"glwiki","title":"Chavacano","badges":[],"url":"https://gl.wikipedia.org/wiki/Chavacano"},"hakwiki":{"site":"hakwiki","title":"Chavacano-ng\u00ee","badges":[],"url":"https://hak.wikipedia.org/wiki/Chavacano-ng%C3%AE"},"hrwiki":{"site":"hrwiki","title":"Chavacano jezik","badges":[],"url":"https://hr.wikipedia.org/wiki/Chavacano_jezik"},"huwiki":{"site":"huwiki","title":"Chabacano nyelv","badges":[],"url":"https://hu.wikipedia.org/wiki/Chabacano_nyelv"},"iawiki":{"site":"iawiki","title":"Lingua chavacan","badges":[],"url":"https://ia.wikipedia.org/wiki/Lingua_chavacan"},"ilowiki":{"site":"ilowiki","title":"Pagsasao a Chabacano","badges":[],"url":"https://ilo.wikipedia.org/wiki/Pagsasao_a_Chabacano"},"itwiki":{"site":"itwiki","title":"Lingua zamboangue\u00f1a","badges":[],"url":"https://it.wikipedia.org/wiki/Lingua_zamboangue%C3%B1a"},"jawiki":{"site":"jawiki","title":"\u30c1\u30e3\u30d0\u30ab\u30ce\u8a9e","badges":[],"url":"https://ja.wikipedia.org/wiki/%E3%83%81%E3%83%A3%E3%83%90%E3%82%AB%E3%83%8E%E8%AA%9E"},"jvwiki":{"site":"jvwiki","title":"Basa Chavacano","badges":[],"url":"https://jv.wikipedia.org/wiki/Basa_Chavacano"},"kowiki":{"site":"kowiki","title":"\ucc28\ubc14\uce74\ub178\uc5b4","badges":[],"url":"https://ko.wikipedia.org/wiki/%EC%B0%A8%EB%B0%94%EC%B9%B4%EB%85%B8%EC%96%B4"},"lawiki":{"site":"lawiki","title":"Chavacano","badges":[],"url":"https://la.wikipedia.org/wiki/Chavacano"},"nlwiki":{"site":"nlwiki","title":"Chavacano","badges":[],"url":"https://nl.wikipedia.org/wiki/Chavacano"},"ocwiki":{"site":"ocwiki","title":"Chavacan","badges":[],"url":"https://oc.wikipedia.org/wiki/Chavacan"},"plwiki":{"site":"plwiki","title":"J\u0119zyk chavacano","badges":[],"url":"https://pl.wikipedia.org/wiki/J%C4%99zyk_chavacano"},"pmswiki":{"site":"pmswiki","title":"Lenga chavacano","badges":[],"url":"https://pms.wikipedia.org/wiki/Lenga_chavacano"},"ptwiki":{"site":"ptwiki","title":"L\u00edngua chavacana","badges":[],"url":"https://pt.wikipedia.org/wiki/L%C3%ADngua_chavacana"},"ruwiki":{"site":"ruwiki","title":"\u0427\u0430\u0431\u0430\u043a\u0430\u043d\u043e","badges":[],"url":"https://ru.wikipedia.org/wiki/%D0%A7%D0%B0%D0%B1%D0%B0%D0%BA%D0%B0%D0%BD%D0%BE"},"svwiki":{"site":"svwiki","title":"Chavacano","badges":[],"url":"https://sv.wikipedia.org/wiki/Chavacano"},"tawiki":{"site":"tawiki","title":"\u0b9a\u0bb5\u0b95\u0bbe\u0ba9 \u0bae\u0bca\u0bb4\u0bbf","badges":[],"url":"https://ta.wikipedia.org/wiki/%E0%AE%9A%E0%AE%B5%E0%AE%95%E0%AE%BE%E0%AE%A9_%E0%AE%AE%E0%AF%8A%E0%AE%B4%E0%AE%BF"},"thwiki":{"site":"thwiki","title":"\u0e20\u0e32\u0e29\u0e32\u0e0a\u0e32\u0e1a\u0e32\u0e01\u0e32\u0e42\u0e19","badges":[],"url":"https://th.wikipedia.org/wiki/%E0%B8%A0%E0%B8%B2%E0%B8%A9%E0%B8%B2%E0%B8%8A%E0%B8%B2%E0%B8%9A%E0%B8%B2%E0%B8%81%E0%B8%B2%E0%B9%82%E0%B8%99"},"tlwiki":{"site":"tlwiki","title":"Chavacano","badges":[],"url":"https://tl.wikipedia.org/wiki/Chavacano"},"trwiki":{"site":"trwiki","title":"Chabacano","badges":[],"url":"https://tr.wikipedia.org/wiki/Chabacano"},"ukwiki":{"site":"ukwiki","title":"\u0427\u0430\u0432\u0430\u043a\u0430\u043d\u043e","badges":[],"url":"https://uk.wikipedia.org/wiki/%D0%A7%D0%B0%D0%B2%D0%B0%D0%BA%D0%B0%D0%BD%D0%BE"},"viwiki":{"site":"viwiki","title":"Ti\u1ebfng Chavacano","badges":[],"url":"https://vi.wikipedia.org/wiki/Ti%E1%BA%BFng_Chavacano"},"zhwiki":{"site":"zhwiki","title":"\u67e5\u74e6\u5361\u8afe\u8a9e","badges":[],"url":"https://zh.wikipedia.org/wiki/%E6%9F%A5%E7%93%A6%E5%8D%A1%E8%AB%BE%E8%AA%9E"}}}}} -------------------------------------------------------------------------------- /tests/fixtures/entities/Q494290.json: -------------------------------------------------------------------------------- 1 | {"entities":{"Q494290":{"pageid":465109,"ns":0,"title":"Q494290","lastrevid":447210814,"modified":"2017-02-12T20:29:19Z","type":"item","id":"Q494290","labels":{"nn":{"language":"nn","value":"Shin Jung-hyeon"},"ko":{"language":"ko","value":"\uc2e0\uc911\ud604"},"en":{"language":"en","value":"Shin Jung-hyeon"},"es":{"language":"es","value":"Shin Jung-hyeon"},"nb":{"language":"nb","value":"Shin Jung-hyeon"},"de":{"language":"de","value":"Shin Jung-hyeon"},"fr":{"language":"fr","value":"Shin Jung-hyeon"},"nl":{"language":"nl","value":"Shin Jung-hyeon"},"zh":{"language":"zh","value":"\u7533\u91cd\u9249"},"ja":{"language":"ja","value":"\u7533\u91cd\u9249"},"zh-hans":{"language":"zh-hans","value":"\u7533\u91cd\u94c9"},"zh-hant":{"language":"zh-hant","value":"\u7533\u91cd\u9249"}},"descriptions":{"de":{"language":"de","value":"s\u00fcdkoreanischer Musiker"},"nn":{"language":"nn","value":"s\u00f8rkoreansk songar"},"nb":{"language":"nb","value":"s\u00f8rkoreansk sanger"},"da":{"language":"da","value":"sydkoreansk sanger"},"sv":{"language":"sv","value":"sydkoreansk s\u00e5ngare"},"fa":{"language":"fa","value":"\u062e\u0648\u0627\u0646\u0646\u062f\u0647 \u0627\u0647\u0644 \u06a9\u0631\u0647 \u062c\u0646\u0648\u0628\u06cc"},"en":{"language":"en","value":"South Korean rock guitarist and singer-songwriter"},"fr":{"language":"fr","value":"guitariste, chanteur et auteur-compositeur cor\u00e9en"},"nl":{"language":"nl","value":"zanger uit Zuid-Korea"},"ja":{"language":"ja","value":"\u97d3\u56fd\u306e\u30ed\u30c3\u30af\u30df\u30e5\u30fc\u30b8\u30b7\u30e3\u30f3"},"ko":{"language":"ko","value":"\ub300\ud55c\ubbfc\uad6d\uc758 \ub85d \uc74c\uc545 \uc2f1\uc5b4\uc1a1\ub77c\uc774\ud130 \ubc0f \uae30\ud0c0\ub9ac\uc2a4\ud2b8"}},"aliases":[],"claims":{"P434":[{"mainsnak":{"snaktype":"value","property":"P434","datavalue":{"value":"3eb63662-a02c-4d2d-9544-845cd92fd4e7","type":"string"},"datatype":"external-id"},"type":"statement","id":"q494290$76208A7D-49A7-4F88-A93E-05146149F5C1","rank":"normal","references":[{"hash":"cc53937a15e6b6d1d36a4ea24bf2674d2fd4e2a5","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":14005,"id":"Q14005"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P646":[{"mainsnak":{"snaktype":"value","property":"P646","datavalue":{"value":"/m/0bxbjc","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q494290$3C809ED9-A273-4591-85C8-4149582792E8","rank":"normal","references":[{"hash":"af38848ab5d9d9325cffd93a5ec656cc6ca889ed","snaks":{"P248":[{"snaktype":"value","property":"P248","datavalue":{"value":{"entity-type":"item","numeric-id":15241312,"id":"Q15241312"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}],"P577":[{"snaktype":"value","property":"P577","datavalue":{"value":{"time":"+2013-10-28T00:00:00Z","timezone":0,"before":0,"after":0,"precision":11,"calendarmodel":"http://www.wikidata.org/entity/Q1985727"},"type":"time"},"datatype":"time"}]},"snaks-order":["P248","P577"]}]}],"P31":[{"mainsnak":{"snaktype":"value","property":"P31","datavalue":{"value":{"entity-type":"item","numeric-id":5,"id":"Q5"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q494290$3abb23cf-4382-489a-6d76-09d8e01d6092","rank":"normal"}],"P21":[{"mainsnak":{"snaktype":"value","property":"P21","datavalue":{"value":{"entity-type":"item","numeric-id":6581097,"id":"Q6581097"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q494290$5a7e29b1-435c-ecc1-fe3f-33a3ed840214","rank":"normal"}],"P27":[{"mainsnak":{"snaktype":"value","property":"P27","datavalue":{"value":{"entity-type":"item","numeric-id":884,"id":"Q884"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q494290$bdf4c44e-4ff4-2fe1-04cc-8a1c92fe4f6e","rank":"normal"}],"P569":[{"mainsnak":{"snaktype":"value","property":"P569","datavalue":{"value":{"time":"+1938-01-04T00:00:00Z","timezone":0,"before":0,"after":0,"precision":11,"calendarmodel":"http://www.wikidata.org/entity/Q1985727"},"type":"time"},"datatype":"time"},"type":"statement","id":"Q494290$55379037-4b29-84e7-169d-e5f20cd6c800","rank":"normal"}],"P19":[{"mainsnak":{"snaktype":"value","property":"P19","datavalue":{"value":{"entity-type":"item","numeric-id":8684,"id":"Q8684"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q494290$6dfbea16-43d0-1ea4-fe34-59863e66d070","rank":"normal"}],"P345":[{"mainsnak":{"snaktype":"value","property":"P345","datavalue":{"value":"nm1142700","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q494290$44A2BF1A-3CA0-4362-ABEB-E2EED49F2C81","rank":"normal","references":[{"hash":"7eb64cf9621d34c54fd4bd040ed4b61a88c4a1a0","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":328,"id":"Q328"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P106":[{"mainsnak":{"snaktype":"value","property":"P106","datavalue":{"value":{"entity-type":"item","numeric-id":177220,"id":"Q177220"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q494290$68095813-6EDB-4EED-914A-7D2804C8BA78","rank":"normal"},{"mainsnak":{"snaktype":"value","property":"P106","datavalue":{"value":{"entity-type":"item","numeric-id":753110,"id":"Q753110"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q494290$25455285-72F3-47B6-8A1B-BCC936556056","rank":"normal"}],"P1303":[{"mainsnak":{"snaktype":"value","property":"P1303","datavalue":{"value":{"entity-type":"item","numeric-id":6607,"id":"Q6607"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q494290$2C27B39B-2F2A-4F84-AC3B-F271587BE93E","rank":"normal"}],"P1953":[{"mainsnak":{"snaktype":"value","property":"P1953","datavalue":{"value":"725205","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q494290$06B82F9D-2B2E-4866-B554-97B01C125D34","rank":"normal","references":[{"hash":"4e912a62228f108a5cd798dfbd9ff49dcea1f7e4","snaks":{"P248":[{"snaktype":"value","property":"P248","datavalue":{"value":{"entity-type":"item","numeric-id":14005,"id":"Q14005"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}],"P813":[{"snaktype":"value","property":"P813","datavalue":{"value":{"time":"+2015-07-25T00:00:00Z","timezone":0,"before":0,"after":0,"precision":11,"calendarmodel":"http://www.wikidata.org/entity/Q1985727"},"type":"time"},"datatype":"time"}]},"snaks-order":["P248","P813"]}]}],"P1728":[{"mainsnak":{"snaktype":"value","property":"P1728","datavalue":{"value":"mn0002730226","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q494290$c94e288a-4c45-6fc2-4393-4a6d1a9ceb4e","rank":"normal"}],"P136":[{"mainsnak":{"snaktype":"value","property":"P136","datavalue":{"value":{"entity-type":"item","numeric-id":11399,"id":"Q11399"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q494290$CFDE676E-6BA6-4B9A-88ED-B85FBECC6F77","rank":"normal","references":[{"hash":"f70116eac7f49194478b3025330bfd8dcffa3c69","snaks":{"P143":[{"snaktype":"value","property":"P143","datavalue":{"value":{"entity-type":"item","numeric-id":8447,"id":"Q8447"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}]},"sitelinks":{"enwiki":{"site":"enwiki","title":"Shin Jung-hyeon","badges":[],"url":"https://en.wikipedia.org/wiki/Shin_Jung-hyeon"},"frwiki":{"site":"frwiki","title":"Shin Jung-hyeon","badges":[],"url":"https://fr.wikipedia.org/wiki/Shin_Jung-hyeon"},"kowiki":{"site":"kowiki","title":"\uc2e0\uc911\ud604","badges":[],"url":"https://ko.wikipedia.org/wiki/%EC%8B%A0%EC%A4%91%ED%98%84"},"nnwiki":{"site":"nnwiki","title":"Shin Jung-hyeon","badges":[],"url":"https://nn.wikipedia.org/wiki/Shin_Jung-hyeon"},"nowiki":{"site":"nowiki","title":"Shin Jung-hyeon","badges":[],"url":"https://no.wikipedia.org/wiki/Shin_Jung-hyeon"}}}}} -------------------------------------------------------------------------------- /tests/fixtures/media/5834c5ab48506a503f290d49e3056427.json: -------------------------------------------------------------------------------- 1 | {"continue":{"iistart":"2010-11-05T23:36:30Z","continue":"||info"},"query":{"pages":{"-1":{"ns":6,"title":"File:Gandhara Buddha (tnm).jpeg","missing":"","known":"","imagerepository":"shared","imageinfo":[{"size":823440,"width":1746,"height":2894,"url":"https://upload.wikimedia.org/wikipedia/commons/b/b8/Gandhara_Buddha_%28tnm%29.jpeg","descriptionurl":"https://commons.wikimedia.org/wiki/File:Gandhara_Buddha_(tnm).jpeg","descriptionshorturl":"https://commons.wikimedia.org/w/index.php?curid=89740","mime":"image/jpeg"}],"contentmodel":"wikitext","pagelanguage":"en","pagelanguagehtmlcode":"en","pagelanguagedir":"ltr","fullurl":"https://www.wikidata.org/wiki/File:Gandhara_Buddha_(tnm).jpeg","editurl":"https://www.wikidata.org/w/index.php?title=File:Gandhara_Buddha_(tnm).jpeg&action=edit","canonicalurl":"https://www.wikidata.org/wiki/File:Gandhara_Buddha_(tnm).jpeg"}}}} -------------------------------------------------------------------------------- /tests/fixtures/media/d7d79f7a3f939df88ff32571cbbeb9c3.json: -------------------------------------------------------------------------------- 1 | {"batchcomplete":"","query":{"pages":{"-1":{"ns":6,"title":"File:KBS \"The Producers\" press conference, 11 May 2015 10.jpg","missing":"","known":"","imagerepository":"shared","imageinfo":[{"size":183014,"width":820,"height":1122,"url":"https://upload.wikimedia.org/wikipedia/commons/6/60/KBS_%22The_Producers%22_press_conference%2C_11_May_2015_10.jpg","descriptionurl":"https://commons.wikimedia.org/wiki/File:KBS_%22The_Producers%22_press_conference,_11_May_2015_10.jpg","descriptionshorturl":"https://commons.wikimedia.org/w/index.php?curid=46500881","mime":"image/jpeg"}],"contentmodel":"wikitext","pagelanguage":"en","pagelanguagehtmlcode":"en","pagelanguagedir":"ltr","fullurl":"https://www.wikidata.org/wiki/File:KBS_%22The_Producers%22_press_conference,_11_May_2015_10.jpg","editurl":"https://www.wikidata.org/w/index.php?title=File:KBS_%22The_Producers%22_press_conference,_11_May_2015_10.jpg&action=edit","canonicalurl":"https://www.wikidata.org/wiki/File:KBS_%22The_Producers%22_press_conference,_11_May_2015_10.jpg"}}}} -------------------------------------------------------------------------------- /tests/globecoordinate_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from wikidata.client import Client 4 | from wikidata.entity import Entity, EntityId 5 | from wikidata.globecoordinate import GlobeCoordinate 6 | 7 | 8 | @fixture 9 | def fx_globecoordinate() -> GlobeCoordinate: 10 | client = Client() 11 | return GlobeCoordinate( 12 | latitude=70.1525, 13 | longitude=70.1525, 14 | precision=0.0002777777777777778, 15 | globe=client.get(EntityId("Q111"))) 16 | 17 | 18 | def test_globecoordinate_value(fx_globecoordinate: GlobeCoordinate): 19 | assert fx_globecoordinate.latitude == 70.1525 20 | assert fx_globecoordinate.longitude == 70.1525 21 | assert fx_globecoordinate.precision == 0.0002777777777777778 22 | assert isinstance(fx_globecoordinate.globe, Entity) 23 | assert fx_globecoordinate.globe.id == "Q111" 24 | 25 | 26 | def test_globecoordinate_repr(fx_globecoordinate: GlobeCoordinate): 27 | assert (repr(fx_globecoordinate) == 28 | ("wikidata.globecoordinate.GlobeCoordinate(70.1525, 70.1525, " 29 | ", 0.0002777777777777778)")) 30 | -------------------------------------------------------------------------------- /tests/mock.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import http.client 3 | import io 4 | import json 5 | import logging 6 | import pathlib 7 | import socket 8 | import traceback 9 | import typing # noqa: F401 10 | import urllib.error 11 | import urllib.parse 12 | import urllib.request 13 | import urllib.response 14 | 15 | __all__ = ('ENTITY_FIXTURES_PATH', 'FIXTURES_PATH', 'MEDIA_FIXTURES_PATH', 16 | 'FixtureOpener') 17 | 18 | 19 | FIXTURES_PATH = pathlib.Path(__file__).parent / 'fixtures' 20 | ENTITY_FIXTURES_PATH = FIXTURES_PATH / 'entities' 21 | MEDIA_FIXTURES_PATH = FIXTURES_PATH / 'media' 22 | 23 | 24 | class FixtureOpener(urllib.request.OpenerDirector): 25 | 26 | def __init__(self, base_url: str) -> None: 27 | self.base_url = base_url 28 | self.entities_base_url = urllib.parse.urlparse( 29 | urllib.parse.urljoin(base_url, './wiki/Special:EntityData/') 30 | ) 31 | self.media_base_url = urllib.parse.urlparse( 32 | urllib.parse.urljoin(base_url, './w/api.php') 33 | ) 34 | # ./w/api.php?action=query&prop=imageinfo|info&inprop=url&iiprop=url|size|mime&format=json&titles={} # noqa: E501 35 | self.records = [] # type: typing.List[typing.Tuple[str, str]] 36 | 37 | @property 38 | def logger(self): 39 | cls = type(self) 40 | return logging.getLogger(cls.__qualname__).getChild(cls.__name__) 41 | 42 | @staticmethod 43 | def match_netloc(a: urllib.parse.ParseResult, 44 | b: urllib.parse.ParseResult) -> bool: 45 | return ( 46 | a.scheme.lower() == b.scheme.lower() and 47 | a.username == b.username and 48 | a.password == b.password and 49 | a.port == b.port and ( 50 | a.hostname.lower() == b.hostname.lower() 51 | if a.hostname is not None and b.hostname is not None 52 | else a.hostname is b.hostname 53 | ) 54 | ) 55 | 56 | def open(self, fullurl, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): 57 | logger = self.logger.getChild('open') 58 | if not isinstance(fullurl, str): 59 | fullurl = fullurl.get_full_url() 60 | self.records.append((fullurl, ''.join(traceback.format_stack()))) 61 | parsed = urllib.parse.urlparse(fullurl) 62 | hdrs = http.client.HTTPMessage() 63 | # media fixtures 64 | if self.match_netloc(parsed, self.media_base_url) and \ 65 | parsed.path == self.media_base_url.path: 66 | qs = urllib.parse.parse_qs(parsed.query, strict_parsing=True) 67 | if not (qs['action'] == ['query'] and 68 | len(qs['prop']) == 1 and 69 | set(qs['prop'][0].split('|')) == {'imageinfo', 'info'} and 70 | qs['inprop'] == ['url'] and 71 | len(qs['iiprop']) == 1 and 72 | set(qs['iiprop'][0].split('|')) == 73 | {'url', 'size', 'mime'} and 74 | qs['format'] == ['json'] and 75 | 'titles' in qs and 76 | len(qs['titles']) == 1): 77 | hdrs.add_header('Content-Type', 'application/json') 78 | fp = io.BytesIO( 79 | json.dumps({ 80 | 'error': {'*': '...', 'code': '...', 'info': '...'}, 81 | 'servedby': '...', 82 | }).encode('utf-8') 83 | ) 84 | return urllib.response.addinfourl(fp, hdrs, fullurl, 200) 85 | title, = qs['titles'] 86 | title_id = hashlib.md5(title.encode('utf-8')).hexdigest().lower() 87 | path = MEDIA_FIXTURES_PATH / (title_id + '.json') 88 | if path.is_file(): 89 | fp = path.open('rb') 90 | else: 91 | result = { 92 | 'batchcomplete': '', 93 | 'query': { 94 | 'pages': { 95 | '-1': { 96 | 'imagerepository': '', 97 | 'missing': '', 98 | 'ns': 6, 99 | 'title': title, 100 | } 101 | } 102 | } 103 | } 104 | fp = io.BytesIO(json.dumps(result).encode('utf-8')) 105 | path = path.relative_to(pathlib.Path.cwd()) 106 | logger.warn("Couldn't find %s; to add a new fixture file " 107 | 'download it from Wikidata website:\n ' 108 | 'curl -o %s "%s"', 109 | path, path, fullurl) 110 | hdrs.add_header('Content-Type', 'application/json') 111 | return urllib.response.addinfourl(fp, hdrs, fullurl, 200) 112 | # entity fixtures 113 | path = None 114 | found = (self.match_netloc(parsed, self.entities_base_url) and 115 | parsed.path.startswith(self.entities_base_url.path) and 116 | parsed.path.endswith('.json')) 117 | if found: 118 | entity_id = parsed.path[len(self.entities_base_url.path):-5] 119 | found = entity_id[0].isupper() and entity_id[1:].isdigit() 120 | if found: 121 | path = ENTITY_FIXTURES_PATH / (entity_id + '.json') 122 | found = path.is_file() 123 | if found: 124 | fp = path.open('rb') 125 | hdrs.add_header('Content-Type', 'application/json') 126 | return urllib.response.addinfourl(fp, hdrs, fullurl, 200) 127 | if path: 128 | path = path.relative_to(pathlib.Path.cwd()) 129 | logger.warn("Couldn't find %s; to add a new fixture file " 130 | 'download it from Wikidata website:\n ' 131 | 'curl -o %s %s', 132 | path, path, fullurl) 133 | hdrs.add_header('Content-Type', 'text/plain; charset=utf-8') 134 | if fullurl[-5:] == '.json': 135 | fp = io.BytesIO( 136 | b'''Bad Request 137 |

Bad Request

138 |

Invalid ID: .

139 | ''') 140 | raise urllib.error.HTTPError( 141 | fullurl, 400, 'Bad Request', hdrs, fp) 142 | fp = io.BytesIO(b'Not Found: ' + fullurl.encode()) 143 | raise urllib.error.HTTPError(fullurl, 404, 'Not Found', hdrs, fp) 144 | -------------------------------------------------------------------------------- /tests/multilingual_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from wikidata.multilingual import Locale, MonolingualText, MultilingualText 4 | 5 | 6 | @fixture 7 | def fx_multilingual_text() -> MultilingualText: 8 | return MultilingualText({ 9 | 'ko': '윤동주', 10 | 'ja': '尹東柱', 11 | 'zh-hans': '尹东柱', 12 | 'en': 'Yun Dong-ju', 13 | }) 14 | 15 | 16 | def test_multilingual_text_mapping(fx_multilingual_text: MultilingualText): 17 | mt = fx_multilingual_text 18 | assert set(mt) == {"ko", "ja", "zh-hans", "en"} 19 | assert len(mt) == 4 20 | assert Locale("ko") in mt 21 | assert Locale("ja") in mt 22 | assert Locale("zh-hans") in mt 23 | assert Locale("en") in mt 24 | assert Locale("ko-kp") not in mt 25 | assert Locale("zh-hant") not in mt 26 | assert Locale("en-gb") not in mt 27 | assert mt[Locale("ko")] == mt.get("ko") == '윤동주' 28 | assert mt[Locale("ja")] == mt.get("ja") == '尹東柱' 29 | assert mt[Locale("zh-hans")] == mt.get("zh-hans") == '尹东柱' 30 | assert mt[Locale("en")] == mt.get("en") == 'Yun Dong-ju' 31 | assert mt.get("ko-kp") is None 32 | assert mt.get('zh-hant') is None 33 | assert mt.get('en-gb') is None 34 | 35 | 36 | def test_multilingual_text_str(fx_multilingual_text: MultilingualText): 37 | assert str(fx_multilingual_text) == 'Yun Dong-ju' 38 | assert str(MultilingualText({})) == '' 39 | 40 | 41 | def test_multilingual_text_repr(fx_multilingual_text: MultilingualText): 42 | assert repr(fx_multilingual_text) == "m'Yun Dong-ju'" 43 | assert repr(MultilingualText({})) == \ 44 | "wikidata.multilingual.MultilingualText({})" 45 | 46 | 47 | def test_monolingual_text_locale(): 48 | a = MonolingualText('윤동주', 'ko') 49 | assert a.locale == 'ko' 50 | c = MonolingualText('周樹人', 'zh-hant') 51 | assert c.locale == 'zh-hant' 52 | 53 | 54 | def test_monolingual_text_repr(): 55 | a = MonolingualText('윤동주', 'ko') 56 | assert repr(a) == "'(ko:) 윤동주'[6:]" 57 | assert eval(repr(a)) == str(a) 58 | b = MonolingualText('周樹人', 'zh-hant') 59 | assert repr(b) == "'(zh-hant:) 周樹人'[11:]" 60 | assert eval(repr(b)) == str(b) 61 | -------------------------------------------------------------------------------- /tests/quantity_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from wikidata.client import Client 4 | from wikidata.entity import Entity, EntityId 5 | from wikidata.quantity import Quantity 6 | 7 | 8 | @fixture 9 | def fx_quantity_unitless() -> Quantity: 10 | # From Q605704 (dozen) 11 | return Quantity( 12 | amount=12, 13 | lower_bound=None, 14 | upper_bound=None, 15 | unit=None) 16 | 17 | 18 | @fixture 19 | def fx_quantity_with_unit() -> Quantity: 20 | client = Client() 21 | # From Q520 22 | return Quantity( 23 | amount=610.13, 24 | lower_bound=610.12, 25 | upper_bound=610.14, 26 | unit=client.get(EntityId("Q828224"))) 27 | 28 | 29 | def test_quantity_unitless_value(fx_quantity_unitless: Quantity): 30 | assert fx_quantity_unitless.amount == 12 31 | assert fx_quantity_unitless.lower_bound is None 32 | assert fx_quantity_unitless.upper_bound is None 33 | assert fx_quantity_unitless.unit is None 34 | 35 | 36 | def test_quantity_unitless_repr(fx_quantity_unitless: Quantity): 37 | assert (repr(fx_quantity_unitless) == 38 | ("wikidata.quantity.Quantity(12, None, None, None)")) 39 | 40 | 41 | def test_quantity_with_unit_value(fx_quantity_with_unit: Quantity): 42 | assert fx_quantity_with_unit.amount == 610.13 43 | assert fx_quantity_with_unit.lower_bound == 610.12 44 | assert fx_quantity_with_unit.upper_bound == 610.14 45 | assert isinstance(fx_quantity_with_unit.unit, Entity) 46 | assert fx_quantity_with_unit.unit.id == "Q828224" 47 | 48 | 49 | def test_quantity_with_unit_repr(fx_quantity_with_unit: Quantity): 50 | assert (repr(fx_quantity_with_unit) == 51 | ("wikidata.quantity.Quantity(610.13, 610.12, 610.14, " 52 | ")")) 53 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | # CHECK If you're going to change the list of supported Python versions 4 | # update .travis.yml, .devcontainer/devcontainer.json, and setup.cfg's 5 | # classifiers as well. 6 | py38, py39, py310, py311, pypy3, mypy, flake8 7 | 8 | [gh-actions] 9 | python = 10 | 3.8: py38 11 | 3.9: py39 12 | 3.10: py310 13 | 3.11: py311, mypy, flake8, docs 14 | pypy3.8: pypy3 15 | pypy3.9: pypy3 16 | 17 | [testenv] 18 | extras = tests 19 | commands = 20 | pytest {posargs:} 21 | 22 | [testenv:mypy] 23 | extras = tests 24 | basepython = python3 25 | commands = 26 | mypy -p wikidata 27 | mypy -p tests 28 | 29 | [testenv:flake8] 30 | extras = tests 31 | basepython = python3 32 | commands = 33 | flake8 34 | 35 | [testenv:docs] 36 | extras = docs 37 | basepython = python3 38 | commands = 39 | sphinx-build docs/ docs/_build/html 40 | rstcheck -r . 41 | -------------------------------------------------------------------------------- /wikidata/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.9.0' 2 | -------------------------------------------------------------------------------- /wikidata/cache.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import hashlib 3 | import logging 4 | import pickle 5 | import re 6 | from typing import NewType, Optional 7 | 8 | __all__ = ('CacheKey', 'CachePolicy', 'CacheValue', 9 | 'MemoryCachePolicy', 'NullCachePolicy', 'ProxyCachePolicy') 10 | 11 | 12 | #: The type of keys to look up cached values. Alias of :class:`str`. 13 | CacheKey = NewType('CacheKey', str) 14 | 15 | #: The type of cached values. 16 | CacheValue = NewType('CacheValue', object) 17 | 18 | 19 | class CachePolicy: 20 | """Interface for caching policies.""" 21 | 22 | def get(self, key: CacheKey) -> Optional[CacheValue]: 23 | r"""Look up a cached value by its ``key``. 24 | 25 | :param key: The key string to look up a cached value. 26 | :type key: :const:`CacheKey` 27 | :return: The cached value if it exists. 28 | :const:`None` if there's no such ``key``. 29 | :rtype: :class:`~typing.Optional`\ [:const:`CacheValue`] 30 | 31 | """ 32 | raise NotImplementedError( 33 | 'Concreate subclasses of {0.__module__}.{0.__qualname__} have to ' 34 | 'override .get() method'.format(CachePolicy) 35 | ) 36 | 37 | def set(self, key: CacheKey, value: Optional[CacheValue]) -> None: 38 | r"""Create or update a cache. 39 | 40 | :param key: A key string to create or update. 41 | :type key: :const:`CacheKey` 42 | :param value: A value to cache. :const:`None` to remove cache. 43 | :type value: :class:`~typing.Optional`\ [:const:`CacheValue`] 44 | 45 | """ 46 | raise NotImplementedError( 47 | 'Concreate subclasses of {0.__module__}.{0.__qualname__} have to ' 48 | 'override .set() method'.format(CachePolicy) 49 | ) 50 | 51 | 52 | class NullCachePolicy(CachePolicy): 53 | """No-op cache policy.""" 54 | 55 | def get(self, key: CacheKey) -> Optional[CacheValue]: 56 | return None 57 | 58 | def set(self, key: CacheKey, value: Optional[CacheValue]) -> None: 59 | pass 60 | 61 | 62 | class MemoryCachePolicy(CachePolicy): 63 | """LRU (least recently used) cache in memory. 64 | 65 | :param max_size: The maximum number of values to cache. 128 by default. 66 | :type max_size: :class:`int` 67 | 68 | """ 69 | 70 | def __init__(self, max_size: int = 128) -> None: 71 | self.max_size = max_size # type: int 72 | self.values = \ 73 | collections.OrderedDict() # type: collections.OrderedDict 74 | 75 | def get(self, key: CacheKey) -> Optional[CacheValue]: 76 | try: 77 | v = self.values[key] 78 | except KeyError: 79 | v = None 80 | else: 81 | self.values.move_to_end(key) 82 | return v 83 | 84 | def set(self, key: CacheKey, value: Optional[CacheValue]) -> None: 85 | try: 86 | del self.values[key] 87 | except KeyError: 88 | pass 89 | if value is None: 90 | return 91 | self.values[key] = value 92 | while len(self.values) > self.max_size: 93 | self.values.popitem(last=False) 94 | 95 | 96 | class ProxyCachePolicy(CachePolicy): 97 | """This proxy policy is a proxy or an adaptor to another cache object. 98 | Cache objects can be anything if they satisfy the following interface:: 99 | 100 | def get(key: str) -> Optional[bytes]: pass 101 | def set(key: str, value: bytes, timeout: int=0) -> None: pass 102 | def delete(key: str) -> None: pass 103 | 104 | (The above methods omit ``self`` parameters.) It's compatible with 105 | de facto interface for caching libraries in Python (e.g. python-memcached, 106 | :mod:`werkzeug.contrib.cache`). 107 | 108 | :param cache_object: The cache object to adapt. 109 | Read the above explanation. 110 | :param timeout: Lifespan of every cache in seconds. 0 means no expiration. 111 | :type timeout: :class:`int` 112 | :param property_timeout: Lifespan of caches for properties (in seconds). 113 | Since properties don't change frequently or 114 | their changes usually don't make important effect, 115 | longer lifespan of properties' cache can be 116 | useful. 0 means no expiration. 117 | Set to the same as ``timeout`` by default. 118 | :type property_timeout: :class:`int` 119 | :param namespace: The common prefix attached to every cache key. 120 | ``'wd_'`` by default. 121 | :type namespace: :class:`str` 122 | 123 | """ 124 | 125 | PROPERTY_KEY_RE = re.compile(r'/P\d+\.json$') 126 | 127 | def __init__(self, cache_object, timeout: int, 128 | property_timeout: Optional[int] = None, 129 | namespace: str = 'wd_') -> None: 130 | self.cache_object = cache_object 131 | self.timeout = timeout # type: int 132 | if property_timeout is None: 133 | property_timeout = timeout 134 | self.property_timeout = property_timeout # type: int 135 | self.namespace = namespace # type: str 136 | 137 | def encode_key(self, key: CacheKey) -> str: 138 | k = self.namespace + hashlib.md5(key.encode('utf-8')).hexdigest() 139 | logging.getLogger(__name__ + '.ProxyCachePolicy.encode_key').debug( 140 | 'Encoded from key %r: %r', key, k 141 | ) 142 | return k 143 | 144 | @classmethod 145 | def is_property(cls, key: CacheKey) -> bool: 146 | return bool(cls.PROPERTY_KEY_RE.search(key)) 147 | 148 | def get(self, key: CacheKey) -> Optional[CacheValue]: 149 | k = self.encode_key(key) 150 | v = self.cache_object.get(k) 151 | if v is None: 152 | return None 153 | return pickle.loads(v) 154 | 155 | def set(self, key: CacheKey, value: Optional[CacheValue]) -> None: 156 | k = self.encode_key(key) 157 | if value is None: 158 | self.cache_object.delete(k) 159 | return 160 | v = pickle.dumps(value) 161 | time = self.property_timeout if self.is_property(key) else self.timeout 162 | self.cache_object.set(k, v, time) 163 | -------------------------------------------------------------------------------- /wikidata/client.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import logging 4 | from typing import ( 5 | TYPE_CHECKING, 6 | Callable, 7 | Mapping, 8 | MutableMapping, 9 | Optional, 10 | Sequence, 11 | Tuple, 12 | Union, 13 | cast, 14 | ) 15 | import urllib.error 16 | import urllib.parse 17 | import urllib.request 18 | import weakref 19 | 20 | from .cache import CacheKey, CachePolicy, NullCachePolicy 21 | from .entity import Entity, EntityId, EntityType 22 | 23 | if TYPE_CHECKING: 24 | from .datavalue import Decoder # noqa: F401 25 | 26 | __all__ = 'WIKIDATA_BASE_URL', 'Client' 27 | 28 | 29 | #: (:class:`str`) The default ``base_url`` of :class:`Client` constructor. 30 | #: 31 | #: .. versionchanged:: 0.3.0 32 | #: As the meaning of :class:`Client` constructor's ``base_url`` parameter, 33 | #: it now became to ``https://www.wikidata.org/`` from 34 | #: ``https://www.wikidata.org/wiki/`` (which contained the trailing path 35 | #: ``wiki/``). 36 | WIKIDATA_BASE_URL = 'https://www.wikidata.org/' 37 | 38 | 39 | class Client: 40 | """Wikidata client session. 41 | 42 | :param base_url: The base url of the Wikidata. 43 | :const:`WIKIDATA_BASE_URL` is used by default. 44 | :type base_url: :class:`str` 45 | :param opener: The opener for :mod:`urllib.request`. 46 | If omitted or :const:`None` the default opener is used. 47 | :type opener: :class:`urllib.request.OpenerDirector` 48 | :param entity_type_guess: Whether to guess :attr:`~.entity.Entity.type` 49 | of :class:`~.entity.Entity` from its 50 | :attr:`~.entity.Entity.id` for less HTTP 51 | requests. :const:`True` by default. 52 | :type entity_type_guess: :class:`bool` 53 | :param cache_policy: A caching policy for API calls. No cache 54 | (:class:`~wikidata.cache.NullCachePolicy`) by default. 55 | :type cache_policy: :class:`~wikidata.cache.CachePolicy` 56 | 57 | .. versionadded:: 0.5.0 58 | The ``cache_policy`` option. 59 | 60 | .. versionchanged:: 0.3.0 61 | The meaning of ``base_url`` parameter changed. It originally meant 62 | ``https://www.wikidata.org/wiki/`` which contained the trailing path 63 | ``wiki/``, but now it means only ``https://www.wikidata.org/``. 64 | 65 | .. versionadded:: 0.2.0 66 | The ``entity_type_guess`` option. 67 | 68 | """ 69 | 70 | #: (:class:`bool`) Whether to guess :attr:`~.entity.Entity.type` 71 | #: of :class:`~.entity.Entity` from its :attr:`~.entity.Entity.id` 72 | #: for less HTTP requests. 73 | #: 74 | #: .. versionadded:: 0.2.0 75 | entity_type_guess = True 76 | 77 | #: (:class:`~typing.Union`\ [:class:`~.datavalue.Decoder`, 78 | #: :class:`~typing.Callable`\ [[:class:`Client`, :class:`str`, 79 | #: :class:`~typing.Mapping`\ [:class:`str`, :class:`object`]], 80 | #: :class:`object`]]) 81 | #: The function to decode the given datavalue. It's typically an instance 82 | #: of :class:`~.decoder.Decoder` or its subclass. 83 | datavalue_decoder = None 84 | 85 | #: (:class:`CachePolicy`) A caching policy for API calls. 86 | #: 87 | #: .. versionadded:: 0.5.0 88 | cache_policy = NullCachePolicy() # type: CachePolicy 89 | 90 | def __init__(self, 91 | # CHECK: If the signature of this function changes, 92 | # the implementation of __reduce__() also should be 93 | # changed. 94 | base_url: str = WIKIDATA_BASE_URL, 95 | opener: Optional[urllib.request.OpenerDirector] = None, 96 | datavalue_decoder: Union['Decoder', 97 | Callable[['Client', str, 98 | Mapping[str, object]], 99 | object], 100 | None] = None, 101 | entity_type_guess: bool = True, 102 | cache_policy: CachePolicy = NullCachePolicy(), 103 | repr_string: Optional[str] = None) -> None: 104 | self._using_default_opener = opener is None 105 | if self._using_default_opener: 106 | if urllib.request._opener is None: # type: ignore 107 | try: 108 | urllib.request.urlopen('') 109 | except (ValueError, TypeError): 110 | pass 111 | opener = urllib.request._opener # type: ignore 112 | assert isinstance(opener, urllib.request.OpenerDirector) 113 | if datavalue_decoder is None: 114 | from .datavalue import Decoder # noqa: F811 115 | datavalue_decoder = Decoder() 116 | assert callable(datavalue_decoder) 117 | self.base_url = base_url 118 | self.opener = opener # type: urllib.request.OpenerDirector 119 | self.datavalue_decoder = datavalue_decoder 120 | self.entity_type_guess = entity_type_guess 121 | self.cache_policy = cache_policy # type: CachePolicy 122 | self.identity_map = cast(MutableMapping[EntityId, Entity], 123 | weakref.WeakValueDictionary()) 124 | self.repr_string = repr_string 125 | 126 | def get(self, entity_id: EntityId, load: bool = False) -> Entity: 127 | """Get a Wikidata entity by its :class:`~.entity.EntityId`. 128 | 129 | :param entity_id: The :attr:`~.entity.Entity.id` of 130 | the :class:`~.entity.Entity` to find. 131 | :type eneity_id: :class:`~.entity.EntityId` 132 | :param load: Eager loading on :const:`True`. 133 | Lazy loading (:const:`False`) by default. 134 | :type load: :class:`bool` 135 | :return: The found entity. 136 | :rtype: :class:`~.entity.Entity` 137 | 138 | .. versionadded:: 0.3.0 139 | The ``load`` option. 140 | 141 | """ 142 | try: 143 | entity = self.identity_map[entity_id] 144 | except KeyError: 145 | entity = Entity(entity_id, self) 146 | self.identity_map[entity_id] = entity 147 | if load: 148 | entity.load() 149 | return entity 150 | 151 | def guess_entity_type(self, entity_id: EntityId) -> Optional[EntityType]: 152 | r"""Guess :class:`~.entity.EntityType` from the given 153 | :class:`~.entity.EntityId`. It could return :const:`None` when it 154 | fails to guess. 155 | 156 | .. note:: 157 | 158 | It always fails to guess when :attr:`entity_type_guess` 159 | is configued to :const:`False`. 160 | 161 | :return: The guessed :class:`~.entity.EntityId`, or :const:`None` 162 | if it fails to guess. 163 | :rtype: :class:`~typing.Optional`\ [:class:`~.entity.EntityType`] 164 | 165 | .. versionadded:: 0.2.0 166 | 167 | """ 168 | if not self.entity_type_guess: 169 | return None 170 | if entity_id[0] == 'Q': 171 | return EntityType.item 172 | elif entity_id[0] == 'P': 173 | return EntityType.property 174 | return None 175 | 176 | def decode_datavalue(self, 177 | datatype: str, 178 | datavalue: Mapping[str, object]) -> object: 179 | """Decode the given ``datavalue`` using the configured 180 | :attr:`datavalue_decoder`. 181 | 182 | .. versionadded:: 0.3.0 183 | 184 | """ 185 | decode = cast(Callable[[Client, str, Mapping[str, object]], object], 186 | self.datavalue_decoder) 187 | return decode(self, datatype, datavalue) 188 | 189 | def request(self, path: str) -> Union[ 190 | bool, int, float, str, 191 | Mapping[str, Union[bool, int, float, str, 192 | Mapping[str, object], Sequence]], 193 | Sequence[Union[bool, int, float, str, Mapping[str, object], Sequence]], 194 | None 195 | ]: 196 | logger = logging.getLogger(__name__ + '.Client.request') 197 | url = urllib.parse.urljoin(self.base_url, path) 198 | result = self.cache_policy.get(CacheKey(url)) 199 | if result is None: 200 | logger.debug('%r: no cache; make a request...', url) 201 | try: 202 | response = self.opener.open(url) 203 | except urllib.error.HTTPError as e: 204 | logger.debug('HTTP error code: %s', e.code, exc_info=True) 205 | if e.code == 400 and b'Invalid ID' in e.read(): 206 | return None 207 | else: 208 | raise e 209 | 210 | buffer_ = io.TextIOWrapper(response, 211 | encoding='utf-8') 212 | result = json.load(buffer_) 213 | self.cache_policy.set(CacheKey(url), result) 214 | else: 215 | logger.debug('%r: cache hit', url) 216 | return result # type: ignore 217 | 218 | def __reduce__(self) -> Tuple[Callable[..., 'Client'], Tuple[object, ...]]: 219 | return type(self), ( 220 | self.base_url, 221 | None if self._using_default_opener else self.opener, 222 | self.datavalue_decoder, 223 | self.entity_type_guess, 224 | self.cache_policy, 225 | self.repr_string, 226 | ) 227 | 228 | def __repr__(self) -> str: 229 | if self.repr_string is not None: 230 | return self.repr_string 231 | return '{0.__module__}.{0.__qualname__}({1!r})'.format( 232 | type(self), 233 | self.base_url 234 | ) 235 | -------------------------------------------------------------------------------- /wikidata/commonsmedia.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | from typing import Mapping, Optional, Tuple, cast 3 | import urllib.parse 4 | 5 | from .client import Client 6 | 7 | __all__ = 'File', 'FileError' 8 | 9 | 10 | class File: 11 | """Represent a file on `Wikimedia Commons`_.""" 12 | 13 | __slots__ = 'client', 'title', 'data' 14 | 15 | def __init__(self, client: Client, title: str) -> None: 16 | self.client = client 17 | self.title = title 18 | self.data = None # type: Optional[Mapping[str, object]] 19 | 20 | @property 21 | def page_url(self) -> str: 22 | """(:class:`str`) The canonical url of the page.""" 23 | url = self.attributes['canonicalurl'] 24 | assert isinstance(url, str) 25 | return url 26 | 27 | @property 28 | def image_url(self) -> Optional[str]: 29 | r"""(:class:`~typing.Optional`\ [:class:`str`]) The image url. 30 | It may be :const:`None` if it's not an image. 31 | 32 | """ 33 | images = self.attributes.get('imageinfo', []) 34 | if images and isinstance(images, collections.abc.Sequence): 35 | return images[0]['url'] 36 | return None 37 | 38 | @property 39 | def image_mimetype(self) -> Optional[str]: 40 | r"""(:class:`~typing.Optional`\ [:class:`str`]) The MIME type of 41 | the image. It may be :const:`None` if it's not an image. 42 | 43 | """ 44 | images = self.attributes.get('imageinfo', []) 45 | if images and isinstance(images, collections.abc.Sequence): 46 | return images[0]['mime'] 47 | return None 48 | 49 | @property 50 | def image_resolution(self) -> Optional[Tuple[int, int]]: 51 | r"""(:class:`~typing.Optional`\ [:class:`~typing.Tuple`\ [:class:`int`, 52 | :class:`int`]]) The (width, height) pair of the image. 53 | It may be :const:`None` if it's not an image. 54 | 55 | """ 56 | images = self.attributes.get('imageinfo', []) 57 | if images and isinstance(images, collections.abc.Sequence): 58 | img = images[0] 59 | return img['width'], img['height'] 60 | return None 61 | 62 | @property 63 | def image_size(self) -> Optional[int]: 64 | r"""(:class:`~typing.Optional`\ [:class:`int`]) The size of the image 65 | in bytes. It may be :const:`None` if it's not an image. 66 | 67 | """ 68 | images = self.attributes.get('imageinfo', []) 69 | if images and isinstance(images, collections.abc.Sequence): 70 | return images[0]['size'] 71 | return None 72 | 73 | @property 74 | def attributes(self) -> Mapping[str, object]: 75 | if self.data is None: 76 | self.load() 77 | assert self.data is not None 78 | return self.data 79 | 80 | def load(self) -> None: 81 | url = './w/api.php?action=query&prop=imageinfo|info&inprop=url&iiprop=url|size|mime&format=json&titles={}' # noqa: E501 82 | url = url.format(urllib.parse.quote(self.title)) 83 | result = cast(Mapping[str, object], self.client.request(url)) 84 | if result.get('error'): 85 | raise FileError('the server respond an error: ' + 86 | repr(result['error'])) 87 | query = result['query'] 88 | assert isinstance(query, collections.abc.Mapping) 89 | self.data = next(iter(query['pages'].values())) 90 | 91 | def __repr__(self) -> str: 92 | return '<{0.__module__}.{0.__qualname__} {1!r}>'.format( 93 | type(self), self.title 94 | ) 95 | 96 | 97 | class FileError(ValueError, RuntimeError): 98 | """Exception raised when something goes wrong with :class:`File`.""" 99 | -------------------------------------------------------------------------------- /wikidata/datavalue.py: -------------------------------------------------------------------------------- 1 | """This module provides the decoder interface for customizing how datavalues 2 | are decoded, and the default :class:`Decoder` implementation. 3 | 4 | Technically the interface is just a callable so that its implementation 5 | doesn't necessarily have to be an instance of :class:`Decoder` or its subclass, 6 | but only need to satify:: 7 | 8 | typing.Callable[[wikidata.client.Client, str, typing.Mapping[str, object]], 9 | object] 10 | 11 | """ 12 | import collections.abc 13 | import datetime 14 | from typing import TYPE_CHECKING, Any, Mapping, Tuple, Union 15 | 16 | from .client import Client 17 | from .commonsmedia import File 18 | from .globecoordinate import GlobeCoordinate 19 | from .multilingual import MonolingualText 20 | from .quantity import Quantity 21 | if TYPE_CHECKING: 22 | from .entity import Entity # noqa: F401 23 | 24 | __all__ = 'DatavalueError', 'Decoder' 25 | 26 | 27 | class DatavalueError(ValueError): 28 | """Exception raised during decoding datavalues. It subclasses 29 | :exc:`ValueError` as well. 30 | 31 | """ 32 | 33 | def __init__(self, *args): 34 | super().__init__(*args) 35 | if len(self.args) < 2: 36 | raise TypeError('expected datavalue from 2nd positional argument') 37 | 38 | @property 39 | def datavalue(self): 40 | """The datavalue which caused the decoding error.""" 41 | return self.args[1] 42 | 43 | def __str__(self) -> str: 44 | message = self.args[0] 45 | if 'type' in self.datavalue: 46 | datavalue = dict(self.datavalue) 47 | type_ = datavalue.pop('type') 48 | return "{}: {{'type': {!r}, {}}}".format( 49 | message, type_, repr(datavalue)[1:-1] 50 | ) 51 | return '{}: {!r}'.format(message, self.datavalue) 52 | 53 | 54 | class Decoder: 55 | """Decode the given datavalue to a value of the appropriate Python type. 56 | For extensibility it uses visitor pattern and is intended to be subclassed. 57 | To customize decoding of datavalues subclass it and configure 58 | ``datavalue_decoder`` option of :class:`~.client.Client` to 59 | the customized decoder. 60 | 61 | It automatically invokes an appropriate visitor method using a simple 62 | rule of name: ``{datatype}__{datavalue[type]}``. For example, 63 | if the following call to a ``decoder`` was made:: 64 | 65 | decoder(client, 'mydatatype', {'type': 'mytype', 'value': '...'}) 66 | 67 | it's delegated to the following visitor method call: 68 | 69 | decoder.mydatatype__mytype(client, {'type': 'mytype', 'value': '...'}) 70 | 71 | If a decoder failed to find a visitor method matched to 72 | ``{datatype}__{datavalue[type]}`` pattern it secondly try to find 73 | a general version of visitor method: ``{datavalue[type]}`` which lacks 74 | double underscores. For example, for the following call:: 75 | 76 | decoder(client, 'mydatatype', {'type': 'mytype', 'value': '...'}) 77 | 78 | It firstly try to find the following visitor method: 79 | 80 | decoder.mydatatype__mytype 81 | 82 | but if there's no such method it secondly try to find the following 83 | general visitor method: 84 | 85 | decoder.mytype 86 | 87 | This twice-try dispatch is useful when to make a visitor method to 88 | be matched regardless of datatype. 89 | 90 | If its ``datavalue[type]`` contains hyphens they're replaced by 91 | underscores. For example:: 92 | 93 | decoder(client, 'string', 94 | {'type': 'wikibase-entityid', 'value': 'a text value'}) 95 | 96 | the above call is delegated to the following visitor method call:: 97 | 98 | decoder.string__wikibase_entityid( 99 | # Note that the ^ underscore 100 | client, 101 | {'type': 'wikibase-entityid', 'value': 'a text value'} 102 | ) 103 | 104 | """ 105 | 106 | def __call__(self, 107 | client: Client, 108 | datatype: str, 109 | datavalue: Mapping[str, object]) -> object: 110 | try: 111 | type_ = datavalue['type'] 112 | except KeyError: 113 | raise DatavalueError('no "type" specified', datavalue) 114 | assert isinstance(type_, str) 115 | if 'value' not in datavalue: 116 | raise DatavalueError('no "value" field', datavalue) 117 | method_name = '{}__{}'.format(datatype, type_).replace('-', '_') 118 | method = getattr(self, method_name, None) 119 | if callable(method): 120 | return method(client, datavalue) 121 | method_name = type_.replace('-', '_') 122 | method = getattr(self, method_name, None) 123 | if callable(method): 124 | return method(client, datavalue) 125 | raise DatavalueError('{!r} is unsupported type'.format(type_), 126 | datavalue) 127 | 128 | def wikibase_entityid(self, 129 | client: Client, 130 | datavalue: Mapping[str, object]) -> 'Entity': 131 | val = datavalue['value'] 132 | if not isinstance(val, collections.abc.Mapping): 133 | raise DatavalueError('expected a dictionary, not {!r}'.format(val), 134 | datavalue) 135 | try: 136 | id_ = val['id'] 137 | except KeyError: 138 | raise DatavalueError('no "id" field', datavalue) 139 | return client.get(id_) 140 | 141 | def string(self, client: Client, datavalue: Mapping[str, object]) -> str: 142 | value = datavalue['value'] 143 | assert isinstance(value, str) 144 | return value 145 | 146 | def time(self, 147 | client: Client, 148 | datavalue: Mapping[str, object]) -> Union[datetime.date, 149 | datetime.datetime, 150 | Tuple[int, int], 151 | int]: 152 | value = datavalue['value'] 153 | if not isinstance(value, collections.abc.Mapping): 154 | raise DatavalueError( 155 | 'expected a dictionary, not {!r}'.format(value), 156 | datavalue 157 | ) 158 | try: 159 | cal = value['calendarmodel'] 160 | except KeyError: 161 | raise DatavalueError('missing "calendarmodel" field', datavalue) 162 | if cal != 'http://www.wikidata.org/entity/Q1985727': 163 | raise DatavalueError('{!r} is unsupported calendarmodel for time ' 164 | 'datavalue'.format(cal), datavalue) 165 | try: 166 | time = value['time'] 167 | except KeyError: 168 | raise DatavalueError('missing "time" field', datavalue) 169 | if time[0] != '+': 170 | raise DatavalueError( 171 | '{!r}: only AD (CE) is supported'.format(time), 172 | datavalue 173 | ) 174 | try: 175 | tz = value['timezone'] 176 | except KeyError: 177 | raise DatavalueError('missing "timezone" field', datavalue) 178 | if tz != 0: 179 | raise DatavalueError( 180 | '{!r}: timezone other than 0 is unsupported'.format( 181 | value['timezone'] 182 | ), 183 | datavalue 184 | ) 185 | if 'before' not in value or 'after' not in value: 186 | raise DatavalueError('before/after field is missing', datavalue) 187 | elif value['before'] != 0 or value['after'] != 0: 188 | raise DatavalueError( 189 | 'uncertainty range time (represented using before/' 190 | 'after) is unsupported', 191 | datavalue 192 | ) 193 | try: 194 | precision = value['precision'] 195 | except KeyError: 196 | raise DatavalueError('precision field is missing', datavalue) 197 | if precision == 7: 198 | return int(time[1:3]) 199 | if precision == 9: 200 | # The time only specifies the year. 201 | return int(time[1:5]) 202 | if precision == 10: 203 | # this time only specifies year and month (no day) 204 | return (int(time[1:5]), int(time[6:8])) 205 | if precision == 11: 206 | return datetime.date(int(time[1:5]), int(time[6:8]), 207 | int(time[9:11])) 208 | elif precision == 14: 209 | return datetime.datetime.strptime( 210 | time[1:], 211 | '%Y-%m-%dT%H:%M:%SZ' 212 | ).replace(tzinfo=datetime.timezone.utc) 213 | else: 214 | raise DatavalueError( 215 | '{!r}: time precision other than 7, 9, 10, 11 or 14 is ' 216 | 'unsupported'.format(precision), 217 | datavalue 218 | ) 219 | 220 | def monolingualtext(self, 221 | client: Client, 222 | datavalue: Mapping[str, object]) -> MonolingualText: 223 | pair = datavalue['value'] 224 | return MonolingualText(pair['text'], pair['language']) # type: ignore 225 | 226 | def quantity(self, 227 | client: Client, 228 | datavalue: Mapping[str, Any]) -> Quantity: 229 | pair = datavalue['value'] 230 | raw_unit = pair.get("unit", "1") 231 | if raw_unit == "1": 232 | # If the value is unitless, the unit has a string value "1" 233 | unit = None 234 | else: 235 | # Try to parse the unit as an Entity. 236 | unit_entity = raw_unit.split("http://www.wikidata.org/entity/") 237 | if len(unit_entity) != 2: 238 | raise DatavalueError( 239 | "Unit string {} does not appear to be a " 240 | "valid WikiData entity URL or \"1\"".format(raw_unit)) 241 | unit = client.get(unit_entity[1]) 242 | # Parse the amount as a float 243 | amount = float(pair["amount"]) 244 | # Parse the lower and upper bound 245 | lower_bound = pair.get('lower_bound', None) 246 | lower_bound = float(lower_bound) if lower_bound else lower_bound 247 | upper_bound = pair.get('upper_bound', None) 248 | upper_bound = float(upper_bound) if upper_bound else upper_bound 249 | return Quantity(amount, 250 | lower_bound, 251 | upper_bound, 252 | unit) 253 | 254 | def globecoordinate(self, 255 | client: Client, 256 | datavalue: Mapping[str, Any]) -> GlobeCoordinate: 257 | pair = datavalue['value'] 258 | # Try to split out the entity from the globe string. 259 | globe_entity = pair["globe"].split("http://www.wikidata.org/entity/") 260 | if len(globe_entity) != 2: 261 | raise DatavalueError( 262 | "Globe string {} does not appear to be a " 263 | "valid WikiData entity URL".format(pair["globe"])) 264 | entity_id = globe_entity[1] 265 | return GlobeCoordinate(pair['latitude'], 266 | pair['longitude'], 267 | client.get(entity_id), 268 | pair['precision']) 269 | 270 | def commonsMedia__string(self, 271 | client: Client, 272 | datavalue: Mapping[str, object]) -> File: 273 | return File(client, 'File:{0}'.format(datavalue['value'])) 274 | -------------------------------------------------------------------------------- /wikidata/entity.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | import enum 3 | import logging 4 | import pprint 5 | from typing import ( 6 | TYPE_CHECKING, 7 | Iterator, 8 | Mapping, 9 | NewType, 10 | Optional, 11 | Sequence, 12 | Tuple, 13 | Type, 14 | cast, 15 | overload, 16 | ) 17 | 18 | from .multilingual import MultilingualText 19 | 20 | if TYPE_CHECKING: 21 | from .client import Client # noqa: F401 22 | 23 | __all__ = 'Entity', 'EntityId', 'EntityState', 'EntityType' 24 | 25 | 26 | #: The identifier of each :class:`Entity`. Alias of :class:`str`. 27 | EntityId = NewType('EntityId', str) 28 | 29 | 30 | class multilingual_attribute: 31 | """Define accessor to a multilingual attribute of entity.""" 32 | 33 | def __init__(self, attribute: str) -> None: 34 | self.attribute = attribute 35 | 36 | @overload 37 | def __get__( 38 | self, 39 | obj: None, 40 | cls: Type['Entity'] = ... 41 | ) -> 'multilingual_attribute': 42 | ... 43 | 44 | @overload 45 | def __get__( 46 | self, 47 | obj: 'Entity', 48 | cls: Type['Entity'] = ... 49 | ) -> MultilingualText: 50 | ... 51 | 52 | def __get__(self, obj, cls=None): 53 | if obj is None or isinstance(obj, type): 54 | return self 55 | cache_id = '$' + self.attribute 56 | try: 57 | value = obj.__dict__[cache_id] 58 | except KeyError: 59 | attr = obj.attributes.get(self.attribute) or {} 60 | assert isinstance(attr, collections.abc.Mapping) 61 | pairs = ( 62 | (item['language'], item['value']) 63 | for item in attr.values() 64 | ) 65 | value = MultilingualText({k: v for k, v in pairs if k}) 66 | obj.__dict__[cache_id] = value 67 | return value 68 | 69 | 70 | class EntityState(enum.Enum): 71 | """Define state of :class:`Entity`. 72 | 73 | .. versionadded:: 0.7.0 74 | 75 | """ 76 | 77 | #: (:class:`EntityState`) Not loaded yet. Unknown whether the entity 78 | #: does exist or not. 79 | not_loaded = 'not_loaded' 80 | 81 | #: (:class:`EntityState`) The entity exists and is already loaded. 82 | loaded = 'loaded' 83 | 84 | #: (:class:`EntityState`) The entity does not exist. 85 | non_existent = 'non_existent' 86 | 87 | 88 | class EntityType(enum.Enum): 89 | """The enumerated type which consists of two possible values: 90 | 91 | - :attr:`~EntityType.item` 92 | - :attr:`~EntityType.property` 93 | 94 | .. versionadded:: 0.2.0 95 | 96 | """ 97 | 98 | #: (:class:`EntityType`) Items are :class:`Entity` objects that are 99 | #: typically represented by Wikipage (at least in some Wikipedia 100 | #: languages). They can be viewed as "the thing that a Wikipage is about," 101 | #: which could be an individual thing (the person `Albert Einstein`_), 102 | #: a general class of things (the class of all Physicists_), 103 | #: and any other concept that is the subject of some Wikipedia page 104 | #: (including things like `History of Berlin`_). 105 | #: 106 | #: .. seealso:: 107 | #: 108 | #: Items_ --- Wikibase Data Model 109 | #: The data model of Wikibase describes the structure of 110 | #: the data that is handled in Wikibase. 111 | #: 112 | #: .. _Albert Einstein: https://en.wikipedia.org/wiki/Albert_Einstein 113 | #: .. _Physicists: https://en.wikipedia.org/wiki/Physicist 114 | #: .. _History of Berlin: https://en.wikipedia.org/wiki/History_of_Berlin 115 | #: .. _Items: https://www.mediawiki.org/wiki/Wikibase/DataModel#Items 116 | item = 'item' 117 | 118 | #: (:class:`EntityType`) Properties are :class:`Entity` objects that 119 | #: describe a relationship between items (or other :class:`Entity` objects) 120 | #: and values of the property. Typical properties are *population* 121 | #: (using numbers as values), *binomial name* (using strings as values), 122 | #: but also *has father* and *author of* (both using items as values). 123 | #: 124 | #: .. seealso:: 125 | #: 126 | #: Properties_ --- Wikibase Data Model 127 | #: The data model of Wikibase describes the structure of 128 | #: the data that is handled in Wikibase. 129 | #: 130 | #: .. _Properties: https://mediawiki.org/wiki/Wikibase/DataModel#Properties 131 | property = 'property' 132 | 133 | 134 | class Entity(collections.abc.Mapping, collections.abc.Hashable): 135 | r"""Wikidata entity. Can be an item or a property. Its attrributes 136 | can be lazily loaded. 137 | 138 | To get an entity use :meth:`Client.get() ` 139 | method instead of the constructor of :class:`Entity`. 140 | 141 | .. note:: 142 | 143 | Although it implements :class:`~typing.Mapping`\ [:class:`EntityId`, 144 | :class:`object`], it actually is multidict. See also :meth:`getlist()` 145 | method. 146 | 147 | .. versionchanged:: 0.2.0 148 | 149 | Implemented :class:`~typing.Mapping`\ [:class:`EntityId`, 150 | :class:`object`] protocol for easy access of statement values. 151 | 152 | .. versionchanged:: 0.2.0 153 | 154 | Implemented :class:`~typing.Hashable` protocol and 155 | :token:`==`/:token:`!=` operators for equality test. 156 | 157 | .. attribute:: state 158 | 159 | (:class:`EntityState`) The loading state. 160 | 161 | .. versionadded:: 0.7.0 162 | 163 | """ 164 | 165 | label = multilingual_attribute('labels') 166 | description = multilingual_attribute('descriptions') 167 | 168 | def __init__(self, id: EntityId, client: 'Client') -> None: 169 | self.id = id 170 | self.client = client 171 | self.data: Optional[Mapping[str, object]] = None 172 | self.state = EntityState.not_loaded # type: EntityState 173 | 174 | def __eq__(self, other) -> bool: 175 | if not isinstance(other, type(self)): 176 | raise TypeError( 177 | 'expected an instance of {0.__module__}.{0.__qualname__}, ' 178 | 'not {1!r}'.format(type(self), other) 179 | ) 180 | return other.id == self.id and self.client is other.client 181 | 182 | def __hash__(self) -> int: 183 | return hash((self.id, id(self.client))) 184 | 185 | def __len__(self) -> int: 186 | claims_map = self.attributes.get('claims') or {} 187 | assert isinstance(claims_map, collections.abc.Mapping) 188 | return len(claims_map) 189 | 190 | def __iter__(self) -> Iterator['Entity']: 191 | client = self.client 192 | claims_map = self.attributes.get('claims') or {} 193 | assert isinstance(claims_map, collections.abc.Mapping) 194 | for prop_id in claims_map: 195 | yield client.get(prop_id) 196 | 197 | def __getitem__(self, key: 'Entity') -> object: 198 | result = self.getlist(key) 199 | if result: 200 | return result[0] 201 | raise KeyError(key) 202 | 203 | def getlist(self, key: 'Entity') -> Sequence[object]: 204 | r"""Return all values associated to the given ``key`` property 205 | in sequence. 206 | 207 | :param key: The property entity. 208 | :type key: :class:`Entity` 209 | :return: A sequence of all values associated to the given ``key`` 210 | property. It can be empty if nothing is associated to 211 | the property. 212 | :rtype: :class:`~typing.Sequence`\ [:class:`object`] 213 | 214 | """ 215 | if not (isinstance(key, type(self)) and 216 | key.type is EntityType.property): 217 | return [] 218 | claims_map = self.attributes.get('claims') or {} 219 | assert isinstance(claims_map, collections.abc.Mapping) 220 | claims = claims_map.get(key.id, []) 221 | claims.sort(key=lambda claim: claim['rank'], # FIXME 222 | reverse=True) 223 | logger = logging.getLogger(__name__ + '.Entity.getitem') 224 | if logger.isEnabledFor(logging.DEBUG): 225 | logger.debug('claim data: %s', pprint.pformat(claims)) 226 | decode = self.client.decode_datavalue 227 | return [decode(snak['datatype'], snak['datavalue']) 228 | for snak in (claim['mainsnak'] for claim in claims) 229 | if snak['snaktype'] == 'value'] 230 | 231 | def iterlists(self) -> Iterator[Tuple['Entity', Sequence[object]]]: 232 | for prop in self: 233 | yield prop, self.getlist(prop) 234 | 235 | def lists(self) -> Sequence[Tuple['Entity', Sequence[object]]]: 236 | """Similar to :meth:`items()` except the returning pairs have 237 | each list of values instead of each single value. 238 | 239 | :return: The pairs of (key, values) where values is a sequence. 240 | :rtype: :class:`~typing.Sequence`\\ [:class:`~typing.Tuple`\\ \ 241 | [:class:`Entity`, :class:`~typing.Sequence`\\ [:class:`object`]]] 242 | 243 | """ 244 | return list(self.iterlists()) 245 | 246 | def iterlistvalues(self) -> Iterator[Sequence[object]]: 247 | for _, values in self.iterlists(): 248 | yield values 249 | 250 | def listvalues(self) -> Sequence[Sequence[object]]: 251 | return list(self.iterlistvalues()) 252 | 253 | @property 254 | def type(self) -> EntityType: 255 | """(:class:`EntityType`) The type of entity, :attr:`~EntityType.item` 256 | or :attr:`~EntityType.property`. 257 | 258 | .. versionadded:: 0.2.0 259 | 260 | """ 261 | if self.data is None: 262 | guessed_type = self.client.guess_entity_type(self.id) 263 | if guessed_type is not None: 264 | return guessed_type 265 | # If guessing was failed follow the straightforward way. 266 | return EntityType(self.attributes['type']) 267 | 268 | @property 269 | def attributes(self) -> Mapping[str, object]: 270 | if self.data is None: 271 | self.load() 272 | assert self.data is not None 273 | return self.data 274 | 275 | def load(self) -> None: 276 | if self.state is EntityState.non_existent: 277 | return 278 | 279 | url = './wiki/Special:EntityData/{}.json'.format(self.id) 280 | result = self.client.request(url) 281 | if result is None: 282 | self.state = EntityState.non_existent 283 | return 284 | 285 | assert isinstance(result, collections.abc.Mapping) 286 | entities = result['entities'] 287 | assert isinstance(entities, collections.abc.Mapping) 288 | assert len(entities) == 1 289 | redirected = False 290 | entity_id = self.id 291 | try: 292 | data = entities[entity_id] 293 | self.state = EntityState.loaded 294 | except KeyError: 295 | entity_id = cast(EntityId, next(iter(entities))) 296 | data = entities[entity_id] 297 | redirected = True 298 | self.state = EntityState.not_loaded 299 | assert isinstance(data, collections.abc.Mapping) 300 | self.data = data 301 | self.id = entity_id 302 | if redirected: 303 | canon = self.client.get(entity_id, load=False) 304 | if canon.data is None: 305 | canon.data = dict(data) 306 | 307 | def __repr__(self) -> str: 308 | if self.data: 309 | label = str(self.label) if self.label else ... 310 | else: 311 | label = None 312 | return '<{0.__module__}.{0.__qualname__} {1}{2}>'.format( 313 | type(self), self.id, 314 | ' {!r}'.format(label) if label else '' 315 | ) 316 | -------------------------------------------------------------------------------- /wikidata/globecoordinate.py: -------------------------------------------------------------------------------- 1 | from .entity import Entity 2 | 3 | __all__ = 'GlobeCoordinate', 4 | 5 | 6 | class GlobeCoordinate: 7 | """ 8 | Literal data for a geographical position given as a latitude-longitude pair 9 | in gms or decimal degrees for the given stellar body. 10 | """ 11 | 12 | latitude = None # type: float 13 | longitude = None # type: float 14 | globe = None # type: Entity 15 | precision = None # type: float 16 | 17 | def __init__(self, 18 | latitude: float, 19 | longitude: float, 20 | globe: Entity, 21 | precision: float) -> None: 22 | self.latitude = latitude 23 | self.longitude = longitude 24 | self.globe = globe 25 | self.precision = precision 26 | 27 | def __eq__(self, other) -> bool: 28 | if not isinstance(other, type(self)): 29 | raise TypeError( 30 | 'expected an instance of {0.__module__}.{0.__qualname__}, ' 31 | 'not {1!r}'.format(type(self), other) 32 | ) 33 | return (other.latitude == self.latitude and 34 | other.longitude == self.longitude and 35 | other.globe == self.globe and 36 | other.precision == self.precision) 37 | 38 | def __hash__(self): 39 | return hash((self.longitude, 40 | self.latitude, 41 | self.globe, 42 | self.precision)) 43 | 44 | def __repr__(self) -> str: 45 | return ('{0.__module__}.{0.__qualname__}({1!r}, ' 46 | '{2!r}, {3!r}, {4!r})').format( 47 | type(self), 48 | self.latitude, 49 | self.longitude, 50 | self.globe, 51 | self.precision 52 | ) 53 | -------------------------------------------------------------------------------- /wikidata/multilingual.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | from typing import Iterator, Mapping, NewType, Type, Union, cast 3 | 4 | __all__ = 'Locale', 'MonolingualText', 'MultilingualText' 5 | 6 | 7 | #: The locale of each :class:`MonolingualText` or internal 8 | #: mapping of each :class:`MultilingualText`. Alias of :class:`str`. 9 | #: 10 | #: .. versionadded:: 0.7.0 11 | Locale = NewType('Locale', str) 12 | 13 | 14 | class MultilingualText(collections.abc.Mapping): 15 | 16 | __slots__ = 'texts', 17 | 18 | def __init__(self, texts: Mapping[Union[Locale, str], str]) -> None: 19 | self.texts = {Locale(lc): t for lc, t in texts.items()} 20 | 21 | def __iter__(self) -> Iterator[Locale]: 22 | for locale in self.texts: 23 | yield locale 24 | 25 | def __len__(self) -> int: 26 | return len(self.texts) 27 | 28 | def __contains__(self, locale: Locale) -> bool: # type: ignore[override] 29 | return locale in self.texts 30 | 31 | def __getitem__(self, locale: Locale) -> str: 32 | return self.texts[locale] 33 | 34 | def __bool__(self) -> bool: 35 | return bool(self.texts) 36 | 37 | def __str__(self) -> str: 38 | try: 39 | return self.texts[Locale('en')] 40 | except KeyError: 41 | value = '' 42 | for lang, value in self.texts.items(): 43 | if lang.startswith('en_'): 44 | return value 45 | return value 46 | 47 | def __repr__(self) -> str: 48 | if self: 49 | return 'm{0!r}'.format(str(self)) 50 | return '{0.__module__}.{0.__qualname__}({{}})'.format(type(self)) 51 | 52 | 53 | class MonolingualText(str): 54 | """ 55 | Locale-denoted text. It's almost equivalent to :class:`str` (and indeed 56 | subclasses :class:`str`) except that it has an extra attribute, 57 | :attr:`locale`, that denotes what language the text is written in. 58 | 59 | .. versionchanged:: 0.8.0 60 | 61 | The type hint of the constructor's ``locale`` parameter became 62 | :class:`Locale`. 63 | 64 | """ 65 | 66 | #: (:class:`Locale`) The code of :attr:`locale`. 67 | #: 68 | #: .. versionchanged:: 0.7.0 69 | #: 70 | #: The type became :class:`Locale` (was :class:`babel.core.Locale`). 71 | locale: Locale 72 | 73 | def __new__(cls: Type[str], 74 | text: str, 75 | locale: Locale) -> 'MonolingualText': 76 | self = cast(MonolingualText, str.__new__(cls, text)) 77 | self.locale = locale 78 | return self 79 | 80 | def __repr__(self) -> str: 81 | altrepr = '({0}:) {1!s}'.format(self.locale, self) 82 | return '{0!r}[{1}:]'.format(altrepr, len(self.locale) + 4) 83 | -------------------------------------------------------------------------------- /wikidata/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dahlia/wikidata/0a3e0eccef8f78c395e7ed8a258fcbe13e1752fc/wikidata/py.typed -------------------------------------------------------------------------------- /wikidata/quantity.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from .entity import Entity 4 | 5 | __all__ = 'Quantity', 6 | 7 | 8 | class Quantity: 9 | """A Quantity value represents a decimal number, together with information 10 | about the uncertainty interval of this number, and a unit of measurement. 11 | """ 12 | 13 | amount = None # type: float 14 | lower_bound = None # type: Optional[float] 15 | upper_bound = None # type: Optional[float] 16 | unit = None # type: Optional[Entity] 17 | 18 | def __init__(self, 19 | amount: float, 20 | lower_bound: Optional[float], 21 | upper_bound: Optional[float], 22 | unit: Optional[Entity]) -> None: 23 | self.amount = amount 24 | self.lower_bound = lower_bound 25 | self.upper_bound = upper_bound 26 | self.unit = unit 27 | 28 | def __eq__(self, other) -> bool: 29 | if not isinstance(other, type(self)): 30 | raise TypeError( 31 | 'expected an instance of {0.__module__}.{0.__qualname__}, ' 32 | 'not {1!r}'.format(type(self), other) 33 | ) 34 | return (other.amount == self.amount and 35 | other.lower_bound == self.lower_bound and 36 | other.upper_bound == self.upper_bound and 37 | other.unit == self.unit) 38 | 39 | def __hash__(self): 40 | return hash((self.amount, 41 | self.lower_bound, 42 | self.upper_bound, 43 | self.unit)) 44 | 45 | def __repr__(self) -> str: 46 | return ('{0.__module__}.{0.__qualname__}({1!r}, ' 47 | '{2!r}, {3!r}, {4!r})').format( 48 | type(self), 49 | self.amount, 50 | self.lower_bound, 51 | self.upper_bound, 52 | self.unit 53 | ) 54 | --------------------------------------------------------------------------------