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
- NotesApiError
- builtins.Exception
- builtins.BaseException
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
- NotesApiError
- builtins.Exception
- builtins.BaseException
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
- NotesApiError
- builtins.Exception
- builtins.BaseException
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 adict
.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 bothmodified
andmodified_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
todt
.Args
dt
:datetime
- The
datetime
object to setNote.modified
to. Defaults todatetime.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
- NotesApiError
- builtins.Exception
- builtins.BaseException
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
andNotesApi.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
andNote.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
andNote.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]]
-
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
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())