Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
ignore = D102, D202, D205, D209, D400, D401, D107
17 changes: 17 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 3 additions & 5 deletions coarnotify/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,4 +12,4 @@
stand-alone inbox you can use for local testing.
"""

__version__ = "1.0.1.3"
__version__ = "1.0.1.3"
70 changes: 36 additions & 34 deletions coarnotify/client.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,24 +10,23 @@


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:

* CREATED - a new resource was created

* 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)
Expand All @@ -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:
Expand All @@ -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"))
Expand Down
4 changes: 1 addition & 3 deletions coarnotify/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
"""
This module contains the central objects that form the basis for the COAR notify patterns
"""
"""This module contains the central objects that form the basis for the COAR notify patterns."""
103 changes: 49 additions & 54 deletions coarnotify/core/activitystreams2.py
Original file line number Diff line number Diff line change
@@ -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`
Expand Down Expand Up @@ -104,6 +105,7 @@ class ActivityStreamsTypes:
TOMBSTONE = "Tombstone"
VIDEO = "Video"


ACTIVITY_STREAMS_OBJECTS = [
ActivityStreamsTypes.ACTIVITY,
ActivityStreamsTypes.APPLICATION,
Expand All @@ -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
"""
Expand All @@ -158,26 +160,24 @@ 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
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]
Expand All @@ -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
Expand All @@ -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
}
return {"@context": self._context, **self._doc}
Loading