Package nextcloud_notes_api

nextcloud-notes-api is an unofficial wrapper for the Nextcloud Notes app API

Usage Examples

All interaction with the API is done through the NotesApi object.

Logging In

Wrong credentials or hostname won't throw, until you interact with the API. Any of the above can be updated, by setting the respective attribute (e.g. NotesApi.password). If your host does not support ETag caching, you can disable it by passing etag_caching=False.

from nextcloud_notes_api import NotesApi, Note

api = NotesApi('username', 'password', 'example.org')
api.password = 's3creTpaSSw0rd'

Fetching Notes

Notes can be retrieved via NotesApi.get_single_note() by their ID.

note = api.get_single_note(666)

To fetch all notes, use NotesApi.get_all_notes(). Since NotesApi.get_all_notes() may return either a typing.Iterator or a collections.abc.Sequence, it is advisibale to only iterate over it with a for loop or converting it to a list.

notes = api.get_all_notes()

for note in notes:
    print(note.title)

Creating Notes

To create a new note first instanciate a Note and then pass it to NotesApi.create_note(). NotesApi.create_note() returns the note with attributes set according the its docs.

# Yes, markdown is supported by the Nextcloud Notes app.
content = """# Todo
- buy chips
- buy beer
- clean up the mess from last weekend
"""
note = Note('Todo', content)
note = api.create_note(note)

Updating Notes

Notes can be updated by passing a Note with the correct Note.id to NotesApi.update_note().

note = api.get_single_note(1337)

note.content = 'elite'
note.update_modified()

api.update_note(note)

Deleting Notes

To delete a note pass it's ID to NotesApi.delete_note().

api.delete_note(420)
Expand source code
"""nextcloud-notes-api is an unofficial wrapper for the
    [Nextcloud Notes app](https://github.com/nextcloud/notes) API

    .. include:: documentation.md
    """

from .api_exceptions import (
    InsufficientNextcloudStorage,
    InvalidNextcloudCredentials,
    InvalidNoteId,
    NoteNotFound,
)
from .api_wrapper import NotesApi
from .note import Note

__version__ = '0.1.0'
__all__ = [
    'InsufficientNextcloudStorage',
    'InvalidNextcloudCredentials',
    'InvalidNoteId',
    'NoteNotFound',
    'NotesApi',
    'Note',
]

Sub-modules

nextcloud_notes_api.api_exceptions
nextcloud_notes_api.api_wrapper
nextcloud_notes_api.note

Classes

class InsufficientNextcloudStorage (hostname: str, note: Note)

Not enough free storage for saving the notes content

Expand source code
class InsufficientNextcloudStorage(NotesApiError):
    """Not enough free storage for saving the notes content"""

    def __init__(self, hostname: str, note: Note):
        NotesApiError.__init__(
            self,
            'Not enough free space for saving note: ',
            hostname=hostname,
            note=note,
        )

Ancestors

class InvalidNextcloudCredentials (username: str, password: str, hostname: str)

Supplied credentials are invalid

Expand source code
class InvalidNextcloudCredentials(NotesApiError):
    """Supplied credentials are invalid"""

    def __init__(self, username: str, password: str, hostname: str):
        NotesApiError.__init__(
            self,
            'Invalid credentials: ',
            username=username,
            password=password,
            hostname=hostname,
        )

Ancestors

class InvalidNoteId (note_id: int, hostname: str)

Requested note id is invalid

Expand source code
class InvalidNoteId(NotesApiError):
    """Requested note id is invalid"""

    def __init__(self, note_id: int, hostname: str):
        NotesApiError.__init__(
            self, 'Invalid note id: ', note_id=note_id, hostname=hostname
        )

Ancestors

class Note (title: Optional[str] = None, content: Optional[str] = None, *, category: Optional[str] = None, favorite: Optional[bool] = None, id: Optional[int] = None, modified: Optional[int] = None, modified_datetime: Optional[datetime] = None, generate_modified: bool = False, **_: Optional[Any])

Represents a Nextcloud Notes app note.

See Note.to_dict() for conversion to a dict.

Args

title : str, optional
Note title. Defaults to None.
content : str, optional
Note content. Defaults to None.
category : str, optional
Note category. Defaults to None.
favorite : bool, optional
Whether the note is marked as a favorite. Defaults to None.
id : int, optional
A unique note ID. Defaults to None.
modified : int, optional
When the note has last been modified, as int posix timestamp. Defaults to None.
modified_datetime : datetime, optional
When the note has last been modified as datetime object, preferred over modified. Defaults to None.
generate_modified : bool
Whether Note.modified should be set to the current time. Overrides both modified and modified_datetime. Defaults to False.

_(Any, optional): Discard unused keyword arguments.

Expand source code
class Note:
    """Represents a Nextcloud Notes app note."""

    def __init__(
        self,
        title: Optional[str] = None,
        content: Optional[str] = None,
        *,
        category: Optional[str] = None,
        favorite: Optional[bool] = None,
        id: Optional[int] = None,
        modified: Optional[int] = None,
        modified_datetime: Optional[datetime] = None,
        generate_modified: bool = False,
        **_: Optional[Any],
    ):
        """See `Note.to_dict` for conversion to a `dict`.

        Args:
            title (str, optional): Note title. Defaults to None.
            content (str, optional): Note content. Defaults to None.
            category (str, optional): Note category. Defaults to None.
            favorite (bool, optional): Whether the note is marked as a favorite.
                Defaults to None.
            id (int, optional): A unique note ID. Defaults to None.
            modified (int, optional): When the note has last been modified, as int posix
                timestamp. Defaults to None.
            modified_datetime (datetime, optional): When the note has last been
                modified as datetime object, preferred over `modified`. Defaults to
                None.
            generate_modified (bool): Whether `Note.modified` should be set
                to the current time. Overrides both `modified` and `modified_datetime`.
                Defaults to False.
            _(Any, optional): Discard unused keyword arguments.
        """
        self.title = title
        """`str`: Note title."""
        self.content = content
        """`str`: Note content."""
        self.category = category
        """`str`: Note category."""
        self.favorite = favorite
        """`bool`: Whether the note is marked as a favorite."""
        self.id = id
        """`int`: A unique note id."""
        self.modified = (
            datetime.fromtimestamp(modified)
            if not modified_datetime and modified
            else modified_datetime
        )
        """`datetime.datetime`: When the note has last been modified."""

        if generate_modified:
            self.update_modified()

    def to_dict(self) -> Dict[str, Any]:
        """Generate a `dict` from this class.

        `Note.modified` is converted to a int posix timestamp.

        Returns:
            Dict[str, Any]: A `dict` containing the attributes of this class.
        """
        return {
            'title': self.title,
            'content': self.content,
            'category': self.category,
            'favorite': self.favorite,
            'id': self.id,
            'modified': self.modified.timestamp() if self.modified else None,
        }

    def update_modified(self, dt: datetime = None) -> None:
        """Set `Note.modified` to `dt`.

        Args:
            dt (datetime): The `datetime` object to set `Note.modified` to. Defaults to
                `datetime.now()`.
        """
        if dt:
            self.modified = dt
        else:
            self.modified = datetime.now()

    def __eq__(self, other: Note) -> bool:
        return self.to_dict() == other.to_dict()

    def __repr__(self) -> str:
        return f'<Note [{self.id}]>'

    def __str__(self) -> str:
        elements = self.to_dict()
        if self.modified:
            elements['modified'] = str(self.modified)
        return f'Note[{elements}]'

Instance variables

var category

str: Note category.

var content

str: Note content.

var favorite

bool: Whether the note is marked as a favorite.

var id

int: A unique note id.

var modified

datetime.datetime: When the note has last been modified.

var title

str: Note title.

Methods

def to_dict(self) ‑> Dict[str, Any]

Generate a dict from this class.

Note.modified is converted to a int posix timestamp.

Returns

Dict[str, Any]
A dict containing the attributes of this class.
Expand source code
def to_dict(self) -> Dict[str, Any]:
    """Generate a `dict` from this class.

    `Note.modified` is converted to a int posix timestamp.

    Returns:
        Dict[str, Any]: A `dict` containing the attributes of this class.
    """
    return {
        'title': self.title,
        'content': self.content,
        'category': self.category,
        'favorite': self.favorite,
        'id': self.id,
        'modified': self.modified.timestamp() if self.modified else None,
    }
def update_modified(self, dt: datetime = None) ‑> NoneType

Set Note.modified to dt.

Args

dt : datetime
The datetime object to set Note.modified to. Defaults to datetime.now().
Expand source code
def update_modified(self, dt: datetime = None) -> None:
    """Set `Note.modified` to `dt`.

    Args:
        dt (datetime): The `datetime` object to set `Note.modified` to. Defaults to
            `datetime.now()`.
    """
    if dt:
        self.modified = dt
    else:
        self.modified = datetime.now()
class NoteNotFound (note_id: int, hostname: str)

Note doesn't exist

Expand source code
class NoteNotFound(NotesApiError):
    """Note doesn't exist"""

    def __init__(self, note_id: int, hostname: str):
        NotesApiError.__init__(
            self, 'Note not found: ', note_id=note_id, hostname=hostname
        )

Ancestors

class NotesApi (username: str, password: str, hostname: str, *, etag_caching: bool = True)

Wraps the Nextcloud Notes app API.

Args

username : str
Nextcloud username.
password : str
Nextcloud password.
hostname : str
Nextcloud hostname.
etag_caching : bool, optional
Whether to cache notes using HTTP ETags, if the server supports it. Defaults to True.
Expand source code
class NotesApi:
    """Wraps the [Nextcloud Notes app API](https://github.com/nextcloud/notes/blob/master/docs/api/v1.md)."""  # noqa: E501

    @dataclass
    class EtagCache:
        """Convenience class for caching notes using HTTP ETags."""

        etag: str = ''
        notes: List[Note] = field(default_factory=list)

    def __init__(
        self, username: str, password: str, hostname: str, *, etag_caching: bool = True
    ):
        """
        Args:
            username (str): Nextcloud username.
            password (str): Nextcloud password.
            hostname (str): Nextcloud hostname.
            etag_caching (bool, optional): Whether to cache notes using HTTP ETags, if
                the server supports it. Defaults to True.
        """
        self.username = username
        """`str`: Nextcloud username."""
        self.password = password
        """`str`: Nextcloud password."""
        self.hostname = hostname
        """`str`: Nextcloud hostname."""
        self.etag_caching = etag_caching
        """`bool`: Whether to cache notes using HTTP ETags."""

        self._etag_cache = NotesApi.EtagCache()
        self._common_headers = {'OCS-APIRequest': 'true', 'Accept': 'application/json'}

    @property
    def auth_pair(self) -> Tuple[str, str]:
        """Tuple[str, str]: Tuple of `NotesApi.username` and `NotesApi.password`."""
        return (self.username, self.password)

    def get_api_version(self) -> str:
        """
        Returns:
            str: Highest supported Notes app api version.
        """
        response = get(
            f'https://{self.hostname}/ocs/v2.php/cloud/capabilities',
            auth=self.auth_pair,
            headers=self._common_headers,
        )

        return response.json()['ocs']['data']['capabilities']['notes']['api_version'][
            -1
        ]

    def get_all_notes(self) -> Union[Iterator[Note], Sequence[Note]]:
        """Fetch all notes.

        Returns:
            Union[Iterator[Note], Sequence[Note]]: A `typing.Iterator` or
                `collections.abc.Sequence` of all notes.

        Raises:
            InvalidNextcloudCredentials: Invalid credentials supplied.
        """
        headers = self._common_headers
        if self.etag_caching:
            headers['If-None-Match'] = self._etag_cache.etag

        response = get(
            f'https://{self.hostname}/index.php/apps/notes/api/v1/notes',
            auth=self.auth_pair,
            headers=headers,
        )

        if response.status_code == 401:
            raise InvalidNextcloudCredentials(
                self.username, self.password, self.hostname
            )

        # Cache is valid
        if response.status_code == 304 and self.etag_caching:
            return self._etag_cache.notes

        if self.etag_caching:
            notes = [Note(**note_dict) for note_dict in response.json()]
            # Update cache
            self._etag_cache = NotesApi.EtagCache(
                response.headers['ETag'], deepcopy(notes)
            )
            return notes
        else:
            return (Note(**note_dict) for note_dict in response.json())

    def get_single_note(self, note_id: int) -> Note:
        """Retrieve note with ID `note_id`.

        Args:
            note_id (int): ID of note to retrieve.

        Returns:
            Note: Note with id `note_id`.

        Raises:
            InvalidNoteId: `note_id` is an invalid ID.
            InvalidNextcloudCredentials: Invalid credentials supplied.
            NoteNotFound: Note with id `note_id` doesn't exist.
        """
        response = get(
            f'https://{self.hostname}/index.php/apps/notes/api/v1/notes/{note_id}',
            auth=self.auth_pair,
            headers=self._common_headers,
        )

        if response.status_code == 400:
            raise InvalidNoteId(note_id, self.hostname)
        elif response.status_code == 401:
            raise InvalidNextcloudCredentials(
                self.username, self.password, self.hostname
            )
        elif response.status_code == 404:
            raise NoteNotFound(note_id, self.hostname)

        return Note(**response.json())

    def create_note(self, note: Note) -> Note:
        """Create new note.

        `Note.id` and `Note.modified` are set by the server.
        `Note.title` will also be changed in case there already is a note with the same
        title, e.g. 'Todo' -> 'Todo (2)'.

        Args:
            note (Note): Note to create.

        Returns:
            Note: Created note with `Note.id` and `Note.modified` set.

        Raises:
            InvalidNextcloudCredentials: Invalid credentials supplied.
            InsufficientNextcloudStorage: Not enough storage to save `note`.
        """
        response = post(
            f'https://{self.hostname}/index.php/apps/notes/api/v1/notes',
            auth=self.auth_pair,
            headers=self._common_headers,
            data=note.to_dict(),
        )

        # Getting a status 400 is impossible since the note id is ignored by the
        # server, although specified by the api docs
        if response.status_code == 401:
            raise InvalidNextcloudCredentials(
                self.username, self.password, self.hostname
            )
        elif response.status_code == 507:
            raise InsufficientNextcloudStorage(self.hostname, note)

        return Note(**response.json())

    def update_note(self, note: Note) -> Note:
        """Update `note`.

        Args:
            note (Note): New note, `Note.id` has to match the ID of the note to be
                replaced.

        Returns:
            Note: Updated note with new `Note.modified`.

        Raises:
            ValueError: `Note.id` is not set.
            InvalidNoteId: `Note.id` is an invalid ID.
            InvalidNextcloudCredentials: Invalid credentials supplied.
            NoteNotFound: Note with id `Note.id` doesn't exist.
            InsufficientNextcloudStorage: Not enough storage to save `note`.
        """
        if not note.id:
            raise ValueError(f'Note id not set {note}')

        data = note.to_dict()
        del data['id']

        response = put(
            f'https://{self.hostname}/index.php/apps/notes/api/v1/notes/{note.id}',
            auth=self.auth_pair,
            headers=self._common_headers,
            data=data,
        )

        if response.status_code == 400:
            raise InvalidNoteId(note.id, self.hostname)
        elif response.status_code == 401:
            raise InvalidNextcloudCredentials(
                self.username, self.password, self.hostname
            )
        elif response.status_code == 404:
            raise NoteNotFound(note.id, self.hostname)
        elif response.status_code == 507:
            raise InsufficientNextcloudStorage(self.hostname, note)

        return Note(**response.json())

    def delete_note(self, note_id: int):
        """Delete note with ID `note_id`.

        Args:
            note_id (int): ID of note to delete

        Raises:
            InvalidNoteId: `note_id` is an invalid ID.
            InvalidNextcloudCredentials: Invalid credentials supplied.
            NoteNotFound: Note with id `note_id` doesn't exist.
        """
        response = delete(
            f'https://{self.hostname}/index.php/apps/notes/api/v1/notes/{note_id}',
            auth=self.auth_pair,
            headers=self._common_headers,
        )

        if response.status_code == 400:
            raise InvalidNoteId(note_id, self.hostname)
        elif response.status_code == 401:
            raise InvalidNextcloudCredentials(
                self.username, self.password, self.hostname
            )
        elif response.status_code == 404:
            raise NoteNotFound(note_id, self.hostname)

    def __repr__(self):
        return f'<NotesApi [{self.hostname}]>'

Class variables

var EtagCache

Convenience class for caching notes using HTTP ETags.

Instance variables

var auth_pair : Tuple[str, str]

Tuple[str, str]: Tuple of NotesApi.username and NotesApi.password.

Expand source code
@property
def auth_pair(self) -> Tuple[str, str]:
    """Tuple[str, str]: Tuple of `NotesApi.username` and `NotesApi.password`."""
    return (self.username, self.password)
var etag_caching

bool: Whether to cache notes using HTTP ETags.

var hostname

str: Nextcloud hostname.

var password

str: Nextcloud password.

var username

str: Nextcloud username.

Methods

def create_note(self, note: Note) ‑> Note

Create new note.

Note.id and Note.modified are set by the server. Note.title will also be changed in case there already is a note with the same title, e.g. 'Todo' -> 'Todo (2)'.

Args

note : Note
Note to create.

Returns

Note
Created note with Note.id and Note.modified set.

Raises

InvalidNextcloudCredentials
Invalid credentials supplied.
InsufficientNextcloudStorage
Not enough storage to save nextcloud_notes_api.note.
Expand source code
def create_note(self, note: Note) -> Note:
    """Create new note.

    `Note.id` and `Note.modified` are set by the server.
    `Note.title` will also be changed in case there already is a note with the same
    title, e.g. 'Todo' -> 'Todo (2)'.

    Args:
        note (Note): Note to create.

    Returns:
        Note: Created note with `Note.id` and `Note.modified` set.

    Raises:
        InvalidNextcloudCredentials: Invalid credentials supplied.
        InsufficientNextcloudStorage: Not enough storage to save `note`.
    """
    response = post(
        f'https://{self.hostname}/index.php/apps/notes/api/v1/notes',
        auth=self.auth_pair,
        headers=self._common_headers,
        data=note.to_dict(),
    )

    # Getting a status 400 is impossible since the note id is ignored by the
    # server, although specified by the api docs
    if response.status_code == 401:
        raise InvalidNextcloudCredentials(
            self.username, self.password, self.hostname
        )
    elif response.status_code == 507:
        raise InsufficientNextcloudStorage(self.hostname, note)

    return Note(**response.json())
def delete_note(self, note_id: int)

Delete note with ID note_id.

Args

note_id : int
ID of note to delete

Raises

InvalidNoteId
note_id is an invalid ID.
InvalidNextcloudCredentials
Invalid credentials supplied.
NoteNotFound
Note with id note_id doesn't exist.
Expand source code
def delete_note(self, note_id: int):
    """Delete note with ID `note_id`.

    Args:
        note_id (int): ID of note to delete

    Raises:
        InvalidNoteId: `note_id` is an invalid ID.
        InvalidNextcloudCredentials: Invalid credentials supplied.
        NoteNotFound: Note with id `note_id` doesn't exist.
    """
    response = delete(
        f'https://{self.hostname}/index.php/apps/notes/api/v1/notes/{note_id}',
        auth=self.auth_pair,
        headers=self._common_headers,
    )

    if response.status_code == 400:
        raise InvalidNoteId(note_id, self.hostname)
    elif response.status_code == 401:
        raise InvalidNextcloudCredentials(
            self.username, self.password, self.hostname
        )
    elif response.status_code == 404:
        raise NoteNotFound(note_id, self.hostname)
def get_all_notes(self) ‑> Union[Iterator[Note], Sequence[Note]]

Fetch all notes.

Returns

Union[Iterator[Note], Sequence[Note]]
A typing.Iterator or collections.abc.Sequence of all notes.

Raises

InvalidNextcloudCredentials
Invalid credentials supplied.
Expand source code
def get_all_notes(self) -> Union[Iterator[Note], Sequence[Note]]:
    """Fetch all notes.

    Returns:
        Union[Iterator[Note], Sequence[Note]]: A `typing.Iterator` or
            `collections.abc.Sequence` of all notes.

    Raises:
        InvalidNextcloudCredentials: Invalid credentials supplied.
    """
    headers = self._common_headers
    if self.etag_caching:
        headers['If-None-Match'] = self._etag_cache.etag

    response = get(
        f'https://{self.hostname}/index.php/apps/notes/api/v1/notes',
        auth=self.auth_pair,
        headers=headers,
    )

    if response.status_code == 401:
        raise InvalidNextcloudCredentials(
            self.username, self.password, self.hostname
        )

    # Cache is valid
    if response.status_code == 304 and self.etag_caching:
        return self._etag_cache.notes

    if self.etag_caching:
        notes = [Note(**note_dict) for note_dict in response.json()]
        # Update cache
        self._etag_cache = NotesApi.EtagCache(
            response.headers['ETag'], deepcopy(notes)
        )
        return notes
    else:
        return (Note(**note_dict) for note_dict in response.json())
def get_api_version(self) ‑> str

Returns

str
Highest supported Notes app api version.
Expand source code
def get_api_version(self) -> str:
    """
    Returns:
        str: Highest supported Notes app api version.
    """
    response = get(
        f'https://{self.hostname}/ocs/v2.php/cloud/capabilities',
        auth=self.auth_pair,
        headers=self._common_headers,
    )

    return response.json()['ocs']['data']['capabilities']['notes']['api_version'][
        -1
    ]
def get_single_note(self, note_id: int) ‑> Note

Retrieve note with ID note_id.

Args

note_id : int
ID of note to retrieve.

Returns

Note
Note with id note_id.

Raises

InvalidNoteId
note_id is an invalid ID.
InvalidNextcloudCredentials
Invalid credentials supplied.
NoteNotFound
Note with id note_id doesn't exist.
Expand source code
def get_single_note(self, note_id: int) -> Note:
    """Retrieve note with ID `note_id`.

    Args:
        note_id (int): ID of note to retrieve.

    Returns:
        Note: Note with id `note_id`.

    Raises:
        InvalidNoteId: `note_id` is an invalid ID.
        InvalidNextcloudCredentials: Invalid credentials supplied.
        NoteNotFound: Note with id `note_id` doesn't exist.
    """
    response = get(
        f'https://{self.hostname}/index.php/apps/notes/api/v1/notes/{note_id}',
        auth=self.auth_pair,
        headers=self._common_headers,
    )

    if response.status_code == 400:
        raise InvalidNoteId(note_id, self.hostname)
    elif response.status_code == 401:
        raise InvalidNextcloudCredentials(
            self.username, self.password, self.hostname
        )
    elif response.status_code == 404:
        raise NoteNotFound(note_id, self.hostname)

    return Note(**response.json())
def update_note(self, note: Note) ‑> Note

Update nextcloud_notes_api.note.

Args

note : Note
New note, Note.id has to match the ID of the note to be replaced.

Returns

Note
Updated note with new Note.modified.

Raises

ValueError
Note.id is not set.
InvalidNoteId
Note.id is an invalid ID.
InvalidNextcloudCredentials
Invalid credentials supplied.
NoteNotFound
Note with id Note.id doesn't exist.
InsufficientNextcloudStorage
Not enough storage to save nextcloud_notes_api.note.
Expand source code
def update_note(self, note: Note) -> Note:
    """Update `note`.

    Args:
        note (Note): New note, `Note.id` has to match the ID of the note to be
            replaced.

    Returns:
        Note: Updated note with new `Note.modified`.

    Raises:
        ValueError: `Note.id` is not set.
        InvalidNoteId: `Note.id` is an invalid ID.
        InvalidNextcloudCredentials: Invalid credentials supplied.
        NoteNotFound: Note with id `Note.id` doesn't exist.
        InsufficientNextcloudStorage: Not enough storage to save `note`.
    """
    if not note.id:
        raise ValueError(f'Note id not set {note}')

    data = note.to_dict()
    del data['id']

    response = put(
        f'https://{self.hostname}/index.php/apps/notes/api/v1/notes/{note.id}',
        auth=self.auth_pair,
        headers=self._common_headers,
        data=data,
    )

    if response.status_code == 400:
        raise InvalidNoteId(note.id, self.hostname)
    elif response.status_code == 401:
        raise InvalidNextcloudCredentials(
            self.username, self.password, self.hostname
        )
    elif response.status_code == 404:
        raise NoteNotFound(note.id, self.hostname)
    elif response.status_code == 507:
        raise InsufficientNextcloudStorage(self.hostname, note)

    return Note(**response.json())