Ladsgroup has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/227483

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(-)


  git pull ssh://gerrit.wikimedia.org:29418/pywikibot/wikibase 
refs/changes/83/227483/1

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: newchange
Gerrit-Change-Id: I3aa14b0fa0083c855bb1d7773bb6898db8d13b3f
Gerrit-PatchSet: 1
Gerrit-Project: pywikibot/wikibase
Gerrit-Branch: master
Gerrit-Owner: Ladsgroup <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to