diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..0f5d9ef --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = D102, D202, D205, D209, D400, D401, D107 \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9460a16 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/psf/black + rev: "25.1.0" + hooks: + - id: black + args: [ --line-length=120, --skip-string-normalization ] + - repo: https://github.com/PyCQA/flake8 + rev: "7.1.2" + hooks: + - id: flake8 + additional_dependencies: [ "flake8-docstrings" ] + args: [ --max-line-length=120 ] + - repo: https://github.com/myint/docformatter + rev: eb1df347edd128b30cd3368dddc3aa65edcfac38 # Don't autoupdate until https://github.com/PyCQA/docformatter/issues/293 is fixed + hooks: + - id: docformatter + args: [ --wrap-summaries=120, --wrap-descriptions=120 ] # add --in-place, to modify files in place \ No newline at end of file diff --git a/coarnotify/__init__.py b/coarnotify/__init__.py index 7c34b2a..6e9d5b7 100644 --- a/coarnotify/__init__.py +++ b/coarnotify/__init__.py @@ -1,8 +1,6 @@ -""" -This is the base of the `coarnotifypy` module. +"""This is the base of the `coarnotifypy` module. -In here you will find -a full set of model objects for all the Notify Patterns documented in +In here you will find a full set of model objects for all the Notify Patterns documented in https://coar-notify.net/specification/1.0.1/ You will also find a client library that will allow you to send notifications @@ -14,4 +12,4 @@ stand-alone inbox you can use for local testing. """ -__version__ = "1.0.1.3" \ No newline at end of file +__version__ = "1.0.1.3" diff --git a/coarnotify/client.py b/coarnotify/client.py index fd9a204..40092fe 100644 --- a/coarnotify/client.py +++ b/coarnotify/client.py @@ -1,7 +1,5 @@ -""" -This module contains all the client-specific code for sending notifications -to an inbox and receiving the responses it may return -""" +"""This module contains all the client-specific code for sending notifications to an inbox and receiving the responses +it may return.""" import json from typing import Union @@ -12,8 +10,7 @@ class NotifyResponse: - """ - An object representing the response from a COAR Notify inbox. + """An object representing the response from a COAR Notify inbox. This contains the action that was carried out on the server: @@ -21,15 +18,15 @@ class NotifyResponse: * ACCEPTED - the request was accepted, but the resource was not yet created - In the event that the resource is created, then there will also be a location - URL which will give you access to the resource + In the event that the resource is created, then there will also be a location URL which will give you access to the + resource """ + CREATED = "created" ACCEPTED = "accepted" def __init__(self, action, location=None): - """ - Construct a new NotifyResponse object with the action (created or accepted) and the location URL (optional) + """Construct a new NotifyResponse object with the action (created or accepted) and the location URL (optional) :param action: The action which the server said it took :param location: The HTTP URI for the resource that was created (if present) @@ -39,48 +36,50 @@ def __init__(self, action, location=None): @property def action(self) -> str: - """The action that was taken, will be one of the constants CREATED or ACCEPTED""" + """The action that was taken, will be one of the constants CREATED or ACCEPTED.""" return self._action @property def location(self) -> Union[str, None]: - """The HTTP URI of the created resource, if present""" + """The HTTP URI of the created resource, if present.""" return self._location class COARNotifyClient: - """ - The COAR Notify Client, which is the mechanism through which you will interact with external inboxes. + """The COAR Notify Client, which is the mechanism through which you will interact with external inboxes. - If you do not supply an inbox URL at construction you will - need to supply it via the ``inbox_url`` setter, or when you send a notification + If you do not supply an inbox URL at construction you will need to supply it via the ``inbox_url`` setter, or when + you send a notification - :param inbox_url: HTTP URI of the inbox to communicate with by default - :param http_layer: An implementation of the HttpLayer interface to use for sending HTTP requests. - If not provided, the default implementation will be used based on ``requests`` + :param inbox_url: HTTP URI of the inbox to communicate with by default + :param http_layer: An implementation of the HttpLayer interface to use for sending HTTP requests. If not provided, + the default implementation will be used based on ``requests`` """ + def __init__(self, inbox_url: str = None, http_layer: HttpLayer = None): self._inbox_url = inbox_url self._http = http_layer if http_layer is not None else RequestsHttpLayer() @property def inbox_url(self) -> Union[str, None]: - """The HTTP URI of the inbox to communicate with by default""" + """The HTTP URI of the inbox to communicate with by default.""" return self._inbox_url @inbox_url.setter def inbox_url(self, value: str): - """Set the HTTP URI of the inbox to communicate with by default""" + """Set the HTTP URI of the inbox to communicate with by default.""" self._inbox_url = value def send(self, notification: NotifyPattern, inbox_url: str = None, validate: bool = True) -> NotifyResponse: - """ - Send the given notification to the inbox. If no inbox URL is provided, the default inbox URL will be used. - - :param notification: The notification object (from the models provided, or a subclass you have made of the NotifyPattern class) - :param inbox_url: The HTTP URI to send the notification to. Omit if using the default inbox_url supplied in the constructor. - If it is omitted, and no value is passed here then we will also look in the ``target.inbox`` property of the notification - :param validate: Whether to validate the notification before sending. If you are sure the notification is valid, you can set this to False + """Send the given notification to the inbox. If no inbox URL is provided, the default inbox URL will be used. + + :param notification: The notification object (from the models provided, or a subclass you have made of the + NotifyPattern class) + :param inbox_url: The HTTP URI to send the notification to. Omit if using the default inbox_url supplied in the + constructor. If it is omitted, and no value is passed here then we will also look in the ``target.inbox`` + property of the notification + :param validate: Whether to validate the notification before sending. If you are sure the notification is valid, + you can set this to False :return: a NotifyResponse object representing the response from the server """ if inbox_url is None: @@ -92,12 +91,15 @@ def send(self, notification: NotifyPattern, inbox_url: str = None, validate: boo if validate: if not notification.validate(): - raise NotifyException("Attempting to send invalid notification; to override set validate=False when calling this method") - - resp = self._http.post(inbox_url, - data=json.dumps(notification.to_jsonld()), - headers={"Content-Type": "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\""} - ) + raise NotifyException( + "Attempting to send invalid notification; to override set validate=False when calling this method" + ) + + resp = self._http.post( + inbox_url, + data=json.dumps(notification.to_jsonld()), + headers={"Content-Type": "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\""}, + ) if resp.status_code == 201: return NotifyResponse(NotifyResponse.CREATED, location=resp.header("Location")) diff --git a/coarnotify/core/__init__.py b/coarnotify/core/__init__.py index 902c805..0e1f16c 100644 --- a/coarnotify/core/__init__.py +++ b/coarnotify/core/__init__.py @@ -1,3 +1 @@ -""" -This module contains the central objects that form the basis for the COAR notify patterns -""" \ No newline at end of file +"""This module contains the central objects that form the basis for the COAR notify patterns.""" diff --git a/coarnotify/core/activitystreams2.py b/coarnotify/core/activitystreams2.py index 62ca981..07095f8 100644 --- a/coarnotify/core/activitystreams2.py +++ b/coarnotify/core/activitystreams2.py @@ -1,67 +1,68 @@ -""" -This module contains everything COAR Notify needs to know about ActivityStreams 2.0 +"""This module contains everything COAR Notify needs to know about ActivityStreams 2.0 https://www.w3.org/TR/activitystreams-core/ -It provides knowledge of the essential AS properties and types, and a class to wrap -ActivityStreams objects and provide a simple interface to work with them. +It provides knowledge of the essential AS properties and types, and a class to wrap ActivityStreams objects and provide +a simple interface to work with them. -**NOTE** this is not a complete implementation of AS 2.0, it is **only** what is required -to work with COAR Notify patterns. +**NOTE** this is not a complete implementation of AS 2.0, it is **only** what is required to work with COAR Notify +patterns. """ + from typing import Union ACTIVITY_STREAMS_NAMESPACE = "https://www.w3.org/ns/activitystreams" -"""Namespace for Actvitity Streams, to be used to construct namespaced properties used in COAR Notify Patterns""" +"""Namespace for Activity Streams, to be used to construct namespaced properties used in COAR Notify Patterns.""" + class Properties: - """ - ActivityStreams 2.0 properties used in COAR Notify Patterns + """ActivityStreams 2.0 properties used in COAR Notify Patterns. These are provided as tuples, where the first element is the property name, and the second element is the namespace. - These are suitbale to be used as property names in all the property getters/setters in the notify pattern objects + These are suitable to be used as property names in all the property getters/setters in the notify pattern objects and in the validation configuration. """ + ID = ("id", ACTIVITY_STREAMS_NAMESPACE) - """``id`` property""" + """``id`` property.""" TYPE = ("type", ACTIVITY_STREAMS_NAMESPACE) - """``type`` property""" + """``type`` property.""" ORIGIN = ("origin", ACTIVITY_STREAMS_NAMESPACE) - """``origin`` property""" + """``origin`` property.""" OBJECT = ("object", ACTIVITY_STREAMS_NAMESPACE) - """``object`` property""" + """``object`` property.""" TARGET = ("target", ACTIVITY_STREAMS_NAMESPACE) - """``target`` property""" + """``target`` property.""" ACTOR = ("actor", ACTIVITY_STREAMS_NAMESPACE) - """``actor`` property""" + """``actor`` property.""" IN_REPLY_TO = ("inReplyTo", ACTIVITY_STREAMS_NAMESPACE) - """``inReplyTo`` property""" + """``inReplyTo`` property.""" CONTEXT = ("context", ACTIVITY_STREAMS_NAMESPACE) - """``context`` property""" + """``context`` property.""" SUMMARY = ("summary", ACTIVITY_STREAMS_NAMESPACE) - """``summary`` property""" + """``summary`` property.""" SUBJECT_TRIPLE = ("as:subject", ACTIVITY_STREAMS_NAMESPACE) - """``as:subject`` property""" + """``as:subject`` property.""" OBJECT_TRIPLE = ("as:object", ACTIVITY_STREAMS_NAMESPACE) - """``as:object`` property""" + """``as:object`` property.""" RELATIONSHIP_TRIPLE = ("as:relationship", ACTIVITY_STREAMS_NAMESPACE) - """``as:relationship`` property""" + """``as:relationship`` property.""" + class ActivityStreamsTypes: - """ - List of all the Activity Streams types COAR Notify may use. + """List of all the Activity Streams types COAR Notify may use. Note that COAR Notify also has its own custom types and they are defined in :py:class:`coarnotify.models.notify.NotifyTypes` @@ -104,6 +105,7 @@ class ActivityStreamsTypes: TOMBSTONE = "Tombstone" VIDEO = "Video" + ACTIVITY_STREAMS_OBJECTS = [ ActivityStreamsTypes.ACTIVITY, ActivityStreamsTypes.APPLICATION, @@ -129,22 +131,22 @@ class ActivityStreamsTypes: ActivityStreamsTypes.QUESTION, ActivityStreamsTypes.SERVICE, ActivityStreamsTypes.TOMBSTONE, - ActivityStreamsTypes.VIDEO + ActivityStreamsTypes.VIDEO, ] -"""The sub-list of ActivityStreams types that are also objects in AS 2.0""" +"""The sub-list of ActivityStreams types that are also objects in AS 2.0.""" + class ActivityStream: - """ - A simple wrapper around an ActivityStreams dictionary object + """A simple wrapper around an ActivityStreams dictionary object. - Construct it with a python dictionary that represents an ActivityStreams object, or - without to create a fresh, blank object. + Construct it with a python dictionary that represents an ActivityStreams object, or without to create a fresh, blank + object. :param raw: the raw ActivityStreams object, as a dictionary """ - def __init__(self, raw: dict=None): - """ - Construct a new ActivityStream object + + def __init__(self, raw: dict = None): + """Construct a new ActivityStream object. :param raw: the raw ActivityStreams object, as a dictionary """ @@ -158,16 +160,16 @@ def __init__(self, raw: dict=None): @property def doc(self) -> dict: - """The internal dictionary representation of the ActivityStream, without the json-ld context""" + """The internal dictionary representation of the ActivityStream, without the json-ld context.""" return self._doc @doc.setter - def doc(self, doc:dict): + def doc(self, doc: dict): self._doc = doc @property def context(self): - """The json-ld context of the ActivityStream""" + """The json-ld context of the ActivityStream.""" return self._context @context.setter @@ -175,9 +177,7 @@ def context(self, context): self._context = context def _register_namespace(self, namespace: Union[str, tuple[str, str]]): - """ - Register a namespace in the context of the ActivityStream - """ + """Register a namespace in the context of the ActivityStream.""" entry = namespace if isinstance(namespace, tuple): url = namespace[1] @@ -188,12 +188,12 @@ def _register_namespace(self, namespace: Union[str, tuple[str, str]]): self._context.append(entry) def set_property(self, property: Union[str, tuple[str, str], tuple[str, tuple[str, str]]], value): - """ - Set an arbitrary property on the object. The property name can be one of: + """Set an arbitrary property on the object. The property name can be one of: * A simple string with the property name * A tuple of the property name and the full namespace ``("name", "http://example.com/ns")`` - * A tuple containing the property name and another tuple of the short name and the full namespace ``("name", ("as", "http://example.com/ns"))`` + * A tuple containing the property name and another tuple of the short name + and the full namespace ``("name", ("as", "http://example.com/ns"))`` :param property: the property name :param value: the value to set @@ -209,31 +209,26 @@ def set_property(self, property: Union[str, tuple[str, str], tuple[str, tuple[st self._register_namespace(namespace) def get_property(self, property: Union[str, tuple[str, str], tuple[str, tuple[str, str]]]): - """ - Get an arbitrary property on the object. The property name can be one of: + """Get an arbitrary property on the object. The property name can be one of: * A simple string with the property name * A tuple of the property name and the full namespace ``("name", "http://example.com/ns")`` - * A tuple containing the property name and another tuple of the short name and the full namespace ``("name", ("as", "http://example.com/ns"))`` + * A tuple containing the property name and another tuple of the short name + and the full namespace ``("name", ("as", "http://example.com/ns"))`` - :param property: the property name + :param property: the property name :return: the value of the property, or None if it does not exist """ prop_name = property - namespace = None + if isinstance(property, tuple): prop_name = property[0] - namespace = property[1] return self._doc.get(prop_name, None) def to_jsonld(self) -> dict: - """ - Get the activity stream as a JSON-LD object + """Get the activity stream as a JSON-LD object. :return: """ - return { - "@context": self._context, - **self._doc - } \ No newline at end of file + return {"@context": self._context, **self._doc} diff --git a/coarnotify/core/notify.py b/coarnotify/core/notify.py index 081695f..ad521e1 100644 --- a/coarnotify/core/notify.py +++ b/coarnotify/core/notify.py @@ -1,6 +1,4 @@ -""" -This module is home to all the core model objects from which the notify patterns extend -""" +"""This module is home to all the core model objects from which the notify patterns extend.""" from coarnotify.core.activitystreams2 import ActivityStream, Properties, ActivityStreamsTypes, ACTIVITY_STREAMS_OBJECTS from coarnotify import validate @@ -10,39 +8,42 @@ from copy import deepcopy NOTIFY_NAMESPACE = "https://coar-notify.net" -"""Namespace for COAR Notify, to be used to construct namespaced properties used in COAR Notify Patterns""" +"""Namespace for COAR Notify, to be used to construct namespaced properties used in COAR Notify Patterns.""" + class NotifyProperties: - """ - COAR Notify properties used in COAR Notify Patterns + """COAR Notify properties used in COAR Notify Patterns. - Most of these are provided as tuples, where the first element is the property name, and the second element is the namespace. - Some are provided as plain strings without namespaces + Most of these are provided as tuples, where the first element is the property name, and the second element is the + namespace. Some are provided as plain strings without namespaces These are suitable to be used as property names in all the property getters/setters in the notify pattern objects and in the validation configuration. """ + INBOX = ("inbox", NOTIFY_NAMESPACE) - """``inbox`` property""" + """``inbox`` property.""" CITE_AS = ("ietf:cite-as", NOTIFY_NAMESPACE) - """``ietf:cite-as`` property""" + """``ietf:cite-as`` property.""" ITEM = ("ietf:item", NOTIFY_NAMESPACE) - """``ietf:item`` property""" + """``ietf:item`` property.""" NAME = "name" - """``name`` property""" + """``name`` property.""" MEDIA_TYPE = "mediaType" - """``mediaType`` property""" + """``mediaType`` property.""" + class NotifyTypes: - """ - List of all the COAR Notify types patterns may use. + """List of all the COAR Notify types patterns may use. - These are in addition to the base Activity Streams types, which are in :py:class:`coarnotify.core.activitystreams2.ActivityStreamsTypes` + These are in addition to the base Activity Streams types, which are in + :py:class:`coarnotify.core.activitystreams2.ActivityStreamsTypes` """ + ENDORSMENT_ACTION = "coar-notify:EndorsementAction" INGEST_ACTION = "coar-notify:IngestAction" RELATIONSHIP_ACTION = "coar-notify:RelationshipAction" @@ -56,64 +57,43 @@ class NotifyTypes: Properties.ID: { "default": validate.absolute_uri, "context": { - Properties.CONTEXT: { - "default": validate.url - }, - Properties.ORIGIN: { - "default": validate.url - }, - Properties.TARGET: { - "default": validate.url - }, - NotifyProperties.ITEM: { - "default": validate.url - } - } + Properties.CONTEXT: {"default": validate.url}, + Properties.ORIGIN: {"default": validate.url}, + Properties.TARGET: {"default": validate.url}, + NotifyProperties.ITEM: {"default": validate.url}, + }, }, Properties.TYPE: { "default": validate.type_checker, "context": { Properties.ACTOR: { - "default": validate.one_of([ - ActivityStreamsTypes.SERVICE, - ActivityStreamsTypes.APPLICATION, - ActivityStreamsTypes.GROUP, - ActivityStreamsTypes.ORGANIZATION, - ActivityStreamsTypes.PERSON - ]) + "default": validate.one_of( + [ + ActivityStreamsTypes.SERVICE, + ActivityStreamsTypes.APPLICATION, + ActivityStreamsTypes.GROUP, + ActivityStreamsTypes.ORGANIZATION, + ActivityStreamsTypes.PERSON, + ] + ) }, - Properties.OBJECT: { - "default": validate.at_least_one_of(ACTIVITY_STREAMS_OBJECTS) #validate.contains("sorg:AboutPage"), + "default": validate.at_least_one_of(ACTIVITY_STREAMS_OBJECTS) # validate.contains("sorg:AboutPage"), }, - Properties.CONTEXT: { - "default": validate.at_least_one_of(ACTIVITY_STREAMS_OBJECTS) #validate.contains("sorg:AboutPage"), + "default": validate.at_least_one_of(ACTIVITY_STREAMS_OBJECTS) # validate.contains("sorg:AboutPage"), }, - NotifyProperties.ITEM: { - "default": validate.at_least_one_of(ACTIVITY_STREAMS_OBJECTS) #validate.contains("sorg:AboutPage"), - } - } - }, - NotifyProperties.CITE_AS: { - "default": validate.url - }, - NotifyProperties.INBOX: { - "default": validate.url - }, - Properties.IN_REPLY_TO: { - "default": validate.absolute_uri - }, - Properties.SUBJECT_TRIPLE: { - "default": validate.absolute_uri - }, - Properties.OBJECT_TRIPLE: { - "default": validate.absolute_uri + "default": validate.at_least_one_of(ACTIVITY_STREAMS_OBJECTS) # validate.contains("sorg:AboutPage"), + }, + }, }, - Properties.RELATIONSHIP_TRIPLE: { - "default": validate.absolute_uri - } + NotifyProperties.CITE_AS: {"default": validate.url}, + NotifyProperties.INBOX: {"default": validate.url}, + Properties.IN_REPLY_TO: {"default": validate.absolute_uri}, + Properties.SUBJECT_TRIPLE: {"default": validate.absolute_uri}, + Properties.OBJECT_TRIPLE: {"default": validate.absolute_uri}, + Properties.RELATIONSHIP_TRIPLE: {"default": validate.absolute_uri}, } VALIDATORS: validate.Validator = validate.Validator(_VALIDATION_RULES) @@ -121,35 +101,39 @@ class NotifyTypes: class NotifyBase: - """ - Base class from which all Notify objects extend. + """Base class from which all Notify objects extend. There are two kinds of Notify objects: 1. Patterns, which are the notifications themselves 2. Pattern Parts, which are nested elements in the Patterns, such as objects, contexts, actors, etc - This class forms the basis for both of those types, and provides essential services, - such as construction, accessors and validation, as well as supporting the essential - properties "id" and "type" + This class forms the basis for both of those types, and provides essential services, such as construction, accessors + and validation, as well as supporting the essential properties "id" and "type" """ - def __init__(self, stream: Union[ActivityStream, dict] = None, - validate_stream_on_construct: bool=True, - validate_properties: bool=True, - validators: validate.Validator=None, - validation_context: Union[str, Tuple[str, str]]=None, - properties_by_reference: bool=True): - """ - Base constructor that all subclasses should call - - :param stream: The activity stream object, or a dict from which one can be created - :param validate_stream_on_construct: should the incoming stream be validated at construction-time - :param validate_properties: should individual properties be validated as they are set - :param validators: the validator object for this class and all nested elements. If not provided will use the default :py:data:`VALIDATORS` - :param validation_context: the context in which this object is being validated. This is used to determine which validators to use - :param properties_by_reference: should properties be get and set by reference (the default) or by value. Use this with caution: setting by value - makes it impossible to set a property in a nested object using the dot notation, like ``obj.actor.name = "Bob"``, instead you will need to retrive - the object, set the value, then set the whole object back on the parent object. + + def __init__( + self, + stream: Union[ActivityStream, dict] = None, + validate_stream_on_construct: bool = True, + validate_properties: bool = True, + validators: validate.Validator = None, + validation_context: Union[str, Tuple[str, str]] = None, + properties_by_reference: bool = True, + ): + """Base constructor that all subclasses should call. + + :param stream: The activity stream object, or a dict from which one can be created + :param validate_stream_on_construct: should the incoming stream be validated at construction-time + :param validate_properties: should individual properties be validated as they are set + :param validators: the validator object for this class and all nested elements. If not provided will use the + default :py:data:`VALIDATORS` + :param validation_context: the context in which this object is being validated. This is used to determine which + validators to use + :param properties_by_reference: should properties be get and set by reference (the default) or by value. Use + this with caution: setting by value makes it impossible to set a property in a nested object using the dot + notation, like ``obj.actor.name = "Bob"``, instead you will need to retrieve the object, set the value, then + set the whole object back on the parent object. """ self._validate_stream_on_construct = validate_stream_on_construct self._validate_properties = validate_properties @@ -175,27 +159,27 @@ def __init__(self, stream: Union[ActivityStream, dict] = None, @property def validate_properties(self) -> bool: - """Are properties being validated on set""" + """Are properties being validated on set.""" return self._validate_properties @property def validate_stream_on_construct(self) -> bool: - """Is the stream validated on construction""" + """Is the stream validated on construction.""" return self._validate_stream_on_construct @property def validators(self) -> validate.Validator: - """The validator object for this instance""" + """The validator object for this instance.""" return self._validators @property def doc(self): - """The underlying ActivityStream object, excluding the JSON-LD @context""" + """The underlying ActivityStream object, excluding the JSON-LD @context.""" return self._stream.doc @property def id(self) -> str: - """The ``id`` of the object""" + """The ``id`` of the object.""" return self.get_property(Properties.ID) @id.setter @@ -204,21 +188,20 @@ def id(self, value: str): @property def type(self) -> Union[str, list[str]]: - """The ``type`` of the object""" + """The ``type`` of the object.""" return self.get_property(Properties.TYPE) @type.setter def type(self, types: Union[str, list[str]]): self.set_property(Properties.TYPE, types) - def get_property(self, prop_name: Union[str, Tuple[str, str]], by_reference: bool=None): - """ - Generic property getter. It is strongly recommended that all accessors proxy for this function - as this enforces by-reference/by-value accessing, and mediates directly with the underlying - activity stream object. + def get_property(self, prop_name: Union[str, Tuple[str, str]], by_reference: bool = None): + """Generic property getter. It is strongly recommended that all accessors proxy for this function as this + enforces by-reference/by-value accessing, and mediates directly with the underlying activity stream object. :param prop_name: The property to retrieve - :param by_reference: Whether to retrieve by_reference or by_value. If not supplied will default to the object-wide setting + :param by_reference: Whether to retrieve by_reference or by_value. If not supplied will default to the object- + wide setting :return: the property value """ if by_reference is None: @@ -229,15 +212,14 @@ def get_property(self, prop_name: Union[str, Tuple[str, str]], by_reference: boo else: return deepcopy(val) - def set_property(self, prop_name: Union[str, Tuple[str, str]], value, by_reference: bool=None): - """ - Generic property setter. It is strongly recommended that all accessors proxy for this function - as this enforces by-reference/by-value accessing, and mediates directly with the underlying - activity stream object. + def set_property(self, prop_name: Union[str, Tuple[str, str]], value, by_reference: bool = None): + """Generic property setter. It is strongly recommended that all accessors proxy for this function as this + enforces by-reference/by-value accessing, and mediates directly with the underlying activity stream object. :param prop_name: The property to set :param value: The value to set - :param by_reference: Whether to set by_reference or by_value. If not supplied will default to the object-wide setting + :param by_reference: Whether to set by_reference or by_value. If not supplied will default to the object-wide + setting """ if by_reference is None: by_reference = self._properties_by_reference @@ -247,12 +229,11 @@ def set_property(self, prop_name: Union[str, Tuple[str, str]], value, by_referen self._stream.set_property(prop_name, value) def validate(self) -> bool: - """ - Validate the object. This provides the basic validation on ``id`` and ``type``. - Subclasses should override this method with their own validation, and call this method via ``super`` first to ensure - the basic properties are validated. + """Validate the object. This provides the basic validation on ``id`` and ``type``. Subclasses should override + this method with their own validation, and call this method via ``super`` first to ensure the basic properties + are validated. - :return: ``True`` or raise a :py:class:`coarnotify.exceptions.ValidationError` if there are errors + :return:``True`` or raise a :py:class:`coarnotify.exceptions.ValidationError` if there are errors """ ve = ValidationError() @@ -263,22 +244,23 @@ def validate(self) -> bool: raise ve return True - def validate_property(self, prop_name: Union[str, Tuple[str, str]], value, - force_validate: bool=False, raise_error: bool=True) -> Tuple[bool, str]: - """ - Validate a single property. This is used internally by :py:meth:`set_property`. + def validate_property( + self, prop_name: Union[str, Tuple[str, str]], value, force_validate: bool = False, raise_error: bool = True + ) -> Tuple[bool, str]: + """Validate a single property. This is used internally by :py:meth:`set_property`. - If the object has ``validate_properties`` set to ``False`` then that behaviour may be overridden by setting ``force_validate`` to ``True`` + If the object has ``validate_properties`` set to ``False`` then that behaviour may be overridden by setting + ``force_validate`` to ``True`` The validator applied to the property will be determined according to the ``validators`` property of the object and the ``validation_context`` of the object. :param prop_name: The property to validate - :param value: the value to validate - :param force_validate: whether to validate anyway, even if property validation is turned off at the object level + :param value: the value to validate + :param force_validate: whether to validate anyway, even if property validation is turned off at the object level :param raise_error: raise an exception on validation failure, or return a tuple with the result - :return: A tuple of whether validation was successful, and the error message if it was not - (the empty string is returned as the second element if validation was successful) + :return: A tuple of whether validation was successful, and the error message if it was not (the empty string is + returned as the second element if validation was successful) """ if value is None: return True, "" @@ -295,26 +277,24 @@ def validate_property(self, prop_name: Union[str, Tuple[str, str]], value, return True, "" def _register_property_validation_error(self, ve: ValidationError, prop_name: Union[str, tuple], value): - """Force validate the property and if an error is found, add it to the validation error""" + """Force validate the property and if an error is found, add it to the validation error.""" e, msg = self.validate_property(prop_name, value, force_validate=True, raise_error=False) if not e: ve.add_error(prop_name, msg) def required(self, ve: ValidationError, prop_name: Union[str, tuple], value): - """ - Add a required error to the validation error if the value is None + """Add a required error to the validation error if the value is None. :param ve: The validation error to which to add the message - :param prop_name: The property to check - :param value: The value + :param prop_name: The property to check + :param value: The value """ if value is None: pn = prop_name if not isinstance(prop_name, tuple) else prop_name[0] ve.add_error(prop_name, validate.REQUIRED_MESSAGE.format(x=pn)) def required_and_validate(self, ve: ValidationError, prop_name: Union[str, tuple], value): - """ - Add a required error to the validation error if the value is None, and then validate the value if not. + """Add a required error to the validation error if the value is None, and then validate the value if not. Any error messages are added to the ``ValidationError`` object @@ -335,8 +315,7 @@ def required_and_validate(self, ve: ValidationError, prop_name: Union[str, tuple self._register_property_validation_error(ve, prop_name, value) def optional_and_validate(self, ve: ValidationError, prop_name: Union[str, tuple], value): - """ - Validate the value if it is not None, but do not raise a validation error if it is None + """Validate the value if it is not None, but do not raise a validation error if it is None. :param ve: :param prop_name: @@ -353,8 +332,7 @@ def optional_and_validate(self, ve: ValidationError, prop_name: Union[str, tuple self._register_property_validation_error(ve, prop_name, value) def to_jsonld(self) -> dict: - """ - Get the notification pattern as JSON-LD + """Get the notification pattern as JSON-LD. :return: JSON-LD representation of the pattern """ @@ -362,42 +340,51 @@ def to_jsonld(self) -> dict: class NotifyPattern(NotifyBase): - """ - Base class for all notification patterns - """ + """Base class for all notification patterns.""" + TYPE = ActivityStreamsTypes.OBJECT - """The type of the pattern. This should be overridden by subclasses, otherwise defaults to ``Object``""" - - def __init__(self, stream: Union[ActivityStream, dict] = None, - validate_stream_on_construct=True, - validate_properties=True, - validators=None, - validation_context=None, - properties_by_reference=True): - """ - Constructor for the NotifyPattern + """The type of the pattern. + + This should be overridden by subclasses, otherwise defaults to ``Object`` + """ + + def __init__( + self, + stream: Union[ActivityStream, dict] = None, + validate_stream_on_construct=True, + validate_properties=True, + validators=None, + validation_context=None, + properties_by_reference=True, + ): + """Constructor for the NotifyPattern. This constructor will ensure that the pattern has its mandated type :py:attr:`TYPE` in the ``type`` property - :param stream: The activity stream object, or a dict from which one can be created - :param validate_stream_on_construct: should the incoming stream be validated at construction-time - :param validate_properties: should individual properties be validated as they are set - :param validators: the validator object for this class and all nested elements. If not provided will use the default :py:data:`VALIDATORS` - :param validation_context: the context in which this object is being validated. This is used to determine which validators to use - :param properties_by_reference: should properties be get and set by reference (the default) or by value. Use this with caution: setting by value - makes it impossible to set a property in a nested object using the dot notation, like ``obj.actor.name = "Bob"``, instead you will need to retrive - the object, set the value, then set the whole object back on the parent object. + :param stream: The activity stream object, or a dict from which one can be created + :param validate_stream_on_construct: should the incoming stream be validated at construction-time + :param validate_properties: should individual properties be validated as they are set + :param validators: the validator object for this class and all nested elements. If not provided will use the + default :py:data:`VALIDATORS` + :param validation_context: the context in which this object is being validated. This is used to determine which + validators to use + :param properties_by_reference: should properties be get and set by reference (the default) or by value. Use + this with caution: setting by value makes it impossible to set a property in a nested object using the dot + notation, like ``obj.actor.name = "Bob"``, instead you will need to retrieve the object, set the value, then + set the whole object back on the parent object. """ - super(NotifyPattern, self).__init__(stream=stream, - validate_stream_on_construct=validate_stream_on_construct, - validate_properties=validate_properties, - validators=validators, - validation_context=validation_context, - properties_by_reference=properties_by_reference) + super(NotifyPattern, self).__init__( + stream=stream, + validate_stream_on_construct=validate_stream_on_construct, + validate_properties=validate_properties, + validators=validators, + validation_context=validation_context, + properties_by_reference=properties_by_reference, + ) self._ensure_type_contains(self.TYPE) def _ensure_type_contains(self, types: Union[str, list[str]]): - """Ensure that the type field contains the given types""" + """Ensure that the type field contains the given types.""" existing = self._stream.get_property(Properties.TYPE) if existing is None: self.set_property(Properties.TYPE, types) @@ -415,15 +402,17 @@ def _ensure_type_contains(self, types: Union[str, list[str]]): @property def origin(self) -> Union["NotifyService", None]: - """Get the origin property of the notification""" + """Get the origin property of the notification.""" o = self.get_property(Properties.ORIGIN) if o is not None: - return NotifyService(o, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.ORIGIN, - properties_by_reference=self._properties_by_reference) + return NotifyService( + o, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.ORIGIN, + properties_by_reference=self._properties_by_reference, + ) return None @origin.setter @@ -432,15 +421,17 @@ def origin(self, value: "NotifyService"): @property def target(self) -> Union["NotifyService", None]: - """Get the target property of the notification""" + """Get the target property of the notification.""" t = self.get_property(Properties.TARGET) if t is not None: - return NotifyService(t, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.TARGET, - properties_by_reference=self._properties_by_reference) + return NotifyService( + t, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.TARGET, + properties_by_reference=self._properties_by_reference, + ) return None @target.setter @@ -449,15 +440,17 @@ def target(self, value: "NotifyService"): @property def object(self) -> Union["NotifyObject", None]: - """Get the object property of the notification""" + """Get the object property of the notification.""" o = self.get_property(Properties.OBJECT) if o is not None: - return NotifyObject(o, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.OBJECT, - properties_by_reference=self._properties_by_reference) + return NotifyObject( + o, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.OBJECT, + properties_by_reference=self._properties_by_reference, + ) return None @object.setter @@ -466,7 +459,7 @@ def object(self, value: "NotifyObject"): @property def in_reply_to(self) -> str: - """Get the inReplyTo property of the notification""" + """Get the inReplyTo property of the notification.""" return self.get_property(Properties.IN_REPLY_TO) @in_reply_to.setter @@ -475,15 +468,17 @@ def in_reply_to(self, value: str): @property def actor(self) -> Union["NotifyActor", None]: - """Get the actor property of the notification""" + """Get the actor property of the notification.""" a = self.get_property(Properties.ACTOR) if a is not None: - return NotifyActor(a, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.ACTOR, - properties_by_reference=self._properties_by_reference) + return NotifyActor( + a, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.ACTOR, + properties_by_reference=self._properties_by_reference, + ) return None @actor.setter @@ -492,15 +487,17 @@ def actor(self, value: "NotifyActor"): @property def context(self) -> Union["NotifyObject", None]: - """Get the context property of the notification""" + """Get the context property of the notification.""" c = self.get_property(Properties.CONTEXT) if c is not None: - return NotifyObject(c, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.CONTEXT, - properties_by_reference=self._properties_by_reference) + return NotifyObject( + c, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.CONTEXT, + properties_by_reference=self._properties_by_reference, + ) return None @context.setter @@ -508,8 +505,7 @@ def context(self, value: "NotifyObject"): self.set_property(Properties.CONTEXT, value.doc) def validate(self) -> bool: - """ - Base validator for all notification patterns. This extends the validate function on the superclass. + """Base validator for all notification patterns. This extends the validate function on the superclass. In addition to the base class's constraints, this applies the following validation: @@ -517,7 +513,7 @@ def validate(self) -> bool: * The ``actor`` ``inReplyTo`` and ``context`` properties are optional, but if present must be valid :py:class:`NotifyBase` - :return: ``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` + :return:``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` """ ve = ValidationError() try: @@ -537,61 +533,77 @@ def validate(self) -> bool: return True + class NotifyPatternPart(NotifyBase): + """Base class for all pattern parts, such as objects, contexts, actors, etc. + + If there is a default type specified, and a type is not given at construction, then the default type will be added + + :param stream: The activity stream object, or a dict from which one can be created + :param validate_stream_on_construct: should the incoming stream be validated at construction-time + :param validate_properties: should individual properties be validated as they are set + :param validators: the validator object for this class and all nested elements. If not provided will use the default + :py:data:`VALIDATORS` + :param validation_context: the context in which this object is being validated. This is used to determine which + validators to use + :param properties_by_reference: should properties be get and set by reference (the default) or by value. Use this + with caution: setting by value makes it impossible to set a property in a nested object using the dot notation, + like ``obj.actor.name = "Bob"``, instead you will need to retrieve the object, set the value, then set the whole + object back on the parent object. """ - Base class for all pattern parts, such as objects, contexts, actors, etc - - If there is a default type specified, and a type is not given at construction, then - the default type will be added - - :param stream: The activity stream object, or a dict from which one can be created - :param validate_stream_on_construct: should the incoming stream be validated at construction-time - :param validate_properties: should individual properties be validated as they are set - :param validators: the validator object for this class and all nested elements. If not provided will use the default :py:data:`VALIDATORS` - :param validation_context: the context in which this object is being validated. This is used to determine which validators to use - :param properties_by_reference: should properties be get and set by reference (the default) or by value. Use this with caution: setting by value - makes it impossible to set a property in a nested object using the dot notation, like ``obj.actor.name = "Bob"``, instead you will need to retrive - the object, set the value, then set the whole object back on the parent object. - """ + DEFAULT_TYPE = None - """The default type for this object, if none is provided on construction. If not provided, then no default type will be set""" + """The default type for this object, if none is provided on construction. + + If not provided, then no default type will be set + """ ALLOWED_TYPES = [] - """The list of types that are permissable for this object. If the list is empty, then any type is allowed""" - - def __init__(self, stream: Union[ActivityStream, dict] = None, - validate_stream_on_construct=True, - validate_properties=True, - validators=None, - validation_context=None, - properties_by_reference=True): - """ - Constructor for the NotifyPatternPart - - If there is a default type specified, and a type is not given at construction, then - the default type will be added - - :param stream: The activity stream object, or a dict from which one can be created - :param validate_stream_on_construct: should the incoming stream be validated at construction-time - :param validate_properties: should individual properties be validated as they are set - :param validators: the validator object for this class and all nested elements. If not provided will use the default :py:data:`VALIDATORS` - :param validation_context: the context in which this object is being validated. This is used to determine which validators to use - :param properties_by_reference: should properties be get and set by reference (the default) or by value. Use this with caution: setting by value - makes it impossible to set a property in a nested object using the dot notation, like ``obj.actor.name = "Bob"``, instead you will need to retrive - the object, set the value, then set the whole object back on the parent object. + """The list of types that are permissable for this object. + + If the list is empty, then any type is allowed + """ + + def __init__( + self, + stream: Union[ActivityStream, dict] = None, + validate_stream_on_construct=True, + validate_properties=True, + validators=None, + validation_context=None, + properties_by_reference=True, + ): + """Constructor for the NotifyPatternPart. + + If there is a default type specified, and a type is not given at construction, then the default type will be + added + + :param stream: The activity stream object, or a dict from which one can be created + :param validate_stream_on_construct: should the incoming stream be validated at construction-time + :param validate_properties: should individual properties be validated as they are set + :param validators: the validator object for this class and all nested elements. If not provided will use the + default :py:data:`VALIDATORS` + :param validation_context: the context in which this object is being validated. This is used to determine which + validators to use + :param properties_by_reference: should properties be get and set by reference (the default) or by value. Use + this with caution: setting by value makes it impossible to set a property in a nested object using the dot + notation, like ``obj.actor.name = "Bob"``, instead you will need to retrieve the object, set the value, then + set the whole object back on the parent object. """ - super(NotifyPatternPart, self).__init__(stream=stream, - validate_stream_on_construct=validate_stream_on_construct, - validate_properties=validate_properties, - validators=validators, - validation_context=validation_context, - properties_by_reference=properties_by_reference) + super(NotifyPatternPart, self).__init__( + stream=stream, + validate_stream_on_construct=validate_stream_on_construct, + validate_properties=validate_properties, + validators=validators, + validation_context=validation_context, + properties_by_reference=properties_by_reference, + ) if self.DEFAULT_TYPE is not None and self.type is None: self.type = self.DEFAULT_TYPE @NotifyBase.type.setter def type(self, types: Union[str, list[str]]): - """Set the type of the object, and validate that it is one of the allowed types if present""" + """Set the type of the object, and validate that it is one of the allowed types if present.""" if not isinstance(types, list): types = [types] @@ -608,19 +620,19 @@ def type(self, types: Union[str, list[str]]): class NotifyService(NotifyPatternPart): - """ - Default class to represent a service in the COAR Notify pattern. + """Default class to represent a service in the COAR Notify pattern. Services are used to represent ``origin`` and ``target`` properties in the notification patterns Specific patterns may need to extend this class to provide their specific behaviours and validation """ + DEFAULT_TYPE = ActivityStreamsTypes.SERVICE - """The default type for a service is ``Service``, but the type can be set to any value""" + """The default type for a service is ``Service``, but the type can be set to any value.""" @property def inbox(self) -> str: - """Get the ``inbox`` property of the service""" + """Get the ``inbox`` property of the service.""" return self.get_property(NotifyProperties.INBOX) @inbox.setter @@ -629,16 +641,15 @@ def inbox(self, value: str): class NotifyObject(NotifyPatternPart): - """ - Deafult class to represent an object in the COAR Notify pattern. Objects can be used for ``object`` or ``context`` properties - in notify patterns + """Default class to represent an object in the COAR Notify pattern. Objects can be used for ``object`` or + ``context`` properties in notify patterns. Specific patterns may need to extend this class to provide their specific behaviours and validation """ @property def cite_as(self) -> str: - """Get the ``ietf:cite-as`` property of the object""" + """Get the ``ietf:cite-as`` property of the object.""" return self.get_property(NotifyProperties.CITE_AS) @cite_as.setter @@ -647,15 +658,17 @@ def cite_as(self, value: str): @property def item(self) -> Union["NotifyItem", None]: - """Get the ``ietf:item`` property of the object""" + """Get the ``ietf:item`` property of the object.""" i = self.get_property(NotifyProperties.ITEM) if i is not None: - return NotifyItem(i, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=NotifyProperties.ITEM, - properties_by_reference=self._properties_by_reference) + return NotifyItem( + i, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=NotifyProperties.ITEM, + properties_by_reference=self._properties_by_reference, + ) return None @item.setter @@ -664,7 +677,7 @@ def item(self, value: "NotifyItem"): @property def triple(self) -> tuple[str, str, str]: - """Get object, relationship and subject properties as a relationship triple""" + """Get object, relationship and subject properties as a relationship triple.""" obj = self.get_property(Properties.OBJECT_TRIPLE) rel = self.get_property(Properties.RELATIONSHIP_TRIPLE) subj = self.get_property(Properties.SUBJECT_TRIPLE) @@ -678,11 +691,10 @@ def triple(self, value: tuple[str, str, str]): self.set_property(Properties.SUBJECT_TRIPLE, subj) def validate(self) -> bool: - """ - Validate the object. This overrides the base validation, as objects only absolutely require an ``id`` property, - so the base requirement for a ``type`` is relaxed. + """Validate the object. This overrides the base validation, as objects only absolutely require an ``id`` + property, so the base requirement for a ``type`` is relaxed. - :return: ``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` + :return:``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` """ ve = ValidationError() @@ -694,26 +706,27 @@ def validate(self) -> bool: class NotifyActor(NotifyPatternPart): - """ - Deafult class to represents an actor in the COAR Notify pattern. - Actors are used to represent the ``actor`` property in the notification patterns + """Default class to represents an actor in the COAR Notify pattern. Actors are used to represent the ``actor`` + property in the notification patterns. Specific patterns may need to extend this class to provide their specific behaviours and validation """ + DEFAULT_TYPE = ActivityStreamsTypes.SERVICE - """Default type is ``Service``, but can also be set as any one of the other allowed types""" - - ALLOWED_TYPES = [DEFAULT_TYPE, - ActivityStreamsTypes.APPLICATION, - ActivityStreamsTypes.GROUP, - ActivityStreamsTypes.ORGANIZATION, - ActivityStreamsTypes.PERSON - ] + """Default type is ``Service``, but can also be set as any one of the other allowed types.""" + + ALLOWED_TYPES = [ + DEFAULT_TYPE, + ActivityStreamsTypes.APPLICATION, + ActivityStreamsTypes.GROUP, + ActivityStreamsTypes.ORGANIZATION, + ActivityStreamsTypes.PERSON, + ] """The allowed types for an actor: ``Service``, ``Application``, ``Group``, ``Organisation``, ``Person``""" @property def name(self) -> str: - """Get the name property of the actor""" + """Get the name property of the actor.""" return self.get_property(NotifyProperties.NAME) @name.setter @@ -722,15 +735,15 @@ def name(self, value: str): class NotifyItem(NotifyPatternPart): - """ - Defult class to represent an item in the COAR Notify pattern. - Items are used to represent the ``ietf:item`` property in the notification patterns + """Default class to represent an item in the COAR Notify pattern. Items are used to represent the ``ietf:item`` + property in the notification patterns. Specific patterns may need to extend this class to provide their specific behaviours and validation """ + @property def media_type(self) -> str: - """Get the ``mediaType`` property of the item""" + """Get the ``mediaType`` property of the item.""" return self.get_property(NotifyProperties.MEDIA_TYPE) @media_type.setter @@ -738,11 +751,10 @@ def media_type(self, value: str): self.set_property(NotifyProperties.MEDIA_TYPE, value) def validate(self): - """ - Validate the item. This overrides the base validation, as objects only absolutely require an ``id`` property, - so the base requirement for a ``type`` is relaxed. + """Validate the item. This overrides the base validation, as objects only absolutely require an ``id`` + property, so the base requirement for a ``type`` is relaxed. - :return: ``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` + :return:``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` """ ve = ValidationError() @@ -753,44 +765,50 @@ def validate(self): return True -## Mixins +# Mixins ########################################################## + class NestedPatternObjectMixin: - """ - A mixin to add to a pattern which can override the default object property to return a full - nested pattern from the ``object`` property, rather than the default :py:class:`NotifyObject` + """A mixin to add to a pattern which can override the default object property to return a full nested pattern from + the ``object`` property, rather than the default :py:class:`NotifyObject` - This mixin needs to be first on the inheritance list, as it overrides the object property - of the NotifyPattern class. + This mixin needs to be first on the inheritance list, as it overrides the object property of the NotifyPattern + class. For example: .. code-block:: python - class MySpecialPattern(NestedPatternObjectMixin, NotifyPattern): - pass + class MySpecialPattern(NestedPatternObjectMixin, NotifyPattern): pass """ + @property def object(self) -> Union[NotifyPattern, NotifyObject, None]: - """Retrieve an object as it's correctly typed pattern, falling back to a default ``NotifyObject`` if no pattern matches""" + """Retrieve an object as it's correctly typed pattern, falling back to a default ``NotifyObject`` if no pattern + matches.""" o = self.get_property(Properties.OBJECT) if o is not None: from coarnotify.factory import COARNotifyFactory # late import to avoid circular dependency - nested = COARNotifyFactory.get_by_object(deepcopy(o), - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=None) # don't supply a validation context, as these objects are not typical nested objects + + nested = COARNotifyFactory.get_by_object( + deepcopy(o), + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=None, + ) # don't supply a validation context, as these objects are not typical nested objects if nested is not None: return nested # if we are unable to construct the typed nested object, just return a generic object - return NotifyObject(deepcopy(o), - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.OBJECT) + return NotifyObject( + deepcopy(o), + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.OBJECT, + ) return None @object.setter @@ -799,14 +817,13 @@ def object(self, value: Union[NotifyObject, NotifyPattern]): class SummaryMixin: - """ - Mixin to provide an API for setting and getting the ``summary`` property of a pattern - """ + """Mixin to provide an API for setting and getting the ``summary`` property of a pattern.""" + @property def summary(self) -> str: - """The summary property of the pattern""" + """The summary property of the pattern.""" return self.get_property(Properties.SUMMARY) @summary.setter def summary(self, summary: str): - self.set_property(Properties.SUMMARY, summary) \ No newline at end of file + self.set_property(Properties.SUMMARY, summary) diff --git a/coarnotify/exceptions.py b/coarnotify/exceptions.py index 4c0e0f8..229bffa 100644 --- a/coarnotify/exceptions.py +++ b/coarnotify/exceptions.py @@ -1,17 +1,14 @@ -""" -Module for custom exceptions -""" +"""Module for custom exceptions.""" + class NotifyException(Exception): - """ - Base class for all exceptions in the coarnotifypy library - """ + """Base class for all exceptions in the coarnotifypy library.""" + pass class ValidationError(NotifyException): - """ - Exception class for validation errors. + """Exception class for validation errors. :param errors: a dictionary of errors to construct the exception around. See below for the details of its structure @@ -70,23 +67,22 @@ def validate(): } } } - """ - def __init__(self, errors: dict=None): + + def __init__(self, errors: dict = None): super().__init__() self._errors = errors if errors is not None else {} @property def errors(self) -> dict: - """The dictionary of errors""" + """The dictionary of errors.""" return self._errors def add_error(self, key: str, value: str): - """ - Record an error on the supplied ``key`` with the message ``value`` + """Record an error on the supplied ``key`` with the message ``value`` :param key: the key for which an error is to be recorded - :param value: the error message + :param value: the error message :return: """ if key not in self._errors: @@ -94,8 +90,7 @@ def add_error(self, key: str, value: str): self._errors[key]["errors"].append(value) def add_nested_errors(self, key: str, subve: "ValidationError"): - """ - Take an existing ValidationError and add it as a nested set of errors under the supplied key + """Take an existing ValidationError and add it as a nested set of errors under the supplied key. :param key: the key under which all the nested validation errors should go :param subve: the existing ValidationError object @@ -110,8 +105,9 @@ def add_nested_errors(self, key: str, subve: "ValidationError"): self._errors[key]["nested"][k] = v def has_errors(self) -> bool: - """Are there any errors registered""" + """Are there any errors registered.""" return len(self._errors) > 0 def __str__(self): - return str(self._errors) \ No newline at end of file + """Return a string representation of the errors.""" + return str(self._errors) diff --git a/coarnotify/factory.py b/coarnotify/factory.py index 9835583..5d81bdb 100644 --- a/coarnotify/factory.py +++ b/coarnotify/factory.py @@ -1,6 +1,4 @@ -""" -Factory for producing the correct model based on the type or data within a payload -""" +"""Factory for producing the correct model based on the type or data within a payload.""" from typing import List, Callable, Union from coarnotify.core.activitystreams2 import ActivityStream, Properties @@ -17,15 +15,13 @@ TentativelyAccept, TentativelyReject, UnprocessableNotification, - UndoOffer + UndoOffer, ) from coarnotify.exceptions import NotifyException class COARNotifyFactory: - """ - Factory for producing the correct model based on the type or data within a payload - """ + """Factory for producing the correct model based on the type or data within a payload.""" MODELS = [ Accept, @@ -39,25 +35,24 @@ class COARNotifyFactory: TentativelyAccept, TentativelyReject, UnprocessableNotification, - UndoOffer + UndoOffer, ] - """The list of model classes recognised by this factory""" + """The list of model classes recognised by this factory.""" @classmethod - def get_by_types(cls, incoming_types:Union[str, List[str]]) -> Union[Callable, None]: - """ - Get the model class based on the supplied types. The returned callable is the class, not an instance. + def get_by_types(cls, incoming_types: Union[str, List[str]]) -> Union[Callable, None]: + """Get the model class based on the supplied types. The returned callable is the class, not an instance. - This is achieved by inspecting all of the known types in ``MODELS``, and performing the following - calculation: + This is achieved by inspecting all of the known types in ``MODELS``, and performing the following calculation: 1. If the supplied types are a subset of the model types, then this is a candidate, keep a reference to it 2. If the candidate fit is exact (supplied types and model types are the same), return the class - 3. If the class is a better fit than the last candidate, update the candidate. If the fit is exact, return the class + 3. If the class is a better fit than the last candidate, update the candidate. + If the fit is exact, return the class 4. Once we have run out of models to check, return the best candidate (or None if none found) :param incoming_types: a single type or list of types. If a list is provided, ALL types must match a candidate - :return: A class representing the best fit for the supplied types, or ``None`` if no match + :return: A class representing the best fit for the supplied types, or ``None`` if no match """ if not isinstance(incoming_types, list): incoming_types = [incoming_types] @@ -88,11 +83,10 @@ def get_by_types(cls, incoming_types:Union[str, List[str]]) -> Union[Callable, N @classmethod def get_by_object(cls, data: dict, *args, **kwargs) -> NotifyPattern: - """ - Get an instance of a model based on the data provided. + """Get an instance of a model based on the data provided. - Internally this calls ``get_by_types`` to determine the class to instantiate, and then creates an instance of that - Using the supplied args and kwargs. + Internally this calls ``get_by_types`` to determine the class to instantiate, and then creates an instance of + that using the supplied args and kwargs. If a model cannot be found that matches the data, a NotifyException is raised. @@ -114,7 +108,8 @@ def get_by_object(cls, data: dict, *args, **kwargs) -> NotifyPattern: @classmethod def register(cls, model: NotifyPattern): + """Register a new model with the factory.""" existing = cls.get_by_types(model.TYPE) if existing is not None: cls.MODELS.remove(existing) - cls.MODELS.append(model) \ No newline at end of file + cls.MODELS.append(model) diff --git a/coarnotify/http.py b/coarnotify/http.py index c1f2bca..4e58e61 100644 --- a/coarnotify/http.py +++ b/coarnotify/http.py @@ -1,19 +1,16 @@ -""" -HTTP layer interface and default implementation using requests lib -""" +"""HTTP layer interface and default implementation using requests lib.""" + import requests class HttpLayer: - """ - Interface for the HTTP layer + """Interface for the HTTP layer. This defines the methods which need to be implemented in order for the client to fully operate """ - def post(self, url: str, data: str, headers: dict=None, *args, **kwargs) -> 'HttpResponse': - """ - Make an HTTP POST request to the supplied URL with the given body data, and headers + def post(self, url: str, data: str, headers: dict = None, *args, **kwargs) -> 'HttpResponse': + """Make an HTTP POST request to the supplied URL with the given body data, and headers. `args` and `kwargs` can be used to pass implementation-specific parameters @@ -26,9 +23,8 @@ def post(self, url: str, data: str, headers: dict=None, *args, **kwargs) -> 'Htt """ raise NotImplementedError() - def get(self, url: str, headers: dict=None, *args, **kwargs) -> 'HttpResponse': - """ - Make an HTTP GET request to the supplied URL with the given headers + def get(self, url: str, headers: dict = None, *args, **kwargs) -> 'HttpResponse': + """Make an HTTP GET request to the supplied URL with the given headers. `args` and `kwargs` can be used to pass implementation-specific parameters @@ -42,15 +38,13 @@ def get(self, url: str, headers: dict=None, *args, **kwargs) -> 'HttpResponse': class HttpResponse: - """ - Interface for the HTTP response object + """Interface for the HTTP response object. This defines the methods which need to be implemented in order for the client to fully operate """ def header(self, header_name: str) -> str: - """ - Get the value of a header from the response + """Get the value of a header from the response. :param header_name: the name of the header :return: the header value @@ -59,8 +53,7 @@ def header(self, header_name: str) -> str: @property def status_code(self) -> int: - """ - Get the status code of the response + """Get the status code of the response. :return: the status code """ @@ -68,20 +61,20 @@ def status_code(self) -> int: ####################################### -## Implementations using requests lib +# Implementations using requests lib + class RequestsHttpLayer(HttpLayer): - """ - Implementation of the HTTP layer using the requests library. This is the default implementation - used when no other implementation is supplied + """Implementation of the HTTP layer using the requests library. + + This is the default implementation used when no other implementation is supplied """ - def post(self, url: str, data: str, headers: dict=None, *args, **kwargs) -> 'RequestsHttpResponse': - """ - Make an HTTP POST request to the supplied URL with the given body data, and headers + def post(self, url: str, data: str, headers: dict = None, *args, **kwargs) -> 'RequestsHttpResponse': + """Make an HTTP POST request to the supplied URL with the given body data, and headers. - `args` and `kwargs` can be used to pass additional parameters to the `requests.post` method, - such as authentication credentials, etc. + `args` and `kwargs` can be used to pass additional parameters to the `requests.post` method, such as + authentication credentials, etc. :param url: the request URL :param data: the body data @@ -93,12 +86,11 @@ def post(self, url: str, data: str, headers: dict=None, *args, **kwargs) -> 'Req resp = requests.post(url, data=data, headers=headers, *args, **kwargs) return RequestsHttpResponse(resp) - def get(self, url: str, headers: dict=None, *args, **kwargs) -> 'RequestsHttpResponse': - """ - Make an HTTP GET request to the supplied URL with the given headers + def get(self, url: str, headers: dict = None, *args, **kwargs) -> 'RequestsHttpResponse': + """Make an HTTP GET request to the supplied URL with the given headers. - `args` and `kwargs` can be used to pass additional parameters to the `requests.get` method, - such as authentication credentials, etc. + `args` and `kwargs` can be used to pass additional parameters to the `requests.get` method, such as + authentication credentials, etc. :param url: the request URL :param headers: HTTP headers as a dict to include in the request @@ -110,9 +102,9 @@ def get(self, url: str, headers: dict=None, *args, **kwargs) -> 'RequestsHttpRes resp = requests.get(url, headers=headers, *args, **kwargs) return RequestsHttpResponse(resp) + class RequestsHttpResponse(HttpResponse): - """ - Implementation fo the HTTP response object using the requests library + """Implementation fo the HTTP response object using the requests library. This wraps the requests response object and provides the interface required by the client @@ -120,16 +112,14 @@ class RequestsHttpResponse(HttpResponse): """ def __init__(self, resp: requests.Response): - """ - Construct the object as a wrapper around the original requests response object + """Construct the object as a wrapper around the original requests response object. :param resp: response object from the requests library """ self._resp = resp def header(self, header_name: str) -> str: - """ - Get the value of a header from the response + """Get the value of a header from the response. :param header_name: the name of the header :return: the header value @@ -138,8 +128,7 @@ def header(self, header_name: str) -> str: @property def status_code(self) -> int: - """ - Get the status code of the response + """Get the status code of the response. :return: the status code """ @@ -147,5 +136,5 @@ def status_code(self) -> int: @property def requests_response(self) -> requests.Response: - """Get the original requests response object""" + """Get the original requests response object.""" return self._resp diff --git a/coarnotify/patterns/__init__.py b/coarnotify/patterns/__init__.py index e183452..b49d1d2 100644 --- a/coarnotify/patterns/__init__.py +++ b/coarnotify/patterns/__init__.py @@ -1,17 +1,17 @@ -""" -All the COAR Notify pattern objects are defined in this module. +"""All the COAR Notify pattern objects are defined in this module. Some of the pattern objects have supporting objects in their individual submodules """ -from coarnotify.patterns.accept import Accept -from coarnotify.patterns.announce_endorsement import AnnounceEndorsement -from coarnotify.patterns.announce_relationship import AnnounceRelationship -from coarnotify.patterns.announce_review import AnnounceReview -from coarnotify.patterns.announce_service_result import AnnounceServiceResult -from coarnotify.patterns.reject import Reject -from coarnotify.patterns.request_endorsement import RequestEndorsement -from coarnotify.patterns.request_review import RequestReview -from coarnotify.patterns.tentatively_accept import TentativelyAccept -from coarnotify.patterns.tentatively_reject import TentativelyReject -from coarnotify.patterns.unprocessable_notification import UnprocessableNotification -from coarnotify.patterns.undo_offer import UndoOffer + +from coarnotify.patterns.accept import Accept # noqa: F401 +from coarnotify.patterns.announce_endorsement import AnnounceEndorsement # noqa: F401 +from coarnotify.patterns.announce_relationship import AnnounceRelationship # noqa: F401 +from coarnotify.patterns.announce_review import AnnounceReview # noqa: F401 +from coarnotify.patterns.announce_service_result import AnnounceServiceResult # noqa: F401 +from coarnotify.patterns.reject import Reject # noqa: F401 +from coarnotify.patterns.request_endorsement import RequestEndorsement # noqa: F401 +from coarnotify.patterns.request_review import RequestReview # noqa: F401 +from coarnotify.patterns.tentatively_accept import TentativelyAccept # noqa: F401 +from coarnotify.patterns.tentatively_reject import TentativelyReject # noqa: F401 +from coarnotify.patterns.unprocessable_notification import UnprocessableNotification # noqa: F401 +from coarnotify.patterns.undo_offer import UndoOffer # noqa: F401 diff --git a/coarnotify/patterns/accept.py b/coarnotify/patterns/accept.py index 0a54b81..4f5a611 100644 --- a/coarnotify/patterns/accept.py +++ b/coarnotify/patterns/accept.py @@ -1,30 +1,30 @@ -""" -Pattern to represent an Accept notification +"""Pattern to represent an Accept notification. + https://coar-notify.net/specification/1.0.0/accept/ """ + from coarnotify.core.notify import NotifyPattern, NestedPatternObjectMixin from coarnotify.core.activitystreams2 import ActivityStreamsTypes, Properties from coarnotify.exceptions import ValidationError __all__ = ["Accept"] + class Accept(NestedPatternObjectMixin, NotifyPattern): - """ - Class to represent an Accept notification - """ + """Class to represent an Accept notification.""" + TYPE = ActivityStreamsTypes.ACCEPT - """ The Accept type """ + """The Accept type.""" def validate(self) -> bool: - """ - Validate the Accept pattern. + """Validate the Accept pattern. In addition to the base validation, this: * Makes ``inReplyTo`` required * Requires the ``inReplyTo`` value to be the same as the ``object.id`` value - :return: ``True`` if valid, otherwise raises a :py:class:`coarnotify.exceptions.ValidationError` + :return:``True`` if valid, otherwise raises a :py:class:`coarnotify.exceptions.ValidationError` """ ve = ValidationError() try: @@ -38,10 +38,16 @@ def validate(self) -> bool: objid = self.object.id if self.object else None if self.in_reply_to != objid: - ve.add_error(Properties.IN_REPLY_TO, - f"Expected inReplyTo id to be the same as the nested object id. inReplyTo: {self.in_reply_to}, object.id: {objid}") + error_msg = ( + f"Expected inReplyTo id to be the same as the nested object id. " + f"inReplyTo: {self.in_reply_to}, object.id: {objid}" + ) + ve.add_error( + Properties.IN_REPLY_TO, + error_msg, + ) if ve.has_errors(): raise ve - return True \ No newline at end of file + return True diff --git a/coarnotify/patterns/announce_endorsement.py b/coarnotify/patterns/announce_endorsement.py index d5bc0ca..9d37375 100644 --- a/coarnotify/patterns/announce_endorsement.py +++ b/coarnotify/patterns/announce_endorsement.py @@ -1,7 +1,8 @@ -""" -Pattern to represent an ``Announce Endorsement`` notification +"""Pattern to represent an ``Announce Endorsement`` notification. + https://coar-notify.net/specification/1.0.0/announce-endorsement/ """ + from coarnotify.core.notify import NotifyPattern, NotifyTypes, NotifyItem, NotifyProperties, NotifyObject from coarnotify.core.activitystreams2 import ActivityStreamsTypes, Properties from coarnotify.exceptions import ValidationError @@ -10,35 +11,35 @@ __all__ = ["AnnounceEndorsement", "AnnounceEndorsementContext", "AnnounceEndorsementItem"] + class AnnounceEndorsement(NotifyPattern): - """ - Class to represent an Announce Endorsement pattern - """ + """Class to represent an Announce Endorsement pattern.""" + TYPE = [ActivityStreamsTypes.ANNOUNCE, NotifyTypes.ENDORSMENT_ACTION] - """Announce Endorsement type, consisting of Activity Streams Announce and Notify Endorsement Action""" + """Announce Endorsement type, consisting of Activity Streams Announce and Notify Endorsement Action.""" @property def context(self) -> Union["AnnounceEndorsementContext", None]: - """ - Get a context specific to Announce Endorsement + """Get a context specific to Announce Endorsement. :return: The Announce Endorsement context object """ c = self.get_property(Properties.CONTEXT) if c is not None: - return AnnounceEndorsementContext(c, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.CONTEXT, - properties_by_reference=self._properties_by_reference) + return AnnounceEndorsementContext( + c, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.CONTEXT, + properties_by_reference=self._properties_by_reference, + ) return None def validate(self) -> bool: - """ - Extends the base validation to make `context` required + """Extends the base validation to make `context` required. - :return: ``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` + :return:``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` """ ve = ValidationError() try: @@ -55,41 +56,38 @@ def validate(self) -> bool: class AnnounceEndorsementContext(NotifyObject): - """ - Announce Endorsement context object, which extends the base NotifyObject - to allow us to pass back a custom :py:class:`AnnounceEndorsementItem` - """ + """Announce Endorsement context object, which extends the base NotifyObject to allow us to pass back a custom + :py:class:`AnnounceEndorsementItem`.""" + @property def item(self) -> Union["AnnounceEndorsementItem", None]: - """ - Get a custom :py:class:`AnnounceEndorsementItem` + """Get a custom :py:class:`AnnounceEndorsementItem` :return: the Announce Endorsement Item """ i = self.get_property(NotifyProperties.ITEM) if i is not None: - return AnnounceEndorsementItem(i, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=NotifyProperties.ITEM, - properties_by_reference=self._properties_by_reference) + return AnnounceEndorsementItem( + i, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=NotifyProperties.ITEM, + properties_by_reference=self._properties_by_reference, + ) return None class AnnounceEndorsementItem(NotifyItem): - """ - Announce Endorsement Item, which extends the base NotifyItem to provide - additional validation - """ + """Announce Endorsement Item, which extends the base NotifyItem to provide additional validation.""" + def validate(self) -> bool: - """ - Extends the base validation with validation custom to Announce Endorsement notifications + """Extends the base validation with validation custom to Announce Endorsement notifications. * Adds type validation, which the base NotifyItem does not apply * Requires the ``mediaType`` value - :return: ``True`` if valid, otherwise raises a ValidationError + :return:``True`` if valid, otherwise raises a ValidationError """ ve = ValidationError() try: @@ -102,4 +100,4 @@ def validate(self) -> bool: if ve.has_errors(): raise ve - return True \ No newline at end of file + return True diff --git a/coarnotify/patterns/announce_relationship.py b/coarnotify/patterns/announce_relationship.py index 46b380a..bead70d 100644 --- a/coarnotify/patterns/announce_relationship.py +++ b/coarnotify/patterns/announce_relationship.py @@ -1,7 +1,8 @@ -""" -Pattern to represent an Announce Relationship notification +"""Pattern to represent an Announce Relationship notification. + https://coar-notify.net/specification/1.0.0/announce-relationship/ """ + from coarnotify.core.notify import NotifyPattern, NotifyTypes, NotifyObject from coarnotify.core.activitystreams2 import ActivityStreamsTypes, Properties from coarnotify.exceptions import ValidationError @@ -10,31 +11,32 @@ __all__ = ["AnnounceRelationship", "AnnounceRelationshipObject"] + class AnnounceRelationship(NotifyPattern): - """ - Class to represent an Announce Relationship notification - """ + """Class to represent an Announce Relationship notification.""" + TYPE = [ActivityStreamsTypes.ANNOUNCE, NotifyTypes.RELATIONSHIP_ACTION] - """Announce Relationship types, including an ActivityStreams announce and a COAR Notify Relationship Action""" + """Announce Relationship types, including an ActivityStreams announce and a COAR Notify Relationship Action.""" @property def object(self) -> Union["AnnounceRelationshipObject", None]: - """Custom getter to retrieve the object property as an AnnounceRelationshipObject""" + """Custom getter to retrieve the object property as an AnnounceRelationshipObject.""" o = self.get_property(Properties.OBJECT) if o is not None: - return AnnounceRelationshipObject(o, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.OBJECT, - properties_by_reference=self._properties_by_reference) + return AnnounceRelationshipObject( + o, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.OBJECT, + properties_by_reference=self._properties_by_reference, + ) return None def validate(self) -> bool: - """ - Extends the base validation to make `context` required + """Extends the base validation to make `context` required. - :return: ``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` + :return:``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` """ ve = ValidationError() try: @@ -51,12 +53,10 @@ def validate(self) -> bool: class AnnounceRelationshipObject(NotifyObject): - """ - Custom object class for Announce Relationship to apply the custom validation - """ + """Custom object class for Announce Relationship to apply the custom validation.""" + def validate(self) -> bool: - """ - Extend the base validation to include the following constraints: + """Extend the base validation to include the following constraints: * The object triple is required and each part must validate diff --git a/coarnotify/patterns/announce_review.py b/coarnotify/patterns/announce_review.py index 9d4c9f9..9df5268 100644 --- a/coarnotify/patterns/announce_review.py +++ b/coarnotify/patterns/announce_review.py @@ -1,7 +1,8 @@ -""" -Pattern to represent the Announce Review notification +"""Pattern to represent the Announce Review notification. + https://coar-notify.net/specification/1.0.0/announce-review/ """ + from coarnotify.core.notify import NotifyPattern, NotifyTypes, NotifyObject, NotifyItem, NotifyProperties from coarnotify.core.activitystreams2 import ActivityStreamsTypes, Properties from coarnotify.exceptions import ValidationError @@ -10,52 +11,53 @@ __all__ = ["AnnounceReview", "AnnounceReviewContext", "AnnounceReviewItem", "AnnounceReviewObject"] + class AnnounceReview(NotifyPattern): - """ - Class to represent Announce Review pattern - """ + """Class to represent Announce Review pattern.""" + TYPE = [ActivityStreamsTypes.ANNOUNCE, NotifyTypes.REVIEW_ACTION] - """ Announce Review type, including Acitivity Streams Announce and Notify Review Action """ + """Announce Review type, including Acitivity Streams Announce and Notify Review Action.""" @property def object(self) -> Union["AnnounceReviewObject", None]: - """ - Custom getter to retrieve Announce Review object + """Custom getter to retrieve Announce Review object. :return: Announce Review Object """ o = self.get_property(Properties.OBJECT) if o is not None: - return AnnounceReviewObject(o, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.OBJECT, - properties_by_reference=self._properties_by_reference) + return AnnounceReviewObject( + o, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.OBJECT, + properties_by_reference=self._properties_by_reference, + ) return None @property def context(self) -> Union["AnnounceReviewContext", None]: - """ - Custom getter to retrieve AnnounceReview Context + """Custom getter to retrieve AnnounceReview Context. :return: AnnounceReviewContext """ c = self.get_property(Properties.CONTEXT) if c is not None: - return AnnounceReviewContext(c, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.CONTEXT, - properties_by_reference=self._properties_by_reference) + return AnnounceReviewContext( + c, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.CONTEXT, + properties_by_reference=self._properties_by_reference, + ) return None def validate(self) -> bool: - """ - Extends the base validation to make `context` required + """Extends the base validation to make `context` required. - :return: ``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` + :return:``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` """ ve = ValidationError() try: @@ -70,42 +72,38 @@ def validate(self) -> bool: return True + class AnnounceReviewContext(NotifyObject): - """ - Custom Context for Announce Review, specifically to return custom - Announce Review Item - """ + """Custom Context for Announce Review, specifically to return custom Announce Review Item.""" + @property def item(self) -> Union["AnnounceReviewItem", None]: - """ - Custom getter to retrieve AnnounceReviewItem + """Custom getter to retrieve AnnounceReviewItem. :return: AnnounceReviewItem """ i = self.get_property(NotifyProperties.ITEM) if i is not None: - return AnnounceReviewItem(i, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=NotifyProperties.ITEM, - properties_by_reference=self._properties_by_reference) + return AnnounceReviewItem( + i, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=NotifyProperties.ITEM, + properties_by_reference=self._properties_by_reference, + ) return None class AnnounceReviewItem(NotifyItem): - """ - Custom AnnounceReviewItem which provides additional validation over the basic NotifyItem - """ + """Custom AnnounceReviewItem which provides additional validation over the basic NotifyItem.""" def validate(self) -> bool: - """ - In addition to the base validator, this: + """In addition to the base validator, this: - * Reintroduces type validation - * make ``mediaType`` a required field + * Reintroduces type validation * make ``mediaType`` a required field - :return: ``True`` if valid, else raises a ValidationError + :return:``True`` if valid, else raises a ValidationError """ ve = ValidationError() try: @@ -122,17 +120,14 @@ def validate(self) -> bool: class AnnounceReviewObject(NotifyObject): - """ - Custom Announce Review Object to apply custom validation for this pattern - """ + """Custom Announce Review Object to apply custom validation for this pattern.""" def validate(self) -> bool: - """ - In addition to the base validator this: + """In addition to the base validator this: * Makes type required - :return: ``True`` if valid, else raises ValidationError + :return:``True`` if valid, else raises ValidationError """ ve = ValidationError() try: @@ -145,4 +140,4 @@ def validate(self) -> bool: if ve.has_errors(): raise ve - return True \ No newline at end of file + return True diff --git a/coarnotify/patterns/announce_service_result.py b/coarnotify/patterns/announce_service_result.py index fb00454..5496a81 100644 --- a/coarnotify/patterns/announce_service_result.py +++ b/coarnotify/patterns/announce_service_result.py @@ -1,62 +1,68 @@ -""" -Pattern to represent the Announce Service Result notification +"""Pattern to represent the Announce Service Result notification. + https://coar-notify.net/specification/1.0.0/announce-resource/ """ + from coarnotify.core.notify import NotifyPattern, NotifyItem, NotifyProperties, NotifyObject from coarnotify.core.activitystreams2 import ActivityStreamsTypes, Properties from coarnotify.exceptions import ValidationError from typing import Union -__all__ = ["AnnounceServiceResult", "AnnounceServiceResultObject", "AnnounceServiceResultContext", "AnnounceServiceResultItem"] +__all__ = [ + "AnnounceServiceResult", + "AnnounceServiceResultObject", + "AnnounceServiceResultContext", + "AnnounceServiceResultItem", +] + class AnnounceServiceResult(NotifyPattern): - """ - Class to represent the Announce Service Result Pattern - """ + """Class to represent the Announce Service Result Pattern.""" TYPE = ActivityStreamsTypes.ANNOUNCE - """Announce Service Result type, the ActivityStreams Announce type""" + """Announce Service Result type, the ActivityStreams Announce type.""" @property def object(self) -> Union["AnnounceServiceResultObject", None]: - """ - Custom getter to retrieve the object property as an AnnounceServiceResultObject + """Custom getter to retrieve the object property as an AnnounceServiceResultObject. :return: AnnounceServiceResultObject """ o = self.get_property(Properties.OBJECT) if o is not None: - return AnnounceServiceResultObject(o, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.OBJECT, - properties_by_reference=self._properties_by_reference) + return AnnounceServiceResultObject( + o, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.OBJECT, + properties_by_reference=self._properties_by_reference, + ) return None @property def context(self) -> Union["AnnounceServiceResultContext", None]: - """ - Custom getter to retrieve the context property as an AnnounceServiceResultContext + """Custom getter to retrieve the context property as an AnnounceServiceResultContext. - :return: AnnounceSericeResultCOntext + :return: AnnounceServiceResultContext """ c = self.get_property(Properties.CONTEXT) if c is not None: - return AnnounceServiceResultContext(c, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.CONTEXT, - properties_by_reference=self._properties_by_reference) + return AnnounceServiceResultContext( + c, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.CONTEXT, + properties_by_reference=self._properties_by_reference, + ) return None def validate(self) -> bool: - """ - Extends the base validation to make `context` required + """Extends the base validation to make `context` required. - :return: ``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` + :return:``True`` if valid, otherwise raises :py:class:`coarnotify.exceptions.ValidationError` """ ve = ValidationError() try: @@ -73,39 +79,34 @@ def validate(self) -> bool: class AnnounceServiceResultContext(NotifyObject): - """ - Custom object class for Announce Service Result to provide the custom item getter - """ + """Custom object class for Announce Service Result to provide the custom item getter.""" @property def item(self) -> Union["AnnounceServiceResultItem", None]: - """ - Custom getter to retrieve the item property as an AnnounceServiceResultItem - :return: - """ + """Custom getter to retrieve the item property as an AnnounceServiceResultItem :return:""" i = self.get_property(NotifyProperties.ITEM) if i is not None: - return AnnounceServiceResultItem(i, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=NotifyProperties.ITEM, - properties_by_reference=self._properties_by_reference) + return AnnounceServiceResultItem( + i, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=NotifyProperties.ITEM, + properties_by_reference=self._properties_by_reference, + ) return None + class AnnounceServiceResultItem(NotifyItem): - """ - Custom item class for Announce Service Result to apply the custom validation - """ + """Custom item class for Announce Service Result to apply the custom validation.""" def validate(self) -> bool: - """ - Beyond the base validation, apply the following: + """Beyond the base validation, apply the following: * Make type required and avlid * Make the ``mediaType`` required - :return: ``True`` if validation passes, else raise a ``ValidationError`` + :return:``True`` if validation passes, else raise a ``ValidationError`` """ ve = ValidationError() try: @@ -120,18 +121,16 @@ def validate(self) -> bool: raise ve return True + class AnnounceServiceResultObject(NotifyObject): - """ - Custom object class for Announce Service Result to apply the custom validation - """ + """Custom object class for Announce Service Result to apply the custom validation.""" def validate(self) -> bool: - """ - Extend the base validation to include the following constraints: + """Extend the base validation to include the following constraints: * The object type is required and must validate - :return: ``True`` if validation passes, else raise a ``ValidationError`` + :return:``True`` if validation passes, else raise a ``ValidationError`` """ ve = ValidationError() try: @@ -144,4 +143,4 @@ def validate(self) -> bool: if ve.has_errors(): raise ve - return True \ No newline at end of file + return True diff --git a/coarnotify/patterns/reject.py b/coarnotify/patterns/reject.py index 77e712a..d0f07ff 100644 --- a/coarnotify/patterns/reject.py +++ b/coarnotify/patterns/reject.py @@ -1,5 +1,5 @@ -""" -Pattern to represent a Reject notification +"""Pattern to represent a Reject notification. + https://coar-notify.net/specification/1.0.0/reject/ """ @@ -9,22 +9,19 @@ __all__ = ["Reject"] + class Reject(NestedPatternObjectMixin, NotifyPattern, SummaryMixin): - """ - Class to represent a Reject notification - """ + """Class to represent a Reject notification.""" TYPE = ActivityStreamsTypes.REJECT - """Reject type, the ActivityStreams Reject type""" + """Reject type, the ActivityStreams Reject type.""" def validate(self) -> bool: - """ - In addition to the base validation apply the following constraints: + """In addition to the base validation apply the following constraints: - * The ``inReplyTo`` property is required - * The ``inReplyTo`` value must match the ``object.id`` value + * The ``inReplyTo`` property is required * The ``inReplyTo`` value must match the ``object.id`` value - :return: ``True`` if the validation passes, otherwise raise a ``ValidationError`` + :return:``True`` if the validation passes, otherwise raise a ``ValidationError`` """ ve = ValidationError() try: @@ -38,7 +35,11 @@ def validate(self) -> bool: objid = self.object.id if self.object else None if self.in_reply_to != objid: - ve.add_error(Properties.IN_REPLY_TO, f"Expected inReplyTo id to be the same as the nested object id. inReplyTo: {self.in_reply_to}, object.id: {objid}") + error_msg = ( + f"Expected inReplyTo id to be the same as the nested object id. " + f"inReplyTo: {self.in_reply_to}, object.id: {objid}" + ) + ve.add_error(Properties.IN_REPLY_TO, error_msg) if ve.has_errors(): raise ve diff --git a/coarnotify/patterns/request_endorsement.py b/coarnotify/patterns/request_endorsement.py index 480b525..1a9a5a1 100644 --- a/coarnotify/patterns/request_endorsement.py +++ b/coarnotify/patterns/request_endorsement.py @@ -1,7 +1,8 @@ -""" -Pattern to represent a Request Endorsement notification +"""Pattern to represent a Request Endorsement notification. + https://coar-notify.net/specification/1.0.0/request-endorsement/ """ + from coarnotify.core.notify import NotifyPattern, NotifyTypes, NotifyItem, NotifyProperties, NotifyObject from coarnotify.core.activitystreams2 import ActivityStreamsTypes, Properties from coarnotify.exceptions import ValidationError @@ -10,66 +11,61 @@ __all__ = ["RequestEndorsement", "RequestEndorsementObject", "RequestEndorsementItem"] + class RequestEndorsement(NotifyPattern): - """ - Class to represent a Request Endorsement notification - """ + """Class to represent a Request Endorsement notification.""" + TYPE = [ActivityStreamsTypes.OFFER, NotifyTypes.ENDORSMENT_ACTION] - """Request Endorsement types, including an ActivityStreams offer and a COAR Notify Endorsement Action""" + """Request Endorsement types, including an ActivityStreams offer and a COAR Notify Endorsement Action.""" @property def object(self) -> Union["RequestEndorsementObject", None]: - """ - Custom getter to retrieve the object property as a RequestEndorsementObject + """Custom getter to retrieve the object property as a RequestEndorsementObject. :return: """ o = self.get_property(Properties.OBJECT) if o is not None: - return RequestEndorsementObject(o, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.OBJECT, - properties_by_reference=self._properties_by_reference) + return RequestEndorsementObject( + o, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.OBJECT, + properties_by_reference=self._properties_by_reference, + ) return None class RequestEndorsementObject(NotifyObject): - """ - Custom object class for Request Endorsement to provide the custom item getter - """ + """Custom object class for Request Endorsement to provide the custom item getter.""" @property def item(self) -> Union["RequestEndorsementItem", None]: - """ - Custom getter to retrieve the item property as a RequestEndorsementItem - :return: - """ + """Custom getter to retrieve the item property as a RequestEndorsementItem :return:""" i = self.get_property(NotifyProperties.ITEM) if i is not None: - return RequestEndorsementItem(i, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=NotifyProperties.ITEM, - properties_by_reference=self._properties_by_reference) + return RequestEndorsementItem( + i, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=NotifyProperties.ITEM, + properties_by_reference=self._properties_by_reference, + ) return None class RequestEndorsementItem(NotifyItem): - """ - Custom item class for Request Endorsement to provide the custom validation - """ + """Custom item class for Request Endorsement to provide the custom validation.""" def validate(self) -> bool: - """ - Extend the base validation to include the following constraints: + """Extend the base validation to include the following constraints: * The item type is required and must validate * The ``mediaType`` property is required - :return: ``True`` if validation passes, otherwise raise a ``ValidationError`` + :return:``True`` if validation passes, otherwise raise a ``ValidationError`` """ ve = ValidationError() try: diff --git a/coarnotify/patterns/request_review.py b/coarnotify/patterns/request_review.py index 129a398..6f7c72f 100644 --- a/coarnotify/patterns/request_review.py +++ b/coarnotify/patterns/request_review.py @@ -1,7 +1,8 @@ -""" -Pattern to represent a Request Review notification +"""Pattern to represent a Request Review notification. + https://coar-notify.net/specification/1.0.0/request-review/ """ + from coarnotify.core.notify import NotifyPattern, NotifyTypes, NotifyObject, NotifyItem, NotifyProperties from coarnotify.core.activitystreams2 import ActivityStreamsTypes, Properties from coarnotify.exceptions import ValidationError @@ -10,65 +11,58 @@ __all__ = ["RequestReview", "RequestReviewObject", "RequestReviewItem"] + class RequestReview(NotifyPattern): - """ - Class to represent a Request Review notification - """ + """Class to represent a Request Review notification.""" TYPE = [ActivityStreamsTypes.OFFER, NotifyTypes.REVIEW_ACTION] - """Request Review types, including an ActivityStreams offer and a COAR Notify Review Action""" + """Request Review types, including an ActivityStreams offer and a COAR Notify Review Action.""" @property def object(self) -> Union["RequestReviewObject", None]: - """ - Custom getter to retrieve the object property as a RequestReviewObject - :return: - """ + """Custom getter to retrieve the object property as a RequestReviewObject :return:""" o = self.get_property(Properties.OBJECT) if o is not None: - return RequestReviewObject(o, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=Properties.OBJECT, - properties_by_reference=self._properties_by_reference) + return RequestReviewObject( + o, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=Properties.OBJECT, + properties_by_reference=self._properties_by_reference, + ) return None class RequestReviewObject(NotifyObject): - """ - Custom Request Review Object class to return the custom RequestReviewItem class - """ + """Custom Request Review Object class to return the custom RequestReviewItem class.""" + @property def item(self) -> Union["RequestReviewItem", None]: - """ - Custom getter to retrieve the item property as a RequestReviewItem - :return: - """ + """Custom getter to retrieve the item property as a RequestReviewItem :return:""" i = self.get_property(NotifyProperties.ITEM) if i is not None: - return RequestReviewItem(i, - validate_stream_on_construct=False, - validate_properties=self.validate_properties, - validators=self.validators, - validation_context=NotifyProperties.ITEM, - properties_by_reference=self._properties_by_reference) + return RequestReviewItem( + i, + validate_stream_on_construct=False, + validate_properties=self.validate_properties, + validators=self.validators, + validation_context=NotifyProperties.ITEM, + properties_by_reference=self._properties_by_reference, + ) return None class RequestReviewItem(NotifyItem): - """ - Custom Request Review Item class to provide the custom validation - """ + """Custom Request Review Item class to provide the custom validation.""" def validate(self) -> bool: - """ - Extend the base validation to include the following constraints: + """Extend the base validation to include the following constraints: * The type property is required and must validate * the ``mediaType`` property is required - :return: ``True`` if validation passes, else raise a ``ValidationError`` + :return:``True`` if validation passes, else raise a ``ValidationError`` """ ve = ValidationError() try: @@ -81,4 +75,4 @@ def validate(self) -> bool: if ve.has_errors(): raise ve - return True \ No newline at end of file + return True diff --git a/coarnotify/patterns/tentatively_accept.py b/coarnotify/patterns/tentatively_accept.py index d3bab24..2aa1e98 100644 --- a/coarnotify/patterns/tentatively_accept.py +++ b/coarnotify/patterns/tentatively_accept.py @@ -1,26 +1,25 @@ -""" -Pattern to represent a Tentative Accept notification +"""Pattern to represent a Tentative Accept notification. + https://coar-notify.net/specification/1.0.0/tentative-accept/ """ + from coarnotify.core.notify import NotifyPattern, SummaryMixin, NestedPatternObjectMixin from coarnotify.core.activitystreams2 import ActivityStreamsTypes, Properties from coarnotify.exceptions import ValidationError __all__ = ["TentativelyAccept"] + class TentativelyAccept(NestedPatternObjectMixin, NotifyPattern, SummaryMixin): - """ - Class to represent a Tentative Accept notification - """ + """Class to represent a Tentative Accept notification.""" + TYPE = ActivityStreamsTypes.TENTATIVE_ACCEPT - """Tentative Accept type, the ActivityStreams Tentative Accept type""" + """Tentative Accept type, the ActivityStreams Tentative Accept type.""" def validate(self) -> bool: - """ - In addition to the base validation apply the following constraints: + """In addition to the base validation apply the following constraints: - * The ``inReplyTo`` property is required - * The ``inReplyTo`` value must match the ``object.id`` value + * The ``inReplyTo`` property is required * The ``inReplyTo`` value must match the ``object.id`` value :return: """ @@ -36,10 +35,13 @@ def validate(self) -> bool: objid = self.object.id if self.object else None if self.in_reply_to != objid: - ve.add_error(Properties.IN_REPLY_TO, - f"Expected inReplyTo id to be the same as the nested object id. inReplyTo: {self.in_reply_to}, object.id: {objid}") + error_msg = ( + f"Expected inReplyTo id to be the same as the nested object id. " + f"inReplyTo: {self.in_reply_to}, object.id: {objid}" + ) + ve.add_error(Properties.IN_REPLY_TO, error_msg) if ve.has_errors(): raise ve - return True \ No newline at end of file + return True diff --git a/coarnotify/patterns/tentatively_reject.py b/coarnotify/patterns/tentatively_reject.py index cdb7cd0..9f608be 100644 --- a/coarnotify/patterns/tentatively_reject.py +++ b/coarnotify/patterns/tentatively_reject.py @@ -1,23 +1,23 @@ -""" -Pattern for the Tentatively Reject notification +"""Pattern for the Tentatively Reject notification. + https://coar-notify.net/specification/1.0.0/tentative-reject/ """ + from coarnotify.core.notify import NotifyPattern, SummaryMixin, NestedPatternObjectMixin from coarnotify.core.activitystreams2 import ActivityStreamsTypes, Properties from coarnotify.exceptions import ValidationError __all__ = ["TentativelyReject"] + class TentativelyReject(NestedPatternObjectMixin, NotifyPattern, SummaryMixin): - """ - Class to represent a Tentative Reject notification - """ + """Class to represent a Tentative Reject notification.""" + TYPE = ActivityStreamsTypes.TENTATIVE_REJECT - """Tentative Reject type, the ActivityStreams Tentative Reject type""" + """Tentative Reject type, the ActivityStreams Tentative Reject type.""" def validate(self) -> bool: - """ - In addition to the base validation apply the following constraints: + """In addition to the base validation apply the following constraints: * The ``inReplyTo`` property is required * The ``inReplyTo`` value must match the ``object.id`` value @@ -36,10 +36,13 @@ def validate(self) -> bool: objid = self.object.id if self.object else None if self.in_reply_to != objid: - ve.add_error(Properties.IN_REPLY_TO, - f"Expected inReplyTo id to be the same as the nested object id. inReplyTo: {self.in_reply_to}, object.id: {objid}") + error_msg = ( + f"Expected inReplyTo id to be the same as the nested object id. " + f"inReplyTo: {self.in_reply_to}, object.id: {objid}" + ) + ve.add_error(Properties.IN_REPLY_TO, error_msg) if ve.has_errors(): raise ve - return True \ No newline at end of file + return True diff --git a/coarnotify/patterns/undo_offer.py b/coarnotify/patterns/undo_offer.py index d196fa7..c67c639 100644 --- a/coarnotify/patterns/undo_offer.py +++ b/coarnotify/patterns/undo_offer.py @@ -1,23 +1,23 @@ -""" -Pattern to represent the Undo Offer notification +"""Pattern to represent the Undo Offer notification. + https://coar-notify.net/specification/1.0.0/undo-offer/ """ + from coarnotify.core.notify import NotifyPattern, NestedPatternObjectMixin, SummaryMixin from coarnotify.core.activitystreams2 import ActivityStreamsTypes, Properties from coarnotify.exceptions import ValidationError __all__ = ["UndoOffer"] + class UndoOffer(NestedPatternObjectMixin, NotifyPattern, SummaryMixin): - """ - Class to represent the Undo Offer notification - """ + """Class to represent the Undo Offer notification.""" + TYPE = ActivityStreamsTypes.UNDO - """Undo Offer type, the ActivityStreams Undo type""" + """Undo Offer type, the ActivityStreams Undo type.""" def validate(self) -> bool: - """ - In addition to the base validation apply the following constraints: + """In addition to the base validation apply the following constraints: * The ``inReplyTo`` property is required * The ``inReplyTo`` value must match the ``object.id`` value @@ -36,7 +36,11 @@ def validate(self) -> bool: objid = self.object.id if self.object else None if self.in_reply_to != objid: - ve.add_error(Properties.IN_REPLY_TO, f"Expected inReplyTo id to be the same as the nested object id. inReplyTo: {self.in_reply_to}, object.id: {objid}") + error_msg = ( + f"Expected inReplyTo id to be the same as the nested object id. " + f"inReplyTo: {self.in_reply_to}, object.id: {objid}" + ) + ve.add_error(Properties.IN_REPLY_TO, error_msg) if ve.has_errors(): raise ve diff --git a/coarnotify/patterns/unprocessable_notification.py b/coarnotify/patterns/unprocessable_notification.py index 3fe3e67..b9beeee 100644 --- a/coarnotify/patterns/unprocessable_notification.py +++ b/coarnotify/patterns/unprocessable_notification.py @@ -1,5 +1,5 @@ -""" -Pattern to represent the Unprocessable Notification notification +"""Pattern to represent the Unprocessable notification. + https://coar-notify.net/specification/1.0.0/unprocessable/ """ @@ -9,16 +9,16 @@ __all__ = ["UnprocessableNotification"] + class UnprocessableNotification(NotifyPattern, SummaryMixin): - """ - Class to represent the Unprocessable Notification notification - """ + """Class to represent the Unprocessable notification.""" + TYPE = [ActivityStreamsTypes.FLAG, NotifyTypes.UNPROCESSABLE_NOTIFICATION] - """Unprocessable Notification types, including an ActivityStreams Flag and a COAR Notify Unprocessable Notification""" + """Unprocessable Notification types, including an ActivityStreams Flag and a COAR Notify Unprocessable + Notification.""" def validate(self) -> bool: - """ - In addition to the base validation apply the following constraints: + """In addition to the base validation apply the following constraints: * The ``inReplyTo`` property is required * The ``summary`` property is required diff --git a/coarnotify/server.py b/coarnotify/server.py index a56e685..ef2ec0e 100644 --- a/coarnotify/server.py +++ b/coarnotify/server.py @@ -1,6 +1,5 @@ -""" -Supporting classes for COAR Notify server implementations -""" +"""Supporting classes for COAR Notify server implementations.""" + import json import typing from typing import Union @@ -12,25 +11,23 @@ class COARNotifyReceipt: - """ - An object representing the response from a COAR Notify server. + """An object representing the response from a COAR Notify server. - Server implementations should construct and return this object with the appropriate properties - when implementing the :py:meth:`COARNotifyServiceBinding.notification_received` binding + Server implementations should construct and return this object with the appropriate properties when implementing the + :py:meth:`COARNotifyServiceBinding.notification_received` binding :param status: the HTTP status code, should be one of the constants ``CREATED`` (201) or ``ACCEPTED`` (202) :param location: the HTTP URI for the resource that was created (if present) """ CREATED = 201 - """The status code for a created resource""" + """The status code for a created resource.""" ACCEPTED = 202 - """The status code for an accepted request""" + """The status code for an accepted request.""" def __init__(self, status: int, location: str = None): - """ - Construct a new COARNotifyReceipt object with the status code and location URL (optional) + """Construct a new COARNotifyReceipt object with the status code and location URL (optional) :param status: the HTTP status code, should be one of the constants ``CREATED`` (201) or ``ACCEPTED`` (202) :param location: the HTTP URI for the resource that was created (if present) @@ -40,28 +37,29 @@ def __init__(self, status: int, location: str = None): @property def status(self) -> int: - """The status code of the response. Should be one of the constants ``CREATED`` (201) or ``ACCEPTED`` (202)""" + """The status code of the response. + + Should be one of the constants ``CREATED`` (201) or ``ACCEPTED`` (202) + """ return self._status @property def location(self) -> Union[str, None]: - """The HTTP URI of the created resource, if present""" + """The HTTP URI of the created resource, if present.""" return self._location class COARNotifyServiceBinding: - """ - Interface for implementing a COAR Notify server binding. + """Interface for implementing a COAR Notify server binding. Server implementation should extend this class and implement the :py:meth:`notification_received` method - That method will receive a :py:class:`NotifyPattern` object, which will be one of the known types - and should return a :py:class:`COARNotifyReceipt` object with the appropriate status code and location URL + That method will receive a :py:class:`NotifyPattern` object, which will be one of the known types and should return + a :py:class:`COARNotifyReceipt` object with the appropriate status code and location URL """ def notification_received(self, notification: 'NotifyPattern') -> COARNotifyReceipt: - """ - Process the receipt of the given notification, and respond with an appropriate receipt object + """Process the receipt of the given notification, and respond with an appropriate receipt object. :param notification: the notification object received :return: the receipt object to send back to the client @@ -70,20 +68,18 @@ def notification_received(self, notification: 'NotifyPattern') -> COARNotifyRece class COARNotifyServerError(Exception): - """ - An exception class for server errors in the COAR Notify server implementation. + """An exception class for server errors in the COAR Notify server implementation. The web layer of your server implementation should be able to intercept this from the - :py:meth:`COARNotifyServer.receive` method and return the appropriate HTTP status code and message to the - user in its standard way. + :py:meth:`COARNotifyServer.receive` method and return the appropriate HTTP status code and message to the user in + its standard way. :param status: HTTP Status code to respond to the client with :param msg: Message to send back to the client """ def __init__(self, status: int, msg: str): - """ - Construct a new COARNotifyServerError with the given status code and message + """Construct a new COARNotifyServerError with the given status code and message. :param status: HTTP Status code to respond to the client with :param msg: Message to send back to the client @@ -94,22 +90,21 @@ def __init__(self, status: int, msg: str): @property def status(self) -> int: - """HTTP status code for the error""" + """HTTP status code for the error.""" return self._status @property def message(self) -> str: - """The error message""" + """The error message.""" return self._msg class COARNotifyServer: - """ - The main entrypoint to the COAR Notify server implementation. + """The main entrypoint to the COAR Notify server implementation. The web layer of your application should pass the json/raw payload of any incoming notification to the - :py:meth:`receive` method, which will parse the payload and pass it to the :py:meth:`COARNotifyServiceBinding.notification_received` - method of your service implementation + :py:meth:`receive` method, which will parse the payload and pass it to the + :py:meth:`COARNotifyServiceBinding.notification_received` method of your service implementation This object should be constructed with your service implementation passed to it, for example @@ -126,20 +121,17 @@ class COARNotifyServer: """ def __init__(self, service_impl: COARNotifyServiceBinding): - """ - Construct a new COARNotifyServer with the given service implementation - :param service_impl: Your service implementation - """ + """Construct a new COARNotifyServer with the given service implementation :param service_impl: Your service + implementation.""" self._service_impl = service_impl def receive(self, raw: Union[dict, str], validate: bool = True) -> COARNotifyReceipt: - """ - Receive an incoming notification as JSON, parse and validate (optional) and then pass to the - service implementation + """Receive an incoming notification as JSON, parse and validate (optional) and then pass to the service + implementation. :param raw: The JSON representation of the data, either as a string or a dictionary - :param validate: Whether to validate the notification before passing to the service implementation - :return: The COARNotifyReceipt response from the service implementation + :param validate: Whether to validate the notification before passing to the service implementation + :return: The COARNotifyReceipt response from the service implementation """ if isinstance(raw, str): raw = json.loads(raw) diff --git a/coarnotify/test/__init__.py b/coarnotify/test/__init__.py index e3c0b32..9b3f6cc 100644 --- a/coarnotify/test/__init__.py +++ b/coarnotify/test/__init__.py @@ -1,15 +1,13 @@ -""" -This module contains all the test infrastructure. +"""This module contains all the test infrastructure. For the purposes of conciseness, the test code is mostly excluded from the auto-documentation. The test code is structured as follows: -- `fixtures` contains test fixtures and factories for generating fixtures -- `integration` contains integration tests for the library. These depend on a running server, and there is a test server provided -- `mocks` contains mock objects for testing -- `server` contains the test server, and documentation for this is generated and available on the docsite -- `unit` contains unit tests for the library +- `fixtures` contains test fixtures and factories for generating fixtures - `integration` contains integration tests for +the library. These depend on a running server, and there is a test server provided - `mocks` contains mock objects +for testing - `server` contains the test server, and documentation for this is generated and available on the docsite - +`unit` contains unit tests for the library For information on running the integration and unit tests, see :doc:`/dev`. -""" \ No newline at end of file +""" diff --git a/coarnotify/test/fixtures/__init__.py b/coarnotify/test/fixtures/__init__.py index 9dea1ab..431d791 100644 --- a/coarnotify/test/fixtures/__init__.py +++ b/coarnotify/test/fixtures/__init__.py @@ -1,14 +1,16 @@ -from coarnotify.test.fixtures.base_fixture_factory import BaseFixtureFactory -from coarnotify.test.fixtures.accept import AcceptFixtureFactory -from coarnotify.test.fixtures.announce_endorsement import AnnounceEndorsementFixtureFactory -from coarnotify.test.fixtures.announce_relationship import AnnounceRelationshipFixtureFactory -from coarnotify.test.fixtures.announce_review import AnnounceReviewFixtureFactory -from coarnotify.test.fixtures.announce_service_result import AnnounceServiceResultFixtureFactory -from coarnotify.test.fixtures.reject import RejectFixtureFactory -from coarnotify.test.fixtures.request_endorsement import RequestEndorsementFixtureFactory -from coarnotify.test.fixtures.uris import URIFixtureFactory -from coarnotify.test.fixtures.request_review import RequestReviewFixtureFactory -from coarnotify.test.fixtures.tentatively_accept import TentativelyAcceptFixtureFactory -from coarnotify.test.fixtures.tentatively_reject import TentativelyRejectFixtureFactory -from coarnotify.test.fixtures.unprocessable_notification import UnprocessableNotificationFixtureFactory -from coarnotify.test.fixtures.undo_offer import UndoOfferFixtureFactory \ No newline at end of file +"""Initialisation for the fixtures module.""" + +from coarnotify.test.fixtures.base_fixture_factory import BaseFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.accept import AcceptFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.announce_endorsement import AnnounceEndorsementFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.announce_relationship import AnnounceRelationshipFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.announce_review import AnnounceReviewFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.announce_service_result import AnnounceServiceResultFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.reject import RejectFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.request_endorsement import RequestEndorsementFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.uris import URIFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.request_review import RequestReviewFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.tentatively_accept import TentativelyAcceptFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.tentatively_reject import TentativelyRejectFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.unprocessable_notification import UnprocessableNotificationFixtureFactory # noqa: F401 +from coarnotify.test.fixtures.undo_offer import UndoOfferFixtureFactory # noqa: F401 diff --git a/coarnotify/test/fixtures/accept.py b/coarnotify/test/fixtures/accept.py index 0f95cb3..0f6f3b6 100644 --- a/coarnotify/test/fixtures/accept.py +++ b/coarnotify/test/fixtures/accept.py @@ -1,39 +1,34 @@ +"""Accept fixture.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory class AcceptFixtureFactory(BaseFixtureFactory): + """Accept fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(ACCEPT) return ACCEPT @classmethod def invalid(cls): + """Return an invalid source.""" source = cls.source() cls._base_invalid(source) return source ACCEPT = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://generic-service-1.com", - "name": "Generic Service", - "type": "Service" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://generic-service-1.com", "name": "Generic Service", "type": "Service"}, "id": "urn:uuid:4fb3af44-d4f8-4226-9475-2d09c2d8d9e0", "inReplyTo": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { - "actor": { - "id": "https://orcid.org/0000-0002-1825-0097", - "name": "Josiah Carberry", - "type": "Person" - }, + "actor": {"id": "https://orcid.org/0000-0002-1825-0097", "name": "Josiah Carberry", "type": "Person"}, "id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { "id": "https://research-organisation.org/repository/preprint/201203/421/", @@ -41,37 +36,31 @@ def invalid(cls): "ietf:item": { "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", "mediaType": "application/pdf", - "type": [ - "Page", - "sorg:AboutPage" - ] + "type": ["Page", "sorg:AboutPage"], }, - "type": "sorg:AboutPage" + "type": "sorg:AboutPage", }, "origin": { "id": "https://research-organisation.org/repository", "inbox": "https://research-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://overlay-journal.com/system", "inbox": "https://overlay-journal.com/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Offer", - "coar-notify:EndorsementAction" - ] + "type": ["Offer", "coar-notify:EndorsementAction"], }, "origin": { "id": "https://generic-service-1.com/origin-system", "inbox": "https://generic-service-1.com/origin-system/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://generic-service-2.com/target-system", "inbox": "https://generic-service-2.com/target-system/inbox/", - "type": "Service" + "type": "Service", }, - "type": "Accept" + "type": "Accept", } diff --git a/coarnotify/test/fixtures/announce_endorsement.py b/coarnotify/test/fixtures/announce_endorsement.py index e195726..a02c625 100644 --- a/coarnotify/test/fixtures/announce_endorsement.py +++ b/coarnotify/test/fixtures/announce_endorsement.py @@ -1,16 +1,22 @@ +"""Announce Endorsement fixture factory.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory class AnnounceEndorsementFixtureFactory(BaseFixtureFactory): + """Announce Endorsement fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(ANNOUNCE_ENDORSEMENT) return ANNOUNCE_ENDORSEMENT @classmethod def invalid(cls): + """Invalid source.""" source = cls.source() cls._base_invalid(source) @@ -22,40 +28,25 @@ def invalid(cls): ANNOUNCE_ENDORSEMENT = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://overlay-journal.com", - "name": "Overlay Journal", - "type": "Service" - }, - "context": { - "id": "https://research-organisation.org/repository/preprint/201203/421/" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://overlay-journal.com", "name": "Overlay Journal", "type": "Service"}, + "context": {"id": "https://research-organisation.org/repository/preprint/201203/421/"}, "id": "urn:uuid:94ecae35-dcfd-4182-8550-22c7164fe23f", "inReplyTo": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { "id": "https://overlay-journal.com/articles/00001/", "ietf:cite-as": "https://overlay-journal.com/articles/00001/", - "type": [ - "Page", - "sorg:WebPage" - ] + "type": ["Page", "sorg:WebPage"], }, "origin": { "id": "https://overlay-journal.com/system", "inbox": "https://overlay-journal.com/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://research-organisation.org/repository", "inbox": "https://research-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Announce", - "coar-notify:EndorsementAction" - ] -} \ No newline at end of file + "type": ["Announce", "coar-notify:EndorsementAction"], +} diff --git a/coarnotify/test/fixtures/announce_relationship.py b/coarnotify/test/fixtures/announce_relationship.py index 9ce9d40..302d97f 100644 --- a/coarnotify/test/fixtures/announce_relationship.py +++ b/coarnotify/test/fixtures/announce_relationship.py @@ -1,15 +1,22 @@ +"""Announce Relationship fixture factory.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory + class AnnounceRelationshipFixtureFactory(BaseFixtureFactory): + """Announce Relationship fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(ANNOUNCE_RELATIONSHIP) return ANNOUNCE_RELATIONSHIP @classmethod def invalid(cls): + """Invalid source.""" source = cls.source() cls._base_invalid(source) cls._actor_invalid(source) @@ -19,30 +26,17 @@ def invalid(cls): ANNOUNCE_RELATIONSHIP = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://research-organisation.org", - "name": "Research Organisation", - "type": "Organization" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://research-organisation.org", "name": "Research Organisation", "type": "Organization"}, "context": { "id": "https://another-research-organisation.org/repository/datasets/item/201203421/", "ietf:cite-as": "https://doi.org/10.5555/999555666", "ietf:item": { "id": "https://another-research-organisation.org/repository/datasets/item/201203421/data_archive.zip", "mediaType": "application/zip", - "type": [ - "Object", - "sorg:Dataset" - ] + "type": ["Object", "sorg:Dataset"], }, - "type": [ - "Page", - "sorg:AboutPage" - ] + "type": ["Page", "sorg:AboutPage"], }, "id": "urn:uuid:94ecae35-dcfd-4182-8550-22c7164fe23f", "object": { @@ -50,20 +44,17 @@ def invalid(cls): "as:relationship": "http://purl.org/vocab/frbr/core#supplement", "as:subject": "https://research-organisation.org/repository/item/201203/421/", "id": "urn:uuid:74FFB356-0632-44D9-B176-888DA85758DC", - "type": "Relationship" + "type": "Relationship", }, "origin": { "id": "https://research-organisation.org/repository", "inbox": "https://research-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://another-research-organisation.org/repository", "inbox": "https://another-research-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Announce", - "coar-notify:RelationshipAction" - ] + "type": ["Announce", "coar-notify:RelationshipAction"], } diff --git a/coarnotify/test/fixtures/announce_review.py b/coarnotify/test/fixtures/announce_review.py index 019eaad..6fdd399 100644 --- a/coarnotify/test/fixtures/announce_review.py +++ b/coarnotify/test/fixtures/announce_review.py @@ -1,52 +1,41 @@ +"""Fixtures for Announce Review Activity Streams 2.0 objects.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory class AnnounceReviewFixtureFactory(BaseFixtureFactory): + """Announce Review fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(ANNOUNCE_REVIEW) return ANNOUNCE_REVIEW ANNOUNCE_REVIEW = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://review-service.com", - "name": "Review Service", - "type": "Service" - }, - "context": { - "id": "https://research-organisation.org/repository/preprint/201203/421/" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://review-service.com", "name": "Review Service", "type": "Service"}, + "context": {"id": "https://research-organisation.org/repository/preprint/201203/421/"}, "id": "urn:uuid:94ecae35-dcfd-4182-8550-22c7164fe23f", "inReplyTo": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { "id": "https://review-service.com/review/geo/202103/0021", "ietf:cite-as": "https://doi.org/10.3214/987654", - "type": [ - "Page", - "sorg:Review" - ] + "type": ["Page", "sorg:Review"], }, "origin": { "id": "https://review-service.com/system", "inbox": "https://review-service.com/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://generic-service.com/system", "inbox": "https://generic-service.com/system/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Announce", - "coar-notify:ReviewAction" - ] + "type": ["Announce", "coar-notify:ReviewAction"], } - diff --git a/coarnotify/test/fixtures/announce_service_result.py b/coarnotify/test/fixtures/announce_service_result.py index 63c4b50..cec65e6 100644 --- a/coarnotify/test/fixtures/announce_service_result.py +++ b/coarnotify/test/fixtures/announce_service_result.py @@ -1,17 +1,23 @@ +"""Announce Service Result fixture factory.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory class AnnounceServiceResultFixtureFactory(BaseFixtureFactory): + """Announce Service Result fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(ANNOUNCE_SERVICE_RESULT) return ANNOUNCE_SERVICE_RESULT @classmethod def invalid(cls): + """Invalid source.""" source = cls.source() cls._base_invalid(source) cls._actor_invalid(source) @@ -19,39 +25,23 @@ def invalid(cls): cls._context_invalid(source) return source + ANNOUNCE_SERVICE_RESULT = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://overlay-journal.com", - "name": "Overlay Journal", - "type": "Service" - }, - "context": { - "id": "https://research-organisation.org/repository/preprint/201203/421/" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://overlay-journal.com", "name": "Overlay Journal", "type": "Service"}, + "context": {"id": "https://research-organisation.org/repository/preprint/201203/421/"}, "id": "urn:uuid:94ecae35-dcfd-4182-8550-22c7164fe23f", "inReplyTo": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", - "object": { - "id": "https://overlay-journal.com/information-page", - "type": [ - "Page", - "sorg:WebPage" - ] - }, + "object": {"id": "https://overlay-journal.com/information-page", "type": ["Page", "sorg:WebPage"]}, "origin": { "id": "https://overlay-journal.com/system", "inbox": "https://overlay-journal.com/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://generic-service.com/system", "inbox": "https://generic-service.com/system/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Announce" - ] -} \ No newline at end of file + "type": ["Announce"], +} diff --git a/coarnotify/test/fixtures/base_fixture_factory.py b/coarnotify/test/fixtures/base_fixture_factory.py index 9f02d45..684f204 100644 --- a/coarnotify/test/fixtures/base_fixture_factory.py +++ b/coarnotify/test/fixtures/base_fixture_factory.py @@ -1,21 +1,30 @@ +"""Base class for fixture factories.""" + + class BaseFixtureFactory: + """Base class for fixture factories.""" + @classmethod def source(cls, copy=True): + """Return the source.""" raise NotImplementedError() @classmethod def invalid(cls): + """Invalid source.""" source = cls.source() cls._base_invalid(source) return source @classmethod def expected_value(cls, path): - source = cls.source(copy=False) # we're only reading the value, so no need to clone it + """Return the expected value.""" + source = cls.source(copy=False) # we're only reading the value, so no need to clone it return cls._value_from_dict(path, source) @classmethod def _base_invalid(cls, source): + """Set the base invalid values.""" source["id"] = "not a uri" source["inReplyTo"] = "not a uri" source["origin"]["id"] = "urn:uuid:4fb3af44-d4f8-4226-9475-2d09c2d8d9e0" @@ -29,18 +38,21 @@ def _base_invalid(cls, source): @classmethod def _actor_invalid(self, source): + """Set the actor invalid values.""" source["actor"]["id"] = "not a uri" source["actor"]["type"] = "NotAValidType" return source @classmethod def _object_invalid(self, source): + """Set the object invalid values.""" source["object"]["id"] = "not a uri" source["object"]["cite_as"] = "urn:uuid:4fb3af44-d4f8-4226-9475-2d09c2d8d9e0" return source @classmethod def _context_invalid(self, source): + """Set the context invalid values.""" source["context"]["id"] = "not a uri" source["context"]["type"] = "NotAValidType" source["context"]["cite_as"] = "urn:uuid:4fb3af44-d4f8-4226-9475-2d09c2d8d9e0" @@ -48,8 +60,9 @@ def _context_invalid(self, source): @classmethod def _value_from_dict(cls, path, dictionary): + """Return the value from the dictionary.""" bits = path.split(".") node = dictionary for bit in bits: node = node[bit] - return node \ No newline at end of file + return node diff --git a/coarnotify/test/fixtures/notify.py b/coarnotify/test/fixtures/notify.py index 827ed69..49ce576 100644 --- a/coarnotify/test/fixtures/notify.py +++ b/coarnotify/test/fixtures/notify.py @@ -1,55 +1,53 @@ +"""Notify Fixture Factory.""" + from copy import deepcopy from coarnotify.core.notify import NotifyObject, NotifyService class NotifyFixtureFactory: + """Notify Fixture Factory.""" + @classmethod def source(cls): + """Return a copy of the source.""" return deepcopy(BASE_NOTIFY) @classmethod def target(cls): + """Return a copy of the target.""" return NotifyService(deepcopy(BASE_NOTIFY["target"])) @classmethod def origin(cls): + """Return a copy of the origin.""" return NotifyService(deepcopy(BASE_NOTIFY["origin"])) @classmethod def object(cls): + """Return a copy of the object.""" return NotifyObject(deepcopy(BASE_NOTIFY["object"])) BASE_NOTIFY = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://purl.org/coar/notify" - ], + "@context": ["https://www.w3.org/ns/activitystreams", "https://purl.org/coar/notify"], "id": "urn:uuid:94ecae35-dcfd-4182-8550-22c7164fe23f", "type": "Object", "origin": { "id": "https://overlay-journal.com/system", "inbox": "https://overlay-journal.com/inbox/", - "type": "Service" + "type": "Service", }, "object": { "id": "https://overlay-journal.com/articles/00001/", "ietf:cite-as": "https://overlay-journal.com/articles/00001/", - "type": [ - "Page", - "sorg:WebPage" - ] + "type": ["Page", "sorg:WebPage"], }, "target": { "id": "https://research-organisation.org/repository", "inbox": "https://research-organisation.org/inbox/", - "type": "Service" - }, - "actor": { - "id": "https://overlay-journal.com", - "name": "Overlay Journal", - "type": "Service" + "type": "Service", }, + "actor": {"id": "https://overlay-journal.com", "name": "Overlay Journal", "type": "Service"}, "inReplyTo": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "context": { "id": "https://research-organisation.org/repository/preprint/201203/421/", @@ -57,11 +55,8 @@ def object(cls): "ietf:item": { "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", "mediaType": "application/pdf", - "type": [ - "Article", - "sorg:ScholarlyArticle" - ] + "type": ["Article", "sorg:ScholarlyArticle"], }, - "type": "sorg:AboutPage" - } -} \ No newline at end of file + "type": "sorg:AboutPage", + }, +} diff --git a/coarnotify/test/fixtures/reject.py b/coarnotify/test/fixtures/reject.py index 579fd13..2929144 100644 --- a/coarnotify/test/fixtures/reject.py +++ b/coarnotify/test/fixtures/reject.py @@ -1,34 +1,28 @@ +"""Reject fixture factory.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory class RejectFixtureFactory(BaseFixtureFactory): + """Reject fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(REJECT) return REJECT REJECT = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://generic-service-1.com", - "name": "Generic Service", - "type": "Service" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://generic-service-1.com", "name": "Generic Service", "type": "Service"}, "id": "urn:uuid:668f26e0-2c8d-4117-a0d2-ee713523bcb1", "inReplyTo": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { - "actor": { - "id": "https://orcid.org/0000-0002-1825-0097", - "name": "Josiah Carberry", - "type": "Person" - }, + "actor": {"id": "https://orcid.org/0000-0002-1825-0097", "name": "Josiah Carberry", "type": "Person"}, "id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { "id": "https://research-organisation.org/repository/preprint/201203/421/", @@ -36,38 +30,32 @@ def source(cls, copy=True): "ietf:item": { "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", "mediaType": "application/pdf", - "type": [ - "Page", - "sorg:AboutPage" - ] + "type": ["Page", "sorg:AboutPage"], }, - "type": "sorg:AboutPage" + "type": "sorg:AboutPage", }, "origin": { "id": "https://research-organisation.org/repository", "inbox": "https://research-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://overlay-journal.com/system", "inbox": "https://overlay-journal.com/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Offer", - "coar-notify:EndorsementAction" - ] + "type": ["Offer", "coar-notify:EndorsementAction"], }, "origin": { "id": "https://generic-service.com/system", "inbox": "https://generic-service.com/system/inbox/", - "type": "Service" + "type": "Service", }, "summary": "The offer has been rejected because...", "target": { "id": "https://some-organisation.org", "inbox": "https://some-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, - "type": "Reject" + "type": "Reject", } diff --git a/coarnotify/test/fixtures/request_endorsement.py b/coarnotify/test/fixtures/request_endorsement.py index 0f3bdf1..9838b7c 100644 --- a/coarnotify/test/fixtures/request_endorsement.py +++ b/coarnotify/test/fixtures/request_endorsement.py @@ -1,17 +1,23 @@ +"""Fixtures for Request Endorsement tests.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory class RequestEndorsementFixtureFactory(BaseFixtureFactory): + """Request Endorsement fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(REQUEST_ENDORSEMENT) return REQUEST_ENDORSEMENT @classmethod def invalid(cls): + """Invalid source.""" source = cls.source() cls._base_invalid(source) cls._actor_invalid(source) @@ -20,15 +26,8 @@ def invalid(cls): REQUEST_ENDORSEMENT = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://orcid.org/0000-0002-1825-0097", - "name": "Josiah Carberry", - "type": "Person" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://orcid.org/0000-0002-1825-0097", "name": "Josiah Carberry", "type": "Person"}, "id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { "id": "https://research-organisation.org/repository/preprint/201203/421/", @@ -36,28 +35,19 @@ def invalid(cls): "ietf:item": { "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", "mediaType": "application/pdf", - "type": [ - "Article", - "sorg:ScholarlyArticle" - ] + "type": ["Article", "sorg:ScholarlyArticle"], }, - "type": [ - "Page", - "sorg:AboutPage" - ] + "type": ["Page", "sorg:AboutPage"], }, "origin": { "id": "https://research-organisation.org/repository", "inbox": "https://research-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://overlay-journal.com/system", "inbox": "https://overlay-journal.com/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Offer", - "coar-notify:EndorsementAction" - ] + "type": ["Offer", "coar-notify:EndorsementAction"], } diff --git a/coarnotify/test/fixtures/request_review.py b/coarnotify/test/fixtures/request_review.py index 60ba407..d6be68e 100644 --- a/coarnotify/test/fixtures/request_review.py +++ b/coarnotify/test/fixtures/request_review.py @@ -1,16 +1,22 @@ +"""Fixtures for RequestReview tests.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory class RequestReviewFixtureFactory(BaseFixtureFactory): + """Request Review fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(REQUEST_REVIEW) return REQUEST_REVIEW @classmethod def invalid(cls): + """Invalid source.""" source = cls.source() cls._base_invalid(source) cls._actor_invalid(source) @@ -19,15 +25,8 @@ def invalid(cls): REQUEST_REVIEW = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://orcid.org/0000-0002-1825-0097", - "name": "Josiah Carberry", - "type": "Person" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://orcid.org/0000-0002-1825-0097", "name": "Josiah Carberry", "type": "Person"}, "id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { "id": "https://research-organisation.org/repository/preprint/201203/421/", @@ -35,28 +34,19 @@ def invalid(cls): "ietf:item": { "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", "mediaType": "application/pdf", - "type": [ - "Article", - "sorg:ScholarlyArticle" - ] + "type": ["Article", "sorg:ScholarlyArticle"], }, - "type": [ - "Page", - "sorg:AboutPage" - ] + "type": ["Page", "sorg:AboutPage"], }, "origin": { "id": "https://research-organisation.org/repository", "inbox": "https://research-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://review-service.com/system", "inbox": "https://review-service.com/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Offer", - "coar-notify:ReviewAction" - ] -} \ No newline at end of file + "type": ["Offer", "coar-notify:ReviewAction"], +} diff --git a/coarnotify/test/fixtures/tentatively_accept.py b/coarnotify/test/fixtures/tentatively_accept.py index 79a2f25..9efc296 100644 --- a/coarnotify/test/fixtures/tentatively_accept.py +++ b/coarnotify/test/fixtures/tentatively_accept.py @@ -1,16 +1,22 @@ +"""Fixtures for TentativelyAccept activity.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory class TentativelyAcceptFixtureFactory(BaseFixtureFactory): + """TentativelyAccept fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(TENTATIVELY_ACCEPT) return TENTATIVELY_ACCEPT @classmethod def invalid(cls): + """Invalid source.""" source = cls.source() cls._base_invalid(source) cls._actor_invalid(source) @@ -25,24 +31,14 @@ def invalid(cls): # node = node[bit] # return node + TENTATIVELY_ACCEPT = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://generic-service-1.com", - "name": "Generic Service", - "type": "Service" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://generic-service-1.com", "name": "Generic Service", "type": "Service"}, "id": "urn:uuid:4fb3af44-d4f8-4226-9475-2d09c2d8d9e0", "inReplyTo": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { - "actor": { - "id": "https://orcid.org/0000-0002-1825-0097", - "name": "Josiah Carberry", - "type": "Person" - }, + "actor": {"id": "https://orcid.org/0000-0002-1825-0097", "name": "Josiah Carberry", "type": "Person"}, "id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { "id": "https://research-organisation.org/repository/preprint/201203/421/", @@ -50,38 +46,32 @@ def invalid(cls): "ietf:item": { "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", "mediaType": "application/pdf", - "type": [ - "Page", - "sorg:AboutPage" - ] + "type": ["Page", "sorg:AboutPage"], }, - "type": "sorg:AboutPage" + "type": "sorg:AboutPage", }, "origin": { "id": "https://research-organisation.org/repository", "inbox": "https://research-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://overlay-journal.com/system", "inbox": "https://overlay-journal.com/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Offer", - "coar-notify:EndorsementAction" - ] + "type": ["Offer", "coar-notify:EndorsementAction"], }, "origin": { "id": "https://generic-service.com/system", "inbox": "https://generic-service.com/system/inbox/", - "type": "Service" + "type": "Service", }, "summary": "The offer has been tentatively accepted, subject to further review.", "target": { "id": "https://some-organisation.org", "inbox": "https://some-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, - "type": "TentativeAccept" + "type": "TentativeAccept", } diff --git a/coarnotify/test/fixtures/tentatively_reject.py b/coarnotify/test/fixtures/tentatively_reject.py index c40937e..b8fa0e1 100644 --- a/coarnotify/test/fixtures/tentatively_reject.py +++ b/coarnotify/test/fixtures/tentatively_reject.py @@ -1,40 +1,36 @@ +"""Fixtures for TentativelyReject activity.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory class TentativelyRejectFixtureFactory(BaseFixtureFactory): + """TentativelyReject fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(TENTATIVELY_REJECT) return TENTATIVELY_REJECT @classmethod def invalid(cls): + """Invalid source.""" source = cls.source() cls._base_invalid(source) cls._actor_invalid(source) cls._object_invalid(source) return source + TENTATIVELY_REJECT = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://generic-service-1.com", - "name": "Generic Service", - "type": "Service" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://generic-service-1.com", "name": "Generic Service", "type": "Service"}, "id": "urn:uuid:b6c7c187-4df2-45c6-8b03-b516b134224b", "inReplyTo": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { - "actor": { - "id": "https://orcid.org/0000-0002-1825-0097", - "name": "Josiah Carberry", - "type": "Person" - }, + "actor": {"id": "https://orcid.org/0000-0002-1825-0097", "name": "Josiah Carberry", "type": "Person"}, "id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { "id": "https://research-organisation.org/repository/preprint/201203/421/", @@ -42,38 +38,32 @@ def invalid(cls): "ietf:item": { "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", "mediaType": "application/pdf", - "type": [ - "Page", - "sorg:AboutPage" - ] + "type": ["Page", "sorg:AboutPage"], }, - "type": "sorg:AboutPage" + "type": "sorg:AboutPage", }, "origin": { "id": "https://research-organisation.org/repository", "inbox": "https://research-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://overlay-journal.com/system", "inbox": "https://overlay-journal.com/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Offer", - "coar-notify:EndorsementAction" - ] + "type": ["Offer", "coar-notify:EndorsementAction"], }, "origin": { "id": "https://generic-service.com/system", "inbox": "https://generic-service.com/system/inbox/", - "type": "Service" + "type": "Service", }, "summary": "The offer has been tentatively rejected, subject to further review.", "target": { "id": "https://some-organisation.org", "inbox": "https://some-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, - "type": "TentativeReject" -} \ No newline at end of file + "type": "TentativeReject", +} diff --git a/coarnotify/test/fixtures/undo_offer.py b/coarnotify/test/fixtures/undo_offer.py index 3b94883..e9a396e 100644 --- a/coarnotify/test/fixtures/undo_offer.py +++ b/coarnotify/test/fixtures/undo_offer.py @@ -1,40 +1,36 @@ +"""Fixtures for Undo Offer activities.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory class UndoOfferFixtureFactory(BaseFixtureFactory): + """Undo Offer fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(UNDO_OFFER) return UNDO_OFFER @classmethod def invalid(cls): + """Invalid .""" source = cls.source() cls._base_invalid(source) cls._actor_invalid(source) return source + UNDO_OFFER = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://some-organisation.org", - "name": "Some Organisation", - "type": "Organization" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://some-organisation.org", "name": "Some Organisation", "type": "Organization"}, "id": "urn:uuid:46956915-e3fe-4528-8789-1d325a356e4f", "inReplyTo": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { - "actor": { - "id": "https://orcid.org/0000-0002-1825-0097", - "name": "Josiah Carberry", - "type": "Person" - }, + "actor": {"id": "https://orcid.org/0000-0002-1825-0097", "name": "Josiah Carberry", "type": "Person"}, "id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", "object": { "id": "https://research-organisation.org/repository/preprint/201203/421/", @@ -42,38 +38,32 @@ def invalid(cls): "ietf:item": { "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", "mediaType": "application/pdf", - "type": [ - "Page", - "sorg:AboutPage" - ] + "type": ["Page", "sorg:AboutPage"], }, - "type": "sorg:AboutPage" + "type": "sorg:AboutPage", }, "origin": { "id": "https://research-organisation.org/repository", "inbox": "https://research-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, "target": { "id": "https://overlay-journal.com/system", "inbox": "https://overlay-journal.com/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Offer", - "coar-notify:EndorsementAction" - ] + "type": ["Offer", "coar-notify:EndorsementAction"], }, "origin": { "id": "https://some-organisation.org", "inbox": "https://some-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, "summary": "The offer has been withdrawn because...", "target": { "id": "https://generic-service.com/system", "inbox": "https://generic-service.com/system/inbox/", - "type": "Service" + "type": "Service", }, - "type": "Undo" -} \ No newline at end of file + "type": "Undo", +} diff --git a/coarnotify/test/fixtures/unprocessable_notification.py b/coarnotify/test/fixtures/unprocessable_notification.py index d4ac856..6f0f7de 100644 --- a/coarnotify/test/fixtures/unprocessable_notification.py +++ b/coarnotify/test/fixtures/unprocessable_notification.py @@ -1,17 +1,23 @@ +"""Fixtures for UnprocessableNotification tests.""" + from copy import deepcopy from coarnotify.test.fixtures import BaseFixtureFactory class UnprocessableNotificationFixtureFactory(BaseFixtureFactory): + """UnprocessableNotification fixture factory.""" + @classmethod def source(cls, copy=True): + """Return the source.""" if copy: return deepcopy(UNPROCESSABLE_NOTIFICATION) return UNPROCESSABLE_NOTIFICATION @classmethod def invalid(cls): + """Invalid source.""" source = cls.source() cls._base_invalid(source) del source["summary"] @@ -19,33 +25,21 @@ def invalid(cls): UNPROCESSABLE_NOTIFICATION = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net" - ], - "actor": { - "id": "https://generic-service-1.com", - "name": "Generic Service", - "type": "Service" - }, + "@context": ["https://www.w3.org/ns/activitystreams", "https://coar-notify.net"], + "actor": {"id": "https://generic-service-1.com", "name": "Generic Service", "type": "Service"}, "id": "urn:uuid:49dae4d9-4a16-4dcf-8ae0-a0cef139254c", "inReplyTo": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", - "object": { - "id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd" - }, + "object": {"id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd"}, "origin": { "id": "https://some-organisation.org", "inbox": "https://some-organisation.org/inbox/", - "type": "Service" + "type": "Service", }, "summary": "Unable to process URL: http://www.example.com/broken-url - returns HTTP error 404", "target": { "id": "https://generic-service.com/system", "inbox": "https://generic-service.com/system/inbox/", - "type": "Service" + "type": "Service", }, - "type": [ - "Flag", - "coar-notify:UnprocessableNotification" - ] -} \ No newline at end of file + "type": ["Flag", "coar-notify:UnprocessableNotification"], +} diff --git a/coarnotify/test/fixtures/uris.py b/coarnotify/test/fixtures/uris.py index 866a981..e2ef075 100644 --- a/coarnotify/test/fixtures/uris.py +++ b/coarnotify/test/fixtures/uris.py @@ -1,9 +1,14 @@ +"""URI fixtures for testing.""" + from copy import deepcopy class URIFixtureFactory: + """URI fixture factory.""" + @classmethod def generate(cls, schemes=None, hosts=None, ports=None, paths=None, queries=None, fragments=None): + """Generate URIs.""" schemes = schemes if schemes is not None else deepcopy(DEFAULT_SCHEMES) hosts = hosts if hosts is not None else deepcopy(DEFAULT_HOSTS) ports = ports if ports is not None else deepcopy(DEFAULT_PORTS) @@ -24,6 +29,7 @@ def generate(cls, schemes=None, hosts=None, ports=None, paths=None, queries=None @classmethod def generate_uri(cls, scheme, host, port, path, query, fragment): + """Generate a URI.""" # account for port numbers and IPv6 addresses if host is not None and ":" in host and port is not None and port != "": host = f"[{host}]" @@ -36,30 +42,13 @@ def generate_uri(cls, scheme, host, port, path, query, fragment): return url -DEFAULT_SCHEMES = [ - "http", - "https" -] +DEFAULT_SCHEMES = ["http", "https"] -DEFAULT_HOSTS = [ - "example.com", - "localhost", - "192.168.0.1", - "2001:db8::7" -] +DEFAULT_HOSTS = ["example.com", "localhost", "192.168.0.1", "2001:db8::7"] -DEFAULT_PORTS = [ - "", - "80", - "8080" -] +DEFAULT_PORTS = ["", "80", "8080"] -DEFAULT_PATHS = [ - "", - "/", - "/path", - "/path/to/file" -] +DEFAULT_PATHS = ["", "/", "/path", "/path/to/file"] DEFAULT_QUERIES = [ "", @@ -67,7 +56,4 @@ def generate_uri(cls, scheme, host, port, path, query, fragment): "query=string&o=1", ] -DEFAULT_FRAGMENTS = [ - "", - "fragment" -] \ No newline at end of file +DEFAULT_FRAGMENTS = ["", "fragment"] diff --git a/coarnotify/test/integration/__init__.py b/coarnotify/test/integration/__init__.py index e69de29..97e95c7 100644 --- a/coarnotify/test/integration/__init__.py +++ b/coarnotify/test/integration/__init__.py @@ -0,0 +1 @@ +"""Integration test module initialization.""" diff --git a/coarnotify/test/integration/test_client.py b/coarnotify/test/integration/test_client.py index d4bebfe..5c0e6af 100644 --- a/coarnotify/test/integration/test_client.py +++ b/coarnotify/test/integration/test_client.py @@ -1,3 +1,5 @@ +"""Test the COARNotifyClient class.""" + from unittest import TestCase from coarnotify.client import COARNotifyClient @@ -13,7 +15,7 @@ TentativelyAccept, TentativelyReject, UnprocessableNotification, - UndoOffer + UndoOffer, ) from coarnotify.test.fixtures import ( @@ -28,14 +30,17 @@ TentativelyAcceptFixtureFactory, TentativelyRejectFixtureFactory, UnprocessableNotificationFixtureFactory, - UndoOfferFixtureFactory + UndoOfferFixtureFactory, ) INBOX = "http://localhost:5005/inbox" class TestClient(TestCase): + """Test the COARNotifyClient class.""" + def test_01_accept(self): + """Accept a notification.""" client = COARNotifyClient(INBOX) source = AcceptFixtureFactory.source() acc = Accept(source) @@ -45,6 +50,7 @@ def test_01_accept(self): print(resp.location) def test_02_announce_endorsement(self): + """Announce an endorsement.""" client = COARNotifyClient(INBOX) source = AnnounceEndorsementFixtureFactory.source() ae = AnnounceEndorsement(source) @@ -54,6 +60,7 @@ def test_02_announce_endorsement(self): print(resp.location) def test_04_announce_relationship(self): + """Announce a relationship.""" client = COARNotifyClient(INBOX) source = AnnounceRelationshipFixtureFactory.source() ae = AnnounceRelationship(source) @@ -63,6 +70,7 @@ def test_04_announce_relationship(self): print(resp.location) def test_05_announce_review(self): + """Announce a review.""" client = COARNotifyClient(INBOX) source = AnnounceReviewFixtureFactory.source() ae = AnnounceReview(source) @@ -72,6 +80,7 @@ def test_05_announce_review(self): print(resp.location) def test_06_announce_service_result(self): + """Announce a service result.""" client = COARNotifyClient(INBOX) source = AnnounceServiceResultFixtureFactory.source() ae = AnnounceServiceResult(source) @@ -90,6 +99,7 @@ def test_07_reject(self): print(resp.location) def test_08_request_endorsement(self): + """Request an endorsement.""" client = COARNotifyClient(INBOX) source = RequestEndorsementFixtureFactory.source() ae = RequestEndorsement(source) @@ -99,6 +109,7 @@ def test_08_request_endorsement(self): print(resp.location) def test_09_request_review(self): + """Request a review.""" client = COARNotifyClient(INBOX) source = RequestReviewFixtureFactory.source() ae = RequestReview(source) @@ -108,6 +119,7 @@ def test_09_request_review(self): print(resp.location) def test_10_tentatively_accept(self): + """Tentatively accept a notification.""" client = COARNotifyClient(INBOX) source = TentativelyAcceptFixtureFactory.source() ae = TentativelyAccept(source) @@ -117,6 +129,7 @@ def test_10_tentatively_accept(self): print(resp.location) def test_11_tentatively_reject(self): + """Tentatively reject a notification.""" client = COARNotifyClient(INBOX) source = TentativelyRejectFixtureFactory.source() ae = TentativelyReject(source) @@ -126,6 +139,7 @@ def test_11_tentatively_reject(self): print(resp.location) def test_12_unprocessable_notification(self): + """Unprocessable notification.""" client = COARNotifyClient(INBOX) source = UnprocessableNotificationFixtureFactory.source() ae = UnprocessableNotification(source) @@ -135,6 +149,7 @@ def test_12_unprocessable_notification(self): print(resp.location) def test_13_undo_offer(self): + """Undo an offer.""" client = COARNotifyClient(INBOX) source = UndoOfferFixtureFactory.source() ae = UndoOffer(source) diff --git a/coarnotify/test/mocks/__init__.py b/coarnotify/test/mocks/__init__.py index e69de29..f02426c 100644 --- a/coarnotify/test/mocks/__init__.py +++ b/coarnotify/test/mocks/__init__.py @@ -0,0 +1 @@ +"""Mocks for tests module initialisation.""" diff --git a/coarnotify/test/mocks/http.py b/coarnotify/test/mocks/http.py index 5b27079..72ae82d 100644 --- a/coarnotify/test/mocks/http.py +++ b/coarnotify/test/mocks/http.py @@ -1,30 +1,43 @@ +"""Mocks for the HTTP layer and HTTP response objects.""" + from coarnotify.http import HttpLayer, HttpResponse class MockHttpLayer(HttpLayer): + """Mock HTTP layer for testing.""" + def __init__(self, status_code=200, location=None): + """Construct a new MockHttpLayer object.""" self._status_code = status_code self._location = location def post(self, url, data, headers=None, *args, **kwargs): + """Mock the POST method.""" return MockHttpResponse(status_code=self._status_code, location=self._location) def get(self, url, headers=None, *args, **kwargs): + """Mock the GET method.""" raise NotImplementedError() def head(self, url, headers=None, *args, **kwargs): + """Head not implemented.""" raise NotImplementedError() class MockHttpResponse(HttpResponse): + """Mock HTTP response object.""" + def __init__(self, status_code=200, location=None): + """Construct a new MockHttpResponse object.""" self._status_code = status_code self._location = location def header(self, header_name): + """Return the value of the given header.""" if header_name.lower() == "location": return self._location @property def status_code(self): - return self._status_code \ No newline at end of file + """Return the status code.""" + return self._status_code diff --git a/coarnotify/test/server/__init__.py b/coarnotify/test/server/__init__.py index 99a3e42..0aa809a 100644 --- a/coarnotify/test/server/__init__.py +++ b/coarnotify/test/server/__init__.py @@ -1,5 +1,4 @@ -""" -Test server implementation +"""Test server implementation. For documentation on how to use this see :doc:`/test_server`. -""" \ No newline at end of file +""" diff --git a/coarnotify/test/server/inbox.py b/coarnotify/test/server/inbox.py index 01dd133..d80ce4d 100644 --- a/coarnotify/test/server/inbox.py +++ b/coarnotify/test/server/inbox.py @@ -1,18 +1,19 @@ -""" -Single file implementation of a test server, showing all the layers of the general -solution in one place. -""" +"""Single file implementation of a test server, showing all the layers of the general solution in one place.""" + from flask import Flask, request, make_response from coarnotify.test.server import settings from coarnotify.server import COARNotifyServer, COARNotifyServiceBinding, COARNotifyReceipt, COARNotifyServerError from coarnotify.core.notify import NotifyPattern -import uuid, json, sys, os +import json +import os +import sys +import uuid from datetime import datetime + def create_app(): - """ - Create the flask app, pulling config from ``settings.py`` then any supplied local config - in environment variable ``COARNOTIFY_SETTINGS``. + """Create the flask app, pulling config from ``settings.py`` then any supplied local config in environment variable + ``COARNOTIFY_SETTINGS``. :return: """ @@ -21,21 +22,21 @@ def create_app(): app.config.from_envvar("COARNOTIFY_SETTINGS", silent=True) return app + app = create_app() -"""The global flask app for the test server""" +"""The global flask app for the test server.""" class COARNotifyServiceTestImpl(COARNotifyServiceBinding): - """ - Test server implementation of the main service binding - """ + """Test server implementation of the main service binding.""" + def notification_received(self, notification: NotifyPattern) -> COARNotifyReceipt: - """ - Process an incoming notification object in the following way: + """Process an incoming notification object in the following way: 1. Generate a name for the notification based on the timestamp and a random UUID 2. Write the notification JSON-LD to a file in the store directory - 3. Return a receipt for the notification using the configured response status and a location pointing to the file + 3. Return a receipt for the notification using the configured response status + and a location pointing to the file :param notification: :return: @@ -59,14 +60,12 @@ def notification_received(self, notification: NotifyPattern) -> COARNotifyReceip @app.route("/inbox", methods=["POST"]) def inbox(): - """ - Main web entry point. POST to /inbox to trigger it + """Main web entry point. POST to /inbox to trigger it. - This pulls the notification out of the request as JSON, and sends it to the server - which will parse it and send it on to the service binding implementation + This pulls the notification out of the request as JSON, and sends it to the server which will parse it and send it + on to the service binding implementation - When it gets the receipt it will return a blank HTTP response with the appropriate - status code and Location header + When it gets the receipt it will return a blank HTTP response with the appropriate status code and Location header :return: """ @@ -86,13 +85,11 @@ def inbox(): def run_server(host=None, port=None, fake_https=False): - """ - Start the web server using the flask built in server + """Start the web server using the flask built in server. :param host: :param port: - :param fake_https: - if fake_https is True, developer can use https:// to access the server + :param fake_https: if fake_https is True, developer can use https:// to access the server :return: """ pycharm_debug = app.config.get('DEBUG_PYCHARM', False) @@ -103,9 +100,13 @@ def run_server(host=None, port=None, fake_https=False): if pycharm_debug: app.config['DEBUG'] = False import pydevd - pydevd.settrace(app.config.get('DEBUG_PYCHARM_SERVER', 'localhost'), - port=app.config.get('DEBUG_PYCHARM_PORT', 6000), - stdoutToServer=True, stderrToServer=True) + + pydevd.settrace( + app.config.get('DEBUG_PYCHARM_SERVER', 'localhost'), + port=app.config.get('DEBUG_PYCHARM_PORT', 6000), + stdoutToServer=True, + stderrToServer=True, + ) # check the store directory exists store = app.config.get("STORE_DIR") @@ -121,9 +122,8 @@ def run_server(host=None, port=None, fake_https=False): host = host or app.config['HOST'] port = port or app.config['PORT'] - app.run(host=host, debug=app.config['DEBUG'], port=port, - **run_kwargs) + app.run(host=host, debug=app.config['DEBUG'], port=port, **run_kwargs) if __name__ == "__main__": - run_server() \ No newline at end of file + run_server() diff --git a/coarnotify/test/server/settings.py b/coarnotify/test/server/settings.py index 6ff9761..9ba5f21 100644 --- a/coarnotify/test/server/settings.py +++ b/coarnotify/test/server/settings.py @@ -1,26 +1,35 @@ +"""Settings for the COAR Notify server.""" + STORE_DIR = "/your/store/dir" -"""The directory on the local machine to use to store incoming JSON files""" +"""The directory on the local machine to use to store incoming JSON files.""" HOST = "localhost" -"""Host where the app will run""" +"""Host where the app will run.""" PORT = 5005 -"""Port to start the app on""" +"""Port to start the app on.""" RESPONSE_STATUS = 201 -"""HTTP Response to provide to any incoming reqeusts. 201 and 202 are the specification compliant values""" +"""HTTP Response to provide to any incoming reqeusts. + +201 and 202 are the specification compliant values +""" VALIDATE_INCOMING = True -"""Should the server attempt to validate the incoming notifications""" +"""Should the server attempt to validate the incoming notifications.""" DEBUG = True -"""Put flask into debug mode for developer convenience""" +"""Put flask into debug mode for developer convenience.""" DEBUG_PYCHARM = False -"""Put the app into PyCharm debug mode. This turns off ``DEBUG`` and starts the PyCharm debugger. You can set this here, or you can start the test server with the ``-d`` option""" +"""Put the app into PyCharm debug mode. + +This turns off ``DEBUG`` and starts the PyCharm debugger. You can set this here, or you can start the test server with +the ``-d`` option +""" DEBUG_PYCHARM_SERVER = "localhost" -"""The host to connect to for PyCharm debugging""" +"""The host to connect to for PyCharm debugging.""" DEBUG_PYCHARM_PORT = 6000 -"""The port to connect to for PyCharm debugging""" \ No newline at end of file +"""The port to connect to for PyCharm debugging.""" diff --git a/coarnotify/test/unit/__init__.py b/coarnotify/test/unit/__init__.py index e69de29..d98a55a 100644 --- a/coarnotify/test/unit/__init__.py +++ b/coarnotify/test/unit/__init__.py @@ -0,0 +1 @@ +"""Unit test module initialisation.""" diff --git a/coarnotify/test/unit/test_activitystreams.py b/coarnotify/test/unit/test_activitystreams.py index 31c7df4..f5dcf7d 100644 --- a/coarnotify/test/unit/test_activitystreams.py +++ b/coarnotify/test/unit/test_activitystreams.py @@ -1,3 +1,5 @@ +"""ActivityStreams2 tests.""" + from unittest import TestCase from copy import deepcopy @@ -6,7 +8,10 @@ class TestActivitystreams(TestCase): + """Test the ActivityStream class.""" + def test_01_construction(self): + """Test the construction of an ActivityStream object.""" as2 = ActivityStream() assert as2.doc == {} assert as2.context == [] @@ -21,6 +26,7 @@ def test_01_construction(self): assert as2.context == s2context def test_02_set_properties(self): + """Test setting properties on an ActivityStream object.""" as2 = ActivityStream() # properties that are just basic json @@ -51,6 +57,7 @@ def test_02_set_properties(self): assert as2.context == [Properties.ID[1], "http://example.com", {"foaf": "http://xmlns.com/foaf/0.1"}] def test_03_get_properties(self): + """Test getting properties from an ActivityStream object.""" as2 = ActivityStream() as2.set_property("random", "value") as2.set_property(Properties.ID, "id") @@ -65,6 +72,7 @@ def test_03_get_properties(self): assert as2.get_property("foaf:name") == "name value" def test_04_to_jsonld(self): + """Test converting an ActivityStream object to JSON-LD.""" # check we can round trip a document source = AnnounceEndorsementFixtureFactory.source() s2 = deepcopy(source) @@ -83,7 +91,7 @@ def test_04_to_jsonld(self): "random": "value", "id": "id", "object": "object value", - "foaf:name": "name value" + "foaf:name": "name value", } assert as2.to_jsonld() == expected diff --git a/coarnotify/test/unit/test_client.py b/coarnotify/test/unit/test_client.py index 8691ef7..ab41e1d 100644 --- a/coarnotify/test/unit/test_client.py +++ b/coarnotify/test/unit/test_client.py @@ -1,13 +1,18 @@ +"""Test cases for the COARNotifyClient class.""" + from unittest import TestCase from coarnotify.client import COARNotifyClient from coarnotify.patterns import AnnounceEndorsement from coarnotify.test.fixtures import AnnounceEndorsementFixtureFactory -from coarnotify.test.mocks.http import MockHttpResponse, MockHttpLayer +from coarnotify.test.mocks.http import MockHttpLayer class TestClient(TestCase): + """Test the COARNotifyClient class.""" + def test_01_construction(self): + """Test the construction of a COARNotifyClient object.""" client = COARNotifyClient() assert client.inbox_url is None @@ -18,21 +23,19 @@ def test_01_construction(self): client = COARNotifyClient("http://example.com/inbox", MockHttpLayer()) def test_02_created_response(self): - client = COARNotifyClient("http://example.com/inbox", MockHttpLayer( - status_code=201, - location="http://example.com/location" - )) + """Test the response to a created resource.""" + client = COARNotifyClient( + "http://example.com/inbox", MockHttpLayer(status_code=201, location="http://example.com/location") + ) source = AnnounceEndorsementFixtureFactory.source() ae = AnnounceEndorsement(source) resp = client.send(ae) assert resp.action == resp.CREATED assert resp.location == "http://example.com/location" - def test_03_accepted_response(self): - client = COARNotifyClient("http://example.com/inbox", MockHttpLayer( - status_code=202 - )) + """Test the response to an accepted request.""" + client = COARNotifyClient("http://example.com/inbox", MockHttpLayer(status_code=202)) source = AnnounceEndorsementFixtureFactory.source() ae = AnnounceEndorsement(source) resp = client.send(ae) diff --git a/coarnotify/test/unit/test_factory.py b/coarnotify/test/unit/test_factory.py index ad9275d..560e6a7 100644 --- a/coarnotify/test/unit/test_factory.py +++ b/coarnotify/test/unit/test_factory.py @@ -1,3 +1,5 @@ +"""Test cases for the factory module.""" + from unittest import TestCase from coarnotify.core.notify import NotifyPattern @@ -13,7 +15,7 @@ TentativelyAccept, TentativelyReject, UnprocessableNotification, - UndoOffer + UndoOffer, ) from coarnotify.factory import COARNotifyFactory @@ -29,12 +31,15 @@ TentativelyAcceptFixtureFactory, TentativelyRejectFixtureFactory, UnprocessableNotificationFixtureFactory, - UndoOfferFixtureFactory + UndoOfferFixtureFactory, ) class TestFactory(TestCase): + """Test the COARNotifyFactory class.""" + def test_01_accept(self): + """Accept a notification.""" acc = COARNotifyFactory.get_by_types(Accept.TYPE) assert acc == Accept @@ -45,6 +50,7 @@ def test_01_accept(self): assert acc.id == source["id"] def test_02_announce_endorsement(self): + """Announce an endorsement.""" ae = COARNotifyFactory.get_by_types(AnnounceEndorsement.TYPE) assert ae == AnnounceEndorsement @@ -55,6 +61,7 @@ def test_02_announce_endorsement(self): assert ae.id == source["id"] def test_04_announce_relationship(self): + """Announce a relationship.""" ar = COARNotifyFactory.get_by_types(AnnounceRelationship.TYPE) assert ar == AnnounceRelationship @@ -65,6 +72,7 @@ def test_04_announce_relationship(self): assert ar.id == source["id"] def test_05_announce_review(self): + """Announce a review.""" ar = COARNotifyFactory.get_by_types(AnnounceReview.TYPE) assert ar == AnnounceReview @@ -75,6 +83,7 @@ def test_05_announce_review(self): assert ar.id == source["id"] def test_06_announce_service_result(self): + """Announce a service result.""" ar = COARNotifyFactory.get_by_types(AnnounceServiceResult.TYPE) assert ar == AnnounceServiceResult @@ -85,6 +94,7 @@ def test_06_announce_service_result(self): assert ar.id == source["id"] def test_07_reject(self): + """Reject a notification.""" ar = COARNotifyFactory.get_by_types(Reject.TYPE) assert ar == Reject @@ -95,6 +105,7 @@ def test_07_reject(self): assert ar.id == source["id"] def test_08_request_endorsement(self): + """Request an endorsement.""" ar = COARNotifyFactory.get_by_types(RequestEndorsement.TYPE) assert ar == RequestEndorsement @@ -105,6 +116,7 @@ def test_08_request_endorsement(self): assert ar.id == source["id"] def test_10_request_review(self): + """Request a review.""" ar = COARNotifyFactory.get_by_types(RequestReview.TYPE) assert ar == RequestReview @@ -115,6 +127,7 @@ def test_10_request_review(self): assert ar.id == source["id"] def test_11_tentatively_accept(self): + """Tentatively accept a notification.""" ar = COARNotifyFactory.get_by_types(TentativelyAccept.TYPE) assert ar == TentativelyAccept @@ -125,6 +138,7 @@ def test_11_tentatively_accept(self): assert ar.id == source["id"] def test_12_tentatively_reject(self): + """Tentatively reject a notification.""" ar = COARNotifyFactory.get_by_types(TentativelyReject.TYPE) assert ar == TentativelyReject @@ -135,6 +149,7 @@ def test_12_tentatively_reject(self): assert ar.id == source["id"] def test_13_unprocessable_notification(self): + """Unprocessable notification.""" ar = COARNotifyFactory.get_by_types(UnprocessableNotification.TYPE) assert ar == UnprocessableNotification @@ -145,6 +160,7 @@ def test_13_unprocessable_notification(self): assert ar.id == source["id"] def test_14_undo_offer(self): + """Undo an offer.""" ar = COARNotifyFactory.get_by_types(UndoOffer.TYPE) assert ar == UndoOffer @@ -155,6 +171,8 @@ def test_14_undo_offer(self): assert ar.id == source["id"] def test_15_register(self): + """Register a new pattern.""" + class TestPattern(NotifyPattern): TYPE = Accept.TYPE @@ -162,4 +180,3 @@ class TestPattern(NotifyPattern): tp = COARNotifyFactory.get_by_types(Accept.TYPE) assert tp == TestPattern - diff --git a/coarnotify/test/unit/test_models.py b/coarnotify/test/unit/test_models.py index a076062..fead8f0 100644 --- a/coarnotify/test/unit/test_models.py +++ b/coarnotify/test/unit/test_models.py @@ -1,3 +1,5 @@ +"""Test models for the COAR Notify pattern.""" + from unittest import TestCase from copy import deepcopy @@ -16,7 +18,7 @@ TentativelyAccept, TentativelyReject, UnprocessableNotification, - UndoOffer + UndoOffer, ) from coarnotify.test.fixtures.notify import NotifyFixtureFactory from coarnotify.test.fixtures import ( @@ -31,13 +33,16 @@ TentativelyAcceptFixtureFactory, TentativelyRejectFixtureFactory, UnprocessableNotificationFixtureFactory, - UndoOfferFixtureFactory + UndoOfferFixtureFactory, ) class TestModels(TestCase): + """Test the COAR Notify pattern models.""" def _get_testable_properties(self, source, prop_map=None): + """Get a list of properties that can be tested.""" + def expand(node, path): paths = [] for k, v in node.items(): @@ -74,6 +79,8 @@ def expand(node, path): return proptest def _apply_property_test(self, proptest, obj, fixtures): + """Apply a test to a set of properties.""" + def get_prop(source, prop): p = prop if isinstance(prop, tuple): @@ -110,6 +117,7 @@ def get_prop(source, prop): assert oval == eval, f"{oprop}:{oval} - {fprop}:{eval}" def test_01_notify_manual_construct(self): + """Test manual construction of a NotifyPattern object.""" n = NotifyPattern() # check the default properties @@ -125,7 +133,7 @@ def test_01_notify_manual_construct(self): # now check the setters n.id = "urn:whatever" - n.ALLOWED_TYPES = ["Object", "Other"] # this is a hack to test the setter + n.ALLOWED_TYPES = ["Object", "Other"] # this is a hack to test the setter n.type = "Other" origin = NotifyService() @@ -172,6 +180,7 @@ def test_01_notify_manual_construct(self): assert n.context.type is None def test_02_notify_from_fixture(self): + """Test construction of a NotifyPattern object from a fixture.""" source = NotifyFixtureFactory.source() n = NotifyPattern(source) @@ -200,6 +209,7 @@ def test_02_notify_from_fixture(self): assert n.type == "Other" def test_03_notify_operations(self): + """Test the NotifyPattern operations.""" n = NotifyPattern() with self.assertRaises(ValidationError): n.validate() @@ -212,6 +222,7 @@ def test_03_notify_operations(self): assert n.to_jsonld() == compare def test_04_accept(self): + """Test the Accept pattern.""" a = Accept() source = AcceptFixtureFactory.source() @@ -224,6 +235,7 @@ def test_04_accept(self): self._apply_property_test(proptest, a, AcceptFixtureFactory) def test_05_announce_endorsement(self): + """Test the AnnounceEndorsement pattern.""" ae = AnnounceEndorsement() source = AnnounceEndorsementFixtureFactory.source() compare = deepcopy(source) @@ -235,6 +247,7 @@ def test_05_announce_endorsement(self): self._apply_property_test(proptest, ae, AnnounceEndorsementFixtureFactory) def test_07_announce_relationship(self): + """Test the AnnounceRelationship pattern.""" ae = AnnounceRelationship() source = AnnounceRelationshipFixtureFactory.source() @@ -247,6 +260,7 @@ def test_07_announce_relationship(self): self._apply_property_test(proptest, ae, AnnounceRelationshipFixtureFactory) def test_08_announce_review(self): + """Test the AnnounceReview pattern.""" ar = AnnounceReview() source = AnnounceReviewFixtureFactory.source() @@ -259,11 +273,12 @@ def test_08_announce_review(self): self._apply_property_test(proptest, ar, AnnounceReviewFixtureFactory) def test_09_announce_service_result(self): + """Test the AnnounceServiceResult pattern.""" asr = AnnounceServiceResult() source = AnnounceServiceResultFixtureFactory.source() compare = deepcopy(source) - compare["type"] = compare["type"][0] # because it's a single field, but is a list in the fixture + compare["type"] = compare["type"][0] # because it's a single field, but is a list in the fixture asr = AnnounceServiceResult(source) assert asr.validate() is True @@ -273,6 +288,7 @@ def test_09_announce_service_result(self): self._apply_property_test(proptest, asr, AnnounceServiceResultFixtureFactory) def test_10_reject(self): + """Test the Reject pattern.""" rej = Reject() source = RejectFixtureFactory.source() @@ -285,6 +301,7 @@ def test_10_reject(self): self._apply_property_test(proptest, rej, RejectFixtureFactory) def test_11_request_endorsement(self): + """Test the RequestEndorsement pattern.""" re = RequestEndorsement() source = RequestEndorsementFixtureFactory.source() @@ -298,6 +315,7 @@ def test_11_request_endorsement(self): self._apply_property_test(proptest, re, RequestEndorsementFixtureFactory) def test_13_request_review(self): + """Test the RequestReview pattern.""" ri = RequestReview() source = RequestReviewFixtureFactory.source() @@ -311,6 +329,7 @@ def test_13_request_review(self): self._apply_property_test(proptest, ri, RequestReviewFixtureFactory) def test_14_tentatively_accept(self): + """Test the TentativelyAccept pattern.""" ta = TentativelyAccept() source = TentativelyAcceptFixtureFactory.source() @@ -324,6 +343,7 @@ def test_14_tentatively_accept(self): self._apply_property_test(proptest, ta, TentativelyAcceptFixtureFactory) def test_15_tentatively_reject(self): + """Test the TentativelyReject pattern.""" ta = TentativelyReject() source = TentativelyRejectFixtureFactory.source() @@ -337,6 +357,7 @@ def test_15_tentatively_reject(self): self._apply_property_test(proptest, ta, TentativelyRejectFixtureFactory) def test_16_unprocessable_notification(self): + """Test the UnprocessableNotification pattern.""" ta = UnprocessableNotification() source = UnprocessableNotificationFixtureFactory.source() @@ -350,6 +371,7 @@ def test_16_unprocessable_notification(self): self._apply_property_test(proptest, ta, UnprocessableNotificationFixtureFactory) def test_17_undo_offer(self): + """Test the UndoOffer pattern.""" ta = UndoOffer() source = UndoOfferFixtureFactory.source() @@ -363,7 +385,8 @@ def test_17_undo_offer(self): self._apply_property_test(proptest, ta, UndoOfferFixtureFactory) def test_18_by_ref(self): - # Create a basic NotifyPatter, and explcitly declare properties by reference to be true (the default) + """Test the NotifyPattern properties_by_reference flag.""" + # Create a basic NotifyPattern, and explicitly declare properties by reference to be true (the default) n = NotifyPattern(properties_by_reference=True) # create an object externally, and confirm that it does not have a specific id @@ -388,7 +411,7 @@ def test_18_by_ref(self): assert n.object.id == "urn:whatever" def test_19_by_value(self): - # Create a basic NotifyPatter, and explcitly declare properties by reference to be false. + # Create a basic NotifyPatter, and explicitly declare properties by reference to be false. # Object should now be copied and passed around by value, so updates to one do not affect # the other n = NotifyPattern(properties_by_reference=False) @@ -410,4 +433,3 @@ def test_19_by_value(self): obj = n.object obj.id = "urn:whatever" assert n.object.id != "urn:whatever" - diff --git a/coarnotify/test/unit/test_validate.py b/coarnotify/test/unit/test_validate.py index 8a9955a..7d08af0 100644 --- a/coarnotify/test/unit/test_validate.py +++ b/coarnotify/test/unit/test_validate.py @@ -1,3 +1,5 @@ +"""Test the validation functions for the various patterns.""" + from unittest import TestCase from coarnotify.core.notify import NotifyPattern, NotifyService, NotifyObject @@ -13,7 +15,7 @@ TentativelyReject, UnprocessableNotification, UndoOffer, - Reject + Reject, ) from coarnotify.test.fixtures.notify import NotifyFixtureFactory from coarnotify.test.fixtures import ( @@ -29,7 +31,7 @@ TentativelyRejectFixtureFactory, UnprocessableNotificationFixtureFactory, UndoOfferFixtureFactory, - RejectFixtureFactory + RejectFixtureFactory, ) from coarnotify.exceptions import ValidationError @@ -40,9 +42,12 @@ class TestValidate(TestCase): + """Test the validation functions for the various patterns.""" + def test_01_structural_empty(self): + """Test the basic structure of the notification pattern.""" n = NotifyPattern() - n.id = None # these are automatically set, so remove them to trigger validation + n.id = None # these are automatically set, so remove them to trigger validation n.type = None with self.assertRaises(ValidationError) as ve: n.validate() @@ -55,6 +60,7 @@ def test_01_structural_empty(self): assert Properties.ORIGIN in errors def test_02_structural_basic(self): + """Test the basic structure of the notification pattern.""" n = NotifyPattern() with self.assertRaises(ValidationError) as ve: n.validate() @@ -67,6 +73,7 @@ def test_02_structural_basic(self): assert Properties.ORIGIN in errors def test_03_structural_valid_document(self): + """Test the basic structure of the notification pattern.""" n = NotifyPattern() n.target = NotifyFixtureFactory.target() n.origin = NotifyFixtureFactory.origin() @@ -75,6 +82,7 @@ def test_03_structural_valid_document(self): assert n.validate() is True def test_04_structural_invalid_nested(self): + """Test the basic structure of the notification pattern.""" n = NotifyPattern() n.target = NotifyService({"whatever": "value"}, validate_stream_on_construct=False) n.origin = NotifyService({"another": "junk"}, validate_stream_on_construct=False) @@ -86,7 +94,9 @@ def test_04_structural_invalid_nested(self): errors = ve.exception.errors assert Properties.ID not in errors assert Properties.TYPE not in errors - assert Properties.OBJECT not in errors # the object is present, and will acquire an id, so will not be in the errors + assert ( + Properties.OBJECT not in errors + ) # the object is present, and will acquire an id, so will not be in the errors assert Properties.TARGET in errors assert Properties.ORIGIN in errors @@ -103,6 +113,7 @@ def test_04_structural_invalid_nested(self): # assert NotifyProperties.INBOX in origin.get("nested") def test_05_validation_modes(self): + """Test the various validation modes.""" valid = NotifyFixtureFactory.source() n = NotifyPattern(stream=valid, validate_stream_on_construct=True) @@ -121,13 +132,14 @@ def test_05_validation_modes(self): n = NotifyPattern(validate_properties=False) n.id = "urn:uuid:4fb3af44-d4f8-4226-9475-2d09c2d8d9e0" # valid - n.id = "http://example.com/^path" # invalid + n.id = "http://example.com/^path" # invalid with self.assertRaises(ValidationError) as ve: n.validate() assert ve.exception.errors.get(Properties.ID) is not None def test_06_validate_id_property(self): + """Test the validation of the ID property.""" n = NotifyPattern() # test the various ways it can fail: with self.assertRaises(ValueError) as ve: @@ -168,11 +180,10 @@ def test_06_validate_id_property(self): n.id = "https://generic-service.com/system/inbox/" def test_07_validate_url(self): + """Test the validation of the URL property.""" urls = URIFixtureFactory.generate(schemes=["http", "https"]) - # print(urls) for url in urls: - # print(url) assert validate.url(None, url) is True with self.assertRaises(ValueError): @@ -185,6 +196,7 @@ def test_07_validate_url(self): validate.url(None, "http://example.com/path^wrong") def test_08_one_of(self): + """Test the one_of validation function.""" values = ["a", "b", "c"] validator = validate.one_of(values) assert validator(None, "a") is True @@ -199,6 +211,7 @@ def test_08_one_of(self): validator(None, ["a", "b"]) def test_09_contains(self): + """Test the contains validation function.""" validator = validate.contains("a") assert validator(None, ["a", "b", "c"]) is True @@ -206,6 +219,7 @@ def test_09_contains(self): validator(None, ["b", "c", "d"]) def test_10_at_least_one_of(self): + """Test the at_least_one_of validation function.""" values = ["a", "b", "c"] validator = validate.at_least_one_of(values) assert validator(None, "a") is True @@ -219,7 +233,7 @@ def test_10_at_least_one_of(self): assert validator(None, ["a", "d"]) is True ######################################## - ## validation methods for specific patterns + # validation methods for specific patterns def _base_validate(self, a): # now try to apply invalid values to it @@ -271,6 +285,7 @@ def _context_validate(self, a): a.context.cite_as = "urn:uuid:4fb3af44-d4f8-4226-9475-2d09c2d8d9e0" def test_11_accept_validate(self): + """Test the validation of the Accept pattern.""" # make a valid one source = AcceptFixtureFactory.source() a = Accept(source) @@ -279,10 +294,11 @@ def test_11_accept_validate(self): # now make one with fully invalid data isource = AcceptFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = Accept(isource) def test_12_announce_endorsement_validate(self): + """Test the validation of the AnnounceEndorsement pattern.""" # make a valid one source = AnnounceEndorsementFixtureFactory.source() a = AnnounceEndorsement(source) @@ -299,10 +315,11 @@ def test_12_announce_endorsement_validate(self): # now make one with fully invalid data isource = AnnounceEndorsementFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = AnnounceEndorsement(isource) def test_13_tentative_accept_validate(self): + """Test the validation of the TentativelyAccept pattern.""" # make a valid one source = TentativelyAcceptFixtureFactory.source() a = TentativelyAccept(source) @@ -314,10 +331,11 @@ def test_13_tentative_accept_validate(self): # now make one with fully invalid data isource = TentativelyAcceptFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = TentativelyAccept(isource) def test_14_tentative_reject_validate(self): + """Test the validation of the TentativelyReject pattern.""" # make a valid one source = TentativelyRejectFixtureFactory.source() a = TentativelyReject(source) @@ -329,10 +347,11 @@ def test_14_tentative_reject_validate(self): # now make one with fully invalid data isource = TentativelyRejectFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = TentativelyReject(isource) def test_15_unprocessable_notification_validate(self): + """Test the validation of the UnprocessableNotification pattern.""" # make a valid one source = UnprocessableNotificationFixtureFactory.source() a = UnprocessableNotification(source) @@ -343,10 +362,11 @@ def test_15_unprocessable_notification_validate(self): # now make one with fully invalid data isource = UnprocessableNotificationFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = UnprocessableNotification(isource) def test_16_undo_offer_validate(self): + """Test the validation of the UndoOffer pattern.""" # make a valid one source = UndoOfferFixtureFactory.source() a = UndoOffer(source) @@ -357,11 +377,11 @@ def test_16_undo_offer_validate(self): # now make one with fully invalid data isource = UndoOfferFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = UnprocessableNotification(isource) - def test_17_announce_review_validate(self): + """Test the validation of the AnnounceReview pattern.""" # make a valid one source = AnnounceReviewFixtureFactory.source() a = AnnounceReview(source) @@ -377,10 +397,11 @@ def test_17_announce_review_validate(self): # now make one with fully invalid data isource = AnnounceReviewFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = AnnounceReview(isource) def test_18_request_endorsement_validate(self): + """Test the validation of the RequestEndorsement pattern.""" # make a valid one source = RequestEndorsementFixtureFactory.source() a = RequestEndorsement(source) @@ -395,10 +416,11 @@ def test_18_request_endorsement_validate(self): # now make one with fully invalid data isource = RequestEndorsementFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = RequestEndorsement(isource) def test_19_request_review_validate(self): + """Test the validation of the RequestReview pattern.""" # make a valid one source = RequestReviewFixtureFactory.source() a = RequestReview(source) @@ -413,10 +435,11 @@ def test_19_request_review_validate(self): # now make one with fully invalid data isource = RequestReviewFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = RequestReview(isource) def test_20_reject_validate(self): + """Test the validation of the Reject pattern.""" # make a valid one source = RejectFixtureFactory.source() a = Reject(source) @@ -427,10 +450,11 @@ def test_20_reject_validate(self): # now make one with fully invalid data isource = RejectFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = Reject(isource) def test_21_announce_relationship_validate(self): + """Test the validation of the AnnounceRelationship pattern.""" # make a valid one source = AnnounceRelationshipFixtureFactory.source() a = AnnounceRelationship(source) @@ -446,10 +470,11 @@ def test_21_announce_relationship_validate(self): # now make one with fully invalid data isource = AnnounceRelationshipFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = AnnounceRelationship(isource) def test_21_announce_service_result_validate(self): + """Test the validation of the AnnounceServiceResult pattern.""" # make a valid one source = AnnounceServiceResultFixtureFactory.source() a = AnnounceServiceResult(source) @@ -461,49 +486,32 @@ def test_21_announce_service_result_validate(self): # now make one with fully invalid data isource = AnnounceServiceResultFixtureFactory.invalid() - with self.assertRaises(ValidationError) as ve: + with self.assertRaises(ValidationError): a = AnnounceServiceResult(isource) def test_22_add_rules(self): + """Test the addition of rules to the validator.""" rules = { Properties.ID: { "default": validate.absolute_uri, "context": { - Properties.CONTEXT: { - "default": validate.url - }, - Properties.ORIGIN: { - "default": validate.url - }, - Properties.TARGET: { - "default": validate.url - }, - NotifyProperties.ITEM: { - "default": validate.url - } - } + Properties.CONTEXT: {"default": validate.url}, + Properties.ORIGIN: {"default": validate.url}, + Properties.TARGET: {"default": validate.url}, + NotifyProperties.ITEM: {"default": validate.url}, + }, }, Properties.TYPE: { "default": validate.type_checker, - } + }, } v = Validator(rules) update = { - Properties.ID: { - "default": validate.url - }, - Properties.TYPE: { - "context": { - Properties.CONTEXT: { - "default": validate.url - } - } - }, - Properties.ACTOR : { - "default": validate.url - } + Properties.ID: {"default": validate.url}, + Properties.TYPE: {"context": {Properties.CONTEXT: {"default": validate.url}}}, + Properties.ACTOR: {"default": validate.url}, } v.add_rules(update) diff --git a/coarnotify/validate.py b/coarnotify/validate.py index e335e58..465faba 100644 --- a/coarnotify/validate.py +++ b/coarnotify/validate.py @@ -1,7 +1,7 @@ -""" -This module provides a set of validation functions that can be used to validate properties on objects. -It also contains a ``Validator`` class which is used to wrap the protocol-wide validation rules which -are shared across all objects. +"""This module provides a set of validation functions that can be used to validate properties on objects. + +It also contains a ``Validator`` class which is used to wrap the protocol-wide validation rules which are shared across +all objects. """ from urllib.parse import urlparse @@ -13,32 +13,24 @@ REQUIRED_MESSAGE = "`{x}` is a required field" + class Validator: - """ - A wrapper around a set of validation rules which can be used to select the appropriate validator - in a given context. + """A wrapper around a set of validation rules which can be used to select the appropriate validator in a given + context. The validation rules are structured as follows: .. code-block:: python - { - "": { - "default": default_validator_function - "context": { - "": { - "default": default_validator_function - } - } - } - } + { "": { "default": default_validator_function "context": { "": { + "default": default_validator_function } } } } Here the ```` key is the name of the property being validated, which may be a string (the property name) or a ``tuple`` of strings (the property name and the namespace for the property name). If a ``context`` is provided, then if the top level property is being validated, and it appears inside a field - present in the ``context`` then the ``default`` validator at the top level is overridden by the ``default`` validator - in the ``context``. + present in the ``context`` then the ``default`` validator at the top level is overridden by the ``default`` + validator in the ``context``. For example, consider the following rules: @@ -58,25 +50,24 @@ class Validator: } } - This tells us that the ``TYPE`` property should be validated with ``validate.type_checker`` by default. But if - we are looking at that ``TYPE`` property inside an ``ACTOR`` object, then instead we should use ``validate.one_of``. + This tells us that the ``TYPE`` property should be validated with ``validate.type_checker`` by default. But if we + are looking at that ``TYPE`` property inside an ``ACTOR`` object, then instead we should use ``validate.one_of``. When the :py:meth:`get` method is called, the ``context`` parameter can be used to specify the context in which the property is being validated. :param rules: The rules to use for validation """ + def __init__(self, rules: dict): - """ - Create a new validator with the given rules + """Create a new validator with the given rules. :param rules: The rules to use for validation """ self._rules = rules - def get(self, property: Union[str, Tuple[str, str]], context: Union[str, Tuple[str, str]]=None) -> Callable: - """ - Get the validation function for the given property in the given context + def get(self, property: Union[str, Tuple[str, str]], context: Union[str, Tuple[str, str]] = None) -> Callable: + """Get the validation function for the given property in the given context. :param property: the property to get the validation function for :param context: the context in which the property is being validated @@ -91,7 +82,7 @@ def get(self, property: Union[str, Tuple[str, str]], context: Union[str, Tuple[s return default def rules(self): - """The ruleset for this validator""" + """The ruleset for this validator.""" return self._rules def add_rules(self, rules): @@ -110,20 +101,23 @@ def merge_dicts_recursive(dict1, dict2): ############################################# -## URI validator +# URI validator URI_RE = r'^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?' SCHEME = re.compile(r'^[a-zA-Z][a-zA-Z0-9+\-.]*$') -IPv6 = re.compile(r"(?:^|(?<=\s))\[{0,1}(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))]{0,1}(?=\s|$)") +IPv6 = re.compile( + r"(?:^|(?<=\s))\[{0,1}(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))]{0,1}(?=\s|$)" # noqa E501 +) HOSTPORT = re.compile( - r'^(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... - r'localhost|' #localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 - r"(?:^|(?<=\s))(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(?=\s|$)" - r')' - r'(?::\d+)?$', # optional port - re.IGNORECASE) + r'^(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... + r'localhost|' # localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 + r"(?:^|(?<=\s))(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(?=\s|$)" # noqa E501 + r')' + r'(?::\d+)?$', # optional port + re.IGNORECASE, +) MARK = "-_.!~*'()" UNRESERVED = "a-zA-Z0-9" + MARK @@ -138,12 +132,11 @@ def merge_dicts_recursive(dict1, dict2): def absolute_uri(obj, uri: str) -> bool: - """ - Validate that the given string is an absolute URI + """Validate that the given string is an absolute URI. :param obj: The Notify object to which the property being validated belongs. :param uri: The string that claims to be an absolute URI - :return: ``True`` if the URI is valid, otherwise ValueError is raised + :return:``True`` if the URI is valid, otherwise ValueError is raised """ m = re.match(URI_RE, uri) if m is None: @@ -173,11 +166,11 @@ def absolute_uri(obj, uri: str) -> bool: if not USERINFO.match(userinfo): raise ValueError(f"Invalid URI authority `{authority}`") # determine if the domain is ipv6 - if hostport.startswith("["): # ipv6 with an optional port + if hostport.startswith("["): # ipv6 with an optional port port_separator = hostport.rfind("]:") port = None if port_separator != -1: - port = hostport[port_separator+2:] + port = hostport[port_separator + 2 :] # noqa: E203 host = hostport[1:port_separator] else: host = hostport[1:-1] @@ -206,16 +199,16 @@ def absolute_uri(obj, uri: str) -> bool: return True + ############################################### -def url(obj, url:str) -> bool: - """ - Validate that the given string is an absolute HTTP URI (i.e. a URL) +def url(obj, url: str) -> bool: + """Validate that the given string is an absolute HTTP URI (i.e. a URL) :param obj: The Notify object to which the property being validated belongs. :param uri: The string that claims to be an HTTP URI - :return: ``True`` if the URI is valid, otherwise ValueError is raised + :return:``True`` if the URI is valid, otherwise ValueError is raised """ absolute_uri(obj, url) o = urlparse(url) @@ -227,28 +220,30 @@ def url(obj, url:str) -> bool: def one_of(values: List[str]) -> Callable: - """ - Closure that returns a validation function that checks that the value is one of the given values + """Closure that returns a validation function that checks that the value is one of the given values. - :param values: The list of values to choose from. When the returned function is run, the value passed to it - must be one of these values + :param values: The list of values to choose from. When the returned function is run, the value passed to it must be + one of these values :return: a validation function """ + def validate(obj, x): if x not in values: raise ValueError(f"`{x}` is not one of the valid values: {values}") return True + return validate + def at_least_one_of(values: List[str]) -> Callable: - """ - Closure that returns a validation function that checks that a list of values contains at least one - of the given values + """Closure that returns a validation function that checks that a list of values contains at least one of the given + values. - :param values: The list of values to choose from. When the returned function is run, the values (plural) passed to it - must contain at least one of these values + :param values: The list of values to choose from. When the returned function is run, the values (plural) passed to + it must contain at least one of these values :return: a validation function """ + def validate(obj, x): if not isinstance(x, list): x = [x] @@ -263,12 +258,12 @@ def validate(obj, x): return validate + def contains(value: str) -> Callable: - """ - Closure that returns a validation function that checks the provided values contain the required value + """Closure that returns a validation function that checks the provided values contain the required value. - :param value: The value that must be present. When the returned function is run, the value(s) passed to it - must contain this value + :param value: The value that must be present. When the returned function is run, the value(s) passed to it must + contain this value :return: a validation function """ values = value @@ -288,9 +283,9 @@ def validate(obj, x): return validate + def type_checker(obj, value): - """ - Validate that the given value is of the correct type for the object. The exact behaviour of this function + """Validate that the given value is of the correct type for the object. The exact behaviour of this function depends on the object provided: * If the object has an ``ALLOWED_TYPES`` attribute which is not an empty list, then the value must be one of @@ -300,7 +295,7 @@ def type_checker(obj, value): :param obj: the notify object being validated :param value: the type being validated - :return: ``True`` if the type is valid, otherwise ValueError is raised + :return:``True`` if the type is valid, otherwise ValueError is raised """ if hasattr(obj, "ALLOWED_TYPES"): allowed = obj.ALLOWED_TYPES diff --git a/docs/source/conf.py b/docs/source/conf.py index 589128f..1110733 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,3 +1,5 @@ +"""Sphinx configuration file for the coarnotify project.""" + # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: @@ -36,7 +38,6 @@ exclude_patterns = [] - # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/setup.py b/setup.py index 4317704..cf254fa 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,12 @@ +"""Setup script for coarnotify.""" + from setuptools import setup, find_packages setup( name="coarnotify", version="1.0.1.3", # Version 3 of the library for the 1.0.1 spec packages=find_packages(), - install_requires=[ - "requests" - ], + install_requires=["requests"], urls=["https://coar-notify.net/", "http://cottagelabs.com/"], author="Cottage Labs", author_email="richard@cottagelabs.com", @@ -16,12 +16,7 @@ license="Apache2", classifiers=[], extras_require={ - 'docs': [ - 'sphinx', - 'sphinx-autoapi' - ], - 'test': [ - "Flask>3.0.0" - ], - } + 'docs': ['sphinx', 'sphinx-autoapi'], + 'test': ["Flask>3.0.0"], + }, )