mirror of
https://github.com/subutux/rmapy.git
synced 2025-12-09 07:03:24 +00:00
Compare commits
9 Commits
v0.2.1
...
bsdz-patch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5e4972775 | ||
|
|
cca7ea05fd | ||
|
|
48239c9b9f | ||
|
|
3554640693 | ||
|
|
f20916bd34 | ||
|
|
7d70d58f6e | ||
|
|
506b34d40a | ||
|
|
3f0e0e36a2 | ||
|
|
00dc50550f |
@@ -1,4 +1,4 @@
|
||||
# rMapi
|
||||
# rMapy
|
||||
This is an (unofficial) Remarkable Cloud API Client written in Python.
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ sys.path.insert(0, os.path.abspath('../..'))
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'rmapi'
|
||||
project = 'rmapy'
|
||||
copyright = '2019, Stijn Van Campenhout'
|
||||
author = 'Stijn Van Campenhout'
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.. rmapi documentation master file, created by
|
||||
.. rmapy documentation master file, created by
|
||||
sphinx-quickstart on Tue Sep 17 19:24:29 2019.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to rmapi's documentation!
|
||||
Welcome to rmapy's documentation!
|
||||
=================================
|
||||
|
||||
This is an (unofficial) Remarkable Cloud API Client written in Python.
|
||||
@@ -50,9 +50,9 @@ API Documentation
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
rmapi
|
||||
rmapy
|
||||
|
||||
.. automodule:: rmapi
|
||||
.. automodule:: rmapy
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
Pip
|
||||
===
|
||||
|
||||
Like any other package, you can install rmapi using pip:
|
||||
Like any other package, you can install rmapy using pip:
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install rmapi
|
||||
pip install rmapy
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
rmapi
|
||||
rmapy
|
||||
=====
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
rmapi
|
||||
rmapy
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
quickstart
|
||||
quick start
|
||||
==========
|
||||
|
||||
If you previously used the go package `rmapi`_ ,the keys for authorization
|
||||
@@ -12,7 +12,7 @@ If not, you'll need to register the client as a new device on `my remarkable`_.
|
||||
.. _rmapi: https://github.com/juruen/rmapi
|
||||
|
||||
|
||||
Registering the API CLient
|
||||
Registering the API Client
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Registering the device is easy. Go to `my remarkable`_ to register a new device
|
||||
@@ -22,20 +22,20 @@ and use the code you see on the webpage
|
||||
:linenos:
|
||||
|
||||
|
||||
from rmapi.api import Client
|
||||
from rmapy.api import Client
|
||||
|
||||
rmapi = Client()
|
||||
# Shoud return False
|
||||
rmapi.is_authenticated()
|
||||
rmapy = Client()
|
||||
# Should return False
|
||||
rmapy.is_auth()
|
||||
# This registers the client as a new device. The received device token is
|
||||
# stored in the users directory in the file ~/.rmapi, the same as with the
|
||||
# go rmapi client.
|
||||
rmapi.register_device("fkgzzklrs")
|
||||
# It's always a good idea to refresh the user token everytime you start
|
||||
rmapy.register_device("fkgzzklrs")
|
||||
# It's always a good idea to refresh the user token every time you start
|
||||
# a new session.
|
||||
rmapi.refresh_token()
|
||||
# Shoud return True
|
||||
rmapi.is_authenticated()
|
||||
rmapy.refresh_token()
|
||||
# Should return True
|
||||
rmapy.is_auth()
|
||||
|
||||
Working with items
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
@@ -50,21 +50,21 @@ We can list the items in the Cloud
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
>>> from rmapi.api import Client
|
||||
>>> rmapi = Client()
|
||||
>>> rmapi.renew_token()
|
||||
>>> from rmapy.api import Client
|
||||
>>> rmapy = Client()
|
||||
>>> rmapy.renew_token()
|
||||
True
|
||||
>>> collection = rmapi.get_meta_items()
|
||||
>>> collection = rmapy.get_meta_items()
|
||||
>>> collection
|
||||
<rmapi.collections.Collection object at 0x7fa1982d7e90>
|
||||
<rmapy.collections.Collection object at 0x7fa1982d7e90>
|
||||
>>> len(collection)
|
||||
181
|
||||
>>> # Count the amount of documents
|
||||
... from rmapi.document import Document
|
||||
... from rmapy.document import Document
|
||||
>>> len([f for f in collection if isinstance(f, Document)])
|
||||
139
|
||||
>>> # Count the amount of folders
|
||||
... from rmapi.folder import Folder
|
||||
... from rmapy.folder import Folder
|
||||
>>> len([f for f in collection if isinstance(f, Folder)])
|
||||
42
|
||||
|
||||
@@ -74,7 +74,7 @@ DocumentType
|
||||
````````````
|
||||
|
||||
A DocumentType is a document. This can be a pdf, epub or notebook.
|
||||
These types are represented by the object :class:`rmapi.document.Document`
|
||||
These types are represented by the object :class:`rmapy.document.Document`
|
||||
|
||||
|
||||
Changing the metadata is easy
|
||||
@@ -83,28 +83,28 @@ Changing the metadata is easy
|
||||
:linenos:
|
||||
|
||||
|
||||
>>> from rmapi.api import Client
|
||||
>>> rmapi = Client()
|
||||
>>> rmapi.renew_token()
|
||||
>>> from rmapy.api import Client
|
||||
>>> rmapy = Client()
|
||||
>>> rmapy.renew_token()
|
||||
True
|
||||
>>> collection = rmapi.get_meta_items()
|
||||
>>> collection = rmapy.get_meta_items()
|
||||
>>> doc = [ d for d in collection if d.VissibleName == 'ModernC'][0]
|
||||
>>> doc
|
||||
<rmapi.document.Document a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3>
|
||||
<rmapy.document.Document a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3>
|
||||
>>> doc.to_dict()
|
||||
{'ID': 'a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3', 'Version': 1, 'Message': '', 'Succes': True, 'BlobURLGet': '', 'BlobURLGetExpires': '0001-01-01T00:00:00Z', 'BlobURLPut': '', 'BlobURLPutExpires': '', 'ModifiedClient': '2019-09-18T20:12:07.206206Z', 'Type': 'DocumentType', 'VissibleName': 'ModernC', 'CurrentPage': 0, 'Bookmarked': False, 'Parent': ''}
|
||||
>>> doc.VissibleName = "Modern C: The book of wisdom"
|
||||
>>> # push the changes back to the Remarkable Cloud
|
||||
... rmapi.update_metadata(doc)
|
||||
... rmapy.update_metadata(doc)
|
||||
True
|
||||
>>> collection = rmapi.get_meta_items()
|
||||
>>> collection = rmapy.get_meta_items()
|
||||
>>> doc = [ d for d in docs if d.VissibleName == 'ModernC'][0]
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
IndexError: list index out of range
|
||||
>>> doc = [ d for d in docs if d.VissibleName == 'Modern C: The book of wisdom'][0]
|
||||
>>> doc
|
||||
<rmapi.document.Document a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3>
|
||||
<rmapy.document.Document a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3>
|
||||
>>> doc.to_dict()
|
||||
{'ID': 'a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3', 'Version': 1, 'Message': '', 'Succes': True, 'BlobURLGet': '', 'BlobURLGetExpires': '0001-01-01T00:00:00Z', 'BlobURLPut': '', 'BlobURLPutExpires': '', 'ModifiedClient': '2019-09-18T20:12:07.206206Z', 'Type': 'DocumentType', 'VissibleName': 'Modern C: The book of wisdom', 'CurrentPage': 0, 'Bookmarked': False, 'Parent': ''}
|
||||
|
||||
@@ -114,7 +114,7 @@ CollectionType
|
||||
|
||||
A CollectionType is a Folder.
|
||||
|
||||
These types are represented by the object :class:`rmapi.folder.Folder`
|
||||
These types are represented by the object :class:`rmapy.folder.Folder`
|
||||
|
||||
Working with folders is easy!
|
||||
|
||||
@@ -122,42 +122,42 @@ Working with folders is easy!
|
||||
:linenos:
|
||||
|
||||
|
||||
>>> from rmapi.api import Client
|
||||
>>> rmapi = Client()
|
||||
>>> rmapi.renew_token()
|
||||
>>> from rmapy.api import Client
|
||||
>>> rmapy = Client()
|
||||
>>> rmapy.renew_token()
|
||||
True
|
||||
>>> collection = rmapi.get_meta_items()
|
||||
>>> collection = rmapy.get_meta_items()
|
||||
>>> collection
|
||||
<rmapi.collections.Collection object at 0x7fc4718e1ed0>
|
||||
>>> from rmapi.folder import Folder
|
||||
<rmapy.collections.Collection object at 0x7fc4718e1ed0>
|
||||
>>> from rmapy.folder import Folder
|
||||
>>> # Get all the folders. Note that the fs of Remarkable is flat in the cloud
|
||||
... folders = [ f for f in collection if isinstance(f, Folder) ]
|
||||
>>> folders
|
||||
[<rmapi.folder.Folder 028400f5-b258-4563-bf5d-9a47c314668c>, <rmapi.folder.Folder 06a36729-f91e-47da-b334-dc088c1e73d2>, ...]
|
||||
[<rmapy.folder.Folder 028400f5-b258-4563-bf5d-9a47c314668c>, <rmapy.folder.Folder 06a36729-f91e-47da-b334-dc088c1e73d2>, ...]
|
||||
>>> # Get the root folders
|
||||
... root = [ f for f in folders if f.Parent == "" ]
|
||||
>>> root
|
||||
[<rmapi.folder.Folder 028400f5-b258-4563-bf5d-9a47c314668c>, <rmapi.folder.Folder 5005a085-d7ee-4867-8859-4cd90dee0d62>, ...]
|
||||
[<rmapy.folder.Folder 028400f5-b258-4563-bf5d-9a47c314668c>, <rmapy.folder.Folder 5005a085-d7ee-4867-8859-4cd90dee0d62>, ...]
|
||||
>>> # Create a new folder
|
||||
... new_folder = Folder("New Folder")
|
||||
>>> new_folder
|
||||
<rmapi.folder.Folder 579df08d-7ee4-4f30-9994-887e6341cae3>
|
||||
>>> rmapi.create_folder(new_folder)
|
||||
<rmapy.folder.Folder 579df08d-7ee4-4f30-9994-887e6341cae3>
|
||||
>>> rmapy.create_folder(new_folder)
|
||||
True
|
||||
>>> # verify
|
||||
... [ f for f in rmapi.get_meta_items() if f.VissibleName == "New Folder" ]
|
||||
[<rmapi.folder.Folder 579df08d-7ee4-4f30-9994-887e6341cae3>]
|
||||
>>> [ f for f in rmapi.get_meta_items() if f.VissibleName == "New Folder" ][0].ID == new_folder.ID
|
||||
... [ f for f in rmapy.get_meta_items() if f.VissibleName == "New Folder" ]
|
||||
[<rmapy.folder.Folder 579df08d-7ee4-4f30-9994-887e6341cae3>]
|
||||
>>> [ f for f in rmapy.get_meta_items() if f.VissibleName == "New Folder" ][0].ID == new_folder.ID
|
||||
True
|
||||
>>> # Move a document in a folder
|
||||
... doc = rmapi.get_doc("a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3")
|
||||
... doc = rmapy.get_doc("a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3")
|
||||
>>> doc
|
||||
<rmapi.document.Document a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3>
|
||||
<rmapy.document.Document a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3>
|
||||
>>> doc.Parent = new_folder.ID
|
||||
>>> # Submit the changes
|
||||
... rmapi.update_metadata(doc)
|
||||
... rmapy.update_metadata(doc)
|
||||
True
|
||||
>>> doc = rmapi.get_doc("a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3")
|
||||
>>> doc = rmapy.get_doc("a969fcd6-64b0-4f71-b1ce-d9533ec4a2a3")
|
||||
>>> doc.Parent == new_folder.ID
|
||||
True
|
||||
|
||||
@@ -193,14 +193,14 @@ the remarkable file format:
|
||||
:linenos:
|
||||
|
||||
|
||||
>>> from rmapi.document import ZipDocument
|
||||
>>> from rmapi.api import Client
|
||||
>>> from rmapy.document import ZipDocument
|
||||
>>> from rmapy.api import Client
|
||||
>>> rm = Client()
|
||||
>>> rm.renew_token()
|
||||
True
|
||||
>>> rawDocument = ZipDocument(doc="/home/svancampenhout/27-11-2019.pdf")
|
||||
>>> rawDocument
|
||||
<rmapi.document.ZipDocument b926ffc2-3600-460e-abfa-0fcf20b0bf99>
|
||||
<rmapy.document.ZipDocument b926ffc2-3600-460e-abfa-0fcf20b0bf99>
|
||||
>>> rawDocument.metadata["VissibleName"]
|
||||
'27-11-2019'
|
||||
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
rmapi package
|
||||
rmapy package
|
||||
=============
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
rmapi.api module
|
||||
rmapy.api module
|
||||
----------------
|
||||
|
||||
.. automodule:: rmapi.api
|
||||
.. automodule:: rmapy.api
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
rmapi.collections module
|
||||
rmapy.collections module
|
||||
------------------------
|
||||
|
||||
.. automodule:: rmapi.collections
|
||||
.. automodule:: rmapy.collections
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
rmapi.config module
|
||||
rmapy.config module
|
||||
-------------------
|
||||
|
||||
.. automodule:: rmapi.config
|
||||
.. automodule:: rmapy.config
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
rmapi.const module
|
||||
rmapy.const module
|
||||
------------------
|
||||
|
||||
.. automodule:: rmapi.const
|
||||
.. automodule:: rmapy.const
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
rmapi.document module
|
||||
rmapy.document module
|
||||
---------------------
|
||||
|
||||
.. automodule:: rmapi.document
|
||||
.. automodule:: rmapy.document
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
rmapi.exceptions module
|
||||
rmapy.exceptions module
|
||||
-----------------------
|
||||
|
||||
.. automodule:: rmapi.exceptions
|
||||
.. automodule:: rmapy.exceptions
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
rmapi.folder module
|
||||
rmapy.folder module
|
||||
-------------------
|
||||
|
||||
.. automodule:: rmapi.folder
|
||||
.. automodule:: rmapy.folder
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
rmapi.meta module
|
||||
rmapy.meta module
|
||||
-----------------
|
||||
|
||||
.. automodule:: rmapi.meta
|
||||
.. automodule:: rmapy.meta
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
rmapi.types module
|
||||
rmapy.types module
|
||||
------------------
|
||||
|
||||
.. automodule:: rmapi.types
|
||||
.. automodule:: rmapy.types
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
@@ -80,7 +80,7 @@ rmapi.types module
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: rmapi
|
||||
.. automodule:: rmapy
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import requests
|
||||
from logging import getLogger
|
||||
from datetime import datetime
|
||||
import json
|
||||
from typing import Union, Optional
|
||||
from uuid import uuid4
|
||||
from .collections import Collection
|
||||
@@ -20,19 +19,15 @@ from .const import (RFC3339Nano,
|
||||
USER_TOKEN_URL,
|
||||
DEVICE,)
|
||||
|
||||
log = getLogger("rmapipy.rmapi")
|
||||
|
||||
log = getLogger("rmapy")
|
||||
DocumentOrFolder = Union[Document, Folder]
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""API Client for Remarkable Cloud
|
||||
|
||||
This allows you to authenticate & communiticate with the Remarkable Cloud
|
||||
This allows you to authenticate & communicate with the Remarkable Cloud
|
||||
and does all the heavy lifting for you.
|
||||
|
||||
Attributes:
|
||||
token_set: the authentication tokens
|
||||
|
||||
"""
|
||||
|
||||
token_set = {
|
||||
@@ -49,7 +44,7 @@ class Client(object):
|
||||
|
||||
def request(self, method: str, path: str,
|
||||
data=None,
|
||||
body=None, headers={},
|
||||
body=None, headers=None,
|
||||
params=None, stream=False) -> requests.Response:
|
||||
"""Creates a request against the Remarkable Cloud API
|
||||
|
||||
@@ -63,12 +58,14 @@ class Client(object):
|
||||
body: the body to request with. This will be converted to json.
|
||||
headers: a dict of additional headers to add to the request.
|
||||
params: Query params to append to the request.
|
||||
steam: Should the response be a stream?
|
||||
stream: Should the response be a stream?
|
||||
Returns:
|
||||
A Response instance containing most likely the response from
|
||||
the server.
|
||||
"""
|
||||
|
||||
if headers is None:
|
||||
headers = {}
|
||||
if not path.startswith("http"):
|
||||
if not path.startswith('/'):
|
||||
path = '/' + path
|
||||
@@ -118,7 +115,7 @@ class Client(object):
|
||||
"deviceID": uuid,
|
||||
|
||||
}
|
||||
response = self.request("POST", DEVICE_TOKEN_URL, body)
|
||||
response = self.request("POST", DEVICE_TOKEN_URL, body=body)
|
||||
if response.ok:
|
||||
self.token_set["devicetoken"] = response.text
|
||||
dump(self.token_set)
|
||||
@@ -185,13 +182,13 @@ class Client(object):
|
||||
|
||||
return collection
|
||||
|
||||
def get_doc(self, ID: str) -> Optional[DocumentOrFolder]:
|
||||
def get_doc(self, _id: str) -> Optional[DocumentOrFolder]:
|
||||
"""Get a meta item by ID
|
||||
|
||||
Fetch a meta item from the Remarkable Cloud by ID.
|
||||
|
||||
Args:
|
||||
ID: The id of the meta item.
|
||||
_id: The id of the meta item.
|
||||
|
||||
Returns:
|
||||
A Document or Folder instance of the requested ID.
|
||||
@@ -199,10 +196,10 @@ class Client(object):
|
||||
DocumentNotFound: When a document cannot be found.
|
||||
"""
|
||||
|
||||
log.debug(f"GETTING DOC {ID}")
|
||||
log.debug(f"GETTING DOC {_id}")
|
||||
response = self.request("GET", "/document-storage/json/2/docs",
|
||||
params={
|
||||
"doc": ID,
|
||||
"doc": _id,
|
||||
"withBlob": True
|
||||
})
|
||||
log.debug(response.url)
|
||||
@@ -215,7 +212,7 @@ class Client(object):
|
||||
elif data_response[0]["Type"] == "DocumentType":
|
||||
return Document(**data_response[0])
|
||||
else:
|
||||
raise DocumentNotFound(f"Cound not find document {ID}")
|
||||
raise DocumentNotFound(f"Could not find document {_id}")
|
||||
return None
|
||||
|
||||
def download(self, document: Document) -> ZipDocument:
|
||||
@@ -251,7 +248,7 @@ class Client(object):
|
||||
Args:
|
||||
doc: A Document or folder to delete.
|
||||
Raises:
|
||||
ApiError: an error occured while uploading the document.
|
||||
ApiError: an error occurred while uploading the document.
|
||||
"""
|
||||
|
||||
response = self.request("PUT", "/document-storage/json/2/delete",
|
||||
@@ -260,28 +257,29 @@ class Client(object):
|
||||
"Version": doc.Version
|
||||
}])
|
||||
|
||||
return self.check_reponse(response)
|
||||
return self.check_response(response)
|
||||
|
||||
def upload(self, zipDoc: ZipDocument, to: Folder = Folder(ID="")):
|
||||
def upload(self, zip_doc: ZipDocument, to: Folder = Folder(ID="")):
|
||||
"""Upload a document to the cloud.
|
||||
|
||||
Add a new document to the Remarkable Cloud.
|
||||
|
||||
Args:
|
||||
zipDoc: A ZipDocument instance containing the data of a Document.
|
||||
zip_doc: A ZipDocument instance containing the data of a Document.
|
||||
to: the parent of the document. (Default root)
|
||||
Raises:
|
||||
ApiError: an error occured while uploading the document.
|
||||
ApiError: an error occurred while uploading the document.
|
||||
|
||||
"""
|
||||
|
||||
BlobURLPut = self._upload_request(zipDoc)
|
||||
zipDoc.dump(zipDoc.zipfile)
|
||||
response = self.request("PUT", BlobURLPut, data=zipDoc.zipfile.read())
|
||||
blob_url_put = self._upload_request(zip_doc)
|
||||
zip_doc.dump(zip_doc.zipfile)
|
||||
response = self.request("PUT", blob_url_put, data=zip_doc.zipfile.read())
|
||||
# Reset seek
|
||||
zipDoc.zipfile.seek(0)
|
||||
zip_doc.zipfile.seek(0)
|
||||
if response.ok:
|
||||
doc = Document(**zipDoc.metadata)
|
||||
doc.ID = zipDoc.ID
|
||||
doc = Document(**zip_doc.metadata)
|
||||
doc.ID = zip_doc.ID
|
||||
doc.Parent = to.ID
|
||||
return self.update_metadata(doc)
|
||||
else:
|
||||
@@ -305,7 +303,7 @@ class Client(object):
|
||||
"/document-storage/json/2/upload/update-status",
|
||||
body=[req])
|
||||
|
||||
return self.check_reponse(res)
|
||||
return self.check_response(res)
|
||||
|
||||
def get_current_version(self, docorfolder: DocumentOrFolder) -> int:
|
||||
"""Get the latest version info from a Document or Folder
|
||||
@@ -319,7 +317,7 @@ class Client(object):
|
||||
the version information.
|
||||
Raises:
|
||||
DocumentNotFound: cannot find the requested Document or Folder.
|
||||
ApiError: An error occured while processing the request.
|
||||
ApiError: An error occurred while processing the request.
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -330,8 +328,8 @@ class Client(object):
|
||||
return 0
|
||||
return int(d.Version)
|
||||
|
||||
def _upload_request(self, zdoc: ZipDocument) -> dict:
|
||||
zipFile, req = zdoc.create_request()
|
||||
def _upload_request(self, zip_doc: ZipDocument) -> str:
|
||||
zip_file, req = zip_doc.create_request()
|
||||
res = self.request("PUT", "/document-storage/json/2/upload/request",
|
||||
body=[req])
|
||||
if not res.ok:
|
||||
@@ -363,7 +361,7 @@ class Client(object):
|
||||
True if the folder is created.
|
||||
"""
|
||||
|
||||
zipFolder, req = folder.create_request()
|
||||
zip_folder, req = folder.create_request()
|
||||
res = self.request("PUT", "/document-storage/json/2/upload/request",
|
||||
body=[req])
|
||||
if not res.ok:
|
||||
@@ -374,7 +372,7 @@ class Client(object):
|
||||
if len(response) > 0:
|
||||
dest = response[0].get("BlobURLPut", None)
|
||||
if dest:
|
||||
res = self.request("PUT", dest, data=zipFolder.read())
|
||||
res = self.request("PUT", dest, data=zip_folder.read())
|
||||
else:
|
||||
raise ApiError(
|
||||
"Cannot create a folder. because BlobURLPut is not set",
|
||||
@@ -383,7 +381,8 @@ class Client(object):
|
||||
self.update_metadata(folder)
|
||||
return True
|
||||
|
||||
def check_reponse(self, response: requests.Response):
|
||||
@staticmethod
|
||||
def check_response(response: requests.Response):
|
||||
"""Check the response from an API Call
|
||||
|
||||
Does some sanity checking on the Response
|
||||
@@ -417,4 +416,3 @@ class Client(object):
|
||||
raise ApiError(
|
||||
f"Got An invalid HTTP Response: {response.status_code}",
|
||||
response=response)
|
||||
return True
|
||||
@@ -9,7 +9,7 @@ DocumentOrFolder = Union[Document, Folder]
|
||||
class Collection(object):
|
||||
"""A collection of meta items
|
||||
|
||||
This is basicly the content of the Remarkable Cloud.
|
||||
This is basically the content of the Remarkable Cloud.
|
||||
|
||||
Attributes:
|
||||
items: A list containing the items.
|
||||
@@ -21,59 +21,59 @@ class Collection(object):
|
||||
for i in items:
|
||||
self.items.append(i)
|
||||
|
||||
def add(self, docdict: dict) -> None:
|
||||
def add(self, doc_dict: dict) -> None:
|
||||
"""Add an item to the collection.
|
||||
It wraps it in the correct class based on the Type parameter of the
|
||||
dict.
|
||||
|
||||
Args:
|
||||
docdict: A dict representing a document or folder.
|
||||
doc_dict: A dict representing a document or folder.
|
||||
"""
|
||||
|
||||
if docdict.get("Type", None) == "DocumentType":
|
||||
self.add_document(docdict)
|
||||
elif docdict.get("Type", None) == "CollectionType":
|
||||
self.add_folder(docdict)
|
||||
if doc_dict.get("Type", None) == "DocumentType":
|
||||
self.add_document(doc_dict)
|
||||
elif doc_dict.get("Type", None) == "CollectionType":
|
||||
self.add_folder(doc_dict)
|
||||
else:
|
||||
raise TypeError("Unsupported type: {_type}"
|
||||
.format(_type=docdict.get("Type", None)))
|
||||
.format(_type=doc_dict.get("Type", None)))
|
||||
|
||||
def add_document(self, docdict: dict) -> None:
|
||||
def add_document(self, doc_dict: dict) -> None:
|
||||
"""Add a document to the collection
|
||||
|
||||
Args:
|
||||
docdict: A dict respresenting a document.
|
||||
doc_dict: A dict representing a document.
|
||||
"""
|
||||
|
||||
self.items.append(Document(**docdict))
|
||||
self.items.append(Document(**doc_dict))
|
||||
|
||||
def add_folder(self, dirdict: dict) -> None:
|
||||
def add_folder(self, dir_dict: dict) -> None:
|
||||
"""Add a document to the collection
|
||||
|
||||
Args:
|
||||
dirdict: A dict respresenting a folder.
|
||||
dir_dict: A dict representing a folder.
|
||||
"""
|
||||
|
||||
self.items.append(Folder(**dirdict))
|
||||
self.items.append(Folder(**dir_dict))
|
||||
|
||||
def parent(self, docorfolder: DocumentOrFolder) -> Folder:
|
||||
def parent(self, doc_or_folder: DocumentOrFolder) -> Folder:
|
||||
"""Returns the paren of a Document or Folder
|
||||
|
||||
Args:
|
||||
docorfolder: A document or folder to get the parent from
|
||||
doc_or_folder: A document or folder to get the parent from
|
||||
|
||||
Returns:
|
||||
The parent folder.
|
||||
"""
|
||||
|
||||
results = [i for i in self.items if i.ID == docorfolder.ID]
|
||||
results = [i for i in self.items if i.ID == doc_or_folder.ID]
|
||||
if len(results) > 0 and isinstance(results[0], Folder):
|
||||
return results[0]
|
||||
else:
|
||||
raise FolderNotFound("Could not found the parent of the document.")
|
||||
|
||||
def children(self, folder: Folder = None) -> List[DocumentOrFolder]:
|
||||
"""Get all the childern from a folder
|
||||
"""Get all the children from a folder
|
||||
|
||||
Args:
|
||||
folder: A folder where to get the children from. If None, this will
|
||||
@@ -6,7 +6,7 @@ from typing import Dict
|
||||
|
||||
|
||||
def load() -> dict:
|
||||
"""Load the .rmapi config file"""
|
||||
"""Load the .rmapy config file"""
|
||||
|
||||
config_file_path = Path.joinpath(Path.home(), ".rmapi")
|
||||
config: Dict[str, str] = {}
|
||||
@@ -18,7 +18,7 @@ def load() -> dict:
|
||||
|
||||
|
||||
def dump(config: dict) -> None:
|
||||
"""Dump config to the .rmapi config file
|
||||
"""Dump config to the .rmapy config file
|
||||
|
||||
Args:
|
||||
config: A dict containing data to dump to the .rmapi
|
||||
@@ -1,7 +1,7 @@
|
||||
RFC3339Nano = "%Y-%m-%dT%H:%M:%SZ"
|
||||
USER_AGENT = "rmapipy"
|
||||
USER_AGENT = "rmapy"
|
||||
BASE_URL = "https://document-storage-production-dot-remarkable-production.appspot.com" # noqa
|
||||
DEVICE_TOKEN_URL = "https://my.remarkable.com/token/json/2/device/new"
|
||||
USER_TOKEN_URL = "https://my.remarkable.com/token/json/2/user/new"
|
||||
DEVICE = "desktop-windows"
|
||||
|
||||
SERVICE_MGR_URL = "https://service-manager-production-dot-remarkable-production.appspot.com" # noqa
|
||||
@@ -4,12 +4,11 @@ from zipfile import ZipFile, ZIP_DEFLATED
|
||||
import shutil
|
||||
from uuid import uuid4
|
||||
import json
|
||||
from typing import NoReturn, TypeVar, List, Tuple
|
||||
from typing import TypeVar, List, Tuple
|
||||
from requests import Response
|
||||
from .meta import Meta
|
||||
|
||||
BytesOrString = TypeVar("BytesOrString", BytesIO, str)
|
||||
BytesOrNone = TypeVar("BytesOrNone", BytesIO, None)
|
||||
|
||||
|
||||
class RmPage(object):
|
||||
@@ -18,7 +17,7 @@ class RmPage(object):
|
||||
Contains the metadata, the page itself & thumbnail.
|
||||
|
||||
"""
|
||||
def __init__(self, page, metadata=None, order=0, thumbnail=None, ID=None):
|
||||
def __init__(self, page, metadata=None, order=0, thumbnail=None, _id=None):
|
||||
self.page = page
|
||||
if metadata:
|
||||
self.metadata = metadata
|
||||
@@ -28,14 +27,14 @@ class RmPage(object):
|
||||
self.order = order
|
||||
if thumbnail:
|
||||
self.thumbnail = thumbnail
|
||||
if ID:
|
||||
self.ID = ID
|
||||
if _id:
|
||||
self.ID = _id
|
||||
else:
|
||||
self.ID = str(uuid4())
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of this object"""
|
||||
return f"<rmapi.document.RmPage {self.order} for {self.ID}>"
|
||||
return f"<rmapy.document.RmPage {self.order} for {self.ID}>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of this object"""
|
||||
@@ -51,7 +50,7 @@ class Document(Meta):
|
||||
Attributes:
|
||||
ID: Id of the meta object.
|
||||
Version: The version of this object.
|
||||
Success: If the last API Call was a succes.
|
||||
Success: If the last API Call was a success.
|
||||
BlobURLGet: The url to get the data blob from. Can be empty.
|
||||
BlobURLGetExpires: The expiration date of the Get url.
|
||||
BlobURLPut: The url to upload the data blob to. Can be empty.
|
||||
@@ -73,7 +72,7 @@ class Document(Meta):
|
||||
|
||||
def __str__(self):
|
||||
"""String representation of this object"""
|
||||
return f"<rmapi.document.Document {self.ID}>"
|
||||
return f"<rmapy.document.Document {self.ID}>"
|
||||
|
||||
def __repr__(self):
|
||||
"""String representation of this object"""
|
||||
@@ -109,7 +108,7 @@ class ZipDocument(object):
|
||||
zipfile: The raw zipfile in memory.
|
||||
pdf: the raw pdf file if there is one.
|
||||
epub: the raw epub file if there is one.
|
||||
rm: A list of :class:rmapi.document.RmPage in this zip.
|
||||
rm: A list of :class:rmapy.document.RmPage in this zip.
|
||||
|
||||
"""
|
||||
# {"extraMetadata": {},
|
||||
@@ -179,17 +178,17 @@ class ZipDocument(object):
|
||||
rm: List[RmPage] = []
|
||||
ID = None
|
||||
|
||||
def __init__(self, ID=None, doc=None, file=None):
|
||||
def __init__(self, _id=None, doc=None, file=None):
|
||||
"""Create a new instance of a ZipDocument
|
||||
|
||||
Args:
|
||||
ID: Can be left empty to generate one
|
||||
_id: Can be left empty to generate one
|
||||
doc: a raw pdf, epub or rm (.lines) file.
|
||||
file: a zipfile to convert from
|
||||
"""
|
||||
if not ID:
|
||||
ID = str(uuid4())
|
||||
self.ID = ID
|
||||
if not _id:
|
||||
_id = str(uuid4())
|
||||
self.ID = _id
|
||||
if doc:
|
||||
ext = doc[-4:]
|
||||
if ext.endswith("pdf"):
|
||||
@@ -207,7 +206,7 @@ class ZipDocument(object):
|
||||
elif ext.endswith("rm"):
|
||||
self.content["fileType"] = "notebook"
|
||||
with open(doc, 'rb') as fb:
|
||||
self.rm.append(RmPage(page=BytesIO(doc.read())))
|
||||
self.rm.append(RmPage(page=BytesIO(fb.read())))
|
||||
name = os.path.splitext(os.path.basename(doc))[0]
|
||||
self.metadata["VissibleName"] = name
|
||||
|
||||
@@ -216,7 +215,7 @@ class ZipDocument(object):
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""string representation of this class"""
|
||||
return f"<rmapi.document.ZipDocument {self.ID}>"
|
||||
return f"<rmapy.document.ZipDocument {self.ID}>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""string representation of this class"""
|
||||
@@ -316,10 +315,8 @@ class ZipDocument(object):
|
||||
pages = [x for x in zf.namelist()
|
||||
if x.startswith(f"{self.ID}/") and x.endswith('.rm')]
|
||||
for p in pages:
|
||||
pagenumber = int(p.replace(f"{self.ID}/", "")
|
||||
page_number = int(p.replace(f"{self.ID}/", "")
|
||||
.replace(".rm", ""))
|
||||
page = BytesIO()
|
||||
thumbnail = BytesIO()
|
||||
with zf.open(p, 'r') as rm:
|
||||
page = BytesIO(rm.read())
|
||||
page.seek(0)
|
||||
@@ -331,34 +328,34 @@ class ZipDocument(object):
|
||||
thumbnail = BytesIO(tn.read())
|
||||
thumbnail.seek(0)
|
||||
|
||||
self.rm.append(RmPage(page, metadata, pagenumber, thumbnail,
|
||||
self.rm.append(RmPage(page, metadata, page_number, thumbnail,
|
||||
self.ID))
|
||||
|
||||
self.zipfile.seek(0)
|
||||
|
||||
|
||||
def from_zip(ID: str, file: str) -> ZipDocument:
|
||||
def from_zip(_id: str, file: str) -> ZipDocument:
|
||||
"""Return A ZipDocument from a zipfile.
|
||||
|
||||
Create a ZipDocument instance from a zipfile.
|
||||
|
||||
Args:
|
||||
ID: The object ID this zipfile represents.
|
||||
_id: The object ID this zipfile represents.
|
||||
file: the filename of the zipfile.
|
||||
Returns:
|
||||
An instance of the supplied zipfile.
|
||||
"""
|
||||
|
||||
return ZipDocument(ID, file=file)
|
||||
return ZipDocument(_id, file=file)
|
||||
|
||||
|
||||
def from_request_stream(ID: str, stream: Response) -> ZipDocument:
|
||||
def from_request_stream(_id: str, stream: Response) -> ZipDocument:
|
||||
"""Return a ZipDocument from a request stream containing a zipfile.
|
||||
|
||||
This is used with the BlobGETUrl from a :class:`rmapi.document.Document`.
|
||||
This is used with the BlobGETUrl from a :class:`rmapy.document.Document`.
|
||||
|
||||
Args:
|
||||
ID: The object ID this zipfile represents.
|
||||
_id: The object ID this zipfile represents.
|
||||
stream: a stream containing the zipfile.
|
||||
Returns:
|
||||
the object of the downloaded zipfile.
|
||||
@@ -367,6 +364,6 @@ def from_request_stream(ID: str, stream: Response) -> ZipDocument:
|
||||
tmp = BytesIO()
|
||||
for chunk in stream.iter_content(chunk_size=8192):
|
||||
tmp.write(chunk)
|
||||
zd = ZipDocument(ID=ID)
|
||||
zd = ZipDocument(_id=_id)
|
||||
zd.load(tmp)
|
||||
return zd
|
||||
@@ -27,4 +27,3 @@ class ApiError(Exception):
|
||||
def __init__(self, msg, response=None):
|
||||
self.response = response
|
||||
super(ApiError, self).__init__(msg)
|
||||
|
||||
@@ -6,20 +6,21 @@ from zipfile import ZipFile, ZIP_DEFLATED
|
||||
from .const import RFC3339Nano
|
||||
from typing import Tuple, Optional
|
||||
|
||||
|
||||
class ZipFolder(object):
|
||||
"""A dummy zipfile to create a folder
|
||||
|
||||
This is needed to create a folder on the Remarkable Cloud
|
||||
"""
|
||||
|
||||
def __init__(self, ID: str):
|
||||
def __init__(self, _id: str):
|
||||
"""Creates a zipfile in memory
|
||||
|
||||
Args:
|
||||
ID: the ID to create a zipFolder for
|
||||
_id: the ID to create a zipFolder for
|
||||
"""
|
||||
super(ZipFolder, self).__init__()
|
||||
self.ID = ID
|
||||
self.ID = _id
|
||||
self.file = BytesIO()
|
||||
self.Version = 1
|
||||
with ZipFile(self.file, 'w', ZIP_DEFLATED) as zf:
|
||||
@@ -37,7 +38,7 @@ class Folder(Meta):
|
||||
|
||||
Args:
|
||||
name: An optional name for this folder. In the end, a name is
|
||||
really needed, but can be ommitted to set a later time.
|
||||
really needed, but can be omitted to set a later time.
|
||||
"""
|
||||
|
||||
super(Folder, self).__init__(**kwargs)
|
||||
@@ -48,9 +49,9 @@ class Folder(Meta):
|
||||
self.ID = str(uuid4())
|
||||
|
||||
def create_request(self) -> Tuple[BytesIO, dict]:
|
||||
"""Prepares the nessesary parameters to create this folder.
|
||||
"""Prepares the necessary parameters to create this folder.
|
||||
|
||||
This creates a ZipFolder & the nessesary json body to
|
||||
This creates a ZipFolder & the necessary json body to
|
||||
create an upload request.
|
||||
"""
|
||||
|
||||
@@ -61,9 +62,9 @@ class Folder(Meta):
|
||||
}
|
||||
|
||||
def update_request(self) -> dict:
|
||||
"""Perpares the nessesary parameters to update a folder.
|
||||
"""Prepares the necessary parameters to update a folder.
|
||||
|
||||
This sets some parameters in the datastructure to submit to the API.
|
||||
This sets some parameters in the data structure to submit to the API.
|
||||
"""
|
||||
|
||||
data = self.to_dict()
|
||||
@@ -72,10 +73,7 @@ class Folder(Meta):
|
||||
return data
|
||||
|
||||
def __str__(self):
|
||||
return f"<rmapi.folder.Folder {self.ID}>"
|
||||
return f"<rmapy.folder.Folder {self.ID}>"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ class Meta(object):
|
||||
Attributes:
|
||||
ID: Id of the meta object.
|
||||
Version: The version of this object.
|
||||
Success: If the last API Call was a succes.
|
||||
Success: If the last API Call was a success.
|
||||
BlobURLGet: The url to get the data blob from. Can be empty.
|
||||
BlobURLGetExpires: The expiration date of the Get url.
|
||||
BlobURLPut: The url to upload the data blob to. Can be empty.
|
||||
@@ -26,7 +26,7 @@ class Meta(object):
|
||||
ID = ""
|
||||
Version = 0
|
||||
Message = ""
|
||||
Succes = True
|
||||
Success = True
|
||||
BlobURLGet = ""
|
||||
BlobURLGetExpires = ""
|
||||
BlobURLPut = ""
|
||||
@@ -39,8 +39,8 @@ class Meta(object):
|
||||
Parent = ""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kkeys = self.to_dict().keys()
|
||||
for k in kkeys:
|
||||
k_keys = self.to_dict().keys()
|
||||
for k in k_keys:
|
||||
setattr(self, k, kwargs.get(k, getattr(self, k)))
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
@@ -56,7 +56,7 @@ class Meta(object):
|
||||
"ID": self.ID,
|
||||
"Version": self.Version,
|
||||
"Message": self.Message,
|
||||
"Succes": self.Succes,
|
||||
"Success": self.Success,
|
||||
"BlobURLGet": self.BlobURLGet,
|
||||
"BlobURLGetExpires": self.BlobURLGetExpires,
|
||||
"BlobURLPut": self.BlobURLPut,
|
||||
20
setup.py
20
setup.py
@@ -22,14 +22,14 @@ setup(
|
||||
# package, this name will be registered for you. It will determine how
|
||||
# users can install this project, e.g.:
|
||||
#
|
||||
# $ pip install sampleproject
|
||||
# $ pip install sampleprojectk
|
||||
#
|
||||
# And where it will live on PyPI: https://pypi.org/project/sampleproject/
|
||||
#
|
||||
# There are some restrictions on what makes a valid project name
|
||||
# specification here:
|
||||
# https://packaging.python.org/specifications/core-metadata/#name
|
||||
name='rmapi', # Required
|
||||
name='rmapy', # Required
|
||||
|
||||
# Versions should comply with PEP 440:
|
||||
# https://www.python.org/dev/peps/pep-0440/
|
||||
@@ -37,7 +37,7 @@ setup(
|
||||
# For a discussion on single-sourcing the version across setup.py and the
|
||||
# project code, see
|
||||
# https://packaging.python.org/en/latest/single_source_version.html
|
||||
version='0.2.0', # Required
|
||||
version='0.2.2', # Required
|
||||
|
||||
# This is a one-line description or tagline of what your project does. This
|
||||
# corresponds to the "Summary" metadata field:
|
||||
@@ -70,7 +70,7 @@ setup(
|
||||
#
|
||||
# This field corresponds to the "Home-Page" metadata field:
|
||||
# https://packaging.python.org/specifications/core-metadata/#home-page-optional
|
||||
url='https://github.com/subutux/rmapi', # Optional
|
||||
url='https://github.com/subutux/rmapy', # Optional
|
||||
|
||||
# This should be your name or the name of the organization which owns the
|
||||
# project.
|
||||
@@ -88,8 +88,8 @@ setup(
|
||||
# 3 - Alpha
|
||||
# 4 - Beta
|
||||
# 5 - Production/Stable
|
||||
'Development Status :: 3 - Alpha',
|
||||
#'Development Status :: 4 - Beta',
|
||||
#'Development Status :: 3 - Alpha',
|
||||
'Development Status :: 4 - Beta',
|
||||
#'Development Status :: 5 - Production/Stable'
|
||||
#'Development Status :: 6 - Mature'
|
||||
# Indicate who your project is intended for
|
||||
@@ -110,7 +110,7 @@ setup(
|
||||
# project page. What does your project relate to?
|
||||
#
|
||||
# Note that this is a string of words separated by whitespace, not a list.
|
||||
keywords='remarkable rmapi cloud paper tablet', # Optional
|
||||
keywords='remarkable rmapy cloud paper tablet', # Optional
|
||||
|
||||
# You can just specify package directories manually here if your project is
|
||||
# simple. Or you can use find_packages().
|
||||
@@ -180,7 +180,7 @@ setup(
|
||||
#
|
||||
# For example, the following would provide a command called `sample` which
|
||||
# executes the function `main` from this package when invoked:
|
||||
# TODO
|
||||
# TODO Create a command line tool
|
||||
# entry_points={ # Optional
|
||||
# 'console_scripts': [
|
||||
# 'sample=sample:main',
|
||||
@@ -197,9 +197,9 @@ setup(
|
||||
# maintainers, and where to support the project financially. The key is
|
||||
# what's used to render the link text on PyPI.
|
||||
project_urls={ # Optional
|
||||
'Bug Reports': 'https://github.com/subutux/rmapi/issues',
|
||||
'Bug Reports': 'https://github.com/subutux/rmapy/issues',
|
||||
'Funding': 'https://donate.pypi.org',
|
||||
'Say Thanks!': 'https://www.paypal.me/subutux',
|
||||
'Source': 'https://github.com/subutux/rmapi/',
|
||||
'Source': 'https://github.com/subutux/rmapy/',
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user