jenkins-bot has submitted this change. (
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/461372 )
Change subject: [FEAT] Add support for Lexicographical data
......................................................................
[FEAT] Add support for Lexicographical data
Bug: T189321
Change-Id: I808dab5c3c4ba8169423e9796bac24f39cabac52
---
M pywikibot/__init__.py
M pywikibot/data/api.py
M pywikibot/page/__init__.py
M pywikibot/page/_collections.py
M pywikibot/page/_wikibase.py
M pywikibot/site/_datasite.py
6 files changed, 603 insertions(+), 4 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/__init__.py b/pywikibot/__init__.py
index 4456faa..348cfa7 100644
--- a/pywikibot/__init__.py
+++ b/pywikibot/__init__.py
@@ -79,7 +79,8 @@
'__version__',
'Bot', 'calledModuleName', 'Category', 'Claim', 'Coordinate', 'critical',
'CurrentPageBot', 'debug', 'error', 'exception', 'FilePage', 'handle_args',
- 'html2unicode', 'input', 'input_choice', 'input_yn', 'ItemPage', 'Link',
+ 'html2unicode', 'input', 'input_choice', 'input_yn', 'ItemPage',
+ 'LexemeForm', 'LexemePage', 'LexemeSense', 'Link',
'log', 'MediaInfo', 'output', 'Page', 'PropertyPage', 'showDiff',
'show_help', 'Site', 'SiteLink', 'stdout', 'Timestamp', 'translate', 'ui',
'url2unicode', 'User', 'warning', 'WbGeoShape', 'WbMonolingualText',
@@ -1222,6 +1223,9 @@
Claim,
FilePage,
ItemPage,
+ LexemeForm,
+ LexemePage,
+ LexemeSense,
Link,
MediaInfo,
Page,
diff --git a/pywikibot/data/api.py b/pywikibot/data/api.py
index c04e093..4a89f13 100644
--- a/pywikibot/data/api.py
+++ b/pywikibot/data/api.py
@@ -1031,6 +1031,8 @@
'wbremovequalifiers', 'wbremovereferences', 'wbsetaliases',
'wbsetclaim', 'wbsetclaimvalue', 'wbsetdescription', 'wbsetlabel',
'wbsetqualifier', 'wbsetreference', 'wbsetsitelink',
+ 'wbladdform', 'wbleditformelements', 'wblmergelexemes',
+ 'wblremoveform',
}
# Client side verification that the request is being performed
# by a logged in user, and warn if it isn't a config username.
diff --git a/pywikibot/page/__init__.py b/pywikibot/page/__init__.py
index 2caa740..7470180 100644
--- a/pywikibot/page/__init__.py
+++ b/pywikibot/page/__init__.py
@@ -24,6 +24,9 @@
PropertyPage,
WikibaseEntity,
WikibasePage,
+ LexemePage,
+ LexemeForm,
+ LexemeSense,
)
from pywikibot.site import BaseSite as _BaseSite
from pywikibot.tools import deprecated, issue_deprecation_warning
@@ -41,6 +44,9 @@
'User',
'WikibasePage',
'ItemPage',
+ 'LexemePage',
+ 'LexemeForm',
+ 'LexemeSense',
'PropertyPage',
'Property',
'Claim',
diff --git a/pywikibot/page/_collections.py b/pywikibot/page/_collections.py
index 078a5cf..84ecfad 100644
--- a/pywikibot/page/_collections.py
+++ b/pywikibot/page/_collections.py
@@ -5,7 +5,7 @@
# Distributed under the terms of the MIT license.
#
from collections import defaultdict
-from collections.abc import MutableMapping
+from collections.abc import MutableMapping, MutableSequence
from typing import Optional
import pywikibot
@@ -17,6 +17,7 @@
'ClaimCollection',
'LanguageDict',
'SiteLinkCollection',
+ 'SubEntityCollection',
)
@@ -476,3 +477,101 @@
for dbname in to_nuke:
del data[dbname]
return data
+
+
+class SubEntityCollection(MutableSequence):
+
+ """Ordered collection of sub-entities indexed by their ids."""
+
+ def __init__(self, repo, data=None):
+ """
+ Initializer.
+
+ :param repo: Wikibase site
+ :type repo: pywikibot.site.DataSite
+ :param data: iterable of LexemeSubEntity
+ :type data: iterable
+ """
+ super().__init__()
+ self.repo = repo
+ self._data = []
+ self._by_key = {}
+ if data:
+ self.extend(data)
+
+ def _validate_isinstance(self, obj):
+ if not isinstance(obj, self.type_class):
+ raise TypeError(
+ '{} should only hold instances of {}, '
+ 'instance of {} was provided'
+ .format(self.__class__.__name__,
+ self.type_class.__name__,
+ obj.__class__.__name__))
+
+ def __getitem__(self, index):
+ if isinstance(index, str):
+ try:
+ index = self._by_key[index]
+ except KeyError as e:
+ raise ValueError('No entity with id {} was found'
+ .format(index)) from e
+ return self._data[index]
+
+ def __setitem__(self, index, value):
+ raise NotImplementedError
+
+ def __delitem__(self, index):
+ if isinstance(index, str):
+ try:
+ index = self._by_key[index]
+ except KeyError as e:
+ raise ValueError('No entity with id {} was found'
+ .format(index)) from e
+ obj = self._data[index]
+ del self._data[index]
+ del self._by_key[obj.id]
+
+ def __len__(self):
+ return len(self._data)
+
+ def insert(self, index, obj):
+ """Insert a sub-entity to the collection."""
+ self._validate_isinstance(obj)
+ self._data.insert(index, obj)
+ self._by_key[obj.id] = index
+
+ @classmethod
+ def new_empty(cls, repo):
+ """Construct a new empty SubEntityCollection."""
+ return cls(repo)
+
+ @classmethod
+ def fromJSON(cls, data, repo):
+ """Construct a new SubEntityCollection from JSON."""
+ this = cls(repo)
+ for entity in data:
+ this.append(cls.type_class.fromJSON(repo, entity))
+ return this
+
+ @classmethod
+ def normalizeData(cls, data: list) -> dict:
+ """
+ Helper function to expand data into the Wikibase API structure.
+
+ :param data: Data to normalize
+ :type data: list
+
+ :return: the altered dict from parameter data.
+ """
+ raise NotImplementedError # todo
+
+ def toJSON(self, diffto: Optional[dict] = None) -> dict:
+ """
+ Create JSON suitable for Wikibase API.
+
+ When diffto is provided, JSON representing differences
+ to the provided data is created.
+
+ :param diffto: JSON containing entity data
+ """
+ raise NotImplementedError # todo
diff --git a/pywikibot/page/_wikibase.py b/pywikibot/page/_wikibase.py
index f90577e..b584121 100644
--- a/pywikibot/page/_wikibase.py
+++ b/pywikibot/page/_wikibase.py
@@ -40,6 +40,7 @@
ClaimCollection,
LanguageDict,
SiteLinkCollection,
+ SubEntityCollection,
)
from pywikibot.page._decorators import allow_asynchronous
from pywikibot.page._pages import BasePage, FilePage
@@ -1854,3 +1855,373 @@
'value': self._formatValue(),
'type': self.value_types.get(self.type, self.type)
}
+
+
+class LexemePage(WikibasePage):
+
+ """Wikibase entity of type 'lexeme'."""
+
+ _cache_attrs = WikibasePage._cache_attrs + (
+ 'lemmas', 'language', 'lexicalCategory', 'forms', 'senses',
+ )
+ entity_type = 'lexeme'
+ title_pattern = r'L[1-9]\d*'
+ DATA_ATTRIBUTES = {
+ 'lemmas': LanguageDict,
+ 'claims': ClaimCollection,
+ # added when defined
+ # 'forms': LexemeFormCollection,
+ # 'senses': LexemeSenseCollection,
+ }
+
+ def __init__(self, site, title=None) -> None:
+ """
+ Initializer.
+
+ :param site: data repository
+ :type site: pywikibot.site.DataSite
+ :param title: identifier of lexeme, "L###",
+ -1 or None for an empty lexeme.
+ :type title: str or None
+ """
+ # Special case for empty lexeme.
+ if title is None or title == '-1':
+ super().__init__(site, '-1', entity_type='lexeme')
+ assert self.id == '-1'
+ return
+
+ # we don't want empty titles
+ if not title:
+ raise InvalidTitleError("Lexeme's title cannot be empty")
+
+ super().__init__(site, title, entity_type='lexeme')
+ assert self.id == self._link.title
+
+ def get_data_for_new_entity(self):
+ """Return data required for creation of a new lexeme."""
+ raise NotImplementedError # todo
+
+ def toJSON(self, diffto: Optional[dict] = None) -> dict:
+ """
+ Create JSON suitable for Wikibase API.
+
+ When diffto is provided, JSON representing differences
+ to the provided data is created.
+
+ :param diffto: JSON containing entity data
+ """
+ data = super().toJSON(diffto=diffto)
+
+ for prop in ('language', 'lexicalCategory'):
+ value = getattr(self, prop, None)
+ if not value:
+ continue
+ if not diffto or diffto.get(prop) != value.getID():
+ data[prop] = value.getID()
+
+ return data
+
+ def get(self, force=False, get_redirect=False, *args, **kwargs):
+ """
+ Fetch all lexeme data, and cache it.
+
+ :param force: override caching
+ :type force: bool
+ :param get_redirect: return the lexeme content, do not follow the
+ redirect, do not raise an exception.
+ :type get_redirect: bool
+ :raise NotImplementedError: a value in args or kwargs
+ :note: dicts returned by this method are references to content
+ of this entity and their modifying may indirectly cause
+ unwanted change to the live content
+ """
+ data = super().get(force, *args, **kwargs)
+
+ if self.isRedirectPage() and not get_redirect:
+ raise IsRedirectPageError(self)
+
+ # language
+ self.language = None
+ if 'language' in self._content:
+ self.language = ItemPage(self.site, self._content['language'])
+
+ # lexicalCategory
+ self.lexicalCategory = None
+ if 'lexicalCategory' in self._content:
+ self.lexicalCategory = ItemPage(
+ self.site, self._content['lexicalCategory'])
+
+ data['language'] = self.language
+ data['lexicalCategory'] = self.lexicalCategory
+
+ return data
+
+ @classmethod
+ def _normalizeData(cls, data: dict) -> dict:
+ """
+ Helper function to expand data into the Wikibase API structure.
+
+ :param data: The dict to normalize
+ :return: the altered dict from parameter data.
+ """
+ new_data = WikibasePage._normalizeData(data)
+ for prop in ('language', 'lexicalCategory'):
+ value = new_data.get(prop)
+ if value:
+ if isinstance(value, ItemPage):
+ new_data[prop] = value.getID()
+ else:
+ new_data[prop] = value
+ return new_data
+
+ @allow_asynchronous
+ def add_form(self, form, **kwargs):
+ """
+ Add a form to the lexeme.
+
+ :param form: The form to add
+ :type form: Form
+ :keyword bot: Whether to flag as bot (if possible)
+ :type bot: bool
+ :keyword asynchronous: if True, launch a separate thread to add form
+ asynchronously
+ :type asynchronous: bool
+ :keyword callback: a callable object that will be called after the
+ claim has been added. It must take two arguments:
+ (1) a LexemePage object, and
+ (2) an exception instance, which will be None if the entity was
+ saved successfully. This is intended for use by bots that need to
+ keep track of which saves were successful.
+ :type callback: callable
+ """
+ if form.on_lexeme is not None:
+ raise ValueError('The provided LexemeForm instance is already '
+ 'used in an entity')
+ data = self.repo.add_form(self, form, **kwargs)
+ form.id = data['form']['id']
+ form.on_lexeme = self
+ form._content = data['form']
+ form.get()
+ self.forms.append(form)
+ self.latest_revision_id = data['lastrevid']
+
+ def remove_form(self, form, **kwargs) -> None:
+ """
+ Remove a form from the lexeme.
+
+ :param form: The form to remove
+ :type form: pywikibot.LexemeForm
+ """
+ data = self.repo.remove_form(form, **kwargs)
+ form.on_lexeme.latest_revision_id = data['lastrevid']
+ form.on_lexeme.forms.remove(form)
+ form.on_lexeme = None
+ form.id = '-1'
+
+ # todo: senses
+
+ def mergeInto(self, lexeme, **kwargs):
+ """
+ Merge the lexeme into another lexeme.
+
+ :param lexeme: The lexeme to merge into
+ :type lexeme: LexemePage
+ """
+ data = self.repo.mergeLexemes(from_lexeme=self, to_lexeme=lexeme,
+ **kwargs)
+ if not data.get('success', 0):
+ return
+ self.latest_revision_id = data['from']['lastrevid']
+ lexeme.latest_revision_id = data['to']['lastrevid']
+ if data.get('redirected', 0):
+ self._isredir = True
+ self._redirtarget = lexeme
+
+ def isRedirectPage(self):
+ """Return True if lexeme is redirect, False if not or not existing."""
+ if hasattr(self, '_content') and not hasattr(self, '_isredir'):
+ self._isredir = self.id != self._content.get('id', self.id)
+ return self._isredir
+ return super().isRedirectPage()
+
+
+class LexemeSubEntity(WikibaseEntity):
+
+ """Common super class for LexemeForm and LexemeSense."""
+
+ def __init__(self, repo, id_=None) -> None:
+ """Initializer."""
+ super().__init__(repo, id_)
+ self._on_lexeme = None
+
+ @classmethod
+ def fromJSON(cls, repo, data):
+ new = cls(repo, data['id'])
+ new._content = data
+ return new
+
+ def toJSON(self, diffto=None) -> dict:
+ data = super().toJSON(diffto)
+ if self.id != '-1':
+ data['id'] = self.id
+ return data
+
+ @property
+ def on_lexeme(self) -> LexemePage:
+ if self._on_lexeme is None:
+ lexeme_id = self.id.partition('-')[0]
+ self._on_lexeme = LexemePage(self.repo, lexeme_id)
+ return self._on_lexeme
+
+ @on_lexeme.setter
+ def on_lexeme(self, lexeme):
+ self._on_lexeme = lexeme
+
+ @on_lexeme.deleter
+ def on_lexeme(self):
+ self._on_lexeme = None
+
+ @allow_asynchronous
+ def addClaim(self, claim, **kwargs):
+ """
+ Add a claim to the form.
+
+ :param claim: The claim to add
+ :type claim: Claim
+ :keyword bot: Whether to flag as bot (if possible)
+ :type bot: bool
+ :keyword asynchronous: if True, launch a separate thread to add claim
+ asynchronously
+ :type asynchronous: bool
+ :keyword callback: a callable object that will be called after the
+ claim has been added. It must take two arguments: (1) a Form
+ object, and (2) an exception instance, which will be None if the
+ form was saved successfully. This is intended for use by bots that
+ need to keep track of which saves were successful.
+ :type callback: callable
+ """
+ self.repo.addClaim(self, claim, **kwargs)
+ claim.on_item = self
+
+ def removeClaims(self, claims, **kwargs) -> None:
+ """
+ Remove the claims from the form.
+
+ :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, pywikibot.Claim):
+ claims = [claims]
+ data = self.repo.removeClaims(claims, **kwargs)
+ for claim in claims:
+ claim.on_item.latest_revision_id = data['pageinfo']['lastrevid']
+ claim.on_item = None
+ claim.snak = None
+
+
+class LexemeForm(LexemeSubEntity):
+
+ """Wikibase lexeme form."""
+
+ entity_type = 'form'
+ title_pattern = LexemePage.title_pattern + r'-F[1-9]\d*'
+ DATA_ATTRIBUTES = {
+ 'representations': LanguageDict,
+ 'claims': ClaimCollection,
+ }
+
+ def toJSON(self, diffto: Optional[dict] = None) -> dict:
+ """Create dict suitable for the MediaWiki API."""
+ data = super().toJSON(diffto=diffto)
+
+ key = 'grammaticalFeatures'
+ if getattr(self, key, None):
+ # could also avoid if no change wrt. diffto
+ data[key] = [value.getID() for value in self.grammaticalFeatures]
+
+ return data
+
+ @classmethod
+ def _normalizeData(cls, data):
+ new_data = LexemeSubEntity._normalizeData(data)
+ if 'grammaticalFeatures' in data:
+ value = []
+ for feat in data['grammaticalFeatures']:
+ if isinstance(feat, ItemPage):
+ value.append(feat.getID())
+ else:
+ value.append(feat)
+ new_data['grammaticalFeatures'] = value
+ return new_data
+
+ def get(self, force: bool = False) -> dict:
+ """
+ Fetch all form data, and cache it.
+
+ :param force: override caching
+ :note: dicts returned by this method are references to content
+ of this entity and their modifying may indirectly cause
+ unwanted change to the live content
+ """
+ data = super().get(force=force)
+
+ # grammaticalFeatures
+ self.grammaticalFeatures = set()
+ for value in self._content.get('grammaticalFeatures', []):
+ self.grammaticalFeatures.add(ItemPage(self.repo, value))
+
+ data['grammaticalFeatures'] = self.grammaticalFeatures
+
+ return data
+
+ def edit_elements(self, data: dict, **kwargs) -> None:
+ """
+ Update form elements.
+
+ :param data: Data to be saved
+ """
+ if self.id == '-1':
+ # Update only locally
+ if 'representations' in data:
+ self.representations = LanguageDict(data['representations'])
+
+ if 'grammaticalFeatures' in data:
+ self.grammaticalFeatures = set()
+ for value in data['grammaticalFeatures']:
+ if not isinstance(value, ItemPage):
+ value = ItemPage(self.repo, value)
+ self.grammaticalFeatures.add(value)
+ else:
+ data = self._normalizeData(data)
+ updates = self.repo.edit_form_elements(self, data, **kwargs)
+ self._content = updates['form']
+
+
+class LexemeSense(LexemeSubEntity):
+
+ """Wikibase lexeme sense."""
+
+ entity_type = 'sense'
+ title_pattern = LexemePage.title_pattern + r'-S[1-9]\d*'
+ DATA_ATTRIBUTES = {
+ 'glosses': LanguageDict,
+ 'claims': ClaimCollection,
+ }
+
+
+class LexemeFormCollection(SubEntityCollection):
+
+ type_class = LexemeForm
+
+
+class LexemeSenseCollection(SubEntityCollection):
+
+ type_class = LexemeSense
+
+
+LexemePage.DATA_ATTRIBUTES.update({
+ 'forms': LexemeFormCollection,
+ 'senses': LexemeSenseCollection,
+})
diff --git a/pywikibot/site/_datasite.py b/pywikibot/site/_datasite.py
index 7662092..b729f25 100644
--- a/pywikibot/site/_datasite.py
+++ b/pywikibot/site/_datasite.py
@@ -21,7 +21,7 @@
NoWikibaseEntityError,
)
from pywikibot.site._apisite import APISite
-from pywikibot.site._decorators import need_right, need_version
+from pywikibot.site._decorators import need_extension, need_right, need_version
from pywikibot.tools import itergroup, merge_unique_dicts, remove_last_args
@@ -42,6 +42,9 @@
'item': pywikibot.ItemPage,
'property': pywikibot.PropertyPage,
'mediainfo': pywikibot.MediaInfo,
+ 'lexeme': pywikibot.LexemePage,
+ 'form': pywikibot.LexemeForm,
+ 'sense': pywikibot.LexemeSense,
}
def _cache_entity_namespaces(self) -> None:
@@ -69,7 +72,8 @@
if entity_type in self._entity_namespaces:
return self._entity_namespaces[entity_type]
raise EntityTypeUnknownError(
- '{!r} does not support entity type "{}"'
+ '{!r} does not support entity type "{}" '
+ "or it doesn't have its own namespace"
.format(self, entity_type))
@property
@@ -626,6 +630,35 @@
req = self.simple_request(**params)
return req.submit()
+ @need_right('item-merge')
+ @need_extension('WikibaseLexeme')
+ def mergeLexemes(self, from_lexeme, to_lexeme, summary=None, *,
+ bot: bool = True) -> dict:
+ """
+ Merge two lexemes together.
+
+ :param from_lexeme: Lexeme to merge from
+ :type from_lexeme: pywikibot.LexemePage
+ :param to_lexeme: Lexeme to merge into
+ :type to_lexeme: pywikibot.LexemePage
+ :param summary: Edit summary
+ :type summary: str
+ :keyword bot: Whether to mark the edit as a bot edit
+ :return: dict API output
+ """
+ params = {
+ 'action': 'wblmergelexemes',
+ 'source': from_lexeme.getID(),
+ 'target': to_lexeme.getID(),
+ 'token': self.tokens['edit'],
+ 'summary': summary,
+ }
+ if bot:
+ params['bot'] = 1
+ req = self._simple_request(**params)
+ data = req.submit()
+ return data
+
@need_right('item-redirect')
def set_redirect_target(self, from_item, to_item, bot: bool = True):
"""
@@ -823,3 +856,87 @@
See self._wbset_action() for parameters
"""
return self._wbset_action(itemdef, 'wbsetsitelink', sitelink, **kwargs)
+
+ @need_right('edit')
+ @need_extension('WikibaseLexeme')
+ def add_form(self, lexeme, form, *, bot: bool = True,
+ baserevid=None) -> dict:
+ """
+ Add a form.
+
+ :param lexeme: Lexeme to modify
+ :type lexeme: pywikibot.LexemePage
+ :param form: Form to be added
+ :type form: pywikibot.LexemeForm
+ :keyword bot: Whether to mark the edit as a bot edit
+ :keyword baserevid: Base revision id override, used to detect
+ conflicts.
+ :type baserevid: long
+ """
+ params = {
+ 'action': 'wbladdform',
+ 'lexemeId': lexeme.getID(),
+ 'data': json.dumps(form.toJSON()),
+ 'bot': bot,
+ 'token': self.tokens['edit'],
+ }
+ if baserevid:
+ params['baserevid'] = baserevid
+ req = self._simple_request(**params)
+ data = req.submit()
+ return data
+
+ @need_right('edit')
+ @need_extension('WikibaseLexeme')
+ def remove_form(self, form, *, bot: bool = True, baserevid=None) -> dict:
+ """
+ Remove a form.
+
+ :param form: Form to be removed
+ :type form: pywikibot.LexemeForm
+ :keyword bot: Whether to mark the edit as a bot edit
+ :keyword baserevid: Base revision id override, used to detect
+ conflicts.
+ :type baserevid: long
+ """
+ params = {
+ 'action': 'wblremoveform',
+ 'id': form.getID(),
+ 'bot': bot,
+ 'token': self.tokens['edit'],
+ }
+ if baserevid:
+ params['baserevid'] = baserevid
+ req = self._simple_request(**params)
+ data = req.submit()
+ return data
+
+ @need_right('edit')
+ @need_extension('WikibaseLexeme')
+ def edit_form_elements(self, form, data, *, bot: bool = True,
+ baserevid=None) -> dict:
+ """
+ Edit lexeme form elements.
+
+ :param form: Form
+ :type form: pywikibot.LexemeForm
+ :param data: data updates
+ :type data: dict
+ :keyword bot: Whether to mark the edit as a bot edit
+ :keyword baserevid: Base revision id override, used to detect
+ conflicts.
+ :type baserevid: long
+ :return: New form data
+ """
+ params = {
+ 'action': 'wbleditformelements',
+ 'formId': form.getID(),
+ 'data': json.dumps(data),
+ 'bot': bot,
+ 'token': self.tokens['edit'],
+ }
+ if baserevid:
+ params['baserevid'] = baserevid
+ req = self._simple_request(**params)
+ data = req.submit()
+ return data
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/461372
To unsubscribe, or for help writing mail filters, visit
https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I808dab5c3c4ba8169423e9796bac24f39cabac52
Gerrit-Change-Number: 461372
Gerrit-PatchSet: 21
Gerrit-Owner: Rua <[email protected]>
Gerrit-Assignee: Matěj Suchánek <[email protected]>
Gerrit-Reviewer: John Vandenberg <[email protected]>
Gerrit-Reviewer: Matěj Suchánek <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
Gerrit-CC: Lokal Profil <[email protected]>
Gerrit-CC: Welcome, new contributor! <[email protected]>
Gerrit-MessageType: merged
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]