Fixed typechecking with mypy

This commit is contained in:
Stijn Van Campenhout
2019-09-20 15:28:26 +02:00
parent a748bf9dd5
commit 5d27b5b3c4
6 changed files with 106 additions and 129 deletions

View File

@@ -2,13 +2,17 @@ import requests
from logging import getLogger from logging import getLogger
from datetime import datetime from datetime import datetime
import json import json
from typing import TypeVar from typing import Union, Optional
from uuid import uuid4 from uuid import uuid4
from .collections import Collection from .collections import Collection
from .config import load, dump from .config import load, dump
from .document import Document, ZipDocument from .document import Document, ZipDocument, from_request_stream
from .folder import Folder from .folder import Folder
from .exceptions import AuthError, DocumentNotFound, ApiError from .exceptions import (
AuthError,
DocumentNotFound,
ApiError,
UnsupportedTypeError,)
from .const import (RFC3339Nano, from .const import (RFC3339Nano,
USER_AGENT, USER_AGENT,
BASE_URL, BASE_URL,
@@ -18,7 +22,7 @@ from .const import (RFC3339Nano,
log = getLogger("rmapipy.rmapi") log = getLogger("rmapipy.rmapi")
DocOrFolder = TypeVar('DocumentOrFolder', Document, Folder) DocumentOrFolder = Union[Document, Folder]
class Client(object): class Client(object):
@@ -33,8 +37,8 @@ class Client(object):
""" """
token_set = { token_set = {
"devicetoken": None, "devicetoken": "",
"usertoken": None "usertoken": ""
} }
def __init__(self): def __init__(self):
@@ -83,17 +87,19 @@ class Client(object):
for k in headers.keys(): for k in headers.keys():
_headers[k] = headers[k] _headers[k] = headers[k]
log.debug(url, _headers) log.debug(url, _headers)
print(method, url, json.dumps(body)) if method == "PUT":
print(method, url, json.dumps(body))
r = requests.request(method, url, r = requests.request(method, url,
json=body, json=body,
data=data, data=data,
headers=_headers, headers=_headers,
params=params, params=params,
stream=stream) stream=stream)
print(r.status_code, r.text) if method == "PUT":
print(r.status_code, r.text)
return r return r
def register_device(self, code: str) -> True: def register_device(self, code: str):
"""Registers a device to on the Remarkable Cloud. """Registers a device to on the Remarkable Cloud.
This uses a unique code the user gets from This uses a unique code the user gets from
@@ -125,7 +131,7 @@ class Client(object):
else: else:
raise AuthError("Can't register device") raise AuthError("Can't register device")
def renew_token(self) -> True: def renew_token(self):
"""Fetches a new user_token. """Fetches a new user_token.
This is the second step of the authentication of the Remarkable Cloud. This is the second step of the authentication of the Remarkable Cloud.
@@ -184,7 +190,7 @@ class Client(object):
return collection return collection
def get_doc(self, ID: str) -> DocOrFolder: def get_doc(self, ID: str) -> Optional[DocumentOrFolder]:
"""Get a meta item by ID """Get a meta item by ID
Fetch a meta item from the Remarkable Cloud by ID. Fetch a meta item from the Remarkable Cloud by ID.
@@ -215,6 +221,7 @@ class Client(object):
return Document(**data_response[0]) return Document(**data_response[0])
else: else:
raise DocumentNotFound(f"Cound not find document {ID}") raise DocumentNotFound(f"Cound not find document {ID}")
return None
def download(self, document: Document) -> ZipDocument: def download(self, document: Document) -> ZipDocument:
"""Download a ZipDocument """Download a ZipDocument
@@ -232,13 +239,18 @@ class Client(object):
""" """
if not document.BlobURLGet: if not document.BlobURLGet:
document = self.get_doc(document.ID) doc = self.get_doc(document.ID)
if isinstance(doc, Document):
document = doc
else:
raise UnsupportedTypeError(
"We expected a document, got {type}"
.format(type=type(doc)))
log.debug("BLOB", document.BlobURLGet) log.debug("BLOB", document.BlobURLGet)
r = self.request("GET", document.BlobURLGet, stream=True) r = self.request("GET", document.BlobURLGet, stream=True)
return ZipDocument.from_request_stream(document.ID, r) return from_request_stream(document.ID, r)
def upload(self, zipDoc: ZipDocument, document: Document) -> True: def upload(self, zipDoc: ZipDocument, document: Document):
"""Upload a document to the cloud. """Upload a document to the cloud.
Add a new document to the Remarkable Cloud. Add a new document to the Remarkable Cloud.
@@ -253,7 +265,7 @@ class Client(object):
return True return True
def update_metadata(self, docorfolder: DocOrFolder) -> True: def update_metadata(self, docorfolder: DocumentOrFolder):
"""Send an update of the current metadata of a meta object """Send an update of the current metadata of a meta object
Update the meta item. Update the meta item.
@@ -272,7 +284,7 @@ class Client(object):
return self.check_reponse(res) return self.check_reponse(res)
def get_current_version(self, docorfolder: DocOrFolder) -> int: def get_current_version(self, docorfolder: DocumentOrFolder) -> int:
"""Get the latest version info from a Document or Folder """Get the latest version info from a Document or Folder
This fetches the latest meta information from the Remarkable Cloud This fetches the latest meta information from the Remarkable Cloud
@@ -295,7 +307,7 @@ class Client(object):
return 0 return 0
return int(d.Version) return int(d.Version)
def create_folder(self, folder: Folder) -> True: def create_folder(self, folder: Folder):
"""Create a new folder meta object. """Create a new folder meta object.
This needs to be done in 3 steps: This needs to be done in 3 steps:
@@ -330,7 +342,7 @@ class Client(object):
self.update_metadata(folder) self.update_metadata(folder)
return True return True
def check_reponse(self, response: requests.Response) -> True: def check_reponse(self, response: requests.Response):
"""Check the response from an API Call """Check the response from an API Call
Does some sanity checking on the Response Does some sanity checking on the Response

View File

@@ -1,9 +1,9 @@
from .document import Document from .document import Document
from .folder import Folder from .folder import Folder
from typing import NoReturn, TypeVar, List from typing import NoReturn, List, Union
from .exceptions import FolderNotFound from .exceptions import FolderNotFound
DocOrFolder = TypeVar('DocumentOrFolder', Document, Folder) DocumentOrFolder = Union[Document, Folder]
class Collection(object): class Collection(object):
@@ -15,13 +15,13 @@ class Collection(object):
items: A list containing the items. items: A list containing the items.
""" """
items = [] items: List[DocumentOrFolder] = []
def __init__(self, *items): def __init__(self, *items):
for i in items: for i in items:
self.items.append(i) self.items.append(i)
def add(self, docdict: dict) -> NoReturn: def add(self, docdict: dict) -> None:
"""Add an item to the collection. """Add an item to the collection.
It wraps it in the correct class based on the Type parameter of the It wraps it in the correct class based on the Type parameter of the
dict. dict.
@@ -31,14 +31,14 @@ class Collection(object):
""" """
if docdict.get("Type", None) == "DocumentType": if docdict.get("Type", None) == "DocumentType":
return self.add_document(docdict) self.add_document(docdict)
elif docdict.get("Type", None) == "CollectionType": elif docdict.get("Type", None) == "CollectionType":
return self.add_folder(docdict) self.add_folder(docdict)
else: else:
raise TypeError("Unsupported type: {_type}" raise TypeError("Unsupported type: {_type}"
.format(_type=docdict.get("Type", None))) .format(_type=docdict.get("Type", None)))
def add_document(self, docdict: dict) -> NoReturn: def add_document(self, docdict: dict) -> None:
"""Add a document to the collection """Add a document to the collection
Args: Args:
@@ -47,7 +47,7 @@ class Collection(object):
self.items.append(Document(**docdict)) self.items.append(Document(**docdict))
def add_folder(self, dirdict: dict) -> NoReturn: def add_folder(self, dirdict: dict) -> None:
"""Add a document to the collection """Add a document to the collection
Args: Args:
@@ -56,7 +56,7 @@ class Collection(object):
self.items.append(Folder(**dirdict)) self.items.append(Folder(**dirdict))
def parent(self, docorfolder: DocOrFolder) -> Folder: def parent(self, docorfolder: DocumentOrFolder) -> Folder:
"""Returns the paren of a Document or Folder """Returns the paren of a Document or Folder
Args: Args:
@@ -67,12 +67,12 @@ class Collection(object):
""" """
results = [i for i in self.items if i.ID == docorfolder.ID] results = [i for i in self.items if i.ID == docorfolder.ID]
if len(results) > 0: if len(results) > 0 and isinstance(results[0], Folder):
return results[0] return results[0]
else: else:
raise FolderNotFound("Could not found the parent of the document.") raise FolderNotFound("Could not found the parent of the document.")
def children(self, folder: Folder = None) -> List[DocOrFolder]: def children(self, folder: Folder = None) -> List[DocumentOrFolder]:
"""Get all the childern from a folder """Get all the childern from a folder
Args: Args:
@@ -90,5 +90,5 @@ class Collection(object):
def __len__(self) -> int: def __len__(self) -> int:
return len(self.items) return len(self.items)
def __getitem__(self, position: int) -> DocOrFolder: def __getitem__(self, position: int) -> DocumentOrFolder:
return self.items[position] return self.items[position]

View File

@@ -1,21 +1,23 @@
from pathlib import Path from pathlib import Path
from yaml import BaseLoader
from yaml import load as yml_load from yaml import load as yml_load
from yaml import dump as yml_dump from yaml import dump as yml_dump
from typing import Dict
def load() -> dict: def load() -> dict:
"""Load the .rmapi config file""" """Load the .rmapi config file"""
config_file_path = Path.joinpath(Path.home(), ".rmapi") config_file_path = Path.joinpath(Path.home(), ".rmapi")
config = {} config: Dict[str, str] = {}
if Path.exists(config_file_path): if Path.exists(config_file_path):
with open(config_file_path, 'r') as config_file: with open(config_file_path, 'r') as config_file:
config = dict(yml_load(config_file.read())) config = dict(yml_load(config_file.read(), Loader=BaseLoader))
return config return config
def dump(config: dict) -> True: def dump(config: dict) -> None:
"""Dump config to the .rmapi config file """Dump config to the .rmapi config file
Args: Args:
@@ -28,6 +30,4 @@ def dump(config: dict) -> True:
with open(config_file_path, 'w') as config_file: with open(config_file_path, 'w') as config_file:
config_file.write(yml_dump(config)) config_file.write(yml_dump(config))
return True

View File

@@ -3,13 +3,44 @@ from zipfile import ZipFile, ZIP_DEFLATED
import shutil import shutil
from uuid import uuid4 from uuid import uuid4
import json import json
from typing import NoReturn, TypeVar from typing import NoReturn, TypeVar, List
from requests import Response from requests import Response
from .meta import Meta
BytesOrString = TypeVar("BytesOrString", BytesIO, str) BytesOrString = TypeVar("BytesOrString", BytesIO, str)
class Document(object): class RmPage(object):
"""A Remarkable Page
Contains the metadata, the page itself & thumbnail.
"""
def __init__(self, page, metadata=None, order=0, thumbnail=None, ID=None):
self.page = page
if metadata:
self.metadata = metadata
else:
self.metadata = {"layers": [{"name": "Layer 1"}]}
self.order = order
if thumbnail:
self.thumbnail = thumbnail
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}>"
def __repr__(self) -> str:
"""String representation of this object"""
return self.__str__()
class Document(Meta):
""" Document represents a real object expected in most """ Document represents a real object expected in most
calls by the remarkable API calls by the remarkable API
@@ -34,51 +65,9 @@ class Document(object):
""" """
ID = ""
Version = 0
Message = ""
Succes = True
BlobURLGet = ""
BlobURLGetExpires = ""
BlobURLPut = ""
BlobURLPutExpires = ""
ModifiedClient = ""
Type = "DocumentType"
VissibleName = ""
CurrentPage = 1
Bookmarked = False
Parent = ""
def __init__(self, **kwargs): def __init__(self, **kwargs):
kkeys = self.to_dict().keys() super(Document, self).__init__(**kwargs)
for k in kkeys: self.Type = "DocumentType"
setattr(self, k, kwargs.get(k, getattr(self, k)))
def to_dict(self) -> dict:
"""Return a dict representation of this object.
Used for API Calls.
Returns
a dict of the current object.
"""
return {
"ID": self.ID,
"Version": self.Version,
"Message": self.Message,
"Succes": self.Succes,
"BlobURLGet": self.BlobURLGet,
"BlobURLGetExpires": self.BlobURLGetExpires,
"BlobURLPut": self.BlobURLPut,
"BlobURLPutExpires": self.BlobURLPutExpires,
"ModifiedClient": self.ModifiedClient,
"Type": self.Type,
"VissibleName": self.VissibleName,
"CurrentPage": self.CurrentPage,
"Bookmarked": self.Bookmarked,
"Parent": self.Parent
}
def __str__(self): def __str__(self):
"""String representation of this object""" """String representation of this object"""
@@ -177,7 +166,7 @@ class ZipDocument(object):
zipfile = BytesIO() zipfile = BytesIO()
pdf = None pdf = None
epub = None epub = None
rm = [] rm: List[RmPage] = []
ID = None ID = None
def __init__(self, ID=None, doc=None, file=None): def __init__(self, ID=None, doc=None, file=None):
@@ -220,7 +209,7 @@ class ZipDocument(object):
"""string representation of this class""" """string representation of this class"""
return self.__str__() return self.__str__()
def dump(self, file: str) -> NoReturn: def dump(self, file: str) -> None:
"""Dump the contents of ZipDocument back to a zip file. """Dump the contents of ZipDocument back to a zip file.
This builds a zipfile to upload back to the Remarkable Cloud. This builds a zipfile to upload back to the Remarkable Cloud.
@@ -236,7 +225,7 @@ class ZipDocument(object):
json.dumps(self.content)) json.dumps(self.content))
if self.pagedata: if self.pagedata:
zf.writestr(f"{self.ID}.pagedata", zf.writestr(f"{self.ID}.pagedata",
self.pagedata.read()) self.pagedata)
if self.pdf: if self.pdf:
zf.writestr(f"{self.ID}.pdf", zf.writestr(f"{self.ID}.pdf",
@@ -257,7 +246,7 @@ class ZipDocument(object):
zf.writestr(f"{self.ID}.thumbnails/{page.order}.jpg", zf.writestr(f"{self.ID}.thumbnails/{page.order}.jpg",
page.thumbnail.read()) page.thumbnail.read())
def load(self, file: BytesOrString) -> NoReturn: def load(self, file: BytesOrString) -> None:
"""Load a zipfile into this class. """Load a zipfile into this class.
Extracts the zipfile and reads in the contents. Extracts the zipfile and reads in the contents.
@@ -287,29 +276,29 @@ class ZipDocument(object):
pass pass
try: try:
with zf.open(f"{self.ID}.pagedata", 'r') as pagedata: with zf.open(f"{self.ID}.pagedata", 'r') as pagedata:
self.pagedata = BytesIO(pagedata.read()) self.pagedata = str(pagedata.read())
except KeyError: except KeyError:
pass pass
try: try:
with zf.open(f"{self.ID}.pdf", 'r') as pdf: with zf.open(f"{self.ID}.pdf", 'rb') as pdf:
self.pdf = BytesIO(pdf.read()) self.pdf = BytesIO(pdf.read())
except KeyError: except KeyError:
pass pass
try: try:
with zf.open(f"{self.ID}.epub", 'r') as epub: with zf.open(f"{self.ID}.epub", 'rb') as epub:
self.epub = BytesIO(epub.read()) self.epub = BytesIO(epub.read())
except KeyError: except KeyError:
pass pass
# Get the RM pages # Get the RM pages
content = [x for x in zf.namelist() pages = [x for x in zf.namelist()
if x.startswith(f"{self.ID}/") and x.endswith('.rm')] if x.startswith(f"{self.ID}/") and x.endswith('.rm')]
for p in content: for p in pages:
pagenumber = p.replace(f"{self.ID}/", "").replace(".rm", "") pagenumber = int(p.replace(f"{self.ID}/", "")
pagenumber = int(pagenumber) .replace(".rm", ""))
page = BytesIO() page = BytesIO()
thumbnail = BytesIO() thumbnail = BytesIO()
with zf.open(p, 'r') as rm: with zf.open(p, 'r') as rm:
@@ -329,36 +318,6 @@ class ZipDocument(object):
self.zipfile.seek(0) self.zipfile.seek(0)
class RmPage(object):
"""A Remarkable Page
Contains the metadata, the page itself & thumbnail.
"""
def __init__(self, page, metadata=None, order=0, thumbnail=None, ID=None):
self.page = page
if metadata:
self.metadata = metadata
else:
self.metadata = {"layers": [{"name": "Layer 1"}]}
self.order = order
if thumbnail:
self.thumbnail = thumbnail
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}>"
def __repr__(self) -> str:
"""String representation of this object"""
return self.__str__()
def from_zip(ID: str, file: str) -> ZipDocument: def from_zip(ID: str, file: str) -> ZipDocument:
"""Return A ZipDocument from a zipfile. """Return A ZipDocument from a zipfile.

View File

@@ -10,6 +10,12 @@ class DocumentNotFound(Exception):
super(DocumentNotFound, self).__init__(msg) super(DocumentNotFound, self).__init__(msg)
class UnsupportedTypeError(Exception):
"""Not the expected type"""
def __init__(self, msg):
super(UnsupportedTypeError, self).__init__(msg)
class FolderNotFound(Exception): class FolderNotFound(Exception):
"""Could not found a requested folder""" """Could not found a requested folder"""
def __init__(self, msg): def __init__(self, msg):

View File

@@ -1,10 +1,10 @@
from .document import Document from .meta import Meta
from datetime import datetime from datetime import datetime
from uuid import uuid4 from uuid import uuid4
from io import BytesIO from io import BytesIO
from zipfile import ZipFile, ZIP_DEFLATED from zipfile import ZipFile, ZIP_DEFLATED
from .const import RFC3339Nano from .const import RFC3339Nano
from typing import Tuple, Optional
class ZipFolder(object): class ZipFolder(object):
"""A dummy zipfile to create a folder """A dummy zipfile to create a folder
@@ -27,12 +27,12 @@ class ZipFolder(object):
self.file.seek(0) self.file.seek(0)
class Folder(Document): class Folder(Meta):
""" """
A Meta type of object used to represent a folder. A Meta type of object used to represent a folder.
""" """
def __init__(self, name=None, **kwargs): def __init__(self, name: Optional[str] = None, **kwargs) -> None:
"""Create a Folder instance """Create a Folder instance
Args: Args:
@@ -47,7 +47,7 @@ class Folder(Document):
if not self.ID: if not self.ID:
self.ID = str(uuid4()) self.ID = str(uuid4())
def create_request(self) -> (ZipFolder, dict): def create_request(self) -> Tuple[BytesIO, dict]:
"""Prepares the nessesary parameters to create this folder. """Prepares the nessesary parameters to create this folder.
This creates a ZipFolder & the nessesary json body to This creates a ZipFolder & the nessesary json body to