Ladsgroup has submitted this change and it was merged.
Change subject: Init commit
......................................................................
Init commit
Codes moved from pywikibot/core.
Change-Id: I3aa14b0fa0083c855bb1d7773bb6898db8d13b3f
---
A README.rst
A pywikibase/__init__.py
A pywikibase/claim.py
A pywikibase/coordinate.py
A pywikibase/exceptions.py
A pywikibase/itempage.py
A pywikibase/propertypage.py
A pywikibase/wbproperty.py
A pywikibase/wbquantity.py
A pywikibase/wbtime.py
A pywikibase/wikibasepage.py
11 files changed, 1,298 insertions(+), 0 deletions(-)
Approvals:
Ladsgroup: Verified; Looks good to me, approved
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..281af27
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,6 @@
+pywikibase is the library designed to handle DataModel of Wikiabse.
+
+It was forked from pywikibot-core in 28 July 2015.
+
+Credits of this code belong to pywikibot team.
+pywikibase and pywikibot are distributed under MIT license.
diff --git a/pywikibase/__init__.py b/pywikibase/__init__.py
new file mode 100644
index 0000000..91cc203
--- /dev/null
+++ b/pywikibase/__init__.py
@@ -0,0 +1,12 @@
+from coordinate import Coordinate
+from wbtime import WbTime
+from wbquantity import WbQuantity
+from itempage import ItemPage
+from wbproperty import Property
+from propertypage import PropertyPage
+from wikibasepage import WikibasePage
+from claim import Claim
+
+# Not to mess with pyflakes
+__all__ = (Coordinate, WbQuantity, WbTime, ItemPage, Property, PropertyPage,
+ WikibasePage, Claim)
diff --git a/pywikibase/claim.py b/pywikibase/claim.py
new file mode 100644
index 0000000..5d40fcd
--- /dev/null
+++ b/pywikibase/claim.py
@@ -0,0 +1,424 @@
+# -*- coding: utf-8 -*-
+"""
+Handling claims in a Wikibase entity.
+"""
+
+#
+# (C) Pywikibot team, 2008-2015
+#
+# Distributed under the terms of the MIT license.
+#
+from __future__ import unicode_literals
+from collections import defaultdict, OrderedDict
+
+from coordinate import Coordinate
+from wbtime import WbTime
+from wbquantity import WbQuantity
+from itempage import ItemPage
+from wbproperty import Property
+
+
+class Claim(Property):
+
+ """
+ A Claim on a Wikibase entity.
+
+ Claims are standard claims as well as references.
+ """
+
+ TARGET_CONVERTER = {
+ 'wikibase-item': lambda value, site:
+ ItemPage(site, 'Q' + str(value['numeric-id'])),
+ 'globe-coordinate': Coordinate.fromWikibase,
+ 'time': lambda value, site: WbTime.fromWikibase(value),
+ 'quantity': lambda value, site: WbQuantity.fromWikibase(value),
+ }
+
+ def __init__(self, site, pid, snak=None, hash=None, isReference=False,
+ isQualifier=False, **kwargs):
+ """
+ Constructor.
+
+ Defined by the "snak" value, supplemented by site + pid
+
+ @param site: repository the claim is on
+ @type site: pywikibot.site.DataSite or None
+ @param pid: property id, with "P" prefix
+ @param snak: snak identifier for claim
+ @param hash: hash identifier for references
+ @param isReference: whether specified claim is a reference
+ @param isQualifier: whether specified claim is a qualifier
+ """
+ Property.__init__(self, site, pid, **kwargs)
+ self.snak = snak
+ self.hash = hash
+ self.isReference = isReference
+ self.isQualifier = isQualifier
+ if self.isQualifier and self.isReference:
+ raise ValueError(
+ u'Claim cannot be both a qualifier and reference.')
+ self.sources = []
+ self.qualifiers = OrderedDict()
+ self.target = None
+ self.snaktype = 'value'
+ self.rank = 'normal'
+ self.on_item = None # The item it's on
+
+ @classmethod
+ def fromJSON(cls, site, data):
+ """
+ Create a claim object from JSON returned in the API call.
+
+ @param data: JSON containing claim data
+ @type data: dict
+
+ @return: Claim
+ """
+ claim = cls(site, data['mainsnak']['property'],
+ datatype=data['mainsnak'].get('datatype', None))
+ if 'id' in data:
+ claim.snak = data['id']
+ elif 'hash' in data:
+ claim.isReference = True
+ claim.hash = data['hash']
+ else:
+ claim.isQualifier = True
+ claim.snaktype = data['mainsnak']['snaktype']
+ if claim.getSnakType() == 'value':
+ value = data['mainsnak']['datavalue']['value']
+ # The default covers string, url types
+ claim.target = Claim.TARGET_CONVERTER.get(
+ claim.type, lambda value, site: value)(value, site)
+ if 'rank' in data: # References/Qualifiers don't have ranks
+ claim.rank = data['rank']
+ if 'references' in data:
+ for source in data['references']:
+ claim.sources.append(cls.referenceFromJSON(site, source))
+ if 'qualifiers' in data:
+ for prop in data['qualifiers-order']:
+ claim.qualifiers[prop] = [
+ cls.qualifierFromJSON(site, qualifier)
+ for qualifier in data['qualifiers'][prop]]
+ return claim
+
+ @classmethod
+ def referenceFromJSON(cls, site, data):
+ """
+ Create a dict of claims from reference JSON returned in the API call.
+
+ Reference objects are represented a
+ bit differently, and require some
+ more handling.
+
+ @return: dict
+ """
+ source = OrderedDict()
+
+ # Before #84516 Wikibase did not implement snaks-order.
+ # https://gerrit.wikimedia.org/r/#/c/84516/
+ if 'snaks-order' in data:
+ prop_list = data['snaks-order']
+ else:
+ prop_list = data['snaks'].keys()
+
+ for prop in prop_list:
+ for claimsnak in data['snaks'][prop]:
+ claim = cls.fromJSON(site, {'mainsnak': claimsnak,
+ 'hash': data['hash']})
+ if claim.getID() not in source:
+ source[claim.getID()] = []
+ source[claim.getID()].append(claim)
+ return source
+
+ @classmethod
+ def qualifierFromJSON(cls, site, data):
+ """
+ Create a Claim for a qualifier from JSON.
+
+ Qualifier objects are represented a bit
+ differently like references, but I'm not
+ sure if this even requires it's own function.
+
+ @return: Claim
+ """
+ return cls.fromJSON(site, {'mainsnak': data,
+ 'hash': data['hash']})
+
+ def toJSON(self):
+ """
+ Create dict suitable for the MediaWiki API.
+
+ @rtype: dict
+ """
+ data = {
+ 'mainsnak': {
+ 'snaktype': self.snaktype,
+ 'property': self.getID()
+ },
+ 'type': 'statement'
+ }
+ if hasattr(self, 'snak') and self.snak is not None:
+ data['id'] = self.snak
+ if hasattr(self, 'rank') and self.rank is not None:
+ data['rank'] = self.rank
+ if self.getSnakType() == 'value':
+ data['mainsnak']['datatype'] = self.type
+ data['mainsnak']['datavalue'] = self._formatDataValue()
+ if self.isQualifier or self.isReference:
+ data = data['mainsnak']
+ if hasattr(self, 'hash') and self.hash is not None:
+ data['hash'] = self.hash
+ else:
+ if len(self.qualifiers) > 0:
+ data['qualifiers'] = {}
+ data['qualifiers-order'] = list(self.qualifiers.keys())
+ for prop, qualifiers in self.qualifiers.items():
+ for qualifier in qualifiers:
+ qualifier.isQualifier = True
+ data['qualifiers'][prop] = [qualifier.toJSON()
+ for qualifier in qualifiers]
+ if len(self.sources) > 0:
+ data['references'] = []
+ for collection in self.sources:
+ reference = {'snaks': {},
+ 'snaks-order': list(collection.keys())}
+ for prop, val in collection.items():
+ reference['snaks'][prop] = []
+ for source in val:
+ source.isReference = True
+ src_data = source.toJSON()
+ if 'hash' in src_data:
+ if 'hash' not in reference:
+ reference['hash'] = src_data['hash']
+ del src_data['hash']
+ reference['snaks'][prop].append(src_data)
+ data['references'].append(reference)
+ return data
+
+ def setTarget(self, value):
+ """
+ Set the target value in the local object.
+
+ @param value: The new target value.
+ @type value: object
+
+ @exception ValueError: if value is not of the type
+ required for the Claim type.
+ """
+ value_class = self.types[self.type]
+ if not isinstance(value, value_class):
+ raise ValueError("%s is not type %s."
+ % (value, value_class))
+ self.target = value
+
+ def getTarget(self):
+ """
+ Return the target value of this Claim.
+
+ None is returned if no target is set
+
+ @return: object
+ """
+ return self.target
+
+ def getSnakType(self):
+ """
+ Return the type of snak.
+
+ @return: str ('value', 'somevalue' or 'novalue')
+ """
+ return self.snaktype
+
+ def setSnakType(self, value):
+ """Set the type of snak.
+
+ @param value: Type of snak
+ @type value: str ('value', 'somevalue', or 'novalue')
+ """
+ if value in ['value', 'somevalue', 'novalue']:
+ self.snaktype = value
+ else:
+ raise ValueError(
+ "snaktype must be 'value', 'somevalue', or 'novalue'.")
+
+ def getRank(self):
+ """Return the rank of the Claim."""
+ return self.rank
+
+ def setRank(self, rank):
+ """Set the rank of the Claim."""
+ self.rank = rank
+
+ def changeRank(self, rank):
+ """Change the rank of the Claim and save."""
+ self.rank = rank
+
+ def changeSnakType(self, value=None, **kwargs):
+ """
+ Save the new snak value.
+
+ TODO: Is this function really needed?
+ """
+ if value:
+ self.setSnakType(value)
+ self.changeTarget(snaktype=self.getSnakType(), **kwargs)
+
+ def getSources(self):
+ """
+ Return a list of sources, each being a list of Claims.
+
+ @return: list
+ """
+ return self.sources
+
+ def addSource(self, claim, **kwargs):
+ """
+ Add the claim as a source.
+
+ @param claim: the claim to add
+ @type claim: pywikibase.Claim
+ """
+ self.addSources([claim], **kwargs)
+
+ def addSources(self, claims, **kwargs):
+ """
+ Add the claims as one source.
+
+ @param claims: the claims to add
+ @type claims: list of pywikibase.Claim
+ """
+ source = defaultdict(list)
+ for claim in claims:
+ source[claim.getID()].append(claim)
+ self.sources.append(source)
+
+ def removeSource(self, source, **kwargs):
+ """
+ Remove the source. Calls removeSources().
+
+ @param source: the source to remove
+ @type source: pywikibase.Claim
+ """
+ self.removeSources([source], **kwargs)
+
+ def removeSources(self, sources, **kwargs):
+ """
+ Remove the sources.
+
+ @param sources: the sources to remove
+ @type sources: list of pywikibase.Claim
+ """
+ for source in sources:
+ source_dict = defaultdict(list)
+ source_dict[source.getID()].append(source)
+ self.sources.remove(source_dict)
+
+ def addQualifier(self, qualifier, **kwargs):
+ """Add the given qualifier.
+
+ @param qualifier: the qualifier to add
+ @type qualifier: Claim
+ """
+ qualifier.isQualifier = True
+ if qualifier.getID() in self.qualifiers:
+ self.qualifiers[qualifier.getID()].append(qualifier)
+ else:
+ self.qualifiers[qualifier.getID()] = [qualifier]
+
+ def target_equals(self, value):
+ """
+ Check whether the Claim's target is equal to specified value.
+
+ The function checks for:
+ - ItemPage ID equality
+ - WbTime year equality
+ - Coordinate equality, regarding precision
+ - direct equality
+
+ @param value: the value to compare with
+ @return: true if the Claim's target is equal to the value provided,
+ false otherwise
+ @rtype: bool
+ """
+ if (isinstance(self.target, ItemPage) and
+ isinstance(value, basestring) and
+ self.target.id == value):
+ return True
+
+ if (isinstance(self.target, WbTime) and
+ not isinstance(value, WbTime) and
+ self.target.year == int(value)):
+ return True
+
+ if (isinstance(self.target, Coordinate) and
+ isinstance(value, basestring)):
+ coord_args = [float(x) for x in value.split(',')]
+ if len(coord_args) >= 3:
+ precision = coord_args[2]
+ else:
+ precision = 0.0001 # Default value (~10 m at equator)
+ try:
+ if self.target.precision is not None:
+ precision = max(precision, self.target.precision)
+ except TypeError:
+ pass
+
+ if (abs(self.target.lat - coord_args[0]) <= precision and
+ abs(self.target.lon - coord_args[1]) <= precision):
+ return True
+
+ if self.target == value:
+ return True
+
+ return False
+
+ def has_qualifier(self, qualifier_id, target):
+ """
+ Check whether Claim contains specified qualifier.
+
+ @param qualifier_id: id of the qualifier
+ @type qualifier_id: str
+ @param target: qualifier target to check presence of
+ @return: true if the qualifier was found, false otherwise
+ @rtype: bool
+ """
+ if self.isQualifier or self.isReference:
+ raise ValueError(u'Qualifiers and references cannot have '
+ u'qualifiers.')
+
+ for qualifier in self.qualifiers.get(qualifier_id, []):
+ if qualifier.target_equals(target):
+ return True
+ return False
+
+ def _formatValue(self):
+ """
+ Format the target into the proper JSON value that Wikibase wants.
+
+ @return: JSON value
+ @rtype: dict
+ """
+ if self.type == 'wikibase-item':
+ value = {'entity-type': 'item',
+ 'numeric-id': self.getTarget().getID(numeric=True)}
+ elif self.type in ('string', 'url'):
+ value = self.getTarget()
+ elif self.type == 'commonsMedia':
+ value = self.getTarget().title(withNamespace=False)
+ elif self.type in ('globe-coordinate', 'time', 'quantity'):
+ value = self.getTarget().toWikibase()
+ else:
+ raise NotImplementedError('%s datatype is not supported yet.'
+ % self.type)
+ return value
+
+ def _formatDataValue(self):
+ """
+ Format the target into the proper JSON datavalue that Wikibase wants.
+
+ @return: Wikibase API representation with type and value.
+ @rtype: dict
+ """
+ return {'value': self._formatValue(),
+ 'type': self.value_types.get(self.type, self.type)
+ }
diff --git a/pywikibase/coordinate.py b/pywikibase/coordinate.py
new file mode 100644
index 0000000..a2328b7
--- /dev/null
+++ b/pywikibase/coordinate.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+"""
+Wikibase coordinate.
+"""
+
+#
+# (C) Pywikibot team, 2008-2015
+#
+# Distributed under the terms of the MIT license.
+#
+from __future__ import unicode_literals
+
+import math
+
+from exceptions import CoordinateGlobeUnknownException
+
+
+class Coordinate(object):
+
+ """
+ Class for handling and storing Coordinates.
+
+ For now its just being used for DataSite, but
+ in the future we can use it for the GeoData extension.
+ """
+
+ def __init__(self, lat, lon, alt=None, precision=None, globe='earth',
+ typ="", name="", dim=None, site=None, entity=''):
+ """
+ Represent a geo coordinate.
+
+ @param lat: Latitude
+ @type lat: float
+ @param lon: Longitude
+ @type lon: float
+ @param alt: Altitute? TODO FIXME
+ @param precision: precision
+ @type precision: float
+ @param globe: Which globe the point is on
+ @type globe: str
+ @param typ: The type of coordinate point
+ @type typ: str
+ @param name: The name
+ @type name: str
+ @param dim: Dimension (in meters)
+ @type dim: int
+ @param entity: The URL entity of a Wikibase item
+ @type entity: str
+ """
+ self.lat = lat
+ self.lon = lon
+ self.alt = alt
+ self._precision = precision
+ if globe:
+ globe = globe.lower()
+ self.globe = globe
+ self._entity = entity
+ self.type = typ
+ self.name = name
+ self._dim = dim
+ self.site = site
+
+ def __repr__(self):
+ string = 'Coordinate(%s, %s' % (self.lat, self.lon)
+ if self.globe != 'earth':
+ string += ', globe="%s"' % self.globe
+ string += ')'
+ return string
+
+ @property
+ def entity(self):
+ if self._entity:
+ return self._entity
+ return self.site.globes()[self.globe]
+
+ def toWikibase(self):
+ """
+ Export the data to a JSON object for the Wikibase API.
+
+ FIXME: Should this be in the DataSite object?
+ """
+ if self.globe not in self.site.globes():
+ raise CoordinateGlobeUnknownException(
+ u"%s is not supported in Wikibase yet."
+ % self.globe)
+ return {'latitude': self.lat,
+ 'longitude': self.lon,
+ 'altitude': self.alt,
+ 'globe': self.entity,
+ 'precision': self.precision,
+ }
+
+ @classmethod
+ def fromWikibase(cls, data, site):
+ """Constructor to create an object from Wikibase's JSON output."""
+ globes = {}
+ for k in site.globes():
+ globes[site.globes()[k]] = k
+
+ globekey = data['globe']
+ if globekey:
+ globe = globes.get(data['globe'])
+ else:
+ # Default to earth or should we use None here?
+ globe = 'earth'
+
+ return cls(data['latitude'], data['longitude'],
+ data['altitude'], data['precision'],
+ globe, site=site, entity=data['globe'])
+
+ @property
+ def precision(self):
+ u"""
+ Return the precision of the geo coordinate.
+
+ The biggest error (in degrees) will be given by the longitudinal error;
+ the same error in meters becomes larger (in degrees) further up north.
+ We can thus ignore the latitudinal error.
+
+ The longitudinal can be derived as follows:
+
+ In small angle approximation (and thus in radians):
+
+ M{Δλ ≈ Δpos / r_φ}, where r_φ is radius of earth at the given latitude.
+ Δλ is the error in longitude.
+
+ M{r_φ = r cos φ}, where r is the radius of earth, φ the latitude
+
+ Therefore::
+ precision = math.degrees(
+ self._dim/(radius*math.cos(math.radians(self.lat))))
+ """
+ if not self._precision:
+ radius = 6378137 # TODO: Support other globes
+ self._precision = math.degrees(
+ self._dim / (radius * math.cos(math.radians(self.lat))))
+ return self._precision
+
+ def precisionToDim(self):
+ """Convert precision from Wikibase to GeoData's dim."""
+ raise NotImplementedError
diff --git a/pywikibase/exceptions.py b/pywikibase/exceptions.py
new file mode 100644
index 0000000..9ad632e
--- /dev/null
+++ b/pywikibase/exceptions.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+"""
+Exceptions
+"""
+
+#
+# (C) Pywikibot team, 2008-2015
+#
+# Distributed under the terms of the MIT license.
+#
+from __future__ import unicode_literals
+
+
+class WikiBaseError(Exception):
+
+ """Wikibase related error."""
+
+ pass
+
+
+class CoordinateGlobeUnknownException(WikiBaseError, NotImplementedError):
+
+ """This globe is not implemented yet in either WikiBase or pywikibase."""
+
+ pass
+
+
+class EntityTypeUnknownException(WikiBaseError):
+
+ """The requested entity type is not recognised on this site."""
+
+ pass
diff --git a/pywikibase/itempage.py b/pywikibase/itempage.py
new file mode 100644
index 0000000..2fb58ed
--- /dev/null
+++ b/pywikibase/itempage.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+"""
+Handling a Wikibase entity.
+"""
+
+#
+# (C) Pywikibot team, 2008-2015
+#
+# Distributed under the terms of the MIT license.
+#
+from __future__ import unicode_literals
+
+import re
+
+from claim import Claim
+from wikibasepage import WikibasePage
+
+
+class ItemPage(WikibasePage):
+
+ """Wikibase entity of type 'item'.
+
+ A Wikibase item may be defined by either a 'Q' id (qid),
+ or by a site & title.
+
+ If an item is defined by site & title, once an item's qid has
+ been looked up, the item is then defined by the qid.
+ """
+
+ def __init__(self, site=None, title=None, content=None):
+ """
+ Constructor.
+
+ @param site: data repository
+ @type site: pywikibot.site.DataSite
+ @param title: id number of item, "Q###",
+ -1 or None for an empty item.
+ @type title: str
+ """
+ # Validate the title is 'Q' and a positive integer.
+ if title and not re.match(r'^Q[1-9]\d*$', title):
+ raise RuntimeError(
+ u"'%s' is not a valid item page title"
+ % title)
+
+ def get(self, *args, **kwargs):
+ """
+ Fetch all item data, and cache it.
+
+ @param args: values of props
+ """
+ data = super(ItemPage, self).get(*args, **kwargs)
+
+ # sitelinks
+ self.sitelinks = {}
+ if 'sitelinks' in self._content:
+ for dbname in self._content['sitelinks']:
+ self.sitelinks[dbname] = self._content[
+ 'sitelinks'][dbname]['title']
+
+ data['claims'] = self.claims
+ data['sitelinks'] = self.sitelinks
+ return data
+
+ def toJSON(self, diffto=None):
+ """
+ Create JSON suitable for Wikibase API.
+
+ When diffto is provided, JSON representing differences
+ to the provided data is created.
+
+ @param diffto: JSON containing claim data
+ @type diffto: dict
+
+ @return: dict
+ """
+ data = super(ItemPage, self).toJSON(diffto=diffto)
+
+ self._diff_to('sitelinks', 'site', 'title', diffto, data)
+
+ return data
+
+ def addClaim(self, claim):
+ """
+ Add a claim to the item.
+
+ @param claim: The claim to add
+ @type claim: Claim
+ """
+ claim.on_item = self
+ if claim.getID() in self.claims:
+ self.claims[claim.getID()].append(claim)
+ else:
+ self.claims[claim.getID()] = [claim]
+
+ def removeClaims(self, claims, **kwargs):
+ """
+ Remove the claims from the item.
+
+ @param claims: list of claims to be removed
+ @type claims: list or pywikibot.Claim
+
+ """
+ # this check allows single claims to be removed by pushing them into a
+ # list of length one.
+ if isinstance(claims, Claim):
+ claims = [claims]
+ for claim in claims:
+ if claim in self.claims.get(claim.getID(), []):
+ self.claims.get(claim.getID(), []).remove(claim)
diff --git a/pywikibase/propertypage.py b/pywikibase/propertypage.py
new file mode 100644
index 0000000..cba7ca6
--- /dev/null
+++ b/pywikibase/propertypage.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+"""
+Handling a Wikibase property pages.
+"""
+
+#
+# (C) Pywikibot team, 2008-2015
+#
+# Distributed under the terms of the MIT license.
+#
+from __future__ import unicode_literals
+
+from wikibasepage import WikibasePage
+from wbproperty import Property
+from calim import Claim
+
+
+class PropertyPage(WikibasePage, Property):
+
+ """
+ A Wikibase entity in the property namespace.
+
+ Should be created as::
+
+ PropertyPage(DataSite, 'P21')
+ """
+
+ def __init__(self, source=None, title=u""):
+ """
+ Constructor.
+
+ @param source: data repository property is on
+ @type source: pywikibot.site.DataSite or None
+ @param title: page name of property, like "P##"
+ @type title: str
+ """
+ WikibasePage.__init__(self, source, title)
+ if not title or not self.id.startswith('P'):
+ raise ValueError(
+ u"'%s' is not an property page title" % title)
+ Property.__init__(self, source, self.id)
+
+ def get(self, force=False, *args):
+ """
+ Fetch the property entity, and cache it.
+
+ @param force: override caching
+ @param args: values of props
+ """
+ if force or not hasattr(self, '_content'):
+ WikibasePage.get(self, force=force, *args)
+
+ def newClaim(self, *args, **kwargs):
+ """
+ Helper function to create a new claim object for this property.
+
+ @return: Claim
+ """
+ return Claim(self.site, self.getID(), datatype=self.type,
+ *args, **kwargs)
diff --git a/pywikibase/wbproperty.py b/pywikibase/wbproperty.py
new file mode 100644
index 0000000..a4f11bf
--- /dev/null
+++ b/pywikibase/wbproperty.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+"""
+Handling a Wikibase property.
+"""
+
+#
+# (C) Pywikibot team, 2008-2015
+#
+# Distributed under the terms of the MIT license.
+#
+from __future__ import unicode_literals
+
+from coordinate import Coordinate
+from wbtime import WbTime
+from wbquantity import WbQuantity
+from itempage import ItemPage
+
+
+class Property():
+
+ """
+ A Wikibase property.
+
+ While every Wikibase property has a Page on the data repository,
+ this object is for when the property is used as part of another concept
+ where the property is not _the_ Page of the property.
+
+ For example, a claim on an ItemPage has many property attributes, and so
+ it subclasses this Property class, but a claim does not have Page like
+ behaviour and semantics.
+ """
+
+ types = {'wikibase-item': ItemPage,
+ 'string': basestring,
+ 'commonsMedia': basestring,
+ 'globe-coordinate': Coordinate,
+ 'url': basestring,
+ 'time': WbTime,
+ 'quantity': WbQuantity,
+ }
+
+ value_types = {'wikibase-item': 'wikibase-entityid',
+ 'commonsMedia': 'string',
+ 'url': 'string',
+ 'globe-coordinate': 'globecoordinate',
+ }
+
+ def __init__(self, site=None, id=None, datatype=None):
+ """
+ Constructor.
+
+ @param site: data repository or None
+ @type site: pywikibot.site.DataSite
+ @param datatype: datatype of the property;
+ if not given, it will be queried via the API
+ @type datatype: basestring
+ """
+ self.repo = site
+ self.id = id.upper()
+ if datatype:
+ self._type = datatype
+
+ @property
+ def type(self):
+ """
+ Return the type of this property.
+
+ @return: str
+ """
+ if not hasattr(self, '_type'):
+ raise ValueError('Please provide type')
+ return self._type
+
+ def getID(self, numeric=False):
+ """
+ Get the identifier of this property.
+
+ @param numeric: Strip the first letter and return an int
+ @type numeric: bool
+ """
+ if numeric:
+ return int(self.id[1:])
+ else:
+ return self.id
diff --git a/pywikibase/wbquantity.py b/pywikibase/wbquantity.py
new file mode 100644
index 0000000..2257cfa
--- /dev/null
+++ b/pywikibase/wbquantity.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+"""
+Wikibase Quantity.
+"""
+
+#
+# (C) Pywikibot team, 2008-2015
+#
+# Distributed under the terms of the MIT license.
+#
+from __future__ import unicode_literals
+
+import json
+
+
+class WbQuantity(object):
+
+ """A Wikibase quantity representation."""
+
+ def __init__(self, amount, unit=None, error=None):
+ u"""
+ Create a new WbQuantity object.
+
+ @param amount: number representing this quantity
+ @type amount: float
+ @param unit: not used (only unit-less quantities are supported)
+ @param error: the uncertainty of the amount (e.g. ±1)
+ @type error: float, or tuple of two floats, where the first value is
+ the upper error and the second is the lower error value.
+ """
+ if amount is None:
+ raise ValueError('no amount given')
+ if unit is not None and unit != '1':
+ raise NotImplementedError(
+ 'Currently only unit-less quantities are supported')
+ if unit is None:
+ unit = '1'
+ self.amount = amount
+ self.unit = unit
+ upperError = lowerError = 0
+ if isinstance(error, tuple):
+ upperError, lowerError = error
+ elif error is not None:
+ upperError = lowerError = error
+ self.upperBound = self.amount + upperError
+ self.lowerBound = self.amount - lowerError
+
+ def toWikibase(self):
+ """Convert the data to a JSON object for the Wikibase API."""
+ json = {'amount': self.amount,
+ 'upperBound': self.upperBound,
+ 'lowerBound': self.lowerBound,
+ 'unit': self.unit
+ }
+ return json
+
+ @classmethod
+ def fromWikibase(cls, wb):
+ """
+ Create a WbQuanity from the JSON data given by the Wikibase API.
+
+ @param wb: Wikibase JSON
+ """
+ amount = eval(wb['amount'])
+ upperBound = eval(wb['upperBound'])
+ lowerBound = eval(wb['lowerBound'])
+ error = (upperBound - amount, amount - lowerBound)
+ return cls(amount, wb['unit'], error)
+
+ def __str__(self):
+ return json.dumps(self.toWikibase(), indent=4, sort_keys=True,
+ separators=(',', ': '))
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ def __repr__(self):
+ return (u"WbQuantity(amount=%(amount)s, upperBound=%(upperBound)s, "
+ u"lowerBound=%(lowerBound)s, unit=%(unit)s)" % self.__dict__)
diff --git a/pywikibase/wbtime.py b/pywikibase/wbtime.py
new file mode 100644
index 0000000..8e9463a
--- /dev/null
+++ b/pywikibase/wbtime.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+"""
+Exceptions
+"""
+
+#
+# (C) Pywikibot team, 2008-2015
+#
+# Distributed under the terms of the MIT license.
+#
+from __future__ import unicode_literals
+
+import re
+import json
+
+
+class WbTime(object):
+
+ """A Wikibase time representation."""
+
+ PRECISION = {'1000000000': 0,
+ '100000000': 1,
+ '10000000': 2,
+ '1000000': 3,
+ '100000': 4,
+ '10000': 5,
+ 'millenia': 6,
+ 'century': 7,
+ 'decade': 8,
+ 'year': 9,
+ 'month': 10,
+ 'day': 11,
+ 'hour': 12,
+ 'minute': 13,
+ 'second': 14
+ }
+
+ FORMATSTR = '{0:+012d}-{1:02d}-{2:02d}T{3:02d}:{4:02d}:{5:02d}Z'
+
+ def __init__(self, year=None, month=None, day=None,
+ hour=None, minute=None, second=None,
+ precision=None, before=0, after=0,
+ timezone=0, calendarmodel=None, site=None):
+ """
+ Create a new WbTime object.
+
+ The precision can be set by the Wikibase int value (0-14) or by a human
+ readable string, e.g., 'hour'. If no precision is given, it is set
+ according to the given time units.
+ """
+ if year is None:
+ raise ValueError('no year given')
+ self.precision = self.PRECISION['second']
+ if second is None:
+ self.precision = self.PRECISION['minute']
+ second = 0
+ if minute is None:
+ self.precision = self.PRECISION['hour']
+ minute = 0
+ if hour is None:
+ self.precision = self.PRECISION['day']
+ hour = 0
+ if day is None:
+ self.precision = self.PRECISION['month']
+ day = 1
+ if month is None:
+ self.precision = self.PRECISION['year']
+ month = 1
+ self.year = long(year)
+ self.month = month
+ self.day = day
+ self.hour = hour
+ self.minute = minute
+ self.second = second
+ self.after = after
+ self.before = before
+ self.timezone = timezone
+ if calendarmodel is None:
+ calendarmodel = site.calendarmodel()
+ self.calendarmodel = calendarmodel
+
+ # if precision is given it overwrites the autodetection above
+ if precision is not None:
+ if (isinstance(precision, int) and
+ precision in self.PRECISION.values()):
+ self.precision = precision
+ elif precision in self.PRECISION:
+ self.precision = self.PRECISION[precision]
+ else:
+ raise ValueError('Invalid precision: "%s"' % precision)
+
+ @classmethod
+ def fromTimestr(cls, datetimestr, precision=14, before=0, after=0,
+ timezone=0, calendarmodel=None, site=None):
+ match = re.match(r'([-+]?\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)Z',
+ datetimestr)
+ if not match:
+ raise ValueError(u"Invalid format: '%s'" % datetimestr)
+ t = match.groups()
+ return cls(long(t[0]), int(t[1]), int(t[2]),
+ int(t[3]), int(t[4]), int(t[5]),
+ precision, before, after, timezone, calendarmodel, site)
+
+ def toTimestr(self):
+ """
+ Convert the data to a UTC date/time string.
+
+ @return: str
+ """
+ return self.FORMATSTR.format(self.year, self.month, self.day,
+ self.hour, self.minute, self.second)
+
+ def toWikibase(self):
+ """
+ Convert the data to a JSON object for the Wikibase API.
+
+ @return: dict
+ """
+ json = {'time': self.toTimestr(),
+ 'precision': self.precision,
+ 'after': self.after,
+ 'before': self.before,
+ 'timezone': self.timezone,
+ 'calendarmodel': self.calendarmodel
+ }
+ return json
+
+ @classmethod
+ def fromWikibase(cls, ts):
+ return cls.fromTimestr(ts[u'time'], ts[u'precision'],
+ ts[u'before'], ts[u'after'],
+ ts[u'timezone'], ts[u'calendarmodel'])
+
+ def __str__(self):
+ return json.dumps(self.toWikibase(), indent=4, sort_keys=True,
+ separators=(',', ': '))
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ def __repr__(self):
+ return u"WbTime(year=%(year)d, month=%(month)d, day=%(day)d, " \
+ u"hour=%(hour)d, minute=%(minute)d, second=%(second)d, " \
+ u"precision=%(precision)d, before=%(before)d, after=%(after)d, " \
+ u"timezone=%(timezone)d, calendarmodel='%(calendarmodel)s')" \
+ % self.__dict__
diff --git a/pywikibase/wikibasepage.py b/pywikibase/wikibasepage.py
new file mode 100644
index 0000000..0f8a7fc
--- /dev/null
+++ b/pywikibase/wikibasepage.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+"""
+Handling a Wikibase entity.
+"""
+
+#
+# (C) Pywikibot team, 2008-2015
+#
+# Distributed under the terms of the MIT license.
+#
+from __future__ import unicode_literals
+from collections import defaultdict, Counter
+
+import json
+
+from claim import Claim
+
+
+class WikibasePage(object):
+
+ """
+ The base page for the Wikibase extension.
+
+ There should be no need to instantiate this directly.
+ """
+
+ def get(self, content=None):
+ """
+ Fetch all page data, and cache it.
+
+ @param force: override caching
+ @type force: bool
+ @param content: content
+ @param args: may be used to specify custom props.
+ """
+ if content:
+ if isinstance(content, dict):
+ self._content = content
+ else:
+ self._content = json.loads(content)
+ if not hasattr(self, '_content'):
+ raise ValueError('You must provide some content.')
+ # aliases
+ self.aliases = {}
+ if 'aliases' in self._content:
+ for lang in self._content['aliases']:
+ self.aliases[lang] = list()
+ for value in self._content['aliases'][lang]:
+ self.aliases[lang].append(value['value'])
+
+ # labels
+ self.labels = {}
+ if 'labels' in self._content:
+ for lang in self._content['labels']:
+ if 'removed' not in self._content['labels'][lang]: # Bug 54767
+ self.labels[lang] = self._content['labels'][lang]['value']
+
+ # descriptions
+ self.descriptions = {}
+ if 'descriptions' in self._content:
+ for lang in self._content['descriptions']:
+ self.descriptions[lang] = self._content[
+ 'descriptions'][lang]['value']
+
+ # claims
+ self.claims = {}
+ if 'claims' in self._content:
+ for pid in self._content['claims']:
+ self.claims[pid] = []
+ for claim in self._content['claims'][pid]:
+ c = Claim.fromJSON(self.repo, claim)
+ c.on_item = self
+ self.claims[pid].append(c)
+
+ return {'aliases': self.aliases,
+ 'labels': self.labels,
+ 'descriptions': self.descriptions,
+ 'claims': self.claims,
+ }
+
+ def _diff_to(self, type_key, key_name, value_name, diffto, data):
+ assert type_key not in data, 'Key type must be defined in data'
+ source = self._normalizeLanguages(getattr(self, type_key)).copy()
+ diffto = {} if not diffto else diffto.get(type_key, {})
+ new = set(source.keys())
+ for key in diffto:
+ if key in new:
+ if source[key] == diffto[key][value_name]:
+ del source[key]
+ else:
+ source[key] = ''
+ for key, value in source.items():
+ source[key] = {key_name: key, value_name: value}
+ if source:
+ data[type_key] = source
+
+ def toJSON(self, diffto=None):
+ """
+ Create JSON suitable for Wikibase API.
+
+ When diffto is provided, JSON representing differences
+ to the provided data is created.
+
+ @param diffto: JSON containing claim data
+ @type diffto: dict
+
+ @return: dict
+ """
+ data = {}
+ self._diff_to('labels', 'language', 'value', diffto, data)
+
+ self._diff_to('descriptions', 'language', 'value', diffto, data)
+
+ aliases = self._normalizeLanguages(self.aliases).copy()
+ if diffto and 'aliases' in diffto:
+ for lang in set(diffto['aliases'].keys()) - set(aliases.keys()):
+ aliases[lang] = []
+ for lang, strings in list(aliases.items()):
+ if diffto and 'aliases' in diffto and lang in diffto['aliases']:
+ empty = len(diffto['aliases'][lang]) - len(strings)
+ if empty > 0:
+ strings += [''] * empty
+ elif Counter(val['value'] for val
+ in diffto['aliases'][lang]) == Counter(strings):
+ del aliases[lang]
+ if lang in aliases:
+ aliases[lang] = [{'language': lang, 'value': i}
+ for i in strings]
+
+ if aliases:
+ data['aliases'] = aliases
+
+ claims = {}
+ for prop in self.claims:
+ if len(self.claims[prop]) > 0:
+ claims[prop] = [claim.toJSON() for claim in self.claims[prop]]
+
+ if diffto and 'claims' in diffto:
+ temp = defaultdict(list)
+ claim_ids = set()
+
+ diffto_claims = diffto['claims']
+
+ for prop in claims:
+ for claim in claims[prop]:
+ if (prop not in diffto_claims or
+ claim not in diffto_claims[prop]):
+ temp[prop].append(claim)
+
+ claim_ids.add(claim['id'])
+
+ for prop, prop_claims in diffto_claims.items():
+ for claim in prop_claims:
+ if 'id' in claim and claim['id'] not in claim_ids:
+ temp[prop].append({'id': claim['id'], 'remove': ''})
+
+ claims = temp
+
+ if claims:
+ data['claims'] = claims
+ return data
+
+ def getID(self, numeric=False, force=False):
+ """
+ Get the entity identifier.
+
+ @param numeric: Strip the first letter and return an int
+ @type numeric: bool
+ @param force: Force an update of new data
+ @type force: bool
+ """
+ if not hasattr(self, 'id') or force:
+ self.get(force=force)
+ if numeric:
+ return int(self.id[1:]) if self.id != '-1' else -1
+
+ return self.id
+
+ @classmethod
+ def _normalizeData(cls, data):
+ """
+ Helper function to expand data into the Wikibase API structure.
+
+ @param data: The dict to normalize
+ @type data: dict
+
+ @return: the altered dict from parameter data.
+ @rtype: dict
+ """
+ for prop in ('labels', 'descriptions'):
+ if prop not in data:
+ continue
+ for key, value in data[prop].items():
+ if isinstance(value, basestring):
+ data[prop][key] = {'language': key, 'value': value}
+
+ if 'aliases' in data:
+ for key, values in data['aliases'].items():
+ if (isinstance(values, list) and
+ isinstance(values[0], basestring)):
+ data['aliases'][key] = [{'language': key, 'value': value}
+ for value in values]
+
+ return data
--
To view, visit https://gerrit.wikimedia.org/r/227483
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I3aa14b0fa0083c855bb1d7773bb6898db8d13b3f
Gerrit-PatchSet: 1
Gerrit-Project: pywikibot/wikibase
Gerrit-Branch: master
Gerrit-Owner: Ladsgroup <[email protected]>
Gerrit-Reviewer: Ladsgroup <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits