Skip to content

Commit

Permalink
Merge pull request #27 from marph91/add-server-api
Browse files Browse the repository at this point in the history
Add server api
  • Loading branch information
marph91 authored Sep 7, 2024
2 parents 671c8de + 7599afc commit 04ba58b
Show file tree
Hide file tree
Showing 10 changed files with 1,604 additions and 184 deletions.
68 changes: 52 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# joppy

Python interface for the [Joplin data API](https://joplinapp.org/api/references/rest_api/).
Python interface for the [Joplin data API](https://joplinapp.org/api/references/rest_api/) (client) and the Joplin server API.

[![build](https://github.com/marph91/joppy/actions/workflows/build.yml/badge.svg)](https://github.com/marph91/joppy/actions/workflows/build.yml)
[![lint](https://github.com/marph91/joppy/actions/workflows/lint.yml/badge.svg)](https://github.com/marph91/joppy/actions/workflows/lint.yml)
Expand All @@ -10,6 +10,13 @@ Python interface for the [Joplin data API](https://joplinapp.org/api/references/
[![https://img.shields.io/badge/Joplin-3.0.15-blueviolet](https://img.shields.io/badge/Joplin-3.0.15-blueviolet)](https://github.com/laurent22/joplin)
[![Python version](https://img.shields.io/pypi/pyversions/joppy.svg)](https://pypi.python.org/pypi/joppy/)

## Features

| | Client API Wrapper | Server API Wrapper |
| --- | --- | --- |
| **Supported** | All functions from the [data API](https://joplinapp.org/help/api/references/rest_api/) | Some reverse engineered functions with a similar interface like the client API wrapper. See the example below and the source code for details. |
| **Not Supported** | - | - Encryption <br>- Some functions that were either to complex or I didn't see a use for automation. |

## :computer: Installation

From pypi:
Expand All @@ -28,6 +35,8 @@ pip install .

## :wrench: Usage

Please backup your data before use!

### General function description

- `add_<type>()`: Create a new element.
Expand All @@ -41,16 +50,18 @@ For details, consult the [implementation](joppy/api.py), [joplin documentation](

## :bulb: Example snippets

### Client API

Start joplin and [get your API token](https://joplinapp.org/api/references/rest_api/#authorisation). Click to expand the examples.

<details>
<summary>Get all notes</summary>

```python name=get_all_notes
from joppy.api import Api
from joppy.client_api import ClientApi

# Create a new Api instance.
api = Api(token=YOUR_TOKEN)
api = ClientApi(token=YOUR_TOKEN)

# Get all notes. Note that this method calls get_notes() multiple times to assemble the unpaginated result.
notes = api.get_all_notes()
Expand All @@ -62,11 +73,11 @@ notes = api.get_all_notes()
<summary>Add a tag to a note</summary>

```python name=add_tag_to_note
from joppy.api import Api
from joppy.client_api import ClientApi

# Create a new Api instance.

api = Api(token=YOUR_TOKEN)
api = ClientApi(token=YOUR_TOKEN)

# Add a notebook.

Expand All @@ -92,11 +103,11 @@ api.add_tag_to_note(tag_id=tag_id, note_id=note_id)
<summary>Add a resource to a note</summary>

```python name=add_resource_to_note
from joppy.api import Api
from joppy.client_api import ClientApi
from joppy import tools

# Create a new Api instance.
api = Api(token=YOUR_TOKEN)
api = ClientApi(token=YOUR_TOKEN)

# Add a notebook.
notebook_id = api.add_notebook(title="My first notebook")
Expand Down Expand Up @@ -124,10 +135,10 @@ Inspired by <https://discourse.joplinapp.org/t/bulk-tag-delete-python-script/549
```python name=remove_tags
import re

from joppy.api import Api
from joppy.client_api import ClientApi

# Create a new Api instance.
api = Api(token=YOUR_TOKEN)
api = ClientApi(token=YOUR_TOKEN)

# Iterate through all tags.
for tag in api.get_all_tags():
Expand All @@ -145,10 +156,10 @@ for tag in api.get_all_tags():
Reference: <https://discourse.joplinapp.org/t/prune-empty-tags-from-web-clipper/36194>

```python name=remove_unused_tags
from joppy.api import Api
from joppy.client_api import ClientApi

# Create a new Api instance.
api = Api(token=YOUR_TOKEN)
api = ClientApi(token=YOUR_TOKEN)

for tag in api.get_all_tags():
notes_for_tag = api.get_all_notes(tag_id=tag.id)
Expand All @@ -167,10 +178,10 @@ Reference: <https://www.reddit.com/r/joplinapp/comments/pozric/batch_remove_spac
```python name=remove_spaces_from_tags
import re

from joppy.api import Api
from joppy.client_api import ClientApi

# Create a new Api instance.
api = Api(token=YOUR_TOKEN)
api = ClientApi(token=YOUR_TOKEN)

# Define the conversion function.
def to_camel_case(name: str) -> str:
Expand All @@ -193,10 +204,10 @@ Note: The note history is not considered. See: <https://discourse.joplinapp.org/
```python name=remove_orphaned_resources
import re

from joppy.api import Api
from joppy.client_api import ClientApi

# Create a new Api instance.
api = Api(token=YOUR_TOKEN)
api = ClientApi(token=YOUR_TOKEN)

# Getting the referenced resource directly doesn't work:
# https://github.com/laurent22/joplin/issues/4535
Expand All @@ -218,7 +229,27 @@ for resource in api.get_all_resources():

</details>

For more usage examples, check the example scripts or [tests](test/test_api.py).
For more usage examples, check the example scripts or [tests](test/test_client_api.py).

### Server API

The server API should work similarly to the client API in most cases. **Be aware that the server API is experimental and may break at any time. I can't provide any help at sync issues or lost data. Make sure you have a backup and know how to restore it.**

```python
from joppy.server_api import ServerApi

# Create a new Api instance.
api = ServerApi(user="admin@localhost", password="admin", url="http://localhost:22300")

# Acquire a lock.
with api.sync_lock():

# Add a notebook.
notebook_id = api.add_notebook(title="My first notebook")

# Add a note in the previously created notebook.
note_id = api.add_note(title="My first note", body="With some content", parent_id=notebook_id)
```

## :newspaper: Examples

Expand Down Expand Up @@ -258,6 +289,11 @@ It's possible to configure the test run via some environment variables:

## :book: Changelog

### Master

- Rename the client API. It should be used by `from joppy.client_api import ClientApi` instead of `from joppy.client_api import ClientApi` now.
- Add support for the server API. It should be used by `from joppy.server_api import ServerApi`.

### 0.2.3

- Don't use the root logger for logging.
Expand Down
37 changes: 11 additions & 26 deletions joppy/api.py → joppy/client_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import time
from typing import (
Any,
Callable,
cast,
Dict,
List,
Expand All @@ -18,6 +17,7 @@
import requests

import joppy.data_types as dt
from joppy import tools


# Use a global session object for better performance.
Expand All @@ -33,12 +33,12 @@


##############################################################################
# Base wrapper that manages the requests to the REST API.
# Base wrapper that manages the requests to the client REST API.
##############################################################################


class ApiBase:
"""Contains the basic requests of the REST API."""
"""Contains the basic requests of the client REST API."""

def __init__(self, token: str, url: str = "http://localhost:41184") -> None:
self.url = url
Expand Down Expand Up @@ -372,7 +372,7 @@ def modify_tag(self, id_: str, **data: dt.JoplinTypes) -> None:
self.put(f"/tags/{id_}", data=data)


class Api(Event, Note, Notebook, Ping, Resource, Revision, Search, Tag):
class ClientApi(Event, Note, Notebook, Ping, Resource, Revision, Search, Tag):
"""
Collects all basic API functions and contains a few more useful methods.
This should be the only class accessed from the users.
Expand Down Expand Up @@ -429,47 +429,32 @@ def delete_all_tags(self) -> None:
assert tag.id is not None
self.delete_tag(tag.id)

@staticmethod
def _unpaginate(
func: Callable[..., dt.DataList[dt.T]], **query: dt.JoplinTypes
) -> List[dt.T]:
"""Calls an Joplin endpoint until it's response doesn't contain more data."""
response = func(**query)
items = response.items
page = 1 # pages are one based
while response.has_more:
page += 1
query["page"] = page
response = func(**query)
items.extend(response.items)
return items

def get_all_events(self, **query: dt.JoplinTypes) -> List[dt.EventData]:
"""Get all events, unpaginated."""
return self._unpaginate(self.get_events, **query)
return tools._unpaginate(self.get_events, **query)

def get_all_notes(self, **query: dt.JoplinTypes) -> List[dt.NoteData]:
"""Get all notes, unpaginated."""
return self._unpaginate(self.get_notes, **query)
return tools._unpaginate(self.get_notes, **query)

def get_all_notebooks(self, **query: dt.JoplinTypes) -> List[dt.NotebookData]:
"""Get all notebooks, unpaginated."""
return self._unpaginate(self.get_notebooks, **query)
return tools._unpaginate(self.get_notebooks, **query)

def get_all_resources(self, **query: dt.JoplinTypes) -> List[dt.ResourceData]:
"""Get all resources, unpaginated."""
return self._unpaginate(self.get_resources, **query)
return tools._unpaginate(self.get_resources, **query)

def get_all_revisions(self, **query: dt.JoplinTypes) -> List[dt.RevisionData]:
"""Get all revisions, unpaginated."""
return self._unpaginate(self.get_revisions, **query)
return tools._unpaginate(self.get_revisions, **query)

def get_all_tags(self, **query: dt.JoplinTypes) -> List[dt.TagData]:
"""Get all tags, unpaginated."""
return self._unpaginate(self.get_tags, **query)
return tools._unpaginate(self.get_tags, **query)

def search_all(
self, **query: dt.JoplinTypes
) -> List[Union[dt.NoteData, dt.NotebookData, dt.ResourceData, dt.TagData]]:
"""Issue a search and get all results, unpaginated."""
return self._unpaginate(self.search, **query) # type: ignore
return tools._unpaginate(self.search, **query) # type: ignore
Loading

0 comments on commit 04ba58b

Please sign in to comment.