Skip to content

Commit

Permalink
Move logic from session (#117)
Browse files Browse the repository at this point in the history
* Fix docs link for external refs

* Refactor paperless to not use Session class

* Refactor tests, remove fastapi deps

* Update poetry.lock

* Remove `PaperlessSession` from module

* Update documentation

---------

Co-authored-by: tb1337 <[email protected]>
  • Loading branch information
tb1337 and tb1337 authored Mar 5, 2024
1 parent 0907038 commit f1eaf0b
Show file tree
Hide file tree
Showing 29 changed files with 1,288 additions and 2,264 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"configurations": [
{
"name": "Python: Aktuelle Datei",
"type": "python",
"type": "debugpy",
"request": "launch",
"program": "++debug.py",
"console": "integratedTerminal",
Expand Down
6 changes: 4 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@

},
"python.testing.pytestArgs": [
"tests"
"tests",
"--no-cov"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
"python.testing.pytestEnabled": true,
"python.analysis.typeCheckingMode": "basic"
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Check out all [contributors here][contributors-url].
[codecov-url]: https://app.codecov.io/gh/tb1337/paperless-api/tree/main
[contributors-tbsch]: https://tbsch.de
[contributors-url]: https://github.com/tb1337/paperless-api/graphs/contributors
[docs-url]: /docs/usage.md
[docs-url]: https://github.com/tb1337/paperless-api/blob/main/docs/usage.md
[license-badge]: https://img.shields.io/github/license/tb1337/paperless-api
[license-url]: /LICENSE.md
[python-badge]: https://img.shields.io/pypi/pyversions/pypaperless
Expand Down
24 changes: 10 additions & 14 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,16 @@ There are some rules for the Paperless-ngx url.

### Custom session

You may want to use a customized session in some cases. The `PaperlessSession` object will pass optional kwargs to each request method call, it is utilizing an `aiohttp.ClientSession` under the hood.
You may want to use an existing `aiohttp.ClientSession` in some cases. Simply pass it to the `Paperless` object.

```python
from pypaperless import Paperless, PaperlessSession

my_session = PaperlessSession("localhost:8000", "your-secret-token", ssl=False, ...)

paperless = Paperless(session=my_session)
```
import aiohttp
from pypaperless import Paperless

You also can implement your own session class. The code of `PaperlessSession` isn't too big and easy to understand. Your custom class must at least implement `__init__` and `request` methods, or simply derive it from `PaperlessSession`.
my_session = aiohttp.ClientSession()
# ...

```python
class MyCustomSession(PaperlessSession):
# start overriding methods
paperless = Paperless("localhost:8000", "your-secret-token", session=my_session)
```

### Creating a token
Expand All @@ -102,17 +97,18 @@ token = Paperless.generate_api_token(
)
```

This method utilizes `PaperlessSession`, so the same rules apply to it as when initiating a regular `Paperless` session. It also accepts a custom `PaperlessSession`:
As for `Paperless` itself, you can provide a custom `aiohttp.ClientSession` object.


```python
url = "localhost:8000"
session = PaperlessSession(url, "") # empty token string
my_session = aiohttp.ClientSession()

token = Paperless.generate_api_token(
"localhost:8000",
"test_user",
"not-so-secret-password-anymore",
session=session,
session=my_session,
)
```

Expand Down
305 changes: 32 additions & 273 deletions poetry.lock

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions pypaperless/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
"""PyPaperless."""

from .api import Paperless
from .sessions import PaperlessSession

__all__ = (
"Paperless",
"PaperlessSession",
)
__all__ = ("Paperless",)
161 changes: 126 additions & 35 deletions pypaperless/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@

from . import helpers
from .const import API_PATH, PaperlessResource
from .exceptions import AuthentificationRequired, BadJsonResponse, JsonResponseWithError
from .sessions import PaperlessSession
from .exceptions import BadJsonResponse, JsonResponseWithError, PaperlessException, RequestException

# from .sessions import PaperlessSession


class Paperless:
Expand Down Expand Up @@ -62,10 +63,12 @@ async def __aexit__(self, *_: object) -> None:

def __init__(
self,
url: str | URL | None = None,
token: str | None = None,
session: PaperlessSession | None = None,
):
url: str | URL,
token: str,
*,
session: aiohttp.ClientSession | None = None,
request_args: dict[str, Any] | None = None,
) -> None:
"""Initialize a `Paperless` instance.
You have to permit either a session, or an url / token pair.
Expand All @@ -74,19 +77,23 @@ def __init__(
`token`: An api token created in Paperless Django settings, or via the helper function.
`session`: A custom `PaperlessSession` object, if existing.
"""
if session is not None:
self._session = session
elif url is not None and token is not None:
self._session = PaperlessSession(url, token)
else:
raise AuthentificationRequired

# if session is not None:
# self._session = session
# elif url is not None and token is not None:
# self._session = PaperlessSession(url, token)
# else:
# raise AuthentificationRequired

self._base_url = self._create_base_url(url)
self._initialized = False
self._local_resources: set[PaperlessResource] = set()
self._remote_resources: set[PaperlessResource] = set()
self._request_args = request_args or {}
self._session = session
self._token = token
self._version: str | None = None

self.logger = logging.getLogger(f"{__package__}[{self._session}]")
self.logger = logging.getLogger(f"{__package__}[{self._base_url.host}]")

@property
def is_initialized(self) -> bool:
Expand All @@ -108,12 +115,56 @@ def remote_resources(self) -> set[PaperlessResource]:
"""Return a set of available resources of the Paperless host."""
return self._remote_resources

@staticmethod
def _create_base_url(url: str | URL) -> URL:
"""Create URL from string or URL and prepare for further use."""
# reverse compatibility, fall back to https
if isinstance(url, str) and "://" not in url:
url = f"https://{url}".rstrip("/")
url = URL(url)

# scheme check. fall back to https
if url.scheme not in ("https", "http"):
url = URL(url).with_scheme("https")

return url

@staticmethod
def _process_form(data: dict[str, Any]) -> aiohttp.FormData:
"""Process form data and create a `aiohttp.FormData` object.
Every field item gets converted to a string-like object.
"""
form = aiohttp.FormData()

def _add_form_value(name: str | None, value: Any) -> Any:
if value is None:
return
params = {}
if isinstance(value, dict):
for dict_key, dict_value in value.items():
_add_form_value(dict_key, dict_value)
return
if isinstance(value, list | set):
for list_value in value:
_add_form_value(name, list_value)
return
if isinstance(value, tuple):
if len(value) == 2:
params["filename"] = f"{value[1]}"
value = value[0]
if name is not None:
form.add_field(name, value if isinstance(value, bytes) else f"{value}", **params)

_add_form_value(None, data)
return form

@staticmethod
async def generate_api_token(
url: str,
username: str,
password: str,
session: PaperlessSession | None = None,
session: aiohttp.ClientSession | None = None,
) -> str:
"""Request Paperless to generate an api token for the given credentials.
Expand All @@ -130,14 +181,15 @@ async def generate_api_token(
# do something
```
"""
session = session or PaperlessSession(url, "")
external_session = session is not None
session = session or aiohttp.ClientSession()
try:
url = url.rstrip("/")
json = {
"username": username,
"password": password,
}
res = await session.request("post", f"{API_PATH['token']}", json=json)
res = await session.request("post", f"{url}{API_PATH['token']}", json=json)
data = await res.json()
res.raise_for_status()
return str(data["token"])
Expand All @@ -148,7 +200,8 @@ async def generate_api_token(
except Exception as exc:
raise exc
finally:
await session.close()
if not external_session:
await session.close()

async def close(self) -> None:
"""Clean up connection."""
Expand Down Expand Up @@ -191,20 +244,55 @@ async def request(
params: dict[str, Any] | None = None,
**kwargs: Any,
) -> AsyncGenerator[aiohttp.ClientResponse, None]:
"""Perform a request."""
if method == "post":
pass
res = await self._session.request(
method,
path,
json=json,
data=data,
form=form,
params=params,
**kwargs,
"""Send a request to the Paperless api and return the `aiohttp.ClientResponse`.
This method provides a little interface for utilizing `aiohttp.FormData`.
`method`: A http method: get, post, patch, put, delete, head, options
`path`: A path to the endpoint or a string url.
`json`: A dict containing the json data.
`data`: A dict containing the data to send in the request body.
`form`: A dict with form data, which gets converted to `aiohttp.FormData`
and replaces `data`.
`params`: A dict with query parameters.
`kwargs`: Optional attributes for the `aiohttp.ClientSession.request` method.
"""
if self._session is None:
self._session = aiohttp.ClientSession()

# add headers
self._session.headers.update(
{
"Accept": "application/json; version=2",
"Authorization": f"Token {self._token}",
}
)
self.logger.debug("%s (%d): %s", method.upper(), res.status, res.url)
yield res

# add request args
kwargs.update(self._request_args)

# overwrite data with a form, when there is a form payload
if isinstance(form, dict):
data = self._process_form(form)

# add base path
url = f"{self._base_url}{path}" if not path.startswith("http") else path

try:
res = await self._session.request(
method=method,
url=url,
json=json,
data=data,
params=params,
**kwargs,
)
self.logger.debug("%s (%d): %s", method.upper(), res.status, res.url)
yield res
except PaperlessException:
raise
except Exception as exc:
raise RequestException(exc, (method, url, params), kwargs) from None

async def request_json(
self,
Expand All @@ -217,11 +305,14 @@ async def request_json(
try:
assert res.content_type == "application/json"
payload = await res.json()

if res.status == 400:
raise JsonResponseWithError(payload)

res.raise_for_status()
except (AssertionError, ValueError) as exc:
raise BadJsonResponse(res) from exc

if res.status == 400:
raise JsonResponseWithError(payload)
res.raise_for_status()
except Exception as exc:
raise exc

return payload
3 changes: 2 additions & 1 deletion pypaperless/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ class JsonResponseWithError(PaperlessException):

def __init__(self, payload: Any) -> None:
"""Initialize a `JsonResponseWithError` instance."""
message: Any = "Unknown error"
key: str = "error"
message: Any = "unknown error"

if isinstance(payload, dict):
key = "error" if "error" in payload else set(payload.keys()).pop()
Expand Down
2 changes: 1 addition & 1 deletion pypaperless/models/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ async def get_next_asn(self) -> int:
raise AsnRequestError from exc

async def more_like(self, pk: int) -> AsyncGenerator[Document, None]:
"""Lookup more documents similar to the given document pk.
"""Lookup documents similar to the given document pk.
Shortcut function. Same behaviour is possible using `reduce()`.
Expand Down
Loading

0 comments on commit f1eaf0b

Please sign in to comment.