# -*- coding: utf-8 -*-
#
# Copyright 2018-2020- Swiss Data Science Center (SDSC)
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
# Eidgenössische Technische Hochschule Zürich (ETHZ).
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Represent provenance agents."""
import configparser
import os
import pathlib
import re
import urllib
import uuid
from urllib.parse import quote
import attr
from attr.validators import instance_of
from calamus.schema import JsonLDSchema
from marshmallow import EXCLUDE
from renku.core import errors
from renku.core.models.calamus import Nested, fields, prov, rdfs, schema, \
wfprov
from renku.version import __version__, version_url
[docs]@attr.s(
slots=True,
)
class Person:
"""Represent a person."""
client = attr.ib(default=None, kw_only=True)
name = attr.ib(kw_only=True, validator=instance_of(str))
email = attr.ib(default=None, kw_only=True)
label = attr.ib(kw_only=True)
affiliation = attr.ib(
default=None,
kw_only=True,
)
alternate_name = attr.ib(
default=None,
kw_only=True,
)
_id = attr.ib(default=None, kw_only=True)
[docs] def default_id(self):
"""Set the default id."""
if self.email:
return 'mailto:{email}'.format(email=self.email)
host = 'localhost'
if self.client:
host = self.client.remote.get('host') or host
host = os.environ.get('RENKU_DOMAIN') or host
id_ = str(uuid.uuid4())
return urllib.parse.urljoin(
'https://{host}'.format(host=host),
pathlib.posixpath.join('/persons', quote(id_, safe=''))
)
[docs] @email.validator
def check_email(self, attribute, value):
"""Check that the email is valid."""
if self.email and not (
isinstance(value, str) and re.match(r'[^@]+@[^@]+\.[^@]+', value)
):
raise ValueError('Email address is invalid.')
[docs] @label.default
def default_label(self):
"""Set the default label."""
return self.name
[docs] @classmethod
def from_commit(cls, commit):
"""Create an instance from a Git commit."""
return cls(
name=commit.author.name,
email=commit.author.email,
)
@property
def short_name(self):
"""Gives full name in short form."""
names = self.name.split()
if len(names) == 1:
return self.name
last_name = names[-1]
initials = [name[0] for name in names]
initials.pop()
return '{0}.{1}'.format('.'.join(initials), last_name)
@property
def full_identity(self):
"""Return name, email, and affiliation."""
email = f' <{self.email}>' if self.email else ''
affiliation = f' [{self.affiliation}]' if self.affiliation else ''
return f'{self.name}{email}{affiliation}'
[docs] @classmethod
def from_git(cls, git):
"""Create an instance from a Git repo."""
git_config = git.config_reader()
try:
name = git_config.get_value('user', 'name', None)
email = git_config.get_value('user', 'email', None)
except (
configparser.NoOptionError, configparser.NoSectionError
): # pragma: no cover
raise errors.ConfigurationError(
'The user name and email are not configured. '
'Please use the "git config" command to configure them.\n\n'
'\tgit config --global --add user.name "John Doe"\n'
'\tgit config --global --add user.email '
'"john.doe@example.com"\n'
)
# Check the git configuration.
if not name: # pragma: no cover
raise errors.MissingUsername()
if not email: # pragma: no cover
raise errors.MissingEmail()
return cls(name=name, email=email)
[docs] @classmethod
def from_string(cls, string):
"""Create an instance from a 'Name <email>' string."""
regex_pattern = r'([^<>\[\]]*)' \
r'(?:<{1}\s*(\S+@\S+\.\S+){0,1}\s*>{1}){0,1}\s*' \
r'(?:\[{1}(.*)\]{1}){0,1}'
name, email, affiliation = re.search(regex_pattern, string).groups()
if name:
name = name.strip()
if affiliation:
affiliation = affiliation.strip()
affiliation = affiliation or None
return cls(name=name, email=email, affiliation=affiliation)
[docs] @classmethod
def from_dict(cls, obj):
"""Create and instance from a dictionary."""
return cls(**obj)
[docs] @classmethod
def from_jsonld(cls, data):
"""Create an instance from JSON-LD data."""
if isinstance(data, cls):
return data
if not isinstance(data, dict):
raise ValueError(data)
return PersonSchema().load(data)
def __attrs_post_init__(self):
"""Finish object initialization."""
# handle the case where ids were improperly set
if self._id == 'mailto:None' or self._id is None:
self._id = self.default_id()
if self.label is None:
self.label = self.default_label()
class PersonSchema(JsonLDSchema):
"""Person schema."""
class Meta:
"""Meta class."""
rdf_type = [prov.Person, schema.Person]
model = Person
unknown = EXCLUDE
name = fields.String(schema.name)
email = fields.String(schema.email, missing=None)
label = fields.String(rdfs.label)
affiliation = fields.String(schema.affiliation, missing=None)
alternate_name = fields.String(schema.alternateName, missing=None)
_id = fields.Id(init_name='id')
[docs]@attr.s(
frozen=True,
slots=True,
)
class SoftwareAgent:
"""Represent executed software."""
label = attr.ib(kw_only=True)
was_started_by = attr.ib(
default=None,
kw_only=True,
)
_id = attr.ib(kw_only=True)
[docs] @classmethod
def from_commit(cls, commit):
"""Create an instance from a Git commit."""
author = Person.from_commit(commit)
if commit.author != commit.committer:
return cls(
label=commit.committer.name,
id=commit.committer.email,
was_started_by=author,
)
return author
[docs] @classmethod
def from_jsonld(cls, data):
"""Create an instance from JSON-LD data."""
if isinstance(data, cls):
return data
if not isinstance(data, dict):
raise ValueError(data)
return SoftwareAgentSchema().load(data)
[docs] def as_jsonld(self):
"""Create JSON-LD."""
return SoftwareAgentSchema().dump(self)
# set up the default agent
renku_agent = SoftwareAgent(
label='renku {0}'.format(__version__), id=version_url
)
class SoftwareAgentSchema(JsonLDSchema):
"""SoftwareAgent schema."""
class Meta:
"""Meta class."""
rdf_type = [prov.SoftwareAgent, wfprov.WorkflowEngine]
model = SoftwareAgent
unknown = EXCLUDE
label = fields.String(rdfs.label)
_id = fields.Id(init_name='id')
was_started_by = Nested(prov.wasStartedBy, PersonSchema, missing=None)